Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ab1fc724ae | |||
| 775250b0ee | |||
| e975051566 | |||
| 8207a9c38a | |||
| bbdf0f158a | |||
| b276a8bc61 | |||
| f31e6f2470 | |||
| ea08345025 | |||
| 3e5537b753 | |||
| 23f86b6c1e |
Generated
+517
-229
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,7 @@
|
||||
set -eu
|
||||
export ENVIRONMENT=${ENVIRONMENT:-"mainnet"}
|
||||
|
||||
probe_git_ref="nym-vpn-core-v1.4.0"
|
||||
probe_git_ref="nym-vpn-core-v1.10.0"
|
||||
|
||||
crate_root=$(dirname $(realpath "$0"))
|
||||
monorepo_root=$(realpath "${crate_root}/../..")
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
[package]
|
||||
name = "nym-node-status-api"
|
||||
version = "2.3.3"
|
||||
version = "2.4.1-rc.1"
|
||||
authors.workspace = true
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
@@ -23,23 +23,39 @@ envy = { workspace = true }
|
||||
futures-util = { workspace = true }
|
||||
itertools = { workspace = true }
|
||||
moka = { workspace = true, features = ["future"] }
|
||||
nym-contracts-common = { path = "../../common/cosmwasm-smart-contracts/contracts-common" }
|
||||
nym-mixnet-contract-common = { path = "../../common/cosmwasm-smart-contracts/mixnet-contract", features = ["utoipa"] }
|
||||
nym-bin-common = { path = "../../common/bin-common", features = ["models"] }
|
||||
nym-node-status-client = { path = "../nym-node-status-client" }
|
||||
nym-crypto = { path = "../../common/crypto", features = ["asymmetric", "serde"] }
|
||||
nym-http-api-client = { path = "../../common/http-api-client" }
|
||||
nym-http-api-common = { path = "../../common/http-api-common", features = ["middleware"]}
|
||||
nym-network-defaults = { path = "../../common/network-defaults" }
|
||||
nym-serde-helpers = { path = "../../common/serde-helpers" }
|
||||
nym-statistics-common = { path = "../../common/statistics" }
|
||||
nym-validator-client = { path = "../../common/client-libs/validator-client" }
|
||||
nym-task = { path = "../../common/task" }
|
||||
nym-node-requests = { path = "../../nym-node/nym-node-requests", features = ["openapi"] }
|
||||
# TODO dz had to switch to cheddar versions because develop is ahead of current
|
||||
# Nym API: revert after Cheddar is out
|
||||
nym-contracts-common = { git = "https://github.com/nymtech/nym.git", branch = "release/2025.11-cheddar" }
|
||||
nym-mixnet-contract-common = { git = "https://github.com/nymtech/nym.git", branch = "release/2025.11-cheddar" }
|
||||
nym-bin-common = { git = "https://github.com/nymtech/nym.git", branch = "release/2025.11-cheddar" }
|
||||
nym-node-status-client = { git = "https://github.com/nymtech/nym.git", branch = "release/2025.11-cheddar" }
|
||||
nym-crypto = { git = "https://github.com/nymtech/nym.git", branch = "release/2025.11-cheddar" }
|
||||
nym-http-api-client = { git = "https://github.com/nymtech/nym.git", branch = "release/2025.11-cheddar" }
|
||||
nym-http-api-common = { git = "https://github.com/nymtech/nym.git", branch = "release/2025.11-cheddar", features = ["middleware"]}
|
||||
nym-network-defaults = { git = "https://github.com/nymtech/nym.git", branch = "release/2025.11-cheddar" }
|
||||
nym-serde-helpers = { git = "https://github.com/nymtech/nym.git", branch = "release/2025.11-cheddar" }
|
||||
nym-statistics-common = { git = "https://github.com/nymtech/nym.git", branch = "release/2025.11-cheddar" }
|
||||
# nym-contracts-common = { path = "../../common/cosmwasm-smart-contracts/contracts-common" }
|
||||
# nym-mixnet-contract-common = { path = "../../common/cosmwasm-smart-contracts/mixnet-contract", features = ["utoipa"] }
|
||||
# nym-bin-common = { path = "../../common/bin-common", features = ["models"] }
|
||||
# nym-node-status-client = { path = "../nym-node-status-client" }
|
||||
# nym-crypto = { path = "../../common/crypto", features = ["asymmetric", "serde"] }
|
||||
# nym-http-api-client = { path = "../../common/http-api-client" }
|
||||
# nym-http-api-common = { path = "../../common/http-api-common", features = ["middleware"]}
|
||||
# nym-network-defaults = { path = "../../common/network-defaults" }
|
||||
# nym-serde-helpers = { path = "../../common/serde-helpers" }
|
||||
# nym-statistics-common = { path = "../../common/statistics" }
|
||||
# nym-validator-client = { path = "../../common/client-libs/validator-client" }
|
||||
nym-validator-client = { git = "https://github.com/nymtech/nym.git", branch = "release/2025.11-cheddar" }
|
||||
# nym-task = { path = "../../common/task" }
|
||||
# nym-node-requests = { path = "../../nym-node/nym-node-requests", features = ["openapi"] }
|
||||
nym-task = { git = "https://github.com/nymtech/nym.git", branch = "release/2025.11-cheddar" }
|
||||
nym-node-requests = { git = "https://github.com/nymtech/nym.git", branch = "release/2025.11-cheddar" }
|
||||
rand = { workspace = true }
|
||||
rand_chacha = { workspace = true }
|
||||
regex = { workspace = true }
|
||||
reqwest = { workspace = true }
|
||||
semver = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
serde_json_path = { workspace = true }
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -111,7 +111,8 @@ pub(crate) async fn get_all_gateways(pool: &DbPool) -> anyhow::Result<Vec<Gatewa
|
||||
error!("Conversion from DTO failed: {e}. Invalidly stored data?");
|
||||
e
|
||||
})?;
|
||||
tracing::trace!("Fetched {} gateways from DB", items.len());
|
||||
// TODO dz revert to trace
|
||||
tracing::info!("Fetched {} gateways from DB", items.len());
|
||||
Ok(items)
|
||||
}
|
||||
|
||||
|
||||
@@ -36,6 +36,8 @@ pub(crate) async fn get_all_nym_nodes(pool: &DbPool) -> anyhow::Result<Vec<NymNo
|
||||
bond_info as "bond_info: serde_json::Value"
|
||||
FROM
|
||||
nym_nodes
|
||||
ORDER BY
|
||||
node_id
|
||||
"#,
|
||||
)
|
||||
.fetch(&mut *conn)
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
use axum::{
|
||||
extract::{Path, Query, State},
|
||||
Json, Router,
|
||||
};
|
||||
use semver::Version;
|
||||
use serde::Deserialize;
|
||||
use std::sync::LazyLock;
|
||||
use tracing::instrument;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::http::models::DVpnGateway;
|
||||
use crate::http::{
|
||||
error::{HttpError, HttpResult},
|
||||
state::AppState,
|
||||
};
|
||||
|
||||
static MIN_SUPPORTED_VERSION: LazyLock<Version> = LazyLock::new(|| Version::new(1, 6, 2));
|
||||
|
||||
pub(crate) fn routes() -> Router<AppState> {
|
||||
Router::new()
|
||||
.route("/", axum::routing::get(dvpn_gateways))
|
||||
.route(
|
||||
"/country/:two_letter_country_code",
|
||||
axum::routing::get(gateways_by_country),
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Deserialize, IntoParams)]
|
||||
#[into_params(parameter_in = Query)]
|
||||
struct MinNodeVersionQuery {
|
||||
min_node_version: Option<String>,
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
tag = "dVPN Directory Cache",
|
||||
get,
|
||||
params(
|
||||
MinNodeVersionQuery
|
||||
),
|
||||
path = "/directory/gateways",
|
||||
summary = "Gets available entry and exit gateways from the Nym network directory",
|
||||
context_path = "/dvpn/v1",
|
||||
responses(
|
||||
(status = 200, body = Vec<DVpnGateway>)
|
||||
)
|
||||
)]
|
||||
#[instrument(level = tracing::Level::INFO, skip(state))]
|
||||
async fn dvpn_gateways(
|
||||
Query(MinNodeVersionQuery { min_node_version }): Query<MinNodeVersionQuery>,
|
||||
state: State<AppState>,
|
||||
) -> HttpResult<Json<Vec<DVpnGateway>>> {
|
||||
let min_node_version = match min_node_version {
|
||||
Some(min_version) => semver::Version::parse(&min_version)
|
||||
.map_err(|_| HttpError::invalid_input("Min version must be valid semver"))?,
|
||||
None => MIN_SUPPORTED_VERSION.clone(),
|
||||
};
|
||||
|
||||
Ok(Json(
|
||||
state
|
||||
.cache()
|
||||
.get_dvpn_gateway_list(state.db_pool(), &min_node_version)
|
||||
.await,
|
||||
))
|
||||
}
|
||||
|
||||
#[allow(dead_code)] // clippy doesn't detect usage in utoipa macros
|
||||
#[derive(Deserialize, IntoParams)]
|
||||
#[into_params(parameter_in = Path)]
|
||||
struct TwoLetterCountryCodeParam {
|
||||
#[param(min_length = 2, max_length = 2)]
|
||||
two_letter_country_code: String,
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
tag = "dVPN Directory Cache",
|
||||
get,
|
||||
params(
|
||||
TwoLetterCountryCodeParam
|
||||
),
|
||||
path = "/directory/gateways/country/{two_letter_country_code}",
|
||||
summary = "Gets available gateways from the Nym network directory by country",
|
||||
context_path = "/dvpn/v1",
|
||||
responses(
|
||||
(status = 200, body = Vec<DVpnGateway>)
|
||||
)
|
||||
)]
|
||||
#[instrument(level = tracing::Level::INFO, skip(state), fields(two_letter_country_code = two_letter_country_code))]
|
||||
async fn gateways_by_country(
|
||||
Path(TwoLetterCountryCodeParam {
|
||||
two_letter_country_code,
|
||||
}): Path<TwoLetterCountryCodeParam>,
|
||||
state: State<AppState>,
|
||||
) -> HttpResult<Json<Vec<DVpnGateway>>> {
|
||||
let country_filter = two_letter_country_code.to_lowercase();
|
||||
match two_letter_country_code.len() {
|
||||
2 => Ok(Json(
|
||||
state
|
||||
.cache()
|
||||
.get_dvpn_gateway_list(state.db_pool(), &MIN_SUPPORTED_VERSION)
|
||||
.await
|
||||
.into_iter()
|
||||
.filter(|gw| {
|
||||
gw.location.two_letter_iso_country_code.to_lowercase() == country_filter
|
||||
})
|
||||
.collect(),
|
||||
)),
|
||||
_ => Err(HttpError::invalid_input(
|
||||
"Only two letter country code is allowed",
|
||||
)),
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ use utoipa_swagger_ui::SwaggerUi;
|
||||
|
||||
use crate::http::{server::HttpServer, state::AppState};
|
||||
|
||||
pub(crate) mod dvpn;
|
||||
pub(crate) mod gateways;
|
||||
pub(crate) mod metrics;
|
||||
pub(crate) mod mixnodes;
|
||||
@@ -44,6 +45,10 @@ impl RouterBuilder {
|
||||
"/explorer/v3",
|
||||
Router::new().nest("/nym-nodes", nym_nodes::routes()),
|
||||
)
|
||||
.nest(
|
||||
"/dvpn/v1",
|
||||
Router::new().nest("/directory/gateways", dvpn::routes()),
|
||||
)
|
||||
.nest(
|
||||
"/internal",
|
||||
Router::new().nest("/testruns", testruns::routes()),
|
||||
|
||||
@@ -1,12 +1,22 @@
|
||||
use cosmwasm_std::{Addr, Coin, Decimal};
|
||||
use nym_mixnet_contract_common::CoinSchema;
|
||||
use nym_node_requests::api::v1::node::models::NodeDescription;
|
||||
use nym_validator_client::client::NodeId;
|
||||
use nym_validator_client::{
|
||||
client::NodeId,
|
||||
models::{
|
||||
AuthenticatorDetails, BinaryBuildInformationOwned, IpPacketRouterDetails, NymNodeData,
|
||||
},
|
||||
nym_api::SkimmedNode,
|
||||
nym_nodes::{BasicEntryInformation, NodeRole},
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing::{error, instrument};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
pub(crate) use nym_node_status_client::models::TestrunAssignment;
|
||||
|
||||
use crate::monitor::ExplorerPrettyBond;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
pub struct Gateway {
|
||||
pub gateway_identity_key: String,
|
||||
@@ -23,6 +33,192 @@ pub struct Gateway {
|
||||
pub config_score: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)]
|
||||
pub struct BuildInformation {
|
||||
pub build_version: String,
|
||||
pub commit_branch: String,
|
||||
pub commit_sha: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)]
|
||||
pub struct Location {
|
||||
pub two_letter_iso_country_code: String,
|
||||
pub latitude: f64,
|
||||
pub longitude: f64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)]
|
||||
pub struct DVpnGateway {
|
||||
pub identity_key: String,
|
||||
pub name: String,
|
||||
pub ip_packet_router: Option<IpPacketRouterDetails>,
|
||||
pub authenticator: Option<AuthenticatorDetails>,
|
||||
pub location: Location,
|
||||
pub last_probe: Option<DirectoryGwProbe>,
|
||||
pub ip_addresses: Vec<String>,
|
||||
pub mix_port: u16,
|
||||
pub role: NodeRole,
|
||||
pub entry: Option<BasicEntryInformation>,
|
||||
// The performance data here originates from the nym-api, and is effectively mixnet performance
|
||||
// at the time of writing this
|
||||
pub performance: String,
|
||||
pub build_information: BinaryBuildInformationOwned,
|
||||
}
|
||||
|
||||
/// based on
|
||||
/// https://github.com/nymtech/nym-vpn-client/blob/nym-vpn-core-v1.10.0/nym-vpn-core/crates/nym-gateway-probe/src/types.rs
|
||||
/// TODO: long term types should be moved into this repo because nym-vpn-client
|
||||
/// could pull it as a dependency and we'd have a single source of truth
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)]
|
||||
pub struct LastProbeResult {
|
||||
node: String,
|
||||
used_entry: String,
|
||||
outcome: ProbeOutcome,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)]
|
||||
pub struct DirectoryGwProbe {
|
||||
last_updated_utc: String,
|
||||
outcome: ProbeOutcome,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)]
|
||||
pub struct ProbeOutcome {
|
||||
as_entry: directory_gw_probe_outcome::Entry,
|
||||
as_exit: Option<directory_gw_probe_outcome::Exit>,
|
||||
wg: Option<wg_outcome_versions::ProbeOutcomeV1>,
|
||||
}
|
||||
|
||||
pub mod directory_gw_probe_outcome {
|
||||
use super::*;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(untagged)]
|
||||
#[allow(clippy::enum_variant_names)]
|
||||
pub enum Entry {
|
||||
Tested(EntryTestResult),
|
||||
NotTested,
|
||||
EntryFailure,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
pub struct EntryTestResult {
|
||||
pub can_connect: bool,
|
||||
pub can_route: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)]
|
||||
pub struct Exit {
|
||||
can_connect: bool,
|
||||
can_route_ip_v4: bool,
|
||||
can_route_ip_external_v4: bool,
|
||||
can_route_ip_v6: bool,
|
||||
can_route_ip_external_v6: bool,
|
||||
}
|
||||
}
|
||||
|
||||
pub mod wg_outcome_versions {
|
||||
use super::*;
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)]
|
||||
pub struct ProbeOutcomeV1 {
|
||||
pub can_register: bool,
|
||||
|
||||
pub can_handshake_v4: bool,
|
||||
pub can_resolve_dns_v4: bool,
|
||||
pub ping_hosts_performance_v4: f32,
|
||||
pub ping_ips_performance_v4: f32,
|
||||
|
||||
pub can_handshake_v6: bool,
|
||||
pub can_resolve_dns_v6: bool,
|
||||
pub ping_hosts_performance_v6: f32,
|
||||
pub ping_ips_performance_v6: f32,
|
||||
|
||||
pub download_duration_sec_v4: u64,
|
||||
pub downloaded_file_v4: String,
|
||||
pub download_error_v4: String,
|
||||
|
||||
pub download_duration_sec_v6: u64,
|
||||
pub downloaded_file_v6: String,
|
||||
pub download_error_v6: String,
|
||||
}
|
||||
}
|
||||
|
||||
impl DVpnGateway {
|
||||
#[instrument(level = tracing::Level::INFO, name = "dvpn_gw_new", skip_all, fields(gateway_key = gateway.gateway_identity_key, node_id = skimmed_node.node_id))]
|
||||
pub(crate) fn new(gateway: Gateway, skimmed_node: &SkimmedNode) -> anyhow::Result<Self> {
|
||||
let location = gateway
|
||||
.explorer_pretty_bond
|
||||
.ok_or_else(|| anyhow::anyhow!("Missing explorer_pretty_bond"))
|
||||
.and_then(|value| {
|
||||
serde_json::from_value::<ExplorerPrettyBond>(value).map_err(From::from)
|
||||
})
|
||||
.map(|bond| bond.location)?;
|
||||
|
||||
let self_described = gateway
|
||||
.self_described
|
||||
.ok_or_else(|| anyhow::anyhow!("Missing self_described"))
|
||||
.and_then(|value| serde_json::from_value::<NymNodeData>(value).map_err(From::from))?;
|
||||
|
||||
let last_probe_result = match gateway.last_probe_result {
|
||||
Some(value) => {
|
||||
let parsed =
|
||||
serde_json::from_value::<LastProbeResult>(value).inspect_err(|err| {
|
||||
error!("Failed to deserialize probe result: {err}");
|
||||
})?;
|
||||
Some(parsed)
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
identity_key: gateway.gateway_identity_key,
|
||||
name: gateway.description.moniker,
|
||||
ip_packet_router: self_described.ip_packet_router,
|
||||
authenticator: self_described.authenticator,
|
||||
location: Location {
|
||||
latitude: location.location.latitude,
|
||||
longitude: location.location.longitude,
|
||||
two_letter_iso_country_code: location.two_letter_iso_country_code,
|
||||
},
|
||||
last_probe: last_probe_result.map(|res| DirectoryGwProbe {
|
||||
last_updated_utc: gateway.last_testrun_utc.unwrap_or_default(),
|
||||
outcome: res.outcome,
|
||||
}),
|
||||
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: to_percent(gateway.performance),
|
||||
build_information: self_described.build_information,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn to_percent(performance: u8) -> String {
|
||||
let fraction = performance as f32 / 100.0;
|
||||
format!("{:.2}", fraction)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn to_percent_should_work() {
|
||||
let starting = [0u8, 33, 50, 99, 100];
|
||||
let expected = ["0.00", "0.33", "0.50", "0.99", "1.00"];
|
||||
|
||||
for (starting, expected) in starting.into_iter().zip(expected) {
|
||||
assert_eq!(expected, to_percent(starting));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)]
|
||||
pub struct GatewaySkinny {
|
||||
pub gateway_identity_key: String,
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
use cosmwasm_std::Decimal;
|
||||
use itertools::Itertools;
|
||||
use moka::{future::Cache, Entry};
|
||||
use nym_contracts_common::NaiveFloat;
|
||||
use nym_crypto::asymmetric::ed25519::PublicKey;
|
||||
use nym_mixnet_contract_common::NodeId;
|
||||
use nym_validator_client::nym_api::SkimmedNode;
|
||||
use semver::Version;
|
||||
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},
|
||||
http::models::{DailyStats, ExtendedNymNode, Gateway, Mixnode, NodeGeoData, SummaryHistory},
|
||||
http::models::{
|
||||
DVpnGateway, DailyStats, ExtendedNymNode, Gateway, Mixnode, NodeGeoData, SummaryHistory,
|
||||
},
|
||||
monitor::{DelegationsCache, NodeGeoCache},
|
||||
};
|
||||
|
||||
@@ -88,6 +92,8 @@ const MIXNODE_STATS_HISTORY_DAYS: usize = 30;
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct HttpCache {
|
||||
gateways: Cache<String, Arc<RwLock<Vec<Gateway>>>>,
|
||||
// each min_version has their own cached list
|
||||
dvpn_gateways: Cache<Version, Arc<RwLock<Vec<DVpnGateway>>>>,
|
||||
mixnodes: Cache<String, Arc<RwLock<Vec<Mixnode>>>>,
|
||||
nym_nodes: Cache<String, Arc<RwLock<Vec<ExtendedNymNode>>>>,
|
||||
mixstats: Cache<String, Arc<RwLock<Vec<DailyStats>>>>,
|
||||
@@ -102,6 +108,10 @@ impl HttpCache {
|
||||
.max_capacity(2)
|
||||
.time_to_live(Duration::from_secs(ttl_seconds))
|
||||
.build(),
|
||||
dvpn_gateways: Cache::builder()
|
||||
.max_capacity(6)
|
||||
.time_to_live(Duration::from_secs(ttl_seconds))
|
||||
.build(),
|
||||
mixnodes: Cache::builder()
|
||||
.max_capacity(2)
|
||||
.time_to_live(Duration::from_secs(ttl_seconds))
|
||||
@@ -158,10 +168,11 @@ impl HttpCache {
|
||||
let gateways = crate::db::queries::get_all_gateways(db)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
self.upsert_gateway_list(gateways.clone()).await;
|
||||
|
||||
if gateways.is_empty() {
|
||||
tracing::warn!("Database contains 0 gateways");
|
||||
tracing::warn!("Database: gateway list is empty");
|
||||
} else {
|
||||
self.upsert_gateway_list(gateways.clone()).await;
|
||||
}
|
||||
|
||||
gateways
|
||||
@@ -169,6 +180,132 @@ impl HttpCache {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn upsert_dvpn_gateway_list(
|
||||
&self,
|
||||
new_gateway_list: Vec<DVpnGateway>,
|
||||
min_node_version: &Version,
|
||||
) -> Entry<Version, Arc<RwLock<Vec<DVpnGateway>>>> {
|
||||
self.dvpn_gateways
|
||||
.entry_by_ref(min_node_version)
|
||||
.and_upsert_with(|maybe_entry| async {
|
||||
if let Some(entry) = maybe_entry {
|
||||
let v = entry.into_value();
|
||||
let mut guard = v.write().await;
|
||||
*guard = new_gateway_list;
|
||||
v.clone()
|
||||
} else {
|
||||
Arc::new(RwLock::new(new_gateway_list))
|
||||
}
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_dvpn_gateway_list(
|
||||
&self,
|
||||
db: &DbPool,
|
||||
min_node_version: &Version,
|
||||
) -> Vec<DVpnGateway> {
|
||||
match self.dvpn_gateways.get(min_node_version).await {
|
||||
Some(guard) => {
|
||||
tracing::trace!("Fetching from cache...");
|
||||
let read_lock = guard.read().await;
|
||||
read_lock.clone()
|
||||
}
|
||||
None => {
|
||||
tracing::trace!("No gateways (dVPN) in cache, refreshing from 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::<HashMap<_, _>>()
|
||||
})
|
||||
.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(|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_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| {
|
||||
let gw_version = &gw.build_information.build_version;
|
||||
if let Ok(gw_version) = Version::parse(gw_version) {
|
||||
&gw_version >= min_node_version
|
||||
} else {
|
||||
warn!("Failed to parse GW version {}", gw_version);
|
||||
false
|
||||
}
|
||||
})
|
||||
.filter(|gw| {
|
||||
// gateways must have a country
|
||||
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| {
|
||||
(
|
||||
item.location.two_letter_iso_country_code.clone(),
|
||||
item.identity_key.clone(),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if res_gws.is_empty() && started_with > 0 {
|
||||
tracing::warn!("Started with {}, got 0 gateways", started_with);
|
||||
} else {
|
||||
self.upsert_dvpn_gateway_list(res_gws.clone(), min_node_version)
|
||||
.await;
|
||||
}
|
||||
|
||||
res_gws
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn upsert_mixnode_list(
|
||||
&self,
|
||||
new_mixnode_list: Vec<Mixnode>,
|
||||
@@ -201,10 +338,11 @@ impl HttpCache {
|
||||
let mixnodes = crate::db::queries::get_all_mixnodes(db)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
self.upsert_mixnode_list(mixnodes.clone()).await;
|
||||
|
||||
if mixnodes.is_empty() {
|
||||
tracing::warn!("Database contains 0 mixnodes");
|
||||
} else {
|
||||
self.upsert_mixnode_list(mixnodes.clone()).await;
|
||||
}
|
||||
|
||||
mixnodes
|
||||
@@ -245,14 +383,13 @@ impl HttpCache {
|
||||
None => {
|
||||
tracing::trace!("No nym nodes in cache, refreshing cache from DB...");
|
||||
|
||||
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;
|
||||
let nym_nodes = aggregate_node_info_from_db(db, node_geocache).await?;
|
||||
|
||||
if nym_nodes.is_empty() {
|
||||
tracing::warn!("Database contains 0 nym nodes");
|
||||
} else {
|
||||
self.upsert_nym_node_list(nym_nodes.clone()).await;
|
||||
}
|
||||
|
||||
Ok(nym_nodes)
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ impl IpInfoClient {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub(crate) struct ExplorerPrettyBond {
|
||||
pub(crate) identity_key: String,
|
||||
pub(crate) owner: Addr,
|
||||
@@ -71,7 +71,7 @@ pub(crate) struct ExplorerPrettyBond {
|
||||
pub(crate) location: Location,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize)]
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub(crate) struct Location {
|
||||
pub(crate) two_letter_iso_country_code: String,
|
||||
#[serde(flatten)]
|
||||
|
||||
@@ -7,7 +7,6 @@ use crate::db::models::{
|
||||
NYMNODES_DESCRIBED_COUNT, NYMNODE_COUNT,
|
||||
};
|
||||
use crate::db::{queries, DbPool};
|
||||
use crate::monitor::geodata::{ExplorerPrettyBond, Location};
|
||||
use crate::utils::now_utc;
|
||||
use crate::utils::{decimal_to_i64, LogError, NumericalCheckedCast};
|
||||
use anyhow::anyhow;
|
||||
@@ -30,7 +29,7 @@ use std::{
|
||||
use tokio::{sync::RwLock, time::Duration};
|
||||
use tracing::instrument;
|
||||
|
||||
pub(crate) use geodata::IpInfoClient;
|
||||
pub(crate) use geodata::{ExplorerPrettyBond, IpInfoClient, Location};
|
||||
pub(crate) use node_delegations::DelegationsCache;
|
||||
|
||||
mod geodata;
|
||||
@@ -106,11 +105,10 @@ impl Monitor {
|
||||
.clone()
|
||||
.expect("rust sdk mainnet default missing api_url");
|
||||
|
||||
let nym_api =
|
||||
nym_http_api_client::ClientBuilder::new_with_urls(vec![default_api_url.into()])
|
||||
.no_hickory_dns()
|
||||
.with_timeout(self.nym_api_client_timeout)
|
||||
.build::<&str>()?;
|
||||
let nym_api = nym_http_api_client::ClientBuilder::new_with_url(default_api_url)
|
||||
.no_hickory_dns()
|
||||
.with_timeout(self.nym_api_client_timeout)
|
||||
.build::<&str>()?;
|
||||
|
||||
let api_client = NymApiClient::from(nym_api);
|
||||
|
||||
@@ -134,6 +132,8 @@ impl Monitor {
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
tracing::info!("🟣 🚪 gateway nodes: {}", gateways.len());
|
||||
|
||||
let bonded_nym_nodes = api_client
|
||||
.get_all_bonded_nym_nodes()
|
||||
.await?
|
||||
@@ -150,7 +150,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);
|
||||
@@ -251,7 +252,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()),
|
||||
@@ -267,7 +268,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()?,
|
||||
@@ -457,11 +458,7 @@ impl Monitor {
|
||||
async fn check_ipinfo_bandwidth(&self) {
|
||||
match self.ipinfo.check_remaining_bandwidth().await {
|
||||
Ok(bandwidth) => {
|
||||
tracing::info!(
|
||||
"ipinfo monthly bandwidth: {}/{} spent",
|
||||
bandwidth.month,
|
||||
bandwidth.limit
|
||||
);
|
||||
tracing::info!("ipinfo monthly bandwidth: {} spent", bandwidth.month);
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::debug!("Couldn't check ipinfo bandwidth: {}", err);
|
||||
|
||||
@@ -57,7 +57,7 @@ async fn run(
|
||||
.clone()
|
||||
.expect("rust sdk mainnet default missing api_url");
|
||||
|
||||
let nym_api = nym_http_api_client::ClientBuilder::new_with_urls(vec![default_api_url.into()])
|
||||
let nym_api = nym_http_api_client::ClientBuilder::new_with_url(default_api_url)
|
||||
.no_hickory_dns()
|
||||
.with_timeout(nym_api_client_timeout)
|
||||
.build::<&str>()?;
|
||||
|
||||
@@ -24,7 +24,7 @@ pub(crate) async fn spawn(pool: DbPool, refresh_interval: Duration) {
|
||||
});
|
||||
}
|
||||
|
||||
#[instrument(level = "debug", name = "testrun_queue", skip_all)]
|
||||
#[instrument(level = "info", name = "testrun_queue", skip_all)]
|
||||
async fn run(pool: &DbPool) -> anyhow::Result<()> {
|
||||
tracing::info!("Spawning testruns...");
|
||||
if pool.is_closed() {
|
||||
|
||||
Reference in New Issue
Block a user