From 2f051fd943664392ebab0cd0b0eb740901398e59 Mon Sep 17 00:00:00 2001 From: Dinko Zdravac <173912580+dynco-nym@users.noreply.github.com> Date: Fri, 13 Sep 2024 17:14:07 +0200 Subject: [PATCH 1/6] Node Status API background task (#4854) * Setup new package * Setup DB * Fetch & store mixnodes/GWs - refactor db package structure - finally solve DATABASE_URL: absolute path works best * Additional query functionality - missing only daily summary, which requires type refactoring * Replace type alias tuples with structs * Insert summary * Add github job to build package * Build script for sqlx * Remove data dir - useless now that sqlx DB sits in OUT_DIR * PR feedback --- Cargo.lock | 23 ++ Cargo.toml | 2 + nym-node-status-api/.gitignore | 1 + nym-node-status-api/Cargo.toml | 37 ++ nym-node-status-api/build.rs | 33 ++ nym-node-status-api/launch_node_status_api.sh | 64 +++ nym-node-status-api/migrations/000_init.sql | 59 +++ nym-node-status-api/src/cli/mod.rs | 17 + nym-node-status-api/src/db/mod.rs | 40 ++ nym-node-status-api/src/db/models.rs | 128 ++++++ .../src/db/queries/gateways.rs | 116 ++++++ nym-node-status-api/src/db/queries/misc.rs | 86 ++++ .../src/db/queries/mixnodes.rs | 101 +++++ nym-node-status-api/src/db/queries/mod.rs | 9 + nym-node-status-api/src/logging.rs | 41 ++ nym-node-status-api/src/main.rs | 38 ++ nym-node-status-api/src/monitor/mod.rs | 374 ++++++++++++++++++ 17 files changed, 1169 insertions(+) create mode 100644 nym-node-status-api/.gitignore create mode 100644 nym-node-status-api/Cargo.toml create mode 100644 nym-node-status-api/build.rs create mode 100755 nym-node-status-api/launch_node_status_api.sh create mode 100644 nym-node-status-api/migrations/000_init.sql create mode 100644 nym-node-status-api/src/cli/mod.rs create mode 100644 nym-node-status-api/src/db/mod.rs create mode 100644 nym-node-status-api/src/db/models.rs create mode 100644 nym-node-status-api/src/db/queries/gateways.rs create mode 100644 nym-node-status-api/src/db/queries/misc.rs create mode 100644 nym-node-status-api/src/db/queries/mixnodes.rs create mode 100644 nym-node-status-api/src/db/queries/mod.rs create mode 100644 nym-node-status-api/src/logging.rs create mode 100644 nym-node-status-api/src/main.rs create mode 100644 nym-node-status-api/src/monitor/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 0a402bea902..a1b8a47c19c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5878,6 +5878,29 @@ dependencies = [ "utoipa", ] +[[package]] +name = "nym-node-status-api" +version = "0.1.0" +dependencies = [ + "anyhow", + "axum 0.7.5", + "chrono", + "clap 4.5.17", + "cosmwasm-std", + "futures-util", + "nym-bin-common", + "nym-explorer-client", + "nym-network-defaults", + "nym-validator-client", + "serde", + "serde_json", + "sqlx", + "thiserror", + "tokio", + "tracing", + "tracing-subscriber", +] + [[package]] name = "nym-node-tester-utils" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 6303eccca06..8f2bc57f13f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -120,6 +120,7 @@ members = [ "nym-node", "nym-node/nym-node-http-api", "nym-node/nym-node-requests", + "nym-node-status-api", "nym-outfox", "nym-validator-rewarder", "tools/echo-server", @@ -238,6 +239,7 @@ eyre = "0.6.9" fastrand = "2.1.1" flate2 = "1.0.34" futures = "0.3.28" +futures-util = "0.3" generic-array = "0.14.7" getrandom = "0.2.10" getset = "0.1.3" diff --git a/nym-node-status-api/.gitignore b/nym-node-status-api/.gitignore new file mode 100644 index 00000000000..8fce603003c --- /dev/null +++ b/nym-node-status-api/.gitignore @@ -0,0 +1 @@ +data/ diff --git a/nym-node-status-api/Cargo.toml b/nym-node-status-api/Cargo.toml new file mode 100644 index 00000000000..f670afc8d82 --- /dev/null +++ b/nym-node-status-api/Cargo.toml @@ -0,0 +1,37 @@ +# Copyright 2024 - Nym Technologies SA +# SPDX-License-Identifier: Apache-2.0 + +[package] +name = "nym-node-status-api" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true + +[dependencies] +anyhow = { workspace = true } +axum = { workspace = true } +chrono = { workspace = true } +clap = { workspace = true, features = ["cargo", "derive"] } +cosmwasm-std = { workspace = true } +futures-util = { workspace = true } +nym-bin-common = { path = "../common/bin-common" } +nym-explorer-client = { path = "../explorer-api/explorer-client" } +nym-network-defaults = { path = "../common/network-defaults" } +nym-validator-client = { path = "../common/client-libs/validator-client" } +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } +sqlx = { workspace = true, features = ["runtime-tokio-rustls", "sqlite"] } +thiserror = { workspace = true } +tokio = { workspace = true, features = ["full"] } +tracing = { workspace = true } +tracing-subscriber = { workspace = true, features = ["env-filter"] } + +[build-dependencies] +anyhow = { workspace = true } +tokio = { workspace = true, features = ["macros" ] } +sqlx = { workspace = true, features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"] } diff --git a/nym-node-status-api/build.rs b/nym-node-status-api/build.rs new file mode 100644 index 00000000000..74c6767c6f4 --- /dev/null +++ b/nym-node-status-api/build.rs @@ -0,0 +1,33 @@ +use anyhow::{anyhow, Result}; +use sqlx::{Connection, SqliteConnection}; + +const SQLITE_DB_FILENAME: &str = "nym-node-status-api.sqlite"; + +#[tokio::main] +async fn main() -> Result<()> { + let out_dir = read_env_var("OUT_DIR")?; + let database_path = format!("sqlite://{}/{}?mode=rwc", out_dir, SQLITE_DB_FILENAME); + + let mut conn = SqliteConnection::connect(&database_path).await?; + sqlx::migrate!("./migrations").run(&mut conn).await?; + + #[cfg(target_family = "unix")] + println!("cargo::rustc-env=DATABASE_URL=sqlite://{}", &database_path); + + #[cfg(target_family = "windows")] + // for some strange reason we need to add a leading `/` to the windows path even though it's + // not a valid windows path... but hey, it works... + println!("cargo::rustc-env=DATABASE_URL=sqlite:///{}", &database_path); + + rerun_if_changed(); + Ok(()) +} + +fn read_env_var(var: &str) -> Result { + std::env::var(var).map_err(|_| anyhow!("You need to set {} env var", var)) +} + +fn rerun_if_changed() { + println!("cargo::rerun-if-changed=migrations"); + println!("cargo::rerun-if-changed=src/db/queries"); +} diff --git a/nym-node-status-api/launch_node_status_api.sh b/nym-node-status-api/launch_node_status_api.sh new file mode 100755 index 00000000000..8af370232f2 --- /dev/null +++ b/nym-node-status-api/launch_node_status_api.sh @@ -0,0 +1,64 @@ +#!/bin/bash + +set -e + +function usage() { + echo "Usage: $0 [-ci]" + echo " -c Clear DB and re-initialize it before launching the binary." + echo " -i Only initialize and prepare database, env vars then exit without" + echo " launching" + exit 0 +} + +function init_db() { + rm -rf data/* + # https://github.com/launchbadge/sqlx/blob/main/sqlx-cli/README.md + cargo sqlx database drop -y + + cargo sqlx database create + cargo sqlx migrate run + cargo sqlx prepare + + echo "Fresh database ready!" +} + +# export DATABASE_URL as absolute path due to this +# https://github.com/launchbadge/sqlx/issues/3099 +db_filename="nym-node-status-api.sqlite" +script_abs_path=$(realpath "$0") +package_dir=$(dirname "$script_abs_path") +db_abs_path="$package_dir/data/$db_filename" +dotenv_file="$package_dir/.env" +echo "DATABASE_URL=sqlite://$db_abs_path" > "$dotenv_file" + +export RUST_LOG=${RUST_LOG:-debug} + +# export DATABASE_URL from .env file +set -a && source "$dotenv_file" && set +a + +clear_db=false +init_only=false + +while getopts "ci" opt; do + case ${opt} in + c) + clear_db=true + ;; + i) + init_only=true + ;; + \?) + usage + ;; + esac +done + +if [ "$clear_db" = true ] || [ "$init_only" = true ]; then + init_db +fi + +if [ "$init_only" = true ]; then + exit 0 +fi + +cargo run --package nym-node-status-api diff --git a/nym-node-status-api/migrations/000_init.sql b/nym-node-status-api/migrations/000_init.sql new file mode 100644 index 00000000000..f950b492293 --- /dev/null +++ b/nym-node-status-api/migrations/000_init.sql @@ -0,0 +1,59 @@ +CREATE TABLE gateways +( + id INTEGER PRIMARY KEY AUTOINCREMENT, + gateway_identity_key VARCHAR NOT NULL UNIQUE, + self_described VARCHAR, + explorer_pretty_bond VARCHAR, + last_probe_result VARCHAR, + last_probe_log VARCHAR, + config_score INTEGER NOT NULL DEFAULT (0), + config_score_successes REAL NOT NULL DEFAULT (0), + config_score_samples REAL NOT NULL DEFAULT (0), + routing_score INTEGER NOT NULL DEFAULT (0), + routing_score_successes REAL NOT NULL DEFAULT (0), + routing_score_samples REAL NOT NULL DEFAULT (0), + test_run_samples REAL NOT NULL DEFAULT (0), + last_testrun_utc INTEGER, + last_updated_utc INTEGER NOT NULL, + bonded INTEGER CHECK (bonded in (0, 1)) NOT NULL DEFAULT 0, + blacklisted INTEGER CHECK (bonded in (0, 1)) NOT NULL DEFAULT 0, + performance INTEGER NOT NULL DEFAULT 0 +); + +CREATE INDEX idx_gateway_description_gateway_identity_key ON gateways (gateway_identity_key); + + +CREATE TABLE mixnodes ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + identity_key VARCHAR NOT NULL UNIQUE, + mix_id INTEGER NOT NULL UNIQUE, + bonded INTEGER CHECK (bonded in (0, 1)) NOT NULL DEFAULT 0, + total_stake INTEGER NOT NULL, + host VARCHAR NOT NULL, + http_api_port INTEGER NOT NULL, + blacklisted INTEGER CHECK (blacklisted in (0, 1)) NOT NULL DEFAULT 0, + full_details VARCHAR, + self_described VARCHAR, + last_updated_utc INTEGER NOT NULL + , is_dp_delegatee INTEGER CHECK (is_dp_delegatee IN (0, 1)) NOT NULL DEFAULT 0); +CREATE INDEX idx_mixnodes_mix_id ON mixnodes (mix_id); +CREATE INDEX idx_mixnodes_identity_key ON mixnodes (identity_key); + + +CREATE TABLE summary +( + key VARCHAR PRIMARY KEY, + value_json VARCHAR, + last_updated_utc INTEGER NOT NULL +); + + +CREATE TABLE summary_history +( + id INTEGER PRIMARY KEY AUTOINCREMENT, + date VARCHAR UNIQUE NOT NULL, + timestamp_utc INTEGER NOT NULL, + value_json VARCHAR +); +CREATE INDEX idx_summary_history_timestamp_utc ON summary_history (timestamp_utc); +CREATE INDEX idx_summary_history_date ON summary_history (date); diff --git a/nym-node-status-api/src/cli/mod.rs b/nym-node-status-api/src/cli/mod.rs new file mode 100644 index 00000000000..7f465788631 --- /dev/null +++ b/nym-node-status-api/src/cli/mod.rs @@ -0,0 +1,17 @@ +use clap::Parser; +use nym_bin_common::bin_info; +use std::sync::OnceLock; + +// Helper for passing LONG_VERSION to clap +fn pretty_build_info_static() -> &'static str { + static PRETTY_BUILD_INFORMATION: OnceLock = OnceLock::new(); + PRETTY_BUILD_INFORMATION.get_or_init(|| bin_info!().pretty_print()) +} + +#[derive(Parser, Debug)] +#[clap(author = "Nymtech", version, long_version = pretty_build_info_static(), about)] +pub(crate) struct Cli { + /// Path pointing to an env file that configures the Nym API. + #[clap(short, long)] + pub(crate) config_env_file: Option, +} diff --git a/nym-node-status-api/src/db/mod.rs b/nym-node-status-api/src/db/mod.rs new file mode 100644 index 00000000000..d9850abf7d8 --- /dev/null +++ b/nym-node-status-api/src/db/mod.rs @@ -0,0 +1,40 @@ +use std::str::FromStr; + +use crate::read_env_var; +use anyhow::{anyhow, Result}; +use sqlx::{migrate::Migrator, sqlite::SqliteConnectOptions, ConnectOptions, SqlitePool}; +pub(crate) const DATABASE_URL_ENV_VAR: &str = "DATABASE_URL"; +static MIGRATOR: Migrator = sqlx::migrate!("./migrations"); + +pub(crate) mod models; +pub(crate) mod queries; + +pub(crate) type DbPool = SqlitePool; + +pub(crate) struct Storage { + pool: DbPool, +} + +impl Storage { + pub async fn init() -> Result { + let connection_url = read_env_var(DATABASE_URL_ENV_VAR)?; + let connect_options = { + let connect_options = SqliteConnectOptions::from_str(&connection_url)?; + let mut connect_options = connect_options.create_if_missing(true); + let connect_options = connect_options.disable_statement_logging(); + (*connect_options).clone() + }; + + let pool = sqlx::SqlitePool::connect_with(connect_options) + .await + .map_err(|err| anyhow!("Failed to connect to {}: {}", &connection_url, err))?; + + MIGRATOR.run(&pool).await?; + + Ok(Storage { pool }) + } + + pub async fn pool(&self) -> &DbPool { + &self.pool + } +} diff --git a/nym-node-status-api/src/db/models.rs b/nym-node-status-api/src/db/models.rs new file mode 100644 index 00000000000..83e04d99eeb --- /dev/null +++ b/nym-node-status-api/src/db/models.rs @@ -0,0 +1,128 @@ +use serde::{Deserialize, Serialize}; + +pub(crate) struct GatewayRecord { + pub(crate) identity_key: String, + pub(crate) bonded: bool, + pub(crate) blacklisted: bool, + pub(crate) self_described: Option, + pub(crate) explorer_pretty_bond: Option, + pub(crate) last_updated_utc: i64, + pub(crate) performance: u8, +} + +pub(crate) struct MixnodeRecord { + pub(crate) mix_id: u32, + pub(crate) identity_key: String, + pub(crate) bonded: bool, + pub(crate) total_stake: i64, + pub(crate) host: String, + pub(crate) http_port: u16, + pub(crate) blacklisted: bool, + pub(crate) full_details: String, + pub(crate) self_described: Option, + pub(crate) last_updated_utc: i64, + pub(crate) is_dp_delegatee: bool, +} + +#[allow(unused)] +#[derive(Debug, Clone)] +pub(crate) struct BondedStatusDto { + pub(crate) id: i64, + pub(crate) identity_key: String, + pub(crate) bonded: bool, +} + +#[allow(unused)] +#[derive(Debug, Clone, Default)] +pub(crate) struct SummaryDto { + pub(crate) key: String, + pub(crate) value_json: String, + pub(crate) last_updated_utc: i64, +} + +pub(crate) const MIXNODES_BONDED_COUNT: &str = "mixnodes.bonded.count"; +pub(crate) const MIXNODES_BONDED_ACTIVE: &str = "mixnodes.bonded.active"; +pub(crate) const MIXNODES_BONDED_INACTIVE: &str = "mixnodes.bonded.inactive"; +pub(crate) const MIXNODES_BONDED_RESERVE: &str = "mixnodes.bonded.reserve"; +pub(crate) const MIXNODES_BLACKLISTED_COUNT: &str = "mixnodes.blacklisted.count"; + +pub(crate) const GATEWAYS_BONDED_COUNT: &str = "gateways.bonded.count"; +pub(crate) const GATEWAYS_EXPLORER_COUNT: &str = "gateways.explorer.count"; +pub(crate) const GATEWAYS_BLACKLISTED_COUNT: &str = "gateways.blacklisted.count"; + +pub(crate) const MIXNODES_HISTORICAL_COUNT: &str = "mixnodes.historical.count"; +pub(crate) const GATEWAYS_HISTORICAL_COUNT: &str = "gateways.historical.count"; + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub(crate) struct NetworkSummary { + pub(crate) mixnodes: mixnode::MixnodeSummary, + pub(crate) gateways: gateway::GatewaySummary, +} + +pub(crate) mod mixnode { + use super::*; + + #[derive(Debug, Clone, Deserialize, Serialize)] + pub(crate) struct MixnodeSummary { + pub(crate) bonded: MixnodeSummaryBonded, + pub(crate) blacklisted: MixnodeSummaryBlacklisted, + pub(crate) historical: MixnodeSummaryHistorical, + } + + #[derive(Debug, Clone, Deserialize, Serialize)] + pub(crate) struct MixnodeSummaryBonded { + pub(crate) count: i32, + pub(crate) active: i32, + pub(crate) inactive: i32, + pub(crate) reserve: i32, + pub(crate) last_updated_utc: String, + } + + #[derive(Debug, Clone, Deserialize, Serialize)] + pub(crate) struct MixnodeSummaryBlacklisted { + pub(crate) count: i32, + pub(crate) last_updated_utc: String, + } + + #[derive(Debug, Clone, Deserialize, Serialize)] + pub(crate) struct MixnodeSummaryHistorical { + pub(crate) count: i32, + pub(crate) last_updated_utc: String, + } +} + +pub(crate) mod gateway { + use super::*; + + #[derive(Debug, Clone, Deserialize, Serialize)] + pub(crate) struct GatewaySummary { + pub(crate) bonded: GatewaySummaryBonded, + pub(crate) blacklisted: GatewaySummaryBlacklisted, + pub(crate) historical: GatewaySummaryHistorical, + pub(crate) explorer: GatewaySummaryExplorer, + } + + #[derive(Debug, Clone, Deserialize, Serialize)] + pub(crate) struct GatewaySummaryExplorer { + pub(crate) count: i32, + pub(crate) last_updated_utc: String, + } + + #[derive(Debug, Clone, Deserialize, Serialize)] + pub(crate) struct GatewaySummaryBonded { + pub(crate) count: i32, + pub(crate) last_updated_utc: String, + } + + #[derive(Debug, Clone, Deserialize, Serialize)] + pub(crate) struct GatewaySummaryHistorical { + pub(crate) count: i32, + pub(crate) last_updated_utc: String, + } + + #[derive(Debug, Clone, Deserialize, Serialize)] + pub(crate) struct GatewaySummaryBlacklisted { + pub(crate) count: i32, + pub(crate) last_updated_utc: String, + } +} diff --git a/nym-node-status-api/src/db/queries/gateways.rs b/nym-node-status-api/src/db/queries/gateways.rs new file mode 100644 index 00000000000..69f77a97fd1 --- /dev/null +++ b/nym-node-status-api/src/db/queries/gateways.rs @@ -0,0 +1,116 @@ +use crate::db::{ + models::{BondedStatusDto, GatewayRecord}, + DbPool, +}; +use futures_util::TryStreamExt; +use nym_validator_client::models::DescribedGateway; + +pub(crate) async fn insert_gateways( + pool: &DbPool, + gateways: Vec, +) -> anyhow::Result<()> { + let mut db = pool.acquire().await?; + for record in gateways { + sqlx::query!( + "INSERT INTO gateways + (gateway_identity_key, bonded, blacklisted, + self_described, explorer_pretty_bond, + last_updated_utc, performance) + VALUES (?, ?, ?, ?, ?, ?, ?) + ON CONFLICT(gateway_identity_key) DO UPDATE SET + bonded=excluded.bonded, + blacklisted=excluded.blacklisted, + self_described=excluded.self_described, + explorer_pretty_bond=excluded.explorer_pretty_bond, + last_updated_utc=excluded.last_updated_utc, + performance = excluded.performance;", + record.identity_key, + record.bonded, + record.blacklisted, + record.self_described, + record.explorer_pretty_bond, + record.last_updated_utc, + record.performance + ) + .execute(&mut *db) + .await?; + } + + Ok(()) +} + +pub(crate) async fn write_blacklisted_gateways_to_db<'a, I>( + pool: &DbPool, + gateways: I, +) -> anyhow::Result<()> +where + I: Iterator, +{ + let mut conn = pool.acquire().await?; + for gateway_identity_key in gateways { + sqlx::query!( + "UPDATE gateways + SET blacklisted = true + WHERE gateway_identity_key = ?;", + gateway_identity_key, + ) + .execute(&mut *conn) + .await?; + } + + Ok(()) +} + +/// Ensure all gateways that are set as bonded, are still bonded +pub(crate) async fn ensure_gateways_still_bonded( + pool: &DbPool, + gateways: &[DescribedGateway], +) -> anyhow::Result { + let bonded_gateways_rows = get_all_bonded_gateways_row_ids_by_status(pool, true).await?; + let unbonded_gateways_rows = bonded_gateways_rows.iter().filter(|v| { + !gateways + .iter() + .any(|bonded| *bonded.bond.identity() == v.identity_key) + }); + + let recently_unbonded_gateways = unbonded_gateways_rows.to_owned().count(); + let last_updated_utc = chrono::offset::Utc::now().timestamp(); + let mut transaction = pool.begin().await?; + for row in unbonded_gateways_rows { + sqlx::query!( + "UPDATE gateways + SET bonded = ?, last_updated_utc = ? + WHERE id = ?;", + false, + last_updated_utc, + row.id, + ) + .execute(&mut *transaction) + .await?; + } + transaction.commit().await?; + + Ok(recently_unbonded_gateways) +} + +async fn get_all_bonded_gateways_row_ids_by_status( + pool: &DbPool, + status: bool, +) -> anyhow::Result> { + let mut conn = pool.acquire().await?; + let items = sqlx::query_as!( + BondedStatusDto, + r#"SELECT + id as "id!", + gateway_identity_key as "identity_key!", + bonded as "bonded: bool" + FROM gateways + WHERE bonded = ?"#, + status, + ) + .fetch(&mut *conn) + .try_collect::>() + .await?; + + Ok(items) +} diff --git a/nym-node-status-api/src/db/queries/misc.rs b/nym-node-status-api/src/db/queries/misc.rs new file mode 100644 index 00000000000..c2c7b6d9ebe --- /dev/null +++ b/nym-node-status-api/src/db/queries/misc.rs @@ -0,0 +1,86 @@ +use crate::db::{models::NetworkSummary, DbPool}; +use chrono::{DateTime, Utc}; + +/// take `last_updated` instead of calculating it so that `summary` matches +/// `daily_summary` +pub(crate) async fn insert_summaries( + pool: &DbPool, + summaries: &[(&str, &usize)], + summary: &NetworkSummary, + last_updated: DateTime, +) -> anyhow::Result<()> { + insert_summary(pool, summaries, last_updated).await?; + + insert_summary_history(pool, summary, last_updated).await?; + + Ok(()) +} + +async fn insert_summary( + pool: &DbPool, + summaries: &[(&str, &usize)], + last_updated: DateTime, +) -> anyhow::Result<()> { + let timestamp = last_updated.timestamp(); + let mut tx = pool.begin().await?; + + for (kind, value) in summaries { + let value = value.to_string(); + sqlx::query!( + "INSERT INTO summary + (key, value_json, last_updated_utc) + VALUES (?, ?, ?) + ON CONFLICT(key) DO UPDATE SET + value_json=excluded.value_json, + last_updated_utc=excluded.last_updated_utc;", + kind, + value, + timestamp + ) + .execute(&mut tx) + .await + .map_err(|err| { + tracing::error!("Failed to insert data for {kind}: {err}, aborting transaction",); + err + })?; + } + + Ok(()) +} + +/// For ``, `summary_history` is updated with fresh data on every +/// iteration. +/// +/// After UTC midnight, summary is inserted for `` and last entry for +/// `` stays there forever. +/// +/// This is not aggregate data, it's a set of latest data points +async fn insert_summary_history( + pool: &DbPool, + summary: &NetworkSummary, + last_updated: DateTime, +) -> anyhow::Result<()> { + let mut conn = pool.acquire().await?; + + let value_json = serde_json::to_string(&summary)?; + let timestamp = last_updated.timestamp(); + let now_rfc3339 = last_updated.to_rfc3339(); + // YYYY-MM-DD, without time + let date = &now_rfc3339[..10]; + + sqlx::query!( + "INSERT INTO summary_history + (date, timestamp_utc, value_json) + VALUES (?, ?, ?) + ON CONFLICT(date) DO UPDATE SET + timestamp_utc=excluded.timestamp_utc, + value_json=excluded.value_json;", + date, + timestamp, + value_json + ) + .execute(&mut *conn) + .await?; + + Ok(()) +} diff --git a/nym-node-status-api/src/db/queries/mixnodes.rs b/nym-node-status-api/src/db/queries/mixnodes.rs new file mode 100644 index 00000000000..aed48ed4d79 --- /dev/null +++ b/nym-node-status-api/src/db/queries/mixnodes.rs @@ -0,0 +1,101 @@ +use futures_util::TryStreamExt; +use nym_validator_client::models::MixNodeBondAnnotated; + +use crate::db::{ + models::{BondedStatusDto, MixnodeRecord}, + DbPool, +}; + +pub(crate) async fn insert_mixnodes( + pool: &DbPool, + mixnodes: Vec, +) -> anyhow::Result<()> { + let mut conn = pool.acquire().await?; + + for record in mixnodes.iter() { + // https://www.sqlite.org/lang_upsert.html + sqlx::query!( + "INSERT INTO mixnodes + (mix_id, identity_key, bonded, total_stake, + host, http_api_port, blacklisted, full_details, + self_described, last_updated_utc, is_dp_delegatee) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT(mix_id) DO UPDATE SET + bonded=excluded.bonded, + total_stake=excluded.total_stake, host=excluded.host, + http_api_port=excluded.http_api_port,blacklisted=excluded.blacklisted, + full_details=excluded.full_details,self_described=excluded.self_described, + last_updated_utc=excluded.last_updated_utc, + is_dp_delegatee = excluded.is_dp_delegatee;", + record.mix_id, + record.identity_key, + record.bonded, + record.total_stake, + record.host, + record.http_port, + record.blacklisted, + record.full_details, + record.self_described, + record.last_updated_utc, + record.is_dp_delegatee + ) + .execute(&mut *conn) + .await?; + } + + Ok(()) +} + +/// Ensure all mixnodes that are set as bonded, are still bonded +pub(crate) async fn ensure_mixnodes_still_bonded( + pool: &DbPool, + mixnodes: &[MixNodeBondAnnotated], +) -> anyhow::Result { + let bonded_mixnodes_rows = get_all_bonded_mixnodes_row_ids_by_status(pool, true).await?; + let unbonded_mixnodes_rows = bonded_mixnodes_rows.iter().filter(|v| { + !mixnodes + .iter() + .any(|bonded| *bonded.mixnode_details.bond_information.identity() == v.identity_key) + }); + + let recently_unbonded_mixnodes = unbonded_mixnodes_rows.to_owned().count(); + let last_updated_utc = chrono::offset::Utc::now().timestamp(); + let mut transaction = pool.begin().await?; + for row in unbonded_mixnodes_rows { + sqlx::query!( + "UPDATE mixnodes + SET bonded = ?, last_updated_utc = ? + WHERE id = ?;", + false, + last_updated_utc, + row.id, + ) + .execute(&mut *transaction) + .await?; + } + transaction.commit().await?; + + Ok(recently_unbonded_mixnodes) +} + +async fn get_all_bonded_mixnodes_row_ids_by_status( + pool: &DbPool, + status: bool, +) -> anyhow::Result> { + let mut conn = pool.acquire().await?; + let items = sqlx::query_as!( + BondedStatusDto, + r#"SELECT + id as "id!", + identity_key as "identity_key!", + bonded as "bonded: bool" + FROM mixnodes + WHERE bonded = ?"#, + status, + ) + .fetch(&mut *conn) + .try_collect::>() + .await?; + + Ok(items) +} diff --git a/nym-node-status-api/src/db/queries/mod.rs b/nym-node-status-api/src/db/queries/mod.rs new file mode 100644 index 00000000000..38f1faab611 --- /dev/null +++ b/nym-node-status-api/src/db/queries/mod.rs @@ -0,0 +1,9 @@ +mod gateways; +mod misc; +mod mixnodes; + +pub(crate) use gateways::{ + ensure_gateways_still_bonded, insert_gateways, write_blacklisted_gateways_to_db, +}; +pub(crate) use misc::insert_summaries; +pub(crate) use mixnodes::{ensure_mixnodes_still_bonded, insert_mixnodes}; diff --git a/nym-node-status-api/src/logging.rs b/nym-node-status-api/src/logging.rs new file mode 100644 index 00000000000..d61cd78ee18 --- /dev/null +++ b/nym-node-status-api/src/logging.rs @@ -0,0 +1,41 @@ +use tracing::level_filters::LevelFilter; +use tracing_subscriber::{filter::Directive, EnvFilter}; + +pub(crate) fn setup_tracing_logger() { + fn directive_checked(directive: String) -> Directive { + directive.parse().expect("Failed to parse log directive") + } + + let log_builder = tracing_subscriber::fmt() + // Use a more compact, abbreviated log format + .compact() + // Display source code file paths + .with_file(true) + // Display source code line numbers + .with_line_number(true) + // Don't display the event's target (module path) + .with_target(false); + + let mut filter = EnvFilter::builder() + // if RUST_LOG isn't set, set default level + .with_default_directive(LevelFilter::INFO.into()) + .from_env_lossy(); + // these crates are more granularly filtered + let filter_crates = [ + "nym_bin_common", + "nym_explorer_client", + "nym_network_defaults", + "nym_validator_client", + "reqwest", + "rustls", + "hyper", + "sqlx", + "h2", + "tendermint_rpc", + ]; + for crate_name in filter_crates { + filter = filter.add_directive(directive_checked(format!("{}=warn", crate_name))); + } + + log_builder.with_env_filter(filter).init(); +} diff --git a/nym-node-status-api/src/main.rs b/nym-node-status-api/src/main.rs new file mode 100644 index 00000000000..6296406701a --- /dev/null +++ b/nym-node-status-api/src/main.rs @@ -0,0 +1,38 @@ +use anyhow::anyhow; +use clap::Parser; +use nym_network_defaults::setup_env; + +mod cli; +mod db; +mod logging; +mod monitor; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + logging::setup_tracing_logger(); + + let args = cli::Cli::parse(); + // if dotenv file is present, load its values + // otherwise, default to mainnet + setup_env(args.config_env_file.as_ref()); + tracing::debug!("{:?}", std::env::var("NETWORK_NAME")); + tracing::debug!("{:?}", std::env::var("EXPLORER_API")); + tracing::debug!("{:?}", std::env::var("NYM_API")); + + let storage = db::Storage::init().await?; + monitor::spawn_in_background(storage) + .await + .expect("Monitor task failed"); + tracing::info!("Started server"); + + Ok(()) +} + +pub(crate) fn read_env_var(env_var: &str) -> anyhow::Result { + std::env::var(env_var) + .map(|value| { + tracing::trace!("{}={}", env_var, value); + value + }) + .map_err(|_| anyhow!("You need to set {}", env_var)) +} diff --git a/nym-node-status-api/src/monitor/mod.rs b/nym-node-status-api/src/monitor/mod.rs new file mode 100644 index 00000000000..810320bdd0c --- /dev/null +++ b/nym-node-status-api/src/monitor/mod.rs @@ -0,0 +1,374 @@ +use crate::db::models::{ + gateway, mixnode, GatewayRecord, MixnodeRecord, NetworkSummary, GATEWAYS_BLACKLISTED_COUNT, + GATEWAYS_BONDED_COUNT, GATEWAYS_EXPLORER_COUNT, GATEWAYS_HISTORICAL_COUNT, + MIXNODES_BLACKLISTED_COUNT, MIXNODES_BONDED_ACTIVE, MIXNODES_BONDED_COUNT, + MIXNODES_BONDED_INACTIVE, MIXNODES_BONDED_RESERVE, MIXNODES_HISTORICAL_COUNT, +}; +use crate::db::{queries, DbPool, Storage}; +use anyhow::anyhow; +use cosmwasm_std::Decimal; +use nym_explorer_client::{ExplorerClient, PrettyDetailedGatewayBond}; +use nym_network_defaults::NymNetworkDetails; +use nym_validator_client::client::NymApiClientExt; +use nym_validator_client::models::{DescribedGateway, DescribedMixNode, MixNodeBondAnnotated}; +use nym_validator_client::nym_nodes::SkimmedNode; +use nym_validator_client::nyxd::contract_traits::PagedMixnetQueryClient; +use nym_validator_client::nyxd::{AccountId, NyxdClient}; +use nym_validator_client::NymApiClient; +use std::collections::HashSet; +use std::str::FromStr; +use tokio::task::JoinHandle; +use tokio::time::Duration; + +const REFRESH_DELAY: Duration = Duration::from_secs(60 * 5); +const FAILURE_RETRY_DELAY: Duration = Duration::from_secs(60); +static DELEGATION_PROGRAM_WALLET: &str = "n1rnxpdpx3kldygsklfft0gech7fhfcux4zst5lw"; + +// TODO dz: query many NYM APIs: +// multiple instances running directory cache, ask sachin +pub(crate) fn spawn_in_background(storage: Storage) -> JoinHandle<()> { + tokio::spawn(async move { + let db_pool = storage.pool().await; + let network_defaults = nym_network_defaults::NymNetworkDetails::new_from_env(); + + loop { + tracing::info!("Refreshing node info..."); + + if let Err(e) = run(db_pool, &network_defaults).await { + tracing::error!( + "Monitor run failed: {e}, retrying in {}s...", + FAILURE_RETRY_DELAY.as_secs() + ); + tokio::time::sleep(FAILURE_RETRY_DELAY).await; + } else { + tracing::info!( + "Info successfully collected, sleeping for {}s...", + REFRESH_DELAY.as_secs() + ); + tokio::time::sleep(REFRESH_DELAY).await; + } + } + }) +} + +async fn run(pool: &DbPool, network_details: &NymNetworkDetails) -> anyhow::Result<()> { + let default_api_url = network_details + .endpoints + .first() + .expect("rust sdk mainnet default incorrectly configured") + .api_url() + .clone() + .expect("rust sdk mainnet default missing api_url"); + let default_explorer_url = network_details.explorer_api.clone().map(|url| { + url.parse() + .expect("rust sdk mainnet default explorer url not parseable") + }); + + let default_explorer_url = + default_explorer_url.expect("explorer url missing in network config"); + let explorer_client = ExplorerClient::new(default_explorer_url)?; + let explorer_gateways = explorer_client.get_gateways().await?; + + let api_client = NymApiClient::new(default_api_url); + let gateways = api_client.get_cached_described_gateways().await?; + tracing::debug!("Fetched {} gateways", gateways.len()); + let skimmed_gateways = api_client.get_basic_gateways(None).await?; + + let mixnodes = api_client.get_cached_mixnodes().await?; + tracing::debug!("Fetched {} mixnodes", mixnodes.len()); + + // TODO dz can we calculate blacklisted GWs from their performance? + // where do we get their performance? + let gateways_blacklisted = api_client + .nym_api + .get_gateways_blacklisted() + .await + .map(|vec| vec.into_iter().collect::>())?; + + // Cached mixnodes don't include blacklisted nodes + // We need that to calculate the total locked tokens later + let mixnodes = api_client + .nym_api + .get_mixnodes_detailed_unfiltered() + .await?; + let mixnodes_described = api_client.nym_api.get_mixnodes_described().await?; + let mixnodes_active = api_client.nym_api.get_active_mixnodes().await?; + let delegation_program_members = get_delegation_program_details(network_details).await?; + + // keep stats for later + let count_bonded_mixnodes = mixnodes.len(); + let count_bonded_gateways = gateways.len(); + let count_explorer_gateways = explorer_gateways.len(); + let count_bonded_mixnodes_active = mixnodes_active.len(); + + let gateway_records = prepare_gateway_data( + &gateways, + &gateways_blacklisted, + explorer_gateways, + skimmed_gateways, + )?; + queries::insert_gateways(pool, gateway_records) + .await + .map(|_| { + tracing::debug!("Gateway info written to DB!"); + })?; + + // instead of counting blacklisted GWs returned from API cache, count from the active set + let count_gateways_blacklisted = gateways + .iter() + .filter(|gw| { + let gw_identity = gw.bond.identity(); + gateways_blacklisted.contains(gw_identity) + }) + .count(); + + if count_gateways_blacklisted > 0 { + queries::write_blacklisted_gateways_to_db(pool, gateways_blacklisted.iter()) + .await + .map(|_| { + tracing::debug!( + "Gateway blacklist info written to DB! {} blacklisted by Nym API", + count_gateways_blacklisted + ) + })?; + } + + let mixnode_records = + prepare_mixnode_data(&mixnodes, mixnodes_described, delegation_program_members)?; + queries::insert_mixnodes(pool, mixnode_records) + .await + .map(|_| { + tracing::debug!("Mixnode info written to DB!"); + })?; + + let count_mixnodes_blacklisted = mixnodes.iter().filter(|elem| elem.blacklisted).count(); + + let recently_unbonded_gateways = queries::ensure_gateways_still_bonded(pool, &gateways).await?; + let recently_unbonded_mixnodes = queries::ensure_mixnodes_still_bonded(pool, &mixnodes).await?; + + let count_bonded_mixnodes_reserve = 0; // TODO: NymAPI doesn't report the reserve set size + let count_bonded_mixnodes_inactive = count_bonded_mixnodes - count_bonded_mixnodes_active; + + let (all_historical_gateways, all_historical_mixnodes) = calculate_stats(pool).await?; + + // + // write summary keys and values to table + // + + let nodes_summary = vec![ + (MIXNODES_BONDED_COUNT, &count_bonded_mixnodes), + (MIXNODES_BONDED_ACTIVE, &count_bonded_mixnodes_active), + (MIXNODES_BONDED_INACTIVE, &count_bonded_mixnodes_inactive), + (MIXNODES_BONDED_RESERVE, &count_bonded_mixnodes_reserve), + (MIXNODES_BLACKLISTED_COUNT, &count_mixnodes_blacklisted), + (GATEWAYS_BONDED_COUNT, &count_bonded_gateways), + (GATEWAYS_EXPLORER_COUNT, &count_explorer_gateways), + (MIXNODES_HISTORICAL_COUNT, &all_historical_mixnodes), + (GATEWAYS_HISTORICAL_COUNT, &all_historical_gateways), + (GATEWAYS_BLACKLISTED_COUNT, &count_gateways_blacklisted), + ]; + + // TODO dz do we need signed int in type definition? maybe because of API? + let last_updated = chrono::offset::Utc::now(); + let last_updated_utc = last_updated.timestamp().to_string(); + let network_summary = NetworkSummary { + mixnodes: mixnode::MixnodeSummary { + bonded: mixnode::MixnodeSummaryBonded { + count: count_bonded_mixnodes as i32, + active: count_bonded_mixnodes_active as i32, + inactive: count_bonded_mixnodes_inactive as i32, + reserve: count_bonded_mixnodes_reserve as i32, + last_updated_utc: last_updated_utc.to_owned(), + }, + blacklisted: mixnode::MixnodeSummaryBlacklisted { + count: count_mixnodes_blacklisted as i32, + last_updated_utc: last_updated_utc.to_owned(), + }, + historical: mixnode::MixnodeSummaryHistorical { + count: all_historical_mixnodes as i32, + last_updated_utc: last_updated_utc.to_owned(), + }, + }, + gateways: gateway::GatewaySummary { + bonded: gateway::GatewaySummaryBonded { + count: count_bonded_gateways as i32, + last_updated_utc: last_updated_utc.to_owned(), + }, + blacklisted: gateway::GatewaySummaryBlacklisted { + count: count_gateways_blacklisted as i32, + last_updated_utc: last_updated_utc.to_owned(), + }, + historical: gateway::GatewaySummaryHistorical { + count: all_historical_gateways as i32, + last_updated_utc: last_updated_utc.to_owned(), + }, + explorer: gateway::GatewaySummaryExplorer { + count: count_explorer_gateways as i32, + last_updated_utc: last_updated_utc.to_owned(), + }, + }, + }; + + queries::insert_summaries(pool, &nodes_summary, &network_summary, last_updated).await?; + + let mut log_lines: Vec = vec![]; + for (key, value) in nodes_summary.iter() { + log_lines.push(format!("{} = {}", key, value)); + } + log_lines.push(format!( + "recently_unbonded_mixnodes = {}", + recently_unbonded_mixnodes + )); + log_lines.push(format!( + "recently_unbonded_gateways = {}", + recently_unbonded_gateways + )); + + tracing::info!("Directory summary: \n{}", log_lines.join("\n")); + + Ok(()) +} + +fn prepare_gateway_data( + gateways: &[DescribedGateway], + gateways_blacklisted: &HashSet, + explorer_gateways: Vec, + skimmed_gateways: Vec, +) -> anyhow::Result> { + let mut gateway_records = Vec::new(); + + for gateway in gateways { + let identity_key = gateway.bond.identity(); + let bonded = true; + let last_updated_utc = chrono::offset::Utc::now().timestamp(); + let blacklisted = gateways_blacklisted.contains(identity_key); + + let self_described = gateway + .self_described + .as_ref() + .and_then(|v| serde_json::to_string(&v).ok()); + + let explorer_pretty_bond = explorer_gateways + .iter() + .find(|g| g.gateway.identity_key.eq(identity_key)); + let explorer_pretty_bond = explorer_pretty_bond.and_then(|g| serde_json::to_string(g).ok()); + + let performance = skimmed_gateways + .iter() + .find(|g| g.ed25519_identity_pubkey.eq(identity_key)) + .map(|g| g.performance) + .unwrap_or_default() + .round_to_integer(); + + gateway_records.push(GatewayRecord { + identity_key: identity_key.to_owned(), + bonded, + blacklisted, + self_described, + explorer_pretty_bond, + last_updated_utc, + performance, + }); + } + + Ok(gateway_records) +} + +fn prepare_mixnode_data( + mixnodes: &[MixNodeBondAnnotated], + mixnodes_described: Vec, + delegation_program_members: Vec, +) -> anyhow::Result> { + let mut mixnode_records = Vec::new(); + + for mixnode in mixnodes { + let mix_id = mixnode.mix_id(); + let identity_key = mixnode.identity_key(); + let bonded = true; + let total_stake = decimal_to_i64(mixnode.mixnode_details.total_stake()); + let blacklisted = mixnode.blacklisted; + let node_info = mixnode.mix_node(); + let host = node_info.host.clone(); + let http_port = node_info.http_api_port; + // Contains all the information including what's above + let full_details = serde_json::to_string(&mixnode)?; + + let mixnode_described = mixnodes_described.iter().find(|m| m.bond.mix_id == mix_id); + let self_described = mixnode_described.and_then(|v| serde_json::to_string(v).ok()); + let is_dp_delegatee = delegation_program_members.contains(&mix_id); + + let last_updated_utc = chrono::offset::Utc::now().timestamp(); + + mixnode_records.push(MixnodeRecord { + mix_id, + identity_key: identity_key.to_owned(), + bonded, + total_stake, + host, + http_port, + blacklisted, + full_details, + self_described, + last_updated_utc, + is_dp_delegatee, + }); + } + + Ok(mixnode_records) +} + +async fn calculate_stats(pool: &DbPool) -> anyhow::Result<(usize, usize)> { + let mut conn = pool.acquire().await?; + + let all_historical_gateways = sqlx::query_scalar!(r#"SELECT count(id) FROM gateways"#) + .fetch_one(&mut *conn) + .await? as usize; + + let all_historical_mixnodes = sqlx::query_scalar!(r#"SELECT count(id) FROM mixnodes"#) + .fetch_one(&mut *conn) + .await? as usize; + + Ok((all_historical_gateways, all_historical_mixnodes)) +} + +async fn get_delegation_program_details( + network_details: &NymNetworkDetails, +) -> anyhow::Result> { + let config = nym_validator_client::nyxd::Config::try_from_nym_network_details(network_details)?; + + // TODO dz should this be configurable? + let client = NyxdClient::connect(config, "https://rpc.nymtech.net") + .map_err(|err| anyhow::anyhow!("Couldn't connect: {}", err))?; + + let account_id = AccountId::from_str(DELEGATION_PROGRAM_WALLET) + .map_err(|e| anyhow!("Invalid bech32 address: {}", e))?; + + let delegations = client.get_all_delegator_delegations(&account_id).await?; + + let mix_ids: Vec = delegations + .iter() + .map(|delegation| delegation.mix_id) + .collect(); + + Ok(mix_ids) +} + +fn decimal_to_i64(decimal: Decimal) -> i64 { + // Convert the underlying Uint128 to a u128 + let atomics = decimal.atomics().u128(); + let precision = 1_000_000_000_000_000_000u128; + + // Get the fractional part + let fractional = atomics % precision; + + // Get the integer part + let integer = atomics / precision; + + // Combine them into a float + let float_value = integer as f64 + (fractional as f64 / 1_000_000_000_000_000_000_f64); + + // Limit to 6 decimal places + let rounded_value = (float_value * 1_000_000.0).round() / 1_000_000.0; + + rounded_value as i64 +} From 56c55f6b95fab734e49f7f146a57e389c2d9f46a Mon Sep 17 00:00:00 2001 From: Dinko Zdravac <173912580+dynco-nym@users.noreply.github.com> Date: Wed, 9 Oct 2024 11:33:12 +0200 Subject: [PATCH 2/6] Working HTTP server (#4941) * Server file structure * Create HTTP server - graceful shutdown - routes - logging, CORS * gateways WIP * gateways API + swagger docs complete * Mixnodes API + swagger docs complete * Services API + swagger docs complete * Commit summary insert * Make troubleshooting DB easier * Summary API + swagger docs * Client log changes * QOL improvements - remove implicit panics via `as` - safer DTO conversions - add logging - new config --- Cargo.lock | 180 +++++++++++++- Cargo.toml | 4 + .../client-libs/validator-client/Cargo.toml | 2 +- .../validator-client/src/client.rs | 7 + .../validator-client/src/connection_tester.rs | 18 +- .../validator-client/src/nym_api/mod.rs | 42 ++++ .../nyxd/contract_traits/dkg_query_client.rs | 2 +- .../client_traits/query_client.rs | 7 +- .../client_traits/signing_client.rs | 2 +- .../src/nyxd/cosmwasm_client/helpers.rs | 2 +- common/http-api-client/src/lib.rs | 7 +- explorer-api/explorer-client/Cargo.toml | 2 +- explorer-api/explorer-client/src/lib.rs | 12 +- nym-node-status-api/.gitignore | 1 + nym-node-status-api/Cargo.toml | 20 +- nym-node-status-api/build.rs | 13 + nym-node-status-api/launch_node_status_api.sh | 60 +---- nym-node-status-api/migrations/000_init.sql | 41 ++++ nym-node-status-api/src/config.rs | 79 +++++++ nym-node-status-api/src/db/mod.rs | 10 +- nym-node-status-api/src/db/models.rs | 196 ++++++++++++++- .../src/db/queries/gateways.rs | 50 +++- nym-node-status-api/src/db/queries/misc.rs | 2 + .../src/db/queries/mixnodes.rs | 82 ++++++- nym-node-status-api/src/db/queries/mod.rs | 9 +- nym-node-status-api/src/db/queries/summary.rs | 209 ++++++++++++++++ nym-node-status-api/src/http/api/gateways.rs | 110 +++++++++ nym-node-status-api/src/http/api/mixnodes.rs | 91 +++++++ nym-node-status-api/src/http/api/mod.rs | 85 +++++++ .../src/http/api/services/json_path.rs | 58 +++++ .../src/http/api/services/mod.rs | 134 +++++++++++ nym-node-status-api/src/http/api/summary.rs | 43 ++++ nym-node-status-api/src/http/api/testruns.rs | 7 + nym-node-status-api/src/http/api_docs.rs | 15 ++ nym-node-status-api/src/http/error.rs | 28 +++ nym-node-status-api/src/http/mod.rs | 71 ++++++ nym-node-status-api/src/http/models.rs | 74 ++++++ nym-node-status-api/src/http/server.rs | 92 ++++++++ nym-node-status-api/src/http/state.rs | 223 ++++++++++++++++++ nym-node-status-api/src/logging.rs | 19 +- nym-node-status-api/src/main.rs | 50 ++-- nym-node-status-api/src/monitor/mod.rs | 178 ++++++++++---- 42 files changed, 2152 insertions(+), 185 deletions(-) create mode 100644 nym-node-status-api/src/config.rs create mode 100644 nym-node-status-api/src/db/queries/summary.rs create mode 100644 nym-node-status-api/src/http/api/gateways.rs create mode 100644 nym-node-status-api/src/http/api/mixnodes.rs create mode 100644 nym-node-status-api/src/http/api/mod.rs create mode 100644 nym-node-status-api/src/http/api/services/json_path.rs create mode 100644 nym-node-status-api/src/http/api/services/mod.rs create mode 100644 nym-node-status-api/src/http/api/summary.rs create mode 100644 nym-node-status-api/src/http/api/testruns.rs create mode 100644 nym-node-status-api/src/http/api_docs.rs create mode 100644 nym-node-status-api/src/http/error.rs create mode 100644 nym-node-status-api/src/http/mod.rs create mode 100644 nym-node-status-api/src/http/models.rs create mode 100644 nym-node-status-api/src/http/server.rs create mode 100644 nym-node-status-api/src/http/state.rs diff --git a/Cargo.lock b/Cargo.lock index a1b8a47c19c..0cab5a15ee1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -318,10 +318,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" dependencies = [ "concurrent-queue", - "event-listener", + "event-listener 2.5.3", "futures-core", ] +[[package]] +name = "async-lock" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18" +dependencies = [ + "event-listener 5.3.1", + "event-listener-strategy", + "pin-project-lite", +] + [[package]] name = "async-stream" version = "0.3.5" @@ -2320,6 +2331,15 @@ dependencies = [ "termcolor", ] +[[package]] +name = "envy" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f47e0157f2cb54f5ae1bd371b30a2ae4311e1c028f575cd4e81de7353215965" +dependencies = [ + "serde", +] + [[package]] name = "equivalent" version = "1.0.1" @@ -2362,6 +2382,27 @@ version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" +[[package]] +name = "event-listener" +version = "5.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f214dc438f977e6d4e3500aaa277f5ad94ca83fbbd9b1a15713ce2344ccc5a1" +dependencies = [ + "event-listener 5.3.1", + "pin-project-lite", +] + [[package]] name = "explorer-api" version = "1.1.41" @@ -3572,7 +3613,7 @@ dependencies = [ "crossbeam-utils", "curl", "curl-sys", - "event-listener", + "event-listener 2.5.3", "futures-lite", "http 0.2.12", "log", @@ -4054,6 +4095,30 @@ dependencies = [ "wasm-utils", ] +[[package]] +name = "moka" +version = "0.12.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32cf62eb4dd975d2dde76432fb1075c49e3ee2331cf36f1f8fd4b66550d32b6f" +dependencies = [ + "async-lock", + "async-trait", + "crossbeam-channel", + "crossbeam-epoch", + "crossbeam-utils", + "event-listener 5.3.1", + "futures-util", + "once_cell", + "parking_lot", + "quanta", + "rustc_version 0.4.0", + "smallvec", + "tagptr", + "thiserror", + "triomphe", + "uuid", +] + [[package]] name = "multer" version = "2.1.0" @@ -5208,12 +5273,12 @@ dependencies = [ name = "nym-explorer-client" version = "0.1.0" dependencies = [ - "log", "nym-explorer-api-requests", "reqwest 0.12.4", "serde", "thiserror", "tokio", + "tracing", "url", ] @@ -5883,22 +5948,34 @@ name = "nym-node-status-api" version = "0.1.0" dependencies = [ "anyhow", - "axum 0.7.5", + "axum 0.7.7", "chrono", - "clap 4.5.17", + "clap 4.5.20", "cosmwasm-std", + "envy", "futures-util", + "moka", "nym-bin-common", "nym-explorer-client", "nym-network-defaults", + "nym-node-requests", + "nym-task", "nym-validator-client", + "reqwest 0.12.4", "serde", "serde_json", + "serde_json_path", "sqlx", "thiserror", "tokio", + "tokio-util", + "tower-http", "tracing", + "tracing-log 0.2.0", "tracing-subscriber", + "utoipa", + "utoipa-swagger-ui", + "utoipauto", ] [[package]] @@ -6495,7 +6572,6 @@ dependencies = [ "flate2", "futures", "itertools 0.13.0", - "log", "nym-api-requests", "nym-coconut-bandwidth-contract-common", "nym-coconut-dkg-common", @@ -6519,6 +6595,7 @@ dependencies = [ "thiserror", "time", "tokio", + "tracing", "ts-rs", "url", "wasmtimer", @@ -7428,6 +7505,21 @@ dependencies = [ "psl-types", ] +[[package]] +name = "quanta" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5167a477619228a0b284fac2674e3c388cba90631d7b7de620e6f1fcd08da5" +dependencies = [ + "crossbeam-utils", + "libc", + "once_cell", + "raw-cpuid", + "wasi", + "web-sys", + "winapi", +] + [[package]] name = "quick-error" version = "1.2.3" @@ -7519,6 +7611,15 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "raw-cpuid" +version = "11.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb9ee317cfe3fbd54b36a511efc1edd42e216903c9cd575e686dd68a2ba90d8d" +dependencies = [ + "bitflags 2.5.0", +] + [[package]] name = "rayon" version = "1.10.0" @@ -8378,6 +8479,59 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_json_path" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bc0207b6351893eafa1e39aa9aea452abb6425ca7b02dd64faf29109e7a33ba" +dependencies = [ + "inventory", + "nom", + "once_cell", + "regex", + "serde", + "serde_json", + "serde_json_path_core", + "serde_json_path_macros", + "thiserror", +] + +[[package]] +name = "serde_json_path_core" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3d64fe53ce1aaa31bea2b2b46d3b6ab6a37e61854bedcbd9f174e188f3f7d79" +dependencies = [ + "inventory", + "once_cell", + "serde", + "serde_json", + "thiserror", +] + +[[package]] +name = "serde_json_path_macros" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a31e8177a443fd3e94917f12946ae7891dfb656e6d4c5e79b8c5d202fbcb723" +dependencies = [ + "inventory", + "once_cell", + "serde_json_path_core", + "serde_json_path_macros_internal", +] + +[[package]] +name = "serde_json_path_macros_internal" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75dde5a1d2ed78dfc411fc45592f72d3694436524d3353683ecb3d22009731dc" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.82", +] + [[package]] name = "serde_path_to_error" version = "0.1.16" @@ -8742,7 +8896,7 @@ dependencies = [ "crc", "crossbeam-queue", "either", - "event-listener", + "event-listener 2.5.3", "futures-channel", "futures-core", "futures-intrusive", @@ -9152,6 +9306,12 @@ dependencies = [ "libc", ] +[[package]] +name = "tagptr" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" + [[package]] name = "tap" version = "1.0.1" @@ -9924,6 +10084,12 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "triomphe" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "859eb650cfee7434994602c3a68b25d77ad9e68c8a6cd491616ef86661382eb3" + [[package]] name = "try-lock" version = "0.2.5" diff --git a/Cargo.toml b/Cargo.toml index 8f2bc57f13f..68c9476f5ee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -235,6 +235,7 @@ dotenvy = "0.15.6" ecdsa = "0.16" ed25519-dalek = "2.1" etherparse = "0.13.0" +envy = "0.4" eyre = "0.6.9" fastrand = "2.1.1" flate2 = "1.0.34" @@ -269,6 +270,7 @@ ledger-transport-hid = "0.10.0" log = "0.4" maxminddb = "0.23.0" mime = "0.3.17" +moka = { version = "0.12", features = ["future"] } nix = "0.27.1" notify = "5.1.0" okapi = "0.7.0" @@ -302,6 +304,7 @@ serde = "1.0.211" serde_bytes = "0.11.15" serde_derive = "1.0" serde_json = "1.0.132" +serde_json_path = "0.6.7" serde_repr = "0.1" serde_with = "3.9.0" serde_yaml = "0.9.25" @@ -331,6 +334,7 @@ tracing = "0.1.37" tracing-opentelemetry = "0.19.0" tracing-subscriber = "0.3.16" tracing-tree = "0.2.2" +tracing-log = "0.2" ts-rs = "10.0.0" tungstenite = { version = "0.20.1", default-features = false } url = "2.5" diff --git a/common/client-libs/validator-client/Cargo.toml b/common/client-libs/validator-client/Cargo.toml index d17b100672e..5cd86e2dba4 100644 --- a/common/client-libs/validator-client/Cargo.toml +++ b/common/client-libs/validator-client/Cargo.toml @@ -25,7 +25,7 @@ serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } nym-http-api-client = { path = "../../../common/http-api-client" } thiserror = { workspace = true } -log = { workspace = true } +tracing = { workspace = true } url = { workspace = true, features = ["serde"] } tokio = { workspace = true, features = ["sync", "time"] } time = { workspace = true, features = ["formatting"] } diff --git a/common/client-libs/validator-client/src/client.rs b/common/client-libs/validator-client/src/client.rs index b8375abdbd4..472a6a99330 100644 --- a/common/client-libs/validator-client/src/client.rs +++ b/common/client-libs/validator-client/src/client.rs @@ -265,6 +265,13 @@ impl NymApiClient { NymApiClient { nym_api } } + #[cfg(not(target_arch = "wasm32"))] + pub fn new_with_timeout(api_url: Url, timeout: std::time::Duration) -> Self { + let nym_api = nym_api::Client::new(api_url, Some(timeout)); + + NymApiClient { nym_api } + } + pub fn new_with_user_agent(api_url: Url, user_agent: UserAgent) -> Self { let nym_api = nym_api::Client::builder::<_, ValidatorClientError>(api_url) .expect("invalid api url") diff --git a/common/client-libs/validator-client/src/connection_tester.rs b/common/client-libs/validator-client/src/connection_tester.rs index 15d2efe2509..c9b4b3e435f 100644 --- a/common/client-libs/validator-client/src/connection_tester.rs +++ b/common/client-libs/validator-client/src/connection_tester.rs @@ -121,36 +121,36 @@ async fn test_nyxd_connection( { Ok(Err(NyxdError::TendermintErrorRpc(e))) => { // If we get a tendermint-rpc error, we classify the node as not contactable - log::warn!("Checking: nyxd url: {url}: {}: {}", "failed".red(), e); + tracing::warn!("Checking: nyxd url: {url}: {}: {}", "failed".red(), e); false } Ok(Err(NyxdError::AbciError { code, log, .. })) => { // We accept the mixnet contract not found as ok from a connection standpoint. This happens // for example on a pre-launch network. - log::debug!( + tracing::debug!( "Checking: nyxd url: {url}: {}, but with abci error: {code}: {log}", "success".green() ); code == 18 } Ok(Err(error @ NyxdError::NoContractAddressAvailable(_))) => { - log::warn!("Checking: nyxd url: {url}: {}: {error}", "failed".red()); + tracing::warn!("Checking: nyxd url: {url}: {}: {error}", "failed".red()); false } Ok(Err(e)) => { // For any other error, we're optimistic and just try anyway. - log::warn!( + tracing::warn!( "Checking: nyxd_url: {url}: {}, but with error: {e}", "success".green() ); true } Ok(Ok(_)) => { - log::debug!("Checking: nyxd_url: {url}: {}", "success".green()); + tracing::debug!("Checking: nyxd_url: {url}: {}", "success".green()); true } Err(e) => { - log::warn!("Checking: nyxd_url: {url}: {}: {e}", "failed".red()); + tracing::warn!("Checking: nyxd_url: {url}: {}: {e}", "failed".red()); false } }; @@ -169,15 +169,15 @@ async fn test_nym_api_connection( .await { Ok(Ok(_)) => { - log::debug!("Checking: api_url: {url}: {}", "success".green()); + tracing::debug!("Checking: api_url: {url}: {}", "success".green()); true } Ok(Err(e)) => { - log::debug!("Checking: api_url: {url}: {}: {e}", "failed".red()); + tracing::debug!("Checking: api_url: {url}: {}: {e}", "failed".red()); false } Err(e) => { - log::debug!("Checking: api_url: {url}: {}: {e}", "failed".red()); + tracing::debug!("Checking: api_url: {url}: {}: {e}", "failed".red()); false } }; diff --git a/common/client-libs/validator-client/src/nym_api/mod.rs b/common/client-libs/validator-client/src/nym_api/mod.rs index 9660e470c7b..2e5ce565f98 100644 --- a/common/client-libs/validator-client/src/nym_api/mod.rs +++ b/common/client-libs/validator-client/src/nym_api/mod.rs @@ -41,6 +41,7 @@ use nym_mixnet_contract_common::mixnode::MixNodeDetails; use nym_mixnet_contract_common::{GatewayBond, IdentityKeyRef, NodeId}; use time::format_description::BorrowedFormatItem; use time::Date; +use tracing::instrument; pub mod error; pub mod routes; @@ -52,11 +53,13 @@ pub fn rfc_3339_date() -> Vec> { #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] pub trait NymApiClientExt: ApiClient { + #[instrument(level = "debug", skip(self))] async fn get_mixnodes(&self) -> Result, NymAPIError> { self.get_json(&[routes::API_VERSION, routes::MIXNODES], NO_PARAMS) .await } + #[instrument(level = "debug", skip(self))] async fn get_mixnodes_detailed(&self) -> Result, NymAPIError> { self.get_json( &[ @@ -70,6 +73,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[instrument(level = "debug", skip(self))] async fn get_gateways_detailed(&self) -> Result, NymAPIError> { self.get_json( &[ @@ -83,6 +87,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[instrument(level = "debug", skip(self))] async fn get_mixnodes_detailed_unfiltered( &self, ) -> Result, NymAPIError> { @@ -98,11 +103,13 @@ pub trait NymApiClientExt: ApiClient { .await } + #[instrument(level = "debug", skip(self))] async fn get_gateways(&self) -> Result, NymAPIError> { self.get_json(&[routes::API_VERSION, routes::GATEWAYS], NO_PARAMS) .await } + #[instrument(level = "debug", skip(self))] async fn get_gateways_described(&self) -> Result, NymAPIError> { self.get_json( &[routes::API_VERSION, routes::GATEWAYS, routes::DESCRIBED], @@ -111,6 +118,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[instrument(level = "debug", skip(self))] async fn get_mixnodes_described(&self) -> Result, NymAPIError> { self.get_json( &[routes::API_VERSION, routes::MIXNODES, routes::DESCRIBED], @@ -119,6 +127,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[tracing::instrument(level = "debug", skip_all)] async fn get_basic_mixnodes( &self, semver_compatibility: Option, @@ -142,6 +151,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[instrument(level = "debug", skip(self))] async fn get_basic_gateways( &self, semver_compatibility: Option, @@ -167,6 +177,7 @@ pub trait NymApiClientExt: ApiClient { /// retrieve basic information for nodes are capable of operating as an entry gateway /// this includes legacy gateways and nym-nodes + #[instrument(level = "debug", skip(self))] async fn get_all_basic_entry_assigned_nodes( &self, semver_compatibility: Option, @@ -208,6 +219,7 @@ pub trait NymApiClientExt: ApiClient { /// retrieve basic information for nodes that got assigned 'mixing' node in this epoch /// this includes legacy mixnodes and nym-nodes + #[instrument(level = "debug", skip(self))] async fn get_basic_active_mixing_assigned_nodes( &self, semver_compatibility: Option, @@ -247,6 +259,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[instrument(level = "debug", skip(self))] async fn get_active_mixnodes(&self) -> Result, NymAPIError> { self.get_json( &[routes::API_VERSION, routes::MIXNODES, routes::ACTIVE], @@ -255,6 +268,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[instrument(level = "debug", skip(self))] async fn get_active_mixnodes_detailed(&self) -> Result, NymAPIError> { self.get_json( &[ @@ -269,6 +283,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[instrument(level = "debug", skip(self))] async fn get_rewarded_mixnodes(&self) -> Result, NymAPIError> { self.get_json( &[routes::API_VERSION, routes::MIXNODES, routes::REWARDED], @@ -277,6 +292,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[instrument(level = "debug", skip(self))] async fn get_mixnode_report( &self, mix_id: NodeId, @@ -294,6 +310,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[instrument(level = "debug", skip(self))] async fn get_gateway_report( &self, identity: IdentityKeyRef<'_>, @@ -311,6 +328,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[instrument(level = "debug", skip(self))] async fn get_mixnode_history( &self, mix_id: NodeId, @@ -328,6 +346,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[instrument(level = "debug", skip(self))] async fn get_gateway_history( &self, identity: IdentityKeyRef<'_>, @@ -345,6 +364,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[instrument(level = "debug", skip(self))] async fn get_rewarded_mixnodes_detailed( &self, ) -> Result, NymAPIError> { @@ -361,6 +381,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[instrument(level = "debug", skip(self))] async fn get_gateway_core_status_count( &self, identity: IdentityKeyRef<'_>, @@ -392,6 +413,7 @@ pub trait NymApiClientExt: ApiClient { } } + #[instrument(level = "debug", skip(self))] async fn get_mixnode_core_status_count( &self, mix_id: NodeId, @@ -424,6 +446,7 @@ pub trait NymApiClientExt: ApiClient { } } + #[instrument(level = "debug", skip(self))] async fn get_mixnode_status( &self, mix_id: NodeId, @@ -441,6 +464,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[instrument(level = "debug", skip(self))] async fn get_mixnode_reward_estimation( &self, mix_id: NodeId, @@ -458,6 +482,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[instrument(level = "debug", skip(self))] async fn compute_mixnode_reward_estimation( &self, mix_id: NodeId, @@ -477,6 +502,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[instrument(level = "debug", skip(self))] async fn get_mixnode_stake_saturation( &self, mix_id: NodeId, @@ -494,6 +520,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[instrument(level = "debug", skip(self))] async fn get_mixnode_inclusion_probability( &self, mix_id: NodeId, @@ -511,6 +538,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[instrument(level = "debug", skip(self))] async fn get_current_node_performance( &self, node_id: NodeId, @@ -541,6 +569,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[instrument(level = "debug", skip(self))] async fn get_mixnodes_blacklisted(&self) -> Result, NymAPIError> { self.get_json( &[routes::API_VERSION, routes::MIXNODES, routes::BLACKLISTED], @@ -549,6 +578,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[instrument(level = "debug", skip(self))] async fn get_gateways_blacklisted(&self) -> Result, NymAPIError> { self.get_json( &[routes::API_VERSION, routes::GATEWAYS, routes::BLACKLISTED], @@ -557,6 +587,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[instrument(level = "debug", skip(self, request_body))] async fn blind_sign( &self, request_body: &BlindSignRequestBody, @@ -573,6 +604,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[instrument(level = "debug", skip(self, request_body))] async fn verify_ecash_ticket( &self, request_body: &VerifyEcashTicketBody, @@ -589,6 +621,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[instrument(level = "debug", skip(self, request_body))] async fn batch_redeem_ecash_tickets( &self, request_body: &BatchRedeemTicketsBody, @@ -605,6 +638,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[instrument(level = "debug", skip(self))] async fn double_spending_filter_v1(&self) -> Result { self.get_json( &[ @@ -617,6 +651,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[instrument(level = "debug", skip(self))] async fn partial_expiration_date_signatures( &self, expiration_date: Option, @@ -640,6 +675,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[instrument(level = "debug", skip(self))] async fn partial_coin_indices_signatures( &self, epoch_id: Option, @@ -660,6 +696,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[instrument(level = "debug", skip(self))] async fn global_expiration_date_signatures( &self, expiration_date: Option, @@ -683,6 +720,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[instrument(level = "debug", skip(self))] async fn global_coin_indices_signatures( &self, epoch_id: Option, @@ -703,6 +741,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[instrument(level = "debug", skip(self))] async fn master_verification_key( &self, epoch_id: Option, @@ -722,6 +761,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[instrument(level = "debug", skip(self))] async fn epoch_credentials( &self, dkg_epoch: EpochId, @@ -738,6 +778,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[instrument(level = "debug", skip(self))] async fn issued_credential( &self, credential_id: i64, @@ -754,6 +795,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[instrument(level = "debug", skip(self))] async fn issued_credentials( &self, credential_ids: Vec, diff --git a/common/client-libs/validator-client/src/nyxd/contract_traits/dkg_query_client.rs b/common/client-libs/validator-client/src/nyxd/contract_traits/dkg_query_client.rs index 4d8fd3237cd..8674d7cb0a1 100644 --- a/common/client-libs/validator-client/src/nyxd/contract_traits/dkg_query_client.rs +++ b/common/client-libs/validator-client/src/nyxd/contract_traits/dkg_query_client.rs @@ -8,9 +8,9 @@ use crate::nyxd::CosmWasmClient; use async_trait::async_trait; use cosmrs::AccountId; use cosmwasm_std::Addr; -use log::trace; use nym_coconut_dkg_common::types::{ChunkIndex, NodeIndex, StateAdvanceResponse}; use serde::Deserialize; +use tracing::trace; use nym_coconut_dkg_common::dealer::RegisteredDealerDetails; pub use nym_coconut_dkg_common::{ diff --git a/common/client-libs/validator-client/src/nyxd/cosmwasm_client/client_traits/query_client.rs b/common/client-libs/validator-client/src/nyxd/cosmwasm_client/client_traits/query_client.rs index 5cea0a1ba25..8feceebdbf2 100644 --- a/common/client-libs/validator-client/src/nyxd/cosmwasm_client/client_traits/query_client.rs +++ b/common/client-libs/validator-client/src/nyxd/cosmwasm_client/client_traits/query_client.rs @@ -29,7 +29,6 @@ use cosmrs::proto::cosmwasm::wasm::v1::{ }; use cosmrs::tendermint::{block, chain, Hash}; use cosmrs::{AccountId, Coin as CosmosCoin, Tx}; -use log::trace; use prost::Message; use serde::{Deserialize, Serialize}; @@ -68,7 +67,7 @@ pub trait CosmWasmClient: TendermintRpcClient { Res: Message + Default, { if let Some(ref abci_path) = path { - trace!("performing query on abci path {abci_path}") + tracing::trace!("performing query on abci path {abci_path}") } let mut buf = Vec::with_capacity(req.encoded_len()); req.encode(&mut buf)?; @@ -297,7 +296,7 @@ pub trait CosmWasmClient: TendermintRpcClient { let start = Instant::now(); loop { - log::debug!( + tracing::debug!( "Polling for result of including {} in a block...", broadcasted.hash ); @@ -522,7 +521,7 @@ pub trait CosmWasmClient: TendermintRpcClient { .make_abci_query::<_, QuerySmartContractStateResponse>(path, req) .await?; - trace!("raw query response: {}", String::from_utf8_lossy(&res.data)); + tracing::trace!("raw query response: {}", String::from_utf8_lossy(&res.data)); Ok(serde_json::from_slice(&res.data)?) } diff --git a/common/client-libs/validator-client/src/nyxd/cosmwasm_client/client_traits/signing_client.rs b/common/client-libs/validator-client/src/nyxd/cosmwasm_client/client_traits/signing_client.rs index 8d0bb0fb4f6..cec29e9c50e 100644 --- a/common/client-libs/validator-client/src/nyxd/cosmwasm_client/client_traits/signing_client.rs +++ b/common/client-libs/validator-client/src/nyxd/cosmwasm_client/client_traits/signing_client.rs @@ -25,12 +25,12 @@ use cosmrs::proto::cosmos::tx::signing::v1beta1::SignMode; use cosmrs::staking::{MsgDelegate, MsgUndelegate}; use cosmrs::tx::{self, Msg}; use cosmrs::{cosmwasm, AccountId, Any, Tx}; -use log::debug; use serde::Serialize; use sha2::Digest; use sha2::Sha256; use std::time::SystemTime; use tendermint_rpc::endpoint::broadcast; +use tracing::debug; fn empty_fee() -> tx::Fee { tx::Fee { diff --git a/common/client-libs/validator-client/src/nyxd/cosmwasm_client/helpers.rs b/common/client-libs/validator-client/src/nyxd/cosmwasm_client/helpers.rs index d6e42daac78..559ad434a23 100644 --- a/common/client-libs/validator-client/src/nyxd/cosmwasm_client/helpers.rs +++ b/common/client-libs/validator-client/src/nyxd/cosmwasm_client/helpers.rs @@ -7,9 +7,9 @@ use base64::Engine; use cosmrs::abci::TxMsgData; use cosmrs::cosmwasm::MsgExecuteContractResponse; use cosmrs::proto::cosmos::base::query::v1beta1::{PageRequest, PageResponse}; -use log::error; use prost::bytes::Bytes; use tendermint_rpc::endpoint::broadcast; +use tracing::error; pub use cosmrs::abci::MsgResponse; diff --git a/common/http-api-client/src/lib.rs b/common/http-api-client/src/lib.rs index a8ad8e64d1c..c90d731adbd 100644 --- a/common/http-api-client/src/lib.rs +++ b/common/http-api-client/src/lib.rs @@ -9,7 +9,7 @@ use serde::{Deserialize, Serialize}; use std::fmt::Display; use std::time::Duration; use thiserror::Error; -use tracing::warn; +use tracing::{instrument, warn}; use url::Url; pub use reqwest::IntoUrl; @@ -202,6 +202,7 @@ impl Client { self.reqwest_client.get(url) } + #[instrument(level = "debug", skip_all, fields(path=?path))] async fn send_get_request( &self, path: PathSegments<'_>, @@ -212,6 +213,7 @@ impl Client { V: AsRef, E: Display, { + tracing::trace!("Sending GET request"); let url = sanitize_url(&self.base_url, path, params); #[cfg(target_arch = "wasm32")] @@ -277,6 +279,7 @@ impl Client { } } + #[instrument(level = "debug", skip_all)] pub async fn get_json( &self, path: PathSegments<'_>, @@ -512,12 +515,14 @@ pub fn sanitize_url, V: AsRef>( url } +#[tracing::instrument(level = "debug", skip_all)] pub async fn parse_response(res: Response, allow_empty: bool) -> Result> where T: DeserializeOwned, E: DeserializeOwned + Display, { let status = res.status(); + tracing::debug!("Status: {} (success: {})", &status, status.is_success()); if !allow_empty { if let Some(0) = res.content_length() { diff --git a/explorer-api/explorer-client/Cargo.toml b/explorer-api/explorer-client/Cargo.toml index 2397c83f7b5..d32429e9a17 100644 --- a/explorer-api/explorer-client/Cargo.toml +++ b/explorer-api/explorer-client/Cargo.toml @@ -7,12 +7,12 @@ license.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -log.workspace = true nym-explorer-api-requests = { path = "../explorer-api-requests" } reqwest = { workspace = true, features = ["json"] } serde.workspace = true thiserror.workspace = true url.workspace = true +tracing = {workspace = true, features = ["attributes"]} [dev-dependencies] tokio = { workspace = true, features = ["full"] } diff --git a/explorer-api/explorer-client/src/lib.rs b/explorer-api/explorer-client/src/lib.rs index 3ff4807b2b9..2415c90c690 100644 --- a/explorer-api/explorer-client/src/lib.rs +++ b/explorer-api/explorer-client/src/lib.rs @@ -3,6 +3,7 @@ use std::time::Duration; use reqwest::StatusCode; use thiserror::Error; +use tracing::instrument; use url::Url; // Re-export request types @@ -47,6 +48,12 @@ impl ExplorerClient { Ok(Self { client, url }) } + #[cfg(not(target_arch = "wasm32"))] + pub fn new_with_timeout(url: url::Url, timeout: Duration) -> Result { + let client = reqwest::Client::builder().timeout(timeout).build()?; + Ok(Self { client, url }) + } + #[cfg(target_arch = "wasm32")] pub fn new(url: url::Url) -> Result { let client = reqwest::Client::builder().build()?; @@ -58,10 +65,11 @@ impl ExplorerClient { paths: &[&str], ) -> Result { let url = combine_url(self.url.clone(), paths)?; - log::trace!("Sending GET request {url:?}"); + tracing::debug!("Sending GET request"); Ok(self.client.get(url).send().await?) } + #[instrument(level = "trace", skip_all, fields(paths=?paths))] async fn query_explorer_api(&self, paths: &[&str]) -> Result where T: std::fmt::Debug, @@ -70,7 +78,7 @@ impl ExplorerClient { let response = self.send_get_request(paths).await?; if response.status().is_success() { let res = response.json::().await?; - log::trace!("Got response: {res:?}"); + tracing::trace!("Got response: {res:?}"); Ok(res) } else if response.status() == StatusCode::NOT_FOUND { Err(ExplorerApiError::NotFound) diff --git a/nym-node-status-api/.gitignore b/nym-node-status-api/.gitignore index 8fce603003c..b2a9b208f06 100644 --- a/nym-node-status-api/.gitignore +++ b/nym-node-status-api/.gitignore @@ -1 +1,2 @@ data/ +enter_db.sh diff --git a/nym-node-status-api/Cargo.toml b/nym-node-status-api/Cargo.toml index f670afc8d82..3b8d315ccb1 100644 --- a/nym-node-status-api/Cargo.toml +++ b/nym-node-status-api/Cargo.toml @@ -14,22 +14,38 @@ rust-version.workspace = true [dependencies] anyhow = { workspace = true } -axum = { workspace = true } +axum = { workspace = true, features = ["tokio"] } chrono = { workspace = true } clap = { workspace = true, features = ["cargo", "derive"] } cosmwasm-std = { workspace = true } +envy = { workspace = true } futures-util = { workspace = true } +moka = { workspace = true, features = ["future"] } nym-bin-common = { path = "../common/bin-common" } nym-explorer-client = { path = "../explorer-api/explorer-client" } nym-network-defaults = { path = "../common/network-defaults" } 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"] } +reqwest = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } +serde_json_path = { workspace = true } sqlx = { workspace = true, features = ["runtime-tokio-rustls", "sqlite"] } thiserror = { workspace = true } -tokio = { workspace = true, features = ["full"] } +tokio = { workspace = true, features = ["rt-multi-thread"] } +tokio-util = { workspace = true } tracing = { workspace = true } tracing-subscriber = { workspace = true, features = ["env-filter"] } +tracing-log = { workspace = true } +tower-http = { workspace = true, features = ["cors", "trace"] } +utoipa = { workspace = true, features = ["axum_extras", "time"] } +utoipa-swagger-ui = { workspace = true, features = ["axum"] } +# TODO dz `cargo update async-trait` +# for automatic schema detection, which was merged, but not released yet +# https://github.com/ProbablyClem/utoipauto/pull/38 +# utoipauto = { git = "https://github.com/ProbablyClem/utoipauto", rev = "eb04cba" } +utoipauto = { workspace = true } [build-dependencies] anyhow = { workspace = true } diff --git a/nym-node-status-api/build.rs b/nym-node-status-api/build.rs index 74c6767c6f4..394083f8bed 100644 --- a/nym-node-status-api/build.rs +++ b/nym-node-status-api/build.rs @@ -1,13 +1,17 @@ use anyhow::{anyhow, Result}; use sqlx::{Connection, SqliteConnection}; +use tokio::{fs::File, io::AsyncWriteExt}; const SQLITE_DB_FILENAME: &str = "nym-node-status-api.sqlite"; +/// If you need to re-run migrations or reset the db, just run +/// cargo clean -p nym-node-status-api #[tokio::main] async fn main() -> Result<()> { let out_dir = read_env_var("OUT_DIR")?; let database_path = format!("sqlite://{}/{}?mode=rwc", out_dir, SQLITE_DB_FILENAME); + write_db_path_to_file(&out_dir, SQLITE_DB_FILENAME).await?; let mut conn = SqliteConnection::connect(&database_path).await?; sqlx::migrate!("./migrations").run(&mut conn).await?; @@ -31,3 +35,12 @@ fn rerun_if_changed() { println!("cargo::rerun-if-changed=migrations"); println!("cargo::rerun-if-changed=src/db/queries"); } + +/// use `./enter_db.sh` to inspect DB +async fn write_db_path_to_file(out_dir: &str, db_filename: &str) -> anyhow::Result<()> { + let mut file = File::create("enter_db.sh").await?; + let _ = file.write(b"#!/bin/bash\n").await?; + file.write_all(format!("sqlite3 {}/{}", out_dir, db_filename).as_bytes()) + .await + .map_err(From::from) +} diff --git a/nym-node-status-api/launch_node_status_api.sh b/nym-node-status-api/launch_node_status_api.sh index 8af370232f2..e5059c50110 100755 --- a/nym-node-status-api/launch_node_status_api.sh +++ b/nym-node-status-api/launch_node_status_api.sh @@ -2,63 +2,9 @@ set -e -function usage() { - echo "Usage: $0 [-ci]" - echo " -c Clear DB and re-initialize it before launching the binary." - echo " -i Only initialize and prepare database, env vars then exit without" - echo " launching" - exit 0 -} - -function init_db() { - rm -rf data/* - # https://github.com/launchbadge/sqlx/blob/main/sqlx-cli/README.md - cargo sqlx database drop -y - - cargo sqlx database create - cargo sqlx migrate run - cargo sqlx prepare - - echo "Fresh database ready!" -} - -# export DATABASE_URL as absolute path due to this -# https://github.com/launchbadge/sqlx/issues/3099 -db_filename="nym-node-status-api.sqlite" -script_abs_path=$(realpath "$0") -package_dir=$(dirname "$script_abs_path") -db_abs_path="$package_dir/data/$db_filename" -dotenv_file="$package_dir/.env" -echo "DATABASE_URL=sqlite://$db_abs_path" > "$dotenv_file" - export RUST_LOG=${RUST_LOG:-debug} -# export DATABASE_URL from .env file -set -a && source "$dotenv_file" && set +a - -clear_db=false -init_only=false - -while getopts "ci" opt; do - case ${opt} in - c) - clear_db=true - ;; - i) - init_only=true - ;; - \?) - usage - ;; - esac -done - -if [ "$clear_db" = true ] || [ "$init_only" = true ]; then - init_db -fi - -if [ "$init_only" = true ]; then - exit 0 -fi +export NYM_API_CLIENT_TIMEOUT=60; +export EXPLORER_CLIENT_TIMEOUT=60; -cargo run --package nym-node-status-api +cargo run --package nym-node-status-api --release -- --config-env-file ../envs/mainnet.env diff --git a/nym-node-status-api/migrations/000_init.sql b/nym-node-status-api/migrations/000_init.sql index f950b492293..35aaa40654b 100644 --- a/nym-node-status-api/migrations/000_init.sql +++ b/nym-node-status-api/migrations/000_init.sql @@ -39,6 +39,21 @@ CREATE TABLE mixnodes ( CREATE INDEX idx_mixnodes_mix_id ON mixnodes (mix_id); CREATE INDEX idx_mixnodes_identity_key ON mixnodes (identity_key); +CREATE TABLE + mixnode_description ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + mix_id INTEGER UNIQUE NOT NULL, + moniker VARCHAR, + website VARCHAR, + security_contact VARCHAR, + details VARCHAR, + last_updated_utc INTEGER NOT NULL, + FOREIGN KEY (mix_id) REFERENCES mixnodes (mix_id) + ); + +-- Indexes for description table +CREATE INDEX idx_mixnode_description_mix_id ON mixnode_description (mix_id); + CREATE TABLE summary ( @@ -57,3 +72,29 @@ CREATE TABLE summary_history ); CREATE INDEX idx_summary_history_timestamp_utc ON summary_history (timestamp_utc); CREATE INDEX idx_summary_history_date ON summary_history (date); + + +CREATE TABLE gateway_description ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + gateway_identity_key VARCHAR UNIQUE NOT NULL, + moniker VARCHAR, + website VARCHAR, + security_contact VARCHAR, + details VARCHAR, + last_updated_utc INTEGER NOT NULL, + FOREIGN KEY (gateway_identity_key) REFERENCES gateways (gateway_identity_key) + ); + + +CREATE TABLE + mixnode_daily_stats ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + mix_id INTEGER NOT NULL, + total_stake BIGINT NOT NULL, + date_utc VARCHAR NOT NULL, + packets_received INTEGER DEFAULT 0, + packets_sent INTEGER DEFAULT 0, + packets_dropped INTEGER DEFAULT 0, + FOREIGN KEY (mix_id) REFERENCES mixnodes (mix_id), + UNIQUE (mix_id, date_utc) -- This constraint automatically creates an index + ); diff --git a/nym-node-status-api/src/config.rs b/nym-node-status-api/src/config.rs new file mode 100644 index 00000000000..24e966a53fc --- /dev/null +++ b/nym-node-status-api/src/config.rs @@ -0,0 +1,79 @@ +use anyhow::anyhow; +use reqwest::Url; +use serde::Deserialize; +use std::time::Duration; + +#[derive(Debug, Clone, Deserialize)] +pub(crate) struct Config { + #[serde(default = "Config::default_http_cache_seconds")] + nym_http_cache_ttl: u64, + #[serde(default = "Config::default_http_port")] + http_port: u16, + #[serde(rename = "nyxd")] + nyxd_addr: Url, + #[serde(default = "Config::default_client_timeout")] + #[serde(deserialize_with = "parse_duration")] + nym_api_client_timeout: Duration, + #[serde(default = "Config::default_client_timeout")] + #[serde(deserialize_with = "parse_duration")] + explorer_client_timeout: Duration, +} + +impl Config { + pub(crate) fn from_env() -> anyhow::Result { + envy::from_env::().map_err(|e| { + tracing::error!("Failed to load config from env: {e}"); + anyhow::Error::from(e) + }) + } + + fn default_client_timeout() -> Duration { + Duration::from_secs(15) + } + + fn default_http_port() -> u16 { + 8000 + } + + fn default_http_cache_seconds() -> u64 { + 30 + } + + pub(crate) fn nym_http_cache_ttl(&self) -> u64 { + self.nym_http_cache_ttl + } + + pub(crate) fn http_port(&self) -> u16 { + self.http_port + } + + pub(crate) fn nyxd_addr(&self) -> &Url { + &self.nyxd_addr + } + + pub(crate) fn nym_api_client_timeout(&self) -> Duration { + self.nym_api_client_timeout.to_owned() + } + + pub(crate) fn nym_explorer_client_timeout(&self) -> Duration { + self.explorer_client_timeout.to_owned() + } +} + +fn parse_duration<'de, D>(deserializer: D) -> Result +where + D: serde::Deserializer<'de>, +{ + let s: String = Deserialize::deserialize(deserializer)?; + let secs: u64 = s.parse().map_err(serde::de::Error::custom)?; + Ok(Duration::from_secs(secs)) +} + +pub(super) fn read_env_var(env_var: &str) -> anyhow::Result { + std::env::var(env_var) + .map_err(|_| anyhow!("You need to set {}", env_var)) + .map(|value| { + tracing::trace!("{}={}", env_var, value); + value + }) +} diff --git a/nym-node-status-api/src/db/mod.rs b/nym-node-status-api/src/db/mod.rs index d9850abf7d8..784a35f17ac 100644 --- a/nym-node-status-api/src/db/mod.rs +++ b/nym-node-status-api/src/db/mod.rs @@ -3,12 +3,13 @@ use std::str::FromStr; use crate::read_env_var; use anyhow::{anyhow, Result}; use sqlx::{migrate::Migrator, sqlite::SqliteConnectOptions, ConnectOptions, SqlitePool}; -pub(crate) const DATABASE_URL_ENV_VAR: &str = "DATABASE_URL"; -static MIGRATOR: Migrator = sqlx::migrate!("./migrations"); pub(crate) mod models; pub(crate) mod queries; +pub(crate) const DATABASE_URL_ENV_VAR: &str = "DATABASE_URL"; +static MIGRATOR: Migrator = sqlx::migrate!("./migrations"); + pub(crate) type DbPool = SqlitePool; pub(crate) struct Storage { @@ -34,7 +35,8 @@ impl Storage { Ok(Storage { pool }) } - pub async fn pool(&self) -> &DbPool { - &self.pool + /// Cloning pool is cheap, it's the same underlying set of connections + pub async fn pool_owned(&self) -> DbPool { + self.pool.clone() } } diff --git a/nym-node-status-api/src/db/models.rs b/nym-node-status-api/src/db/models.rs index 83e04d99eeb..54164b34e32 100644 --- a/nym-node-status-api/src/db/models.rs +++ b/nym-node-status-api/src/db/models.rs @@ -1,4 +1,10 @@ +use crate::{ + http::{self, models::SummaryHistory}, + monitor::NumericalCheckedCast, +}; +use nym_node_requests::api::v1::node::models::NodeDescription; use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; pub(crate) struct GatewayRecord { pub(crate) identity_key: String, @@ -10,6 +16,88 @@ pub(crate) struct GatewayRecord { pub(crate) performance: u8, } +#[derive(Debug, Clone)] +pub(crate) struct GatewayDto { + pub(crate) gateway_identity_key: String, + pub(crate) bonded: bool, + pub(crate) blacklisted: bool, + pub(crate) performance: i64, + pub(crate) self_described: Option, + pub(crate) explorer_pretty_bond: Option, + pub(crate) last_probe_result: Option, + pub(crate) last_probe_log: Option, + pub(crate) last_testrun_utc: Option, + pub(crate) last_updated_utc: i64, + pub(crate) moniker: String, + pub(crate) security_contact: String, + pub(crate) details: String, + pub(crate) website: String, +} + +impl TryFrom for http::models::Gateway { + type Error = anyhow::Error; + + fn try_from(value: GatewayDto) -> Result { + // Instead of using routing_score_successes / routing_score_samples, we use the + // number of successful testruns in the last 24h. + let routing_score = 0f32; + let config_score = 0u32; + let last_updated_utc = + timestamp_as_utc(value.last_updated_utc.cast_checked()?).to_rfc3339(); + let last_testrun_utc = value + .last_testrun_utc + .and_then(|i| i.cast_checked().ok()) + .map(|t| timestamp_as_utc(t).to_rfc3339()); + + let self_described = value.self_described.clone().unwrap_or("null".to_string()); + let explorer_pretty_bond = value + .explorer_pretty_bond + .clone() + .unwrap_or("null".to_string()); + let last_probe_result = value + .last_probe_result + .clone() + .unwrap_or("null".to_string()); + let last_probe_log = value.last_probe_log.clone(); + + let self_described = serde_json::from_str(&self_described).unwrap_or(None); + let explorer_pretty_bond = serde_json::from_str(&explorer_pretty_bond).unwrap_or(None); + let last_probe_result = serde_json::from_str(&last_probe_result).unwrap_or(None); + + let bonded = value.bonded; + let blacklisted = value.blacklisted; + let performance = value.performance as u8; + + let description = NodeDescription { + moniker: value.moniker.clone(), + website: value.website.clone(), + security_contact: value.security_contact.clone(), + details: value.details.clone(), + }; + + Ok(http::models::Gateway { + gateway_identity_key: value.gateway_identity_key.clone(), + bonded, + blacklisted, + performance, + self_described, + explorer_pretty_bond, + description, + last_probe_result, + last_probe_log, + routing_score, + config_score, + last_testrun_utc, + last_updated_utc, + }) + } +} + +fn timestamp_as_utc(unix_timestamp: u64) -> chrono::DateTime { + let d = std::time::UNIX_EPOCH + std::time::Duration::from_secs(unix_timestamp); + d.into() +} + pub(crate) struct MixnodeRecord { pub(crate) mix_id: u32, pub(crate) identity_key: String, @@ -24,6 +112,63 @@ pub(crate) struct MixnodeRecord { pub(crate) is_dp_delegatee: bool, } +#[derive(Debug, Clone)] +pub(crate) struct MixnodeDto { + pub(crate) mix_id: i64, + pub(crate) bonded: bool, + pub(crate) blacklisted: bool, + pub(crate) is_dp_delegatee: bool, + pub(crate) total_stake: i64, + pub(crate) full_details: String, + pub(crate) self_described: Option, + pub(crate) last_updated_utc: i64, + pub(crate) moniker: String, + pub(crate) website: String, + pub(crate) security_contact: String, + pub(crate) details: String, +} + +impl TryFrom for http::models::Mixnode { + type Error = anyhow::Error; + + fn try_from(value: MixnodeDto) -> Result { + let mix_id = value.mix_id.cast_checked()?; + let full_details = value.full_details.clone(); + let full_details = serde_json::from_str(&full_details).unwrap_or(None); + + let self_described = value + .self_described + .clone() + .map(|v| serde_json::from_str(&v).unwrap_or(serde_json::Value::Null)); + + let last_updated_utc = + timestamp_as_utc(value.last_updated_utc.cast_checked()?).to_rfc3339(); + let blacklisted = value.blacklisted; + let is_dp_delegatee = value.is_dp_delegatee; + let moniker = value.moniker.clone(); + let website = value.website.clone(); + let security_contact = value.security_contact.clone(); + let details = value.details.clone(); + + Ok(http::models::Mixnode { + mix_id, + bonded: value.bonded, + blacklisted, + is_dp_delegatee, + total_stake: value.total_stake, + full_details, + description: NodeDescription { + moniker, + website, + security_contact, + details, + }, + self_described, + last_updated_utc, + }) + } +} + #[allow(unused)] #[derive(Debug, Clone)] pub(crate) struct BondedStatusDto { @@ -40,6 +185,28 @@ pub(crate) struct SummaryDto { pub(crate) last_updated_utc: i64, } +#[derive(Debug, Clone, Default)] +pub(crate) struct SummaryHistoryDto { + #[allow(dead_code)] + pub id: i64, + pub date: String, + pub value_json: String, + pub timestamp_utc: i64, +} + +impl TryFrom for SummaryHistory { + type Error = anyhow::Error; + + fn try_from(value: SummaryHistoryDto) -> Result { + let value_json = serde_json::from_str(&value.value_json).unwrap_or_default(); + Ok(SummaryHistory { + value_json, + date: value.date.clone(), + timestamp_utc: timestamp_as_utc(value.timestamp_utc.cast_checked()?).to_rfc3339(), + }) + } +} + pub(crate) const MIXNODES_BONDED_COUNT: &str = "mixnodes.bonded.count"; pub(crate) const MIXNODES_BONDED_ACTIVE: &str = "mixnodes.bonded.active"; pub(crate) const MIXNODES_BONDED_INACTIVE: &str = "mixnodes.bonded.inactive"; @@ -53,23 +220,28 @@ pub(crate) const GATEWAYS_BLACKLISTED_COUNT: &str = "gateways.blacklisted.count" pub(crate) const MIXNODES_HISTORICAL_COUNT: &str = "mixnodes.historical.count"; pub(crate) const GATEWAYS_HISTORICAL_COUNT: &str = "gateways.historical.count"; -#[derive(Debug, Clone, Deserialize, Serialize)] +// `utoipa`` goes crazy if you use module-qualified prefix as field type so we +// have to import it +use gateway::GatewaySummary; +use mixnode::MixnodeSummary; + +#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)] pub(crate) struct NetworkSummary { - pub(crate) mixnodes: mixnode::MixnodeSummary, - pub(crate) gateways: gateway::GatewaySummary, + pub(crate) mixnodes: MixnodeSummary, + pub(crate) gateways: GatewaySummary, } pub(crate) mod mixnode { use super::*; - #[derive(Debug, Clone, Deserialize, Serialize)] + #[derive(Debug, Clone, Deserialize, Serialize, ToSchema)] pub(crate) struct MixnodeSummary { pub(crate) bonded: MixnodeSummaryBonded, pub(crate) blacklisted: MixnodeSummaryBlacklisted, pub(crate) historical: MixnodeSummaryHistorical, } - #[derive(Debug, Clone, Deserialize, Serialize)] + #[derive(Debug, Clone, Deserialize, Serialize, ToSchema)] pub(crate) struct MixnodeSummaryBonded { pub(crate) count: i32, pub(crate) active: i32, @@ -78,13 +250,13 @@ pub(crate) mod mixnode { pub(crate) last_updated_utc: String, } - #[derive(Debug, Clone, Deserialize, Serialize)] + #[derive(Debug, Clone, Deserialize, Serialize, ToSchema)] pub(crate) struct MixnodeSummaryBlacklisted { pub(crate) count: i32, pub(crate) last_updated_utc: String, } - #[derive(Debug, Clone, Deserialize, Serialize)] + #[derive(Debug, Clone, Deserialize, Serialize, ToSchema)] pub(crate) struct MixnodeSummaryHistorical { pub(crate) count: i32, pub(crate) last_updated_utc: String, @@ -94,7 +266,7 @@ pub(crate) mod mixnode { pub(crate) mod gateway { use super::*; - #[derive(Debug, Clone, Deserialize, Serialize)] + #[derive(Debug, Clone, Deserialize, Serialize, ToSchema)] pub(crate) struct GatewaySummary { pub(crate) bonded: GatewaySummaryBonded, pub(crate) blacklisted: GatewaySummaryBlacklisted, @@ -102,25 +274,25 @@ pub(crate) mod gateway { pub(crate) explorer: GatewaySummaryExplorer, } - #[derive(Debug, Clone, Deserialize, Serialize)] + #[derive(Debug, Clone, Deserialize, Serialize, ToSchema)] pub(crate) struct GatewaySummaryExplorer { pub(crate) count: i32, pub(crate) last_updated_utc: String, } - #[derive(Debug, Clone, Deserialize, Serialize)] + #[derive(Debug, Clone, Deserialize, Serialize, ToSchema)] pub(crate) struct GatewaySummaryBonded { pub(crate) count: i32, pub(crate) last_updated_utc: String, } - #[derive(Debug, Clone, Deserialize, Serialize)] + #[derive(Debug, Clone, Deserialize, Serialize, ToSchema)] pub(crate) struct GatewaySummaryHistorical { pub(crate) count: i32, pub(crate) last_updated_utc: String, } - #[derive(Debug, Clone, Deserialize, Serialize)] + #[derive(Debug, Clone, Deserialize, Serialize, ToSchema)] pub(crate) struct GatewaySummaryBlacklisted { pub(crate) count: i32, pub(crate) last_updated_utc: String, diff --git a/nym-node-status-api/src/db/queries/gateways.rs b/nym-node-status-api/src/db/queries/gateways.rs index 69f77a97fd1..92a599154f7 100644 --- a/nym-node-status-api/src/db/queries/gateways.rs +++ b/nym-node-status-api/src/db/queries/gateways.rs @@ -1,9 +1,13 @@ -use crate::db::{ - models::{BondedStatusDto, GatewayRecord}, - DbPool, +use crate::{ + db::{ + models::{BondedStatusDto, GatewayDto, GatewayRecord}, + DbPool, + }, + http::models::Gateway, }; use futures_util::TryStreamExt; use nym_validator_client::models::DescribedGateway; +use tracing::error; pub(crate) async fn insert_gateways( pool: &DbPool, @@ -114,3 +118,43 @@ async fn get_all_bonded_gateways_row_ids_by_status( Ok(items) } + +pub(crate) async fn get_all_gateways(pool: &DbPool) -> anyhow::Result> { + let mut conn = pool.acquire().await?; + let items = sqlx::query_as!( + GatewayDto, + r#"SELECT + gw.gateway_identity_key as "gateway_identity_key!", + gw.bonded as "bonded: bool", + gw.blacklisted as "blacklisted: bool", + gw.performance as "performance!", + gw.self_described as "self_described?", + gw.explorer_pretty_bond as "explorer_pretty_bond?", + gw.last_probe_result as "last_probe_result?", + gw.last_probe_log as "last_probe_log?", + gw.last_testrun_utc as "last_testrun_utc?", + gw.last_updated_utc as "last_updated_utc!", + COALESCE(gd.moniker, "NA") as "moniker!", + COALESCE(gd.website, "NA") as "website!", + COALESCE(gd.security_contact, "NA") as "security_contact!", + COALESCE(gd.details, "NA") as "details!" + FROM gateways gw + LEFT JOIN gateway_description gd + ON gw.gateway_identity_key = gd.gateway_identity_key + ORDER BY gw.gateway_identity_key"#, + ) + .fetch(&mut conn) + .try_collect::>() + .await?; + + let items: Vec = items + .into_iter() + .map(|item| item.try_into()) + .collect::>>() + .map_err(|e| { + error!("Conversion from DTO failed: {e}. Invalidly stored data?"); + e + })?; + tracing::trace!("Fetched {} gateways from DB", items.len()); + Ok(items) +} diff --git a/nym-node-status-api/src/db/queries/misc.rs b/nym-node-status-api/src/db/queries/misc.rs index c2c7b6d9ebe..64b2f3cd24f 100644 --- a/nym-node-status-api/src/db/queries/misc.rs +++ b/nym-node-status-api/src/db/queries/misc.rs @@ -45,6 +45,8 @@ async fn insert_summary( })?; } + tx.commit().await?; + Ok(()) } diff --git a/nym-node-status-api/src/db/queries/mixnodes.rs b/nym-node-status-api/src/db/queries/mixnodes.rs index aed48ed4d79..8bc8020ef9c 100644 --- a/nym-node-status-api/src/db/queries/mixnodes.rs +++ b/nym-node-status-api/src/db/queries/mixnodes.rs @@ -1,9 +1,13 @@ use futures_util::TryStreamExt; use nym_validator_client::models::MixNodeBondAnnotated; +use tracing::error; -use crate::db::{ - models::{BondedStatusDto, MixnodeRecord}, - DbPool, +use crate::{ + db::{ + models::{BondedStatusDto, MixnodeDto, MixnodeRecord}, + DbPool, + }, + http::models::{DailyStats, Mixnode}, }; pub(crate) async fn insert_mixnodes( @@ -46,6 +50,78 @@ pub(crate) async fn insert_mixnodes( Ok(()) } +pub(crate) async fn get_all_mixnodes(pool: &DbPool) -> anyhow::Result> { + let mut conn = pool.acquire().await?; + let items = sqlx::query_as!( + MixnodeDto, + r#"SELECT + mn.mix_id as "mix_id!", + mn.bonded as "bonded: bool", + mn.blacklisted as "blacklisted: bool", + mn.is_dp_delegatee as "is_dp_delegatee: bool", + mn.total_stake as "total_stake!", + mn.full_details as "full_details!", + mn.self_described as "self_described", + mn.last_updated_utc as "last_updated_utc!", + COALESCE(md.moniker, "NA") as "moniker!", + COALESCE(md.website, "NA") as "website!", + COALESCE(md.security_contact, "NA") as "security_contact!", + COALESCE(md.details, "NA") as "details!" + FROM mixnodes mn + LEFT JOIN mixnode_description md ON mn.mix_id = md.mix_id + ORDER BY mn.mix_id"# + ) + .fetch(&mut conn) + .try_collect::>() + .await?; + + let items = items + .into_iter() + .map(|item| item.try_into()) + .collect::>>() + .map_err(|e| { + error!("Conversion from DTO failed: {e}. Invalidly stored data?"); + e + })?; + Ok(items) +} + +/// We fetch the latest 30 days of data as a subquery and then +/// return it in ascending order, so we don't break existing UI +pub(crate) async fn get_daily_stats(pool: &DbPool) -> anyhow::Result> { + let mut conn = pool.acquire().await?; + let items = sqlx::query_as!( + DailyStats, + r#" + SELECT + date_utc as "date_utc!", + packets_received as "total_packets_received!: i64", + packets_sent as "total_packets_sent!: i64", + packets_dropped as "total_packets_dropped!: i64", + total_stake as "total_stake!: i64" + FROM ( + SELECT + date_utc, + SUM(packets_received) as packets_received, + SUM(packets_sent) as packets_sent, + SUM(packets_dropped) as packets_dropped, + SUM(total_stake) as total_stake + FROM mixnode_daily_stats + GROUP BY date_utc + ORDER BY date_utc DESC + LIMIT 30 + ) + GROUP BY date_utc + ORDER BY date_utc + "# + ) + .fetch(&mut conn) + .try_collect::>() + .await?; + + Ok(items) +} + /// Ensure all mixnodes that are set as bonded, are still bonded pub(crate) async fn ensure_mixnodes_still_bonded( pool: &DbPool, diff --git a/nym-node-status-api/src/db/queries/mod.rs b/nym-node-status-api/src/db/queries/mod.rs index 38f1faab611..279d31dc342 100644 --- a/nym-node-status-api/src/db/queries/mod.rs +++ b/nym-node-status-api/src/db/queries/mod.rs @@ -1,9 +1,14 @@ mod gateways; mod misc; mod mixnodes; +mod summary; pub(crate) use gateways::{ - ensure_gateways_still_bonded, insert_gateways, write_blacklisted_gateways_to_db, + ensure_gateways_still_bonded, get_all_gateways, insert_gateways, + write_blacklisted_gateways_to_db, }; pub(crate) use misc::insert_summaries; -pub(crate) use mixnodes::{ensure_mixnodes_still_bonded, insert_mixnodes}; +pub(crate) use mixnodes::{ + ensure_mixnodes_still_bonded, get_all_mixnodes, get_daily_stats, insert_mixnodes, +}; +pub(crate) use summary::{get_summary, get_summary_history}; diff --git a/nym-node-status-api/src/db/queries/summary.rs b/nym-node-status-api/src/db/queries/summary.rs new file mode 100644 index 00000000000..d3855639f69 --- /dev/null +++ b/nym-node-status-api/src/db/queries/summary.rs @@ -0,0 +1,209 @@ +use chrono::{DateTime, Utc}; +use futures_util::TryStreamExt; +use std::collections::HashMap; +use tracing::error; + +use crate::{ + db::{ + models::{ + gateway::{ + GatewaySummary, GatewaySummaryBlacklisted, GatewaySummaryBonded, + GatewaySummaryExplorer, GatewaySummaryHistorical, + }, + mixnode::{ + MixnodeSummary, MixnodeSummaryBlacklisted, MixnodeSummaryBonded, + MixnodeSummaryHistorical, + }, + NetworkSummary, SummaryDto, SummaryHistoryDto, + }, + DbPool, + }, + http::{ + error::{HttpError, HttpResult}, + models::SummaryHistory, + }, +}; + +pub(crate) async fn get_summary_history(pool: &DbPool) -> anyhow::Result> { + let mut conn = pool.acquire().await?; + let items = sqlx::query_as!( + SummaryHistoryDto, + r#"SELECT + id as "id!", + date as "date!", + timestamp_utc as "timestamp_utc!", + value_json as "value_json!" + FROM summary_history + ORDER BY date DESC + LIMIT 30"#, + ) + .fetch(&mut conn) + .try_collect::>() + .await?; + + let items = items + .into_iter() + .map(|item| item.try_into()) + .collect::>>() + .map_err(|e| { + error!("Conversion from DTO failed: {e}. Invalidly stored data?"); + e + })?; + Ok(items) +} + +async fn get_summary_dto(pool: &DbPool) -> anyhow::Result> { + let mut conn = pool.acquire().await?; + Ok(sqlx::query_as!( + SummaryDto, + r#"SELECT + key as "key!", + value_json as "value_json!", + last_updated_utc as "last_updated_utc!" + FROM summary"# + ) + .fetch(&mut conn) + .try_collect::>() + .await?) +} + +pub(crate) async fn get_summary(pool: &DbPool) -> HttpResult { + let items = get_summary_dto(pool).await.map_err(|err| { + tracing::error!("Couldn't get Summary from DB: {err}"); + HttpError::internal() + })?; + from_summary_dto(items).await +} + +async fn from_summary_dto(items: Vec) -> HttpResult { + const MIXNODES_BONDED_COUNT: &str = "mixnodes.bonded.count"; + const MIXNODES_BONDED_ACTIVE: &str = "mixnodes.bonded.active"; + const MIXNODES_BONDED_INACTIVE: &str = "mixnodes.bonded.inactive"; + const MIXNODES_BONDED_RESERVE: &str = "mixnodes.bonded.reserve"; + const MIXNODES_BLACKLISTED_COUNT: &str = "mixnodes.blacklisted.count"; + const GATEWAYS_BONDED_COUNT: &str = "gateways.bonded.count"; + const GATEWAYS_EXPLORER_COUNT: &str = "gateways.explorer.count"; + const GATEWAYS_BLACKLISTED_COUNT: &str = "gateways.blacklisted.count"; + const MIXNODES_HISTORICAL_COUNT: &str = "mixnodes.historical.count"; + const GATEWAYS_HISTORICAL_COUNT: &str = "gateways.historical.count"; + + // convert database rows into a map by key + let mut map = HashMap::new(); + for item in items { + map.insert(item.key.clone(), item); + } + + // check we have all the keys we are expecting, and build up a map of errors for missing one + let keys = [ + GATEWAYS_BONDED_COUNT, + GATEWAYS_EXPLORER_COUNT, + GATEWAYS_HISTORICAL_COUNT, + GATEWAYS_BLACKLISTED_COUNT, + MIXNODES_BLACKLISTED_COUNT, + MIXNODES_BONDED_ACTIVE, + MIXNODES_BONDED_COUNT, + MIXNODES_BONDED_INACTIVE, + MIXNODES_BONDED_RESERVE, + MIXNODES_HISTORICAL_COUNT, + ]; + + let mut errors: Vec<&str> = vec![]; + for key in keys { + if !map.contains_key(key) { + errors.push(key); + } + } + + // return an error if anything is missing, with a nice list + if !errors.is_empty() { + tracing::error!("Summary value missing: {}", errors.join(", ")); + return Err(HttpError::internal()); + } + + // strip the options and use default values (anything missing is trapped above) + let mixnodes_bonded_count: SummaryDto = + map.get(MIXNODES_BONDED_COUNT).cloned().unwrap_or_default(); + let mixnodes_bonded_active: SummaryDto = + map.get(MIXNODES_BONDED_ACTIVE).cloned().unwrap_or_default(); + let mixnodes_bonded_inactive: SummaryDto = map + .get(MIXNODES_BONDED_INACTIVE) + .cloned() + .unwrap_or_default(); + let mixnodes_bonded_reserve: SummaryDto = map + .get(MIXNODES_BONDED_RESERVE) + .cloned() + .unwrap_or_default(); + let mixnodes_blacklisted_count: SummaryDto = map + .get(MIXNODES_BLACKLISTED_COUNT) + .cloned() + .unwrap_or_default(); + let gateways_bonded_count: SummaryDto = + map.get(GATEWAYS_BONDED_COUNT).cloned().unwrap_or_default(); + let gateways_explorer_count: SummaryDto = map + .get(GATEWAYS_EXPLORER_COUNT) + .cloned() + .unwrap_or_default(); + let mixnodes_historical_count: SummaryDto = map + .get(MIXNODES_HISTORICAL_COUNT) + .cloned() + .unwrap_or_default(); + let gateways_historical_count: SummaryDto = map + .get(GATEWAYS_HISTORICAL_COUNT) + .cloned() + .unwrap_or_default(); + let gateways_blacklisted_count: SummaryDto = map + .get(GATEWAYS_BLACKLISTED_COUNT) + .cloned() + .unwrap_or_default(); + + Ok(NetworkSummary { + mixnodes: MixnodeSummary { + bonded: MixnodeSummaryBonded { + count: to_count_i32(&mixnodes_bonded_count), + active: to_count_i32(&mixnodes_bonded_active), + reserve: to_count_i32(&mixnodes_bonded_reserve), + inactive: to_count_i32(&mixnodes_bonded_inactive), + last_updated_utc: to_timestamp(&mixnodes_bonded_count), + }, + blacklisted: MixnodeSummaryBlacklisted { + count: to_count_i32(&mixnodes_blacklisted_count), + last_updated_utc: to_timestamp(&mixnodes_blacklisted_count), + }, + historical: MixnodeSummaryHistorical { + count: to_count_i32(&mixnodes_historical_count), + last_updated_utc: to_timestamp(&mixnodes_historical_count), + }, + }, + gateways: GatewaySummary { + bonded: GatewaySummaryBonded { + count: to_count_i32(&gateways_bonded_count), + last_updated_utc: to_timestamp(&gateways_bonded_count), + }, + blacklisted: GatewaySummaryBlacklisted { + count: to_count_i32(&gateways_blacklisted_count), + last_updated_utc: to_timestamp(&gateways_blacklisted_count), + }, + historical: GatewaySummaryHistorical { + count: to_count_i32(&gateways_historical_count), + last_updated_utc: to_timestamp(&gateways_historical_count), + }, + explorer: GatewaySummaryExplorer { + count: to_count_i32(&gateways_explorer_count), + last_updated_utc: to_timestamp(&gateways_explorer_count), + }, + }, + }) +} + +fn to_count_i32(value: &SummaryDto) -> i32 { + value.value_json.parse::().unwrap_or_default() +} + +fn to_timestamp(value: &SummaryDto) -> String { + timestamp_as_utc(value.last_updated_utc as u64).to_rfc3339() +} + +fn timestamp_as_utc(unix_timestamp: u64) -> DateTime { + let d = std::time::UNIX_EPOCH + std::time::Duration::from_secs(unix_timestamp); + d.into() +} diff --git a/nym-node-status-api/src/http/api/gateways.rs b/nym-node-status-api/src/http/api/gateways.rs new file mode 100644 index 00000000000..9dec3134e48 --- /dev/null +++ b/nym-node-status-api/src/http/api/gateways.rs @@ -0,0 +1,110 @@ +use axum::{ + extract::{Path, Query, State}, + Json, Router, +}; +use serde::Deserialize; +use utoipa::IntoParams; + +use crate::http::{ + error::{HttpError, HttpResult}, + models::{Gateway, GatewaySkinny}, + state::AppState, + PagedResult, Pagination, +}; + +pub(crate) fn routes() -> Router { + Router::new() + .route("/", axum::routing::get(gateways)) + .route("/skinny", axum::routing::get(gateways_skinny)) + .route("/skinny/:identity_key", axum::routing::get(get_gateway)) +} + +#[utoipa::path( + tag = "Gateways", + get, + params( + Pagination + ), + path = "/v2/gateways", + responses( + (status = 200, body = PagedGateway) + ) +)] +async fn gateways( + Query(pagination): Query, + State(state): State, +) -> HttpResult>> { + let db = state.db_pool(); + let res = state.cache().get_gateway_list(db).await; + + Ok(Json(PagedResult::paginate(pagination, res))) +} + +#[utoipa::path( + tag = "Gateways", + get, + params( + Pagination + ), + path = "/v2/gateways/skinny", + responses( + (status = 200, body = PagedGatewaySkinny) + ) +)] +async fn gateways_skinny( + Query(pagination): Query, + State(state): State, +) -> HttpResult>> { + let db = state.db_pool(); + let res = state.cache().get_gateway_list(db).await; + let res: Vec = res + .iter() + .filter(|g| g.bonded) + .map(|g| GatewaySkinny { + gateway_identity_key: g.gateway_identity_key.clone(), + self_described: g.self_described.clone(), + performance: g.performance, + explorer_pretty_bond: g.explorer_pretty_bond.clone(), + last_probe_result: g.last_probe_result.clone(), + last_testrun_utc: g.last_testrun_utc.clone(), + last_updated_utc: g.last_updated_utc.clone(), + routing_score: g.routing_score, + config_score: g.config_score, + }) + .collect(); + + Ok(Json(PagedResult::paginate(pagination, res))) +} + +#[derive(Deserialize, IntoParams)] +#[into_params(parameter_in = Path)] +struct IdentityKeyParam { + identity_key: String, +} + +#[utoipa::path( + tag = "Gateways", + get, + params( + IdentityKeyParam + ), + path = "/v2/gateways/{identity_key}", + responses( + (status = 200, body = Gateway) + ) +)] +async fn get_gateway( + Path(IdentityKeyParam { identity_key }): Path, + State(state): State, +) -> HttpResult> { + let db = state.db_pool(); + let res = state.cache().get_gateway_list(db).await; + + match res + .iter() + .find(|item| item.gateway_identity_key == identity_key) + { + Some(res) => Ok(Json(res.clone())), + None => Err(HttpError::invalid_input(identity_key)), + } +} diff --git a/nym-node-status-api/src/http/api/mixnodes.rs b/nym-node-status-api/src/http/api/mixnodes.rs new file mode 100644 index 00000000000..f42d0bf91c8 --- /dev/null +++ b/nym-node-status-api/src/http/api/mixnodes.rs @@ -0,0 +1,91 @@ +use axum::{ + extract::{Path, Query, State}, + Json, Router, +}; +use serde::Deserialize; +use tracing::instrument; +use utoipa::IntoParams; + +use crate::http::{ + error::{HttpError, HttpResult}, + models::{DailyStats, Mixnode}, + state::AppState, + PagedResult, Pagination, +}; + +pub(crate) fn routes() -> Router { + Router::new() + .route("/", axum::routing::get(mixnodes)) + .route("/:mix_id", axum::routing::get(get_mixnodes)) + .route("/stats", axum::routing::get(get_stats)) +} + +#[utoipa::path( + tag = "Mixnodes", + get, + params( + Pagination + ), + path = "/v2/mixnodes", + responses( + (status = 200, body = PagedMixnode) + ) +)] +#[instrument(level = tracing::Level::DEBUG, skip_all, fields(page=pagination.page, size=pagination.size))] +async fn mixnodes( + Query(pagination): Query, + State(state): State, +) -> HttpResult>> { + let db = state.db_pool(); + let res = state.cache().get_mixnodes_list(db).await; + + Ok(Json(PagedResult::paginate(pagination, res))) +} + +#[derive(Deserialize, IntoParams)] +#[into_params(parameter_in = Path)] +struct MixIdParam { + mix_id: String, +} + +#[utoipa::path( + tag = "Mixnodes", + get, + params( + MixIdParam + ), + path = "/v2/mixnodes/{mix_id}", + responses( + (status = 200, body = Mixnode) + ) +)] +#[instrument(level = tracing::Level::DEBUG, skip_all, fields(mix_id = mix_id))] +async fn get_mixnodes( + Path(MixIdParam { mix_id }): Path, + State(state): State, +) -> HttpResult> { + match mix_id.parse::() { + Ok(parsed_mix_id) => { + let res = state.cache().get_mixnodes_list(state.db_pool()).await; + + match res.iter().find(|item| item.mix_id == parsed_mix_id) { + Some(res) => Ok(Json(res.clone())), + None => Err(HttpError::invalid_input(mix_id)), + } + } + Err(_e) => Err(HttpError::invalid_input(mix_id)), + } +} + +#[utoipa::path( + tag = "Mixnodes", + get, + path = "/v2/mixnodes/stats", + responses( + (status = 200, body = Vec) + ) +)] +async fn get_stats(State(state): State) -> HttpResult>> { + let stats = state.cache().get_mixnode_stats(state.db_pool()).await; + Ok(Json(stats)) +} diff --git a/nym-node-status-api/src/http/api/mod.rs b/nym-node-status-api/src/http/api/mod.rs new file mode 100644 index 00000000000..4d385904b55 --- /dev/null +++ b/nym-node-status-api/src/http/api/mod.rs @@ -0,0 +1,85 @@ +use anyhow::anyhow; +use axum::{response::Redirect, Router}; +use tokio::net::ToSocketAddrs; +use tower_http::{cors::CorsLayer, trace::TraceLayer}; +use utoipa::OpenApi; +use utoipa_swagger_ui::SwaggerUi; + +use crate::http::{server::HttpServer, state::AppState}; + +pub(crate) mod gateways; +pub(crate) mod mixnodes; +pub(crate) mod services; +pub(crate) mod summary; +pub(crate) mod testruns; + +pub(crate) struct RouterBuilder { + unfinished_router: Router, +} + +impl RouterBuilder { + pub(crate) fn with_default_routes() -> Self { + let router = Router::new() + .merge( + SwaggerUi::new("/swagger") + .url("/api-docs/openapi.json", super::api_docs::ApiDoc::openapi()), + ) + .route( + "/", + axum::routing::get(|| async { Redirect::permanent("/swagger") }), + ) + .nest( + "/v2", + Router::new() + .nest("/gateways", gateways::routes()) + .nest("/mixnodes", mixnodes::routes()) + .nest("/services", services::routes()) + .nest("/summary", summary::routes()), + // .nest("/testruns", testruns::_routes()), + ); + + Self { + unfinished_router: router, + } + } + + pub(crate) fn with_state(self, state: AppState) -> RouterWithState { + RouterWithState { + router: self.finalize_routes().with_state(state), + } + } + + fn finalize_routes(self) -> Router { + // layers added later wrap earlier layers + self.unfinished_router + // CORS layer needs to wrap other API layers + .layer(setup_cors()) + // logger should be outermost layer + .layer(TraceLayer::new_for_http()) + } +} + +pub(crate) struct RouterWithState { + router: Router, +} + +impl RouterWithState { + pub(crate) async fn build_server( + self, + bind_address: A, + ) -> anyhow::Result { + tokio::net::TcpListener::bind(bind_address) + .await + .map(|listener| HttpServer::new(self.router, listener)) + .map_err(|err| anyhow!("Couldn't bind to address due to {}", err)) + } +} + +fn setup_cors() -> CorsLayer { + use axum::http::Method; + CorsLayer::new() + .allow_origin(tower_http::cors::Any) + .allow_methods([Method::POST, Method::GET, Method::PATCH, Method::OPTIONS]) + .allow_headers(tower_http::cors::Any) + .allow_credentials(false) +} diff --git a/nym-node-status-api/src/http/api/services/json_path.rs b/nym-node-status-api/src/http/api/services/json_path.rs new file mode 100644 index 00000000000..caefc6489db --- /dev/null +++ b/nym-node-status-api/src/http/api/services/json_path.rs @@ -0,0 +1,58 @@ +use serde_json_path::JsonPath; + +use crate::http::models::Gateway; + +pub(super) struct ParseJsonPaths { + pub(super) path_ip_address: JsonPath, + pub(super) path_hostname: JsonPath, + pub(super) path_service_provider_client_id: JsonPath, +} + +impl ParseJsonPaths { + pub fn new() -> Result { + Ok(ParseJsonPaths { + path_ip_address: JsonPath::parse("$.host_information.ip_address[0]")?, + path_hostname: JsonPath::parse("$.host_information.hostname")?, + path_service_provider_client_id: JsonPath::parse("$.network_requester.address")?, + }) + } +} + +pub(super) struct ParsedDetails { + pub(super) ip_address: Option, + pub(super) hostname: Option, + pub(super) service_provider_client_id: Option, +} + +impl ParsedDetails { + fn get_string_from_json_path( + value: &Option, + path: &JsonPath, + ) -> Option { + match value { + Some(value) => path + .query(value) + .exactly_one() + .map(|v2| v2.as_str().map(|v3| v3.to_string())) + .ok() + .flatten(), + None => None, + } + } + pub fn new(paths: &ParseJsonPaths, g: &Gateway) -> ParsedDetails { + ParsedDetails { + hostname: ParsedDetails::get_string_from_json_path( + &g.self_described, + &paths.path_hostname, + ), + ip_address: ParsedDetails::get_string_from_json_path( + &g.self_described, + &paths.path_ip_address, + ), + service_provider_client_id: ParsedDetails::get_string_from_json_path( + &g.self_described, + &paths.path_service_provider_client_id, + ), + } + } +} diff --git a/nym-node-status-api/src/http/api/services/mod.rs b/nym-node-status-api/src/http/api/services/mod.rs new file mode 100644 index 00000000000..5650684c43e --- /dev/null +++ b/nym-node-status-api/src/http/api/services/mod.rs @@ -0,0 +1,134 @@ +use crate::http::{ + error::{HttpError, HttpResult}, + models::Service, + state::AppState, + PagedResult, Pagination, +}; +use axum::{ + extract::{Query, State}, + Json, Router, +}; +use json_path::{ParseJsonPaths, ParsedDetails}; +use tracing::instrument; + +mod json_path; + +pub(crate) fn routes() -> Router { + Router::new().route("/", axum::routing::get(mixnodes)) +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, utoipa::IntoParams)] +#[into_params(parameter_in = Query)] +pub(crate) struct ServicesQueryParams { + size: Option, + page: Option, + wss: Option, + hostname: Option, + entry: Option, +} + +#[utoipa::path( + tag = "Services", + get, + params( + ServicesQueryParams, + ), + path = "/v2/services", + responses( + (status = 200, body = PagedService) + ) +)] +#[instrument(level = tracing::Level::DEBUG, skip(state))] +async fn mixnodes( + Query(ServicesQueryParams { + size, + page, + wss, + hostname, + entry, + }): Query, + State(state): State, +) -> HttpResult>> { + let db = state.db_pool(); + let cache = state.cache(); + + let show_only_wss = wss.unwrap_or(false); + let show_only_with_hostname = hostname.unwrap_or(false); + let show_entry_gateways_only = entry.unwrap_or(false); + + let paths = ParseJsonPaths::new().map_err(|e| { + tracing::error!("Invalidly configured ParseJsonPaths: {e}"); + HttpError::internal() + })?; + let res = cache.get_gateway_list(db).await; + let res: Vec = res + .iter() + .map(|g| { + let details = ParsedDetails::new(&paths, g); + + let s = Service { + gateway_identity_key: g.gateway_identity_key.clone(), + ip_address: details.ip_address, + service_provider_client_id: details.service_provider_client_id, + hostname: details.hostname, + last_successful_ping_utc: g.last_testrun_utc.clone(), + last_updated_utc: g.last_updated_utc.clone(), + // routing_score: g.routing_score, + routing_score: 1f32, + mixnet_websockets: g + .self_described + .clone() + .and_then(|s| s.get("mixnet_websockets").cloned()), + }; + + let f = ServiceFilter::new(&s); + + (s, f) + }) + .filter(|(_, f)| { + let mut keep = f.has_network_requester_sp; + + if show_entry_gateways_only { + keep = true; + } + + if show_only_wss { + keep &= f.has_wss; + } + if show_only_with_hostname { + keep &= f.has_hostname; + } + + keep + }) + .map(|(s, _)| s) + .collect(); + + Ok(Json(PagedResult::paginate(Pagination { size, page }, res))) +} + +struct ServiceFilter { + has_wss: bool, + has_network_requester_sp: bool, + has_hostname: bool, +} + +impl ServiceFilter { + fn new(s: &Service) -> Self { + let has_wss = match &s.mixnet_websockets { + Some(v) => v.get("wss_port").map(|v2| !v2.is_null()).unwrap_or(false), + None => false, + }; + let has_hostname = s.hostname.is_some(); + let has_network_requester_sp = match &s.service_provider_client_id { + Some(v) => !v.is_empty(), + None => false, + }; + + ServiceFilter { + has_wss, + has_hostname, + has_network_requester_sp, + } + } +} diff --git a/nym-node-status-api/src/http/api/summary.rs b/nym-node-status-api/src/http/api/summary.rs new file mode 100644 index 00000000000..729141509c8 --- /dev/null +++ b/nym-node-status-api/src/http/api/summary.rs @@ -0,0 +1,43 @@ +use axum::{extract::State, Json, Router}; +use tracing::instrument; + +use crate::{ + db::models::NetworkSummary, + http::{error::HttpResult, models::SummaryHistory, state::AppState}, +}; + +pub(crate) fn routes() -> Router { + Router::new() + .route("/", axum::routing::get(summary)) + .route("/history", axum::routing::get(summary_history)) +} + +#[utoipa::path( + tag = "Summary", + get, + path = "/v2/summary", + responses( + (status = 200, body = NetworkSummary) + ) +)] +#[instrument(level = tracing::Level::DEBUG, skip_all)] +async fn summary(State(state): State) -> HttpResult> { + crate::db::queries::get_summary(state.db_pool()) + .await + .map(Json) +} + +#[utoipa::path( + tag = "Summary", + get, + path = "/v2/summary/history", + responses( + (status = 200, body = Vec) + ) +)] +#[instrument(level = tracing::Level::DEBUG, skip_all)] +async fn summary_history(State(state): State) -> HttpResult>> { + Ok(Json( + state.cache().get_summary_history(state.db_pool()).await, + )) +} diff --git a/nym-node-status-api/src/http/api/testruns.rs b/nym-node-status-api/src/http/api/testruns.rs new file mode 100644 index 00000000000..875012b0fbb --- /dev/null +++ b/nym-node-status-api/src/http/api/testruns.rs @@ -0,0 +1,7 @@ +use axum::Router; + +use crate::http::state::AppState; + +pub(crate) fn _routes() -> Router { + unimplemented!() +} diff --git a/nym-node-status-api/src/http/api_docs.rs b/nym-node-status-api/src/http/api_docs.rs new file mode 100644 index 00000000000..172aa899a39 --- /dev/null +++ b/nym-node-status-api/src/http/api_docs.rs @@ -0,0 +1,15 @@ +use crate::http::{Gateway, GatewaySkinny, Mixnode, Service}; +use utoipa::OpenApi; +use utoipauto::utoipauto; + +// manually import external structs which are behind feature flags because they +// can't be automatically discovered +// https://github.com/ProbablyClem/utoipauto/issues/13#issuecomment-1974911829 +#[utoipauto(paths = "./nym-node-status-api/src")] +#[derive(OpenApi)] +#[openapi( + info(title = "Nym API"), + tags(), + components(schemas(nym_node_requests::api::v1::node::models::NodeDescription,)) +)] +pub(super) struct ApiDoc; diff --git a/nym-node-status-api/src/http/error.rs b/nym-node-status-api/src/http/error.rs new file mode 100644 index 00000000000..8bbd59e0959 --- /dev/null +++ b/nym-node-status-api/src/http/error.rs @@ -0,0 +1,28 @@ +pub(crate) type HttpResult = Result; + +pub(crate) struct HttpError { + message: String, + status: axum::http::StatusCode, +} + +impl HttpError { + pub(crate) fn invalid_input(message: String) -> Self { + Self { + message, + status: axum::http::StatusCode::BAD_REQUEST, + } + } + + pub(crate) fn internal() -> Self { + Self { + message: serde_json::json!({"message": "Internal server error"}).to_string(), + status: axum::http::StatusCode::INTERNAL_SERVER_ERROR, + } + } +} + +impl axum::response::IntoResponse for HttpError { + fn into_response(self) -> axum::response::Response { + (self.status, self.message).into_response() + } +} diff --git a/nym-node-status-api/src/http/mod.rs b/nym-node-status-api/src/http/mod.rs new file mode 100644 index 00000000000..1cc317337f9 --- /dev/null +++ b/nym-node-status-api/src/http/mod.rs @@ -0,0 +1,71 @@ +use models::{Gateway, GatewaySkinny, Mixnode, Service}; + +pub(crate) mod api; +pub(crate) mod api_docs; +pub(crate) mod error; +pub(crate) mod models; +pub(crate) mod server; +pub(crate) mod state; + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, utoipa::ToSchema)] +// exclude generic from auto-discovery +#[utoipauto::utoipa_ignore] +// https://docs.rs/utoipa/latest/utoipa/derive.ToSchema.html#generic-schemas-with-aliases +// Generic structs can only be included via aliases, not directly, because they +// it would cause an error in generated Swagger docs. +// Instead, you have to manually monomorphize each generic struct that +// you wish to document +#[aliases( + PagedGateway = PagedResult, + PagedGatewaySkinny = PagedResult, + PagedMixnode = PagedResult, + PagedService = PagedResult, +)] +pub struct PagedResult { + pub page: usize, + pub size: usize, + pub total: usize, + pub items: Vec, +} + +impl PagedResult { + pub fn paginate(pagination: Pagination, res: Vec) -> Self { + let total = res.len(); + let (size, mut page) = pagination.intoto_inner_values(); + + if page * size > total { + page = total / size; + } + + let chunks: Vec<&[T]> = res.chunks(size).collect(); + + PagedResult { + page, + size, + total, + items: chunks.get(page).cloned().unwrap_or_default().into(), + } + } +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, utoipa::IntoParams)] +#[into_params(parameter_in = Query)] +pub(crate) struct Pagination { + size: Option, + page: Option, +} + +impl Pagination { + // unwrap stored values or use predefined defaults + pub(crate) fn intoto_inner_values(self) -> (usize, usize) { + const SIZE_DEFAULT: usize = 10; + const SIZE_MAX: usize = 200; + + const PAGE_DEFAULT: usize = 0; + + ( + self.size.unwrap_or(SIZE_DEFAULT).min(SIZE_MAX), + self.page.unwrap_or(PAGE_DEFAULT), + ) + } +} diff --git a/nym-node-status-api/src/http/models.rs b/nym-node-status-api/src/http/models.rs new file mode 100644 index 00000000000..3a4c348b25b --- /dev/null +++ b/nym-node-status-api/src/http/models.rs @@ -0,0 +1,74 @@ +use nym_node_requests::api::v1::node::models::NodeDescription; +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; + +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +pub struct Gateway { + pub gateway_identity_key: String, + pub bonded: bool, + pub blacklisted: bool, + pub performance: u8, + pub self_described: Option, + pub explorer_pretty_bond: Option, + pub description: NodeDescription, + pub last_probe_result: Option, + pub last_probe_log: Option, + pub last_testrun_utc: Option, + pub last_updated_utc: String, + pub routing_score: f32, + pub config_score: u32, +} + +#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)] +pub struct GatewaySkinny { + pub gateway_identity_key: String, + pub self_described: Option, + pub explorer_pretty_bond: Option, + pub last_probe_result: Option, + pub last_testrun_utc: Option, + pub last_updated_utc: String, + pub routing_score: f32, + pub config_score: u32, + pub performance: u8, +} + +#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)] +pub struct Mixnode { + pub mix_id: u32, + pub bonded: bool, + pub blacklisted: bool, + pub is_dp_delegatee: bool, + pub total_stake: i64, + pub full_details: Option, + pub self_described: Option, + pub description: NodeDescription, + pub last_updated_utc: String, +} + +#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)] +pub struct DailyStats { + pub date_utc: String, + pub total_packets_received: i64, + pub total_packets_sent: i64, + pub total_packets_dropped: i64, + pub total_stake: i64, +} + +#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)] +pub struct Service { + pub gateway_identity_key: String, + pub last_updated_utc: String, + pub routing_score: f32, + pub service_provider_client_id: Option, + pub ip_address: Option, + pub hostname: Option, + pub mixnet_websockets: Option, + pub last_successful_ping_utc: Option, +} + +#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)] +pub(crate) struct SummaryHistory { + pub date: String, + pub value_json: serde_json::Value, + pub timestamp_utc: String, +} diff --git a/nym-node-status-api/src/http/server.rs b/nym-node-status-api/src/http/server.rs new file mode 100644 index 00000000000..694f5fa79ab --- /dev/null +++ b/nym-node-status-api/src/http/server.rs @@ -0,0 +1,92 @@ +use axum::Router; +use core::net::SocketAddr; +use tokio::{net::TcpListener, task::JoinHandle}; +use tokio_util::sync::{CancellationToken, WaitForCancellationFutureOwned}; + +use crate::{ + db::DbPool, + http::{api::RouterBuilder, state::AppState}, +}; + +/// Return handles that allow for graceful shutdown of server + awaiting its +/// background tokio task +pub(crate) async fn start_http_api( + db_pool: DbPool, + http_port: u16, + nym_http_cache_ttl: u64, +) -> anyhow::Result { + let router_builder = RouterBuilder::with_default_routes(); + + let state = AppState::new(db_pool, nym_http_cache_ttl); + let router = router_builder.with_state(state); + + // TODO dz do we need this to be configurable? + let bind_addr = format!("0.0.0.0:{}", http_port); + let server = router.build_server(bind_addr).await?; + + Ok(start_server(server)) +} + +fn start_server(server: HttpServer) -> ShutdownHandles { + // one copy is stored to trigger a graceful shutdown later + let shutdown_button = CancellationToken::new(); + // other copy is given to server to listen for a shutdown + let shutdown_receiver = shutdown_button.clone(); + let shutdown_receiver = shutdown_receiver.cancelled_owned(); + + let server_handle = tokio::spawn(async move { server.run(shutdown_receiver).await }); + + ShutdownHandles { + server_handle, + shutdown_button, + } +} + +pub(crate) struct ShutdownHandles { + server_handle: JoinHandle>, + shutdown_button: CancellationToken, +} + +impl ShutdownHandles { + /// Send graceful shutdown signal to server and wait for server task to complete + pub(crate) async fn shutdown(self) -> anyhow::Result<()> { + self.shutdown_button.cancel(); + + match self.server_handle.await { + Ok(Ok(_)) => { + tracing::info!("HTTP server shut down without errors"); + } + Ok(Err(err)) => { + tracing::error!("HTTP server terminated with: {err}"); + anyhow::bail!(err) + } + Err(err) => { + tracing::error!("Server task panicked: {err}"); + } + }; + + Ok(()) + } +} + +pub(crate) struct HttpServer { + router: Router, + listener: TcpListener, +} + +impl HttpServer { + pub(crate) fn new(router: Router, listener: TcpListener) -> Self { + Self { router, listener } + } + + pub(crate) async fn run(self, receiver: WaitForCancellationFutureOwned) -> std::io::Result<()> { + // into_make_service_with_connect_info allows us to see client ip address + axum::serve( + self.listener, + self.router + .into_make_service_with_connect_info::(), + ) + .with_graceful_shutdown(receiver) + .await + } +} diff --git a/nym-node-status-api/src/http/state.rs b/nym-node-status-api/src/http/state.rs new file mode 100644 index 00000000000..6bccea39a12 --- /dev/null +++ b/nym-node-status-api/src/http/state.rs @@ -0,0 +1,223 @@ +use std::{sync::Arc, time::Duration}; + +use moka::{future::Cache, Entry}; +use tokio::sync::RwLock; + +use crate::{ + db::DbPool, + http::models::{DailyStats, Gateway, Mixnode, SummaryHistory}, +}; + +#[derive(Debug, Clone)] +pub(crate) struct AppState { + db_pool: DbPool, + cache: HttpCache, +} + +impl AppState { + pub(crate) fn new(db_pool: DbPool, cache_ttl: u64) -> Self { + Self { + db_pool, + cache: HttpCache::new(cache_ttl), + } + } + + pub(crate) fn db_pool(&self) -> &DbPool { + &self.db_pool + } + + pub(crate) fn cache(&self) -> &HttpCache { + &self.cache + } +} + +static GATEWAYS_LIST_KEY: &str = "gateways"; +static MIXNODES_LIST_KEY: &str = "mixnodes"; +static MIXSTATS_LIST_KEY: &str = "mixstats"; +static SUMMARY_HISTORY_LIST_KEY: &str = "summary-history"; + +#[derive(Debug, Clone)] +pub(crate) struct HttpCache { + gateways: Cache>>>, + mixnodes: Cache>>>, + mixstats: Cache>>>, + history: Cache>>>, +} + +impl HttpCache { + pub fn new(ttl_seconds: u64) -> Self { + HttpCache { + gateways: Cache::builder() + .max_capacity(2) + .time_to_live(Duration::from_secs(ttl_seconds)) + .build(), + mixnodes: Cache::builder() + .max_capacity(2) + .time_to_live(Duration::from_secs(ttl_seconds)) + .build(), + mixstats: Cache::builder() + .max_capacity(2) + .time_to_live(Duration::from_secs(ttl_seconds)) + .build(), + history: Cache::builder() + .max_capacity(2) + .time_to_live(Duration::from_secs(ttl_seconds)) + .build(), + } + } + + pub async fn upsert_gateway_list( + &self, + new_gateway_list: Vec, + ) -> Entry>>> { + self.gateways + .entry_by_ref(GATEWAYS_LIST_KEY) + .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_gateway_list(&self, db: &DbPool) -> Vec { + match self.gateways.get(GATEWAYS_LIST_KEY).await { + Some(guard) => { + let read_lock = guard.read().await; + read_lock.clone() + } + None => { + // the key is missing so populate it + tracing::warn!("No gateways in cache, refreshing cache from DB..."); + + 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"); + } + + gateways + } + } + } + + pub async fn upsert_mixnode_list( + &self, + new_mixnode_list: Vec, + ) -> Entry>>> { + self.mixnodes + .entry_by_ref(MIXNODES_LIST_KEY) + .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_mixnode_list; + v.clone() + } else { + Arc::new(RwLock::new(new_mixnode_list)) + } + }) + .await + } + + pub async fn get_mixnodes_list(&self, db: &DbPool) -> Vec { + match self.mixnodes.get(MIXNODES_LIST_KEY).await { + Some(guard) => { + let read_lock = guard.read().await; + read_lock.clone() + } + None => { + tracing::warn!("No mixnodes in cache, refreshing cache from DB..."); + + 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"); + } + + mixnodes + } + } + } + + pub async fn upsert_mixnode_stats( + &self, + mixnode_stats: Vec, + ) -> Entry>>> { + self.mixstats + .entry_by_ref(MIXSTATS_LIST_KEY) + .and_upsert_with(|maybe_entry| async { + if let Some(entry) = maybe_entry { + let v = entry.into_value(); + let mut guard = v.write().await; + *guard = mixnode_stats; + v.clone() + } else { + Arc::new(RwLock::new(mixnode_stats)) + } + }) + .await + } + + pub async fn get_mixnode_stats(&self, db: &DbPool) -> Vec { + match self.mixstats.get(MIXSTATS_LIST_KEY).await { + Some(guard) => { + let read_lock = guard.read().await; + read_lock.to_vec() + } + None => { + let mixnode_stats = crate::db::queries::get_daily_stats(db) + .await + .unwrap_or_default(); + self.upsert_mixnode_stats(mixnode_stats.clone()).await; + mixnode_stats + } + } + } + + pub async fn get_summary_history(&self, db: &DbPool) -> Vec { + match self.history.get(SUMMARY_HISTORY_LIST_KEY).await { + Some(guard) => { + let read_lock = guard.read().await; + read_lock.to_vec() + } + None => { + let summary_history = crate::db::queries::get_summary_history(db) + .await + .unwrap_or(vec![]); + self.upsert_summary_history(summary_history.clone()).await; + summary_history + } + } + } + + pub async fn upsert_summary_history( + &self, + summary_history: Vec, + ) -> Entry>>> { + self.history + .entry_by_ref(SUMMARY_HISTORY_LIST_KEY) + .and_upsert_with(|maybe_entry| async { + if let Some(entry) = maybe_entry { + let v = entry.into_value(); + let mut guard = v.write().await; + *guard = summary_history; + v.clone() + } else { + Arc::new(RwLock::new(summary_history)) + } + }) + .await + } +} diff --git a/nym-node-status-api/src/logging.rs b/nym-node-status-api/src/logging.rs index d61cd78ee18..01dd31562e4 100644 --- a/nym-node-status-api/src/logging.rs +++ b/nym-node-status-api/src/logging.rs @@ -2,8 +2,11 @@ use tracing::level_filters::LevelFilter; use tracing_subscriber::{filter::Directive, EnvFilter}; pub(crate) fn setup_tracing_logger() { - fn directive_checked(directive: String) -> Directive { - directive.parse().expect("Failed to parse log directive") + fn directive_checked(directive: impl Into) -> Directive { + directive + .into() + .parse() + .expect("Failed to parse log directive") } let log_builder = tracing_subscriber::fmt() @@ -13,6 +16,7 @@ pub(crate) fn setup_tracing_logger() { .with_file(true) // Display source code line numbers .with_line_number(true) + .with_thread_ids(true) // Don't display the event's target (module path) .with_target(false); @@ -22,20 +26,23 @@ pub(crate) fn setup_tracing_logger() { .from_env_lossy(); // these crates are more granularly filtered let filter_crates = [ - "nym_bin_common", - "nym_explorer_client", - "nym_network_defaults", - "nym_validator_client", "reqwest", "rustls", "hyper", "sqlx", "h2", "tendermint_rpc", + "tower_http", + "axum", ]; for crate_name in filter_crates { filter = filter.add_directive(directive_checked(format!("{}=warn", crate_name))); } + filter = filter.add_directive(directive_checked("nym_bin_common=debug")); + filter = filter.add_directive(directive_checked("nym_explorer_client=debug")); + filter = filter.add_directive(directive_checked("nym_network_defaults=debug")); + filter = filter.add_directive(directive_checked("nym_validator_client=debug")); + log_builder.with_env_filter(filter).init(); } diff --git a/nym-node-status-api/src/main.rs b/nym-node-status-api/src/main.rs index 6296406701a..0fb1d0e1d2e 100644 --- a/nym-node-status-api/src/main.rs +++ b/nym-node-status-api/src/main.rs @@ -1,9 +1,13 @@ -use anyhow::anyhow; use clap::Parser; use nym_network_defaults::setup_env; +use nym_task::signal::wait_for_signal; + +use crate::config::read_env_var; mod cli; +mod config; mod db; +mod http; mod logging; mod monitor; @@ -15,24 +19,36 @@ async fn main() -> anyhow::Result<()> { // if dotenv file is present, load its values // otherwise, default to mainnet setup_env(args.config_env_file.as_ref()); - tracing::debug!("{:?}", std::env::var("NETWORK_NAME")); - tracing::debug!("{:?}", std::env::var("EXPLORER_API")); - tracing::debug!("{:?}", std::env::var("NYM_API")); + tracing::debug!("{:?}", read_env_var("NETWORK_NAME")); + tracing::debug!("{:?}", read_env_var("EXPLORER_API")); + tracing::debug!("{:?}", read_env_var("NYM_API")); + + let conf = config::Config::from_env()?; + tracing::debug!("Using config:\n{:#?}", conf); let storage = db::Storage::init().await?; - monitor::spawn_in_background(storage) - .await - .expect("Monitor task failed"); - tracing::info!("Started server"); + let db_pool = storage.pool_owned().await; + let conf_clone = conf.clone(); + tokio::spawn(async move { + monitor::spawn_in_background(db_pool, conf_clone).await; + }); + tracing::info!("Started monitor task"); - Ok(()) -} + let shutdown_handles = http::server::start_http_api( + storage.pool_owned().await, + conf.http_port(), + conf.nym_http_cache_ttl(), + ) + .await + .expect("Failed to start server"); -pub(crate) fn read_env_var(env_var: &str) -> anyhow::Result { - std::env::var(env_var) - .map(|value| { - tracing::trace!("{}={}", env_var, value); - value - }) - .map_err(|_| anyhow!("You need to set {}", env_var)) + tracing::info!("Started HTTP server on port {}", conf.http_port()); + + wait_for_signal().await; + + if let Err(err) = shutdown_handles.shutdown().await { + tracing::error!("{err}"); + }; + + Ok(()) } diff --git a/nym-node-status-api/src/monitor/mod.rs b/nym-node-status-api/src/monitor/mod.rs index 810320bdd0c..4215e297ccf 100644 --- a/nym-node-status-api/src/monitor/mod.rs +++ b/nym-node-status-api/src/monitor/mod.rs @@ -1,10 +1,11 @@ +use crate::config::Config; use crate::db::models::{ gateway, mixnode, GatewayRecord, MixnodeRecord, NetworkSummary, GATEWAYS_BLACKLISTED_COUNT, GATEWAYS_BONDED_COUNT, GATEWAYS_EXPLORER_COUNT, GATEWAYS_HISTORICAL_COUNT, MIXNODES_BLACKLISTED_COUNT, MIXNODES_BONDED_ACTIVE, MIXNODES_BONDED_COUNT, MIXNODES_BONDED_INACTIVE, MIXNODES_BONDED_RESERVE, MIXNODES_HISTORICAL_COUNT, }; -use crate::db::{queries, DbPool, Storage}; +use crate::db::{queries, DbPool}; use anyhow::anyhow; use cosmwasm_std::Decimal; use nym_explorer_client::{ExplorerClient, PrettyDetailedGatewayBond}; @@ -15,43 +16,46 @@ use nym_validator_client::nym_nodes::SkimmedNode; use nym_validator_client::nyxd::contract_traits::PagedMixnetQueryClient; use nym_validator_client::nyxd::{AccountId, NyxdClient}; use nym_validator_client::NymApiClient; +use reqwest::Url; use std::collections::HashSet; use std::str::FromStr; use tokio::task::JoinHandle; use tokio::time::Duration; const REFRESH_DELAY: Duration = Duration::from_secs(60 * 5); -const FAILURE_RETRY_DELAY: Duration = Duration::from_secs(60); +const FAILURE_RETRY_DELAY: Duration = Duration::from_secs(15); + static DELEGATION_PROGRAM_WALLET: &str = "n1rnxpdpx3kldygsklfft0gech7fhfcux4zst5lw"; // TODO dz: query many NYM APIs: // multiple instances running directory cache, ask sachin -pub(crate) fn spawn_in_background(storage: Storage) -> JoinHandle<()> { - tokio::spawn(async move { - let db_pool = storage.pool().await; - let network_defaults = nym_network_defaults::NymNetworkDetails::new_from_env(); - - loop { - tracing::info!("Refreshing node info..."); - - if let Err(e) = run(db_pool, &network_defaults).await { - tracing::error!( - "Monitor run failed: {e}, retrying in {}s...", - FAILURE_RETRY_DELAY.as_secs() - ); - tokio::time::sleep(FAILURE_RETRY_DELAY).await; - } else { - tracing::info!( - "Info successfully collected, sleeping for {}s...", - REFRESH_DELAY.as_secs() - ); - tokio::time::sleep(REFRESH_DELAY).await; - } +pub(crate) async fn spawn_in_background(db_pool: DbPool, config: Config) -> JoinHandle<()> { + let network_defaults = nym_network_defaults::NymNetworkDetails::new_from_env(); + + loop { + tracing::info!("Refreshing node info..."); + + if let Err(e) = run(&db_pool, &network_defaults, &config).await { + tracing::error!( + "Monitor run failed: {e}, retrying in {}s...", + FAILURE_RETRY_DELAY.as_secs() + ); + tokio::time::sleep(FAILURE_RETRY_DELAY).await; + } else { + tracing::info!( + "Info successfully collected, sleeping for {}s...", + REFRESH_DELAY.as_secs() + ); + tokio::time::sleep(REFRESH_DELAY).await; } - }) + } } -async fn run(pool: &DbPool, network_details: &NymNetworkDetails) -> anyhow::Result<()> { +async fn run( + pool: &DbPool, + network_details: &NymNetworkDetails, + config: &Config, +) -> anyhow::Result<()> { let default_api_url = network_details .endpoints .first() @@ -66,15 +70,32 @@ async fn run(pool: &DbPool, network_details: &NymNetworkDetails) -> anyhow::Resu let default_explorer_url = default_explorer_url.expect("explorer url missing in network config"); - let explorer_client = ExplorerClient::new(default_explorer_url)?; - let explorer_gateways = explorer_client.get_gateways().await?; + let explorer_client = ExplorerClient::new_with_timeout( + default_explorer_url, + config.nym_explorer_client_timeout(), + )?; + let explorer_gateways = explorer_client + .get_gateways() + .await + .log_error("get_gateways")?; + tracing::debug!("6"); - let api_client = NymApiClient::new(default_api_url); - let gateways = api_client.get_cached_described_gateways().await?; + let api_client = + NymApiClient::new_with_timeout(default_api_url, config.nym_api_client_timeout()); + let gateways = api_client + .get_cached_described_gateways() + .await + .log_error("get_described_gateways")?; tracing::debug!("Fetched {} gateways", gateways.len()); - let skimmed_gateways = api_client.get_basic_gateways(None).await?; + let skimmed_gateways = api_client + .get_basic_gateways(None) + .await + .log_error("get_basic_gateways")?; - let mixnodes = api_client.get_cached_mixnodes().await?; + let mixnodes = api_client + .get_cached_mixnodes() + .await + .log_error("get_cached_mixnodes")?; tracing::debug!("Fetched {} mixnodes", mixnodes.len()); // TODO dz can we calculate blacklisted GWs from their performance? @@ -83,17 +104,28 @@ async fn run(pool: &DbPool, network_details: &NymNetworkDetails) -> anyhow::Resu .nym_api .get_gateways_blacklisted() .await - .map(|vec| vec.into_iter().collect::>())?; + .map(|vec| vec.into_iter().collect::>()) + .log_error("get_gateways_blacklisted")?; // Cached mixnodes don't include blacklisted nodes // We need that to calculate the total locked tokens later let mixnodes = api_client .nym_api .get_mixnodes_detailed_unfiltered() - .await?; - let mixnodes_described = api_client.nym_api.get_mixnodes_described().await?; - let mixnodes_active = api_client.nym_api.get_active_mixnodes().await?; - let delegation_program_members = get_delegation_program_details(network_details).await?; + .await + .log_error("get_mixnodes_detailed_unfiltered")?; + let mixnodes_described = api_client + .nym_api + .get_mixnodes_described() + .await + .log_error("get_mixnodes_described")?; + let mixnodes_active = api_client + .nym_api + .get_active_mixnodes() + .await + .log_error("get_active_mixnodes")?; + let delegation_program_members = + get_delegation_program_details(network_details, config.nyxd_addr()).await?; // keep stats for later let count_bonded_mixnodes = mixnodes.len(); @@ -168,42 +200,41 @@ async fn run(pool: &DbPool, network_details: &NymNetworkDetails) -> anyhow::Resu (GATEWAYS_BLACKLISTED_COUNT, &count_gateways_blacklisted), ]; - // TODO dz do we need signed int in type definition? maybe because of API? let last_updated = chrono::offset::Utc::now(); let last_updated_utc = last_updated.timestamp().to_string(); let network_summary = NetworkSummary { mixnodes: mixnode::MixnodeSummary { bonded: mixnode::MixnodeSummaryBonded { - count: count_bonded_mixnodes as i32, - active: count_bonded_mixnodes_active as i32, - inactive: count_bonded_mixnodes_inactive as i32, - reserve: count_bonded_mixnodes_reserve as i32, + count: count_bonded_mixnodes.cast_checked()?, + active: count_bonded_mixnodes_active.cast_checked()?, + inactive: count_bonded_mixnodes_inactive.cast_checked()?, + reserve: count_bonded_mixnodes_reserve.cast_checked()?, last_updated_utc: last_updated_utc.to_owned(), }, blacklisted: mixnode::MixnodeSummaryBlacklisted { - count: count_mixnodes_blacklisted as i32, + count: count_mixnodes_blacklisted.cast_checked()?, last_updated_utc: last_updated_utc.to_owned(), }, historical: mixnode::MixnodeSummaryHistorical { - count: all_historical_mixnodes as i32, + count: all_historical_mixnodes.cast_checked()?, last_updated_utc: last_updated_utc.to_owned(), }, }, gateways: gateway::GatewaySummary { bonded: gateway::GatewaySummaryBonded { - count: count_bonded_gateways as i32, + count: count_bonded_gateways.cast_checked()?, last_updated_utc: last_updated_utc.to_owned(), }, blacklisted: gateway::GatewaySummaryBlacklisted { - count: count_gateways_blacklisted as i32, + count: count_gateways_blacklisted.cast_checked()?, last_updated_utc: last_updated_utc.to_owned(), }, historical: gateway::GatewaySummaryHistorical { - count: all_historical_gateways as i32, + count: all_historical_gateways.cast_checked()?, last_updated_utc: last_updated_utc.to_owned(), }, explorer: gateway::GatewaySummaryExplorer { - count: count_explorer_gateways as i32, + count: count_explorer_gateways.cast_checked()?, last_updated_utc: last_updated_utc.to_owned(), }, }, @@ -317,27 +348,56 @@ fn prepare_mixnode_data( Ok(mixnode_records) } +// TODO dz is there a common monorepo place this can be put? +pub trait NumericalCheckedCast +where + T: TryFrom, + >::Error: std::error::Error, + Self: std::fmt::Display + Copy, +{ + fn cast_checked(self) -> anyhow::Result { + T::try_from(self).map_err(|e| { + anyhow::anyhow!( + "Couldn't cast {} to {}: {}", + self, + std::any::type_name::(), + e + ) + }) + } +} + +impl NumericalCheckedCast for T +where + U: TryFrom, + >::Error: std::error::Error, + T: std::fmt::Display + Copy, +{ +} + async fn calculate_stats(pool: &DbPool) -> anyhow::Result<(usize, usize)> { let mut conn = pool.acquire().await?; let all_historical_gateways = sqlx::query_scalar!(r#"SELECT count(id) FROM gateways"#) .fetch_one(&mut *conn) - .await? as usize; + .await? + .cast_checked()?; let all_historical_mixnodes = sqlx::query_scalar!(r#"SELECT count(id) FROM mixnodes"#) .fetch_one(&mut *conn) - .await? as usize; + .await? + .cast_checked()?; Ok((all_historical_gateways, all_historical_mixnodes)) } async fn get_delegation_program_details( network_details: &NymNetworkDetails, + nyxd_addr: &Url, ) -> anyhow::Result> { let config = nym_validator_client::nyxd::Config::try_from_nym_network_details(network_details)?; - // TODO dz should this be configurable? - let client = NyxdClient::connect(config, "https://rpc.nymtech.net") + let client = NyxdClient::connect(config, nyxd_addr.as_str()) .map_err(|err| anyhow::anyhow!("Couldn't connect: {}", err))?; let account_id = AccountId::from_str(DELEGATION_PROGRAM_WALLET) @@ -372,3 +432,19 @@ fn decimal_to_i64(decimal: Decimal) -> i64 { rounded_value as i64 } + +trait LogError { + fn log_error(self, msg: &str) -> Result; +} + +impl LogError for anyhow::Result +where + E: std::error::Error, +{ + fn log_error(self, msg: &str) -> Result { + if let Err(e) = &self { + tracing::error!("[{msg}]:\t{e}"); + } + self + } +} From e5a29cc76e9c91ae0e4ea36a9f89c2944d3e96f3 Mon Sep 17 00:00:00 2001 From: dynco-nym <173912580+dynco-nym@users.noreply.github.com> Date: Mon, 14 Oct 2024 16:32:39 +0200 Subject: [PATCH 3/6] Work with directory pre-v2.1 Rebase + point to earlier network client code Adjust to new Nym API types Refer to earlier client code Revert "Rebase + point to earlier network client code" This reverts commit dd75e7dc0695c25b0883e2f5dd15b7d70165e9e8. Point to earlier commit --- Cargo.lock | 1028 ++++++++++++----- Cargo.toml | 1 + nym-node-status-api/Cargo.toml | 10 +- nym-node-status-api/src/config.rs | 7 - nym-node-status-api/src/db/mod.rs | 9 +- .../src/db/queries/gateways.rs | 2 +- nym-node-status-api/src/db/queries/misc.rs | 2 +- .../src/db/queries/mixnodes.rs | 4 +- nym-node-status-api/src/db/queries/summary.rs | 4 +- nym-node-status-api/src/monitor/mod.rs | 3 +- 10 files changed, 727 insertions(+), 343 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0cab5a15ee1..b9d06b3a8e9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2153,8 +2153,8 @@ dependencies = [ "cosmwasm-std", "cosmwasm-storage", "cw-storage-plus", - "nym-coconut-dkg-common", - "nym-contracts-common", + "nym-coconut-dkg-common 0.1.0", + "nym-contracts-common 0.5.0", ] [[package]] @@ -2415,13 +2415,13 @@ dependencies = [ "itertools 0.13.0", "log", "maxminddb", - "nym-bin-common", - "nym-contracts-common", + "nym-bin-common 0.6.0", + "nym-contracts-common 0.5.0", "nym-explorer-api-requests", - "nym-mixnet-contract-common", - "nym-network-defaults", + "nym-mixnet-contract-common 0.6.0", + "nym-network-defaults 0.1.0", "nym-task", - "nym-validator-client", + "nym-validator-client 0.1.0", "okapi", "pretty_env_logger", "rand", @@ -3404,11 +3404,11 @@ dependencies = [ "clap 4.5.20", "dirs", "importer-contract", - "nym-bin-common", - "nym-mixnet-contract-common", - "nym-network-defaults", - "nym-validator-client", - "nym-vesting-contract-common", + "nym-bin-common 0.6.0", + "nym-mixnet-contract-common 0.6.0", + "nym-network-defaults 0.1.0", + "nym-validator-client 0.1.0", + "nym-vesting-contract-common 0.7.0", "serde", "serde_json", "tokio", @@ -4077,8 +4077,8 @@ dependencies = [ "async-trait", "futures", "js-sys", - "nym-bin-common", - "nym-http-api-client", + "nym-bin-common 0.6.0", + "nym-http-api-client 0.1.0", "nym-ordered-buffer", "nym-service-providers-common", "nym-socks5-requests", @@ -4437,37 +4437,37 @@ dependencies = [ "humantime-serde", "itertools 0.13.0", "k256", - "nym-api-requests", + "nym-api-requests 0.1.0", "nym-bandwidth-controller", - "nym-bin-common", + "nym-bin-common 0.6.0", "nym-coconut", - "nym-coconut-dkg-common", - "nym-compact-ecash", - "nym-config", - "nym-contracts-common", + "nym-coconut-dkg-common 0.1.0", + "nym-compact-ecash 0.1.0", + "nym-config 0.1.0", + "nym-contracts-common 0.5.0", "nym-credential-storage", "nym-credentials", - "nym-credentials-interface", - "nym-crypto", + "nym-credentials-interface 0.1.0", + "nym-crypto 0.4.0", "nym-dkg", - "nym-ecash-contract-common", + "nym-ecash-contract-common 0.1.0", "nym-ecash-double-spending", - "nym-ecash-time", + "nym-ecash-time 0.1.0", "nym-gateway-client", "nym-http-api-common", "nym-inclusion-probability", - "nym-mixnet-contract-common", - "nym-multisig-contract-common", - "nym-node-requests", + "nym-mixnet-contract-common 0.6.0", + "nym-multisig-contract-common 0.1.0", + "nym-node-requests 0.1.0", "nym-node-tester-utils", - "nym-pemstore", - "nym-serde-helpers", + "nym-pemstore 0.3.0", + "nym-serde-helpers 0.1.0", "nym-sphinx", "nym-task", "nym-topology", "nym-types", - "nym-validator-client", - "nym-vesting-contract-common", + "nym-validator-client 0.1.0", + "nym-vesting-contract-common 0.7.0", "pin-project", "rand", "rand_chacha", @@ -4502,14 +4502,14 @@ dependencies = [ "cosmwasm-std", "ecdsa", "getset", - "nym-compact-ecash", - "nym-credentials-interface", - "nym-crypto", - "nym-ecash-time", - "nym-mixnet-contract-common", - "nym-network-defaults", - "nym-node-requests", - "nym-serde-helpers", + "nym-compact-ecash 0.1.0", + "nym-credentials-interface 0.1.0", + "nym-crypto 0.4.0", + "nym-ecash-time 0.1.0", + "nym-mixnet-contract-common 0.6.0", + "nym-network-defaults 0.1.0", + "nym-node-requests 0.1.0", + "nym-serde-helpers 0.1.0", "schemars", "serde", "serde_json", @@ -4521,6 +4521,32 @@ dependencies = [ "utoipa", ] +[[package]] +name = "nym-api-requests" +version = "0.1.0" +source = "git+https://github.com/nymtech/nym?rev=f86e08866#f86e0886631a98b0638fe09e6fcbe5458d47adc1" +dependencies = [ + "bs58", + "cosmrs 0.17.0-pre", + "cosmwasm-std", + "ecdsa", + "getset", + "nym-compact-ecash 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", + "nym-credentials-interface 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", + "nym-crypto 0.4.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", + "nym-ecash-time 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", + "nym-mixnet-contract-common 0.6.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", + "nym-node-requests 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", + "nym-serde-helpers 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", + "schemars", + "serde", + "sha2 0.10.8", + "tendermint 0.37.0", + "thiserror", + "time", + "utoipa", +] + [[package]] name = "nym-async-file-watcher" version = "0.1.0" @@ -4546,16 +4572,16 @@ dependencies = [ "ipnetwork 0.20.0", "log", "nym-authenticator-requests", - "nym-bin-common", + "nym-bin-common 0.6.0", "nym-client-core", - "nym-config", + "nym-config 0.1.0", "nym-credential-verification", - "nym-credentials-interface", - "nym-crypto", + "nym-credentials-interface 0.1.0", + "nym-crypto 0.4.0", "nym-gateway-requests", "nym-gateway-storage", "nym-id", - "nym-network-defaults", + "nym-network-defaults 0.1.0", "nym-sdk", "nym-service-provider-requests-common", "nym-service-providers-common", @@ -4563,7 +4589,7 @@ dependencies = [ "nym-task", "nym-types", "nym-wireguard", - "nym-wireguard-types", + "nym-wireguard-types 0.1.0", "rand", "serde", "serde_json", @@ -4581,11 +4607,11 @@ dependencies = [ "base64 0.22.1", "bincode", "hmac", - "nym-credentials-interface", - "nym-crypto", + "nym-credentials-interface 0.1.0", + "nym-crypto 0.4.0", "nym-service-provider-requests-common", "nym-sphinx", - "nym-wireguard-types", + "nym-wireguard-types 0.1.0", "rand", "serde", "sha2 0.10.8", @@ -4601,12 +4627,12 @@ dependencies = [ "log", "nym-credential-storage", "nym-credentials", - "nym-credentials-interface", - "nym-crypto", - "nym-ecash-contract-common", - "nym-ecash-time", - "nym-network-defaults", - "nym-validator-client", + "nym-credentials-interface 0.1.0", + "nym-crypto 0.4.0", + "nym-ecash-contract-common 0.1.0", + "nym-ecash-time 0.1.0", + "nym-network-defaults 0.1.0", + "nym-validator-client 0.1.0", "rand", "thiserror", "url", @@ -4636,6 +4662,21 @@ dependencies = [ "vergen", ] +[[package]] +name = "nym-bin-common" +version = "0.6.0" +source = "git+https://github.com/nymtech/nym?rev=f86e08866#f86e0886631a98b0638fe09e6fcbe5458d47adc1" +dependencies = [ + "const-str", + "log", + "pretty_env_logger", + "schemars", + "semver 1.0.23", + "serde", + "utoipa", + "vergen", +] + [[package]] name = "nym-bity-integration" version = "0.1.0" @@ -4645,7 +4686,7 @@ dependencies = [ "eyre", "k256", "nym-cli-commands", - "nym-validator-client", + "nym-validator-client 0.1.0", "serde", "serde_json", "thiserror", @@ -4665,10 +4706,10 @@ dependencies = [ "dotenvy", "inquire", "log", - "nym-bin-common", + "nym-bin-common 0.6.0", "nym-cli-commands", - "nym-network-defaults", - "nym-validator-client", + "nym-network-defaults 0.1.0", + "nym-validator-client 0.1.0", "pretty_env_logger", "serde", "serde_json", @@ -4699,26 +4740,26 @@ dependencies = [ "k256", "log", "nym-bandwidth-controller", - "nym-bin-common", + "nym-bin-common 0.6.0", "nym-client-core", - "nym-coconut-dkg-common", - "nym-config", - "nym-contracts-common", + "nym-coconut-dkg-common 0.1.0", + "nym-config 0.1.0", + "nym-contracts-common 0.5.0", "nym-credential-storage", "nym-credential-utils", "nym-credentials", - "nym-credentials-interface", - "nym-crypto", - "nym-ecash-contract-common", + "nym-credentials-interface 0.1.0", + "nym-crypto 0.4.0", + "nym-ecash-contract-common 0.1.0", "nym-id", - "nym-mixnet-contract-common", - "nym-multisig-contract-common", - "nym-network-defaults", - "nym-pemstore", + "nym-mixnet-contract-common 0.6.0", + "nym-multisig-contract-common 0.1.0", + "nym-network-defaults 0.1.0", + "nym-pemstore 0.3.0", "nym-sphinx", "nym-types", - "nym-validator-client", - "nym-vesting-contract-common", + "nym-validator-client 0.1.0", + "nym-vesting-contract-common 0.7.0", "rand", "serde", "serde_json", @@ -4742,21 +4783,21 @@ dependencies = [ "futures", "log", "nym-bandwidth-controller", - "nym-bin-common", + "nym-bin-common 0.6.0", "nym-client-core", "nym-client-websocket-requests", - "nym-config", + "nym-config 0.1.0", "nym-credential-storage", "nym-credentials", - "nym-crypto", + "nym-crypto 0.4.0", "nym-gateway-requests", "nym-id", - "nym-network-defaults", - "nym-pemstore", + "nym-network-defaults 0.1.0", + "nym-pemstore 0.3.0", "nym-sphinx", "nym-task", "nym-topology", - "nym-validator-client", + "nym-validator-client 0.1.0", "rand", "serde", "serde_json", @@ -4790,24 +4831,24 @@ dependencies = [ "nym-client-core-config-types", "nym-client-core-gateways-storage", "nym-client-core-surb-storage", - "nym-config", + "nym-config 0.1.0", "nym-country-group", "nym-credential-storage", - "nym-credentials-interface", - "nym-crypto", - "nym-ecash-time", + "nym-credentials-interface 0.1.0", + "nym-crypto 0.4.0", + "nym-ecash-time 0.1.0", "nym-explorer-client", "nym-gateway-client", "nym-gateway-requests", "nym-id", "nym-metrics", - "nym-network-defaults", + "nym-network-defaults 0.1.0", "nym-nonexhaustive-delayqueue", - "nym-pemstore", + "nym-pemstore 0.3.0", "nym-sphinx", "nym-task", "nym-topology", - "nym-validator-client", + "nym-validator-client 0.1.0", "rand", "rand_chacha", "serde", @@ -4835,9 +4876,9 @@ name = "nym-client-core-config-types" version = "0.1.0" dependencies = [ "humantime-serde", - "nym-config", + "nym-config 0.1.0", "nym-country-group", - "nym-pemstore", + "nym-pemstore 0.3.0", "nym-sphinx-addressing", "nym-sphinx-params", "serde", @@ -4852,7 +4893,7 @@ dependencies = [ "async-trait", "cosmrs 0.17.0-pre", "log", - "nym-crypto", + "nym-crypto 0.4.0", "nym-gateway-requests", "serde", "sqlx", @@ -4870,7 +4911,7 @@ dependencies = [ "async-trait", "dashmap", "log", - "nym-crypto", + "nym-crypto 0.4.0", "nym-sphinx", "nym-task", "sqlx", @@ -4886,7 +4927,7 @@ dependencies = [ "anyhow", "futures", "js-sys", - "nym-bin-common", + "nym-bin-common 0.6.0", "nym-node-tester-utils", "nym-node-tester-wasm", "rand", @@ -4925,7 +4966,7 @@ dependencies = [ "group", "itertools 0.13.0", "nym-dkg", - "nym-pemstore", + "nym-pemstore 0.3.0", "rand", "rand_chacha", "serde", @@ -4942,7 +4983,17 @@ dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw2", - "nym-multisig-contract-common", + "nym-multisig-contract-common 0.1.0", +] + +[[package]] +name = "nym-coconut-bandwidth-contract-common" +version = "0.1.0" +source = "git+https://github.com/nymtech/nym?rev=f86e08866#f86e0886631a98b0638fe09e6fcbe5458d47adc1" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "nym-multisig-contract-common 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", ] [[package]] @@ -4954,8 +5005,22 @@ dependencies = [ "cw-utils", "cw2", "cw4", - "nym-contracts-common", - "nym-multisig-contract-common", + "nym-contracts-common 0.5.0", + "nym-multisig-contract-common 0.1.0", +] + +[[package]] +name = "nym-coconut-dkg-common" +version = "0.1.0" +source = "git+https://github.com/nymtech/nym?rev=f86e08866#f86e0886631a98b0638fe09e6fcbe5458d47adc1" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-utils", + "cw2", + "cw4", + "nym-contracts-common 0.5.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", + "nym-multisig-contract-common 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", ] [[package]] @@ -4971,8 +5036,8 @@ dependencies = [ "ff", "group", "itertools 0.13.0", - "nym-network-defaults", - "nym-pemstore", + "nym-network-defaults 0.1.0", + "nym-pemstore 0.3.0", "rand", "rayon", "serde", @@ -4982,6 +5047,28 @@ dependencies = [ "zeroize", ] +[[package]] +name = "nym-compact-ecash" +version = "0.1.0" +source = "git+https://github.com/nymtech/nym?rev=f86e08866#f86e0886631a98b0638fe09e6fcbe5458d47adc1" +dependencies = [ + "bincode", + "bls12_381", + "bs58", + "cfg-if", + "digest 0.9.0", + "ff", + "group", + "itertools 0.12.1", + "nym-network-defaults 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", + "nym-pemstore 0.3.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", + "rand", + "serde", + "sha2 0.9.9", + "thiserror", + "zeroize", +] + [[package]] name = "nym-config" version = "0.1.0" @@ -4989,7 +5076,21 @@ dependencies = [ "dirs", "handlebars", "log", - "nym-network-defaults", + "nym-network-defaults 0.1.0", + "serde", + "toml 0.8.14", + "url", +] + +[[package]] +name = "nym-config" +version = "0.1.0" +source = "git+https://github.com/nymtech/nym?rev=f86e08866#f86e0886631a98b0638fe09e6fcbe5458d47adc1" +dependencies = [ + "dirs", + "handlebars", + "log", + "nym-network-defaults 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", "serde", "toml 0.8.14", "url", @@ -5010,6 +5111,21 @@ dependencies = [ "vergen", ] +[[package]] +name = "nym-contracts-common" +version = "0.5.0" +source = "git+https://github.com/nymtech/nym?rev=f86e08866#f86e0886631a98b0638fe09e6fcbe5458d47adc1" +dependencies = [ + "bs58", + "cosmwasm-schema", + "cosmwasm-std", + "cw-storage-plus", + "schemars", + "serde", + "thiserror", + "vergen", +] + [[package]] name = "nym-country-group" version = "0.1.0" @@ -5025,7 +5141,7 @@ dependencies = [ "anyhow", "bs58", "lazy_static", - "nym-bin-common", + "nym-bin-common 0.6.0", "nym-ffi-shared", "nym-sdk", "nym-sphinx-anonymous-replies", @@ -5039,9 +5155,9 @@ dependencies = [ "async-trait", "bincode", "log", - "nym-compact-ecash", + "nym-compact-ecash 0.1.0", "nym-credentials", - "nym-ecash-time", + "nym-ecash-time 0.1.0", "serde", "sqlx", "thiserror", @@ -5056,12 +5172,12 @@ dependencies = [ "log", "nym-bandwidth-controller", "nym-client-core", - "nym-config", + "nym-config 0.1.0", "nym-credential-storage", "nym-credentials", - "nym-credentials-interface", - "nym-ecash-time", - "nym-validator-client", + "nym-credentials-interface 0.1.0", + "nym-ecash-time 0.1.0", + "nym-validator-client 0.1.0", "thiserror", "time", "tokio", @@ -5075,15 +5191,15 @@ dependencies = [ "cosmwasm-std", "cw-utils", "futures", - "nym-api-requests", + "nym-api-requests 0.1.0", "nym-credentials", - "nym-credentials-interface", - "nym-ecash-contract-common", + "nym-credentials-interface 0.1.0", + "nym-ecash-contract-common 0.1.0", "nym-ecash-double-spending", "nym-gateway-requests", "nym-gateway-storage", "nym-task", - "nym-validator-client", + "nym-validator-client 0.1.0", "rand", "si-scale", "thiserror", @@ -5100,14 +5216,14 @@ dependencies = [ "bls12_381", "cosmrs 0.17.0-pre", "log", - "nym-api-requests", - "nym-credentials-interface", - "nym-crypto", - "nym-ecash-contract-common", - "nym-ecash-time", - "nym-network-defaults", - "nym-serde-helpers", - "nym-validator-client", + "nym-api-requests 0.1.0", + "nym-credentials-interface 0.1.0", + "nym-crypto 0.4.0", + "nym-ecash-contract-common 0.1.0", + "nym-ecash-time 0.1.0", + "nym-network-defaults 0.1.0", + "nym-serde-helpers 0.1.0", + "nym-validator-client 0.1.0", "rand", "serde", "thiserror", @@ -5120,9 +5236,25 @@ name = "nym-credentials-interface" version = "0.1.0" dependencies = [ "bls12_381", - "nym-compact-ecash", - "nym-ecash-time", - "nym-network-defaults", + "nym-compact-ecash 0.1.0", + "nym-ecash-time 0.1.0", + "nym-network-defaults 0.1.0", + "rand", + "serde", + "strum 0.26.3", + "thiserror", + "time", +] + +[[package]] +name = "nym-credentials-interface" +version = "0.1.0" +source = "git+https://github.com/nymtech/nym?rev=f86e08866#f86e0886631a98b0638fe09e6fcbe5458d47adc1" +dependencies = [ + "bls12_381", + "nym-compact-ecash 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", + "nym-ecash-time 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", + "nym-network-defaults 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", "rand", "serde", "strum 0.26.3", @@ -5146,8 +5278,8 @@ dependencies = [ "generic-array 0.14.7", "hkdf", "hmac", - "nym-pemstore", - "nym-sphinx-types", + "nym-pemstore 0.3.0", + "nym-sphinx-types 0.2.0", "rand", "rand_chacha", "serde", @@ -5158,6 +5290,23 @@ dependencies = [ "zeroize", ] +[[package]] +name = "nym-crypto" +version = "0.4.0" +source = "git+https://github.com/nymtech/nym?rev=f86e08866#f86e0886631a98b0638fe09e6fcbe5458d47adc1" +dependencies = [ + "bs58", + "ed25519-dalek", + "nym-pemstore 0.3.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", + "nym-sphinx-types 0.2.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", + "serde", + "serde_bytes", + "subtle-encoding", + "thiserror", + "x25519-dalek", + "zeroize", +] + [[package]] name = "nym-data-observatory" version = "0.1.0" @@ -5166,9 +5315,9 @@ dependencies = [ "axum 0.7.7", "chrono", "clap 4.5.20", - "nym-bin-common", - "nym-network-defaults", - "nym-node-requests", + "nym-bin-common 0.6.0", + "nym-network-defaults 0.1.0", + "nym-node-requests 0.1.0", "nym-task", "serde", "serde_json", @@ -5194,8 +5343,8 @@ dependencies = [ "ff", "group", "lazy_static", - "nym-contracts-common", - "nym-pemstore", + "nym-contracts-common 0.5.0", + "nym-pemstore 0.3.0", "rand", "rand_chacha", "rand_core 0.6.4", @@ -5216,7 +5365,21 @@ dependencies = [ "cw-controllers", "cw-utils", "cw2", - "nym-multisig-contract-common", + "nym-multisig-contract-common 0.1.0", + "thiserror", +] + +[[package]] +name = "nym-ecash-contract-common" +version = "0.1.0" +source = "git+https://github.com/nymtech/nym?rev=f86e08866#f86e0886631a98b0638fe09e6fcbe5458d47adc1" +dependencies = [ + "bs58", + "cosmwasm-schema", + "cosmwasm-std", + "cw-controllers", + "cw-utils", + "nym-multisig-contract-common 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", "thiserror", ] @@ -5226,14 +5389,22 @@ version = "0.1.0" dependencies = [ "bit-vec", "bloomfilter", - "nym-network-defaults", + "nym-network-defaults 0.1.0", +] + +[[package]] +name = "nym-ecash-time" +version = "0.1.0" +dependencies = [ + "nym-compact-ecash 0.1.0", + "time", ] [[package]] name = "nym-ecash-time" version = "0.1.0" +source = "git+https://github.com/nymtech/nym?rev=f86e08866#f86e0886631a98b0638fe09e6fcbe5458d47adc1" dependencies = [ - "nym-compact-ecash", "time", ] @@ -5257,13 +5428,25 @@ dependencies = [ "utoipa", ] +[[package]] +name = "nym-exit-policy" +version = "0.1.0" +source = "git+https://github.com/nymtech/nym?rev=f86e08866#f86e0886631a98b0638fe09e6fcbe5458d47adc1" +dependencies = [ + "serde", + "serde_json", + "thiserror", + "tracing", + "utoipa", +] + [[package]] name = "nym-explorer-api-requests" version = "0.1.0" dependencies = [ - "nym-api-requests", - "nym-contracts-common", - "nym-mixnet-contract-common", + "nym-api-requests 0.1.0", + "nym-contracts-common 0.5.0", + "nym-mixnet-contract-common 0.6.0", "schemars", "serde", "ts-rs", @@ -5289,7 +5472,7 @@ dependencies = [ "anyhow", "bs58", "lazy_static", - "nym-bin-common", + "nym-bin-common 0.6.0", "nym-sdk", "nym-sphinx-anonymous-replies", "tokio", @@ -5314,31 +5497,31 @@ dependencies = [ "futures", "humantime-serde", "ipnetwork 0.20.0", - "nym-api-requests", + "nym-api-requests 0.1.0", "nym-authenticator", - "nym-bin-common", - "nym-config", + "nym-bin-common 0.6.0", + "nym-config 0.1.0", "nym-credential-verification", "nym-credentials", - "nym-credentials-interface", - "nym-crypto", + "nym-credentials-interface 0.1.0", + "nym-crypto 0.4.0", "nym-gateway-requests", "nym-gateway-stats-storage", "nym-gateway-storage", "nym-ip-packet-router", "nym-mixnet-client", "nym-mixnode-common", - "nym-network-defaults", + "nym-network-defaults 0.1.0", "nym-network-requester", "nym-node-http-api", - "nym-pemstore", + "nym-pemstore 0.3.0", "nym-sphinx", "nym-statistics-common", "nym-task", "nym-types", - "nym-validator-client", + "nym-validator-client 0.1.0", "nym-wireguard", - "nym-wireguard-types", + "nym-wireguard-types 0.1.0", "once_cell", "rand", "serde", @@ -5367,14 +5550,14 @@ dependencies = [ "nym-bandwidth-controller", "nym-credential-storage", "nym-credentials", - "nym-credentials-interface", - "nym-crypto", + "nym-credentials-interface 0.1.0", + "nym-crypto 0.4.0", "nym-gateway-requests", - "nym-network-defaults", - "nym-pemstore", + "nym-network-defaults 0.1.0", + "nym-pemstore 0.3.0", "nym-sphinx", "nym-task", - "nym-validator-client", + "nym-validator-client 0.1.0", "rand", "serde", "si-scale", @@ -5400,11 +5583,11 @@ dependencies = [ "bs58", "futures", "generic-array 0.14.7", - "nym-compact-ecash", + "nym-compact-ecash 0.1.0", "nym-credentials", - "nym-credentials-interface", - "nym-crypto", - "nym-pemstore", + "nym-credentials-interface 0.1.0", + "nym-crypto 0.4.0", + "nym-pemstore 0.3.0", "nym-sphinx", "nym-task", "rand", @@ -5423,7 +5606,7 @@ dependencies = [ name = "nym-gateway-stats-storage" version = "0.1.0" dependencies = [ - "nym-credentials-interface", + "nym-credentials-interface 0.1.0", "nym-sphinx", "sqlx", "thiserror", @@ -5440,7 +5623,7 @@ dependencies = [ "bincode", "defguard_wireguard_rs", "log", - "nym-credentials-interface", + "nym-credentials-interface 0.1.0", "nym-gateway-requests", "nym-sphinx", "sqlx", @@ -5456,7 +5639,7 @@ version = "0.2.0" dependencies = [ "anyhow", "lazy_static", - "nym-bin-common", + "nym-bin-common 0.6.0", "nym-ffi-shared", "nym-sdk", "nym-sphinx-anonymous-replies", @@ -5477,13 +5660,42 @@ dependencies = [ "serde", ] +[[package]] +name = "nym-group-contract-common" +version = "0.1.0" +source = "git+https://github.com/nymtech/nym?rev=f86e08866#f86e0886631a98b0638fe09e6fcbe5458d47adc1" +dependencies = [ + "cosmwasm-schema", + "cw-controllers", + "cw4", + "schemars", + "serde", +] + +[[package]] +name = "nym-http-api-client" +version = "0.1.0" +dependencies = [ + "async-trait", + "http 1.1.0", + "nym-bin-common 0.6.0", + "reqwest 0.12.4", + "serde", + "serde_json", + "thiserror", + "tracing", + "url", + "wasmtimer", +] + [[package]] name = "nym-http-api-client" version = "0.1.0" +source = "git+https://github.com/nymtech/nym?rev=f86e08866#f86e0886631a98b0638fe09e6fcbe5458d47adc1" dependencies = [ "async-trait", "http 1.1.0", - "nym-bin-common", + "nym-bin-common 0.6.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", "reqwest 0.12.4", "serde", "serde_json", @@ -5527,7 +5739,7 @@ dependencies = [ "anyhow", "bs58", "clap 4.5.20", - "nym-bin-common", + "nym-bin-common 0.6.0", "nym-credential-storage", "nym-id", "tokio", @@ -5549,8 +5761,8 @@ version = "0.1.0" dependencies = [ "bincode", "bytes", - "nym-bin-common", - "nym-crypto", + "nym-bin-common 0.6.0", + "nym-crypto 0.4.0", "nym-sphinx", "rand", "serde", @@ -5572,14 +5784,14 @@ dependencies = [ "etherparse", "futures", "log", - "nym-bin-common", + "nym-bin-common 0.6.0", "nym-client-core", - "nym-config", - "nym-crypto", - "nym-exit-policy", + "nym-config 0.1.0", + "nym-crypto 0.4.0", + "nym-exit-policy 0.1.0", "nym-id", "nym-ip-packet-requests", - "nym-network-defaults", + "nym-network-defaults 0.1.0", "nym-network-requester", "nym-sdk", "nym-service-providers-common", @@ -5588,7 +5800,7 @@ dependencies = [ "nym-tun", "nym-types", "nym-wireguard", - "nym-wireguard-types", + "nym-wireguard-types 0.1.0", "rand", "reqwest 0.12.4", "serde", @@ -5647,7 +5859,7 @@ dependencies = [ "cw2", "humantime-serde", "log", - "nym-contracts-common", + "nym-contracts-common 0.5.0", "rand_chacha", "schemars", "serde", @@ -5659,6 +5871,26 @@ dependencies = [ "utoipa", ] +[[package]] +name = "nym-mixnet-contract-common" +version = "0.6.0" +source = "git+https://github.com/nymtech/nym?rev=f86e08866#f86e0886631a98b0638fe09e6fcbe5458d47adc1" +dependencies = [ + "bs58", + "cosmwasm-schema", + "cosmwasm-std", + "cw-controllers", + "humantime-serde", + "log", + "nym-contracts-common 0.5.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", + "schemars", + "serde", + "serde-json-wasm", + "serde_repr", + "thiserror", + "time", +] + [[package]] name = "nym-mixnode" version = "1.1.37" @@ -5674,24 +5906,24 @@ dependencies = [ "humantime-serde", "lazy_static", "log", - "nym-bin-common", - "nym-config", - "nym-contracts-common", - "nym-crypto", + "nym-bin-common 0.6.0", + "nym-config 0.1.0", + "nym-contracts-common 0.5.0", + "nym-crypto 0.4.0", "nym-http-api-common", "nym-metrics", "nym-mixnet-client", "nym-mixnode-common", "nym-node-http-api", "nym-nonexhaustive-delayqueue", - "nym-pemstore", + "nym-pemstore 0.3.0", "nym-sphinx", "nym-sphinx-params", - "nym-sphinx-types", + "nym-sphinx-types 0.2.0", "nym-task", "nym-topology", "nym-types", - "nym-validator-client", + "nym-validator-client 0.1.0", "rand", "serde", "serde_json", @@ -5712,19 +5944,19 @@ dependencies = [ "futures", "humantime-serde", "log", - "nym-bin-common", - "nym-crypto", + "nym-bin-common 0.6.0", + "nym-crypto 0.4.0", "nym-metrics", - "nym-network-defaults", + "nym-network-defaults 0.1.0", "nym-node-http-api", "nym-sphinx-acknowledgements", "nym-sphinx-addressing", "nym-sphinx-forwarding", "nym-sphinx-framing", "nym-sphinx-params", - "nym-sphinx-types", + "nym-sphinx-types 0.2.0", "nym-task", - "nym-validator-client", + "nym-validator-client 0.1.0", "rand", "serde", "thiserror", @@ -5749,9 +5981,38 @@ dependencies = [ "thiserror", ] +[[package]] +name = "nym-multisig-contract-common" +version = "0.1.0" +source = "git+https://github.com/nymtech/nym?rev=f86e08866#f86e0886631a98b0638fe09e6fcbe5458d47adc1" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-storage-plus", + "cw-utils", + "cw3", + "cw4", + "schemars", + "serde", + "thiserror", +] + +[[package]] +name = "nym-network-defaults" +version = "0.1.0" +dependencies = [ + "dotenvy", + "log", + "schemars", + "serde", + "url", + "utoipa", +] + [[package]] name = "nym-network-defaults" version = "0.1.0" +source = "git+https://github.com/nymtech/nym?rev=f86e08866#f86e0886631a98b0638fe09e6fcbe5458d47adc1" dependencies = [ "dotenvy", "log", @@ -5771,14 +6032,14 @@ dependencies = [ "dashmap", "futures", "log", - "nym-bin-common", - "nym-crypto", - "nym-network-defaults", + "nym-bin-common 0.6.0", + "nym-crypto 0.4.0", + "nym-network-defaults 0.1.0", "nym-sdk", "nym-sphinx", "nym-topology", "nym-types", - "nym-validator-client", + "nym-validator-client 0.1.0", "petgraph", "rand", "rand_chacha", @@ -5806,16 +6067,16 @@ dependencies = [ "ipnetwork 0.20.0", "log", "nym-async-file-watcher", - "nym-bin-common", + "nym-bin-common 0.6.0", "nym-client-core", "nym-client-websocket-requests", - "nym-config", + "nym-config 0.1.0", "nym-credential-storage", "nym-credentials", - "nym-crypto", - "nym-exit-policy", + "nym-crypto 0.4.0", + "nym-exit-policy 0.1.0", "nym-id", - "nym-network-defaults", + "nym-network-defaults 0.1.0", "nym-ordered-buffer", "nym-sdk", "nym-service-providers-common", @@ -5857,22 +6118,22 @@ dependencies = [ "humantime-serde", "ipnetwork 0.20.0", "nym-authenticator", - "nym-bin-common", + "nym-bin-common 0.6.0", "nym-client-core-config-types", - "nym-config", - "nym-crypto", + "nym-config 0.1.0", + "nym-crypto 0.4.0", "nym-gateway", "nym-ip-packet-router", "nym-mixnode", "nym-network-requester", "nym-node-http-api", - "nym-pemstore", + "nym-pemstore 0.3.0", "nym-sphinx-acknowledgements", "nym-sphinx-addressing", "nym-task", "nym-types", "nym-wireguard", - "nym-wireguard-types", + "nym-wireguard-types 0.1.0", "rand", "semver 1.0.23", "serde", @@ -5900,10 +6161,10 @@ dependencies = [ "hmac", "hyper 1.4.1", "ipnetwork 0.20.0", - "nym-crypto", + "nym-crypto 0.4.0", "nym-http-api-common", "nym-metrics", - "nym-node-requests", + "nym-node-requests 0.1.0", "nym-task", "nym-wireguard", "rand", @@ -5928,11 +6189,11 @@ dependencies = [ "celes", "humantime 2.1.0", "humantime-serde", - "nym-bin-common", - "nym-crypto", - "nym-exit-policy", - "nym-http-api-client", - "nym-wireguard-types", + "nym-bin-common 0.6.0", + "nym-crypto 0.4.0", + "nym-exit-policy 0.1.0", + "nym-http-api-client 0.1.0", + "nym-wireguard-types 0.1.0", "rand_chacha", "schemars", "serde", @@ -5943,6 +6204,29 @@ dependencies = [ "utoipa", ] +[[package]] +name = "nym-node-requests" +version = "0.1.0" +source = "git+https://github.com/nymtech/nym?rev=f86e08866#f86e0886631a98b0638fe09e6fcbe5458d47adc1" +dependencies = [ + "async-trait", + "base64 0.22.1", + "celes", + "humantime 2.1.0", + "humantime-serde", + "nym-bin-common 0.6.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", + "nym-crypto 0.4.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", + "nym-exit-policy 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", + "nym-http-api-client 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", + "nym-wireguard-types 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", + "schemars", + "serde", + "serde_json", + "thiserror", + "time", + "utoipa", +] + [[package]] name = "nym-node-status-api" version = "0.1.0" @@ -5955,12 +6239,12 @@ dependencies = [ "envy", "futures-util", "moka", - "nym-bin-common", + "nym-bin-common 0.6.0", "nym-explorer-client", - "nym-network-defaults", - "nym-node-requests", + "nym-network-defaults 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", + "nym-node-requests 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", "nym-task", - "nym-validator-client", + "nym-validator-client 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", "reqwest 0.12.4", "serde", "serde_json", @@ -5984,7 +6268,7 @@ version = "0.1.0" dependencies = [ "futures", "log", - "nym-crypto", + "nym-crypto 0.4.0", "nym-sphinx", "nym-sphinx-params", "nym-task", @@ -6035,8 +6319,8 @@ dependencies = [ "anyhow", "clap 4.5.20", "log", - "nym-bin-common", - "nym-network-defaults", + "nym-bin-common 0.6.0", + "nym-network-defaults 0.1.0", "nym-sdk", "nym-service-providers-common", "nym-socks5-requests", @@ -6078,6 +6362,14 @@ dependencies = [ "pem", ] +[[package]] +name = "nym-pemstore" +version = "0.3.0" +source = "git+https://github.com/nymtech/nym?rev=f86e08866#f86e0886631a98b0638fe09e6fcbe5458d47adc1" +dependencies = [ + "pem", +] + [[package]] name = "nym-sdk" version = "0.1.0" @@ -6097,15 +6389,15 @@ dependencies = [ "httpcodec", "log", "nym-bandwidth-controller", - "nym-bin-common", + "nym-bin-common 0.6.0", "nym-client-core", "nym-credential-storage", "nym-credential-utils", "nym-credentials", - "nym-credentials-interface", - "nym-crypto", + "nym-credentials-interface 0.1.0", + "nym-crypto 0.4.0", "nym-gateway-requests", - "nym-network-defaults", + "nym-network-defaults 0.1.0", "nym-ordered-buffer", "nym-service-providers-common", "nym-socks5-client-core", @@ -6113,7 +6405,7 @@ dependencies = [ "nym-sphinx", "nym-task", "nym-topology", - "nym-validator-client", + "nym-validator-client 0.1.0", "parking_lot", "pretty_env_logger", "rand", @@ -6143,6 +6435,17 @@ dependencies = [ "time", ] +[[package]] +name = "nym-serde-helpers" +version = "0.1.0" +source = "git+https://github.com/nymtech/nym?rev=f86e08866#f86e0886631a98b0638fe09e6fcbe5458d47adc1" +dependencies = [ + "base64 0.22.1", + "bs58", + "serde", + "time", +] + [[package]] name = "nym-service-provider-requests-common" version = "0.1.0" @@ -6157,7 +6460,7 @@ dependencies = [ "anyhow", "async-trait", "log", - "nym-bin-common", + "nym-bin-common 0.6.0", "nym-sdk", "nym-socks5-requests", "nym-sphinx-anonymous-replies", @@ -6174,21 +6477,21 @@ dependencies = [ "bs58", "clap 4.5.20", "log", - "nym-bin-common", + "nym-bin-common 0.6.0", "nym-client-core", - "nym-config", + "nym-config 0.1.0", "nym-credential-storage", "nym-credentials", - "nym-crypto", + "nym-crypto 0.4.0", "nym-gateway-requests", "nym-id", - "nym-network-defaults", + "nym-network-defaults 0.1.0", "nym-ordered-buffer", - "nym-pemstore", + "nym-pemstore 0.3.0", "nym-socks5-client-core", "nym-sphinx", "nym-topology", - "nym-validator-client", + "nym-validator-client 0.1.0", "rand", "serde", "serde_json", @@ -6210,17 +6513,17 @@ dependencies = [ "log", "nym-bandwidth-controller", "nym-client-core", - "nym-config", - "nym-contracts-common", + "nym-config 0.1.0", + "nym-contracts-common 0.5.0", "nym-credential-storage", - "nym-mixnet-contract-common", - "nym-network-defaults", + "nym-mixnet-contract-common 0.6.0", + "nym-network-defaults 0.1.0", "nym-service-providers-common", "nym-socks5-proxy-helpers", "nym-socks5-requests", "nym-sphinx", "nym-task", - "nym-validator-client", + "nym-validator-client 0.1.0", "pin-project", "rand", "reqwest 0.12.4", @@ -6242,11 +6545,11 @@ dependencies = [ "jni", "lazy_static", "log", - "nym-bin-common", + "nym-bin-common 0.6.0", "nym-client-core", - "nym-config", + "nym-config 0.1.0", "nym-credential-storage", - "nym-crypto", + "nym-crypto 0.4.0", "nym-socks5-client-core", "rand", "safer-ffi", @@ -6275,7 +6578,7 @@ version = "0.1.0" dependencies = [ "bincode", "log", - "nym-exit-policy", + "nym-exit-policy 0.1.0", "nym-service-providers-common", "nym-sphinx-addressing", "serde", @@ -6289,9 +6592,9 @@ name = "nym-sphinx" version = "0.1.0" dependencies = [ "log", - "nym-crypto", + "nym-crypto 0.4.0", "nym-metrics", - "nym-mixnet-contract-common", + "nym-mixnet-contract-common 0.6.0", "nym-sphinx-acknowledgements", "nym-sphinx-addressing", "nym-sphinx-anonymous-replies", @@ -6301,7 +6604,7 @@ dependencies = [ "nym-sphinx-framing", "nym-sphinx-params", "nym-sphinx-routing", - "nym-sphinx-types", + "nym-sphinx-types 0.2.0", "nym-topology", "rand", "rand_chacha", @@ -6315,12 +6618,12 @@ name = "nym-sphinx-acknowledgements" version = "0.1.0" dependencies = [ "generic-array 0.14.7", - "nym-crypto", - "nym-pemstore", + "nym-crypto 0.4.0", + "nym-pemstore 0.3.0", "nym-sphinx-addressing", "nym-sphinx-params", "nym-sphinx-routing", - "nym-sphinx-types", + "nym-sphinx-types 0.2.0", "nym-topology", "rand", "serde", @@ -6332,8 +6635,8 @@ dependencies = [ name = "nym-sphinx-addressing" version = "0.1.0" dependencies = [ - "nym-crypto", - "nym-sphinx-types", + "nym-crypto 0.4.0", + "nym-sphinx-types 0.2.0", "rand", "serde", "thiserror", @@ -6344,11 +6647,11 @@ name = "nym-sphinx-anonymous-replies" version = "0.1.0" dependencies = [ "bs58", - "nym-crypto", + "nym-crypto 0.4.0", "nym-sphinx-addressing", "nym-sphinx-params", "nym-sphinx-routing", - "nym-sphinx-types", + "nym-sphinx-types 0.2.0", "nym-topology", "rand", "rand_chacha", @@ -6363,11 +6666,11 @@ version = "0.1.0" dependencies = [ "dashmap", "log", - "nym-crypto", + "nym-crypto 0.4.0", "nym-metrics", "nym-sphinx-addressing", "nym-sphinx-params", - "nym-sphinx-types", + "nym-sphinx-types 0.2.0", "rand", "serde", "thiserror", @@ -6378,14 +6681,14 @@ dependencies = [ name = "nym-sphinx-cover" version = "0.1.0" dependencies = [ - "nym-crypto", + "nym-crypto 0.4.0", "nym-sphinx-acknowledgements", "nym-sphinx-addressing", "nym-sphinx-chunking", "nym-sphinx-forwarding", "nym-sphinx-params", "nym-sphinx-routing", - "nym-sphinx-types", + "nym-sphinx-types 0.2.0", "nym-topology", "rand", "thiserror", @@ -6398,7 +6701,7 @@ dependencies = [ "nym-outfox", "nym-sphinx-addressing", "nym-sphinx-params", - "nym-sphinx-types", + "nym-sphinx-types 0.2.0", "thiserror", ] @@ -6413,7 +6716,7 @@ dependencies = [ "nym-sphinx-addressing", "nym-sphinx-forwarding", "nym-sphinx-params", - "nym-sphinx-types", + "nym-sphinx-types 0.2.0", "thiserror", "tokio", "tokio-util", @@ -6423,8 +6726,8 @@ dependencies = [ name = "nym-sphinx-params" version = "0.1.0" dependencies = [ - "nym-crypto", - "nym-sphinx-types", + "nym-crypto 0.4.0", + "nym-sphinx-types 0.2.0", "serde", "thiserror", ] @@ -6434,7 +6737,7 @@ name = "nym-sphinx-routing" version = "0.1.0" dependencies = [ "nym-sphinx-addressing", - "nym-sphinx-types", + "nym-sphinx-types 0.2.0", "thiserror", ] @@ -6447,12 +6750,21 @@ dependencies = [ "thiserror", ] +[[package]] +name = "nym-sphinx-types" +version = "0.2.0" +source = "git+https://github.com/nymtech/nym?rev=f86e08866#f86e0886631a98b0638fe09e6fcbe5458d47adc1" +dependencies = [ + "sphinx-packet", + "thiserror", +] + [[package]] name = "nym-statistics-common" version = "0.1.0" dependencies = [ "futures", - "nym-credentials-interface", + "nym-credentials-interface 0.1.0", "nym-sphinx", "time", ] @@ -6492,14 +6804,14 @@ dependencies = [ "async-trait", "bs58", "log", - "nym-api-requests", - "nym-bin-common", - "nym-config", - "nym-crypto", - "nym-mixnet-contract-common", + "nym-api-requests 0.1.0", + "nym-bin-common 0.6.0", + "nym-config 0.1.0", + "nym-crypto 0.4.0", + "nym-mixnet-contract-common 0.6.0", "nym-sphinx-addressing", "nym-sphinx-routing", - "nym-sphinx-types", + "nym-sphinx-types 0.2.0", "rand", "reqwest 0.12.4", "semver 1.0.23", @@ -6517,7 +6829,7 @@ version = "0.1.0" dependencies = [ "etherparse", "log", - "nym-wireguard-types", + "nym-wireguard-types 0.1.0", "thiserror", "tokio", "tokio-tun", @@ -6534,11 +6846,11 @@ dependencies = [ "hmac", "itertools 0.13.0", "log", - "nym-config", - "nym-crypto", - "nym-mixnet-contract-common", - "nym-validator-client", - "nym-vesting-contract-common", + "nym-config 0.1.0", + "nym-crypto 0.4.0", + "nym-mixnet-contract-common 0.6.0", + "nym-validator-client 0.1.0", + "nym-vesting-contract-common 0.7.0", "reqwest 0.12.4", "schemars", "serde", @@ -6572,20 +6884,20 @@ dependencies = [ "flate2", "futures", "itertools 0.13.0", - "nym-api-requests", - "nym-coconut-bandwidth-contract-common", - "nym-coconut-dkg-common", - "nym-compact-ecash", - "nym-config", - "nym-contracts-common", - "nym-ecash-contract-common", - "nym-group-contract-common", - "nym-http-api-client", - "nym-mixnet-contract-common", - "nym-multisig-contract-common", - "nym-network-defaults", - "nym-serde-helpers", - "nym-vesting-contract-common", + "nym-api-requests 0.1.0", + "nym-coconut-bandwidth-contract-common 0.1.0", + "nym-coconut-dkg-common 0.1.0", + "nym-compact-ecash 0.1.0", + "nym-config 0.1.0", + "nym-contracts-common 0.5.0", + "nym-ecash-contract-common 0.1.0", + "nym-group-contract-common 0.1.0", + "nym-http-api-client 0.1.0", + "nym-mixnet-contract-common 0.6.0", + "nym-multisig-contract-common 0.1.0", + "nym-network-defaults 0.1.0", + "nym-serde-helpers 0.1.0", + "nym-vesting-contract-common 0.7.0", "prost 0.12.6", "reqwest 0.12.4", "serde", @@ -6602,6 +6914,55 @@ dependencies = [ "zeroize", ] +[[package]] +name = "nym-validator-client" +version = "0.1.0" +source = "git+https://github.com/nymtech/nym?rev=f86e08866#f86e0886631a98b0638fe09e6fcbe5458d47adc1" +dependencies = [ + "async-trait", + "base64 0.22.1", + "bip32", + "bip39", + "colored", + "cosmrs 0.17.0-pre", + "cosmwasm-std", + "cw-controllers", + "cw-utils", + "cw2", + "cw3", + "cw4", + "eyre", + "flate2", + "futures", + "itertools 0.13.0", + "log", + "nym-api-requests 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", + "nym-coconut-bandwidth-contract-common 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", + "nym-coconut-dkg-common 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", + "nym-compact-ecash 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", + "nym-config 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", + "nym-contracts-common 0.5.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", + "nym-ecash-contract-common 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", + "nym-group-contract-common 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", + "nym-http-api-client 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", + "nym-mixnet-contract-common 0.6.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", + "nym-multisig-contract-common 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", + "nym-network-defaults 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", + "nym-vesting-contract-common 0.7.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", + "prost 0.12.6", + "reqwest 0.12.4", + "serde", + "serde_json", + "sha2 0.9.9", + "tendermint-rpc", + "thiserror", + "time", + "tokio", + "url", + "wasmtimer", + "zeroize", +] + [[package]] name = "nym-validator-rewarder" version = "0.1.0" @@ -6613,18 +6974,18 @@ dependencies = [ "futures", "humantime 2.1.0", "humantime-serde", - "nym-bin-common", - "nym-coconut-bandwidth-contract-common", - "nym-coconut-dkg-common", - "nym-compact-ecash", - "nym-config", + "nym-bin-common 0.6.0", + "nym-coconut-bandwidth-contract-common 0.1.0", + "nym-coconut-dkg-common 0.1.0", + "nym-compact-ecash 0.1.0", + "nym-config 0.1.0", "nym-credentials", - "nym-credentials-interface", - "nym-crypto", - "nym-ecash-time", - "nym-network-defaults", + "nym-credentials-interface 0.1.0", + "nym-crypto 0.4.0", + "nym-ecash-time 0.1.0", + "nym-network-defaults 0.1.0", "nym-task", - "nym-validator-client", + "nym-validator-client 0.1.0", "nyxd-scraper", "rand_chacha", "serde", @@ -6646,13 +7007,26 @@ dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw2", - "nym-contracts-common", - "nym-mixnet-contract-common", + "nym-contracts-common 0.5.0", + "nym-mixnet-contract-common 0.6.0", "serde", "thiserror", "ts-rs", ] +[[package]] +name = "nym-vesting-contract-common" +version = "0.7.0" +source = "git+https://github.com/nymtech/nym?rev=f86e08866#f86e0886631a98b0638fe09e6fcbe5458d47adc1" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "nym-contracts-common 0.5.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", + "nym-mixnet-contract-common 0.6.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", + "serde", + "thiserror", +] + [[package]] name = "nym-wallet-types" version = "1.0.0" @@ -6660,12 +7034,12 @@ dependencies = [ "cosmrs 0.15.0", "cosmwasm-std", "hex-literal", - "nym-config", - "nym-mixnet-contract-common", - "nym-network-defaults", + "nym-config 0.1.0", + "nym-mixnet-contract-common 0.6.0", + "nym-network-defaults 0.1.0", "nym-types", - "nym-validator-client", - "nym-vesting-contract-common", + "nym-validator-client 0.1.0", + "nym-vesting-contract-common 0.7.0", "serde", "serde_json", "strum 0.23.0", @@ -6686,11 +7060,11 @@ dependencies = [ "log", "nym-authenticator-requests", "nym-credential-verification", - "nym-crypto", + "nym-crypto 0.4.0", "nym-gateway-storage", - "nym-network-defaults", + "nym-network-defaults 0.1.0", "nym-task", - "nym-wireguard-types", + "nym-wireguard-types 0.1.0", "thiserror", "tokio", "tokio-stream", @@ -6703,15 +7077,29 @@ version = "0.1.0" dependencies = [ "base64 0.22.1", "log", - "nym-config", - "nym-crypto", - "nym-network-defaults", + "nym-config 0.1.0", + "nym-crypto 0.4.0", + "nym-network-defaults 0.1.0", "rand", "serde", "thiserror", "x25519-dalek", ] +[[package]] +name = "nym-wireguard-types" +version = "0.1.0" +source = "git+https://github.com/nymtech/nym?rev=f86e08866#f86e0886631a98b0638fe09e6fcbe5458d47adc1" +dependencies = [ + "base64 0.22.1", + "log", + "nym-config 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", + "nym-network-defaults 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", + "serde", + "thiserror", + "x25519-dalek", +] + [[package]] name = "nymvisor" version = "0.1.8" @@ -6727,8 +7115,8 @@ dependencies = [ "humantime-serde", "nix 0.27.1", "nym-async-file-watcher", - "nym-bin-common", - "nym-config", + "nym-bin-common 0.6.0", + "nym-config 0.1.0", "nym-task", "reqwest 0.12.4", "serde", @@ -9506,19 +9894,19 @@ dependencies = [ "cw-utils", "dkg-bypass-contract", "indicatif", - "nym-bin-common", - "nym-coconut-dkg-common", - "nym-compact-ecash", - "nym-config", - "nym-contracts-common", - "nym-crypto", - "nym-ecash-contract-common", - "nym-group-contract-common", - "nym-mixnet-contract-common", - "nym-multisig-contract-common", - "nym-pemstore", - "nym-validator-client", - "nym-vesting-contract-common", + "nym-bin-common 0.6.0", + "nym-coconut-dkg-common 0.1.0", + "nym-compact-ecash 0.1.0", + "nym-config 0.1.0", + "nym-contracts-common 0.5.0", + "nym-crypto 0.4.0", + "nym-ecash-contract-common 0.1.0", + "nym-group-contract-common 0.1.0", + "nym-mixnet-contract-common 0.6.0", + "nym-multisig-contract-common 0.1.0", + "nym-pemstore 0.3.0", + "nym-validator-client 0.1.0", + "nym-vesting-contract-common 0.7.0", "rand", "serde", "serde_json", @@ -10112,11 +10500,11 @@ name = "ts-rs-cli" version = "0.1.0" dependencies = [ "anyhow", - "nym-api-requests", - "nym-mixnet-contract-common", + "nym-api-requests 0.1.0", + "nym-mixnet-contract-common 0.6.0", "nym-types", - "nym-validator-client", - "nym-vesting-contract-common", + "nym-validator-client 0.1.0", + "nym-vesting-contract-common 0.7.0", "nym-wallet-types", "ts-rs", "walkdir", @@ -10760,15 +11148,15 @@ dependencies = [ "js-sys", "nym-bandwidth-controller", "nym-client-core", - "nym-config", + "nym-config 0.1.0", "nym-credential-storage", - "nym-crypto", + "nym-crypto 0.4.0", "nym-gateway-client", "nym-sphinx", "nym-sphinx-acknowledgements", "nym-task", "nym-topology", - "nym-validator-client", + "nym-validator-client 0.1.0", "rand", "serde", "serde-wasm-bindgen 0.6.5", @@ -11323,12 +11711,12 @@ dependencies = [ "bs58", "getrandom", "js-sys", - "nym-bin-common", + "nym-bin-common 0.6.0", "nym-coconut", - "nym-compact-ecash", + "nym-compact-ecash 0.1.0", "nym-credentials", - "nym-crypto", - "nym-http-api-client", + "nym-crypto 0.4.0", + "nym-http-api-client 0.1.0", "rand", "reqwest 0.12.4", "serde", diff --git a/Cargo.toml b/Cargo.toml index 68c9476f5ee..d431d4567b9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -155,6 +155,7 @@ default-members = [ "nym-data-observatory", "nym-node", "nym-validator-rewarder", + "nym-node-status-api", "service-providers/authenticator", "service-providers/ip-packet-router", "service-providers/network-requester", diff --git a/nym-node-status-api/Cargo.toml b/nym-node-status-api/Cargo.toml index 3b8d315ccb1..03e6fe60a71 100644 --- a/nym-node-status-api/Cargo.toml +++ b/nym-node-status-api/Cargo.toml @@ -23,10 +23,14 @@ futures-util = { workspace = true } moka = { workspace = true, features = ["future"] } nym-bin-common = { path = "../common/bin-common" } nym-explorer-client = { path = "../explorer-api/explorer-client" } -nym-network-defaults = { path = "../common/network-defaults" } -nym-validator-client = { path = "../common/client-libs/validator-client" } +# TODO dz: ref before Nym API client changes. Update to latest develop once new Nym API is live +nym-network-defaults = { git = "https://github.com/nymtech/nym", rev = "f86e08866" } +nym-validator-client = { git = "https://github.com/nymtech/nym", rev = "f86e08866" } +# nym-network-defaults = { path = "../common/network-defaults" } +# 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"] } +nym-node-requests = { git = "https://github.com/nymtech/nym", rev = "f86e08866" } +# nym-node-requests = { path = "../nym-node/nym-node-requests", features = ["openapi"] } reqwest = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } diff --git a/nym-node-status-api/src/config.rs b/nym-node-status-api/src/config.rs index 24e966a53fc..c499ef3c65f 100644 --- a/nym-node-status-api/src/config.rs +++ b/nym-node-status-api/src/config.rs @@ -13,9 +13,6 @@ pub(crate) struct Config { nyxd_addr: Url, #[serde(default = "Config::default_client_timeout")] #[serde(deserialize_with = "parse_duration")] - nym_api_client_timeout: Duration, - #[serde(default = "Config::default_client_timeout")] - #[serde(deserialize_with = "parse_duration")] explorer_client_timeout: Duration, } @@ -51,10 +48,6 @@ impl Config { &self.nyxd_addr } - pub(crate) fn nym_api_client_timeout(&self) -> Duration { - self.nym_api_client_timeout.to_owned() - } - pub(crate) fn nym_explorer_client_timeout(&self) -> Duration { self.explorer_client_timeout.to_owned() } diff --git a/nym-node-status-api/src/db/mod.rs b/nym-node-status-api/src/db/mod.rs index 784a35f17ac..2df2483687f 100644 --- a/nym-node-status-api/src/db/mod.rs +++ b/nym-node-status-api/src/db/mod.rs @@ -19,12 +19,9 @@ pub(crate) struct Storage { impl Storage { pub async fn init() -> Result { let connection_url = read_env_var(DATABASE_URL_ENV_VAR)?; - let connect_options = { - let connect_options = SqliteConnectOptions::from_str(&connection_url)?; - let mut connect_options = connect_options.create_if_missing(true); - let connect_options = connect_options.disable_statement_logging(); - (*connect_options).clone() - }; + let connect_options = SqliteConnectOptions::from_str(&connection_url)? + .create_if_missing(true) + .disable_statement_logging(); let pool = sqlx::SqlitePool::connect_with(connect_options) .await diff --git a/nym-node-status-api/src/db/queries/gateways.rs b/nym-node-status-api/src/db/queries/gateways.rs index 92a599154f7..02b5d05dc3c 100644 --- a/nym-node-status-api/src/db/queries/gateways.rs +++ b/nym-node-status-api/src/db/queries/gateways.rs @@ -143,7 +143,7 @@ pub(crate) async fn get_all_gateways(pool: &DbPool) -> anyhow::Result>() .await?; diff --git a/nym-node-status-api/src/db/queries/misc.rs b/nym-node-status-api/src/db/queries/misc.rs index 64b2f3cd24f..2aa6356051e 100644 --- a/nym-node-status-api/src/db/queries/misc.rs +++ b/nym-node-status-api/src/db/queries/misc.rs @@ -37,7 +37,7 @@ async fn insert_summary( value, timestamp ) - .execute(&mut tx) + .execute(&mut *tx) .await .map_err(|err| { tracing::error!("Failed to insert data for {kind}: {err}, aborting transaction",); diff --git a/nym-node-status-api/src/db/queries/mixnodes.rs b/nym-node-status-api/src/db/queries/mixnodes.rs index 8bc8020ef9c..58af9bd4296 100644 --- a/nym-node-status-api/src/db/queries/mixnodes.rs +++ b/nym-node-status-api/src/db/queries/mixnodes.rs @@ -71,7 +71,7 @@ pub(crate) async fn get_all_mixnodes(pool: &DbPool) -> anyhow::Result>() .await?; @@ -115,7 +115,7 @@ pub(crate) async fn get_daily_stats(pool: &DbPool) -> anyhow::Result>() .await?; diff --git a/nym-node-status-api/src/db/queries/summary.rs b/nym-node-status-api/src/db/queries/summary.rs index d3855639f69..103712a9a4e 100644 --- a/nym-node-status-api/src/db/queries/summary.rs +++ b/nym-node-status-api/src/db/queries/summary.rs @@ -37,7 +37,7 @@ pub(crate) async fn get_summary_history(pool: &DbPool) -> anyhow::Result>() .await?; @@ -62,7 +62,7 @@ async fn get_summary_dto(pool: &DbPool) -> anyhow::Result> { last_updated_utc as "last_updated_utc!" FROM summary"# ) - .fetch(&mut conn) + .fetch(&mut *conn) .try_collect::>() .await?) } diff --git a/nym-node-status-api/src/monitor/mod.rs b/nym-node-status-api/src/monitor/mod.rs index 4215e297ccf..5079af3b5a5 100644 --- a/nym-node-status-api/src/monitor/mod.rs +++ b/nym-node-status-api/src/monitor/mod.rs @@ -81,7 +81,8 @@ async fn run( tracing::debug!("6"); let api_client = - NymApiClient::new_with_timeout(default_api_url, config.nym_api_client_timeout()); + // TODO dz introduce timeout ? + NymApiClient::new(default_api_url); let gateways = api_client .get_cached_described_gateways() .await From 40d9321aecc68d4d58d027610570366b35c79260 Mon Sep 17 00:00:00 2001 From: Fran Arbanas Date: Fri, 18 Oct 2024 17:14:44 +0200 Subject: [PATCH 4/6] Node status API dockerfile and env vars (#4986) * feat: add dockerfile and env variables * Added workflow for pushing node status api on harbor * Misc changes to pathing and using yq instead of jq * fix: change the way we read env vars for nyxd, nym api and explorer * fix: docker build workflow * Remove config in favor of clap args * Added naming and tags * change from value to result --------- Co-authored-by: Lawrence Stalder Co-authored-by: dynco-nym <173912580+dynco-nym@users.noreply.github.com> --- .github/workflows/push-node-status-api.yaml | 54 ++++++++++++-- envs/mainnet.env | 6 +- nym-node-status-api/Cargo.toml | 11 ++- nym-node-status-api/Dockerfile | 15 ++++ nym-node-status-api/launch_node_status_api.sh | 14 +++- nym-node-status-api/src/cli/mod.rs | 62 ++++++++++++++-- nym-node-status-api/src/config.rs | 72 ------------------- nym-node-status-api/src/db/mod.rs | 5 +- nym-node-status-api/src/main.rs | 26 +++---- nym-node-status-api/src/monitor/mod.rs | 14 ++-- 10 files changed, 158 insertions(+), 121 deletions(-) create mode 100644 nym-node-status-api/Dockerfile delete mode 100644 nym-node-status-api/src/config.rs diff --git a/.github/workflows/push-node-status-api.yaml b/.github/workflows/push-node-status-api.yaml index 1a082a1ad20..941a8619f0f 100644 --- a/.github/workflows/push-node-status-api.yaml +++ b/.github/workflows/push-node-status-api.yaml @@ -1,11 +1,55 @@ name: Build and upload Node Status API container to harbor.nymte.ch - on: workflow_dispatch: +env: + WORKING_DIRECTORY: "nym-node-status-api" + CONTAINER_NAME: "node-status-api" + jobs: - my-job: - runs-on: arc-ubuntu-22.04 + build-container: + runs-on: arc-ubuntu-22.04-dind steps: - - name: my-step - run: echo "Hello World!" + - name: Login to Harbor + uses: docker/login-action@v3 + with: + registry: harbor.nymte.ch + username: ${{ secrets.HARBOR_ROBOT_USERNAME }} + password: ${{ secrets.HARBOR_ROBOT_SECRET }} + + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Configure git identity + run: | + git config --global user.email "lawrence@nymtech.net" + git config --global user.name "Lawrence Stalder" + + - name: Get version from cargo.toml + uses: mikefarah/yq@v4.44.3 + id: get_version + with: + cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml + + - name: Check if tag exists + run: | + if git rev-parse ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }} >/dev/null 2>&1; then + echo "Tag ${{ steps.get_version.outputs.result }} already exists" + fi + + - name: Remove existing tag if exists + run: | + if git rev-parse ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }} >/dev/null 2>&1; then + git push --delete origin ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }} + git tag -d ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }} + fi + + - name: Create tag + run: | + git tag -a ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }} -m "Version ${{ steps.get_version.outputs.result }}" + git push origin ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }} + + - name: BuildAndPushImageOnHarbor + run: | + docker build -f ${{ env.WORKING_DIRECTORY }}/Dockerfile . -t harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }}:${{ steps.get_version.outputs.result }} -t harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }}:latest + docker push harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }} --all-tags diff --git a/envs/mainnet.env b/envs/mainnet.env index e40e54a4711..419547bf974 100644 --- a/envs/mainnet.env +++ b/envs/mainnet.env @@ -21,8 +21,8 @@ COCONUT_DKG_CONTRACT_ADDRESS=n19604yflqggs9mk2z26mqygq43q2kr3n932egxx630svywd5mp REWARDING_VALIDATOR_ADDRESS=n10yyd98e2tuwu0f7ypz9dy3hhjw7v772q6287gy STATISTICS_SERVICE_DOMAIN_ADDRESS="https://mainnet-stats.nymte.ch:8090" -NYXD="https://rpc.nymtech.net" -NYM_API="https://validator.nymtech.net/api/" +NYXD=https://rpc.nymtech.net +NYM_API=https://validator.nymtech.net/api/ NYXD_WS="wss://rpc.nymtech.net/websocket" -EXPLORER_API="https://explorer.nymtech.net/api/" +EXPLORER_API=https://explorer.nymtech.net/api/ NYM_VPN_API="https://nymvpn.com/api" diff --git a/nym-node-status-api/Cargo.toml b/nym-node-status-api/Cargo.toml index 03e6fe60a71..056271e41d9 100644 --- a/nym-node-status-api/Cargo.toml +++ b/nym-node-status-api/Cargo.toml @@ -16,7 +16,7 @@ rust-version.workspace = true anyhow = { workspace = true } axum = { workspace = true, features = ["tokio"] } chrono = { workspace = true } -clap = { workspace = true, features = ["cargo", "derive"] } +clap = { workspace = true, features = ["cargo", "derive", "env", "string"] } cosmwasm-std = { workspace = true } envy = { workspace = true } futures-util = { workspace = true } @@ -53,5 +53,10 @@ utoipauto = { workspace = true } [build-dependencies] anyhow = { workspace = true } -tokio = { workspace = true, features = ["macros" ] } -sqlx = { workspace = true, features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"] } +tokio = { workspace = true, features = ["macros"] } +sqlx = { workspace = true, features = [ + "runtime-tokio-rustls", + "sqlite", + "macros", + "migrate", +] } diff --git a/nym-node-status-api/Dockerfile b/nym-node-status-api/Dockerfile new file mode 100644 index 00000000000..ceab7c93921 --- /dev/null +++ b/nym-node-status-api/Dockerfile @@ -0,0 +1,15 @@ +FROM rust:latest AS builder + +COPY ./ /usr/src/nym +WORKDIR /usr/src/nym/nym-node-status-api + +RUN cargo build --release + +FROM ubuntu:24.04 + +RUN apt-get update && apt-get install -y ca-certificates + +WORKDIR /nym + +COPY --from=builder /usr/src/nym/target/release/nym-node-status-api ./ +ENTRYPOINT [ "/nym/nym-node-status-api" ] diff --git a/nym-node-status-api/launch_node_status_api.sh b/nym-node-status-api/launch_node_status_api.sh index e5059c50110..1db6bcc1b2b 100755 --- a/nym-node-status-api/launch_node_status_api.sh +++ b/nym-node-status-api/launch_node_status_api.sh @@ -4,7 +4,15 @@ set -e export RUST_LOG=${RUST_LOG:-debug} -export NYM_API_CLIENT_TIMEOUT=60; -export EXPLORER_CLIENT_TIMEOUT=60; +export NYM_API_CLIENT_TIMEOUT=60 +export EXPLORER_CLIENT_TIMEOUT=60 +#export NYXD=https://rpc.nymtech.net +#export NYM_API=https://validator.nymtech.net/api/ +#export EXPLORER_API=https://explorer.nymtech.net/api/ +#export NETWORK_NAME=mainnet -cargo run --package nym-node-status-api --release -- --config-env-file ../envs/mainnet.env +#cargo run --package nym-node-status-api --release -- --connection-url "sqlite://node-status-api.sqlite?mode=rwc" + +cd .. +docker build -t node-status-api -f nym-node-status-api/Dockerfile . +docker run --env-file envs/mainnet.env -e NYM_NODE_STATUS_API_CONNECTION_URL="sqlite://node-status-api.sqlite?mode=rwc" node-status-api diff --git a/nym-node-status-api/src/cli/mod.rs b/nym-node-status-api/src/cli/mod.rs index 7f465788631..ad214b6a23a 100644 --- a/nym-node-status-api/src/cli/mod.rs +++ b/nym-node-status-api/src/cli/mod.rs @@ -1,6 +1,7 @@ use clap::Parser; use nym_bin_common::bin_info; -use std::sync::OnceLock; +use reqwest::Url; +use std::{sync::OnceLock, time::Duration}; // Helper for passing LONG_VERSION to clap fn pretty_build_info_static() -> &'static str { @@ -8,10 +9,61 @@ fn pretty_build_info_static() -> &'static str { PRETTY_BUILD_INFORMATION.get_or_init(|| bin_info!().pretty_print()) } -#[derive(Parser, Debug)] +#[derive(Clone, Debug, Parser)] #[clap(author = "Nymtech", version, long_version = pretty_build_info_static(), about)] pub(crate) struct Cli { - /// Path pointing to an env file that configures the Nym API. - #[clap(short, long)] - pub(crate) config_env_file: Option, + /// Network name for the network to which we're connecting. + #[clap(long, env = "NETWORK_NAME")] + pub(crate) network_name: String, + + /// Explorer api url. + #[clap(short, long, env = "EXPLORER_API")] + pub(crate) explorer_api: String, + + /// Nym api url. + #[clap(short, long, env = "NYM_API")] + pub(crate) nym_api: String, + + /// TTL for the http cache. + #[clap( + long, + default_value_t = 30, + env = "NYM_NODE_STATUS_API_NYM_HTTP_CACHE_TTL" + )] + pub(crate) nym_http_cache_ttl: u64, + + /// HTTP port on which to run node status api. + #[clap(long, default_value_t = 8000, env = "NYM_NODE_STATUS_API_HTTP_PORT")] + pub(crate) http_port: u16, + + /// Nyxd address. + #[clap(long, env = "NYXD")] + pub(crate) nyxd_addr: Url, + + /// Nym api client timeout. + #[clap( + long, + default_value = "15", + env = "NYM_NODE_STATUS_API_NYM_API_CLIENT_TIMEOUT" + )] + #[arg(value_parser = parse_duration)] + pub(crate) nym_api_client_timeout: Duration, + + /// Explorer api client timeout. + #[clap( + long, + default_value = "15", + env = "NYM_NODE_STATUS_API_EXPLORER_CLIENT_TIMEOUT" + )] + #[arg(value_parser = parse_duration)] + pub(crate) explorer_client_timeout: Duration, + + /// Connection url for the database. + #[clap(long, env = "NYM_NODE_STATUS_API_CONNECTION_URL")] + pub(crate) connection_url: String, +} + +fn parse_duration(arg: &str) -> Result { + let seconds = arg.parse()?; + Ok(std::time::Duration::from_secs(seconds)) } diff --git a/nym-node-status-api/src/config.rs b/nym-node-status-api/src/config.rs deleted file mode 100644 index c499ef3c65f..00000000000 --- a/nym-node-status-api/src/config.rs +++ /dev/null @@ -1,72 +0,0 @@ -use anyhow::anyhow; -use reqwest::Url; -use serde::Deserialize; -use std::time::Duration; - -#[derive(Debug, Clone, Deserialize)] -pub(crate) struct Config { - #[serde(default = "Config::default_http_cache_seconds")] - nym_http_cache_ttl: u64, - #[serde(default = "Config::default_http_port")] - http_port: u16, - #[serde(rename = "nyxd")] - nyxd_addr: Url, - #[serde(default = "Config::default_client_timeout")] - #[serde(deserialize_with = "parse_duration")] - explorer_client_timeout: Duration, -} - -impl Config { - pub(crate) fn from_env() -> anyhow::Result { - envy::from_env::().map_err(|e| { - tracing::error!("Failed to load config from env: {e}"); - anyhow::Error::from(e) - }) - } - - fn default_client_timeout() -> Duration { - Duration::from_secs(15) - } - - fn default_http_port() -> u16 { - 8000 - } - - fn default_http_cache_seconds() -> u64 { - 30 - } - - pub(crate) fn nym_http_cache_ttl(&self) -> u64 { - self.nym_http_cache_ttl - } - - pub(crate) fn http_port(&self) -> u16 { - self.http_port - } - - pub(crate) fn nyxd_addr(&self) -> &Url { - &self.nyxd_addr - } - - pub(crate) fn nym_explorer_client_timeout(&self) -> Duration { - self.explorer_client_timeout.to_owned() - } -} - -fn parse_duration<'de, D>(deserializer: D) -> Result -where - D: serde::Deserializer<'de>, -{ - let s: String = Deserialize::deserialize(deserializer)?; - let secs: u64 = s.parse().map_err(serde::de::Error::custom)?; - Ok(Duration::from_secs(secs)) -} - -pub(super) fn read_env_var(env_var: &str) -> anyhow::Result { - std::env::var(env_var) - .map_err(|_| anyhow!("You need to set {}", env_var)) - .map(|value| { - tracing::trace!("{}={}", env_var, value); - value - }) -} diff --git a/nym-node-status-api/src/db/mod.rs b/nym-node-status-api/src/db/mod.rs index 2df2483687f..8e840252f6e 100644 --- a/nym-node-status-api/src/db/mod.rs +++ b/nym-node-status-api/src/db/mod.rs @@ -1,13 +1,11 @@ use std::str::FromStr; -use crate::read_env_var; use anyhow::{anyhow, Result}; use sqlx::{migrate::Migrator, sqlite::SqliteConnectOptions, ConnectOptions, SqlitePool}; pub(crate) mod models; pub(crate) mod queries; -pub(crate) const DATABASE_URL_ENV_VAR: &str = "DATABASE_URL"; static MIGRATOR: Migrator = sqlx::migrate!("./migrations"); pub(crate) type DbPool = SqlitePool; @@ -17,8 +15,7 @@ pub(crate) struct Storage { } impl Storage { - pub async fn init() -> Result { - let connection_url = read_env_var(DATABASE_URL_ENV_VAR)?; + pub async fn init(connection_url: String) -> Result { let connect_options = SqliteConnectOptions::from_str(&connection_url)? .create_if_missing(true) .disable_statement_logging(); diff --git a/nym-node-status-api/src/main.rs b/nym-node-status-api/src/main.rs index 0fb1d0e1d2e..a15cfe68a4c 100644 --- a/nym-node-status-api/src/main.rs +++ b/nym-node-status-api/src/main.rs @@ -1,11 +1,7 @@ use clap::Parser; -use nym_network_defaults::setup_env; use nym_task::signal::wait_for_signal; -use crate::config::read_env_var; - mod cli; -mod config; mod db; mod http; mod logging; @@ -16,33 +12,27 @@ async fn main() -> anyhow::Result<()> { logging::setup_tracing_logger(); let args = cli::Cli::parse(); - // if dotenv file is present, load its values - // otherwise, default to mainnet - setup_env(args.config_env_file.as_ref()); - tracing::debug!("{:?}", read_env_var("NETWORK_NAME")); - tracing::debug!("{:?}", read_env_var("EXPLORER_API")); - tracing::debug!("{:?}", read_env_var("NYM_API")); - let conf = config::Config::from_env()?; - tracing::debug!("Using config:\n{:#?}", conf); + let connection_url = args.connection_url.clone(); + tracing::debug!("Using config:\n{:#?}", args); - let storage = db::Storage::init().await?; + let storage = db::Storage::init(connection_url).await?; let db_pool = storage.pool_owned().await; - let conf_clone = conf.clone(); + let args_clone = args.clone(); tokio::spawn(async move { - monitor::spawn_in_background(db_pool, conf_clone).await; + monitor::spawn_in_background(db_pool, args_clone).await; }); tracing::info!("Started monitor task"); let shutdown_handles = http::server::start_http_api( storage.pool_owned().await, - conf.http_port(), - conf.nym_http_cache_ttl(), + args.http_port, + args.nym_http_cache_ttl, ) .await .expect("Failed to start server"); - tracing::info!("Started HTTP server on port {}", conf.http_port()); + tracing::info!("Started HTTP server on port {}", args.http_port); wait_for_signal().await; diff --git a/nym-node-status-api/src/monitor/mod.rs b/nym-node-status-api/src/monitor/mod.rs index 5079af3b5a5..3c5172c8a9f 100644 --- a/nym-node-status-api/src/monitor/mod.rs +++ b/nym-node-status-api/src/monitor/mod.rs @@ -1,4 +1,4 @@ -use crate::config::Config; +use crate::cli::Cli; use crate::db::models::{ gateway, mixnode, GatewayRecord, MixnodeRecord, NetworkSummary, GATEWAYS_BLACKLISTED_COUNT, GATEWAYS_BONDED_COUNT, GATEWAYS_EXPLORER_COUNT, GATEWAYS_HISTORICAL_COUNT, @@ -29,7 +29,7 @@ static DELEGATION_PROGRAM_WALLET: &str = "n1rnxpdpx3kldygsklfft0gech7fhfcux4zst5 // TODO dz: query many NYM APIs: // multiple instances running directory cache, ask sachin -pub(crate) async fn spawn_in_background(db_pool: DbPool, config: Config) -> JoinHandle<()> { +pub(crate) async fn spawn_in_background(db_pool: DbPool, config: Cli) -> JoinHandle<()> { let network_defaults = nym_network_defaults::NymNetworkDetails::new_from_env(); loop { @@ -54,7 +54,7 @@ pub(crate) async fn spawn_in_background(db_pool: DbPool, config: Config) -> Join async fn run( pool: &DbPool, network_details: &NymNetworkDetails, - config: &Config, + config: &Cli, ) -> anyhow::Result<()> { let default_api_url = network_details .endpoints @@ -70,10 +70,8 @@ async fn run( let default_explorer_url = default_explorer_url.expect("explorer url missing in network config"); - let explorer_client = ExplorerClient::new_with_timeout( - default_explorer_url, - config.nym_explorer_client_timeout(), - )?; + let explorer_client = + ExplorerClient::new_with_timeout(default_explorer_url, config.explorer_client_timeout)?; let explorer_gateways = explorer_client .get_gateways() .await @@ -126,7 +124,7 @@ async fn run( .await .log_error("get_active_mixnodes")?; let delegation_program_members = - get_delegation_program_details(network_details, config.nyxd_addr()).await?; + get_delegation_program_details(network_details, &config.nyxd_addr).await?; // keep stats for later let count_bonded_mixnodes = mixnodes.len(); From cc983963d42c1241760eee68d4be6c5feeabbd60 Mon Sep 17 00:00:00 2001 From: Dinko Zdravac <173912580+dynco-nym@users.noreply.github.com> Date: Mon, 28 Oct 2024 16:53:36 +0100 Subject: [PATCH 5/6] Fully functional network scores (#5048) * Compile & copy wg probe * Node status agent WIP * Enable debug logging * Agent submits results - add clap to agent - agent runs network probe - /submit endpoint on NS API * Build clients with timeouts * Update logging and dev scripts * Replace /blaclisted endpoint * Testruns fully functional - task that queues testruns periodically - testruns read/write in DB * Probe scores fully working - testruns are assigned on API - submit updates testruns correctly on NS API side - agent registers with API - agent submits results correctly * Clippy fixes * PR feedback * Clippy again * PR feedback * Run clippy earlier in CI * Make refresh delay configurable in server & agent --- .github/workflows/ci-build.yml | 12 +- Cargo.lock | 185 +++++++++++------- Cargo.toml | 5 + common/bin-common/Cargo.toml | 1 + common/http-api-client/src/lib.rs | 12 ++ common/models/Cargo.toml | 14 ++ common/models/src/lib.rs | 1 + common/models/src/ns_api.rs | 8 + envs/canary.env | 4 +- envs/qa.env | 4 +- envs/sandbox.env | 6 +- explorer-api/explorer-client/src/lib.rs | 4 +- nym-data-observatory/README_SQLX.md | 1 + nym-node-status-agent/.gitignore | 1 + nym-node-status-agent/Cargo.toml | 27 +++ nym-node-status-agent/run.sh | 49 +++++ nym-node-status-agent/src/cli.rs | 109 +++++++++++ nym-node-status-agent/src/main.rs | 78 ++++++++ nym-node-status-agent/src/probe.rs | 54 +++++ nym-node-status-api/.gitignore | 4 + nym-node-status-api/Cargo.toml | 16 +- nym-node-status-api/Dockerfile.dev | 8 + nym-node-status-api/build.rs | 5 + nym-node-status-api/launch_node_status_api.sh | 37 +++- nym-node-status-api/migrations/000_init.sql | 12 ++ nym-node-status-api/src/cli/mod.rs | 30 +-- nym-node-status-api/src/db/mod.rs | 5 +- nym-node-status-api/src/db/models.rs | 34 ++++ nym-node-status-api/src/db/queries/mod.rs | 1 + .../src/db/queries/testruns.rs | 126 ++++++++++++ nym-node-status-api/src/http/api/gateways.rs | 2 +- nym-node-status-api/src/http/api/mod.rs | 5 +- nym-node-status-api/src/http/api/testruns.rs | 117 ++++++++++- nym-node-status-api/src/http/api_docs.rs | 2 +- nym-node-status-api/src/http/error.rs | 14 ++ nym-node-status-api/src/http/models.rs | 12 ++ nym-node-status-api/src/http/server.rs | 1 + nym-node-status-api/src/logging.rs | 49 +++-- nym-node-status-api/src/main.rs | 21 +- nym-node-status-api/src/monitor/mod.rs | 63 +++--- nym-node-status-api/src/testruns/mod.rs | 76 +++++++ nym-node-status-api/src/testruns/models.rs | 16 ++ nym-node-status-api/src/testruns/queue.rs | 118 +++++++++++ 43 files changed, 1184 insertions(+), 165 deletions(-) create mode 100644 common/models/Cargo.toml create mode 100644 common/models/src/lib.rs create mode 100644 common/models/src/ns_api.rs create mode 100644 nym-node-status-agent/.gitignore create mode 100644 nym-node-status-agent/Cargo.toml create mode 100755 nym-node-status-agent/run.sh create mode 100644 nym-node-status-agent/src/cli.rs create mode 100644 nym-node-status-agent/src/main.rs create mode 100644 nym-node-status-agent/src/probe.rs create mode 100644 nym-node-status-api/Dockerfile.dev create mode 100644 nym-node-status-api/src/db/queries/testruns.rs create mode 100644 nym-node-status-api/src/testruns/mod.rs create mode 100644 nym-node-status-api/src/testruns/models.rs create mode 100644 nym-node-status-api/src/testruns/queue.rs diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index 77d220e6337..d7a76f2673c 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -57,6 +57,12 @@ jobs: command: fmt args: --all -- --check + - name: Clippy + uses: actions-rs/cargo@v1 + with: + command: clippy + args: --workspace --all-targets -- -D warnings + - name: Build all binaries uses: actions-rs/cargo@v1 with: @@ -82,9 +88,3 @@ jobs: with: command: test args: --workspace -- --ignored - - - name: Clippy - uses: actions-rs/cargo@v1 - with: - command: clippy - args: --workspace --all-targets -- -D warnings diff --git a/Cargo.lock b/Cargo.lock index b9d06b3a8e9..def19342ded 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -466,6 +466,7 @@ checksum = "504e3947307ac8326a5437504c517c4b56716c9d98fac0028c2acc7ca47d70ae" dependencies = [ "async-trait", "axum-core 0.4.5", + "axum-macros", "bytes", "futures-util", "http 1.1.0", @@ -553,6 +554,17 @@ dependencies = [ "tracing", ] +[[package]] +name = "axum-macros" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57d123550fa8d071b7255cb0cc04dc302baa6c8c4a79f55701552684d8399bce" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.82", +] + [[package]] name = "axum-test" version = "16.2.0" @@ -4524,20 +4536,20 @@ dependencies = [ [[package]] name = "nym-api-requests" version = "0.1.0" -source = "git+https://github.com/nymtech/nym?rev=f86e08866#f86e0886631a98b0638fe09e6fcbe5458d47adc1" +source = "git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork#8d68cf88dac1adf1f68726a9b6e0740f8cddcf32" dependencies = [ "bs58", "cosmrs 0.17.0-pre", "cosmwasm-std", "ecdsa", "getset", - "nym-compact-ecash 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", - "nym-credentials-interface 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", - "nym-crypto 0.4.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", - "nym-ecash-time 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", - "nym-mixnet-contract-common 0.6.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", - "nym-node-requests 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", - "nym-serde-helpers 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", + "nym-compact-ecash 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", + "nym-credentials-interface 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", + "nym-crypto 0.4.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", + "nym-ecash-time 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", + "nym-mixnet-contract-common 0.6.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", + "nym-node-requests 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", + "nym-serde-helpers 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", "schemars", "serde", "sha2 0.10.8", @@ -4665,7 +4677,7 @@ dependencies = [ [[package]] name = "nym-bin-common" version = "0.6.0" -source = "git+https://github.com/nymtech/nym?rev=f86e08866#f86e0886631a98b0638fe09e6fcbe5458d47adc1" +source = "git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork#8d68cf88dac1adf1f68726a9b6e0740f8cddcf32" dependencies = [ "const-str", "log", @@ -4989,11 +5001,11 @@ dependencies = [ [[package]] name = "nym-coconut-bandwidth-contract-common" version = "0.1.0" -source = "git+https://github.com/nymtech/nym?rev=f86e08866#f86e0886631a98b0638fe09e6fcbe5458d47adc1" +source = "git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork#8d68cf88dac1adf1f68726a9b6e0740f8cddcf32" dependencies = [ "cosmwasm-schema", "cosmwasm-std", - "nym-multisig-contract-common 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", + "nym-multisig-contract-common 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", ] [[package]] @@ -5012,15 +5024,22 @@ dependencies = [ [[package]] name = "nym-coconut-dkg-common" version = "0.1.0" -source = "git+https://github.com/nymtech/nym?rev=f86e08866#f86e0886631a98b0638fe09e6fcbe5458d47adc1" +source = "git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork#8d68cf88dac1adf1f68726a9b6e0740f8cddcf32" dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw-utils", "cw2", "cw4", - "nym-contracts-common 0.5.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", - "nym-multisig-contract-common 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", + "nym-contracts-common 0.5.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", + "nym-multisig-contract-common 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", +] + +[[package]] +name = "nym-common-models" +version = "0.1.0" +dependencies = [ + "serde", ] [[package]] @@ -5050,7 +5069,7 @@ dependencies = [ [[package]] name = "nym-compact-ecash" version = "0.1.0" -source = "git+https://github.com/nymtech/nym?rev=f86e08866#f86e0886631a98b0638fe09e6fcbe5458d47adc1" +source = "git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork#8d68cf88dac1adf1f68726a9b6e0740f8cddcf32" dependencies = [ "bincode", "bls12_381", @@ -5060,8 +5079,8 @@ dependencies = [ "ff", "group", "itertools 0.12.1", - "nym-network-defaults 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", - "nym-pemstore 0.3.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", + "nym-network-defaults 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", + "nym-pemstore 0.3.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", "rand", "serde", "sha2 0.9.9", @@ -5085,12 +5104,12 @@ dependencies = [ [[package]] name = "nym-config" version = "0.1.0" -source = "git+https://github.com/nymtech/nym?rev=f86e08866#f86e0886631a98b0638fe09e6fcbe5458d47adc1" +source = "git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork#8d68cf88dac1adf1f68726a9b6e0740f8cddcf32" dependencies = [ "dirs", "handlebars", "log", - "nym-network-defaults 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", + "nym-network-defaults 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", "serde", "toml 0.8.14", "url", @@ -5114,7 +5133,7 @@ dependencies = [ [[package]] name = "nym-contracts-common" version = "0.5.0" -source = "git+https://github.com/nymtech/nym?rev=f86e08866#f86e0886631a98b0638fe09e6fcbe5458d47adc1" +source = "git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork#8d68cf88dac1adf1f68726a9b6e0740f8cddcf32" dependencies = [ "bs58", "cosmwasm-schema", @@ -5249,12 +5268,12 @@ dependencies = [ [[package]] name = "nym-credentials-interface" version = "0.1.0" -source = "git+https://github.com/nymtech/nym?rev=f86e08866#f86e0886631a98b0638fe09e6fcbe5458d47adc1" +source = "git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork#8d68cf88dac1adf1f68726a9b6e0740f8cddcf32" dependencies = [ "bls12_381", - "nym-compact-ecash 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", - "nym-ecash-time 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", - "nym-network-defaults 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", + "nym-compact-ecash 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", + "nym-ecash-time 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", + "nym-network-defaults 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", "rand", "serde", "strum 0.26.3", @@ -5293,12 +5312,12 @@ dependencies = [ [[package]] name = "nym-crypto" version = "0.4.0" -source = "git+https://github.com/nymtech/nym?rev=f86e08866#f86e0886631a98b0638fe09e6fcbe5458d47adc1" +source = "git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork#8d68cf88dac1adf1f68726a9b6e0740f8cddcf32" dependencies = [ "bs58", "ed25519-dalek", - "nym-pemstore 0.3.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", - "nym-sphinx-types 0.2.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", + "nym-pemstore 0.3.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", + "nym-sphinx-types 0.2.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", "serde", "serde_bytes", "subtle-encoding", @@ -5372,14 +5391,14 @@ dependencies = [ [[package]] name = "nym-ecash-contract-common" version = "0.1.0" -source = "git+https://github.com/nymtech/nym?rev=f86e08866#f86e0886631a98b0638fe09e6fcbe5458d47adc1" +source = "git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork#8d68cf88dac1adf1f68726a9b6e0740f8cddcf32" dependencies = [ "bs58", "cosmwasm-schema", "cosmwasm-std", "cw-controllers", "cw-utils", - "nym-multisig-contract-common 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", + "nym-multisig-contract-common 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", "thiserror", ] @@ -5403,7 +5422,7 @@ dependencies = [ [[package]] name = "nym-ecash-time" version = "0.1.0" -source = "git+https://github.com/nymtech/nym?rev=f86e08866#f86e0886631a98b0638fe09e6fcbe5458d47adc1" +source = "git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork#8d68cf88dac1adf1f68726a9b6e0740f8cddcf32" dependencies = [ "time", ] @@ -5431,7 +5450,7 @@ dependencies = [ [[package]] name = "nym-exit-policy" version = "0.1.0" -source = "git+https://github.com/nymtech/nym?rev=f86e08866#f86e0886631a98b0638fe09e6fcbe5458d47adc1" +source = "git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork#8d68cf88dac1adf1f68726a9b6e0740f8cddcf32" dependencies = [ "serde", "serde_json", @@ -5663,7 +5682,7 @@ dependencies = [ [[package]] name = "nym-group-contract-common" version = "0.1.0" -source = "git+https://github.com/nymtech/nym?rev=f86e08866#f86e0886631a98b0638fe09e6fcbe5458d47adc1" +source = "git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork#8d68cf88dac1adf1f68726a9b6e0740f8cddcf32" dependencies = [ "cosmwasm-schema", "cw-controllers", @@ -5691,11 +5710,11 @@ dependencies = [ [[package]] name = "nym-http-api-client" version = "0.1.0" -source = "git+https://github.com/nymtech/nym?rev=f86e08866#f86e0886631a98b0638fe09e6fcbe5458d47adc1" +source = "git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork#8d68cf88dac1adf1f68726a9b6e0740f8cddcf32" dependencies = [ "async-trait", "http 1.1.0", - "nym-bin-common 0.6.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", + "nym-bin-common 0.6.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", "reqwest 0.12.4", "serde", "serde_json", @@ -5874,7 +5893,7 @@ dependencies = [ [[package]] name = "nym-mixnet-contract-common" version = "0.6.0" -source = "git+https://github.com/nymtech/nym?rev=f86e08866#f86e0886631a98b0638fe09e6fcbe5458d47adc1" +source = "git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork#8d68cf88dac1adf1f68726a9b6e0740f8cddcf32" dependencies = [ "bs58", "cosmwasm-schema", @@ -5882,7 +5901,7 @@ dependencies = [ "cw-controllers", "humantime-serde", "log", - "nym-contracts-common 0.5.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", + "nym-contracts-common 0.5.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", "schemars", "serde", "serde-json-wasm", @@ -5984,7 +6003,7 @@ dependencies = [ [[package]] name = "nym-multisig-contract-common" version = "0.1.0" -source = "git+https://github.com/nymtech/nym?rev=f86e08866#f86e0886631a98b0638fe09e6fcbe5458d47adc1" +source = "git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork#8d68cf88dac1adf1f68726a9b6e0740f8cddcf32" dependencies = [ "cosmwasm-schema", "cosmwasm-std", @@ -6012,7 +6031,7 @@ dependencies = [ [[package]] name = "nym-network-defaults" version = "0.1.0" -source = "git+https://github.com/nymtech/nym?rev=f86e08866#f86e0886631a98b0638fe09e6fcbe5458d47adc1" +source = "git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork#8d68cf88dac1adf1f68726a9b6e0740f8cddcf32" dependencies = [ "dotenvy", "log", @@ -6207,18 +6226,18 @@ dependencies = [ [[package]] name = "nym-node-requests" version = "0.1.0" -source = "git+https://github.com/nymtech/nym?rev=f86e08866#f86e0886631a98b0638fe09e6fcbe5458d47adc1" +source = "git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork#8d68cf88dac1adf1f68726a9b6e0740f8cddcf32" dependencies = [ "async-trait", "base64 0.22.1", "celes", "humantime 2.1.0", "humantime-serde", - "nym-bin-common 0.6.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", - "nym-crypto 0.4.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", - "nym-exit-policy 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", - "nym-http-api-client 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", - "nym-wireguard-types 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", + "nym-bin-common 0.6.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", + "nym-crypto 0.4.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", + "nym-exit-policy 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", + "nym-http-api-client 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", + "nym-wireguard-types 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", "schemars", "serde", "serde_json", @@ -6227,6 +6246,22 @@ dependencies = [ "utoipa", ] +[[package]] +name = "nym-node-status-agent" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap 4.5.20", + "nym-bin-common 0.6.0", + "nym-common-models", + "reqwest 0.12.4", + "serde_json", + "tokio", + "tokio-util", + "tracing", + "tracing-subscriber", +] + [[package]] name = "nym-node-status-api" version = "0.1.0" @@ -6240,16 +6275,20 @@ dependencies = [ "futures-util", "moka", "nym-bin-common 0.6.0", + "nym-common-models", "nym-explorer-client", - "nym-network-defaults 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", - "nym-node-requests 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", + "nym-network-defaults 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", + "nym-node-requests 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", "nym-task", - "nym-validator-client 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", + "nym-validator-client 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", + "regex", "reqwest 0.12.4", "serde", "serde_json", "serde_json_path", "sqlx", + "strum 0.26.3", + "strum_macros 0.26.4", "thiserror", "tokio", "tokio-util", @@ -6365,7 +6404,7 @@ dependencies = [ [[package]] name = "nym-pemstore" version = "0.3.0" -source = "git+https://github.com/nymtech/nym?rev=f86e08866#f86e0886631a98b0638fe09e6fcbe5458d47adc1" +source = "git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork#8d68cf88dac1adf1f68726a9b6e0740f8cddcf32" dependencies = [ "pem", ] @@ -6438,7 +6477,7 @@ dependencies = [ [[package]] name = "nym-serde-helpers" version = "0.1.0" -source = "git+https://github.com/nymtech/nym?rev=f86e08866#f86e0886631a98b0638fe09e6fcbe5458d47adc1" +source = "git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork#8d68cf88dac1adf1f68726a9b6e0740f8cddcf32" dependencies = [ "base64 0.22.1", "bs58", @@ -6753,7 +6792,7 @@ dependencies = [ [[package]] name = "nym-sphinx-types" version = "0.2.0" -source = "git+https://github.com/nymtech/nym?rev=f86e08866#f86e0886631a98b0638fe09e6fcbe5458d47adc1" +source = "git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork#8d68cf88dac1adf1f68726a9b6e0740f8cddcf32" dependencies = [ "sphinx-packet", "thiserror", @@ -6917,7 +6956,7 @@ dependencies = [ [[package]] name = "nym-validator-client" version = "0.1.0" -source = "git+https://github.com/nymtech/nym?rev=f86e08866#f86e0886631a98b0638fe09e6fcbe5458d47adc1" +source = "git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork#8d68cf88dac1adf1f68726a9b6e0740f8cddcf32" dependencies = [ "async-trait", "base64 0.22.1", @@ -6935,20 +6974,19 @@ dependencies = [ "flate2", "futures", "itertools 0.13.0", - "log", - "nym-api-requests 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", - "nym-coconut-bandwidth-contract-common 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", - "nym-coconut-dkg-common 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", - "nym-compact-ecash 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", - "nym-config 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", - "nym-contracts-common 0.5.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", - "nym-ecash-contract-common 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", - "nym-group-contract-common 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", - "nym-http-api-client 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", - "nym-mixnet-contract-common 0.6.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", - "nym-multisig-contract-common 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", - "nym-network-defaults 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", - "nym-vesting-contract-common 0.7.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", + "nym-api-requests 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", + "nym-coconut-bandwidth-contract-common 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", + "nym-coconut-dkg-common 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", + "nym-compact-ecash 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", + "nym-config 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", + "nym-contracts-common 0.5.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", + "nym-ecash-contract-common 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", + "nym-group-contract-common 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", + "nym-http-api-client 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", + "nym-mixnet-contract-common 0.6.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", + "nym-multisig-contract-common 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", + "nym-network-defaults 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", + "nym-vesting-contract-common 0.7.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", "prost 0.12.6", "reqwest 0.12.4", "serde", @@ -6958,6 +6996,7 @@ dependencies = [ "thiserror", "time", "tokio", + "tracing", "url", "wasmtimer", "zeroize", @@ -7017,12 +7056,12 @@ dependencies = [ [[package]] name = "nym-vesting-contract-common" version = "0.7.0" -source = "git+https://github.com/nymtech/nym?rev=f86e08866#f86e0886631a98b0638fe09e6fcbe5458d47adc1" +source = "git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork#8d68cf88dac1adf1f68726a9b6e0740f8cddcf32" dependencies = [ "cosmwasm-schema", "cosmwasm-std", - "nym-contracts-common 0.5.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", - "nym-mixnet-contract-common 0.6.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", + "nym-contracts-common 0.5.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", + "nym-mixnet-contract-common 0.6.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", "serde", "thiserror", ] @@ -7089,12 +7128,12 @@ dependencies = [ [[package]] name = "nym-wireguard-types" version = "0.1.0" -source = "git+https://github.com/nymtech/nym?rev=f86e08866#f86e0886631a98b0638fe09e6fcbe5458d47adc1" +source = "git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork#8d68cf88dac1adf1f68726a9b6e0740f8cddcf32" dependencies = [ "base64 0.22.1", "log", - "nym-config 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", - "nym-network-defaults 0.1.0 (git+https://github.com/nymtech/nym?rev=f86e08866)", + "nym-config 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", + "nym-network-defaults 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", "serde", "thiserror", "x25519-dalek", @@ -8001,9 +8040,9 @@ dependencies = [ [[package]] name = "raw-cpuid" -version = "11.1.0" +version = "11.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb9ee317cfe3fbd54b36a511efc1edd42e216903c9cd575e686dd68a2ba90d8d" +checksum = "1ab240315c661615f2ee9f0f2cd32d5a7343a84d5ebcccb99d46e6637565e7b0" dependencies = [ "bitflags 2.5.0", ] diff --git a/Cargo.toml b/Cargo.toml index d431d4567b9..d99e3d75320 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -61,6 +61,7 @@ members = [ "common/ip-packet-requests", "common/ledger", "common/mixnode-common", + "common/models", "common/network-defaults", "common/node-tester-utils", "common/nonexhaustive-delayqueue", @@ -121,6 +122,7 @@ members = [ "nym-node/nym-node-http-api", "nym-node/nym-node-requests", "nym-node-status-api", + "nym-node-status-agent", "nym-outfox", "nym-validator-rewarder", "tools/echo-server", @@ -148,12 +150,14 @@ members = [ default-members = [ "clients/native", "clients/socks5", + "common/models", "explorer-api", "gateway", "mixnode", "nym-api", "nym-data-observatory", "nym-node", + "nym-node-status-api", "nym-validator-rewarder", "nym-node-status-api", "service-providers/authenticator", @@ -314,6 +318,7 @@ si-scale = "0.2.3" sphinx-packet = "0.1.1" sqlx = "0.7.4" strum = "0.26" +strum_macros = "0.26" subtle-encoding = "0.5" syn = "1" sysinfo = "0.30.13" diff --git a/common/bin-common/Cargo.toml b/common/bin-common/Cargo.toml index b63631ddaae..11a2c76f2b9 100644 --- a/common/bin-common/Cargo.toml +++ b/common/bin-common/Cargo.toml @@ -45,3 +45,4 @@ tracing = [ "opentelemetry", ] clap = [ "dep:clap", "dep:clap_complete", "dep:clap_complete_fig" ] +models = [] diff --git a/common/http-api-client/src/lib.rs b/common/http-api-client/src/lib.rs index c90d731adbd..f35c662e88b 100644 --- a/common/http-api-client/src/lib.rs +++ b/common/http-api-client/src/lib.rs @@ -531,6 +531,18 @@ where } if res.status().is_success() { + #[cfg(debug_assertions)] + { + let text = res.text().await.inspect_err(|err| { + tracing::error!("Couldn't even get response text: {err}"); + })?; + tracing::trace!("Result:\n{:#?}", text); + + serde_json::from_str(&text) + .map_err(|err| HttpClientError::GenericRequestFailure(err.to_string())) + } + + #[cfg(not(debug_assertions))] Ok(res.json().await?) } else if res.status() == StatusCode::NOT_FOUND { Err(HttpClientError::NotFound) diff --git a/common/models/Cargo.toml b/common/models/Cargo.toml new file mode 100644 index 00000000000..acb6e35682d --- /dev/null +++ b/common/models/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "nym-common-models" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true + +[dependencies] +serde = { workspace = true, features = ["derive"] } diff --git a/common/models/src/lib.rs b/common/models/src/lib.rs new file mode 100644 index 00000000000..3d85e66947b --- /dev/null +++ b/common/models/src/lib.rs @@ -0,0 +1 @@ +pub mod ns_api; diff --git a/common/models/src/ns_api.rs b/common/models/src/ns_api.rs new file mode 100644 index 00000000000..9c3373802a4 --- /dev/null +++ b/common/models/src/ns_api.rs @@ -0,0 +1,8 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct TestrunAssignment { + /// has nothing to do with GW identity key. This is PK from `gateways` table + pub testrun_id: i64, + pub gateway_pk_id: i64, +} diff --git a/envs/canary.env b/envs/canary.env index c4aac150823..db70a19fd05 100644 --- a/envs/canary.env +++ b/envs/canary.env @@ -19,5 +19,5 @@ MULTISIG_CONTRACT_ADDRESS=n1zwv6feuzhy6a9wekh96cd57lsarmqlwxdypdsplw6zhfncqw6ftq COCONUT_DKG_CONTRACT_ADDRESS=n1aakfpghcanxtc45gpqlx8j3rq0zcpyf49qmhm9mdjrfx036h4z5sy2vfh9 EXPLORER_API=https://canary-explorer.performance.nymte.ch/api -NYXD="https://canary-validator.performance.nymte.ch" -NYM_API="https://canary-api.performance.nymte.ch/api" +NYXD=https://canary-validator.performance.nymte.ch +NYM_API=https://canary-api.performance.nymte.ch/api diff --git a/envs/qa.env b/envs/qa.env index e88f8454160..34306a62eae 100644 --- a/envs/qa.env +++ b/envs/qa.env @@ -19,5 +19,5 @@ VESTING_CONTRACT_ADDRESS=n1jlzdxnyces4hrhqz68dqk28mrw5jgwtcfq0c2funcwrmw0dx9l9s8 REWARDING_VALIDATOR_ADDRESS=n1rfvpsynktze6wvn6ldskj8xgwfzzk5v6pnff39 EXPLORER_API=https://qa-network-explorer.qa.nymte.ch/api -NYXD="https://qa-validator.qa.nymte.ch" -NYM_API="https://qa-nym-api.qa.nymte.ch/api" +NYXD=https://qa-validator.qa.nymte.ch +NYM_API=https://qa-nym-api.qa.nymte.ch/api diff --git a/envs/sandbox.env b/envs/sandbox.env index 4763269a6f3..6310b8fa897 100644 --- a/envs/sandbox.env +++ b/envs/sandbox.env @@ -20,6 +20,6 @@ ECASH_CONTRACT_ADDRESS=n1v3vydvs2ued84yv3khqwtgldmgwn0elljsdh08dr5s2j9x4rc5fs9jl STATISTICS_SERVICE_DOMAIN_ADDRESS="http://0.0.0.0" EXPLORER_API=https://sandbox-explorer.nymtech.net/api -NYXD="https://rpc.sandbox.nymtech.net" -NYXD_WS="wss://rpc.sandbox.nymtech.net/websocket" -NYM_API="https://sandbox-nym-api1.nymtech.net/api" +NYXD=https://rpc.sandbox.nymtech.net +NYXD_WS=wss://rpc.sandbox.nymtech.net/websocket +NYM_API=https://sandbox-nym-api1.nymtech.net/api diff --git a/explorer-api/explorer-client/src/lib.rs b/explorer-api/explorer-client/src/lib.rs index 2415c90c690..50c5431c25f 100644 --- a/explorer-api/explorer-client/src/lib.rs +++ b/explorer-api/explorer-client/src/lib.rs @@ -83,7 +83,9 @@ impl ExplorerClient { } else if response.status() == StatusCode::NOT_FOUND { Err(ExplorerApiError::NotFound) } else { - Err(ExplorerApiError::RequestFailure(response.text().await?)) + let status = response.status(); + let err_msg = format!("{}: {}", response.text().await?, status); + Err(ExplorerApiError::RequestFailure(err_msg)) } } diff --git a/nym-data-observatory/README_SQLX.md b/nym-data-observatory/README_SQLX.md index 3e1021c8eb1..72cca4294f1 100644 --- a/nym-data-observatory/README_SQLX.md +++ b/nym-data-observatory/README_SQLX.md @@ -68,6 +68,7 @@ warning: no queries found; do you have the `offline` feature enabled ### Possible solutions - does your `sqlx-cli` version match `sqlx` version from `Cargo.toml`? + + `cargo install -f sqlx-cli --version ` ``` cargo install sqlx-cli --version --force ``` diff --git a/nym-node-status-agent/.gitignore b/nym-node-status-agent/.gitignore new file mode 100644 index 00000000000..bf462f3e052 --- /dev/null +++ b/nym-node-status-agent/.gitignore @@ -0,0 +1 @@ +nym-gateway-probe diff --git a/nym-node-status-agent/Cargo.toml b/nym-node-status-agent/Cargo.toml new file mode 100644 index 00000000000..a2ce8cdf977 --- /dev/null +++ b/nym-node-status-agent/Cargo.toml @@ -0,0 +1,27 @@ +# Copyright 2024 - Nym Technologies SA +# SPDX-License-Identifier: Apache-2.0 + + +[package] +name = "nym-node-status-agent" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true + +[dependencies] +anyhow = { workspace = true} +clap = { workspace = true, features = ["derive", "env"] } +nym-bin-common = { path = "../common/bin-common", features = ["models"]} +nym-common-models = { path = "../common/models" } +tokio = { workspace = true, features = ["macros", "rt-multi-thread", "process"] } +tokio-util = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true, features = ["env-filter"] } +reqwest = { workspace = true, features = ["json"] } +serde_json = { workspace = true } diff --git a/nym-node-status-agent/run.sh b/nym-node-status-agent/run.sh new file mode 100755 index 00000000000..5054a90825d --- /dev/null +++ b/nym-node-status-agent/run.sh @@ -0,0 +1,49 @@ +#!/bin/bash + +set -eu + +export RUST_LOG=${RUST_LOG:-debug} + +crate_root=$(dirname $(realpath "$0")) +gateway_probe_src=$(dirname $(dirname "$crate_root"))/nym-vpn-client/nym-vpn-core +echo "gateway_probe_src=$gateway_probe_src" +echo "crate_root=$crate_root" + +export NODE_STATUS_AGENT_PROBE_PATH="$crate_root/nym-gateway-probe" + +# build & copy over GW probe +function copy_gw_probe() { + pushd $gateway_probe_src + cargo build --release --package nym-gateway-probe + cp target/release/nym-gateway-probe "$crate_root" + $crate_root/nym-gateway-probe --version + popd +} + +function build_agent() { + cargo build --package nym-node-status-agent --release +} + +function swarm() { + local workers=$1 + echo "Running $workers in parallel" + + build_agent + + for ((i=1; i<=$workers; i++)); do + ../target/release/nym-node-status-agent run-probe & + done + + wait + + echo "All agents completed" +} + +export NODE_STATUS_AGENT_SERVER_ADDRESS="http://127.0.0.1" +export NODE_STATUS_AGENT_SERVER_PORT="8000" + +copy_gw_probe + +swarm 30 + +# cargo run -- run-probe diff --git a/nym-node-status-agent/src/cli.rs b/nym-node-status-agent/src/cli.rs new file mode 100644 index 00000000000..c4465797d0d --- /dev/null +++ b/nym-node-status-agent/src/cli.rs @@ -0,0 +1,109 @@ +use clap::{Parser, Subcommand}; +use nym_bin_common::bin_info; +use nym_common_models::ns_api::TestrunAssignment; +use std::sync::OnceLock; +use tracing::instrument; + +use crate::probe::GwProbe; + +// Helper for passing LONG_VERSION to clap +fn pretty_build_info_static() -> &'static str { + static PRETTY_BUILD_INFORMATION: OnceLock = OnceLock::new(); + PRETTY_BUILD_INFORMATION.get_or_init(|| bin_info!().pretty_print()) +} + +#[derive(Parser, Debug)] +#[clap(author = "Nymtech", version, long_version = pretty_build_info_static(), about)] +pub(crate) struct Args { + #[command(subcommand)] + pub(crate) command: Command, + #[arg(short, long, env = "NODE_STATUS_AGENT_SERVER_ADDRESS")] + pub(crate) server_address: String, + + #[arg(short = 'p', long, env = "NODE_STATUS_AGENT_SERVER_PORT")] + pub(crate) server_port: u16, + // TODO dz accept keypair for identification / auth +} + +#[derive(Subcommand, Debug)] +pub(crate) enum Command { + RunProbe { + /// path of binary to run + #[arg(long, env = "NODE_STATUS_AGENT_PROBE_PATH")] + probe_path: String, + #[arg(short, long, env = "NODE_STATUS_AGENT_GATEWAY_ID")] + gateway_id: Option, + }, +} + +impl Args { + pub(crate) async fn execute(&self) -> anyhow::Result<()> { + match &self.command { + Command::RunProbe { + probe_path, + gateway_id, + } => self.run_probe(probe_path, gateway_id).await?, + } + + Ok(()) + } + + async fn run_probe(&self, probe_path: &str, gateway_id: &Option) -> anyhow::Result<()> { + let server_address = format!("{}:{}", &self.server_address, self.server_port); + + let probe = GwProbe::new(probe_path.to_string()); + + let version = probe.version().await; + tracing::info!("Probe version:\n{}", version); + + let testrun = request_testrun(&server_address).await?; + + let log = probe.run_and_get_log(gateway_id); + + submit_results(&server_address, testrun.testrun_id, log).await?; + + Ok(()) + } +} + +const URL_BASE: &str = "internal/testruns"; + +#[instrument(level = "debug", skip_all)] +async fn request_testrun(server_addr: &str) -> anyhow::Result { + let target_url = format!("{}/{}", server_addr, URL_BASE); + let client = reqwest::Client::new(); + let res = client + .get(target_url) + .send() + .await + .and_then(|response| response.error_for_status())?; + res.json() + .await + .map(|testrun| { + tracing::info!("Received testrun assignment: {:?}", testrun); + testrun + }) + .map_err(|err| { + tracing::error!("err"); + err.into() + }) +} + +#[instrument(level = "debug", skip(probe_outcome))] +async fn submit_results( + server_addr: &str, + testrun_id: i64, + probe_outcome: String, +) -> anyhow::Result<()> { + let target_url = format!("{}/{}/{}", server_addr, URL_BASE, testrun_id); + let client = reqwest::Client::new(); + let res = client + .post(target_url) + .body(probe_outcome) + .send() + .await + .and_then(|response| response.error_for_status())?; + + tracing::debug!("Submitted results: {})", res.status()); + Ok(()) +} diff --git a/nym-node-status-agent/src/main.rs b/nym-node-status-agent/src/main.rs new file mode 100644 index 00000000000..133828b7fa1 --- /dev/null +++ b/nym-node-status-agent/src/main.rs @@ -0,0 +1,78 @@ +use crate::cli::Args; +use clap::Parser; +use tracing::level_filters::LevelFilter; +use tracing_subscriber::{filter::Directive, EnvFilter}; + +mod cli; +mod probe; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + setup_tracing(); + let args = Args::parse(); + + let server_addr = format!("{}:{}", args.server_address, args.server_port); + test_ns_api_conn(&server_addr).await?; + + args.execute().await?; + + Ok(()) +} + +async fn test_ns_api_conn(server_addr: &str) -> anyhow::Result<()> { + reqwest::get(server_addr) + .await + .map(|res| { + tracing::info!( + "Testing connection to NS API at {server_addr}: {}", + res.status() + ); + }) + .map_err(|err| anyhow::anyhow!("Couldn't connect to server on {}: {}", server_addr, err)) +} + +pub(crate) fn setup_tracing() { + fn directive_checked(directive: impl Into) -> Directive { + directive + .into() + .parse() + .expect("Failed to parse log directive") + } + + let log_builder = tracing_subscriber::fmt() + // Use a more compact, abbreviated log format + .compact() + // Display source code file paths + .with_file(true) + // Display source code line numbers + .with_line_number(true) + .with_thread_ids(true) + // Don't display the event's target (module path) + .with_target(false); + + let mut filter = EnvFilter::builder() + // if RUST_LOG isn't set, set default level + .with_default_directive(LevelFilter::INFO.into()) + .from_env_lossy(); + // these crates are more granularly filtered + let filter_crates = [ + "reqwest", + "rustls", + "hyper", + "sqlx", + "h2", + "tendermint_rpc", + "tower_http", + "axum", + ]; + for crate_name in filter_crates { + filter = filter.add_directive(directive_checked(format!("{}=warn", crate_name))); + } + + filter = filter.add_directive(directive_checked("nym_bin_common=debug")); + filter = filter.add_directive(directive_checked("nym_explorer_client=debug")); + filter = filter.add_directive(directive_checked("nym_network_defaults=debug")); + filter = filter.add_directive(directive_checked("nym_validator_client=debug")); + + log_builder.with_env_filter(filter).init(); +} diff --git a/nym-node-status-agent/src/probe.rs b/nym-node-status-agent/src/probe.rs new file mode 100644 index 00000000000..c75900e936f --- /dev/null +++ b/nym-node-status-agent/src/probe.rs @@ -0,0 +1,54 @@ +use tracing::error; + +pub(crate) struct GwProbe { + path: String, +} + +impl GwProbe { + pub(crate) fn new(probe_path: String) -> Self { + Self { path: probe_path } + } + + pub(crate) async fn version(&self) -> String { + let mut command = tokio::process::Command::new(&self.path); + command.stdout(std::process::Stdio::piped()); + command.arg("--version"); + + match command.spawn() { + Ok(child) => { + if let Ok(output) = child.wait_with_output().await { + return String::from_utf8(output.stdout) + .unwrap_or("Unable to get log from test run".to_string()); + } + "Unable to get probe version".to_string() + } + Err(e) => { + error!("Failed to get probe version: {}", e); + "Failed to get probe version".to_string() + } + } + } + + pub(crate) fn run_and_get_log(&self, gateway_key: &Option) -> String { + let mut command = std::process::Command::new(&self.path); + command.stdout(std::process::Stdio::piped()); + + if let Some(gateway_id) = gateway_key { + command.arg("--gateway").arg(gateway_id); + } + + match command.spawn() { + Ok(child) => { + if let Ok(output) = child.wait_with_output() { + return String::from_utf8(output.stdout) + .unwrap_or("Unable to get log from test run".to_string()); + } + "Unable to get log from test run".to_string() + } + Err(e) => { + error!("Failed to spawn test: {}", e); + "Failed to spawn test run task".to_string() + } + } + } +} diff --git a/nym-node-status-api/.gitignore b/nym-node-status-api/.gitignore index b2a9b208f06..91459afabef 100644 --- a/nym-node-status-api/.gitignore +++ b/nym-node-status-api/.gitignore @@ -1,2 +1,6 @@ data/ enter_db.sh +nym-gateway-probe +nym-node-status-api +*.sqlite +*.sqlite-journal diff --git a/nym-node-status-api/Cargo.toml b/nym-node-status-api/Cargo.toml index 056271e41d9..4cc9ff5d283 100644 --- a/nym-node-status-api/Cargo.toml +++ b/nym-node-status-api/Cargo.toml @@ -14,27 +14,31 @@ rust-version.workspace = true [dependencies] anyhow = { workspace = true } -axum = { workspace = true, features = ["tokio"] } +axum = { workspace = true, features = ["tokio", "macros"] } chrono = { workspace = true } clap = { workspace = true, features = ["cargo", "derive", "env", "string"] } cosmwasm-std = { workspace = true } envy = { workspace = true } futures-util = { workspace = true } moka = { workspace = true, features = ["future"] } -nym-bin-common = { path = "../common/bin-common" } +nym-bin-common = { path = "../common/bin-common", features = ["models"]} +nym-common-models = { path = "../common/models" } nym-explorer-client = { path = "../explorer-api/explorer-client" } -# TODO dz: ref before Nym API client changes. Update to latest develop once new Nym API is live -nym-network-defaults = { git = "https://github.com/nymtech/nym", rev = "f86e08866" } -nym-validator-client = { git = "https://github.com/nymtech/nym", rev = "f86e08866" } +# TODO dz: before Nym API client breaking changes. Update to latest develop once new Nym API is live +nym-network-defaults = { git = "https://github.com/nymtech/nym", branch = "pre-dir-v2-fork" } +nym-validator-client = { git = "https://github.com/nymtech/nym", branch = "pre-dir-v2-fork" } # nym-network-defaults = { path = "../common/network-defaults" } # nym-validator-client = { path = "../common/client-libs/validator-client" } nym-task = { path = "../common/task" } -nym-node-requests = { git = "https://github.com/nymtech/nym", rev = "f86e08866" } +nym-node-requests = { git = "https://github.com/nymtech/nym", branch = "pre-dir-v2-fork" } # nym-node-requests = { path = "../nym-node/nym-node-requests", features = ["openapi"] } +regex = { workspace = true } reqwest = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } serde_json_path = { workspace = true } +strum = { workspace = true } +strum_macros = { workspace = true } sqlx = { workspace = true, features = ["runtime-tokio-rustls", "sqlite"] } thiserror = { workspace = true } tokio = { workspace = true, features = ["rt-multi-thread"] } diff --git a/nym-node-status-api/Dockerfile.dev b/nym-node-status-api/Dockerfile.dev new file mode 100644 index 00000000000..2967a6e6059 --- /dev/null +++ b/nym-node-status-api/Dockerfile.dev @@ -0,0 +1,8 @@ +FROM ubuntu:22.04 + +RUN apt-get update && apt-get install -y ca-certificates + +WORKDIR /nym + +COPY nym-node-status-api/nym-node-status-api ./ +ENTRYPOINT [ "/nym/nym-node-status-api" ] diff --git a/nym-node-status-api/build.rs b/nym-node-status-api/build.rs index 394083f8bed..7bc42cb03f0 100644 --- a/nym-node-status-api/build.rs +++ b/nym-node-status-api/build.rs @@ -1,5 +1,7 @@ use anyhow::{anyhow, Result}; use sqlx::{Connection, SqliteConnection}; +use std::fs::Permissions; +use std::os::unix::fs::PermissionsExt; use tokio::{fs::File, io::AsyncWriteExt}; const SQLITE_DB_FILENAME: &str = "nym-node-status-api.sqlite"; @@ -41,6 +43,9 @@ async fn write_db_path_to_file(out_dir: &str, db_filename: &str) -> anyhow::Resu let mut file = File::create("enter_db.sh").await?; let _ = file.write(b"#!/bin/bash\n").await?; file.write_all(format!("sqlite3 {}/{}", out_dir, db_filename).as_bytes()) + .await?; + + file.set_permissions(Permissions::from_mode(0o755)) .await .map_err(From::from) } diff --git a/nym-node-status-api/launch_node_status_api.sh b/nym-node-status-api/launch_node_status_api.sh index 1db6bcc1b2b..f9ebd364a9b 100755 --- a/nym-node-status-api/launch_node_status_api.sh +++ b/nym-node-status-api/launch_node_status_api.sh @@ -6,13 +6,34 @@ export RUST_LOG=${RUST_LOG:-debug} export NYM_API_CLIENT_TIMEOUT=60 export EXPLORER_CLIENT_TIMEOUT=60 -#export NYXD=https://rpc.nymtech.net -#export NYM_API=https://validator.nymtech.net/api/ -#export EXPLORER_API=https://explorer.nymtech.net/api/ -#export NETWORK_NAME=mainnet -#cargo run --package nym-node-status-api --release -- --connection-url "sqlite://node-status-api.sqlite?mode=rwc" +export ENVIRONMENT="mainnet.env" -cd .. -docker build -t node-status-api -f nym-node-status-api/Dockerfile . -docker run --env-file envs/mainnet.env -e NYM_NODE_STATUS_API_CONNECTION_URL="sqlite://node-status-api.sqlite?mode=rwc" node-status-api +function run_bare() { + # export necessary env vars + set -a + source ../envs/$ENVIRONMENT + set +a + export RUST_LOG=debug + + # --conection-url is provided in build.rs + cargo run --package nym-node-status-api +} + +function run_docker() { + cargo build --package nym-node-status-api --release + cp ../target/release/nym-node-status-api . + + cd .. + docker build -t node-status-api -f nym-node-status-api/Dockerfile.dev . + docker run --env-file envs/${ENVIRONMENT} \ + -e EXPLORER_CLIENT_TIMEOUT=$EXPLORER_CLIENT_TIMEOUT \ + -e NYM_API_CLIENT_TIMEOUT=$NYM_API_CLIENT_TIMEOUT \ + -e DATABASE_URL="sqlite://node-status-api.sqlite?mode=rwc" \ + -e RUST_LOG=${RUST_LOG} node-status-api + +} + +run_bare + +# run_docker diff --git a/nym-node-status-api/migrations/000_init.sql b/nym-node-status-api/migrations/000_init.sql index 35aaa40654b..1e5683e2c94 100644 --- a/nym-node-status-api/migrations/000_init.sql +++ b/nym-node-status-api/migrations/000_init.sql @@ -98,3 +98,15 @@ CREATE TABLE FOREIGN KEY (mix_id) REFERENCES mixnodes (mix_id), UNIQUE (mix_id, date_utc) -- This constraint automatically creates an index ); + + +CREATE TABLE testruns +( + id INTEGER PRIMARY KEY AUTOINCREMENT, + gateway_id INTEGER, + status INTEGER NOT NULL, -- 0=pending, 1=in-progress, 2=complete + timestamp_utc INTEGER NOT NULL, + ip_address VARCHAR NOT NULL, + log VARCHAR NOT NULL, + FOREIGN KEY (gateway_id) REFERENCES gateways (id) +); diff --git a/nym-node-status-api/src/cli/mod.rs b/nym-node-status-api/src/cli/mod.rs index ad214b6a23a..84ee86577fb 100644 --- a/nym-node-status-api/src/cli/mod.rs +++ b/nym-node-status-api/src/cli/mod.rs @@ -41,26 +41,34 @@ pub(crate) struct Cli { pub(crate) nyxd_addr: Url, /// Nym api client timeout. - #[clap( - long, - default_value = "15", - env = "NYM_NODE_STATUS_API_NYM_API_CLIENT_TIMEOUT" - )] + #[clap(long, default_value = "15", env = "NYM_API_CLIENT_TIMEOUT")] #[arg(value_parser = parse_duration)] pub(crate) nym_api_client_timeout: Duration, /// Explorer api client timeout. + #[clap(long, default_value = "15", env = "EXPLORER_CLIENT_TIMEOUT")] + #[arg(value_parser = parse_duration)] + pub(crate) explorer_client_timeout: Duration, + + /// Connection url for the database. + #[clap(long, env = "DATABASE_URL")] + pub(crate) database_url: String, + #[clap( long, - default_value = "15", - env = "NYM_NODE_STATUS_API_EXPLORER_CLIENT_TIMEOUT" + default_value = "600", + env = "NODE_STATUS_API_MONITOR_REFRESH_INTERVAL" )] #[arg(value_parser = parse_duration)] - pub(crate) explorer_client_timeout: Duration, + pub(crate) monitor_refresh_interval: Duration, - /// Connection url for the database. - #[clap(long, env = "NYM_NODE_STATUS_API_CONNECTION_URL")] - pub(crate) connection_url: String, + #[clap( + long, + default_value = "600", + env = "NODE_STATUS_API_TESTRUN_REFRESH_INTERVAL" + )] + #[arg(value_parser = parse_duration)] + pub(crate) testruns_refresh_interval: Duration, } fn parse_duration(arg: &str) -> Result { diff --git a/nym-node-status-api/src/db/mod.rs b/nym-node-status-api/src/db/mod.rs index 8e840252f6e..86e25e92272 100644 --- a/nym-node-status-api/src/db/mod.rs +++ b/nym-node-status-api/src/db/mod.rs @@ -1,7 +1,6 @@ -use std::str::FromStr; - use anyhow::{anyhow, Result}; use sqlx::{migrate::Migrator, sqlite::SqliteConnectOptions, ConnectOptions, SqlitePool}; +use std::str::FromStr; pub(crate) mod models; pub(crate) mod queries; @@ -30,7 +29,7 @@ impl Storage { } /// Cloning pool is cheap, it's the same underlying set of connections - pub async fn pool_owned(&self) -> DbPool { + pub fn pool_owned(&self) -> DbPool { self.pool.clone() } } diff --git a/nym-node-status-api/src/db/models.rs b/nym-node-status-api/src/db/models.rs index 54164b34e32..f704c1ed8a5 100644 --- a/nym-node-status-api/src/db/models.rs +++ b/nym-node-status-api/src/db/models.rs @@ -4,6 +4,7 @@ use crate::{ }; use nym_node_requests::api::v1::node::models::NodeDescription; use serde::{Deserialize, Serialize}; +use strum_macros::{EnumString, FromRepr}; use utoipa::ToSchema; pub(crate) struct GatewayRecord { @@ -298,3 +299,36 @@ pub(crate) mod gateway { pub(crate) last_updated_utc: String, } } + +#[derive(Debug, Clone)] +pub struct TestRunDto { + pub id: i64, + pub gateway_id: i64, + pub status: i64, + pub timestamp_utc: i64, + pub ip_address: String, + pub log: String, +} + +#[derive(Debug, Clone, strum_macros::Display, EnumString, FromRepr, PartialEq)] +#[repr(u8)] +pub(crate) enum TestRunStatus { + Complete = 2, + InProgress = 1, + Pending = 0, +} + +#[derive(Debug, Clone)] +pub struct GatewayIdentityDto { + pub gateway_identity_key: String, + pub bonded: bool, +} + +#[allow(dead_code)] // it's not dead code but clippy doesn't detect usage in sqlx macros +#[derive(Debug, Clone)] +pub struct GatewayInfoDto { + pub id: i64, + pub gateway_identity_key: String, + pub self_described: Option, + pub explorer_pretty_bond: Option, +} diff --git a/nym-node-status-api/src/db/queries/mod.rs b/nym-node-status-api/src/db/queries/mod.rs index 279d31dc342..349e62e7c7a 100644 --- a/nym-node-status-api/src/db/queries/mod.rs +++ b/nym-node-status-api/src/db/queries/mod.rs @@ -2,6 +2,7 @@ mod gateways; mod misc; mod mixnodes; mod summary; +pub(crate) mod testruns; pub(crate) use gateways::{ ensure_gateways_still_bonded, get_all_gateways, insert_gateways, diff --git a/nym-node-status-api/src/db/queries/testruns.rs b/nym-node-status-api/src/db/queries/testruns.rs new file mode 100644 index 00000000000..cf5b8a3bbf1 --- /dev/null +++ b/nym-node-status-api/src/db/queries/testruns.rs @@ -0,0 +1,126 @@ +use crate::http::models::TestrunAssignment; +use crate::{ + db::models::{TestRunDto, TestRunStatus}, + testruns::now_utc, +}; +use anyhow::Context; +use sqlx::{pool::PoolConnection, Sqlite}; + +pub(crate) async fn get_testrun_by_id( + conn: &mut PoolConnection, + testrun_id: i64, +) -> anyhow::Result { + sqlx::query_as!( + TestRunDto, + r#"SELECT + id as "id!", + gateway_id as "gateway_id!", + status as "status!", + timestamp_utc as "timestamp_utc!", + ip_address as "ip_address!", + log as "log!" + FROM testruns + WHERE id = ? + ORDER BY timestamp_utc"#, + testrun_id + ) + .fetch_one(conn.as_mut()) + .await + .context(format!("Couldn't retrieve testrun {testrun_id}")) +} + +pub(crate) async fn get_oldest_testrun_and_make_it_pending( + // TODO dz accept mut reference, repeat in all similar functions + conn: PoolConnection, +) -> anyhow::Result> { + let mut conn = conn; + let assignment = sqlx::query_as!( + TestrunAssignment, + r#"UPDATE testruns + SET status = ? + WHERE rowid = + ( + SELECT rowid + FROM testruns + WHERE status = ? + ORDER BY timestamp_utc asc + LIMIT 1 + ) + RETURNING + id as "testrun_id!", + gateway_id as "gateway_pk_id!" + "#, + TestRunStatus::InProgress as i64, + TestRunStatus::Pending as i64, + ) + .fetch_optional(&mut *conn) + .await?; + + Ok(assignment) +} + +pub(crate) async fn update_testrun_status( + conn: &mut PoolConnection, + testrun_id: i64, + status: TestRunStatus, +) -> anyhow::Result<()> { + let status = status as u32; + sqlx::query!( + "UPDATE testruns SET status = ? WHERE id = ?", + status, + testrun_id + ) + .execute(conn.as_mut()) + .await?; + + Ok(()) +} + +pub(crate) async fn update_gateway_last_probe_log( + conn: &mut PoolConnection, + gateway_pk: i64, + log: &str, +) -> anyhow::Result<()> { + sqlx::query!( + "UPDATE gateways SET last_probe_log = ? WHERE id = ?", + log, + gateway_pk + ) + .execute(conn.as_mut()) + .await + .map(drop) + .map_err(From::from) +} + +pub(crate) async fn update_gateway_last_probe_result( + conn: &mut PoolConnection, + gateway_pk: i64, + result: &str, +) -> anyhow::Result<()> { + sqlx::query!( + "UPDATE gateways SET last_probe_result = ? WHERE id = ?", + result, + gateway_pk + ) + .execute(conn.as_mut()) + .await + .map(drop) + .map_err(From::from) +} + +pub(crate) async fn update_gateway_score( + conn: &mut PoolConnection, + gateway_pk: i64, +) -> anyhow::Result<()> { + let now = now_utc().timestamp(); + sqlx::query!( + "UPDATE gateways SET last_testrun_utc = ?, last_updated_utc = ? WHERE id = ?", + now, + now, + gateway_pk + ) + .execute(conn.as_mut()) + .await + .map(drop) + .map_err(From::from) +} diff --git a/nym-node-status-api/src/http/api/gateways.rs b/nym-node-status-api/src/http/api/gateways.rs index 9dec3134e48..c1f40737671 100644 --- a/nym-node-status-api/src/http/api/gateways.rs +++ b/nym-node-status-api/src/http/api/gateways.rs @@ -16,7 +16,7 @@ pub(crate) fn routes() -> Router { Router::new() .route("/", axum::routing::get(gateways)) .route("/skinny", axum::routing::get(gateways_skinny)) - .route("/skinny/:identity_key", axum::routing::get(get_gateway)) + .route("/:identity_key", axum::routing::get(get_gateway)) } #[utoipa::path( diff --git a/nym-node-status-api/src/http/api/mod.rs b/nym-node-status-api/src/http/api/mod.rs index 4d385904b55..ed24fa80f5a 100644 --- a/nym-node-status-api/src/http/api/mod.rs +++ b/nym-node-status-api/src/http/api/mod.rs @@ -35,7 +35,10 @@ impl RouterBuilder { .nest("/mixnodes", mixnodes::routes()) .nest("/services", services::routes()) .nest("/summary", summary::routes()), - // .nest("/testruns", testruns::_routes()), + ) + .nest( + "/internal", + Router::new().nest("/testruns", testruns::routes()), ); Self { diff --git a/nym-node-status-api/src/http/api/testruns.rs b/nym-node-status-api/src/http/api/testruns.rs index 875012b0fbb..4bbc4d6295b 100644 --- a/nym-node-status-api/src/http/api/testruns.rs +++ b/nym-node-status-api/src/http/api/testruns.rs @@ -1,7 +1,116 @@ -use axum::Router; +use axum::Json; +use axum::{ + extract::{Path, State}, + Router, +}; +use reqwest::StatusCode; -use crate::http::state::AppState; +use crate::db::models::TestRunStatus; +use crate::db::queries; +use crate::{ + db, + http::{ + error::{HttpError, HttpResult}, + models::TestrunAssignment, + state::AppState, + }, +}; -pub(crate) fn _routes() -> Router { - unimplemented!() +// TODO dz consider adding endpoint to trigger testrun scan for a given gateway_id +// like in H< src/http/testruns.rs + +pub(crate) fn routes() -> Router { + Router::new() + .route("/", axum::routing::get(request_testrun)) + .route("/:testrun_id", axum::routing::post(submit_testrun)) +} + +#[tracing::instrument(level = "debug", skip_all)] +async fn request_testrun(State(state): State) -> HttpResult> { + // TODO dz log agent's key + // TODO dz log agent's network probe version + tracing::debug!("Agent X requested testrun"); + + let db = state.db_pool(); + let conn = db + .acquire() + .await + .map_err(HttpError::internal_with_logging)?; + + return match db::queries::testruns::get_oldest_testrun_and_make_it_pending(conn).await { + Ok(res) => { + if let Some(testrun) = res { + // TODO dz consider adding a column to testruns table with agent's public key + tracing::debug!( + "🏃‍ Assigned testrun row_id {} to agent X", + &testrun.testrun_id + ); + Ok(Json(testrun)) + } else { + Err(HttpError::not_found("No testruns available")) + } + } + Err(err) => Err(HttpError::internal_with_logging(err)), + }; +} + +// TODO dz accept testrun_id as query parameter +#[tracing::instrument(level = "debug", skip_all)] +async fn submit_testrun( + Path(testrun_id): Path, + State(state): State, + body: String, +) -> HttpResult { + tracing::debug!( + "Agent submitted testrun {}. Total length: {}", + testrun_id, + body.len(), + ); + // TODO dz store testrun results + + let db = state.db_pool(); + let mut conn = db + .acquire() + .await + .map_err(HttpError::internal_with_logging)?; + + let testrun = queries::testruns::get_testrun_by_id(&mut conn, testrun_id) + .await + .map_err(|e| { + tracing::error!("{e}"); + HttpError::not_found(testrun_id) + })?; + // TODO dz this should be part of a single transaction: commit after everything is done + queries::testruns::update_testrun_status(&mut conn, testrun_id, TestRunStatus::Complete) + .await + .map_err(HttpError::internal_with_logging)?; + queries::testruns::update_gateway_last_probe_log(&mut conn, testrun.gateway_id, &body) + .await + .map_err(HttpError::internal_with_logging)?; + let result = get_result_from_log(&body); + queries::testruns::update_gateway_last_probe_result(&mut conn, testrun.gateway_id, &result) + .await + .map_err(HttpError::internal_with_logging)?; + queries::testruns::update_gateway_score(&mut conn, testrun.gateway_id) + .await + .map_err(HttpError::internal_with_logging)?; + // TODO dz log gw identity key + + tracing::info!( + "✅ Testrun row_id {} for gateway {} complete", + testrun.id, + testrun.gateway_id + ); + + Ok(StatusCode::CREATED) +} + +fn get_result_from_log(log: &str) -> String { + let re = regex::Regex::new(r"\n\{\s").unwrap(); + let result: Vec<_> = re.splitn(log, 2).collect(); + if result.len() == 2 { + let res = format!("{} {}", "{", result[1]).to_string(); + return res; + } + "".to_string() } diff --git a/nym-node-status-api/src/http/api_docs.rs b/nym-node-status-api/src/http/api_docs.rs index 172aa899a39..9ac5238fc4d 100644 --- a/nym-node-status-api/src/http/api_docs.rs +++ b/nym-node-status-api/src/http/api_docs.rs @@ -8,7 +8,7 @@ use utoipauto::utoipauto; #[utoipauto(paths = "./nym-node-status-api/src")] #[derive(OpenApi)] #[openapi( - info(title = "Nym API"), + info(title = "Node Status API"), tags(), components(schemas(nym_node_requests::api::v1::node::models::NodeDescription,)) )] diff --git a/nym-node-status-api/src/http/error.rs b/nym-node-status-api/src/http/error.rs index 8bbd59e0959..81a61e2d3e8 100644 --- a/nym-node-status-api/src/http/error.rs +++ b/nym-node-status-api/src/http/error.rs @@ -1,3 +1,5 @@ +use std::fmt::Display; + pub(crate) type HttpResult = Result; pub(crate) struct HttpError { @@ -13,12 +15,24 @@ impl HttpError { } } + pub(crate) fn internal_with_logging(msg: impl Display) -> Self { + tracing::error!("{}", msg.to_string()); + Self::internal() + } + pub(crate) fn internal() -> Self { Self { message: serde_json::json!({"message": "Internal server error"}).to_string(), status: axum::http::StatusCode::INTERNAL_SERVER_ERROR, } } + + pub(crate) fn not_found(msg: impl Display) -> Self { + Self { + message: serde_json::json!({"message": msg.to_string()}).to_string(), + status: axum::http::StatusCode::NOT_FOUND, + } + } } impl axum::response::IntoResponse for HttpError { diff --git a/nym-node-status-api/src/http/models.rs b/nym-node-status-api/src/http/models.rs index 3a4c348b25b..315bf085dc2 100644 --- a/nym-node-status-api/src/http/models.rs +++ b/nym-node-status-api/src/http/models.rs @@ -1,7 +1,10 @@ +use crate::db::models::TestRunDto; use nym_node_requests::api::v1::node::models::NodeDescription; use serde::{Deserialize, Serialize}; use utoipa::ToSchema; +pub(crate) use nym_common_models::ns_api::TestrunAssignment; + #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] pub struct Gateway { pub gateway_identity_key: String, @@ -72,3 +75,12 @@ pub(crate) struct SummaryHistory { pub value_json: serde_json::Value, pub timestamp_utc: String, } + +impl From for TestrunAssignment { + fn from(value: TestRunDto) -> Self { + Self { + gateway_pk_id: value.gateway_id, + testrun_id: value.id, + } + } +} diff --git a/nym-node-status-api/src/http/server.rs b/nym-node-status-api/src/http/server.rs index 694f5fa79ab..17d5d64ab01 100644 --- a/nym-node-status-api/src/http/server.rs +++ b/nym-node-status-api/src/http/server.rs @@ -22,6 +22,7 @@ pub(crate) async fn start_http_api( // TODO dz do we need this to be configurable? let bind_addr = format!("0.0.0.0:{}", http_port); + tracing::info!("Binding server to {bind_addr}"); let server = router.build_server(bind_addr).await?; Ok(start_server(server)) diff --git a/nym-node-status-api/src/logging.rs b/nym-node-status-api/src/logging.rs index 01dd31562e4..6bdad3bae57 100644 --- a/nym-node-status-api/src/logging.rs +++ b/nym-node-status-api/src/logging.rs @@ -1,12 +1,10 @@ use tracing::level_filters::LevelFilter; use tracing_subscriber::{filter::Directive, EnvFilter}; -pub(crate) fn setup_tracing_logger() { - fn directive_checked(directive: impl Into) -> Directive { - directive - .into() - .parse() - .expect("Failed to parse log directive") +// TODO dz you can get the tracing-subscriber via basic-tracing feature on nym-bin-common +pub(crate) fn setup_tracing_logger() -> anyhow::Result<()> { + fn directive_checked(directive: impl Into) -> anyhow::Result { + directive.into().parse().map_err(From::from) } let log_builder = tracing_subscriber::fmt() @@ -22,10 +20,11 @@ pub(crate) fn setup_tracing_logger() { let mut filter = EnvFilter::builder() // if RUST_LOG isn't set, set default level - .with_default_directive(LevelFilter::INFO.into()) + .with_default_directive(LevelFilter::DEBUG.into()) .from_env_lossy(); + // these crates are more granularly filtered - let filter_crates = [ + let warn_crates = [ "reqwest", "rustls", "hyper", @@ -35,14 +34,36 @@ pub(crate) fn setup_tracing_logger() { "tower_http", "axum", ]; - for crate_name in filter_crates { - filter = filter.add_directive(directive_checked(format!("{}=warn", crate_name))); + for crate_name in warn_crates { + filter = filter.add_directive(directive_checked(format!("{}=warn", crate_name))?); } - filter = filter.add_directive(directive_checked("nym_bin_common=debug")); - filter = filter.add_directive(directive_checked("nym_explorer_client=debug")); - filter = filter.add_directive(directive_checked("nym_network_defaults=debug")); - filter = filter.add_directive(directive_checked("nym_validator_client=debug")); + let log_level_hint = filter.max_level_hint(); + + // debug or higher granularity (e.g. trace) + let debug_or_higher = std::cmp::max( + log_level_hint.unwrap_or(LevelFilter::DEBUG), + LevelFilter::DEBUG, + ); + filter = filter.add_directive(directive_checked(format!( + "nym_bin_common={}", + debug_or_higher + ))?); + filter = filter.add_directive(directive_checked(format!( + "nym_explorer_client={}", + debug_or_higher + ))?); + filter = filter.add_directive(directive_checked(format!( + "nym_network_defaults={}", + debug_or_higher + ))?); + filter = filter.add_directive(directive_checked(format!( + "nym_validator_client={}", + debug_or_higher + ))?); log_builder.with_env_filter(filter).init(); + tracing::info!("Log level: {:?}", log_level_hint); + + Ok(()) } diff --git a/nym-node-status-api/src/main.rs b/nym-node-status-api/src/main.rs index a15cfe68a4c..b2c68b391bb 100644 --- a/nym-node-status-api/src/main.rs +++ b/nym-node-status-api/src/main.rs @@ -6,26 +6,35 @@ mod db; mod http; mod logging; mod monitor; +mod testruns; #[tokio::main] async fn main() -> anyhow::Result<()> { - logging::setup_tracing_logger(); + logging::setup_tracing_logger()?; let args = cli::Cli::parse(); - let connection_url = args.connection_url.clone(); + let connection_url = args.database_url.clone(); tracing::debug!("Using config:\n{:#?}", args); let storage = db::Storage::init(connection_url).await?; - let db_pool = storage.pool_owned().await; + let db_pool = storage.pool_owned(); let args_clone = args.clone(); tokio::spawn(async move { - monitor::spawn_in_background(db_pool, args_clone).await; + monitor::spawn_in_background( + db_pool, + args_clone.explorer_client_timeout, + args_clone.nym_api_client_timeout, + &args_clone.nyxd_addr, + args_clone.monitor_refresh_interval, + ) + .await; + tracing::info!("Started monitor task"); }); - tracing::info!("Started monitor task"); + testruns::spawn(storage.pool_owned(), args.testruns_refresh_interval).await; let shutdown_handles = http::server::start_http_api( - storage.pool_owned().await, + storage.pool_owned(), args.http_port, args.nym_http_cache_ttl, ) diff --git a/nym-node-status-api/src/monitor/mod.rs b/nym-node-status-api/src/monitor/mod.rs index 3c5172c8a9f..680a9ce312a 100644 --- a/nym-node-status-api/src/monitor/mod.rs +++ b/nym-node-status-api/src/monitor/mod.rs @@ -1,4 +1,3 @@ -use crate::cli::Cli; use crate::db::models::{ gateway, mixnode, GatewayRecord, MixnodeRecord, NetworkSummary, GATEWAYS_BLACKLISTED_COUNT, GATEWAYS_BONDED_COUNT, GATEWAYS_EXPLORER_COUNT, GATEWAYS_HISTORICAL_COUNT, @@ -19,34 +18,50 @@ use nym_validator_client::NymApiClient; use reqwest::Url; use std::collections::HashSet; use std::str::FromStr; -use tokio::task::JoinHandle; use tokio::time::Duration; +use tracing::instrument; -const REFRESH_DELAY: Duration = Duration::from_secs(60 * 5); -const FAILURE_RETRY_DELAY: Duration = Duration::from_secs(15); +// TODO dz should be configurable +const FAILURE_RETRY_DELAY: Duration = Duration::from_secs(60); static DELEGATION_PROGRAM_WALLET: &str = "n1rnxpdpx3kldygsklfft0gech7fhfcux4zst5lw"; // TODO dz: query many NYM APIs: // multiple instances running directory cache, ask sachin -pub(crate) async fn spawn_in_background(db_pool: DbPool, config: Cli) -> JoinHandle<()> { +#[instrument(level = "debug", name = "data_monitor", skip_all)] +pub(crate) async fn spawn_in_background( + db_pool: DbPool, + explorer_client_timeout: Duration, + nym_api_client_timeout: Duration, + nyxd_addr: &Url, + refresh_interval: Duration, +) { let network_defaults = nym_network_defaults::NymNetworkDetails::new_from_env(); loop { tracing::info!("Refreshing node info..."); - if let Err(e) = run(&db_pool, &network_defaults, &config).await { + if let Err(e) = run( + &db_pool, + &network_defaults, + explorer_client_timeout, + nym_api_client_timeout, + nyxd_addr, + ) + .await + { tracing::error!( "Monitor run failed: {e}, retrying in {}s...", FAILURE_RETRY_DELAY.as_secs() ); + // TODO dz implement some sort of backoff tokio::time::sleep(FAILURE_RETRY_DELAY).await; } else { tracing::info!( "Info successfully collected, sleeping for {}s...", - REFRESH_DELAY.as_secs() + refresh_interval.as_secs() ); - tokio::time::sleep(REFRESH_DELAY).await; + tokio::time::sleep(refresh_interval).await; } } } @@ -54,7 +69,9 @@ pub(crate) async fn spawn_in_background(db_pool: DbPool, config: Cli) -> JoinHan async fn run( pool: &DbPool, network_details: &NymNetworkDetails, - config: &Cli, + explorer_client_timeout: Duration, + nym_api_client_timeout: Duration, + nyxd_addr: &Url, ) -> anyhow::Result<()> { let default_api_url = network_details .endpoints @@ -68,19 +85,17 @@ async fn run( .expect("rust sdk mainnet default explorer url not parseable") }); + // TODO dz replace explorer api with ipinfo.io let default_explorer_url = default_explorer_url.expect("explorer url missing in network config"); let explorer_client = - ExplorerClient::new_with_timeout(default_explorer_url, config.explorer_client_timeout)?; + ExplorerClient::new_with_timeout(default_explorer_url, explorer_client_timeout)?; let explorer_gateways = explorer_client .get_gateways() .await .log_error("get_gateways")?; - tracing::debug!("6"); - let api_client = - // TODO dz introduce timeout ? - NymApiClient::new(default_api_url); + let api_client = NymApiClient::new_with_timeout(default_api_url, nym_api_client_timeout); let gateways = api_client .get_cached_described_gateways() .await @@ -97,14 +112,16 @@ async fn run( .log_error("get_cached_mixnodes")?; tracing::debug!("Fetched {} mixnodes", mixnodes.len()); - // TODO dz can we calculate blacklisted GWs from their performance? - // where do we get their performance? - let gateways_blacklisted = api_client - .nym_api - .get_gateways_blacklisted() - .await - .map(|vec| vec.into_iter().collect::>()) - .log_error("get_gateways_blacklisted")?; + let gateways_blacklisted = skimmed_gateways + .iter() + .filter_map(|gw| { + if gw.performance.round_to_integer() <= 50 { + Some(gw.ed25519_identity_pubkey.to_owned()) + } else { + None + } + }) + .collect::>(); // Cached mixnodes don't include blacklisted nodes // We need that to calculate the total locked tokens later @@ -124,7 +141,7 @@ async fn run( .await .log_error("get_active_mixnodes")?; let delegation_program_members = - get_delegation_program_details(network_details, &config.nyxd_addr).await?; + get_delegation_program_details(network_details, nyxd_addr).await?; // keep stats for later let count_bonded_mixnodes = mixnodes.len(); diff --git a/nym-node-status-api/src/testruns/mod.rs b/nym-node-status-api/src/testruns/mod.rs new file mode 100644 index 00000000000..86b36cf48ef --- /dev/null +++ b/nym-node-status-api/src/testruns/mod.rs @@ -0,0 +1,76 @@ +use crate::db::models::GatewayIdentityDto; +use crate::db::DbPool; +use futures_util::TryStreamExt; +use std::time::Duration; +use tracing::instrument; + +pub(crate) mod models; +mod queue; +pub(crate) use queue::now_utc; + +pub(crate) async fn spawn(pool: DbPool, refresh_interval: Duration) { + tokio::spawn(async move { + loop { + tracing::info!("Spawning testruns..."); + + if let Err(e) = run(&pool).await { + tracing::error!("Cron job failed: {}", e); + } + tracing::debug!("Sleeping for {}s...", refresh_interval.as_secs()); + tokio::time::sleep(refresh_interval).await; + } + }); +} + +// TODO dz make number of max agents configurable + +// TODO dz periodically clean up stale pending testruns +#[instrument(level = "debug", name = "testrun_queue", skip_all)] +async fn run(pool: &DbPool) -> anyhow::Result<()> { + if pool.is_closed() { + tracing::debug!("DB pool closed, returning early"); + return Ok(()); + } + + let mut conn = pool.acquire().await?; + + let gateways = sqlx::query_as!( + GatewayIdentityDto, + r#"SELECT + gateway_identity_key as "gateway_identity_key!", + bonded as "bonded: bool" + FROM gateways + ORDER BY last_testrun_utc"#, + ) + .fetch(&mut *conn) + .try_collect::>() + .await?; + + // TODO dz this filtering could be done in SQL + let gateways: Vec = gateways.into_iter().filter(|g| g.bonded).collect(); + + tracing::debug!("Trying to queue {} testruns", gateways.len()); + let mut testruns_created = 0; + for gateway in gateways { + if let Err(e) = queue::try_queue_testrun( + &mut conn, + gateway.gateway_identity_key.clone(), + // TODO dz read from config + "127.0.0.1".to_string(), + ) + .await + // TODO dz measure how many were actually inserted and how many were skipped + { + tracing::debug!( + "Skipping test for identity {} with error {}", + &gateway.gateway_identity_key, + e + ); + } else { + testruns_created += 1; + } + } + tracing::debug!("{} testruns queued in total", testruns_created); + + Ok(()) +} diff --git a/nym-node-status-api/src/testruns/models.rs b/nym-node-status-api/src/testruns/models.rs new file mode 100644 index 00000000000..fe4b33384c9 --- /dev/null +++ b/nym-node-status-api/src/testruns/models.rs @@ -0,0 +1,16 @@ +use serde::{Deserialize, Serialize}; + +#[allow(dead_code)] // it's not dead code but clippy doesn't detect usage in sqlx macros +#[derive(Debug, Clone)] +pub struct GatewayIdentityDto { + pub gateway_identity_key: String, + pub bonded: bool, +} + +#[derive(Debug, Clone, Deserialize, Serialize, utoipa::ToSchema)] +pub struct TestRun { + pub id: u32, + pub identity_key: String, + pub status: String, + pub log: String, +} diff --git a/nym-node-status-api/src/testruns/queue.rs b/nym-node-status-api/src/testruns/queue.rs new file mode 100644 index 00000000000..aa0ce32fa22 --- /dev/null +++ b/nym-node-status-api/src/testruns/queue.rs @@ -0,0 +1,118 @@ +use crate::db::models::{GatewayInfoDto, TestRunDto, TestRunStatus}; +use crate::testruns::models::TestRun; +use anyhow::anyhow; +use chrono::DateTime; +use futures_util::TryStreamExt; +use sqlx::pool::PoolConnection; +use sqlx::Sqlite; +use std::time::SystemTime; + +pub(crate) async fn try_queue_testrun( + conn: &mut PoolConnection, + identity_key: String, + ip_address: String, +) -> anyhow::Result { + let timestamp = now_utc().timestamp(); + let timestamp_pretty = now_utc_as_rfc3339(); + + let items = sqlx::query_as!( + GatewayInfoDto, + r#"SELECT + id as "id!", + gateway_identity_key as "gateway_identity_key!", + self_described as "self_described?", + explorer_pretty_bond as "explorer_pretty_bond?" + FROM gateways + WHERE gateway_identity_key = ? + ORDER BY gateway_identity_key + LIMIT 1"#, + identity_key, + ) + // TODO dz shoudl call .fetch_one + // TODO dz replace this in other queries as well + .fetch(conn.as_mut()) + .try_collect::>() + .await?; + + let gateway = items + .iter() + .find(|g| g.gateway_identity_key == identity_key); + + // TODO dz if let Some() = gateway.first() ... + if gateway.is_none() { + return Err(anyhow!("Unknown gateway {identity_key}")); + } + let gateway_id = gateway.unwrap().id; + + // + // check if there is already a test run for this gateway + // + let items = sqlx::query_as!( + TestRunDto, + r#"SELECT + id as "id!", + gateway_id as "gateway_id!", + status as "status!", + timestamp_utc as "timestamp_utc!", + ip_address as "ip_address!", + log as "log!" + FROM testruns + WHERE gateway_id = ? AND status != 2 + ORDER BY id DESC + LIMIT 1"#, + gateway_id, + ) + .fetch(conn.as_mut()) + .try_collect::>() + .await?; + + if !items.is_empty() { + let testrun = items.first().unwrap(); + return Ok(TestRun { + id: testrun.id as u32, + identity_key, + status: format!( + "{}", + TestRunStatus::from_repr(testrun.status as u8).unwrap() + ), + log: testrun.log.clone(), + }); + } + + // + // save test run + // + let status = TestRunStatus::Pending as u32; + let log = format!( + "Test for {identity_key} requested at {} UTC\n\n", + timestamp_pretty + ); + + let id = sqlx::query!( + "INSERT INTO testruns (gateway_id, status, ip_address, timestamp_utc, log) VALUES (?, ?, ?, ?, ?)", + gateway_id, + status, + ip_address, + timestamp, + log, + ) + .execute(conn.as_mut()) + .await? + .last_insert_rowid(); + + Ok(TestRun { + id: id as u32, + identity_key, + status: format!("{}", TestRunStatus::Pending), + log, + }) +} + +// TODO dz do we need these? +pub fn now_utc() -> DateTime { + SystemTime::now().into() +} + +pub fn now_utc_as_rfc3339() -> String { + now_utc().to_rfc3339() +} From 9583a5c6c88bf74248b213c0285102d3f7af10e2 Mon Sep 17 00:00:00 2001 From: dynco-nym <173912580+dynco-nym@users.noreply.github.com> Date: Tue, 29 Oct 2024 00:24:18 +0100 Subject: [PATCH 6/6] Fix build script --- nym-node-status-api/build.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nym-node-status-api/build.rs b/nym-node-status-api/build.rs index 7bc42cb03f0..bfe6e54e740 100644 --- a/nym-node-status-api/build.rs +++ b/nym-node-status-api/build.rs @@ -8,7 +8,7 @@ const SQLITE_DB_FILENAME: &str = "nym-node-status-api.sqlite"; /// If you need to re-run migrations or reset the db, just run /// cargo clean -p nym-node-status-api -#[tokio::main] +#[tokio::main(flavor = "current_thread")] async fn main() -> Result<()> { let out_dir = read_env_var("OUT_DIR")?; let database_path = format!("sqlite://{}/{}?mode=rwc", out_dir, SQLITE_DB_FILENAME);