From baf7178dc880d16c6f84a8a298b648debbdc448b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Thu, 5 Oct 2023 17:11:24 +0100 Subject: [PATCH] nym-api caching node self described information --- Cargo.toml | 1 + .../validator-client/src/client.rs | 6 +- common/http-api-client/Cargo.toml | 1 + common/http-api-client/src/lib.rs | 63 +++++-- common/network-defaults/Cargo.toml | 2 +- common/socks5-client-core/Cargo.toml | 2 +- explorer-api/Cargo.toml | 2 +- explorer-api/explorer-api-requests/Cargo.toml | 2 +- nym-api/Cargo.toml | 10 +- nym-api/nym-api-requests/Cargo.toml | 5 +- nym-api/nym-api-requests/src/models.rs | 31 ++++ nym-api/src/main.rs | 17 ++ nym-api/src/node_describe_cache/mod.rs | 158 ++++++++++++++++++ .../src/node_status_api/cache/node_sets.rs | 1 + nym-api/src/node_status_api/mod.rs | 13 +- nym-api/src/nym_nodes/mod.rs | 16 ++ nym-api/src/nym_nodes/routes.rs | 44 +++++ nym-api/src/support/config/mod.rs | 12 ++ .../src/support/config/old_config_v1_1_21.rs | 1 + nym-api/src/support/http/mod.rs | 6 +- nym-connect/desktop/Cargo.lock | 14 ++ nym-node/nym-node-requests/Cargo.toml | 1 + nym-node/nym-node-requests/src/api/mod.rs | 4 +- .../src/api/v1/gateway/models.rs | 9 +- .../src/api/v1/mixnode/models.rs | 3 +- .../src/api/v1/network_requester/models.rs | 3 +- .../src/api/v1/node/models.rs | 7 +- 27 files changed, 388 insertions(+), 46 deletions(-) create mode 100644 nym-api/src/node_describe_cache/mod.rs create mode 100644 nym-api/src/nym_nodes/mod.rs create mode 100644 nym-api/src/nym_nodes/routes.rs diff --git a/Cargo.toml b/Cargo.toml index b4499e56a3..f3ce3e1322 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -162,6 +162,7 @@ once_cell = "1.7.2" parking_lot = "0.12.1" rand = "0.8.5" reqwest = "0.11.18" +schemars = "0.8.1" serde = "1.0.152" serde_json = "1.0.91" tap = "1.0.1" diff --git a/common/client-libs/validator-client/src/client.rs b/common/client-libs/validator-client/src/client.rs index b6be937bcc..a3d827aa16 100644 --- a/common/client-libs/validator-client/src/client.rs +++ b/common/client-libs/validator-client/src/client.rs @@ -148,7 +148,7 @@ impl Client { impl Client { pub fn new_with_rpc_client(config: Config, rpc_client: C) -> Self { - let nym_api_client = nym_api::Client::new(config.api_url.clone()); + let nym_api_client = nym_api::Client::new(config.api_url.clone(), None); Client { nym_api: nym_api_client, @@ -162,7 +162,7 @@ impl Client { where S: OfflineSigner, { - let nym_api_client = nym_api::Client::new(config.api_url.clone()); + let nym_api_client = nym_api::Client::new(config.api_url.clone(), None); Client { nym_api: nym_api_client, @@ -242,7 +242,7 @@ pub struct NymApiClient { impl NymApiClient { pub fn new(api_url: Url) -> Self { - let nym_api = nym_api::Client::new(api_url); + let nym_api = nym_api::Client::new(api_url, None); NymApiClient { nym_api } } diff --git a/common/http-api-client/Cargo.toml b/common/http-api-client/Cargo.toml index 25c65608bf..7b784149a5 100644 --- a/common/http-api-client/Cargo.toml +++ b/common/http-api-client/Cargo.toml @@ -17,3 +17,4 @@ url = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } thiserror = { workspace = true } +tracing = { workspace = true } \ No newline at end of file diff --git a/common/http-api-client/src/lib.rs b/common/http-api-client/src/lib.rs index f631af2be7..7ebefd4ba0 100644 --- a/common/http-api-client/src/lib.rs +++ b/common/http-api-client/src/lib.rs @@ -6,9 +6,13 @@ use reqwest::{IntoUrl, Response, StatusCode}; use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; use std::fmt::Display; +use std::time::Duration; use thiserror::Error; +use tracing::warn; use url::Url; +pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(3); + pub type PathSegments<'a> = &'a [&'a str]; pub type Params<'a, K, V> = &'a [(K, V)]; @@ -28,14 +32,20 @@ pub enum HttpClientError { source: url::ParseError, }, - #[error("not found")] + #[error("the requested resource could not be found")] NotFound, #[error("request failed with error message: {0}")] GenericRequestFailure(String), - #[error("failed to resolve request. status code: {status}, additional error message: {error}")] - EndpointFailure { status: u16, error: E }, + #[error("the request failed with status '{status}'. no additional error message provided")] + RequestFailure { status: StatusCode }, + + #[error("the returned response was empty. status: '{status}'")] + EmptyResponse { status: StatusCode }, + + #[error("failed to resolve request. status: '{status}', additional error message: {error}")] + EndpointFailure { status: StatusCode, error: E }, } /// A simple extendable client wrapper for http request with extra url sanitization. @@ -46,19 +56,35 @@ pub struct Client { } impl Client { - pub fn new(base_url: Url) -> Self { + pub fn new(base_url: Url, timeout: Option) -> Self { Client { base_url, - reqwest_client: reqwest::Client::new(), + + // TODO: we should probably be propagating the error rather than panicking, + // but that'd break bunch of things due to type changes + reqwest_client: reqwest::ClientBuilder::new() + .timeout(timeout.unwrap_or(DEFAULT_TIMEOUT)) + .build() + .expect("Client::new()"), } } - pub fn new_url(url: U) -> Result> + pub fn new_url(url: U, timeout: Option) -> Result> where U: IntoUrl, E: Display, { - Ok(Self::new(url.into_url()?)) + // a naive check: if the provided URL does not start with http(s), add that scheme + let str_url = url.as_str(); + + if !str_url.starts_with("http") { + let alt = format!("http://{str_url}"); + warn!("the provided url ('{str_url}') does not contain scheme information. Changing it to '{alt}' ..."); + // TODO: or should we maybe default to https? + Self::new_url(alt, timeout) + } else { + Ok(Self::new(url.into_url()?, timeout)) + } } pub fn change_base_url(&mut self, new_url: Url) { @@ -111,7 +137,7 @@ impl Client { E: Display + DeserializeOwned, { let res = self.send_get_request(path, params).await?; - parse_response(res).await + parse_response(res, false).await } pub async fn post_json( @@ -128,7 +154,7 @@ impl Client { E: Display + DeserializeOwned, { let res = self.send_post_request(path, params, json_body).await?; - parse_response(res).await + parse_response(res, true).await } pub async fn get_json_endpoint(&self, endpoint: S) -> Result> @@ -142,7 +168,7 @@ impl Client { .get(self.base_url.join(endpoint.as_ref())?) .send() .await?; - parse_response(res).await + parse_response(res, false).await } pub async fn post_json_endpoint( @@ -162,7 +188,7 @@ impl Client { .json(json_body) .send() .await?; - parse_response(res).await + parse_response(res, true).await } } @@ -299,22 +325,31 @@ pub fn sanitize_url, V: AsRef>( url } -async fn parse_response(res: Response) -> Result> +async fn parse_response(res: Response, allow_empty: bool) -> Result> where T: DeserializeOwned, E: DeserializeOwned + Display, { let status = res.status(); + if !allow_empty { + if let Some(0) = res.content_length() { + return Err(HttpClientError::EmptyResponse { status }); + } + } + if res.status().is_success() { Ok(res.json().await?) } else if res.status() == StatusCode::NOT_FOUND { Err(HttpClientError::NotFound) } else { - let plaintext = res.text().await?; + let Ok(plaintext) = res.text().await else { + return Err(HttpClientError::RequestFailure { status }); + }; + if let Ok(request_error) = serde_json::from_str(&plaintext) { Err(HttpClientError::EndpointFailure { - status: status.as_u16(), + status, error: request_error, }) } else { diff --git a/common/network-defaults/Cargo.toml b/common/network-defaults/Cargo.toml index bc5bddb497..5d5e071343 100644 --- a/common/network-defaults/Cargo.toml +++ b/common/network-defaults/Cargo.toml @@ -12,7 +12,7 @@ cfg-if = { workspace = true } dotenvy = { workspace = true } hex-literal = "0.3.3" once_cell = { workspace = true } -schemars = { version = "0.8", features = ["preserve_order"] } +schemars = { workspace = true, features = ["preserve_order"] } serde = { workspace = true, features = ["derive"]} thiserror = { workspace = true } url = { workspace = true } diff --git a/common/socks5-client-core/Cargo.toml b/common/socks5-client-core/Cargo.toml index cc6fcd52dc..a02684958f 100644 --- a/common/socks5-client-core/Cargo.toml +++ b/common/socks5-client-core/Cargo.toml @@ -13,7 +13,7 @@ log = { workspace = true } pin-project = "1.0" rand = { version = "0.7.3", features = ["wasm-bindgen"] } reqwest = { workspace = true } -schemars = { version = "0.8", features = ["preserve_order"] } +schemars = { workspace = true, features = ["preserve_order"] } serde = { workspace = true, features = ["derive"] } # for config serialization/deserialization tap = "1.0.1" thiserror = { workspace = true } diff --git a/explorer-api/Cargo.toml b/explorer-api/Cargo.toml index 7517f02d06..935a76cb52 100644 --- a/explorer-api/Cargo.toml +++ b/explorer-api/Cargo.toml @@ -24,7 +24,7 @@ reqwest = { workspace = true } rocket = { version = "0.5.0-rc.2", features = ["json"] } rocket_cors = { git="https://github.com/lawliet89/rocket_cors", rev="dfd3662c49e2f6fc37df35091cb94d82f7fb5915" } rocket_okapi = { version = "0.8.0-rc.2", features = ["swagger"] } -schemars = { version = "0.8", features = ["preserve_order"] } +schemars = { workspace = true, features = ["preserve_order"] } serde = { workspace = true } serde_json = { workspace = true } thiserror = { workspace = true } diff --git a/explorer-api/explorer-api-requests/Cargo.toml b/explorer-api/explorer-api-requests/Cargo.toml index d7139b9f78..825af93a91 100644 --- a/explorer-api/explorer-api-requests/Cargo.toml +++ b/explorer-api/explorer-api-requests/Cargo.toml @@ -7,6 +7,6 @@ edition = "2021" nym-contracts-common = { path = "../../common/cosmwasm-smart-contracts/contracts-common" } nym-mixnet-contract-common = { path = "../../common/cosmwasm-smart-contracts/mixnet-contract" } nym-api-requests = { path = "../../nym-api/nym-api-requests" } -schemars = { version = "0.8", features = ["preserve_order"] } +schemars = { workspace = true, features = ["preserve_order"] } serde = { version = "1.0", features = ["derive"] } ts-rs = { workspace = true, optional = true } diff --git a/nym-api/Cargo.toml b/nym-api/Cargo.toml index 4389d27de8..4691bf250f 100644 --- a/nym-api/Cargo.toml +++ b/nym-api/Cargo.toml @@ -16,7 +16,7 @@ rust-version = "1.56" [dependencies] async-trait = { workspace = true } -bs58 = {version = "0.4.0" } +bs58 = { version = "0.4.0" } bip39 = { workspace = true } cfg-if = "1.0" clap = { version = "4.0", features = ["cargo", "derive"] } @@ -27,7 +27,6 @@ humantime-serde = "1.0" lazy_static = "1.4.0" log = { workspace = true } pin-project = "1.0" -pretty_env_logger = "0.4.0" rand = "0.8.5" rand-07 = { package = "rand", version = "0.7.3" } # required for compatibility reqwest = { workspace = true, features = ["json"] } @@ -49,7 +48,7 @@ url = { workspace = true } ts-rs = { workspace = true, optional = true} -anyhow = "1.0" +anyhow = { workspace = true } getset = "0.1.1" sqlx = { version = "0.6.2", features = [ @@ -61,7 +60,7 @@ sqlx = { version = "0.6.2", features = [ okapi = { version = "0.7.0-rc.1", features = ["impl_json_schema"] } rocket_okapi = { version = "0.8.0-rc.2", features = ["swagger"] } -schemars = { version = "0.8", features = ["preserve_order"] } +schemars = { workspace = true, features = ["preserve_order"] } zeroize = { workspace = true } ## ephemera-specific @@ -106,13 +105,14 @@ nym-api-requests = { path = "nym-api-requests" } nym-validator-client = { path = "../common/client-libs/validator-client" } nym-bin-common = { path = "../common/bin-common" } nym-node-tester-utils = { path = "../common/node-tester-utils" } +nym-node-requests = { path = "../nym-node/nym-node-requests" } [features] no-reward = [] generate-ts = ["ts-rs"] [build-dependencies] -tokio = { version = "1.24.1", features = ["rt-multi-thread", "macros"] } +tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } sqlx = { version = "0.6.2", features = [ "runtime-tokio-rustls", "sqlite", diff --git a/nym-api/nym-api-requests/Cargo.toml b/nym-api/nym-api-requests/Cargo.toml index d98fe9015d..d9869bd7ae 100644 --- a/nym-api/nym-api-requests/Cargo.toml +++ b/nym-api/nym-api-requests/Cargo.toml @@ -10,13 +10,14 @@ bs58 = "0.4.0" cosmrs = { workspace = true } cosmwasm-std = { workspace = true } getset = "0.1.1" -schemars = { version = "0.8", features = ["preserve_order"] } +schemars = { workspace = true, features = ["preserve_order"] } serde = { workspace = true, features = ["derive"] } ts-rs = { workspace = true, optional = true } nym-coconut-interface = { path = "../../common/coconut-interface" } nym-mixnet-contract-common = { path= "../../common/cosmwasm-smart-contracts/mixnet-contract" } +nym-node-requests = { path = "../../nym-node/nym-node-requests", default-features = false } [features] default = [] -generate-ts = ["ts-rs"] +generate-ts = ["ts-rs", "nym-mixnet-contract-common/generate-ts"] diff --git a/nym-api/nym-api-requests/src/models.rs b/nym-api/nym-api-requests/src/models.rs index 86866f7da6..0ee2160623 100644 --- a/nym-api/nym-api-requests/src/models.rs +++ b/nym-api/nym-api-requests/src/models.rs @@ -9,6 +9,7 @@ use nym_mixnet_contract_common::rewarding::RewardEstimate; use nym_mixnet_contract_common::{ GatewayBond, IdentityKey, Interval, MixId, MixNode, Percent, RewardedSetNodeStatus, }; +use nym_node_requests::api::v1::gateway::models::WebSockets; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::fmt::{Display, Formatter}; @@ -142,6 +143,10 @@ impl MixNodeBondAnnotated { #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] pub struct GatewayBondAnnotated { pub gateway_bond: GatewayBond, + + #[serde(default)] + pub self_described: Option, + // NOTE: the performance field is deprecated in favour of node_performance pub performance: Performance, pub node_performance: NodePerformance, @@ -158,6 +163,11 @@ impl GatewayBondAnnotated { } } +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] +pub struct GatewayDescription { + // for now only expose what we need. this struct will evolve in the future (or be incorporated into nym-node properly) +} + #[derive(Debug, Serialize, Deserialize, JsonSchema)] pub struct ComputeRewardEstParam { pub performance: Option, @@ -344,3 +354,24 @@ pub struct CirculatingSupplyResponse { pub vesting_tokens: Coin, pub circulating_supply: Coin, } + +#[derive(Clone, Serialize, Deserialize, schemars::JsonSchema)] +pub struct NymNodeDescription { + // for now we only care about their ws/wss situation, nothing more + pub mixnet_websockets: WebSockets, +} + +#[derive(Clone, Serialize, Deserialize, schemars::JsonSchema)] +pub struct DescribedGateway { + pub bond: GatewayBond, + pub self_described: Option, +} + +impl From for DescribedGateway { + fn from(bond: GatewayBond) -> Self { + DescribedGateway { + bond, + self_described: None, + } + } +} diff --git a/nym-api/src/main.rs b/nym-api/src/main.rs index a259879ace..1d16067f75 100644 --- a/nym-api/src/main.rs +++ b/nym-api/src/main.rs @@ -6,7 +6,9 @@ extern crate rocket; use crate::epoch_operations::RewardedSetUpdater; use crate::network::models::NetworkDetails; +use crate::node_describe_cache::DescribedNodes; use crate::node_status_api::uptime_updater::HistoricalUptimeUpdater; +use crate::support::caching::cache::SharedCache; use crate::support::cli; use crate::support::cli::CliArgs; use crate::support::config::Config; @@ -34,8 +36,10 @@ mod ephemera; mod epoch_operations; pub(crate) mod network; mod network_monitor; +pub(crate) mod node_describe_cache; pub(crate) mod node_status_api; pub(crate) mod nym_contract_cache; +pub(crate) mod nym_nodes; pub(crate) mod support; struct ShutdownHandles { @@ -90,6 +94,18 @@ async fn start_nym_api_tasks( let node_status_cache_state = rocket.state::().unwrap(); let circulating_supply_cache_state = rocket.state::().unwrap(); let maybe_storage = rocket.state::(); + let described_nodes_state = rocket.state::>().unwrap(); + + // start note describe cache refresher + // we should be doing the below, but can't due to our current startup structure + // let refresher = node_describe_cache::new_refresher(&config.topology_cacher); + // let cache = refresher.get_shared_cache(); + node_describe_cache::new_refresher_with_initial_value( + &config.topology_cacher, + described_nodes_state.to_owned(), + ) + .named("node-self-described-data-refresher") + .start(shutdown.subscribe_named("node-self-described-data-refresher")); // start all the caches first let nym_contract_cache_listener = nym_contract_cache::start_refresher( @@ -98,6 +114,7 @@ async fn start_nym_api_tasks( nyxd_client.clone(), &shutdown, ); + node_status_api::start_cache_refresh( &config.node_status_api, nym_contract_cache_state, diff --git a/nym-api/src/node_describe_cache/mod.rs b/nym-api/src/node_describe_cache/mod.rs new file mode 100644 index 0000000000..cc2b547402 --- /dev/null +++ b/nym-api/src/node_describe_cache/mod.rs @@ -0,0 +1,158 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::support::caching::cache::{SharedCache, UninitialisedCache}; +use crate::support::caching::refresher::{CacheItemProvider, CacheRefresher}; +use crate::support::config; +use crate::support::config::DEFAULT_NODE_DESCRIBE_BATCH_SIZE; +use futures_util::{stream, StreamExt}; +use nym_api_requests::models::NymNodeDescription; +use nym_contracts_common::IdentityKey; +use nym_mixnet_contract_common::{Gateway, GatewayBond}; +use nym_node_requests::api::client::{NymNodeApiClientError, NymNodeApiClientExt}; +use nym_node_requests::api::v1::gateway::models::WebSockets; +use std::collections::HashMap; +use std::ops::Deref; +use thiserror::Error; + +// type alias for ease of use +pub type DescribedNodes = HashMap; + +#[derive(Debug, Error)] +pub enum NodeDescribeCacheError { + #[error("contract cache hasn't been initialised")] + UninitialisedContractCache { + #[from] + source: UninitialisedCache, + }, + + #[error("gateway {gateway} has provided malformed host information ({host}: {source}")] + MalformedHost { + host: String, + + gateway: IdentityKey, + + #[source] + source: NymNodeApiClientError, + }, + + #[error("failed to query gateway '{gateway}': {source}")] + ApiFailure { + gateway: IdentityKey, + + #[source] + source: NymNodeApiClientError, + }, +} + +pub struct NodeDescriptionProvider { + // for now we only care about gateways, nothing more + network_gateways: SharedCache>, + + batch_size: usize, +} + +impl NodeDescriptionProvider { + pub(crate) fn new() -> NodeDescriptionProvider { + NodeDescriptionProvider { + network_gateways: SharedCache::new(), + batch_size: DEFAULT_NODE_DESCRIBE_BATCH_SIZE, + } + } + + #[must_use] + pub(crate) fn with_batch_size(mut self, batch_size: usize) -> Self { + self.batch_size = batch_size; + self + } +} + +async fn get_gateway_websocket_support( + gateway: Gateway, +) -> Result<(IdentityKey, WebSockets), NodeDescribeCacheError> { + let client = match nym_node_requests::api::Client::new_url(&gateway.host, None) { + Ok(client) => client, + Err(err) => { + return Err(NodeDescribeCacheError::MalformedHost { + host: gateway.host, + gateway: gateway.identity_key, + source: err, + }); + } + }; + + let websockets = + client + .get_mixnet_websockets() + .await + .map_err(|err| NodeDescribeCacheError::ApiFailure { + gateway: gateway.identity_key.clone(), + source: err, + })?; + + Ok((gateway.identity_key, websockets)) +} + +#[async_trait] +impl CacheItemProvider for NodeDescriptionProvider { + type Item = HashMap; + type Error = NodeDescribeCacheError; + + async fn try_refresh(&self) -> Result { + let guard = self.network_gateways.get().await?; + let gateways = &*guard; + + // TODO: somehow bypass the 'higher-ranked lifetime error' and remove that redundant clone + let websockets = stream::iter( + gateways + .deref() + .clone() + .into_iter() + .map(|bond| bond.gateway) + .map(get_gateway_websocket_support), + ) + .buffer_unordered(self.batch_size) + .filter_map(|res| async move { + match res { + Ok((identity, mixnet_websockets)) => { + Some((identity, NymNodeDescription { mixnet_websockets })) + } + Err(err) => { + // TODO: reduce it to trace/debug before PR + warn!("{err}"); + None + } + } + }) + .collect::>() + .await; + + Ok(websockets) + } +} + +// currently dead code : ( +#[allow(dead_code)] +pub(crate) fn new_refresher( + config: &config::TopologyCacher, +) -> CacheRefresher { + CacheRefresher::new( + Box::new( + NodeDescriptionProvider::new().with_batch_size(config.debug.node_describe_batch_size), + ), + config.debug.node_describe_caching_interval, + ) +} + +pub(crate) fn new_refresher_with_initial_value( + config: &config::TopologyCacher, + initial: SharedCache, +) -> CacheRefresher { + CacheRefresher::new_with_initial_value( + Box::new( + NodeDescriptionProvider::new().with_batch_size(config.debug.node_describe_batch_size), + ), + config.debug.node_describe_caching_interval, + initial, + ) +} diff --git a/nym-api/src/node_status_api/cache/node_sets.rs b/nym-api/src/node_status_api/cache/node_sets.rs index a80c84d2d1..f871505016 100644 --- a/nym-api/src/node_status_api/cache/node_sets.rs +++ b/nym-api/src/node_status_api/cache/node_sets.rs @@ -180,6 +180,7 @@ pub(crate) async fn annotate_gateways_with_details( annotated.push(GatewayBondAnnotated { blacklisted: blacklist.contains(&gateway_bond.gateway.identity_key), gateway_bond, + self_described: None, performance, node_performance, }); diff --git a/nym-api/src/node_status_api/mod.rs b/nym-api/src/node_status_api/mod.rs index d3488eb99a..c87b7e5b1c 100644 --- a/nym-api/src/node_status_api/mod.rs +++ b/nym-api/src/node_status_api/mod.rs @@ -1,6 +1,12 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use self::cache::refresher::NodeStatusCacheRefresher; +use crate::support::config; +use crate::{ + nym_contract_cache::cache::NymContractCache, + support::{self, storage}, +}; pub(crate) use cache::NodeStatusCache; use nym_task::TaskManager; use okapi::openapi3::OpenApi; @@ -8,13 +14,6 @@ use rocket::Route; use rocket_okapi::{openapi_get_routes_spec, settings::OpenApiSettings}; use std::time::Duration; -use crate::support::config; -use crate::{ - nym_contract_cache::cache::NymContractCache, - support::{self, storage}, -}; - -use self::cache::refresher::NodeStatusCacheRefresher; pub(crate) mod cache; pub(crate) mod helpers; pub(crate) mod local_guard; diff --git a/nym-api/src/nym_nodes/mod.rs b/nym-api/src/nym_nodes/mod.rs new file mode 100644 index 0000000000..4e2a10c5b0 --- /dev/null +++ b/nym-api/src/nym_nodes/mod.rs @@ -0,0 +1,16 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use okapi::openapi3::OpenApi; +use rocket::Route; +use rocket_okapi::openapi_get_routes_spec; +use rocket_okapi::settings::OpenApiSettings; + +pub(crate) mod routes; + +/// Merges the routes with http information and returns it to Rocket for serving +pub(crate) fn nym_node_routes(settings: &OpenApiSettings) -> (Vec, OpenApi) { + openapi_get_routes_spec![ + settings: routes::get_gateways_described + ] +} diff --git a/nym-api/src/nym_nodes/routes.rs b/nym-api/src/nym_nodes/routes.rs new file mode 100644 index 0000000000..cad4549b45 --- /dev/null +++ b/nym-api/src/nym_nodes/routes.rs @@ -0,0 +1,44 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::node_describe_cache::DescribedNodes; +use crate::nym_contract_cache::cache::NymContractCache; +use crate::support::caching::cache::SharedCache; +use nym_api_requests::models::DescribedGateway; +use rocket::serde::json::Json; +use rocket::State; +use rocket_okapi::openapi; +use std::ops::Deref; + +// obviously this should get refactored later on because gateways will go away. +// unless maybe this will be filtering based on which nodes got assigned gateway role? TBD + +#[openapi(tag = "Nym Nodes")] +#[get("/gateways/described")] +pub async fn get_gateways_described( + contract_cache: &State, + describe_cache: &State>, +) -> Json> { + let gateways = contract_cache.gateways_filtered().await; + if gateways.is_empty() { + return Json(Vec::new()); + } + + // if the self describe cache is unavailable, well, don't attach describe data + let Ok(self_descriptions) = describe_cache.get().await else { + return Json(gateways.into_iter().map(Into::into).collect()); + }; + + // TODO: this is extremely inefficient, but given we don't have many gateways, + // it shouldn't be too much of a problem until we go ahead with directory v3 / the smoosh 2: electric smoosharoo, + // but at that point (I hope) the whole caching situation should get refactored + Json( + gateways + .into_iter() + .map(|bond| DescribedGateway { + self_described: self_descriptions.deref().get(bond.identity()).cloned(), + bond, + }) + .collect(), + ) +} diff --git a/nym-api/src/support/config/mod.rs b/nym-api/src/support/config/mod.rs index 9cf1404b38..a215bdd4aa 100644 --- a/nym-api/src/support/config/mod.rs +++ b/nym-api/src/support/config/mod.rs @@ -49,6 +49,11 @@ const DEFAULT_PER_NODE_TEST_PACKETS: usize = 3; const DEFAULT_TOPOLOGY_CACHE_INTERVAL: Duration = Duration::from_secs(30); const DEFAULT_NODE_STATUS_CACHE_INTERVAL: Duration = Duration::from_secs(120); const DEFAULT_CIRCULATING_SUPPLY_CACHE_INTERVAL: Duration = Duration::from_secs(3600); + +pub(crate) const DEFAULT_NODE_DESCRIBE_CACHE_INTERVAL: Duration = Duration::from_secs(4500); +pub(crate) const DEFAULT_NODE_DESCRIBE_BATCH_SIZE: usize = 50; + + const DEFAULT_MONITOR_THRESHOLD: u8 = 60; const DEFAULT_MIN_MIXNODE_RELIABILITY: u8 = 50; const DEFAULT_MIN_GATEWAY_RELIABILITY: u8 = 20; @@ -451,12 +456,19 @@ pub struct TopologyCacher { pub struct TopologyCacherDebug { #[serde(with = "humantime_serde")] pub caching_interval: Duration, + + #[serde(with = "humantime_serde")] + pub node_describe_caching_interval: Duration, + + pub node_describe_batch_size: usize, } impl Default for TopologyCacherDebug { fn default() -> Self { TopologyCacherDebug { caching_interval: DEFAULT_TOPOLOGY_CACHE_INTERVAL, + node_describe_caching_interval: DEFAULT_NODE_DESCRIBE_CACHE_INTERVAL, + node_describe_batch_size: DEFAULT_NODE_DESCRIBE_BATCH_SIZE, } } } diff --git a/nym-api/src/support/config/old_config_v1_1_21.rs b/nym-api/src/support/config/old_config_v1_1_21.rs index 2f13baaad0..be8c06d7c8 100644 --- a/nym-api/src/support/config/old_config_v1_1_21.rs +++ b/nym-api/src/support/config/old_config_v1_1_21.rs @@ -126,6 +126,7 @@ impl From for Config { topology_cacher: TopologyCacher { debug: TopologyCacherDebug { caching_interval: value.topology_cacher.caching_interval, + ..Default::default() }, }, circulating_supply_cacher: CirculatingSupplyCacher { diff --git a/nym-api/src/support/http/mod.rs b/nym-api/src/support/http/mod.rs index b21ccdb381..5d0b646107 100644 --- a/nym-api/src/support/http/mod.rs +++ b/nym-api/src/support/http/mod.rs @@ -5,11 +5,13 @@ use crate::circulating_supply_api::cache::CirculatingSupplyCache; use crate::coconut::{self, comm::QueryCommunicationChannel, InternalSignRequest}; use crate::network::models::NetworkDetails; use crate::network::network_routes; +use crate::node_describe_cache::DescribedNodes; use crate::node_status_api::{self, NodeStatusCache}; use crate::nym_contract_cache::cache::NymContractCache; +use crate::support::caching::cache::SharedCache; use crate::support::config::Config; use crate::support::{nyxd, storage}; -use crate::{circulating_supply_api, nym_contract_cache}; +use crate::{circulating_supply_api, nym_contract_cache, nym_nodes::nym_node_routes}; use anyhow::Result; use rocket::http::Method; use rocket::{Ignite, Rocket}; @@ -39,10 +41,12 @@ pub(crate) async fn setup_rocket( "" => nym_contract_cache::nym_contract_cache_routes(&openapi_settings), "/status" => node_status_api::node_status_routes(&openapi_settings, config.network_monitor.enabled), "/network" => network_routes(&openapi_settings), + "" => nym_node_routes(&openapi_settings), } let rocket = rocket .manage(network_details) + .manage(SharedCache::::new()) .mount("/swagger", make_swagger_ui(&openapi::get_docs())) .attach(setup_cors()?) .attach(NymContractCache::stage()) diff --git a/nym-connect/desktop/Cargo.lock b/nym-connect/desktop/Cargo.lock index a3ef6ab1bd..a574a51bf3 100644 --- a/nym-connect/desktop/Cargo.lock +++ b/nym-connect/desktop/Cargo.lock @@ -2973,6 +2973,7 @@ dependencies = [ "serde", "serde_json", "thiserror", + "tracing", "url", ] @@ -3886,6 +3887,7 @@ dependencies = [ "getset", "nym-coconut-interface", "nym-mixnet-contract-common", + "nym-node-requests", "schemars", "serde", ] @@ -4322,6 +4324,18 @@ dependencies = [ "url", ] +[[package]] +name = "nym-node-requests" +version = "0.1.0" +dependencies = [ + "nym-bin-common", + "nym-crypto", + "schemars", + "serde", + "serde_json", + "thiserror", +] + [[package]] name = "nym-nonexhaustive-delayqueue" version = "0.1.0" diff --git a/nym-node/nym-node-requests/Cargo.toml b/nym-node/nym-node-requests/Cargo.toml index baf56a9a8d..4ee8464503 100644 --- a/nym-node/nym-node-requests/Cargo.toml +++ b/nym-node/nym-node-requests/Cargo.toml @@ -11,6 +11,7 @@ license.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +schemars = { workspace = true, features = ["preserve_order"] } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } thiserror = { workspace = true } diff --git a/nym-node/nym-node-requests/src/api/mod.rs b/nym-node/nym-node-requests/src/api/mod.rs index 5c3efbc73d..6c7811a9e2 100644 --- a/nym-node/nym-node-requests/src/api/mod.rs +++ b/nym-node/nym-node-requests/src/api/mod.rs @@ -1,12 +1,14 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::api::v1::node::models::HostInformation; use crate::error::Error; use nym_crypto::asymmetric::identity; use serde::Serialize; use std::ops::Deref; +#[cfg(feature = "openapi")] +use crate::api::v1::node::models::HostInformation; + #[cfg(feature = "client")] pub mod client; pub mod v1; diff --git a/nym-node/nym-node-requests/src/api/v1/gateway/models.rs b/nym-node/nym-node-requests/src/api/v1/gateway/models.rs index 4fc446f509..6287e9612a 100644 --- a/nym-node/nym-node-requests/src/api/v1/gateway/models.rs +++ b/nym-node/nym-node-requests/src/api/v1/gateway/models.rs @@ -1,15 +1,16 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -#[derive(Serialize, Deserialize, Debug, Clone)] +#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)] #[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] pub struct Gateway { pub client_interfaces: ClientInterfaces, } -#[derive(Serialize, Deserialize, Debug, Clone)] +#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)] #[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] pub struct Wireguard { #[cfg_attr(feature = "openapi", schema(example = 51820, default = 51820))] @@ -18,7 +19,7 @@ pub struct Wireguard { pub public_key: String, } -#[derive(Serialize, Deserialize, Debug, Clone)] +#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)] #[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] pub struct ClientInterfaces { pub wireguard: Option, @@ -27,7 +28,7 @@ pub struct ClientInterfaces { // pub mixnet_tcp: } -#[derive(Serialize, Deserialize, Debug, Clone, Copy)] +#[derive(Serialize, Deserialize, Debug, Clone, Copy, JsonSchema)] #[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] pub struct WebSockets { #[cfg_attr(feature = "openapi", schema(example = 9000, default = 9000))] diff --git a/nym-node/nym-node-requests/src/api/v1/mixnode/models.rs b/nym-node/nym-node-requests/src/api/v1/mixnode/models.rs index 8ca8807eb7..76b8c9cc5b 100644 --- a/nym-node/nym-node-requests/src/api/v1/mixnode/models.rs +++ b/nym-node/nym-node-requests/src/api/v1/mixnode/models.rs @@ -1,9 +1,10 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -#[derive(Serialize, Deserialize, Debug, Clone)] +#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)] #[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] pub struct Mixnode { // /// Base58 encoded ed25519 EdDSA public key of the mixnode. diff --git a/nym-node/nym-node-requests/src/api/v1/network_requester/models.rs b/nym-node/nym-node-requests/src/api/v1/network_requester/models.rs index 53589de8fa..b8aa29236b 100644 --- a/nym-node/nym-node-requests/src/api/v1/network_requester/models.rs +++ b/nym-node/nym-node-requests/src/api/v1/network_requester/models.rs @@ -1,9 +1,10 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -#[derive(Serialize, Deserialize, Debug, Clone)] +#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)] #[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] pub struct NetworkRequester { /// Base58 encoded ed25519 EdDSA public key of the network requester. diff --git a/nym-node/nym-node-requests/src/api/v1/node/models.rs b/nym-node/nym-node-requests/src/api/v1/node/models.rs index 682a0f35ca..2be2625c34 100644 --- a/nym-node/nym-node-requests/src/api/v1/node/models.rs +++ b/nym-node/nym-node-requests/src/api/v1/node/models.rs @@ -1,13 +1,14 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use schemars::JsonSchema; use serde::{Deserialize, Serialize}; #[cfg(feature = "openapi")] pub use crate::api::SignedHostInformation; pub use nym_bin_common::build_information::BinaryBuildInformationOwned; -#[derive(Clone, Default, Debug, Copy, Serialize, Deserialize)] +#[derive(Clone, Default, Debug, Copy, Serialize, Deserialize, JsonSchema)] #[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] pub struct NodeRoles { pub mixnode_enabled: bool, @@ -15,7 +16,7 @@ pub struct NodeRoles { pub network_requester_enabled: bool, } -#[derive(Clone, Default, Debug, Serialize, Deserialize)] +#[derive(Clone, Default, Debug, Serialize, Deserialize, JsonSchema)] #[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] pub struct HostInformation { /// Ip address(es) of this host, such as `1.1.1.1`. @@ -28,7 +29,7 @@ pub struct HostInformation { pub keys: HostKeys, } -#[derive(Clone, Default, Debug, Serialize, Deserialize)] +#[derive(Clone, Default, Debug, Serialize, Deserialize, JsonSchema)] #[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] pub struct HostKeys { /// Base58-encoded ed25519 public key of this node. Currently it corresponds to either mixnode's or gateway's identity.