From ac56717b2327d1e9920b78e52829980f6372665d Mon Sep 17 00:00:00 2001 From: benedetta davico <46782255+benedettadavico@users.noreply.github.com> Date: Mon, 2 Dec 2024 13:48:05 +0100 Subject: [PATCH 01/18] Update ci-build-upload-binaries.yml --- .github/workflows/ci-build-upload-binaries.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-build-upload-binaries.yml b/.github/workflows/ci-build-upload-binaries.yml index 1da94877fa4..f0f6e851216 100644 --- a/.github/workflows/ci-build-upload-binaries.yml +++ b/.github/workflows/ci-build-upload-binaries.yml @@ -21,7 +21,7 @@ jobs: strategy: fail-fast: false matrix: - platform: [ arc-ubuntu-20.04 ] + platform: [ ubuntu-20.04 ] runs-on: ${{ matrix.platform }} env: From 645be5fa22c414af1471e4892e24d6e11bd4f1c7 Mon Sep 17 00:00:00 2001 From: benedetta davico <46782255+benedettadavico@users.noreply.github.com> Date: Mon, 2 Dec 2024 14:03:44 +0100 Subject: [PATCH 02/18] Update ci-build-upload-binaries.yml --- .github/workflows/ci-build-upload-binaries.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-build-upload-binaries.yml b/.github/workflows/ci-build-upload-binaries.yml index f0f6e851216..1da94877fa4 100644 --- a/.github/workflows/ci-build-upload-binaries.yml +++ b/.github/workflows/ci-build-upload-binaries.yml @@ -21,7 +21,7 @@ jobs: strategy: fail-fast: false matrix: - platform: [ ubuntu-20.04 ] + platform: [ arc-ubuntu-20.04 ] runs-on: ${{ matrix.platform }} env: From feefde9022dbd17fc16d2a3bb09121a948a8bc74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Mon, 2 Dec 2024 14:52:35 +0000 Subject: [PATCH 03/18] Bugfix/credential proxy sequencing (#5187) * using common middleware for all http servers * improved span handling in credential-proxy * ensure increase in sequence number upon making deposit * added explicit connect options for the db * fixed further instances of incorrect span instrumentation * batch deposit requests together to improve concurrency * ignore cancelled requests * updated credential proxy version to 0.1.4 * adjusted Dockerfile with new binary location * log binary version on startup * reduce default log level * guard against unavaiable commit sha * apply review comments: dont exit(0), instead just shutdown normally * add skip_webhook parameter to obtain-async * removing dead code --- Cargo.lock | 6 +- .../bandwidth-controller/src/acquire/mod.rs | 2 +- .../src/nyxd/cosmwasm_client/helpers.rs | 88 ++++++-- .../src/nyxd/cosmwasm_client/mod.rs | 2 +- .../src/ecash/credential_sender.rs | 2 +- common/http-api-common/Cargo.toml | 3 + common/http-api-common/src/lib.rs | 2 +- .../src/middleware/bearer_auth.rs | 2 +- .../src/{ => middleware}/logging.rs | 3 +- common/http-api-common/src/middleware/mod.rs | 5 + nym-api/src/support/http/router.rs | 2 +- .../nym-credential-proxy-requests/Cargo.toml | 1 - .../src/api/v1/ticketbook/models.rs | 3 + .../nym-credential-proxy/Cargo.toml | 3 +- .../nym-credential-proxy/Dockerfile | 2 +- .../nym-credential-proxy/src/cli.rs | 9 + .../src/credentials/ticketbook/mod.rs | 67 ++++-- .../nym-credential-proxy/src/deposit_maker.rs | 205 ++++++++++++++++++ .../nym-credential-proxy/src/error.rs | 3 + .../src/http/middleware/logging.rs | 64 ------ .../src/http/middleware/mod.rs | 5 - .../nym-credential-proxy/src/http/mod.rs | 10 +- .../src/http/router/api/mod.rs | 2 +- .../src/http/router/api/v1/mod.rs | 4 +- .../src/http/router/api/v1/ticketbook/mod.rs | 197 +++++++++-------- .../http/router/api/v1/ticketbook/shares.rs | 160 +++++++------- .../src/http/router/mod.rs | 4 +- .../src/http/state/mod.rs | 157 +++++++++++--- .../nym-credential-proxy/src/main.rs | 62 +++++- .../nym-credential-proxy/src/storage/mod.rs | 12 +- .../nym-credential-proxy/src/tasks.rs | 5 +- .../nym-credential-proxy/src/webhook.rs | 43 ++-- nym-node/nym-node-http-api/src/lib.rs | 1 - .../src/middleware/logging.rs | 51 ----- .../nym-node-http-api/src/middleware/mod.rs | 4 - nym-node/nym-node-http-api/src/router/mod.rs | 2 +- 36 files changed, 764 insertions(+), 429 deletions(-) rename nym-credential-proxy/nym-credential-proxy/src/http/middleware/auth.rs => common/http-api-common/src/middleware/bearer_auth.rs (98%) rename common/http-api-common/src/{ => middleware}/logging.rs (96%) create mode 100644 common/http-api-common/src/middleware/mod.rs create mode 100644 nym-credential-proxy/nym-credential-proxy/src/deposit_maker.rs delete mode 100644 nym-credential-proxy/nym-credential-proxy/src/http/middleware/logging.rs delete mode 100644 nym-credential-proxy/nym-credential-proxy/src/http/middleware/mod.rs delete mode 100644 nym-node/nym-node-http-api/src/middleware/logging.rs delete mode 100644 nym-node/nym-node-http-api/src/middleware/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 5c88232caae..0a16ff38eba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5093,7 +5093,7 @@ dependencies = [ [[package]] name = "nym-credential-proxy" -version = "0.1.3" +version = "0.1.6" dependencies = [ "anyhow", "async-trait", @@ -5113,6 +5113,7 @@ dependencies = [ "nym-credentials", "nym-credentials-interface", "nym-crypto", + "nym-ecash-contract-common", "nym-http-api-common", "nym-network-defaults", "nym-validator-client", @@ -5620,12 +5621,15 @@ dependencies = [ "axum-client-ip", "bytes", "colored", + "futures", "mime", "serde", "serde_json", "serde_yaml", + "tower 0.4.13", "tracing", "utoipa", + "zeroize", ] [[package]] diff --git a/common/bandwidth-controller/src/acquire/mod.rs b/common/bandwidth-controller/src/acquire/mod.rs index e2d18d55458..450c9e338ea 100644 --- a/common/bandwidth-controller/src/acquire/mod.rs +++ b/common/bandwidth-controller/src/acquire/mod.rs @@ -17,7 +17,7 @@ use nym_validator_client::coconut::all_ecash_api_clients; use nym_validator_client::nym_api::EpochId; use nym_validator_client::nyxd::contract_traits::EcashSigningClient; use nym_validator_client::nyxd::contract_traits::{DkgQueryClient, EcashQueryClient}; -use nym_validator_client::nyxd::cosmwasm_client::ToSingletonContractData; +use nym_validator_client::nyxd::cosmwasm_client::ContractResponseData; use nym_validator_client::EcashApiClient; use rand::rngs::OsRng; 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 559ad434a23..0c4c6c8dcb3 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 @@ -13,6 +13,44 @@ use tracing::error; pub use cosmrs::abci::MsgResponse; +pub fn parse_singleton_u32_from_contract_response(b: Vec) -> Result { + if b.len() != 4 { + return Err(NyxdError::MalformedResponseData { + got: b.len(), + expected: 4, + }); + } + Ok(u32::from_be_bytes([b[0], b[1], b[2], b[3]])) +} + +pub fn parse_singleton_u64_from_contract_response(b: Vec) -> Result { + if b.len() != 8 { + return Err(NyxdError::MalformedResponseData { + got: b.len(), + expected: 8, + }); + } + Ok(u64::from_be_bytes([ + b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7], + ])) +} + +#[derive(Debug, Clone)] +pub struct ParsedContractResponse { + pub message_index: usize, + pub response: Vec, +} + +impl ParsedContractResponse { + pub fn parse_singleton_u32_contract_data(self) -> Result { + parse_singleton_u32_from_contract_response(self.response) + } + + pub fn parse_singleton_u64_contract_data(self) -> Result { + parse_singleton_u64_from_contract_response(self.response) + } +} + pub fn parse_msg_responses(data: Bytes) -> Vec { // it seems that currently, on wasmd 0.43 + tendermint-rs 0.37 + cosmrs 0.17.0-pre // the data is left in undecoded base64 form, but I'd imagine this might change so if the decoding fails, @@ -34,35 +72,25 @@ pub fn parse_msg_responses(data: Bytes) -> Vec { } // requires there's a single response message -pub trait ToSingletonContractData: Sized { +pub trait ContractResponseData: Sized { fn parse_singleton_u32_contract_data(&self) -> Result { let b = self.to_singleton_contract_data()?; - if b.len() != 4 { - return Err(NyxdError::MalformedResponseData { - got: b.len(), - expected: 4, - }); - } - Ok(u32::from_be_bytes([b[0], b[1], b[2], b[3]])) + parse_singleton_u32_from_contract_response(b) } fn parse_singleton_u64_contract_data(&self) -> Result { let b = self.to_singleton_contract_data()?; - if b.len() != 8 { - return Err(NyxdError::MalformedResponseData { - got: b.len(), - expected: 8, - }); - } - Ok(u64::from_be_bytes([ - b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7], - ])) + parse_singleton_u64_from_contract_response(b) } fn to_singleton_contract_data(&self) -> Result, NyxdError>; + + fn to_unchecked_contract_data(&self) -> Result>, NyxdError>; + + fn to_contract_data(&self) -> Result, NyxdError>; } -impl ToSingletonContractData for ExecuteResult { +impl ContractResponseData for ExecuteResult { fn to_singleton_contract_data(&self) -> Result, NyxdError> { if self.msg_responses.len() != 1 { return Err(NyxdError::UnexpectedNumberOfMsgResponses { @@ -72,6 +100,30 @@ impl ToSingletonContractData for ExecuteResult { self.msg_responses[0].to_contract_response_data() } + + fn to_unchecked_contract_data(&self) -> Result>, NyxdError> { + self.msg_responses + .iter() + .map(ToContractResponseData::to_contract_response_data) + .collect() + } + + fn to_contract_data(&self) -> Result, NyxdError> { + let mut response = Vec::new(); + + for (message_index, msg) in self.msg_responses.iter().enumerate() { + // unfortunately `Name` trait has not been derived for `MsgExecuteContractResponse`, + // so we have to make an explicit string comparison instead + if msg.type_url == "/cosmwasm.wasm.v1.MsgExecuteContractResponse" { + response.push(ParsedContractResponse { + message_index, + response: msg.to_contract_response_data()?, + }) + } + } + + Ok(response) + } } pub trait ToContractResponseData: Sized { diff --git a/common/client-libs/validator-client/src/nyxd/cosmwasm_client/mod.rs b/common/client-libs/validator-client/src/nyxd/cosmwasm_client/mod.rs index a0b65bfdd93..65165883396 100644 --- a/common/client-libs/validator-client/src/nyxd/cosmwasm_client/mod.rs +++ b/common/client-libs/validator-client/src/nyxd/cosmwasm_client/mod.rs @@ -23,7 +23,7 @@ use tendermint_rpc::endpoint::*; use tendermint_rpc::query::Query; use tendermint_rpc::{Error as TendermintRpcError, Order, Paging, SimpleRequest}; -pub use helpers::{ToContractResponseData, ToSingletonContractData}; +pub use helpers::{ContractResponseData, ToContractResponseData}; #[cfg(feature = "http-client")] use crate::http_client; diff --git a/common/credential-verification/src/ecash/credential_sender.rs b/common/credential-verification/src/ecash/credential_sender.rs index 4da2095f43e..dc304cf86f2 100644 --- a/common/credential-verification/src/ecash/credential_sender.rs +++ b/common/credential-verification/src/ecash/credential_sender.rs @@ -18,7 +18,7 @@ use nym_validator_client::nym_api::EpochId; use nym_validator_client::nyxd::contract_traits::{ EcashSigningClient, MultisigQueryClient, MultisigSigningClient, PagedMultisigQueryClient, }; -use nym_validator_client::nyxd::cosmwasm_client::ToSingletonContractData; +use nym_validator_client::nyxd::cosmwasm_client::ContractResponseData; use nym_validator_client::nyxd::cw3::Status; use nym_validator_client::nyxd::AccountId; use nym_validator_client::EcashApiClient; diff --git a/common/http-api-common/Cargo.toml b/common/http-api-common/Cargo.toml index c7b6db2ff6a..e00af2e4efe 100644 --- a/common/http-api-common/Cargo.toml +++ b/common/http-api-common/Cargo.toml @@ -15,12 +15,15 @@ axum-client-ip.workspace = true axum.workspace = true bytes = { workspace = true } colored.workspace = true +futures = { workspace = true } mime = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json.workspace = true serde_yaml = { workspace = true } +tower = { workspace = true } tracing.workspace = true utoipa = { workspace = true, optional = true } +zeroize = { workspace = true } [features] utoipa = ["dep:utoipa"] diff --git a/common/http-api-common/src/lib.rs b/common/http-api-common/src/lib.rs index f84e6e2ecc2..dbc3bc0125e 100644 --- a/common/http-api-common/src/lib.rs +++ b/common/http-api-common/src/lib.rs @@ -7,7 +7,7 @@ use axum::Json; use bytes::{BufMut, BytesMut}; use serde::{Deserialize, Serialize}; -pub mod logging; +pub mod middleware; #[derive(Debug, Clone)] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] diff --git a/nym-credential-proxy/nym-credential-proxy/src/http/middleware/auth.rs b/common/http-api-common/src/middleware/bearer_auth.rs similarity index 98% rename from nym-credential-proxy/nym-credential-proxy/src/http/middleware/auth.rs rename to common/http-api-common/src/middleware/bearer_auth.rs index ce9c4e09120..68867f32a34 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/http/middleware/auth.rs +++ b/common/http-api-common/src/middleware/bearer_auth.rs @@ -1,5 +1,5 @@ // Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only +// SPDX-License-Identifier: Apache-2.0 use axum::http::{header, HeaderValue, StatusCode}; use axum::response::IntoResponse; diff --git a/common/http-api-common/src/logging.rs b/common/http-api-common/src/middleware/logging.rs similarity index 96% rename from common/http-api-common/src/logging.rs rename to common/http-api-common/src/middleware/logging.rs index 8de60b338f0..fd60ca30b18 100644 --- a/common/http-api-common/src/logging.rs +++ b/common/http-api-common/src/middleware/logging.rs @@ -1,5 +1,5 @@ // Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only +// SPDX-License-Identifier: Apache-2.0 use axum::extract::Request; use axum::http::header::{HOST, USER_AGENT}; @@ -11,6 +11,7 @@ use colored::Colorize; use std::time::Instant; use tracing::info; +/// Simple logger for requests pub async fn logger( InsecureClientIp(addr): InsecureClientIp, request: Request, diff --git a/common/http-api-common/src/middleware/mod.rs b/common/http-api-common/src/middleware/mod.rs new file mode 100644 index 00000000000..d81923bce55 --- /dev/null +++ b/common/http-api-common/src/middleware/mod.rs @@ -0,0 +1,5 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod bearer_auth; +pub mod logging; diff --git a/nym-api/src/support/http/router.rs b/nym-api/src/support/http/router.rs index 51d432b45be..a6c0390ab9f 100644 --- a/nym-api/src/support/http/router.rs +++ b/nym-api/src/support/http/router.rs @@ -17,7 +17,7 @@ use axum::response::Redirect; use axum::routing::get; use axum::Router; use core::net::SocketAddr; -use nym_http_api_common::logging::logger; +use nym_http_api_common::middleware::logging::logger; use tokio::net::TcpListener; use tokio_util::sync::WaitForCancellationFutureOwned; use tower_http::cors::CorsLayer; diff --git a/nym-credential-proxy/nym-credential-proxy-requests/Cargo.toml b/nym-credential-proxy/nym-credential-proxy-requests/Cargo.toml index 86e281eb8a6..ed887405d24 100644 --- a/nym-credential-proxy/nym-credential-proxy-requests/Cargo.toml +++ b/nym-credential-proxy/nym-credential-proxy-requests/Cargo.toml @@ -34,7 +34,6 @@ nym-serde-helpers = { path = "../../common/serde-helpers", features = ["bs58"] } workspace = true features = ["tokio"] - [features] default = ["query-types"] query-types = ["nym-http-api-common"] diff --git a/nym-credential-proxy/nym-credential-proxy-requests/src/api/v1/ticketbook/models.rs b/nym-credential-proxy/nym-credential-proxy-requests/src/api/v1/ticketbook/models.rs index 1d88b28d004..81082ea154d 100644 --- a/nym-credential-proxy/nym-credential-proxy-requests/src/api/v1/ticketbook/models.rs +++ b/nym-credential-proxy/nym-credential-proxy-requests/src/api/v1/ticketbook/models.rs @@ -268,6 +268,9 @@ pub struct WebhookTicketbookWalletSharesRequest { pub struct TicketbookObtainQueryParams { pub output: Option, + #[serde(default)] + pub skip_webhook: bool, + pub include_master_verification_key: bool, pub include_coin_index_signatures: bool, diff --git a/nym-credential-proxy/nym-credential-proxy/Cargo.toml b/nym-credential-proxy/nym-credential-proxy/Cargo.toml index d7a789b83e4..8069d51493e 100644 --- a/nym-credential-proxy/nym-credential-proxy/Cargo.toml +++ b/nym-credential-proxy/nym-credential-proxy/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nym-credential-proxy" -version = "0.1.3" +version = "0.1.6" authors.workspace = true repository.workspace = true homepage.workspace = true @@ -48,6 +48,7 @@ nym-config = { path = "../../common/config" } nym-crypto = { path = "../../common/crypto", features = ["asymmetric", "rand", "serde"] } nym-credentials = { path = "../../common/credentials" } nym-credentials-interface = { path = "../../common/credentials-interface" } +nym-ecash-contract-common = { path = "../../common/cosmwasm-smart-contracts/ecash-contract" } nym-http-api-common = { path = "../../common/http-api-common", features = ["utoipa"] } nym-validator-client = { path = "../../common/client-libs/validator-client" } nym-network-defaults = { path = "../../common/network-defaults" } diff --git a/nym-credential-proxy/nym-credential-proxy/Dockerfile b/nym-credential-proxy/nym-credential-proxy/Dockerfile index e4548e89289..b6da10f4f53 100644 --- a/nym-credential-proxy/nym-credential-proxy/Dockerfile +++ b/nym-credential-proxy/nym-credential-proxy/Dockerfile @@ -30,6 +30,6 @@ RUN apt update && apt install -yy curl ca-certificates WORKDIR /nym -COPY --from=builder /usr/src/nym/nym-credential-proxy/target/release/nym-credential-proxy ./ +COPY --from=builder /usr/src/nym/target/release/nym-credential-proxy ./ ENTRYPOINT [ "/nym/nym-credential-proxy" ] diff --git a/nym-credential-proxy/nym-credential-proxy/src/cli.rs b/nym-credential-proxy/nym-credential-proxy/src/cli.rs index 3ade5649b05..cf5b25cf230 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/cli.rs +++ b/nym-credential-proxy/nym-credential-proxy/src/cli.rs @@ -55,6 +55,15 @@ pub struct Cli { )] pub(crate) http_auth_token: String, + /// Specify the maximum number of deposits the credential proxy can make in a single transaction + /// (default: 32) + #[clap( + long, + env = "NYM_CREDENTIAL_PROXY_MAX_CONCURRENT_DEPOSITS", + default_value_t = 32 + )] + pub(crate) max_concurrent_deposits: usize, + #[clap(long, env = "NYM_CREDENTIAL_PROXY_PERSISTENT_STORAGE_STORAGE")] pub(crate) persistent_storage_path: Option, } diff --git a/nym-credential-proxy/nym-credential-proxy/src/credentials/ticketbook/mod.rs b/nym-credential-proxy/nym-credential-proxy/src/credentials/ticketbook/mod.rs index 3a91724a4fc..5e2c858b26b 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/credentials/ticketbook/mod.rs +++ b/nym-credential-proxy/nym-credential-proxy/src/credentials/ticketbook/mod.rs @@ -1,6 +1,7 @@ // Copyright 2024 Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only +use crate::deposit_maker::{DepositRequest, DepositResponse}; use crate::error::VpnApiError; use crate::http::state::ApiState; use crate::storage::models::BlindedShares; @@ -14,21 +15,48 @@ use nym_credentials::IssuanceTicketBook; use nym_credentials_interface::Base58; use nym_crypto::asymmetric::ed25519; use nym_validator_client::ecash::BlindSignRequestBody; -use nym_validator_client::nyxd::contract_traits::EcashSigningClient; -use nym_validator_client::nyxd::cosmwasm_client::ToSingletonContractData; +use nym_validator_client::nyxd::Coin; use rand::rngs::OsRng; use std::collections::HashMap; use std::sync::Arc; use std::time::Duration; use time::OffsetDateTime; -use tokio::sync::Mutex; -use tokio::time::timeout; -use tracing::{debug, error, info, instrument}; +use tokio::sync::{oneshot, Mutex}; +use tokio::time::{timeout, Instant}; +use tracing::{debug, error, info, instrument, warn}; use uuid::Uuid; // use the same type alias as our contract without importing the whole thing just for this single line pub type NodeId = u64; +#[instrument(skip(state), ret, err(Display))] +async fn make_deposit( + state: &ApiState, + pub_key: ed25519::PublicKey, + deposit_amount: &Coin, +) -> Result { + let start = Instant::now(); + let (on_done_tx, on_done_rx) = oneshot::channel(); + let request = DepositRequest::new(pub_key, deposit_amount, on_done_tx); + state.request_deposit(request).await; + + let time_taken = start.elapsed(); + let formatted = humantime::format_duration(time_taken); + + let Ok(deposit_response) = on_done_rx.await else { + error!("failed to receive deposit response: the corresponding sender channel got dropped by the DepositMaker!"); + return Err(VpnApiError::DepositFailure); + }; + + if time_taken > Duration::from_secs(20) { + warn!("attempting to resolve deposit request took {formatted}. perhaps the buffer is too small or the process/chain is overloaded?") + } else { + debug!("attempting to resolve deposit request took {formatted}") + } + + deposit_response.ok_or(VpnApiError::DepositFailure) +} + #[instrument( skip(state, request_data, request, requested_on), fields( @@ -59,25 +87,12 @@ pub(crate) async fn try_obtain_wallet_shares( .await?; let ecash_api_clients = state.ecash_clients(epoch).await?.clone(); - let chain_write_permit = state.start_chain_tx().await; - - info!("starting the deposit!"); - // TODO: batch those up - // TODO: batch those up - let deposit_res = chain_write_permit - .make_ticketbook_deposit( - ed25519_keypair.public_key().to_base58_string(), - deposit_amount.clone(), - None, - ) - .await?; - - // explicitly drop it here so other tasks could start using it - drop(chain_write_permit); + let DepositResponse { + deposit_id, + tx_hash, + } = make_deposit(state, *ed25519_keypair.public_key(), &deposit_amount).await?; - let deposit_id = deposit_res.parse_singleton_u32_contract_data()?; - let tx_hash = deposit_res.transaction_hash; - info!(deposit_id = %deposit_id, tx_hash = %tx_hash, "deposit finished"); + info!(deposit_id = %deposit_id, "deposit finished"); // store the deposit information so if we fail, we could perhaps still reuse it for another issuance state @@ -342,6 +357,7 @@ pub(crate) async fn try_obtain_blinded_ticketbook_async( params: TicketbookObtainQueryParams, pending: BlindedShares, ) { + let skip_webhook = params.skip_webhook; if let Err(err) = try_obtain_blinded_ticketbook_async_inner( &state, request, @@ -352,6 +368,11 @@ pub(crate) async fn try_obtain_blinded_ticketbook_async( ) .await { + if skip_webhook { + info!(uuid = %request,"the webhook is not going to be called for this request"); + return; + } + // post to the webhook to notify of errors on this side if let Err(webhook_err) = try_trigger_webhook_request_for_error( &state, diff --git a/nym-credential-proxy/nym-credential-proxy/src/deposit_maker.rs b/nym-credential-proxy/nym-credential-proxy/src/deposit_maker.rs new file mode 100644 index 00000000000..2ec685d656a --- /dev/null +++ b/nym-credential-proxy/nym-credential-proxy/src/deposit_maker.rs @@ -0,0 +1,205 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::error::VpnApiError; +use crate::http::state::ChainClient; +use nym_crypto::asymmetric::ed25519; +use nym_ecash_contract_common::deposit::DepositId; +use nym_validator_client::nyxd::cosmwasm_client::ContractResponseData; +use nym_validator_client::nyxd::{Coin, Hash}; +use tokio::sync::{mpsc, oneshot}; +use tokio_util::sync::CancellationToken; +use tracing::{debug, error, info, warn}; + +#[derive(Debug)] +pub(crate) struct DepositResponse { + pub tx_hash: Hash, + pub deposit_id: DepositId, +} + +pub(crate) struct DepositRequest { + pubkey: ed25519::PublicKey, + deposit_amount: Coin, + on_done: oneshot::Sender>, +} + +impl DepositRequest { + pub(crate) fn new( + pubkey: ed25519::PublicKey, + deposit_amount: &Coin, + on_done: oneshot::Sender>, + ) -> Self { + DepositRequest { + pubkey, + deposit_amount: deposit_amount.clone(), + on_done, + } + } +} + +pub(crate) type DepositRequestReceiver = mpsc::Receiver; + +pub(crate) fn new_control_channels( + max_concurrent_deposits: usize, +) -> (DepositRequestSender, DepositRequestReceiver) { + let (tx, rx) = mpsc::channel(max_concurrent_deposits); + (tx.into(), rx) +} + +#[derive(Debug, Clone)] +pub struct DepositRequestSender(mpsc::Sender); + +impl From> for DepositRequestSender { + fn from(inner: mpsc::Sender) -> Self { + DepositRequestSender(inner) + } +} + +impl DepositRequestSender { + pub(crate) async fn request_deposit(&self, request: DepositRequest) { + if self.0.send(request).await.is_err() { + error!("failed to request deposit: the DepositMaker must have died!") + } + } +} + +pub(crate) struct DepositMaker { + client: ChainClient, + max_concurrent_deposits: usize, + deposit_request_sender: DepositRequestSender, + deposit_request_receiver: DepositRequestReceiver, + short_sha: &'static str, + cancellation_token: CancellationToken, +} + +impl DepositMaker { + pub(crate) fn new( + short_sha: &'static str, + client: ChainClient, + max_concurrent_deposits: usize, + cancellation_token: CancellationToken, + ) -> Self { + let (deposit_request_sender, deposit_request_receiver) = + new_control_channels(max_concurrent_deposits); + + DepositMaker { + client, + max_concurrent_deposits, + deposit_request_sender, + deposit_request_receiver, + short_sha, + cancellation_token, + } + } + + pub(crate) fn deposit_request_sender(&self) -> DepositRequestSender { + self.deposit_request_sender.clone() + } + + pub(crate) async fn process_deposit_requests( + &mut self, + requests: Vec, + ) -> Result<(), VpnApiError> { + let chain_write_permit = self.client.start_chain_tx().await; + + info!("starting deposits"); + let mut contents = Vec::new(); + let mut replies = Vec::new(); + for request in requests { + // check if the channel is still open in case the receiver client has cancelled the request + if request.on_done.is_closed() { + warn!( + "the request for deposit from {} got cancelled", + request.pubkey + ); + continue; + } + + contents.push((request.pubkey.to_base58_string(), request.deposit_amount)); + replies.push(request.on_done); + } + + let deposits_res = chain_write_permit + .make_deposits(self.short_sha, contents) + .await; + let execute_res = match deposits_res { + Ok(res) => res, + Err(err) => { + // we have to let requesters know the deposit(s) failed + for reply in replies { + if reply.send(None).is_err() { + warn!("one of the deposit requesters has been terminated") + } + } + return Err(err); + } + }; + + let tx_hash = execute_res.transaction_hash; + info!("{} deposits made in transaction: {tx_hash}", replies.len()); + + let contract_data = match execute_res.to_contract_data() { + Ok(contract_data) => contract_data, + Err(err) => { + // that one is tricky. deposits technically got made, but we somehow failed to parse response, + // in this case terminate the proxy with 0 exit code so it wouldn't get automatically restarted + // because it requires some serious MANUAL intervention + error!("CRITICAL FAILURE: failed to parse out deposit information from the contract transaction. either the chain got upgraded and the schema changed or the ecash contract got changed! terminating the process. it has to be inspected manually. error was: {err}"); + self.cancellation_token.cancel(); + return Err(VpnApiError::DepositFailure); + } + }; + + if contract_data.len() != replies.len() { + // another critical failure, that one should be quite impossible and thus has to be manually inspected + error!("CRITICAL FAILURE: failed to parse out all deposit information from the contract transaction. got {} responses while we sent {} deposits! either the chain got upgraded and the schema changed or the ecash contract got changed! terminating the process. it has to be inspected manually", contract_data.len(), replies.len()); + self.cancellation_token.cancel(); + return Err(VpnApiError::DepositFailure); + } + + for (reply_channel, response) in replies.into_iter().zip(contract_data) { + let response_index = response.message_index; + let deposit_id = match response.parse_singleton_u32_contract_data() { + Ok(deposit_id) => deposit_id, + Err(err) => { + // another impossibility + error!("CRITICAL FAILURE: failed to parse out deposit id out of the response at index {response_index}: {err}. either the chain got upgraded and the schema changed or the ecash contract got changed! terminating the process. it has to be inspected manually"); + self.cancellation_token.cancel(); + return Err(VpnApiError::DepositFailure); + } + }; + + if reply_channel + .send(Some(DepositResponse { + deposit_id, + tx_hash, + })) + .is_err() + { + warn!("one of the deposit requesters has been terminated. deposit {deposit_id} will remain unclaimed!"); + // this shouldn't happen as the requester task shouldn't be killed, but it's not a critical failure + // we just lost some tokens, but it's not an undefined on-chain behaviour + } + } + + Ok(()) + } + + pub async fn run_forever(mut self) { + info!("starting the deposit maker task"); + loop { + let mut receive_buffer = Vec::with_capacity(self.max_concurrent_deposits); + tokio::select! { + _ = self.cancellation_token.cancelled() => { + break + } + received = self.deposit_request_receiver.recv_many(&mut receive_buffer, self.max_concurrent_deposits) => { + debug!("received {received} deposit requests"); + if let Err(err) = self.process_deposit_requests(receive_buffer).await { + error!("failed to process received deposit requests: {err}") + } + } + } + } + } +} diff --git a/nym-credential-proxy/nym-credential-proxy/src/error.rs b/nym-credential-proxy/nym-credential-proxy/src/error.rs index 4ffba86be79..56e18a0673d 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/error.rs +++ b/nym-credential-proxy/nym-credential-proxy/src/error.rs @@ -115,6 +115,9 @@ pub enum VpnApiError { #[error("timed out while attempting to obtain partial wallet from {client_repr}")] EcashApiRequestTimeout { client_repr: String }, + + #[error("failed to create deposit")] + DepositFailure, } impl VpnApiError { diff --git a/nym-credential-proxy/nym-credential-proxy/src/http/middleware/logging.rs b/nym-credential-proxy/nym-credential-proxy/src/http/middleware/logging.rs deleted file mode 100644 index 825104ec5b7..00000000000 --- a/nym-credential-proxy/nym-credential-proxy/src/http/middleware/logging.rs +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only - -use axum::{ - extract::{ConnectInfo, Request}, - http::{ - header::{HOST, USER_AGENT}, - HeaderValue, - }, - middleware::Next, - response::IntoResponse, -}; -use colored::*; -use std::net::SocketAddr; -use tokio::time::Instant; -use tracing::info; - -/// Simple logger for requests -pub async fn logger( - ConnectInfo(addr): ConnectInfo, - req: Request, - next: Next, -) -> impl IntoResponse { - let method = req.method().to_string().green(); - let uri = req.uri().to_string().blue(); - let agent = header_map( - req.headers().get(USER_AGENT), - "Unknown User Agent".to_string(), - ); - - let host = header_map(req.headers().get(HOST), "Unknown Host".to_string()); - - let start = Instant::now(); - let res = next.run(req).await; - let time_taken = start.elapsed(); - let status = res.status(); - let print_status = if status.is_client_error() || status.is_server_error() { - status.to_string().red() - } else if status.is_success() { - status.to_string().green() - } else { - status.to_string().yellow() - }; - - let taken = "time taken".bold(); - - let time_taken = match time_taken.as_millis() { - ms if ms > 500 => format!("{taken}: {}", format!("{ms}ms").red()), - ms if ms > 200 => format!("{taken}: {}", format!("{ms}ms").yellow()), - ms if ms > 50 => format!("{taken}: {}", format!("{ms}ms").bright_yellow()), - ms => format!("{taken}: {ms}ms"), - }; - - let agent_str = "agent".bold(); - info!("[{addr} -> {host}] {method} '{uri}': {print_status} {time_taken} {agent_str}: {agent}"); - - res -} - -fn header_map(header: Option<&HeaderValue>, msg: String) -> String { - header - .map(|x| x.to_str().unwrap_or(&msg).to_string()) - .unwrap_or(msg) -} diff --git a/nym-credential-proxy/nym-credential-proxy/src/http/middleware/mod.rs b/nym-credential-proxy/nym-credential-proxy/src/http/middleware/mod.rs deleted file mode 100644 index b51d9a59b5c..00000000000 --- a/nym-credential-proxy/nym-credential-proxy/src/http/middleware/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -// Copyright 2024 Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only - -pub mod auth; -pub mod logging; diff --git a/nym-credential-proxy/nym-credential-proxy/src/http/mod.rs b/nym-credential-proxy/nym-credential-proxy/src/http/mod.rs index 1267b3abfb8..e9359ac16ae 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/http/mod.rs +++ b/nym-credential-proxy/nym-credential-proxy/src/http/mod.rs @@ -10,7 +10,6 @@ use tokio_util::sync::CancellationToken; use tracing::info; pub mod helpers; -pub mod middleware; pub mod router; pub mod state; pub mod types; @@ -22,10 +21,15 @@ pub struct HttpServer { } impl HttpServer { - pub fn new(bind_address: SocketAddr, state: ApiState, auth_token: String) -> Self { + pub fn new( + bind_address: SocketAddr, + state: ApiState, + auth_token: String, + cancellation: CancellationToken, + ) -> Self { HttpServer { bind_address, - cancellation: state.cancellation_token(), + cancellation, router: build_router(state, auth_token), } } diff --git a/nym-credential-proxy/nym-credential-proxy/src/http/router/api/mod.rs b/nym-credential-proxy/nym-credential-proxy/src/http/router/api/mod.rs index 597887fb30f..aea8d4c5c62 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/http/router/api/mod.rs +++ b/nym-credential-proxy/nym-credential-proxy/src/http/router/api/mod.rs @@ -4,8 +4,8 @@ use crate::http::state::ApiState; use axum::Router; use nym_credential_proxy_requests::routes; +use nym_http_api_common::middleware::bearer_auth::AuthLayer; -use crate::http::middleware::auth::AuthLayer; pub(crate) use nym_http_api_common::{Output, OutputParams}; pub mod v1; diff --git a/nym-credential-proxy/nym-credential-proxy/src/http/router/api/v1/mod.rs b/nym-credential-proxy/nym-credential-proxy/src/http/router/api/v1/mod.rs index 59ee7b4df95..b77fe0d09ed 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/http/router/api/v1/mod.rs +++ b/nym-credential-proxy/nym-credential-proxy/src/http/router/api/v1/mod.rs @@ -1,13 +1,11 @@ // Copyright 2024 Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::http::middleware::auth::AuthLayer; use crate::http::state::ApiState; use axum::Router; use nym_credential_proxy_requests::routes::api::v1; +use nym_http_api_common::middleware::bearer_auth::AuthLayer; -// pub mod bandwidth_voucher; -// pub mod freepass; pub mod openapi; pub mod ticketbook; diff --git a/nym-credential-proxy/nym-credential-proxy/src/http/router/api/v1/ticketbook/mod.rs b/nym-credential-proxy/nym-credential-proxy/src/http/router/api/v1/ticketbook/mod.rs index 2dccc8d3494..335107f3dba 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/http/router/api/v1/ticketbook/mod.rs +++ b/nym-credential-proxy/nym-credential-proxy/src/http/router/api/v1/ticketbook/mod.rs @@ -21,7 +21,7 @@ use nym_credential_proxy_requests::api::v1::ticketbook::models::{ use nym_credential_proxy_requests::routes::api::v1::ticketbook; use nym_http_api_common::{FormattedResponse, OutputParams}; use time::OffsetDateTime; -use tracing::{error, info, span, warn, Level}; +use tracing::{error, info, span, warn, Instrument, Level}; pub(crate) mod shares; @@ -71,55 +71,58 @@ pub(crate) async fn obtain_ticketbook_shares( let requested_on = OffsetDateTime::now_utc(); let span = span!(Level::INFO, "obtain ticketboook", uuid = %uuid); - let _entered = span.enter(); - info!(""); + async move { + info!(""); - let output = params.output.unwrap_or_default(); + let output = params.output.unwrap_or_default(); - state.ensure_not_in_epoch_transition(Some(uuid)).await?; - let epoch_id = state - .current_epoch_id() - .await - .map_err(|err| RequestError::new_server_error(err, uuid))?; - - if let Err(err) = ensure_sane_expiration_date(payload.expiration_date) { - warn!("failure due to invalid expiration date"); - return Err(RequestError::new_with_uuid( - err.to_string(), - uuid, - StatusCode::BAD_REQUEST, - )); - } + state.ensure_not_in_epoch_transition(Some(uuid)).await?; + let epoch_id = state + .current_epoch_id() + .await + .map_err(|err| RequestError::new_server_error(err, uuid))?; - // if additional data was requested, grab them first in case there are any cache/network issues - let ( - master_verification_key, - aggregated_expiration_date_signatures, - aggregated_coin_index_signatures, - ) = state - .response_global_data( - params.include_master_verification_key, - params.include_expiration_date_signatures, - params.include_coin_index_signatures, - epoch_id, - payload.expiration_date, - uuid, - ) - .await?; + if let Err(err) = ensure_sane_expiration_date(payload.expiration_date) { + warn!("failure due to invalid expiration date"); + return Err(RequestError::new_with_uuid( + err.to_string(), + uuid, + StatusCode::BAD_REQUEST, + )); + } - let shares = try_obtain_wallet_shares(&state, uuid, requested_on, payload) - .await - .inspect_err(|err| warn!("request failure: {err}")) - .map_err(|err| RequestError::new(err.to_string(), StatusCode::INTERNAL_SERVER_ERROR))?; + // if additional data was requested, grab them first in case there are any cache/network issues + let ( + master_verification_key, + aggregated_expiration_date_signatures, + aggregated_coin_index_signatures, + ) = state + .response_global_data( + params.include_master_verification_key, + params.include_expiration_date_signatures, + params.include_coin_index_signatures, + epoch_id, + payload.expiration_date, + uuid, + ) + .await?; - info!("request was successful!"); - Ok(output.to_response(TicketbookWalletSharesResponse { - epoch_id, - shares, - master_verification_key, - aggregated_coin_index_signatures, - aggregated_expiration_date_signatures, - })) + let shares = try_obtain_wallet_shares(&state, uuid, requested_on, payload) + .await + .inspect_err(|err| warn!("request failure: {err}")) + .map_err(|err| RequestError::new(err.to_string(), StatusCode::INTERNAL_SERVER_ERROR))?; + + info!("request was successful!"); + Ok(output.to_response(TicketbookWalletSharesResponse { + epoch_id, + shares, + master_verification_key, + aggregated_coin_index_signatures, + aggregated_expiration_date_signatures, + })) + } + .instrument(span) + .await } /// Attempt to obtain blinded shares of an ecash ticketbook wallet asynchronously @@ -159,63 +162,69 @@ pub(crate) async fn obtain_ticketbook_shares_async( let requested_on = OffsetDateTime::now_utc(); let span = span!(Level::INFO, "[async] obtain ticketboook", uuid = %uuid); - let _entered = span.enter(); - info!(""); - - let output = params.output.unwrap_or_default(); + async move { + info!(""); + let output = params.output.unwrap_or_default(); - // 1. perform basic validation - state.ensure_not_in_epoch_transition(Some(uuid)).await?; + // 1. perform basic validation + state.ensure_not_in_epoch_transition(Some(uuid)).await?; - if let Err(err) = ensure_sane_expiration_date(payload.inner.expiration_date) { - warn!("failure due to invalid expiration date"); - return Err(RequestError::new_with_uuid( - err.to_string(), - uuid, - StatusCode::BAD_REQUEST, - )); - } - - // 2. store the request to retrieve the id - let pending = match state - .storage() - .insert_new_pending_async_shares_request(uuid, &payload.device_id, &payload.credential_id) - .await - { - Err(err) => { - error!("failed to insert new pending async shares: {err}"); + if let Err(err) = ensure_sane_expiration_date(payload.inner.expiration_date) { + warn!("failure due to invalid expiration date"); return Err(RequestError::new_with_uuid( err.to_string(), uuid, - StatusCode::CONFLICT, + StatusCode::BAD_REQUEST, )); } - Ok(pending) => pending, - }; - let id = pending.id; - - // 3. try to spawn a new task attempting to resolve the request - if state - .try_spawn(try_obtain_blinded_ticketbook_async( - state.clone(), - uuid, - requested_on, - payload, - params, - pending, - )) - .is_none() - { - // we're going through the shutdown - return Err(RequestError::new_with_uuid( - "server shutdown in progress", - uuid, - StatusCode::INTERNAL_SERVER_ERROR, - )); - } - // 4. in the meantime, return the id to the user - Ok(output.to_response(TicketbookWalletSharesAsyncResponse { id, uuid })) + // 2. store the request to retrieve the id + let pending = match state + .storage() + .insert_new_pending_async_shares_request( + uuid, + &payload.device_id, + &payload.credential_id, + ) + .await + { + Err(err) => { + error!("failed to insert new pending async shares: {err}"); + return Err(RequestError::new_with_uuid( + err.to_string(), + uuid, + StatusCode::CONFLICT, + )); + } + Ok(pending) => pending, + }; + let id = pending.id; + + // 3. try to spawn a new task attempting to resolve the request + if state + .try_spawn(try_obtain_blinded_ticketbook_async( + state.clone(), + uuid, + requested_on, + payload, + params, + pending, + )) + .is_none() + { + // we're going through the shutdown + return Err(RequestError::new_with_uuid( + "server shutdown in progress", + uuid, + StatusCode::INTERNAL_SERVER_ERROR, + )); + } + + // 4. in the meantime, return the id to the user + Ok(output.to_response(TicketbookWalletSharesAsyncResponse { id, uuid })) + } + .instrument(span) + .await } /// Obtain the current value of the bandwidth voucher deposit diff --git a/nym-credential-proxy/nym-credential-proxy/src/http/router/api/v1/ticketbook/shares.rs b/nym-credential-proxy/nym-credential-proxy/src/http/router/api/v1/ticketbook/shares.rs index 66be4e99616..cfe4893a69b 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/http/router/api/v1/ticketbook/shares.rs +++ b/nym-credential-proxy/nym-credential-proxy/src/http/router/api/v1/ticketbook/shares.rs @@ -17,7 +17,7 @@ use nym_credential_proxy_requests::api::v1::ticketbook::models::{ use nym_credential_proxy_requests::routes::api::v1::ticketbook::shares; use nym_http_api_common::OutputParams; use nym_validator_client::nym_api::EpochId; -use tracing::{debug, span, Level}; +use tracing::{debug, span, Instrument, Level}; use uuid::Uuid; async fn shares_to_response( @@ -100,50 +100,51 @@ pub(crate) async fn query_for_shares_by_id( let uuid = random_uuid(); let span = span!(Level::INFO, "query shares by id", uuid = %uuid, share_id = %share_id); - let _entered = span.enter(); - debug!(""); - - // TODO: edge case: this will **NOT** work if shares got created in epoch X, - // but this query happened in epoch X+1 - let shares = match state - .storage() - .load_wallet_shares_by_shares_id(share_id) - .await - { - Ok(shares) => { - if shares.is_empty() { - debug!("shares not found"); - - // check for explicit error - match state - .storage() - .load_shares_error_by_shares_id(share_id) - .await - { - Ok(maybe_error_message) => { - if let Some(error_message) = maybe_error_message { - return Err(RequestError::new_with_uuid( - format!("failed to obtain wallet shares: {error_message} - share_id = {share_id}"), - uuid, - StatusCode::INTERNAL_SERVER_ERROR, - )); + async move { + debug!(""); + + // TODO: edge case: this will **NOT** work if shares got created in epoch X, + // but this query happened in epoch X+1 + let shares = match state + .storage() + .load_wallet_shares_by_shares_id(share_id) + .await + { + Ok(shares) => { + if shares.is_empty() { + debug!("shares not found"); + + // check for explicit error + match state + .storage() + .load_shares_error_by_shares_id(share_id) + .await + { + Ok(maybe_error_message) => { + if let Some(error_message) = maybe_error_message { + return Err(RequestError::new_with_uuid( + format!("failed to obtain wallet shares: {error_message} - share_id = {share_id}"), + uuid, + StatusCode::INTERNAL_SERVER_ERROR, + )); + } } + Err(err) => return db_failure(err, uuid), } - Err(err) => return db_failure(err, uuid), - } - return Err(RequestError::new_with_uuid( - format!("not found - share_id = {share_id}"), - uuid, - StatusCode::NOT_FOUND, - )); + return Err(RequestError::new_with_uuid( + format!("not found - share_id = {share_id}"), + uuid, + StatusCode::NOT_FOUND, + )); + } + shares } - shares - } - Err(err) => return db_failure(err, uuid), - }; + Err(err) => return db_failure(err, uuid), + }; - shares_to_response(state, uuid, shares, params).await + shares_to_response(state, uuid, shares, params).await + }.instrument(span).await } /// Query by id for blinded wallet shares of a ticketbook @@ -174,50 +175,51 @@ pub(crate) async fn query_for_shares_by_device_id_and_credential_id( let uuid = random_uuid(); let span = span!(Level::INFO, "query shares by device and credential ids", uuid = %uuid, device_id = %device_id, credential_id = %credential_id); - let _entered = span.enter(); - debug!(""); - - // TODO: edge case: this will **NOT** work if shares got created in epoch X, - // but this query happened in epoch X+1 - let shares = match state - .storage() - .load_wallet_shares_by_device_and_credential_id(&device_id, &credential_id) - .await - { - Ok(shares) => { - if shares.is_empty() { - debug!("shares not found"); - - // check for explicit error - match state - .storage() - .load_shares_error_by_device_and_credential_id(&device_id, &credential_id) - .await - { - Ok(maybe_error_message) => { - if let Some(error_message) = maybe_error_message { - return Err(RequestError::new_with_uuid( - format!("failed to obtain wallet shares: {error_message} - device_id = {device_id}, credential_id = {credential_id}"), - uuid, - StatusCode::INTERNAL_SERVER_ERROR, - )); + async move { + debug!(""); + + // TODO: edge case: this will **NOT** work if shares got created in epoch X, + // but this query happened in epoch X+1 + let shares = match state + .storage() + .load_wallet_shares_by_device_and_credential_id(&device_id, &credential_id) + .await + { + Ok(shares) => { + if shares.is_empty() { + debug!("shares not found"); + + // check for explicit error + match state + .storage() + .load_shares_error_by_device_and_credential_id(&device_id, &credential_id) + .await + { + Ok(maybe_error_message) => { + if let Some(error_message) = maybe_error_message { + return Err(RequestError::new_with_uuid( + format!("failed to obtain wallet shares: {error_message} - device_id = {device_id}, credential_id = {credential_id}"), + uuid, + StatusCode::INTERNAL_SERVER_ERROR, + )); + } } + Err(err) => return db_failure(err, uuid), } - Err(err) => return db_failure(err, uuid), - } - return Err(RequestError::new_with_uuid( - format!("not found - device_id = {device_id}, credential_id = {credential_id}"), - uuid, - StatusCode::NOT_FOUND, - )); + return Err(RequestError::new_with_uuid( + format!("not found - device_id = {device_id}, credential_id = {credential_id}"), + uuid, + StatusCode::NOT_FOUND, + )); + } + shares } - shares - } - Err(err) => return db_failure(err, uuid), - }; + Err(err) => return db_failure(err, uuid), + }; - shares_to_response(state, uuid, shares, params).await + shares_to_response(state, uuid, shares, params).await + }.instrument(span).await } pub(crate) fn routes() -> Router { diff --git a/nym-credential-proxy/nym-credential-proxy/src/http/router/mod.rs b/nym-credential-proxy/nym-credential-proxy/src/http/router/mod.rs index fd0ecf267af..c7030b470de 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/http/router/mod.rs +++ b/nym-credential-proxy/nym-credential-proxy/src/http/router/mod.rs @@ -1,13 +1,13 @@ // Copyright 2024 Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::http::middleware::auth::AuthLayer; -use crate::http::middleware::logging; use crate::http::state::ApiState; use axum::response::Redirect; use axum::routing::{get, MethodRouter}; use axum::Router; use nym_credential_proxy_requests::routes; +use nym_http_api_common::middleware::bearer_auth::AuthLayer; +use nym_http_api_common::middleware::logging; use std::sync::Arc; use zeroize::Zeroizing; diff --git a/nym-credential-proxy/nym-credential-proxy/src/http/state/mod.rs b/nym-credential-proxy/nym-credential-proxy/src/http/state/mod.rs index bcc62f9c7eb..dc6309e8a7f 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/http/state/mod.rs +++ b/nym-credential-proxy/nym-credential-proxy/src/http/state/mod.rs @@ -1,6 +1,7 @@ // Copyright 2024 Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only +use crate::deposit_maker::{DepositRequest, DepositRequestSender}; use crate::error::VpnApiError; use crate::helpers::LockTimer; use crate::http::types::RequestError; @@ -28,20 +29,24 @@ use nym_credentials::{ AggregatedCoinIndicesSignatures, AggregatedExpirationDateSignatures, EpochVerificationKey, }; use nym_credentials_interface::VerificationKeyAuth; +use nym_ecash_contract_common::msg::ExecuteMsg; use nym_validator_client::coconut::EcashApiError; use nym_validator_client::nym_api::EpochId; use nym_validator_client::nyxd::contract_traits::dkg_query_client::Epoch; use nym_validator_client::nyxd::contract_traits::{ DkgQueryClient, EcashQueryClient, NymContractsProvider, PagedDkgQueryClient, }; -use nym_validator_client::nyxd::{Coin, NyxdClient}; +use nym_validator_client::nyxd::cosmwasm_client::types::ExecuteResult; +use nym_validator_client::nyxd::{Coin, CosmWasmClient, NyxdClient}; use nym_validator_client::{nyxd, DirectSigningHttpRpcNyxdClient, EcashApiClient}; use std::future::Future; use std::ops::Deref; use std::sync::Arc; +use std::time::Duration; use time::{Date, OffsetDateTime}; use tokio::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard}; use tokio::task::JoinHandle; +use tokio::time::Instant; use tokio_util::sync::CancellationToken; use tokio_util::task::TaskTracker; use tracing::{debug, info, warn}; @@ -59,36 +64,19 @@ impl ApiState { pub async fn new( storage: VpnApiStorage, zk_nym_web_hook_config: ZkNymWebHookConfig, - mnemonic: Mnemonic, + client: ChainClient, + deposit_requester: DepositRequestSender, + cancellation_token: CancellationToken, ) -> Result { - let network_details = nym_network_defaults::NymNetworkDetails::new_from_env(); - let client_config = nyxd::Config::try_from_nym_network_details(&network_details)?; - - let nyxd_url = network_details - .endpoints - .first() - .ok_or_else(|| VpnApiError::NoNyxEndpointsAvailable)? - .nyxd_url - .as_str(); - - let client = NyxdClient::connect_with_mnemonic(client_config, nyxd_url, mnemonic)?; - - if client.ecash_contract_address().is_none() { - return Err(VpnApiError::UnavailableEcashContract); - } - - if client.dkg_contract_address().is_none() { - return Err(VpnApiError::UnavailableDKGContract); - } - let state = ApiState { inner: Arc::new(ApiStateInner { storage, - client: RwLock::new(client), + client, ecash_state: EcashState::default(), zk_nym_web_hook_config, task_tracker: TaskTracker::new(), - cancellation_token: CancellationToken::new(), + deposit_requester, + cancellation_token, }), }; @@ -136,10 +124,6 @@ impl ApiState { self.inner.task_tracker.wait().await } - pub(crate) fn cancellation_token(&self) -> CancellationToken { - self.inner.cancellation_token.clone() - } - pub(crate) fn zk_nym_web_hook(&self) -> &ZkNymWebHookConfig { &self.inner.zk_nym_web_hook_config } @@ -220,16 +204,19 @@ impl ApiState { } pub(crate) async fn query_chain(&self) -> RwLockReadGuard { - let _acquire_timer = LockTimer::new("acquire chain query permit"); - self.inner.client.read().await + self.inner.client.query_chain().await } - pub(crate) async fn start_chain_tx(&self) -> ChainWritePermit { - let _acquire_timer = LockTimer::new("acquire exclusive chain write permit"); + pub(crate) async fn request_deposit(&self, request: DepositRequest) { + let start = Instant::now(); + self.inner.deposit_requester.request_deposit(request).await; - ChainWritePermit { - lock_timer: LockTimer::new("exclusive chain access permit"), - inner: self.inner.client.write().await, + let time_taken = start.elapsed(); + let formatted = humantime::format_duration(time_taken); + if time_taken > Duration::from_secs(10) { + warn!("attempting to push new deposit request onto the queue took {formatted}. perhaps the buffer is too small or the process/chain is overloaded?") + } else { + debug!("attempting to push new deposit request onto the queue took {formatted}") } } @@ -604,10 +591,57 @@ impl ApiState { } } +#[derive(Clone)] +pub struct ChainClient(Arc>); + +impl ChainClient { + pub fn new(mnemonic: Mnemonic) -> Result { + let network_details = nym_network_defaults::NymNetworkDetails::new_from_env(); + let client_config = nyxd::Config::try_from_nym_network_details(&network_details)?; + + let nyxd_url = network_details + .endpoints + .first() + .ok_or_else(|| VpnApiError::NoNyxEndpointsAvailable)? + .nyxd_url + .as_str(); + + let client = NyxdClient::connect_with_mnemonic(client_config, nyxd_url, mnemonic)?; + + if client.ecash_contract_address().is_none() { + return Err(VpnApiError::UnavailableEcashContract); + } + + if client.dkg_contract_address().is_none() { + return Err(VpnApiError::UnavailableDKGContract); + } + + Ok(ChainClient(Arc::new(RwLock::new(client)))) + } + + pub(crate) async fn query_chain(&self) -> ChainReadPermit { + let _acquire_timer = LockTimer::new("acquire chain query permit"); + self.0.read().await + } + + pub(crate) async fn start_chain_tx(&self) -> ChainWritePermit { + let _acquire_timer = LockTimer::new("acquire exclusive chain write permit"); + + ChainWritePermit { + lock_timer: LockTimer::new("exclusive chain access permit"), + inner: self.0.write().await, + } + } +} + +// + struct ApiStateInner { storage: VpnApiStorage, - client: RwLock, + client: ChainClient, + + deposit_requester: DepositRequestSender, zk_nym_web_hook_config: ZkNymWebHookConfig, @@ -666,6 +700,8 @@ pub(crate) struct EcashState { CachedImmutableItems, } +pub(crate) type ChainReadPermit<'a> = RwLockReadGuard<'a, DirectSigningHttpRpcNyxdClient>; + // explicitly wrap the WriteGuard for extra information regarding time taken pub(crate) struct ChainWritePermit<'a> { // it's not really dead, we only care about it being dropped @@ -674,6 +710,55 @@ pub(crate) struct ChainWritePermit<'a> { inner: RwLockWriteGuard<'a, DirectSigningHttpRpcNyxdClient>, } +impl<'a> ChainWritePermit<'a> { + pub(crate) async fn make_deposits( + self, + short_sha: &'static str, + info: Vec<(String, Coin)>, + ) -> Result { + let address = self.inner.address(); + let starting_sequence = self.inner.get_sequence(&address).await?.sequence; + + let deposits = info.len(); + + let ecash_contract = self + .inner + .ecash_contract_address() + .ok_or(VpnApiError::UnavailableEcashContract)?; + let deposit_messages = info + .into_iter() + .map(|(identity_key, amount)| { + ( + ExecuteMsg::DepositTicketBookFunds { identity_key }, + vec![amount], + ) + }) + .collect::>(); + + let res = self + .inner + .execute_multiple( + ecash_contract, + deposit_messages, + None, + format!("cp-{short_sha}: performing {deposits} deposits"), + ) + .await?; + + loop { + let updated_sequence = self.inner.get_sequence(&address).await?.sequence; + + if updated_sequence > starting_sequence { + break; + } + warn!("wrong sequence number... waiting before releasing chain lock"); + tokio::time::sleep(Duration::from_millis(50)).await; + } + + Ok(res) + } +} + impl Deref for ChainWritePermit<'_> { type Target = DirectSigningHttpRpcNyxdClient; diff --git a/nym-credential-proxy/nym-credential-proxy/src/main.rs b/nym-credential-proxy/nym-credential-proxy/src/main.rs index b534f442c8d..71dd082b106 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/main.rs +++ b/nym-credential-proxy/nym-credential-proxy/src/main.rs @@ -7,19 +7,23 @@ #![warn(clippy::dbg_macro)] use crate::cli::Cli; +use crate::deposit_maker::DepositMaker; use crate::error::VpnApiError; -use crate::http::state::ApiState; +use crate::http::state::{ApiState, ChainClient}; use crate::http::HttpServer; use crate::storage::VpnApiStorage; use crate::tasks::StoragePruner; use clap::Parser; use nym_bin_common::logging::setup_tracing_logger; +use nym_bin_common::{bin_info, bin_info_owned}; use nym_network_defaults::setup_env; -use tracing::{info, trace}; +use tokio_util::sync::CancellationToken; +use tracing::{error, info, trace}; pub mod cli; pub mod config; pub mod credentials; +mod deposit_maker; pub mod error; pub mod helpers; pub mod http; @@ -50,6 +54,20 @@ pub async fn wait_for_signal() { } } +fn build_sha_short() -> &'static str { + let bin_info = bin_info!(); + if bin_info.commit_sha.len() < 7 { + panic!("unavailable build commit sha") + } + + if bin_info.commit_sha == "VERGEN_IDEMPOTENT_OUTPUT" { + error!("the binary hasn't been built correctly. it doesn't have a commit sha information"); + return "unknown"; + } + + &bin_info.commit_sha[..7] +} + async fn run_api(cli: Cli) -> Result<(), VpnApiError> { // create the tasks let bind_address = cli.bind_address(); @@ -58,14 +76,37 @@ async fn run_api(cli: Cli) -> Result<(), VpnApiError> { let mnemonic = cli.mnemonic; let auth_token = cli.http_auth_token; let webhook_cfg = cli.webhook; - let api_state = ApiState::new(storage.clone(), webhook_cfg, mnemonic).await?; - let http_server = HttpServer::new(bind_address, api_state.clone(), auth_token); + let chain_client = ChainClient::new(mnemonic)?; + let cancellation_token = CancellationToken::new(); + + let deposit_maker = DepositMaker::new( + build_sha_short(), + chain_client.clone(), + cli.max_concurrent_deposits, + cancellation_token.clone(), + ); - let storage_pruner = StoragePruner::new(api_state.cancellation_token(), storage); + let deposit_request_sender = deposit_maker.deposit_request_sender(); + let api_state = ApiState::new( + storage.clone(), + webhook_cfg, + chain_client, + deposit_request_sender, + cancellation_token.clone(), + ) + .await?; + let http_server = HttpServer::new( + bind_address, + api_state.clone(), + auth_token, + cancellation_token.clone(), + ); + let storage_pruner = StoragePruner::new(cancellation_token, storage); // spawn all the tasks api_state.try_spawn(http_server.run_forever()); api_state.try_spawn(storage_pruner.run_forever()); + api_state.try_spawn(deposit_maker.run_forever()); // wait for cancel signal (SIGINT, SIGTERM or SIGQUIT) wait_for_signal().await; @@ -78,10 +119,10 @@ async fn run_api(cli: Cli) -> Result<(), VpnApiError> { #[tokio::main] async fn main() -> anyhow::Result<()> { - std::env::set_var( - "RUST_LOG", - "trace,handlebars=warn,tendermint_rpc=warn,h2=warn,hyper=warn,rustls=warn,reqwest=warn,tungstenite=warn,async_tungstenite=warn,tokio_util=warn,tokio_tungstenite=warn,tokio-util=warn,nym_validator_client=info", - ); + // std::env::set_var( + // "RUST_LOG", + // "trace,handlebars=warn,tendermint_rpc=warn,h2=warn,hyper=warn,rustls=warn,reqwest=warn,tungstenite=warn,async_tungstenite=warn,tokio_util=warn,tokio_tungstenite=warn,tokio-util=warn,axum=warn,sqlx-core=warn,nym_validator_client=info", + // ); let cli = Cli::parse(); cli.webhook.ensure_valid_client_url()?; @@ -90,6 +131,9 @@ async fn main() -> anyhow::Result<()> { setup_env(cli.config_env_file.as_ref()); setup_tracing_logger(); + let bin_info = bin_info_owned!(); + info!("using the following version: {bin_info}"); + run_api(cli).await?; Ok(()) } diff --git a/nym-credential-proxy/nym-credential-proxy/src/storage/mod.rs b/nym-credential-proxy/nym-credential-proxy/src/storage/mod.rs index 9d45149d0cc..6133d1acbc1 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/storage/mod.rs +++ b/nym-credential-proxy/nym-credential-proxy/src/storage/mod.rs @@ -19,7 +19,9 @@ use nym_validator_client::nyxd::Coin; use sqlx::ConnectOptions; use std::fmt::Debug; use std::path::Path; +use std::time::Duration; use time::{Date, OffsetDateTime}; +use tracing::log::LevelFilter; use tracing::{debug, error, info, instrument}; use uuid::Uuid; use zeroize::Zeroizing; @@ -40,9 +42,15 @@ impl VpnApiStorage { let opts = sqlx::sqlite::SqliteConnectOptions::new() .filename(database_path) .create_if_missing(true) - .disable_statement_logging(); + .log_statements(LevelFilter::Trace) + .log_slow_statements(LevelFilter::Warn, Duration::from_millis(250)); - let connection_pool = match sqlx::SqlitePool::connect_with(opts).await { + let pool_opts = sqlx::sqlite::SqlitePoolOptions::new() + .min_connections(5) + .max_connections(25) + .acquire_timeout(Duration::from_secs(60)); + + let connection_pool = match pool_opts.connect_with(opts).await { Ok(db) => db, Err(err) => { error!("Failed to connect to SQLx database: {err}"); diff --git a/nym-credential-proxy/nym-credential-proxy/src/tasks.rs b/nym-credential-proxy/nym-credential-proxy/src/tasks.rs index 6389f584529..f886b7fd440 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/tasks.rs +++ b/nym-credential-proxy/nym-credential-proxy/src/tasks.rs @@ -19,10 +19,11 @@ impl StoragePruner { } pub async fn run_forever(self) { - while !self.cancellation_token.is_cancelled() { + info!("starting the storage pruner task"); + loop { tokio::select! { _ = self.cancellation_token.cancelled() => { - // The token was cancelled, task can shut down + break } _ = tokio::time::sleep(std::time::Duration::from_secs(60 * 60)) => { match self.storage.prune_old_blinded_shares().await { diff --git a/nym-credential-proxy/nym-credential-proxy/src/webhook.rs b/nym-credential-proxy/nym-credential-proxy/src/webhook.rs index 9ebdca250f4..d32f27c9291 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/webhook.rs +++ b/nym-credential-proxy/nym-credential-proxy/src/webhook.rs @@ -5,7 +5,7 @@ use crate::error::VpnApiError; use clap::Args; use reqwest::header::AUTHORIZATION; use serde::Serialize; -use tracing::{debug, error, instrument, span, Level}; +use tracing::{debug, error, instrument, span, Instrument, Level}; use url::Url; use uuid::Uuid; @@ -46,30 +46,33 @@ impl ZkNymWebHookConfig { pub async fn try_trigger(&self, original_uuid: Uuid, payload: &T) { let url = self.unchecked_client_url(); let span = span!(Level::DEBUG, "webhook", uuid = %original_uuid, url = %url); - let _entered = span.enter(); - debug!("🕸️ about to trigger the webhook"); + async move { + debug!("🕸️ about to trigger the webhook"); - match reqwest::Client::new() - .post(url.clone()) - .header(AUTHORIZATION, self.bearer_token()) - .json(payload) - .send() - .await - { - Ok(res) => { - if !res.status().is_success() { - error!("❌🕸️ failed to call webhook: {res:?}"); - } else { - debug!("✅🕸️ webhook triggered successfully: {res:?}"); - if let Ok(body) = res.text().await { - debug!("body = {body}"); + match reqwest::Client::new() + .post(url.clone()) + .header(AUTHORIZATION, self.bearer_token()) + .json(payload) + .send() + .await + { + Ok(res) => { + if !res.status().is_success() { + error!("❌🕸️ failed to call webhook: {res:?}"); + } else { + debug!("✅🕸️ webhook triggered successfully: {res:?}"); + if let Ok(body) = res.text().await { + debug!("body = {body}"); + } } } - } - Err(err) => { - error!("failed to call webhook: {err}") + Err(err) => { + error!("failed to call webhook: {err}") + } } } + .instrument(span) + .await } } diff --git a/nym-node/nym-node-http-api/src/lib.rs b/nym-node/nym-node-http-api/src/lib.rs index 2621a30edab..3363151b0c2 100644 --- a/nym-node/nym-node-http-api/src/lib.rs +++ b/nym-node/nym-node-http-api/src/lib.rs @@ -11,7 +11,6 @@ use std::net::SocketAddr; use tracing::{debug, error}; pub mod error; -pub mod middleware; pub mod router; pub mod state; diff --git a/nym-node/nym-node-http-api/src/middleware/logging.rs b/nym-node/nym-node-http-api/src/middleware/logging.rs deleted file mode 100644 index ae3c5f1d4e9..00000000000 --- a/nym-node/nym-node-http-api/src/middleware/logging.rs +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright 2023-2024 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only - -use axum::{ - extract::{ConnectInfo, Request}, - http::{ - header::{HOST, USER_AGENT}, - HeaderValue, - }, - middleware::Next, - response::IntoResponse, -}; -use colored::*; -use std::net::SocketAddr; -use tracing::info; - -/// Simple logger for requests -pub async fn logger( - ConnectInfo(addr): ConnectInfo, - req: Request, - next: Next, -) -> impl IntoResponse { - let method = req.method().to_string().green(); - let uri = req.uri().to_string().blue(); - let agent = header_map( - req.headers().get(USER_AGENT), - "Unknown User Agent".to_string(), - ); - - let host = header_map(req.headers().get(HOST), "Unknown Host".to_string()); - - let res = next.run(req).await; - let status = res.status(); - let print_status = if status.is_client_error() || status.is_server_error() { - status.to_string().red() - } else if status.is_success() { - status.to_string().green() - } else { - status.to_string().yellow() - }; - - info!(target: "incoming request", "[{addr} -> {host}] {method} '{uri}': {print_status} / agent: {agent}"); - - res -} - -fn header_map(header: Option<&HeaderValue>, msg: String) -> String { - header - .map(|x| x.to_str().unwrap_or(&msg).to_string()) - .unwrap_or(msg) -} diff --git a/nym-node/nym-node-http-api/src/middleware/mod.rs b/nym-node/nym-node-http-api/src/middleware/mod.rs deleted file mode 100644 index 54a67e01478..00000000000 --- a/nym-node/nym-node-http-api/src/middleware/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -// Copyright 2023 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only - -pub mod logging; diff --git a/nym-node/nym-node-http-api/src/router/mod.rs b/nym-node/nym-node-http-api/src/router/mod.rs index 5721274174f..c0487a05aff 100644 --- a/nym-node/nym-node-http-api/src/router/mod.rs +++ b/nym-node/nym-node-http-api/src/router/mod.rs @@ -2,12 +2,12 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::error::NymNodeHttpError; -use crate::middleware::logging; use crate::state::AppState; use crate::NymNodeHTTPServer; use axum::response::Redirect; use axum::routing::get; use axum::Router; +use nym_http_api_common::middleware::logging; use nym_node_requests::api::v1::authenticator::models::Authenticator; use nym_node_requests::api::v1::gateway::models::{Gateway, Wireguard}; use nym_node_requests::api::v1::ip_packet_router::models::IpPacketRouter; From 60c21a8d1da97d682b3ede245d9e691ad7ae8317 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bogdan-=C8=98tefan=20Neac=C5=9Fu?= Date: Mon, 2 Dec 2024 19:52:59 +0200 Subject: [PATCH 04/18] Fix backwards compat mac generation (#5202) --- .../authenticator/src/mixnet_listener.rs | 26 ++++++++++++++----- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/service-providers/authenticator/src/mixnet_listener.rs b/service-providers/authenticator/src/mixnet_listener.rs index 8fbd3e2b294..0c4d8dfb0ff 100644 --- a/service-providers/authenticator/src/mixnet_listener.rs +++ b/service-providers/authenticator/src/mixnet_listener.rs @@ -316,6 +316,7 @@ impl MixnetListener { .filter(|r| r.1.is_none()) .choose(&mut thread_rng()) .ok_or(AuthenticatorError::NoFreeIp)?; + let private_ips = *private_ip_ref.0; // mark it as used, even though it's not final *private_ip_ref.1 = Some(SystemTime::now()); let gateway_data = GatewayClient::new( @@ -337,11 +338,12 @@ impl MixnetListener { v1::response::AuthenticatorResponse::new_pending_registration_success( v1::registration::RegistrationData { nonce: registration_data.nonce, - gateway_data: v1::GatewayClient { - pub_key: gateway_data.pub_key, - private_ip: gateway_data.private_ips.ipv4.into(), - mac: v1::ClientMac::new(gateway_data.mac.to_vec()), - }, + gateway_data: v1::registration::GatewayClient::new( + self.keypair().private_key(), + remote_public.inner(), + private_ips.ipv4.into(), + nonce, + ), wg_port: registration_data.wg_port, }, request_id, @@ -356,7 +358,12 @@ impl MixnetListener { v2::response::AuthenticatorResponse::new_pending_registration_success( v2::registration::RegistrationData { nonce: registration_data.nonce, - gateway_data: registration_data.gateway_data.into(), + gateway_data: v2::registration::GatewayClient::new( + self.keypair().private_key(), + remote_public.inner(), + private_ips.ipv4.into(), + nonce, + ), wg_port: registration_data.wg_port, }, request_id, @@ -371,7 +378,12 @@ impl MixnetListener { v3::response::AuthenticatorResponse::new_pending_registration_success( v3::registration::RegistrationData { nonce: registration_data.nonce, - gateway_data: registration_data.gateway_data.into(), + gateway_data: v3::registration::GatewayClient::new( + self.keypair().private_key(), + remote_public.inner(), + private_ips.ipv4.into(), + nonce, + ), wg_port: registration_data.wg_port, }, request_id, From 29ea4623c8483d4c18a7abc3bba327b3e9323930 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Tue, 3 Dec 2024 11:24:46 +0000 Subject: [PATCH 05/18] adjusted config score penalty calculation (#5206) * adjusted config score penalty calculation * updated contract schema --- .../mixnet-contract/src/types.rs | 77 +++++++++++++++++-- .../mixnet/schema/nym-mixnet-contract.json | 4 +- contracts/mixnet/schema/raw/instantiate.json | 2 +- contracts/mixnet/schema/raw/migrate.json | 2 +- 4 files changed, 76 insertions(+), 9 deletions(-) diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/types.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/types.rs index 802754fd5a6..2a2cd296370 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/types.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/types.rs @@ -253,10 +253,22 @@ impl ConfigScoreParams { 1 }; - major_diff * self.version_weights.major - + minor_diff * self.version_weights.minor - + patch_diff * self.version_weights.patch - + prerelease_diff * self.version_weights.prerelease + // if you're a major version behind, ignore minor and patch and treat it as 0 + if major_diff != 0 { + return major_diff * self.version_weights.major + + expected.minor as u32 * self.version_weights.minor + + expected.patch as u32 * self.version_weights.patch + + prerelease_diff * self.version_weights.prerelease; + } + + // if you're minor version behind, ignore patch and treat is as 0 + if minor_diff != 0 { + return minor_diff * self.version_weights.minor + + expected.patch as u32 * self.version_weights.patch + + prerelease_diff * self.version_weights.prerelease; + } + + patch_diff * self.version_weights.patch + prerelease_diff * self.version_weights.prerelease } } @@ -274,7 +286,7 @@ impl Default for OutdatedVersionWeights { fn default() -> Self { OutdatedVersionWeights { major: 100, - minor: 10, + minor: 1, patch: 1, prerelease: 1, } @@ -314,3 +326,58 @@ impl ConfigScoreParamsUpdate { || self.version_score_formula_params.is_some() } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn versions_behind() { + let weights = OutdatedVersionWeights { + major: 100, + minor: 1, + patch: 1, + prerelease: 1, + }; + let c = ConfigScoreParams { + current_nym_node_semver: "2.2.3".to_string(), + version_weights: weights, + version_score_formula_params: Default::default(), + }; + + // 1 patch behind + assert_eq!(1, c.versions_behind(&"2.2.2".parse().unwrap())); + + // 1 minor behind + assert_eq!(4, c.versions_behind(&"2.1.0".parse().unwrap())); + assert_eq!(4, c.versions_behind(&"2.1.15".parse().unwrap())); + + // 2 patch behind + assert_eq!(2, c.versions_behind(&"2.2.1".parse().unwrap())); + + // 2 minor behind + assert_eq!(5, c.versions_behind(&"2.0.0".parse().unwrap())); + assert_eq!(5, c.versions_behind(&"2.0.123".parse().unwrap())); + + // lying about being 1 patch AHEAD (you're still penalised as if behind) + assert_eq!(1, c.versions_behind(&"2.2.4".parse().unwrap())); + + // major behind + assert_eq!(105, c.versions_behind(&"1.0.0".parse().unwrap())); + assert_eq!(105, c.versions_behind(&"1.2.0".parse().unwrap())); + assert_eq!(105, c.versions_behind(&"1.0.123".parse().unwrap())); + + // different prerelease + let c = ConfigScoreParams { + current_nym_node_semver: "1.2.3-important-patch".to_string(), + version_weights: weights, + version_score_formula_params: Default::default(), + }; + assert_eq!(1, c.versions_behind(&"1.2.3".parse().unwrap())); + + // different prerelease + patch + assert_eq!(2, c.versions_behind(&"1.2.2".parse().unwrap())); + + assert_eq!(5, c.versions_behind(&"1.1.0".parse().unwrap())); + } +} diff --git a/contracts/mixnet/schema/nym-mixnet-contract.json b/contracts/mixnet/schema/nym-mixnet-contract.json index b164efb55f8..306816df999 100644 --- a/contracts/mixnet/schema/nym-mixnet-contract.json +++ b/contracts/mixnet/schema/nym-mixnet-contract.json @@ -72,7 +72,7 @@ "version_score_weights": { "default": { "major": 100, - "minor": 10, + "minor": 1, "patch": 1, "prerelease": 1 }, @@ -3455,7 +3455,7 @@ "version_score_weights": { "default": { "major": 100, - "minor": 10, + "minor": 1, "patch": 1, "prerelease": 1 }, diff --git a/contracts/mixnet/schema/raw/instantiate.json b/contracts/mixnet/schema/raw/instantiate.json index c856dce3129..cbd3717aebe 100644 --- a/contracts/mixnet/schema/raw/instantiate.json +++ b/contracts/mixnet/schema/raw/instantiate.json @@ -68,7 +68,7 @@ "version_score_weights": { "default": { "major": 100, - "minor": 10, + "minor": 1, "patch": 1, "prerelease": 1 }, diff --git a/contracts/mixnet/schema/raw/migrate.json b/contracts/mixnet/schema/raw/migrate.json index 36e279f35e5..ee125fae2fc 100644 --- a/contracts/mixnet/schema/raw/migrate.json +++ b/contracts/mixnet/schema/raw/migrate.json @@ -29,7 +29,7 @@ "version_score_weights": { "default": { "major": 100, - "minor": 10, + "minor": 1, "patch": 1, "prerelease": 1 }, From 78bf413e6ab6458f57fc6cc213ee3de228732141 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Wed, 4 Dec 2024 16:49:26 +0000 Subject: [PATCH 06/18] introduce UNSTABLE endpoints for returning network monitor run details (#5214) --- .../20241204120000_test_run_report.sql | 23 +++ nym-api/nym-api-requests/src/models.rs | 13 ++ nym-api/src/network_monitor/monitor/mod.rs | 30 +++- .../monitor/summary_producer.rs | 136 +++++++++++------- .../handlers/network_monitor.rs | 16 ++- .../src/node_status_api/handlers/unstable.rs | 91 +++++++++++- nym-api/src/support/storage/manager.rs | 77 +++++++++- nym-api/src/support/storage/mod.rs | 60 +++++++- nym-api/src/support/storage/models.rs | 17 +++ 9 files changed, 399 insertions(+), 64 deletions(-) create mode 100644 nym-api/migrations/20241204120000_test_run_report.sql diff --git a/nym-api/migrations/20241204120000_test_run_report.sql b/nym-api/migrations/20241204120000_test_run_report.sql new file mode 100644 index 00000000000..62b820b000d --- /dev/null +++ b/nym-api/migrations/20241204120000_test_run_report.sql @@ -0,0 +1,23 @@ +/* + * Copyright 2024 - Nym Technologies SA + * SPDX-License-Identifier: GPL-3.0-only + */ + +CREATE TABLE monitor_run_report +( + monitor_run_id INTEGER PRIMARY KEY REFERENCES monitor_run (id), + network_reliability FLOAT NOT NULL, + packets_sent INTEGER NOT NULL, + packets_received INTEGER NOT NULL +); + +CREATE TABLE monitor_run_score +( +-- mixnode or gateway + typ TEXT NOT NULL, + monitor_run_id INTEGER NOT NULL REFERENCES monitor_run_report (monitor_run_id), + rounded_score INTEGER NOT NULL, + nodes_count INTEGER NOT NULL +); + +CREATE INDEX monitor_run_score_id ON monitor_run_score (monitor_run_id); \ No newline at end of file diff --git a/nym-api/nym-api-requests/src/models.rs b/nym-api/nym-api-requests/src/models.rs index ee87d37f74e..8465905b629 100644 --- a/nym-api/nym-api-requests/src/models.rs +++ b/nym-api/nym-api-requests/src/models.rs @@ -32,6 +32,7 @@ use schemars::gen::SchemaGenerator; use schemars::schema::{InstanceType, Schema, SchemaObject}; use schemars::JsonSchema; use serde::{Deserialize, Deserializer, Serialize}; +use std::collections::BTreeMap; use std::fmt::{Debug, Display, Formatter}; use std::net::IpAddr; use std::ops::{Deref, DerefMut}; @@ -1255,6 +1256,18 @@ pub struct PartialTestResult { pub type MixnodeTestResultResponse = PaginatedResponse; pub type GatewayTestResultResponse = PaginatedResponse; +#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] +pub struct NetworkMonitorRunDetailsResponse { + pub monitor_run_id: i64, + pub network_reliability: f64, + pub total_sent: usize, + pub total_received: usize, + + // integer score to number of nodes with that score + pub mixnode_results: BTreeMap, + pub gateway_results: BTreeMap, +} + #[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] pub struct NoiseDetails { #[schemars(with = "String")] diff --git a/nym-api/src/network_monitor/monitor/mod.rs b/nym-api/src/network_monitor/monitor/mod.rs index 4d24cb0504d..a5ea5ab818d 100644 --- a/nym-api/src/network_monitor/monitor/mod.rs +++ b/nym-api/src/network_monitor/monitor/mod.rs @@ -4,7 +4,7 @@ use crate::network_monitor::monitor::preparer::PacketPreparer; use crate::network_monitor::monitor::processor::ReceivedProcessor; use crate::network_monitor::monitor::sender::PacketSender; -use crate::network_monitor::monitor::summary_producer::{SummaryProducer, TestSummary}; +use crate::network_monitor::monitor::summary_producer::{SummaryProducer, TestReport, TestSummary}; use crate::network_monitor::test_packet::NodeTestMessage; use crate::network_monitor::test_route::TestRoute; use crate::storage::NymApiStorage; @@ -78,10 +78,10 @@ impl Monitor { // while it might have been cleaner to put this into a separate `Notifier` structure, // I don't see much point considering it's only a single, small, method - async fn submit_new_node_statuses(&mut self, test_summary: TestSummary) { + async fn submit_new_node_statuses(&mut self, test_summary: TestSummary, report: TestReport) { // indicate our run has completed successfully and should be used in any future // uptime calculations - if let Err(err) = self + let monitor_run_id = match self .node_status_storage .insert_monitor_run_results( test_summary.mixnode_results, @@ -94,8 +94,22 @@ impl Monitor { ) .await { - error!("Failed to submit monitor run information to the database: {err}",); + Ok(id) => id, + Err(err) => { + error!("Failed to submit monitor run information to the database: {err}",); + return; + } + }; + + if let Err(err) = self + .node_status_storage + .insert_monitor_run_report(report, monitor_run_id) + .await + { + error!("failed to submit monitor run report to the database: {err}",); } + + info!("finished persisting monitor run with id {monitor_run_id}"); } fn analyse_received_test_route_packets( @@ -279,9 +293,13 @@ impl Monitor { ); let report = summary.create_report(total_sent, total_received); - info!("{report}"); - self.submit_new_node_statuses(summary).await; + let display_report = summary + .create_report(total_sent, total_received) + .to_display_report(&summary.route_results); + info!("{display_report}"); + + self.submit_new_node_statuses(summary, report).await; } async fn test_run(&mut self) { diff --git a/nym-api/src/network_monitor/monitor/summary_producer.rs b/nym-api/src/network_monitor/monitor/summary_producer.rs index 5f8449f75fe..a55212c89c4 100644 --- a/nym-api/src/network_monitor/monitor/summary_producer.rs +++ b/nym-api/src/network_monitor/monitor/summary_producer.rs @@ -32,38 +32,53 @@ impl RouteResult { } } -#[derive(Default, Debug)] +#[derive(Debug)] pub(crate) struct TestReport { - pub(crate) network_reliability: f32, + pub(crate) network_reliability: f64, pub(crate) total_sent: usize, pub(crate) total_received: usize, - pub(crate) route_results: Vec, - - pub(crate) exceptional_mixnodes: usize, - pub(crate) exceptional_gateways: usize, - - pub(crate) fine_mixnodes: usize, - pub(crate) fine_gateways: usize, - - pub(crate) poor_mixnodes: usize, - pub(crate) poor_gateways: usize, - - pub(crate) unreliable_mixnodes: usize, - pub(crate) unreliable_gateways: usize, - - pub(crate) unroutable_mixnodes: usize, - pub(crate) unroutable_gateways: usize, + // integer score to number of nodes with that score + pub(crate) mixnode_results: HashMap, + pub(crate) gateway_results: HashMap, } impl TestReport { - fn new( + pub(crate) fn new( total_sent: usize, total_received: usize, - mixnode_results: &[NodeResult], - gateway_results: &[NodeResult], - route_results: &[RouteResult], + raw_mixnode_results: &[NodeResult], + raw_gateway_results: &[NodeResult], ) -> Self { + let network_reliability = total_received as f64 / total_sent as f64 * 100.0; + + let mut mixnode_results = HashMap::new(); + let mut gateway_results = HashMap::new(); + + for res in raw_mixnode_results { + mixnode_results + .entry(res.reliability) + .and_modify(|c| *c += 1) + .or_insert(1); + } + + for res in raw_gateway_results { + gateway_results + .entry(res.reliability) + .and_modify(|c| *c += 1) + .or_insert(1); + } + + TestReport { + network_reliability, + total_sent, + total_received, + mixnode_results, + gateway_results, + } + } + + pub(crate) fn to_display_report(&self, route_results: &[RouteResult]) -> DisplayTestReport { let mut exceptional_mixnodes = 0; let mut exceptional_gateways = 0; @@ -79,40 +94,38 @@ impl TestReport { let mut unroutable_mixnodes = 0; let mut unroutable_gateways = 0; - for mixnode_result in mixnode_results { - if mixnode_result.reliability >= EXCEPTIONAL_THRESHOLD { - exceptional_mixnodes += 1; - } else if mixnode_result.reliability >= FINE_THRESHOLD { - fine_mixnodes += 1; - } else if mixnode_result.reliability >= POOR_THRESHOLD { - poor_mixnodes += 1; - } else if mixnode_result.reliability >= UNRELIABLE_THRESHOLD { - unreliable_mixnodes += 1; + for (&score, &count) in &self.mixnode_results { + if score >= EXCEPTIONAL_THRESHOLD { + exceptional_mixnodes += count; + } else if score >= FINE_THRESHOLD { + fine_mixnodes += count; + } else if score >= POOR_THRESHOLD { + poor_mixnodes += count; + } else if score >= UNRELIABLE_THRESHOLD { + unreliable_mixnodes += count; } else { - unroutable_mixnodes += 1; + unroutable_mixnodes += count; } } - for gateway_result in gateway_results { - if gateway_result.reliability >= EXCEPTIONAL_THRESHOLD { - exceptional_gateways += 1; - } else if gateway_result.reliability >= FINE_THRESHOLD { - fine_gateways += 1; - } else if gateway_result.reliability >= POOR_THRESHOLD { - poor_gateways += 1; - } else if gateway_result.reliability >= UNRELIABLE_THRESHOLD { - unreliable_gateways += 1; + for (&score, &count) in &self.gateway_results { + if score >= EXCEPTIONAL_THRESHOLD { + exceptional_gateways += count; + } else if score >= FINE_THRESHOLD { + fine_gateways += count; + } else if score >= POOR_THRESHOLD { + poor_gateways += count; + } else if score >= UNRELIABLE_THRESHOLD { + unreliable_gateways += count; } else { - unroutable_gateways += 1; + unroutable_gateways += count; } } - let network_reliability = total_received as f32 / total_sent as f32 * 100.0; - - TestReport { - network_reliability, - total_sent, - total_received, + DisplayTestReport { + network_reliability: self.network_reliability, + total_sent: self.total_sent, + total_received: self.total_received, route_results: route_results.to_vec(), exceptional_mixnodes, exceptional_gateways, @@ -128,7 +141,31 @@ impl TestReport { } } -impl Display for TestReport { +#[derive(Default, Debug)] +pub(crate) struct DisplayTestReport { + pub(crate) network_reliability: f64, + pub(crate) total_sent: usize, + pub(crate) total_received: usize, + + pub(crate) route_results: Vec, + + pub(crate) exceptional_mixnodes: usize, + pub(crate) exceptional_gateways: usize, + + pub(crate) fine_mixnodes: usize, + pub(crate) fine_gateways: usize, + + pub(crate) poor_mixnodes: usize, + pub(crate) poor_gateways: usize, + + pub(crate) unreliable_mixnodes: usize, + pub(crate) unreliable_gateways: usize, + + pub(crate) unroutable_mixnodes: usize, + pub(crate) unroutable_gateways: usize, +} + +impl Display for DisplayTestReport { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { writeln!(f, "Mix Network Test Report")?; writeln!( @@ -218,7 +255,6 @@ impl TestSummary { total_received, &self.mixnode_results, &self.gateway_results, - &self.route_results, ) } } diff --git a/nym-api/src/node_status_api/handlers/network_monitor.rs b/nym-api/src/node_status_api/handlers/network_monitor.rs index 5f71eac079c..dbd37200894 100644 --- a/nym-api/src/node_status_api/handlers/network_monitor.rs +++ b/nym-api/src/node_status_api/handlers/network_monitor.rs @@ -1,6 +1,8 @@ // Copyright 2021-2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only +use super::unstable; +use crate::node_status_api::handlers::unstable::{latest_monitor_run_report, monitor_run_report}; use crate::node_status_api::handlers::MixIdParam; use crate::node_status_api::helpers::{ _compute_mixnode_reward_estimation, _gateway_core_status_count, _gateway_report, @@ -23,8 +25,6 @@ use nym_api_requests::models::{ use serde::Deserialize; use utoipa::IntoParams; -use super::unstable; - // we want to mark the routes as deprecated in swagger, but still expose them #[allow(deprecated)] pub(super) fn network_monitor_routes() -> Router { @@ -84,6 +84,18 @@ pub(super) fn network_monitor_routes() -> Router { axum::routing::get(unstable::gateway_test_results), ), ) + .nest( + "/network-monitor/unstable", + Router::new() + .route( + "/run/:monitor_run_id/details", + axum::routing::get(monitor_run_report), + ) + .route( + "/run/latest/details", + axum::routing::get(latest_monitor_run_report), + ), + ) } #[utoipa::path( diff --git a/nym-api/src/node_status_api/handlers/unstable.rs b/nym-api/src/node_status_api/handlers/unstable.rs index 2b72f729084..1378b6a055c 100644 --- a/nym-api/src/node_status_api/handlers/unstable.rs +++ b/nym-api/src/node_status_api/handlers/unstable.rs @@ -6,15 +6,17 @@ use crate::node_status_api::models::{AxumErrorResponse, AxumResult}; use crate::support::http::helpers::PaginationRequest; use crate::support::http::state::AppState; use crate::support::storage::NymApiStorage; +use anyhow::bail; use axum::extract::{Path, Query, State}; use axum::Json; use nym_api_requests::models::{ - GatewayTestResultResponse, MixnodeTestResultResponse, PartialTestResult, TestNode, TestRoute, + GatewayTestResultResponse, MixnodeTestResultResponse, NetworkMonitorRunDetailsResponse, + PartialTestResult, TestNode, TestRoute, }; use nym_api_requests::pagination::Pagination; use nym_mixnet_contract_common::NodeId; use std::cmp::min; -use std::collections::HashMap; +use std::collections::{BTreeMap, HashMap}; use std::sync::Arc; use tokio::sync::RwLock; use tracing::{error, trace}; @@ -301,3 +303,88 @@ pub async fn gateway_test_results( ))), } } + +async fn _monitor_run_report( + monitor_run_id: i64, + storage: &NymApiStorage, +) -> anyhow::Result { + let Some((raw_report, raw_scores)) = storage.get_monitor_run_report(monitor_run_id).await? + else { + bail!("no results found for monitor run {monitor_run_id}"); + }; + + let mut mixnode_results = BTreeMap::new(); + let mut gateway_results = BTreeMap::new(); + + for score in raw_scores { + if score.typ == "mixnode" { + mixnode_results.insert(score.rounded_score, score.nodes_count as usize); + } else if score.typ == "gateway" { + gateway_results.insert(score.rounded_score, score.nodes_count as usize); + } + } + + Ok(NetworkMonitorRunDetailsResponse { + monitor_run_id, + network_reliability: raw_report.network_reliability, + total_sent: raw_report.packets_sent as usize, + total_received: raw_report.packets_received as usize, + mixnode_results, + gateway_results, + }) +} + +async fn _latest_monitor_run_report( + storage: &NymApiStorage, +) -> anyhow::Result { + let Some(latest_id) = storage.get_latest_monitor_run_id().await? else { + bail!("no network monitor run found"); + }; + + _monitor_run_report(latest_id, storage).await +} + +#[utoipa::path( + tag = "UNSTABLE - DO **NOT** USE", + get, + params( + PaginationRequest + ), + path = "/v1/status/network-monitor/unstable/run/{monitor_run_id}/details", + responses( + (status = 200, body = NetworkMonitorRunDetailsResponse) + ) +)] +pub async fn monitor_run_report( + Path(monitor_run_id): Path, + State(state): State, +) -> AxumResult> { + match _monitor_run_report(monitor_run_id, state.storage()).await { + Ok(res) => Ok(Json(res)), + Err(err) => Err(AxumErrorResponse::internal_msg(format!( + "failed to retrieve monitor run report for run {monitor_run_id}: {err}" + ))), + } +} + +#[utoipa::path( + tag = "UNSTABLE - DO **NOT** USE", + get, + params( + PaginationRequest + ), + path = "/v1/status/network-monitor/unstable/run/latest/details", + responses( + (status = 200, body = NetworkMonitorRunDetailsResponse) + ) +)] +pub async fn latest_monitor_run_report( + State(state): State, +) -> AxumResult> { + match _latest_monitor_run_report(state.storage()).await { + Ok(res) => Ok(Json(res)), + Err(err) => Err(AxumErrorResponse::internal_msg(format!( + "failed to retrieve the latest monitor run report: {err}" + ))), + } +} diff --git a/nym-api/src/support/storage/manager.rs b/nym-api/src/support/storage/manager.rs index f34ddb4045c..e30b316c0d9 100644 --- a/nym-api/src/support/storage/manager.rs +++ b/nym-api/src/support/storage/manager.rs @@ -4,8 +4,9 @@ use crate::node_status_api::models::{HistoricalUptime as ApiHistoricalUptime, Uptime}; use crate::node_status_api::utils::{ActiveGatewayStatuses, ActiveMixnodeStatuses}; use crate::support::storage::models::{ - ActiveGateway, ActiveMixnode, GatewayDetails, HistoricalUptime, MixnodeDetails, NodeStatus, - RewardingReport, TestedGatewayStatus, TestedMixnodeStatus, TestingRoute, + ActiveGateway, ActiveMixnode, GatewayDetails, HistoricalUptime, MixnodeDetails, + MonitorRunReport, MonitorRunScore, NodeStatus, RewardingReport, TestedGatewayStatus, + TestedMixnodeStatus, TestingRoute, }; use crate::support::storage::DbIdCache; use nym_mixnet_contract_common::{EpochId, IdentityKey, NodeId}; @@ -883,6 +884,78 @@ impl StorageManager { Ok(res.last_insert_rowid()) } + pub(super) async fn insert_monitor_run_report( + &self, + monitor_run_id: i64, + network_reliability: f64, + total_packets_sent: u32, + total_packets_received: u32, + ) -> Result<(), sqlx::Error> { + sqlx::query!( + r#" + INSERT INTO monitor_run_report( + monitor_run_id, + network_reliability, + packets_sent, + packets_received + ) VALUES (?, ?, ?, ?) + "#, + monitor_run_id, + network_reliability, + total_packets_sent, + total_packets_received + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + pub(super) async fn get_monitor_run_report( + &self, + monitor_run_id: i64, + ) -> Result, sqlx::Error> { + sqlx::query_as("SELECT * FROM monitor_run_report WHERE monitor_run_id = ?") + .bind(monitor_run_id) + .fetch_optional(&self.connection_pool) + .await + } + + pub(super) async fn get_latest_monitor_run_id(&self) -> Result, sqlx::Error> { + sqlx::query!("SELECT id from monitor_run ORDER BY id DESC limit 1") + .fetch_optional(&self.connection_pool) + .await + .map(|r| r.map(|r| r.id)) + } + + pub(super) async fn insert_monitor_run_scores( + &self, + scores: Vec, + ) -> Result<(), sqlx::Error> { + let mut query_builder = sqlx::QueryBuilder::new( + "INSERT INTO monitor_run_score (typ, monitor_run_id, rounded_score, nodes_count) ", + ); + + query_builder.push_values(scores, |mut b, score| { + b.push_bind(score.typ) + .push_bind(score.monitor_run_id) + .push_bind(score.rounded_score) + .push_bind(score.nodes_count); + }); + + query_builder.build().execute(&self.connection_pool).await?; + Ok(()) + } + + pub(super) async fn get_monitor_run_scores( + &self, + monitor_run_id: i64, + ) -> Result, sqlx::Error> { + sqlx::query_as("SELECT * FROM monitor_run_score WHERE monitor_run_id = ?") + .bind(monitor_run_id) + .fetch_all(&self.connection_pool) + .await + } + /// Obtains number of network monitor test runs that have occurred within the specified interval. /// /// # Arguments diff --git a/nym-api/src/support/storage/mod.rs b/nym-api/src/support/storage/mod.rs index b89062d09a2..72ff9aea1df 100644 --- a/nym-api/src/support/storage/mod.rs +++ b/nym-api/src/support/storage/mod.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: GPL-3.0-only use self::manager::{AvgGatewayReliability, AvgMixnodeReliability}; +use crate::network_monitor::monitor::summary_producer::TestReport; use crate::network_monitor::test_route::TestRoute; use crate::node_status_api::models::{ GatewayStatusReport, GatewayUptimeHistory, HistoricalUptime as ApiHistoricalUptime, @@ -11,7 +12,8 @@ use crate::node_status_api::{ONE_DAY, ONE_HOUR}; use crate::storage::manager::StorageManager; use crate::storage::models::{NodeStatus, TestingRoute}; use crate::support::storage::models::{ - GatewayDetails, HistoricalUptime, MixnodeDetails, TestedGatewayStatus, TestedMixnodeStatus, + GatewayDetails, HistoricalUptime, MixnodeDetails, MonitorRunReport, MonitorRunScore, + TestedGatewayStatus, TestedMixnodeStatus, }; use dashmap::DashMap; use nym_mixnet_contract_common::NodeId; @@ -730,7 +732,7 @@ impl NymApiStorage { mixnode_results: Vec, gateway_results: Vec, test_routes: Vec, - ) -> Result<(), NymApiStorageError> { + ) -> Result { info!("Submitting new node results to the database. There are {} mixnode results and {} gateway results", mixnode_results.len(), gateway_results.len()); let now = OffsetDateTime::now_utc().unix_timestamp(); @@ -749,9 +751,63 @@ impl NymApiStorage { self.insert_test_route(monitor_run_id, test_route).await?; } + Ok(monitor_run_id) + } + + pub(crate) async fn insert_monitor_run_report( + &self, + report: TestReport, + monitor_run_id: i64, + ) -> Result<(), NymApiStorageError> { + self.manager + .insert_monitor_run_report( + monitor_run_id, + report.network_reliability, + report.total_sent as u32, + report.total_received as u32, + ) + .await?; + + let mut scores = Vec::new(); + for (score, count) in report.mixnode_results { + scores.push(MonitorRunScore { + typ: "mixnode".to_string(), + monitor_run_id, + rounded_score: score, + nodes_count: count as u32, + }) + } + for (score, count) in report.gateway_results { + scores.push(MonitorRunScore { + typ: "gateway".to_string(), + monitor_run_id, + rounded_score: score, + nodes_count: count as u32, + }) + } + + self.manager.insert_monitor_run_scores(scores).await?; + Ok(()) } + pub(crate) async fn get_monitor_run_report( + &self, + monitor_run_id: i64, + ) -> Result)>, NymApiStorageError> { + let Some(report) = self.manager.get_monitor_run_report(monitor_run_id).await? else { + return Ok(None); + }; + let scores = self.manager.get_monitor_run_scores(monitor_run_id).await?; + Ok(Some((report, scores))) + } + + pub(crate) async fn get_latest_monitor_run_id( + &self, + ) -> Result, NymApiStorageError> { + Ok(self.manager.get_latest_monitor_run_id().await?) + } + pub(crate) async fn submit_mixnode_statuses_v2( &self, mixnode_results: &[NodeResult], diff --git a/nym-api/src/support/storage/models.rs b/nym-api/src/support/storage/models.rs index 1418cab9f87..e082961b01a 100644 --- a/nym-api/src/support/storage/models.rs +++ b/nym-api/src/support/storage/models.rs @@ -6,6 +6,23 @@ use nym_mixnet_contract_common::NodeId; use sqlx::FromRow; use time::Date; +#[derive(sqlx::FromRow, Debug, Clone, Copy)] +pub(crate) struct MonitorRunReport { + #[allow(dead_code)] + pub(crate) monitor_run_id: i64, + pub(crate) network_reliability: f64, + pub(crate) packets_sent: i64, + pub(crate) packets_received: i64, +} + +#[derive(sqlx::FromRow, Debug, Clone)] +pub(crate) struct MonitorRunScore { + pub(crate) typ: String, + pub(crate) monitor_run_id: i64, + pub(crate) rounded_score: u8, + pub(crate) nodes_count: u32, +} + // Internally used struct to catch results from the database to calculate uptimes for given mixnode/gateway pub(crate) struct NodeStatus { pub timestamp: Option, From 15c30121999520d8b8c112bc7a994453f5ef0580 Mon Sep 17 00:00:00 2001 From: Mark Sinclair <14054343+mmsinclair@users.noreply.github.com> Date: Thu, 5 Dec 2024 08:17:30 +0000 Subject: [PATCH 07/18] explorer-api: add nym node endpoints + UI to show nym-nodes and account balances (#5183) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * explorer-api: add nym node endpoints + UI to show nym-nodes and account balances * explorer-api: add endpoints to get operator rewards explorer-ui: show delegations on nym-nodes, show operator rewards, bug fixes * explorer-ui: change summary screen to only show nym-node stats * explorer-api: add unstable routes to get legacy mixnodes and gateways from the contract instead of the Nym API explorer-ui: adapt front-end to show less information in legacy nodes with plain bond types * explorer-ui: fix up source of legacy mixnode data * explorer-ui: add more account page null and undefined checks * explorer-ui: filter out null gateway versions * explorer-ui: sanitise gateway versions * explorer-ui: add more guards on the balance parts to check that greater than 0 * explorer-api: make /tmp/unstable/gateways endpoint compatible with the current Harbour Master API * explorer-ui: fix typo * cargo fmt * Add node-id, total stake and links to nodes list --------- Co-authored-by: Mark Sinclair Co-authored-by: Jędrzej Stuczyński --- .../validator-client/src/client.rs | 11 +- .../validator-client/src/nym_api/mod.rs | 17 + explorer-api/explorer-api-requests/src/lib.rs | 35 +- .../src/country_statistics/distribution.rs | 12 +- explorer-api/src/gateways/models.rs | 56 ++- explorer-api/src/http/mod.rs | 4 +- explorer-api/src/main.rs | 2 +- explorer-api/src/mix_node/http.rs | 2 +- explorer-api/src/mix_nodes/models.rs | 33 +- explorer-api/src/nym_nodes/http.rs | 26 -- explorer-api/src/overview/http.rs | 35 +- explorer-api/src/overview/models.rs | 15 + explorer-api/src/state.rs | 156 ++++++- explorer-api/src/tasks.rs | 52 ++- explorer-api/src/unstable/http.rs | 258 +++++++++++ .../src/{nym_nodes => unstable}/location.rs | 0 .../src/{nym_nodes => unstable}/mod.rs | 0 .../src/{nym_nodes => unstable}/models.rs | 58 ++- explorer-nextjs/app/account/[id]/page.tsx | 407 ++++++++++++++++++ explorer-nextjs/app/api/constants.ts | 13 +- explorer-nextjs/app/api/index.ts | 74 +++- .../app/components/DetailTable.tsx | 5 +- .../app/components/Gateways/Gateways.ts | 10 +- .../app/components/Nav/DesktopNav.tsx | 4 + .../app/components/Nav/MobileNav.tsx | 11 +- explorer-nextjs/app/components/Nav/Search.tsx | 76 ++++ explorer-nextjs/app/components/StyledLink.tsx | 2 +- explorer-nextjs/app/context/main.tsx | 55 ++- explorer-nextjs/app/context/nav.tsx | 16 +- explorer-nextjs/app/context/node.tsx | 77 ++++ .../network-components/gateways/[id]/page.tsx | 21 +- .../app/network-components/gateways/page.tsx | 197 ++------- .../network-components/mixnodes/[id]/page.tsx | 7 +- .../app/network-components/mixnodes/page.tsx | 24 +- .../network-components/nodes/DeclaredRole.tsx | 11 + .../nodes/[id]/NodeDelegationsTable.tsx | 93 ++++ .../network-components/nodes/[id]/page.tsx | 278 ++++++++++++ .../app/network-components/nodes/page.tsx | 254 +++++++++++ .../service-providers/page.tsx | 140 ------ explorer-nextjs/app/page.tsx | 82 ++-- explorer-nextjs/app/typeDefs/explorer-api.ts | 19 +- explorer-nextjs/app/utils/currency.ts | 10 + explorer-nextjs/package.json | 3 +- package.json | 5 +- yarn.lock | 165 ++++++- 45 files changed, 2326 insertions(+), 505 deletions(-) delete mode 100644 explorer-api/src/nym_nodes/http.rs create mode 100644 explorer-api/src/unstable/http.rs rename explorer-api/src/{nym_nodes => unstable}/location.rs (100%) rename explorer-api/src/{nym_nodes => unstable}/mod.rs (100%) rename explorer-api/src/{nym_nodes => unstable}/models.rs (72%) create mode 100644 explorer-nextjs/app/account/[id]/page.tsx create mode 100644 explorer-nextjs/app/components/Nav/Search.tsx create mode 100644 explorer-nextjs/app/context/node.tsx create mode 100644 explorer-nextjs/app/network-components/nodes/DeclaredRole.tsx create mode 100644 explorer-nextjs/app/network-components/nodes/[id]/NodeDelegationsTable.tsx create mode 100644 explorer-nextjs/app/network-components/nodes/[id]/page.tsx create mode 100644 explorer-nextjs/app/network-components/nodes/page.tsx delete mode 100644 explorer-nextjs/app/network-components/service-providers/page.tsx diff --git a/common/client-libs/validator-client/src/client.rs b/common/client-libs/validator-client/src/client.rs index 076421bc6ad..93db55e6dd3 100644 --- a/common/client-libs/validator-client/src/client.rs +++ b/common/client-libs/validator-client/src/client.rs @@ -19,8 +19,8 @@ use nym_api_requests::ecash::{ PartialExpirationDateSignatureResponse, VerificationKeyResponse, }; use nym_api_requests::models::{ - ApiHealthResponse, GatewayCoreStatusResponse, MixnodeCoreStatusResponse, MixnodeStatusResponse, - NymNodeDescription, RewardEstimationResponse, StakeSaturationResponse, + ApiHealthResponse, GatewayBondAnnotated, GatewayCoreStatusResponse, MixnodeCoreStatusResponse, + MixnodeStatusResponse, NymNodeDescription, RewardEstimationResponse, StakeSaturationResponse, }; use nym_api_requests::models::{LegacyDescribedGateway, MixNodeBondAnnotated}; use nym_api_requests::nym_nodes::SkimmedNode; @@ -257,6 +257,13 @@ impl Client { Ok(self.nym_api.get_gateways().await?) } + #[deprecated] + pub async fn get_cached_gateways_detailed_unfiltered( + &self, + ) -> Result, ValidatorClientError> { + Ok(self.nym_api.get_gateways_detailed_unfiltered().await?) + } + // TODO: combine with NymApiClient... pub async fn get_all_cached_described_nodes( &self, 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 7e54b3f9ec8..f7c13682670 100644 --- a/common/client-libs/validator-client/src/nym_api/mod.rs +++ b/common/client-libs/validator-client/src/nym_api/mod.rs @@ -102,6 +102,23 @@ pub trait NymApiClientExt: ApiClient { .await } + #[deprecated] + #[instrument(level = "debug", skip(self))] + async fn get_gateways_detailed_unfiltered( + &self, + ) -> Result, NymAPIError> { + self.get_json( + &[ + routes::API_VERSION, + routes::STATUS, + routes::GATEWAYS, + routes::DETAILED_UNFILTERED, + ], + NO_PARAMS, + ) + .await + } + #[deprecated] #[instrument(level = "debug", skip(self))] async fn get_mixnodes_detailed_unfiltered( diff --git a/explorer-api/explorer-api-requests/src/lib.rs b/explorer-api/explorer-api-requests/src/lib.rs index 3ea6ed72009..0eddf354263 100644 --- a/explorer-api/explorer-api-requests/src/lib.rs +++ b/explorer-api/explorer-api-requests/src/lib.rs @@ -1,6 +1,8 @@ -use nym_api_requests::models::NodePerformance; +use nym_api_requests::models::{DescribedNodeType, NodePerformance, NymNodeData}; use nym_contracts_common::Percent; -use nym_mixnet_contract_common::{Addr, Coin, Gateway, LegacyMixLayer, MixNode, NodeId}; +use nym_mixnet_contract_common::{ + Addr, Coin, Delegation, Gateway, LegacyMixLayer, MixNode, NodeId, NodeRewarding, NymNodeBond, +}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -52,3 +54,32 @@ pub struct PrettyDetailedGatewayBond { pub proxy: Option, pub location: Option, } + +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] +pub struct NymNodeWithDescriptionAndLocation { + pub node_id: NodeId, + pub contract_node_type: Option, + pub description: Option, + pub bond_information: NymNodeBond, + pub rewarding_details: NodeRewarding, + pub location: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] +pub struct NymNodeWithDescriptionAndLocationAndDelegations { + pub node_id: NodeId, + pub contract_node_type: Option, + pub description: Option, + pub bond_information: NymNodeBond, + pub rewarding_details: NodeRewarding, + pub location: Option, + pub delegations: Option>, +} + +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] +pub struct NymVestingAccount { + pub locked: Coin, + pub vested: Coin, + pub vesting: Coin, + pub spendable: Coin, +} diff --git a/explorer-api/src/country_statistics/distribution.rs b/explorer-api/src/country_statistics/distribution.rs index f834b831c72..2625952353e 100644 --- a/explorer-api/src/country_statistics/distribution.rs +++ b/explorer-api/src/country_statistics/distribution.rs @@ -38,8 +38,14 @@ impl CountryStatisticsDistributionTask { /// Retrieves the current list of mixnodes from the validators and calculates how many nodes are in each country async fn calculate_nodes_per_country(&mut self) { let cache = self.state.inner.mixnodes.get_locations().await; + let nym_nodes = self + .state + .inner + .nymnodes + .get_bonded_nymnodes_locations() + .await; - let three_letter_iso_country_codes: Vec = cache + let mut three_letter_iso_country_codes: Vec = cache .values() .flat_map(|i| { i.location @@ -48,6 +54,10 @@ impl CountryStatisticsDistributionTask { }) .collect(); + for node in nym_nodes { + three_letter_iso_country_codes.push(node.three_letter_iso_country_code); + } + let mut distribution = CountryNodesDistribution::new(); info!("Calculating country distribution from located mixnodes..."); diff --git a/explorer-api/src/gateways/models.rs b/explorer-api/src/gateways/models.rs index a3ed4060672..d2298b2a0c2 100644 --- a/explorer-api/src/gateways/models.rs +++ b/explorer-api/src/gateways/models.rs @@ -2,8 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 use crate::{cache::Cache, location::LocationCacheItem}; +use nym_contracts_common::IdentityKey; use nym_explorer_api_requests::{Location, PrettyDetailedGatewayBond}; -use nym_mixnet_contract_common::{GatewayBond, IdentityKey}; +use nym_mixnet_contract_common::GatewayBond; +use nym_validator_client::models::GatewayBondAnnotated; use serde::Serialize; use std::{sync::Arc, time::SystemTime}; use tokio::sync::RwLock; @@ -11,7 +13,7 @@ use tokio::sync::RwLock; use super::location::GatewayLocationCache; pub(crate) struct GatewayCache { - pub(crate) gateways: Cache, + pub(crate) gateways: Cache, } #[derive(Clone, Debug, Serialize, JsonSchema)] @@ -22,6 +24,7 @@ pub(crate) struct GatewaySummary { #[derive(Clone)] pub(crate) struct ThreadsafeGatewayCache { gateways: Arc>, + legacy_gateway_bonds: Arc>, locations: Arc>, } @@ -31,6 +34,9 @@ impl ThreadsafeGatewayCache { gateways: Arc::new(RwLock::new(GatewayCache { gateways: Cache::new(), })), + legacy_gateway_bonds: Arc::new(RwLock::new(GatewayCache { + gateways: Cache::new(), + })), locations: Arc::new(RwLock::new(GatewayLocationCache::new())), } } @@ -51,7 +57,14 @@ impl ThreadsafeGatewayCache { } pub(crate) async fn get_gateways(&self) -> Vec { - self.gateways.read().await.gateways.get_all() + self.gateways + .read() + .await + .gateways + .get_all() + .iter() + .map(|g| g.gateway_bond.bond.clone()) + .collect() } pub(crate) async fn get_detailed_gateways(&self) -> Vec { @@ -64,7 +77,22 @@ impl ThreadsafeGatewayCache { .iter() .map(|bond| { let location = location_guard.get(bond.identity()); - self.create_detailed_gateway(bond.to_owned(), location) + self.create_detailed_gateway(bond.gateway_bond.bond.to_owned(), location) + }) + .collect() + } + + pub(crate) async fn get_legacy_detailed_gateways(&self) -> Vec { + let legacy_gateways = self.legacy_gateway_bonds.read().await; + let location_guard = self.locations.read().await; + + legacy_gateways + .gateways + .get_all() + .iter() + .map(|bond| { + let location = location_guard.get(bond.identity()); + self.create_detailed_gateway(bond.gateway_bond.bond.to_owned(), location) }) .collect() } @@ -80,6 +108,9 @@ impl ThreadsafeGatewayCache { gateways: Arc::new(RwLock::new(GatewayCache { gateways: Cache::new(), })), + legacy_gateway_bonds: Arc::new(RwLock::new(GatewayCache { + gateways: Cache::new(), + })), locations: Arc::new(RwLock::new(locations)), } } @@ -106,13 +137,26 @@ impl ThreadsafeGatewayCache { .insert(identy_key, LocationCacheItem::new_from_location(location)); } - pub(crate) async fn update_cache(&self, gateways: Vec) { + pub(crate) async fn update_cache( + &self, + gateways: Vec, + legacy_gateway_bonds: Vec, + ) { let mut guard = self.gateways.write().await; + let mut guard_legacy_gateways = self.legacy_gateway_bonds.write().await; for gateway in gateways { guard .gateways - .set(gateway.gateway.identity_key.clone(), gateway) + .set(gateway.gateway_bond.gateway.identity_key.clone(), gateway) + } + + for legacy_gateway in legacy_gateway_bonds { + if let Some(g) = guard.gateways.get(&legacy_gateway.gateway.identity_key) { + guard_legacy_gateways + .gateways + .set(legacy_gateway.gateway.identity_key, g.clone()); + } } } } diff --git a/explorer-api/src/http/mod.rs b/explorer-api/src/http/mod.rs index 2d4e2d9fabf..19802e5fcca 100644 --- a/explorer-api/src/http/mod.rs +++ b/explorer-api/src/http/mod.rs @@ -10,11 +10,11 @@ use crate::gateways::http::gateways_make_default_routes; use crate::http::swagger::get_docs; use crate::mix_node::http::mix_node_make_default_routes; use crate::mix_nodes::http::mix_nodes_make_default_routes; -use crate::nym_nodes::http::unstable_temp_nymnodes_make_default_routes; use crate::overview::http::overview_make_default_routes; use crate::ping::http::ping_make_default_routes; use crate::service_providers::http::service_providers_make_default_routes; use crate::state::ExplorerApiStateContext; +use crate::unstable::http::unstable_temp_make_default_routes; use crate::validators::http::validators_make_default_routes; mod swagger; @@ -59,7 +59,7 @@ fn configure_rocket(state: ExplorerApiStateContext) -> Rocket { "/ping" => ping_make_default_routes(&openapi_settings), "/validators" => validators_make_default_routes(&openapi_settings), "/service-providers" => service_providers_make_default_routes(&openapi_settings), - "/tmp/unstable" => unstable_temp_nymnodes_make_default_routes(&openapi_settings), + "/tmp/unstable" => unstable_temp_make_default_routes(&openapi_settings), }; building_rocket diff --git a/explorer-api/src/main.rs b/explorer-api/src/main.rs index ecaae3f4922..9811eed41ac 100644 --- a/explorer-api/src/main.rs +++ b/explorer-api/src/main.rs @@ -22,12 +22,12 @@ mod http; mod location; mod mix_node; pub(crate) mod mix_nodes; -mod nym_nodes; mod overview; mod ping; pub(crate) mod service_providers; mod state; mod tasks; +mod unstable; mod validators; const COUNTRY_DATA_REFRESH_INTERVAL: u64 = 60 * 15; // every 15 minutes diff --git a/explorer-api/src/mix_node/http.rs b/explorer-api/src/mix_node/http.rs index 2a44097275c..78263d9b132 100644 --- a/explorer-api/src/mix_node/http.rs +++ b/explorer-api/src/mix_node/http.rs @@ -44,7 +44,7 @@ async fn get_mix_node_stats(host: &str, port: u16) -> Result")] pub(crate) async fn get_by_id( mix_id: NodeId, diff --git a/explorer-api/src/mix_nodes/models.rs b/explorer-api/src/mix_nodes/models.rs index 4ffd99304f7..ce5876c9496 100644 --- a/explorer-api/src/mix_nodes/models.rs +++ b/explorer-api/src/mix_nodes/models.rs @@ -7,7 +7,7 @@ use crate::location::LocationCacheItem; use crate::mix_nodes::CACHE_ENTRY_TTL; use nym_explorer_api_requests::{Location, MixnodeStatus, PrettyDetailedMixNodeBond}; use nym_mixnet_contract_common::rewarding::helpers::truncate_reward; -use nym_mixnet_contract_common::NodeId; +use nym_mixnet_contract_common::{MixNodeBond, NodeId}; use nym_validator_client::models::MixNodeBondAnnotated; use serde::Serialize; use std::collections::{HashMap, HashSet}; @@ -80,6 +80,7 @@ impl MixNodesResult { #[derive(Clone)] pub(crate) struct ThreadsafeMixNodesCache { mixnodes: Arc>, + legacy_mixnode_bonds: Arc>, locations: Arc>, } @@ -87,6 +88,7 @@ impl ThreadsafeMixNodesCache { pub(crate) fn new() -> Self { ThreadsafeMixNodesCache { mixnodes: Arc::new(RwLock::new(MixNodesResult::new())), + legacy_mixnode_bonds: Arc::new(RwLock::new(MixNodesResult::new())), locations: Arc::new(RwLock::new(MixnodeLocationCache::new())), } } @@ -94,6 +96,7 @@ impl ThreadsafeMixNodesCache { pub(crate) fn new_with_location_cache(locations: MixnodeLocationCache) -> Self { ThreadsafeMixNodesCache { mixnodes: Arc::new(RwLock::new(MixNodesResult::new())), + legacy_mixnode_bonds: Arc::new(RwLock::new(MixNodesResult::new())), locations: Arc::new(RwLock::new(locations)), } } @@ -188,13 +191,35 @@ impl ThreadsafeMixNodesCache { .collect() } + pub(crate) async fn get_legacy_detailed_mixnodes(&self) -> Vec { + let legacy_mixnodes = self.legacy_mixnode_bonds.read().await; + let location_guard = self.locations.read().await; + + legacy_mixnodes + .all_mixnodes + .values() + .map(|bond| { + let location = location_guard.get(&bond.mix_id()); + self.create_detailed_mixnode(bond.mix_id(), &legacy_mixnodes, location, bond) + }) + .collect() + } + pub(crate) async fn update_cache( &self, all_bonds: Vec, rewarded_nodes: HashSet, active_nodes: HashSet, + legacy_mixnode_bonds: Vec, ) { let mut guard = self.mixnodes.write().await; + let mut guard_legacy_mixnodes = self.legacy_mixnode_bonds.write().await; + + let legacy_mixnode_bond_ids: Vec<&NodeId> = legacy_mixnode_bonds + .iter() + .map(|bond| &bond.mix_id) + .collect(); + guard.all_mixnodes = all_bonds .into_iter() .map(|bond| (bond.mix_id(), bond)) @@ -202,5 +227,11 @@ impl ThreadsafeMixNodesCache { guard.rewarded_mixnodes = rewarded_nodes; guard.active_mixnodes = active_nodes; guard.valid_until = SystemTime::now() + CACHE_ENTRY_TTL; + guard_legacy_mixnodes.all_mixnodes = guard + .all_mixnodes + .clone() + .into_iter() + .filter(|(node_id, _bond)| legacy_mixnode_bond_ids.iter().any(|i| **i == *node_id)) + .collect(); } } diff --git a/explorer-api/src/nym_nodes/http.rs b/explorer-api/src/nym_nodes/http.rs deleted file mode 100644 index 378fcc78d79..00000000000 --- a/explorer-api/src/nym_nodes/http.rs +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::state::ExplorerApiStateContext; -use nym_explorer_api_requests::PrettyDetailedGatewayBond; -use okapi::openapi3::OpenApi; -use rocket::serde::json::Json; -use rocket::{Route, State}; -use rocket_okapi::settings::OpenApiSettings; - -pub fn unstable_temp_nymnodes_make_default_routes( - settings: &OpenApiSettings, -) -> (Vec, OpenApi) { - openapi_get_routes_spec![settings: all_gateways] -} - -#[openapi(tag = "UNSTABLE")] -#[get("/gateways")] -pub(crate) async fn all_gateways( - state: &State, -) -> Json> { - let mut gateways = state.inner.gateways.get_detailed_gateways().await; - gateways.append(&mut state.inner.nymnodes.pretty_gateways().await); - - Json(gateways) -} diff --git a/explorer-api/src/overview/http.rs b/explorer-api/src/overview/http.rs index a5cfb127c58..1ef904918ab 100644 --- a/explorer-api/src/overview/http.rs +++ b/explorer-api/src/overview/http.rs @@ -1,3 +1,4 @@ +use nym_validator_client::models::NymNodeData; use rocket::serde::json::Json; use rocket::{Route, State}; use rocket_okapi::okapi::openapi3::OpenApi; @@ -5,19 +6,51 @@ use rocket_okapi::openapi_get_routes_spec; use rocket_okapi::settings::OpenApiSettings; use crate::mix_nodes::http::get_mixnode_summary; -use crate::overview::models::OverviewSummary; +use crate::overview::models::{NymNodeSummary, OverviewSummary, RoleSummary}; use crate::state::ExplorerApiStateContext; pub fn overview_make_default_routes(settings: &OpenApiSettings) -> (Vec, OpenApi) { openapi_get_routes_spec![settings: summary] } +fn get_nym_nodes_by_role(nodes: &Vec) -> RoleSummary { + let mut summary = RoleSummary::default(); + + for node in nodes { + if node.declared_role.entry { + summary.entry += 1; + } + if node.declared_role.exit_ipr { + summary.exit_ipr += 1; + } + if node.declared_role.exit_nr { + summary.exit_nr += 1; + } + if node.declared_role.mixnode { + summary.mixnode += 1; + } + } + + summary +} + #[openapi(tag = "overview")] #[get("/summary")] pub(crate) async fn summary(state: &State) -> Json { + let nym_nodes = state + .inner + .nymnodes + .get_bonded_nymnodes_descriptions() + .await; + let roles = get_nym_nodes_by_role(&nym_nodes); + Json(OverviewSummary { mixnodes: get_mixnode_summary(state).await, validators: state.inner.validators.get_validator_summary().await, gateways: state.inner.gateways.get_gateway_summary().await, + nymnodes: NymNodeSummary { + count: nym_nodes.len(), + roles, + }, }) } diff --git a/explorer-api/src/overview/models.rs b/explorer-api/src/overview/models.rs index b396e2e3fb0..2edcffaaf8b 100644 --- a/explorer-api/src/overview/models.rs +++ b/explorer-api/src/overview/models.rs @@ -7,9 +7,24 @@ use crate::gateways::models::GatewaySummary; use crate::mix_nodes::models::MixNodeSummary; use crate::validators::models::ValidatorSummary; +#[derive(Clone, Debug, Serialize, JsonSchema, Default)] +pub(crate) struct RoleSummary { + pub mixnode: usize, + pub entry: usize, + pub exit_nr: usize, + pub exit_ipr: usize, +} + +#[derive(Clone, Debug, Serialize, JsonSchema, Default)] +pub(crate) struct NymNodeSummary { + pub count: usize, + pub roles: RoleSummary, +} + #[derive(Clone, Debug, Serialize, JsonSchema)] pub(crate) struct OverviewSummary { pub mixnodes: MixNodeSummary, pub gateways: GatewaySummary, pub validators: ValidatorSummary, + pub nymnodes: NymNodeSummary, } diff --git a/explorer-api/src/state.rs b/explorer-api/src/state.rs index 28373aef809..729e1e95739 100644 --- a/explorer-api/src/state.rs +++ b/explorer-api/src/state.rs @@ -3,12 +3,18 @@ use std::path::Path; use chrono::{DateTime, Utc}; use log::info; -use nym_mixnet_contract_common::NodeId; +use nym_explorer_api_requests::NymVestingAccount; +use nym_mixnet_contract_common::{Addr, Delegation, NodeId, PendingRewardResponse}; use serde::{Deserialize, Serialize}; use crate::client::ThreadsafeValidatorClient; use crate::geo_ip::location::ThreadsafeGeoIp; +use nym_mixnet_contract_common::Coin as CosmWasmCoin; use nym_validator_client::models::MixNodeBondAnnotated; +use nym_validator_client::nyxd::contract_traits::{ + MixnetQueryClient, PagedMixnetQueryClient, VestingQueryClient, +}; +use nym_validator_client::nyxd::{AccountId, Coin, CosmWasmClient}; use crate::country_statistics::country_nodes_distribution::{ CountryNodesDistribution, ThreadsafeCountryNodesDistribution, @@ -18,9 +24,9 @@ use crate::gateways::models::ThreadsafeGatewayCache; use crate::mix_node::models::ThreadsafeMixNodeCache; use crate::mix_nodes::location::MixnodeLocationCache; use crate::mix_nodes::models::ThreadsafeMixNodesCache; -use crate::nym_nodes::location::NymNodeLocationCache; -use crate::nym_nodes::models::ThreadSafeNymNodesCache; use crate::ping::models::ThreadsafePingCache; +use crate::unstable::location::NymNodeLocationCache; +use crate::unstable::models::ThreadSafeNymNodesCache; use crate::validators::models::ThreadsafeValidatorCache; // TODO: change to an environment variable with a default value @@ -45,6 +51,150 @@ impl ExplorerApiState { pub(crate) async fn get_mix_node(&self, mix_id: NodeId) -> Option { self.mixnodes.get_mixnode(mix_id).await } + + pub(crate) async fn get_delegations_by_node( + &self, + node_id: NodeId, + ) -> Result, rocket::response::status::NotFound> { + match self + .validator_client + .0 + .nyxd + .get_all_single_mixnode_delegations(node_id) + .await + { + Ok(res) => Ok(res), + Err(e) => Err(rocket::response::status::NotFound(format!("{}", e))), + } + } + + pub(crate) async fn get_balance( + &self, + addr: &AccountId, + ) -> Result, rocket::response::status::NotFound> { + match self.validator_client.0.nyxd.get_all_balances(addr).await { + Ok(res) => Ok(res), + Err(e) => Err(rocket::response::status::NotFound(format!("{}", e))), + } + } + + pub(crate) async fn get_vesting_balance( + &self, + addr: &AccountId, + ) -> Result, rocket::response::status::NotFound> { + match nym_validator_client::nyxd::contract_traits::VestingQueryClient::get_account( + &self.validator_client.0.nyxd, + addr.as_ref(), + ) + .await + { + // 1. is there a vesting account? + Ok(_res) => { + // 2. there is vesting account, get all the coins + let mut locked = CosmWasmCoin::default(); + let mut vested = CosmWasmCoin::default(); + let mut vesting = CosmWasmCoin::default(); + let mut spendable = CosmWasmCoin::default(); + + // 3. try to get each coin type + if let Ok(coin) = self + .validator_client + .0 + .nyxd + .locked_coins(addr.as_ref(), None) + .await + { + locked = coin.into(); + } + if let Ok(coin) = self + .validator_client + .0 + .nyxd + .vested_coins(addr.as_ref(), None) + .await + { + vested = coin.into(); + } + if let Ok(coin) = self + .validator_client + .0 + .nyxd + .vesting_coins(addr.as_ref(), None) + .await + { + vesting = coin.into(); + } + if let Ok(coin) = self + .validator_client + .0 + .nyxd + .spendable_coins(addr.as_ref(), None) + .await + { + spendable = coin.into(); + } + + // 4.combine into a response + Ok(Some(NymVestingAccount { + locked, + vested, + vesting, + spendable, + })) + } + Err(e) => Err(rocket::response::status::NotFound(format!("{}", e))), + } + } + + pub(crate) async fn get_delegations( + &self, + addr: &AccountId, + ) -> Result, rocket::response::status::NotFound> { + match self + .validator_client + .0 + .nyxd + .get_all_delegator_delegations(addr) + .await + { + Ok(res) => Ok(res), + Err(e) => Err(rocket::response::status::NotFound(format!("{}", e))), + } + } + + pub(crate) async fn get_delegation_rewards( + &self, + addr: &AccountId, + node_id: &NodeId, + proxy: &Option, + ) -> Result> { + match self + .validator_client + .0 + .nyxd + .get_pending_delegator_reward(addr, *node_id, proxy.clone().map(|d| d.to_string())) + .await + { + Ok(res) => Ok(res), + Err(e) => Err(rocket::response::status::NotFound(format!("{}", e))), + } + } + + pub(crate) async fn get_operator_rewards( + &self, + addr: &AccountId, + ) -> Result> { + match self + .validator_client + .0 + .nyxd + .get_pending_operator_reward(addr) + .await + { + Ok(res) => Ok(res), + Err(e) => Err(rocket::response::status::NotFound(format!("{}", e))), + } + } } #[derive(Debug, Serialize, Deserialize)] diff --git a/explorer-api/src/tasks.rs b/explorer-api/src/tasks.rs index fa43bfc9e46..1fa43d98654 100644 --- a/explorer-api/src/tasks.rs +++ b/explorer-api/src/tasks.rs @@ -3,9 +3,12 @@ use crate::mix_nodes::CACHE_REFRESH_RATE; use crate::state::ExplorerApiStateContext; -use nym_mixnet_contract_common::{GatewayBond, NymNodeDetails}; +use nym_mixnet_contract_common::{GatewayBond, MixNodeBond, NymNodeDetails}; use nym_task::TaskClient; -use nym_validator_client::models::{MixNodeBondAnnotated, NymNodeDescription}; +use nym_validator_client::models::{ + GatewayBondAnnotated, MixNodeBondAnnotated, NymNodeDescription, +}; +use nym_validator_client::nyxd::contract_traits::PagedMixnetQueryClient; use nym_validator_client::nyxd::error::NyxdError; use nym_validator_client::nyxd::{Paging, TendermintRpcClient, ValidatorResponse}; use nym_validator_client::{QueryHttpRpcValidatorClient, ValidatorClientError}; @@ -71,13 +74,15 @@ impl ExplorerApiTasks { .await } - async fn retrieve_all_gateways(&self) -> Result, ValidatorClientError> { + async fn retrieve_all_gateways( + &self, + ) -> Result, ValidatorClientError> { info!("About to retrieve all gateways..."); self.state .inner .validator_client .0 - .get_cached_gateways() + .get_cached_gateways_detailed_unfiltered() .await } @@ -115,7 +120,30 @@ impl ExplorerApiTasks { .await } + async fn retrieve_legacy_gateway_bonds(&self) -> Vec { + self.state + .inner + .validator_client + .0 + .nyxd + .get_all_gateways() + .await + .unwrap_or(vec![]) + } + + async fn retrieve_legacy_mixnode_bonds(&self) -> Vec { + self.state + .inner + .validator_client + .0 + .nyxd + .get_all_mixnode_bonds() + .await + .unwrap_or(vec![]) + } + async fn update_mixnode_cache(&self) { + let legacy_mixnode_bonds = self.retrieve_legacy_mixnode_bonds().await; let all_bonds = self.retrieve_all_mixnodes().await; let rewarded_nodes = self .retrieve_rewarded_mixnodes() @@ -132,7 +160,12 @@ impl ExplorerApiTasks { self.state .inner .mixnodes - .update_cache(all_bonds, rewarded_nodes, active_nodes) + .update_cache( + all_bonds, + rewarded_nodes, + active_nodes, + legacy_mixnode_bonds, + ) .await; } @@ -146,8 +179,15 @@ impl ExplorerApiTasks { } async fn update_gateways_cache(&self) { + let legacy_gateway_bonds = self.retrieve_legacy_gateway_bonds().await; match self.retrieve_all_gateways().await { - Ok(response) => self.state.inner.gateways.update_cache(response).await, + Ok(response) => { + self.state + .inner + .gateways + .update_cache(response, legacy_gateway_bonds) + .await + } Err(err) => { error!("Failed to get gateways: {err}") } diff --git a/explorer-api/src/unstable/http.rs b/explorer-api/src/unstable/http.rs new file mode 100644 index 00000000000..46e3f0aa0b2 --- /dev/null +++ b/explorer-api/src/unstable/http.rs @@ -0,0 +1,258 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::state::ExplorerApiStateContext; +use nym_explorer_api_requests::{ + NymNodeWithDescriptionAndLocation, NymNodeWithDescriptionAndLocationAndDelegations, + NymVestingAccount, PrettyDetailedGatewayBond, PrettyDetailedMixNodeBond, +}; +use nym_mixnet_contract_common::{Addr, Coin, NodeId}; +use nym_validator_client::nyxd::AccountId; +use okapi::openapi3::OpenApi; +use rocket::response::status::NotFound; +use rocket::serde::json::Json; +use rocket::serde::{Deserialize, Serialize}; +use rocket::{Route, State}; +use rocket_okapi::settings::OpenApiSettings; +use std::collections::HashMap; +use std::str::FromStr; + +pub fn unstable_temp_make_default_routes(settings: &OpenApiSettings) -> (Vec, OpenApi) { + openapi_get_routes_spec![settings: all_gateways, all_gateway_bonds, all_mixnode_bonds, all_nym_nodes, get_nym_node_by_id, get_account_by_addr] +} + +#[openapi(tag = "UNSTABLE")] +#[get("/gateways")] +pub(crate) async fn all_gateways( + state: &State, +) -> Json> { + let mut gateways = state.inner.gateways.get_legacy_detailed_gateways().await; + let mut nym_node_gateways: Vec = state + .inner + .nymnodes + .pretty_gateways() + .await + .clone() + .into_iter() + .filter(|g| { + !gateways + .iter() + .any(|g2| g.gateway.identity_key == g2.gateway.identity_key) + }) + .collect(); + gateways.append(&mut nym_node_gateways); + + Json(gateways) +} + +#[openapi(tag = "UNSTABLE")] +#[get("/legacy-gateway-bonds")] +pub(crate) async fn all_gateway_bonds( + state: &State, +) -> Json> { + Json(state.inner.gateways.get_legacy_detailed_gateways().await) +} + +#[openapi(tag = "UNSTABLE")] +#[get("/legacy-mixnode-bonds")] +pub(crate) async fn all_mixnode_bonds( + state: &State, +) -> Json> { + Json(state.inner.mixnodes.get_legacy_detailed_mixnodes().await) +} + +#[openapi(tag = "UNSTABLE")] +#[get("/nym-nodes")] +pub(crate) async fn all_nym_nodes( + state: &State, +) -> Json> { + let nodes = state + .inner + .nymnodes + .get_bonded_nymnodes_with_description_and_location() + .await; + Json(nodes.values().cloned().collect()) +} + +#[openapi(tag = "UNSTABLE")] +#[get("/nym-nodes/")] +pub(crate) async fn get_nym_node_by_id( + node_id: NodeId, + state: &State, +) -> Json> { + let nodes = state + .inner + .nymnodes + .get_bonded_nymnodes_with_description_and_location() + .await; + Json(match nodes.get(&node_id).cloned() { + None => None, + Some(node) => { + let delegations = state.inner.get_delegations_by_node(node_id).await.ok(); + Some(NymNodeWithDescriptionAndLocationAndDelegations { + node_id: node.node_id, + contract_node_type: node.contract_node_type, + description: node.description, + bond_information: node.bond_information, + rewarding_details: node.rewarding_details, + location: node.location, + delegations, + }) + } + }) +} + +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] +pub struct NyxAccountDelegationDetails { + pub node_id: NodeId, + pub delegated: Coin, + pub height: u64, + pub proxy: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] +pub struct NyxAccountDelegationRewardDetails { + pub node_id: NodeId, + pub rewards: Coin, + pub amount_staked: Coin, + pub node_still_fully_bonded: bool, +} + +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] +pub struct NyxAccountDetails { + pub address: String, + pub balances: Vec, + pub total_value: Coin, + pub delegations: Vec, + pub accumulated_rewards: Vec, + pub total_delegations: Coin, + pub claimable_rewards: Coin, + pub vesting_account: Option, + pub operator_rewards: Option, +} + +#[openapi(tag = "UNSTABLE")] +#[get("/account/")] +pub(crate) async fn get_account_by_addr( + addr: String, + state: &State, +) -> Result, NotFound> { + match AccountId::from_str(&addr) { + Ok(address) => { + let mut total_value = 0u128; + + // 1. get balances of chain tokens + let balances: Vec = state + .inner + .get_balance(&address) + .await? + .into_iter() + .map(|c| { + if c.denom == "unym" { + total_value += c.amount; + } + c.into() + }) + .collect(); + + // 2. get list of delegations (history) + let delegations: Vec = state + .inner + .get_delegations(&address) + .await? + .into_iter() + .map(|d| NyxAccountDelegationDetails { + delegated: d.amount, + height: d.height, + node_id: d.node_id, + proxy: d.proxy, + }) + .collect(); + + // 3. get the current reward for each active delegation + let mut rewards_map: HashMap<&NodeId, NyxAccountDelegationRewardDetails> = + HashMap::new(); + for d in &delegations { + if rewards_map.contains_key(&d.node_id) { + continue; + } + + if let Ok(r) = state + .inner + .get_delegation_rewards(&address, &d.node_id, &d.proxy) + .await + { + if let Some(rewards) = r.amount_earned { + rewards_map.insert( + &d.node_id, + NyxAccountDelegationRewardDetails { + node_id: d.node_id, + rewards, + amount_staked: r.amount_staked.unwrap_or_default(), + node_still_fully_bonded: r.node_still_fully_bonded, + }, + ); + } + } + } + + // 4. make the map of rewards into a vec and sum the rewards and delegations + let accumulated_rewards: Vec = + rewards_map.values().cloned().collect(); + + let mut claimable_rewards = 0u128; + let mut total_delegations = 0u128; + for r in &accumulated_rewards { + claimable_rewards += r.rewards.amount.u128(); + total_delegations += r.amount_staked.amount.u128(); + total_value += r.rewards.amount.u128(); + total_value += r.amount_staked.amount.u128(); + } + + // 5. get vesting account details (if present) + let vesting_account = state + .inner + .get_vesting_balance(&address) + .await + .unwrap_or_default(); + + if let Some(vesting_account) = vesting_account.clone() { + total_value += vesting_account.locked.amount.u128(); + total_value += vesting_account.spendable.amount.u128(); + } + + // 6. get operator rewards + + let operator_rewards: Option = if let Ok(operator_rewards_res) = + state.inner.get_operator_rewards(&address).await + { + if let Some(operator_reward_amount) = &operator_rewards_res.amount_earned { + total_value += operator_reward_amount.amount.u128(); + } + + operator_rewards_res.amount_earned + } else { + None + }; + + // 7. convert totals + + let claimable_rewards = Coin::new(claimable_rewards, "unym"); + let total_delegations = Coin::new(total_delegations, "unym"); + let total_value = Coin::new(total_value, "unym"); + + Ok(Json(NyxAccountDetails { + address: address.to_string(), + balances, + delegations, + accumulated_rewards, + claimable_rewards, + total_delegations, + total_value, + vesting_account, + operator_rewards, + })) + } + Err(_e) => Err(NotFound("Account not found".to_string())), + } +} diff --git a/explorer-api/src/nym_nodes/location.rs b/explorer-api/src/unstable/location.rs similarity index 100% rename from explorer-api/src/nym_nodes/location.rs rename to explorer-api/src/unstable/location.rs diff --git a/explorer-api/src/nym_nodes/mod.rs b/explorer-api/src/unstable/mod.rs similarity index 100% rename from explorer-api/src/nym_nodes/mod.rs rename to explorer-api/src/unstable/mod.rs diff --git a/explorer-api/src/nym_nodes/models.rs b/explorer-api/src/unstable/models.rs similarity index 72% rename from explorer-api/src/nym_nodes/models.rs rename to explorer-api/src/unstable/models.rs index 7cdbbbdee37..39254d9c071 100644 --- a/explorer-api/src/nym_nodes/models.rs +++ b/explorer-api/src/unstable/models.rs @@ -2,11 +2,13 @@ // SPDX-License-Identifier: Apache-2.0 use crate::location::{LocationCache, LocationCacheItem}; -use crate::nym_nodes::location::NymNodeLocationCache; -use crate::nym_nodes::CACHE_ENTRY_TTL; -use nym_explorer_api_requests::{Location, PrettyDetailedGatewayBond}; +use crate::unstable::location::NymNodeLocationCache; +use crate::unstable::CACHE_ENTRY_TTL; +use nym_explorer_api_requests::{ + Location, NymNodeWithDescriptionAndLocation, PrettyDetailedGatewayBond, +}; use nym_mixnet_contract_common::{Gateway, NodeId, NymNodeDetails}; -use nym_validator_client::models::NymNodeDescription; +use nym_validator_client::models::{NymNodeData, NymNodeDescription}; use std::collections::HashMap; use std::sync::Arc; use std::time::{Duration, SystemTime}; @@ -70,6 +72,54 @@ impl ThreadSafeNymNodesCache { RwLockReadGuard::map(guard, |n| &n.bonded_nym_nodes) } + pub(crate) async fn get_bonded_nymnodes_descriptions(&self) -> Vec { + let guard = self.nymnodes.read().await; + guard + .described_nodes + .values() + .map(|i| i.description.clone()) + .collect() + } + + pub(crate) async fn get_bonded_nymnodes_locations(&self) -> Vec { + let guard_locations = self.locations.read().await; + let mut locations: Vec = vec![]; + for location in guard_locations.values() { + if let Some(l) = &location.location { + locations.push(l.clone()); + } + } + locations + } + + pub(crate) async fn get_bonded_nymnodes_with_description_and_location( + &self, + ) -> HashMap { + let guard_nodes = self.nymnodes.read().await; + let guard_locations = self.locations.read().await; + + let mut map: HashMap = HashMap::new(); + + for (node_id, node) in guard_nodes.bonded_nym_nodes.clone() { + let description = guard_nodes.described_nodes.get(&node_id); + let location = guard_locations.get(&node_id); + + map.insert( + node_id, + NymNodeWithDescriptionAndLocation { + node_id, + description: description.map(|d| d.description.clone()), + location: location.and_then(|l| l.location.clone()), + contract_node_type: description.map(|d| d.contract_node_type), + bond_information: node.bond_information, + rewarding_details: node.rewarding_details, + }, + ); + } + + map + } + pub(crate) async fn get_locations(&self) -> NymNodeLocationCache { self.locations.read().await.clone() } diff --git a/explorer-nextjs/app/account/[id]/page.tsx b/explorer-nextjs/app/account/[id]/page.tsx new file mode 100644 index 00000000000..c0c482476cd --- /dev/null +++ b/explorer-nextjs/app/account/[id]/page.tsx @@ -0,0 +1,407 @@ +'use client' + +import * as React from 'react' +import {Alert, AlertTitle, Box, Button, Chip, CircularProgress, Grid, Tooltip, Typography} from '@mui/material' +import { useParams } from 'next/navigation' +import { useMainContext } from '@/app/context/main' +import { Title } from '@/app/components/Title' +import { MaterialReactTable, MRT_ColumnDef, useMaterialReactTable } from "material-react-table"; +import { useMemo } from "react"; +import { humanReadableCurrencyToString } from "@/app/utils/currency"; +import Table from '@mui/material/Table'; +import TableBody from '@mui/material/TableBody'; +import TableCell from '@mui/material/TableCell'; +import TableContainer from '@mui/material/TableContainer'; +import TableRow from '@mui/material/TableRow'; +import Paper from '@mui/material/Paper'; +import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline'; +import WarningAmberIcon from '@mui/icons-material/WarningAmber'; +import { PieChart } from '@mui/x-charts/PieChart'; +import { useTheme } from "@mui/material/styles"; +import { useIsMobile } from "@/app/hooks"; +import { StyledLink } from "@/app/components"; + +const AccumulatedRewards = ({account}: { account?: any}) => { + const columns = useMemo< + MRT_ColumnDef[] + >(() => { + return [ + { + id: 'accumulated-rewards-data', + header: 'Accumulated Rewards Data', + columns: [ + { + id: 'node_id', + accessorKey: 'node_id', + header: 'Node ID', + size: 150, + Cell: ({ row }) => ( + {row.original.node_id} + ), + }, + { + id: 'node_still_fully_bonded', + accessorKey: 'node_still_fully_bonded', + header: 'Node still bonded?', + width: 150, + Cell: ({ row }) => ( + <>{row.original.node_still_fully_bonded ? : + theme.palette.warning.main }}> + + Unbonded + } + ) + }, + { + id: 'amount_staked', + accessorKey: 'amount_staked', + header: 'Amount', + width: 150, + Cell: ({ row }) => ( + <>{humanReadableCurrencyToString(row.original.amount_staked)} + ) + }, + { + id: 'rewards', + accessorKey: 'rewards', + header: 'Rewards', + width: 150, + Cell: ({ row }) => ( + {humanReadableCurrencyToString(row.original.rewards)} + ) + }, + ], + }, + ] + }, []) + + const table = useMaterialReactTable({ + columns, + data: account?.accumulated_rewards || [], + enableFullScreenToggle: false, + }) + + return (); +} + +const DelegationHistory = ({account}: { account?: any}) => { + const columns = useMemo< + MRT_ColumnDef[] + >(() => { + return [ + { + id: 'delegation-history-data', + header: 'Delegation History', + columns: [ + { + id: 'node_id', + accessorKey: 'node_id', + header: 'Node ID', + size: 150, + }, + { + id: 'delegated', + accessorKey: 'delegated', + header: 'Amount', + width: 150, + Cell: ({ row }) => ( + <>{humanReadableCurrencyToString(row.original.delegated)} + ) + }, + { + id: 'height', + accessorKey: 'height', + header: 'Delegated at height', + width: 150, + Cell: ({ row }) => ( + <>{row.original.height} + ) + }, + ], + }, + ] + }, []) + + const table = useMaterialReactTable({ + columns, + data: account?.delegations || [], + enableFullScreenToggle: false, + }) + + return (); +} + + +/** + * Shows account details + */ +const PageAccountWithState = ({ account }: { + account?: any; +}) => { + const theme = useTheme(); + const isMobile = useIsMobile(); + + const pieChartData = React.useMemo(() => { + if(!account) { + return []; + } + + const parts = []; + + const nymBalance = Number.parseFloat(account.balances.find((b: any) => b.denom === "unym")?.amount || "0") / 1e6; + + if(nymBalance > 0) { + parts.push({label: "Spendable", value: nymBalance, color: theme.palette.primary.main}); + } + + if(account.vesting_account) { + if (`${account.vesting_account.locked?.amount}` !== "0") { + const value = Number.parseFloat(account.vesting_account.locked.amount) / 1e6; + if(value > 0) { + parts.push({ + label: "Vesting locked", + value, + color: 'red' + }); + } + } + if (`${account.vesting_account.spendable?.amount}` !== "0") { + const value = Number.parseFloat(account.vesting_account.spendable.amount) / 1e6; + if(value > 0) { + parts.push({ + label: "Vesting spendable", + value, + color: theme.palette.primary.light + }); + } + } + } + + if (account.claimable_rewards &&`${account.claimable_rewards.amount}` !== "0") { + const value = Number.parseFloat(account.claimable_rewards.amount) / 1e6; + if(value > 0) { + parts.push({ + label: "Claimable delegation rewards", + value, + color: theme.palette.success.light + }); + } + } + if (account.operator_rewards && `${account.operator_rewards.amount}` !== "0") { + const value = Number.parseFloat(account.operator_rewards.amount) / 1e6; + if(value > 0) { + parts.push({ + label: "Claimable operator rewards", + value, + color: theme.palette.success.dark + }); + } + } + if (account.total_delegations && `${account.total_delegations.amount}` !== "0") { + const value = Number.parseFloat(account.total_delegations.amount) / 1e6; + if(value > 0) { + parts.push({ + label: "Total delegations", + value, + color: '#888' + }); + } + } + + return parts; + }, [account]); + + return ( + + + + </Box> + + <Box mt={4} sx={{ maxWidth: "600px" }}> + <PieChart + series={[ + { + data: pieChartData, + innerRadius: 40, + outerRadius: 80, + cy: isMobile ? 200 : undefined, + }, + ]} + height={300} + slotProps={isMobile ? { + legend: { position: { vertical: "top", horizontal: "right" } } + } : undefined} + /> + </Box> + + <Box mt={4}> + <TableContainer component={Paper} sx={{ maxWidth: "400px" }}> + <Table> + <TableBody> + <TableRow sx={{ color: theme => theme.palette.primary.main }}> + <TableCell component="th" scope="row" sx={{ color: "inherit" }}> + <strong>Spendable Balance</strong> + </TableCell> + <TableCell align="right" sx={{ color: "inherit" }}> + {account.balances.map((b: any) => (<strong key={`balance-${b.denom}`}>{humanReadableCurrencyToString(b)}<br/></strong>))} + </TableCell> + </TableRow> + <TableRow> + <TableCell component="th" scope="row"> + Total delegations + </TableCell> + <TableCell align="right"> + {humanReadableCurrencyToString(account.total_delegations)} + </TableCell> + </TableRow> + {account.claimable_rewards && <TableRow sx={{ color: theme => theme.palette.success.light }}> + <TableCell component="th" scope="row" sx={{ color: "inherit" }}> + Claimable delegation rewards + </TableCell> + <TableCell align="right" sx={{ color: "inherit" }}> + {humanReadableCurrencyToString(account.claimable_rewards)} + </TableCell> + </TableRow>} + {account.operator_rewards && `${account.operator_rewards.amount}` !== "0" && <TableRow sx={{ color: theme => theme.palette.success.light }}> + <TableCell component="th" scope="row" sx={{ color: "inherit" }}> + Claimable operator rewards + </TableCell> + <TableCell align="right" sx={{ color: "inherit" }}> + {humanReadableCurrencyToString(account.operator_rewards)} + </TableCell> + </TableRow>} + {account.vesting_account && ( + <> + <TableRow> + <TableCell component="th" scope="row" colSpan={2}> + Vesting account + </TableCell> + </TableRow> + {`${account.vesting_account.locked.amount}` !== "0" && + <TableRow> + <TableCell component="th" scope="row" sx={{ pl: 4 }}> + Locked + </TableCell> + <TableCell align="right" sx={{ color: "inherit" }}> + {humanReadableCurrencyToString(account.vesting_account.locked)} + </TableCell> + </TableRow> + } + {`${account.vesting_account.vested.amount}` !== "0" && + <TableRow> + <TableCell component="th" scope="row" sx={{ pl: 4 }}> + Vested + </TableCell> + <TableCell align="right" sx={{ color: "inherit" }}> + {humanReadableCurrencyToString(account.vesting_account.vested)} + </TableCell> + </TableRow> + } + {`${account.vesting_account.vesting.amount}` !== "0" && + <TableRow> + <TableCell component="th" scope="row" sx={{ pl: 4 }}> + Vesting + </TableCell> + <TableCell align="right" sx={{ color: "inherit" }}> + {humanReadableCurrencyToString(account.vesting_account.vesting)} + </TableCell> + </TableRow> + } + {`${account.vesting_account.spendable.amount}` !== "0" && + <TableRow> + <TableCell component="th" scope="row" sx={{ pl: 4 }}> + Spendable + </TableCell> + <TableCell align="right" sx={{ color: "inherit" }}> + {humanReadableCurrencyToString(account.vesting_account.spendable)} + </TableCell> + </TableRow> + } + </> + )} + <TableRow> + <TableCell component="th" scope="row" sx={{ color: "inherit" }}> + <h3>Total value</h3> + </TableCell> + <TableCell align="right" sx={{ color: "inherit" }}> + <h3>{humanReadableCurrencyToString(account.total_value)}</h3> + </TableCell> + </TableRow> + </TableBody> + </Table> + </TableContainer> + </Box> + <Box mt={4}> + <AccumulatedRewards account={account}/> + </Box> + <Box mt={4}> + <DelegationHistory account={account}/> + </Box> + </Box> + ) +} + +/** + * Guard component to handle loading and not found states + */ +const PageAccountDetailGuard = ({ account } : { account: string }) => { + const [accountDetails, setAccountDetails] = React.useState<any>(); + const [isLoading, setLoading] = React.useState<boolean>(true); + const [error, setError] = React.useState<string>(); + const { fetchAccountById } = useMainContext() + const { id } = useParams() + + React.useEffect(() => { + setLoading(true); + (async () => { + if(typeof(id) === "string") { + try { + const res = await fetchAccountById(account); + setAccountDetails(res); + } catch(e: any) { + setError(e.message); + } + finally { + setLoading(false); + } + } + })(); + }, [id]) + + if (isLoading) { + return <CircularProgress /> + } + + // loaded, but not found + if (error) { + return ( + <Alert severity="warning"> + <AlertTitle>Account not found</AlertTitle> + Sorry, we could not find the account <code>{id || ''}</code> + </Alert> + ) + } + + return <PageAccountWithState account={accountDetails} /> +} + +/** + * Wrapper component that adds the account details based on the `id` in the address URL + */ +const PageAccountDetail = () => { + const { id } = useParams() + + if (!id || typeof id !== 'string') { + return ( + <Alert severity="error">Oh no! Could not find that account</Alert> + ) + } + + return ( + <PageAccountDetailGuard account={id} /> + ) +} + +export default PageAccountDetail diff --git a/explorer-nextjs/app/api/constants.ts b/explorer-nextjs/app/api/constants.ts index e83aca4d027..515655c1840 100644 --- a/explorer-nextjs/app/api/constants.ts +++ b/explorer-nextjs/app/api/constants.ts @@ -5,28 +5,33 @@ export const NYM_API_BASE_URL = process.env.NEXT_PUBLIC_NYM_API_URL || 'https:// export const NYX_RPC_BASE_URL = process.env.NEXT_PUBLIC_NYX_RPC_BASE_URL || 'https://rpc.nymtech.net'; export const VALIDATOR_BASE_URL = process.env.NEXT_PUBLIC_VALIDATOR_URL || 'https://rpc.nymtech.net'; -export const BIG_DIPPER = process.env.NEXT_PUBLIC_BIG_DIPPER_URL || 'https://nym.explorers.guru'; +export const BLOCK_EXPLORER_BASE_URL = process.env.NEXT_PUBLIC_BIG_DIPPER_URL || 'https://nym.explorers.guru'; // specific API routes export const OVERVIEW_API = `${API_BASE_URL}/overview`; export const MIXNODE_PING = `${API_BASE_URL}/ping`; export const MIXNODES_API = `${API_BASE_URL}/mix-nodes`; export const MIXNODE_API = `${API_BASE_URL}/mix-node`; -export const GATEWAYS_EXPLORER_API = `${API_BASE_URL}/gateways`; -export const GATEWAYS_API = `${NYM_API_BASE_URL}/api/v1/status/gateways/detailed`; export const VALIDATORS_API = `${NYX_RPC_BASE_URL}/validators`; export const BLOCK_API = `${NYX_RPC_BASE_URL}/block`; export const COUNTRY_DATA_API = `${API_BASE_URL}/countries`; export const UPTIME_STORY_API = `${NYM_API_BASE_URL}/api/v1/status/mixnode`; // add ID then '/history' to this. export const UPTIME_STORY_API_GATEWAY = `${NYM_API_BASE_URL}/api/v1/status/gateway`; // add ID then '/history' or '/report' to this export const SERVICE_PROVIDERS = `${API_BASE_URL}/service-providers`; +export const TEMP_UNSTABLE_NYM_NODES = `${API_BASE_URL}/tmp/unstable/nym-nodes`; +export const TEMP_UNSTABLE_ACCOUNT = `${API_BASE_URL}/tmp/unstable/account`; +export const NYM_API_NODE_UPTIME = `${NYM_API_BASE_URL}/api/v1/nym-nodes/uptime-history`; +export const NYM_API_NODE_PERFORMANCE = `${NYM_API_BASE_URL}/api/v1/nym-nodes/performance-history`; + +export const LEGACY_MIXNODES_API = `${API_BASE_URL}/tmp/unstable/legacy-mixnode-bonds`; +export const LEGACY_GATEWAYS_API = `${API_BASE_URL}/tmp/unstable/legacy-gateway-bonds`; // errors export const MIXNODE_API_ERROR = "We're having trouble finding that record, please try again or Contact Us."; export const NYM_WEBSITE = 'https://nymtech.net'; -export const NYM_BIG_DIPPER = 'https://mixnet.explorers.guru'; +export const EXPLORER_FOR_ACCOUNTS = ''; // set to empty to use this Nym Explorer and NOT an external one export const NYM_MIXNET_CONTRACT = process.env.NYM_MIXNET_CONTRACT || 'n17srjznxl9dvzdkpwpw24gg668wc73val88a6m5ajg6ankwvz9wtst0cznr'; diff --git a/explorer-nextjs/app/api/index.ts b/explorer-nextjs/app/api/index.ts index a2b134d4aeb..ae190de9b9d 100644 --- a/explorer-nextjs/app/api/index.ts +++ b/explorer-nextjs/app/api/index.ts @@ -3,7 +3,6 @@ import { API_BASE_URL, BLOCK_API, COUNTRY_DATA_API, - GATEWAYS_API, UPTIME_STORY_API_GATEWAY, MIXNODE_API, MIXNODE_PING, @@ -12,7 +11,11 @@ import { UPTIME_STORY_API, VALIDATORS_API, SERVICE_PROVIDERS, - GATEWAYS_EXPLORER_API, + TEMP_UNSTABLE_NYM_NODES, + NYM_API_NODE_UPTIME, + NYM_API_NODE_PERFORMANCE, + TEMP_UNSTABLE_ACCOUNT, + LEGACY_MIXNODES_API, LEGACY_GATEWAYS_API, } from './constants'; import { @@ -59,7 +62,14 @@ export class Api { return cache; } const res = await fetch(`${OVERVIEW_API}/summary`); - const json = await res.json(); + const json: SummaryOverviewResponse = await res.json(); + + if (json.nymnodes?.roles) { + json.mixnodes.count += json.nymnodes.roles.mixnode; + json.gateways.count += json.nymnodes.roles.entry; + json.gateways.count += Math.max(json.nymnodes.roles.exit_ipr, json.nymnodes.roles.exit_nr); + } + storeInCache('overview-summary', JSON.stringify(json)); return json; }; @@ -70,7 +80,7 @@ export class Api { return cachedMixnodes; } - const res = await fetch(MIXNODES_API); + const res = await fetch(LEGACY_MIXNODES_API); const json = await res.json(); storeInCache('mixnodes', JSON.stringify(json)); return json; @@ -98,17 +108,21 @@ export class Api { return response.json(); }; - static fetchGateways = async (): Promise<GatewayBond[]> => { - const res = await fetch(GATEWAYS_API); - const gatewaysAnnotated: GatewayBondAnnotated[] = await res.json(); - const res2 = await fetch(GATEWAYS_EXPLORER_API); - const locatedGateways: LocatedGateway[] = await res2.json(); - const locatedGatewaysByOwner = keyBy(locatedGateways, 'owner'); - return gatewaysAnnotated.map(({ gateway_bond, node_performance }) => ({ - ...gateway_bond, - node_performance, - location: locatedGatewaysByOwner[gateway_bond.owner]?.location, - })); + static fetchGateways = async (): Promise<LocatedGateway[]> => { + // const res = await fetch(GATEWAYS_API); + // const gatewaysAnnotated: GatewayBondAnnotated[] = await res.json(); + // const res2 = await fetch(GATEWAYS_EXPLORER_API); + // const locatedGateways: LocatedGateway[] = await res2.json(); + // const locatedGatewaysByOwner = keyBy(locatedGateways, 'owner'); + // return gatewaysAnnotated.map(({ gateway_bond, node_performance }) => ({ + // ...gateway_bond, + // node_performance, + // location: locatedGatewaysByOwner[gateway_bond.owner]?.location, + // })); + + const res = await fetch(LEGACY_GATEWAYS_API); + const locatedGateways: LocatedGateway[] = await res.json(); + return locatedGateways; }; static fetchGatewayUptimeStoryById = async (id: string): Promise<UptimeStoryResponse> => @@ -165,6 +179,36 @@ export class Api { const json = await res.json(); return json; }; + + static fetchNodes = async () => { + const res = await fetch(TEMP_UNSTABLE_NYM_NODES); + const json = await res.json(); + return json; + } + + static fetchNodeById = async (id: number) => { + const res = await fetch(`${TEMP_UNSTABLE_NYM_NODES}/${id}`); + const json = await res.json(); + return json; + } + + static fetchNymNodeUptimeHistoryById = async (id: number | string) => { + const res = await fetch(`${NYM_API_NODE_UPTIME}/${id}`) + const json = await res.json(); + return json; + } + + static fetchNymNodePerformanceById = async (id: number | string) => { + const res = await fetch(`${NYM_API_NODE_PERFORMANCE}/${id}`) + const json = await res.json(); + return json; + } + + static fetchAccountById = async (id: string) => { + const res = await fetch(`${TEMP_UNSTABLE_ACCOUNT}/${id}`); + const json = await res.json(); + return json; + } } export const getEnvironment = (): Environment => { diff --git a/explorer-nextjs/app/components/DetailTable.tsx b/explorer-nextjs/app/components/DetailTable.tsx index 73868b8ce9e..b068a5c8524 100644 --- a/explorer-nextjs/app/components/DetailTable.tsx +++ b/explorer-nextjs/app/components/DetailTable.tsx @@ -18,6 +18,7 @@ import { unymToNym } from '@/app/utils/currency' import { GatewayEnrichedRowType } from './Gateways/Gateways' import { MixnodeRowType } from './MixNodes' import { StakeSaturationProgressBar } from './MixNodes/Economics/StakeSaturationProgressBar' +import {EXPLORER_FOR_ACCOUNTS} from "@/app/api/constants"; export type ColumnsType = { field: string @@ -57,7 +58,7 @@ function formatCellValues(val: string | number, field: string) { underline="none" color="inherit" target="_blank" - href={`https://mixnet.explorers.guru/account/${val}`} + href={`${EXPLORER_FOR_ACCOUNTS}/account/${val}`} > {val} </Link> @@ -74,7 +75,7 @@ function formatCellValues(val: string | number, field: string) { export const DetailTable: FCWithChildren<{ tableName: string columnsData: ColumnsType[] - rows: MixnodeRowType[] | GatewayEnrichedRowType[] + rows: MixnodeRowType[] | GatewayEnrichedRowType[] | any[] }> = ({ tableName, columnsData, rows }: UniversalTableProps) => { const theme = useTheme() return ( diff --git a/explorer-nextjs/app/components/Gateways/Gateways.ts b/explorer-nextjs/app/components/Gateways/Gateways.ts index d3aead4f13f..f4dbf2743cb 100644 --- a/explorer-nextjs/app/components/Gateways/Gateways.ts +++ b/explorer-nextjs/app/components/Gateways/Gateways.ts @@ -1,4 +1,4 @@ -import { GatewayResponse, GatewayBond, GatewayReportResponse } from '@/app/typeDefs/explorer-api'; +import {GatewayResponse, GatewayBond, GatewayReportResponse, LocatedGateway} from '@/app/typeDefs/explorer-api'; import { toPercentInteger } from '@/app/utils'; export type GatewayRowType = { @@ -9,7 +9,7 @@ export type GatewayRowType = { host: string; location: string; version: string; - node_performance: number; +// node_performance: number; }; export type GatewayEnrichedRowType = GatewayRowType & { @@ -30,11 +30,11 @@ export function gatewayToGridRow(arrayOfGateways: GatewayResponse): GatewayRowTy bond: gw.pledge_amount.amount || 0, host: gw.gateway.host || '', version: gw.gateway.version || '', - node_performance: toPercentInteger(gw.node_performance.last_24h), +// node_performance: toPercentInteger(gw.node_performance.last_24h), })); } -export function gatewayEnrichedToGridRow(gateway: GatewayBond, report: GatewayReportResponse): GatewayEnrichedRowType { +export function gatewayEnrichedToGridRow(gateway: LocatedGateway, report: GatewayReportResponse): GatewayEnrichedRowType { return { id: gateway.owner, owner: gateway.owner, @@ -47,6 +47,6 @@ export function gatewayEnrichedToGridRow(gateway: GatewayBond, report: GatewayRe mixPort: gateway.gateway.mix_port || 0, routingScore: `${report.most_recent}%`, avgUptime: `${report.last_day || report.last_hour}%`, - node_performance: toPercentInteger(gateway.node_performance.most_recent), +// node_performance: toPercentInteger(gateway.node_performance.most_recent), }; } diff --git a/explorer-nextjs/app/components/Nav/DesktopNav.tsx b/explorer-nextjs/app/components/Nav/DesktopNav.tsx index abc4c0901a1..618037ea040 100644 --- a/explorer-nextjs/app/components/Nav/DesktopNav.tsx +++ b/explorer-nextjs/app/components/Nav/DesktopNav.tsx @@ -24,6 +24,7 @@ import { DarkLightSwitchDesktop } from '@/app/components/Switch' import { Footer } from '@/app/components/Footer' import { ConnectKeplrWallet } from '@/app/components/Wallet/ConnectKeplrWallet' import { usePathname, useRouter } from 'next/navigation' +import {SearchToolbar} from "@/app/components/Nav/Search"; const drawerWidth = 255 const bannerHeight = 80 @@ -292,6 +293,9 @@ export const Nav: FCWithChildren = ({ children }) => { display: 'flex', }} > + <Box> + <SearchToolbar/> + </Box> <Box sx={{ display: 'flex', diff --git a/explorer-nextjs/app/components/Nav/MobileNav.tsx b/explorer-nextjs/app/components/Nav/MobileNav.tsx index a2e23e620e9..91cb766ffcc 100644 --- a/explorer-nextjs/app/components/Nav/MobileNav.tsx +++ b/explorer-nextjs/app/components/Nav/MobileNav.tsx @@ -22,6 +22,7 @@ import { ExpandableButton } from './DesktopNav' import { ConnectKeplrWallet } from '../Wallet/ConnectKeplrWallet' import { NetworkTitle } from '../NetworkTitle' import { originalNavOptions } from '@/app/context/nav' +import {SearchToolbar} from "@/app/components/Nav/Search"; export const MobileNav: FCWithChildren = ({ children }) => { const theme = useTheme() @@ -70,7 +71,15 @@ export const MobileNav: FCWithChildren = ({ children }) => { </IconButton> {!isSmallMobile && <NetworkTitle />} </Box> - <ConnectKeplrWallet /> + <Box sx={{ + alignItems: 'center', + display: 'flex', + }}> + <Box mr={0.5}> + <SearchToolbar/> + </Box> + <ConnectKeplrWallet /> + </Box> </Toolbar> </AppBar> <Drawer diff --git a/explorer-nextjs/app/components/Nav/Search.tsx b/explorer-nextjs/app/components/Nav/Search.tsx new file mode 100644 index 00000000000..888125b84a0 --- /dev/null +++ b/explorer-nextjs/app/components/Nav/Search.tsx @@ -0,0 +1,76 @@ +import React from "react"; +import { styled, alpha } from '@mui/material/styles'; +import AppBar from '@mui/material/AppBar'; +import Box from '@mui/material/Box'; +import Toolbar from '@mui/material/Toolbar'; +import IconButton from '@mui/material/IconButton'; +import Typography from '@mui/material/Typography'; +import InputBase from '@mui/material/InputBase'; +import MenuIcon from '@mui/icons-material/Menu'; +import SearchIcon from '@mui/icons-material/Search'; +import {useRouter} from "next/navigation"; + +const Search = styled('div')(({ theme }) => ({ + position: 'relative', + borderRadius: theme.shape.borderRadius, + backgroundColor: alpha(theme.palette.common.white, 0.15), + '&:hover': { + backgroundColor: alpha(theme.palette.common.white, 0.25), + }, + marginLeft: 0, + width: '100%', + [theme.breakpoints.up('sm')]: { + marginLeft: theme.spacing(1), + width: 'auto', + }, +})); + +const SearchIconWrapper = styled('div')(({ theme }) => ({ + padding: theme.spacing(0, 2), + height: '100%', + position: 'absolute', + pointerEvents: 'none', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', +})); + +const StyledInputBase = styled(InputBase)(({ theme }) => ({ + color: 'inherit', + width: '100%', + '& .MuiInputBase-input': { + padding: theme.spacing(1, 1, 1, 0), + // vertical padding + font size from searchIcon + paddingLeft: `calc(1em + ${theme.spacing(4)})`, + [theme.breakpoints.up('sm')]: { + width: '30ch', + }, + }, +})); + +export const SearchToolbar = () => { + const [search, setSearch] = React.useState<string>(); + const router = useRouter(); + const handleSubmit = async (e: React.SyntheticEvent<HTMLFormElement>) => { + e.preventDefault(); + if(search?.trim().length) { + router.push(`/account/${search.trim()}`); + } + } + return ( + <Search> + <SearchIconWrapper> + <SearchIcon /> + </SearchIconWrapper> + <form onSubmit={handleSubmit}> + <StyledInputBase + placeholder="Search for account id…" + inputProps={{ 'aria-label': 'search' }} + onChange={(event: React.ChangeEvent<HTMLInputElement>) => { + setSearch(event.target.value); + }} + /> + </form> + </Search> + ); +} \ No newline at end of file diff --git a/explorer-nextjs/app/components/StyledLink.tsx b/explorer-nextjs/app/components/StyledLink.tsx index 4402b1d0d63..36dd7981807 100644 --- a/explorer-nextjs/app/components/StyledLink.tsx +++ b/explorer-nextjs/app/components/StyledLink.tsx @@ -4,7 +4,7 @@ import Link from 'next/link' type StyledLinkProps = { to: string - children: string + children: React.ReactNode target?: React.HTMLAttributeAnchorTarget dataTestId?: string color?: string diff --git a/explorer-nextjs/app/context/main.tsx b/explorer-nextjs/app/context/main.tsx index 4e2abfb9b74..0494b87749c 100644 --- a/explorer-nextjs/app/context/main.tsx +++ b/explorer-nextjs/app/context/main.tsx @@ -26,6 +26,7 @@ interface StateData { gateways?: ApiState<GatewayResponse> globalError?: string | undefined mixnodes?: ApiState<MixNodeResponse> + nodes?: ApiState<any> mode: PaletteMode validators?: ApiState<ValidatorsResponse> environment?: Environment @@ -37,6 +38,9 @@ interface StateApi { status?: MixnodeStatus ) => Promise<MixNodeResponse | undefined> filterMixnodes: (filters: any, status: any) => void + fetchNodes: () => Promise<any> + fetchNodeById: (id: number) => Promise<any> + fetchAccountById: (accountAddr: string) => Promise<any> toggleMode: () => void } @@ -47,6 +51,9 @@ export const MainContext = React.createContext<State>({ toggleMode: () => undefined, filterMixnodes: () => null, fetchMixnodes: () => Promise.resolve(undefined), + fetchNodes: async () => undefined, + fetchNodeById: async () => undefined, + fetchAccountById: async () => undefined, }) export const useMainContext = (): React.ContextType<typeof MainContext> => @@ -65,6 +72,7 @@ export const MainContextProvider: FCWithChildren = ({ children }) => { // various APIs for Overview page const [summaryOverview, setSummaryOverview] = React.useState<ApiState<SummaryOverviewResponse>>() + const [nodes, setNodes] = React.useState<ApiState<any>>() const [mixnodes, setMixnodes] = React.useState<ApiState<MixNodeResponse>>() const [gateways, setGateways] = React.useState<ApiState<GatewayResponse>>() const [validators, setValidators] = @@ -92,13 +100,11 @@ export const MainContextProvider: FCWithChildren = ({ children }) => { } } - const fetchMixnodes = async (status?: MixnodeStatus) => { + const fetchMixnodes = async () => { let data setMixnodes((d) => ({ ...d, isLoading: true })) try { - data = status - ? await Api.fetchMixnodesActiveSetByStatus(status) - : await Api.fetchMixnodes() + data = await Api.fetchMixnodes() setMixnodes({ data, isLoading: false }) } catch (error) { setMixnodes({ @@ -114,9 +120,7 @@ export const MainContextProvider: FCWithChildren = ({ children }) => { status?: MixnodeStatus ) => { setMixnodes((d) => ({ ...d, isLoading: true })) - const mxns = status - ? await Api.fetchMixnodesActiveSetByStatus(status) - : await Api.fetchMixnodes() + const mxns = await Api.fetchMixnodes() const filtered = mxns?.filter( (m) => @@ -205,6 +209,38 @@ export const MainContextProvider: FCWithChildren = ({ children }) => { } } + const fetchNodes = async () => { + setNodes({ data: undefined, isLoading: true }) + try { + const res = await Api.fetchNodes(); + res.forEach((node: any) => node.total_stake = + Math.round(Number.parseFloat(node.rewarding_details?.operator || "0") + + Number.parseFloat(node.rewarding_details?.delegates || "0")) + ); + setNodes({ + data: res.sort((a: any, b: any) => b.total_stake - a.total_stake), + isLoading: false, + }) + } catch (error) { + setNodes({ + error: + error instanceof Error + ? error + : new Error('Service provider api fail'), + isLoading: false, + }) + } }; + + const fetchNodeById = async (id: number) => { + const res = await Api.fetchNodeById(id); + return res; + }; + + const fetchAccountById = async (id: string) => { + const res = await Api.fetchAccountById(id); + return res; + }; + React.useEffect(() => { if (environment === 'mainnet') { fetchServiceProviders() @@ -231,12 +267,16 @@ export const MainContextProvider: FCWithChildren = ({ children }) => { globalError, mixnodes, mode, + nodes, summaryOverview, validators, serviceProviders, toggleMode, fetchMixnodes, filterMixnodes, + fetchNodes, + fetchNodeById, + fetchAccountById, }), [ environment, @@ -246,6 +286,7 @@ export const MainContextProvider: FCWithChildren = ({ children }) => { globalError, mixnodes, mode, + nodes, summaryOverview, validators, serviceProviders, diff --git a/explorer-nextjs/app/context/nav.tsx b/explorer-nextjs/app/context/nav.tsx index b2be00a5270..e41882edbba 100644 --- a/explorer-nextjs/app/context/nav.tsx +++ b/explorer-nextjs/app/context/nav.tsx @@ -2,7 +2,7 @@ import * as React from 'react' import { DelegateIcon } from '@/app/icons/DelevateSVG' -import { BIG_DIPPER } from '@/app/api/constants' +import { BLOCK_EXPLORER_BASE_URL } from '@/app/api/constants' import { OverviewSVG } from '@/app/icons/OverviewSVG' import { NodemapSVG } from '@/app/icons/NodemapSVG' import { NetworkComponentsSVG } from '@/app/icons/NetworksSVG' @@ -27,23 +27,23 @@ export const originalNavOptions: NavOptionType[] = [ title: 'Network Components', Icon: <NetworkComponentsSVG />, nested: [ + { + url: '/network-components/nodes', + title: 'Nodes', + }, { url: '/network-components/mixnodes', - title: 'Mixnodes', + title: 'Mixnodes (legacy)', }, { url: '/network-components/gateways', - title: 'Gateways', + title: 'Gateways (legacy)', }, { - url: `${BIG_DIPPER}/validators`, + url: `${BLOCK_EXPLORER_BASE_URL}/validators`, title: 'Validators', isExternal: true, }, - { - url: '/network-components/service-providers', - title: 'Service Providers', - }, ], }, { diff --git a/explorer-nextjs/app/context/node.tsx b/explorer-nextjs/app/context/node.tsx new file mode 100644 index 00000000000..2aa885439c2 --- /dev/null +++ b/explorer-nextjs/app/context/node.tsx @@ -0,0 +1,77 @@ +'use client' + +import * as React from 'react' +import { + ApiState, + NymNodeReportResponse, + UptimeStoryResponse, +} from '@/app/typeDefs/explorer-api' +import { Api } from '@/app/api' +import { useApiState } from './hooks' + +/** + * This context provides the state for a single gateway by identity key. + */ + +interface NymNodeState { + uptimeReport?: ApiState<NymNodeReportResponse> + uptimeHistory?: ApiState<UptimeStoryResponse> +} + +export const NymNodeContext = React.createContext<NymNodeState>({}) + +export const useNymNodeContext = (): React.ContextType<typeof NymNodeContext> => + React.useContext<NymNodeState>(NymNodeContext) + +/** + * Provides a state context for a gateway by identity + * @param gatewayIdentityKey The identity key of the gateway + */ +export const NymNodeContextProvider = ({ + nymNodeId, + children, +}: { + nymNodeId: string + children: JSX.Element +}) => { + const [uptimeReport, fetchUptimeReportById, clearUptimeReportById] = + useApiState<any>( + nymNodeId, + Api.fetchNymNodePerformanceById, + 'Failed to fetch gateway uptime report by id' + ) + + const [uptimeHistory, fetchUptimeHistory, clearUptimeHistory] = + useApiState<UptimeStoryResponse>( + nymNodeId, + async (arg) => { + const res = await Api.fetchNymNodeUptimeHistoryById(arg); + const uptimeHistory: UptimeStoryResponse = { + history: res.history.data, + identity: '', + owner: '', + } + return uptimeHistory; + }, + 'Failed to fetch gateway uptime history' + ) + + React.useEffect(() => { + // when the identity key changes, remove all previous data + clearUptimeReportById() + clearUptimeHistory() + Promise.all([fetchUptimeReportById(), fetchUptimeHistory()]) + }, [nymNodeId]) + + const state = React.useMemo<NymNodeState>( + () => ({ + uptimeReport, + uptimeHistory, + }), + [uptimeReport, uptimeHistory] + ) + + return ( + <NymNodeContext.Provider value={state}>{children}</NymNodeContext.Provider> + ) +} diff --git a/explorer-nextjs/app/network-components/gateways/[id]/page.tsx b/explorer-nextjs/app/network-components/gateways/[id]/page.tsx index 8f4b99e6f98..87f779872a9 100644 --- a/explorer-nextjs/app/network-components/gateways/[id]/page.tsx +++ b/explorer-nextjs/app/network-components/gateways/[id]/page.tsx @@ -3,7 +3,7 @@ import * as React from 'react' import { Alert, AlertTitle, Box, CircularProgress, Grid } from '@mui/material' import { useParams } from 'next/navigation' -import { GatewayBond } from '@/app/typeDefs/explorer-api' +import {GatewayBond, LocatedGateway} from '@/app/typeDefs/explorer-api' import { ColumnsType, DetailTable } from '@/app/components/DetailTable' import { gatewayEnrichedToGridRow, @@ -32,13 +32,6 @@ const columns: ColumnsType[] = [ title: 'Bond', headerAlign: 'left', }, - { - field: 'node_performance', - title: 'Routing Score', - headerAlign: 'left', - tooltipInfo: - "Gateway's most recent score (measured in the last 15 minutes). Routing score is relative to that of the network. Each time a gateway is tested, the test packets have to go through the full path of the network (gateway + 3 nodes). If a node in the path drop packets it will affect the score of the gateway and other nodes in the test", - }, { field: 'avgUptime', title: 'Avg. Score', @@ -74,7 +67,7 @@ const columns: ColumnsType[] = [ const PageGatewayDetailsWithState = ({ selectedGateway, }: { - selectedGateway: GatewayBond | undefined + selectedGateway: LocatedGateway | undefined }) => { const [enrichGateway, setEnrichGateway] = React.useState<GatewayEnrichedRowType>() @@ -97,7 +90,13 @@ const PageGatewayDetailsWithState = ({ return ( <Box component="main"> - <Title text="Gateway Detail" /> + <Title text="Legacy Gateway Detail" /> + + <Alert variant="filled" severity="warning" sx={{ my : 2, pt: 2 }}> + <AlertTitle> + Please update to the latest <code>nym-node</code> binary and migrate your bond and delegations from the wallet + </AlertTitle> + </Alert> <Grid container> <Grid item xs={12}> @@ -146,7 +145,7 @@ const PageGatewayDetailsWithState = ({ * Guard component to handle loadingW and not found states */ const PageGatewayDetailGuard = () => { - const [selectedGateway, setSelectedGateway] = React.useState<GatewayBond>() + const [selectedGateway, setSelectedGateway] = React.useState<LocatedGateway>() const { gateways } = useMainContext() const { id } = useParams() diff --git a/explorer-nextjs/app/network-components/gateways/page.tsx b/explorer-nextjs/app/network-components/gateways/page.tsx index d0306e73d09..c2660ce3870 100644 --- a/explorer-nextjs/app/network-components/gateways/page.tsx +++ b/explorer-nextjs/app/network-components/gateways/page.tsx @@ -18,7 +18,7 @@ import { CustomColumnHeading } from '@/app/components/CustomColumnHeading' import { Title } from '@/app/components/Title' import { unymToNym } from '@/app/utils/currency' import { Tooltip } from '@/app/components/Tooltip' -import { NYM_BIG_DIPPER } from '@/app/api/constants' +import { EXPLORER_FOR_ACCOUNTS } from '@/app/api/constants' import { splice } from '@/app/utils' import { VersionDisplaySelector, @@ -29,6 +29,23 @@ import { GatewayRowType, gatewayToGridRow, } from '@/app/components/Gateways/Gateways' +import {LocatedGateway} from "@/app/typeDefs/explorer-api"; + +const gatewaySanitize = (g?: LocatedGateway): boolean => { + if(!g) { + return false; + } + + if(!g.gateway.version || !g.gateway.version.trim().length) { + return false; + } + + if(g.gateway.version === "null") { + return false; + } + + return true; +} const PageGateways = () => { const { gateways } = useMainContext() @@ -39,7 +56,7 @@ const PageGateways = () => { const highestVersion = React.useMemo(() => { if (gateways?.data) { - const versions = gateways.data.reduce( + const versions = gateways.data.filter(gatewaySanitize).reduce( (a: string[], b) => [...a, b.gateway.version], [] ) @@ -51,7 +68,7 @@ const PageGateways = () => { }, [gateways]) const filterByLatestVersions = React.useMemo(() => { - const filtered = gateways?.data?.filter((gw) => { + const filtered = gateways?.data?.filter(gatewaySanitize).filter((gw) => { const versionDiff = diff(highestVersion, gw.gateway.version) return versionDiff === 'patch' || versionDiff === null }) @@ -60,7 +77,7 @@ const PageGateways = () => { }, [gateways]) const filterByOlderVersions = React.useMemo(() => { - const filtered = gateways?.data?.filter((gw) => { + const filtered = gateways?.data?.filter(gatewaySanitize).filter((gw) => { const versionDiff = diff(highestVersion, gw.gateway.version) return versionDiff === 'major' || versionDiff === 'minor' }) @@ -89,7 +106,7 @@ const PageGateways = () => { return [ { id: 'gateway-data', - header: 'Gatewsay Data', + header: 'Gateways Data', columns: [ { id: 'identity_key', @@ -116,41 +133,6 @@ const PageGateways = () => { ) }, }, - { - id: 'node_performance', - header: 'Node Performance', - accessorKey: 'node_performance', - size: 200, - Header: () => { - return ( - <Box display="flex"> - <InfoTooltip - id="gateways-list-routing-score" - title="Gateway's most recent score (measured in the last 15 minutes). Routing score is relative to that of the network. Each time a gateway is tested, the test packets have to go through the full path of the network (gateway + 3 nodes). If a node in the path drop packets it will affect the score of the gateway and other nodes in the test" - placement="top-start" - textColor={theme.palette.nym.networkExplorer.tooltip.color} - bgColor={ - theme.palette.nym.networkExplorer.tooltip.background - } - maxWidth={230} - arrow - /> - <CustomColumnHeading headingTitle="Routing Score" /> - </Box> - ) - }, - Cell: ({ row }) => { - return ( - <StyledLink - to={`/network-components/gateways/${row.original.identity_key}`} - data-testid="node-performance" - color="text.primary" - > - {`${row.original.node_performance}%`} - </StyledLink> - ) - }, - }, { id: 'version', header: 'Version', @@ -222,7 +204,7 @@ const PageGateways = () => { Cell: ({ row }) => { return ( <StyledLink - to={`${NYM_BIG_DIPPER}/account/${row.original.owner}`} + to={`${EXPLORER_FOR_ACCOUNTS}/account/${row.original.owner}`} target="_blank" data-testid="owner" color="text.primary" @@ -237,137 +219,6 @@ const PageGateways = () => { ] }, []) - const _columns: GridColDef[] = [ - { - field: 'node_performance', - align: 'center', - renderHeader: () => ( - <> - <InfoTooltip - id="gateways-list-routing-score" - title="Gateway's most recent score (measured in the last 15 minutes). Routing score is relative to that of the network. Each time a gateway is tested, the test packets have to go through the full path of the network (gateway + 3 nodes). If a node in the path drop packets it will affect the score of the gateway and other nodes in the test" - placement="top-start" - textColor={theme.palette.nym.networkExplorer.tooltip.color} - bgColor={theme.palette.nym.networkExplorer.tooltip.background} - maxWidth={230} - arrow - /> - <CustomColumnHeading headingTitle="Routing Score" /> - </> - ), - width: 120, - disableColumnMenu: true, - headerAlign: 'center', - headerClassName: 'MuiDataGrid-header-override', - renderCell: (params: GridRenderCellParams) => ( - <StyledLink - to={`/network-components/gateways/${params.row.identity_key}`} - data-testid="pledge-amount" - > - {`${params.value}%`} - </StyledLink> - ), - }, - { - field: 'version', - align: 'center', - renderHeader: () => <CustomColumnHeading headingTitle="Version" />, - width: 150, - disableColumnMenu: true, - headerAlign: 'center', - headerClassName: 'MuiDataGrid-header-override', - renderCell: (params: GridRenderCellParams) => ( - <StyledLink - to={`/network-components/gateways/${params.row.identity_key}`} - data-testid="version" - > - {params.value} - </StyledLink> - ), - sortComparator: (a, b) => { - if (gte(a, b)) return 1 - return -1 - }, - }, - { - field: 'location', - renderHeader: () => <CustomColumnHeading headingTitle="Location" />, - width: 180, - disableColumnMenu: true, - headerAlign: 'left', - headerClassName: 'MuiDataGrid-header-override', - renderCell: (params: GridRenderCellParams) => ( - <Box - sx={{ justifyContent: 'flex-start', cursor: 'pointer' }} - data-testid="location-button" - > - <Tooltip text={params.value} id="gateway-location-text"> - <Box - sx={{ - overflow: 'hidden', - whiteSpace: 'nowrap', - textOverflow: 'ellipsis', - }} - > - {params.value} - </Box> - </Tooltip> - </Box> - ), - }, - { - field: 'host', - renderHeader: () => <CustomColumnHeading headingTitle="IP:Port" />, - width: 180, - disableColumnMenu: true, - headerAlign: 'left', - headerClassName: 'MuiDataGrid-header-override', - renderCell: (params: GridRenderCellParams) => ( - <StyledLink - to={`/network-components/gateways/${params.row.identity_key}`} - data-testid="host" - > - {params.value} - </StyledLink> - ), - }, - { - field: 'owner', - headerName: 'Owner', - renderHeader: () => <CustomColumnHeading headingTitle="Owner" />, - width: 180, - disableColumnMenu: true, - headerAlign: 'left', - headerClassName: 'MuiDataGrid-header-override', - renderCell: (params: GridRenderCellParams) => ( - <StyledLink - to={`${NYM_BIG_DIPPER}/account/${params.value}`} - target="_blank" - data-testid="owner" - > - {splice(7, 29, params.value)} - </StyledLink> - ), - }, - { - field: 'bond', - width: 150, - disableColumnMenu: true, - type: 'number', - renderHeader: () => <CustomColumnHeading headingTitle="Bond" />, - headerClassName: 'MuiDataGrid-header-override', - headerAlign: 'left', - renderCell: (params: GridRenderCellParams) => ( - <StyledLink - to={`/network-components/gateways/${params.row.identity_key}`} - data-testid="pledge-amount" - > - {`${unymToNym(params.value, 6)}`} - </StyledLink> - ), - }, - ] - const table = useMaterialReactTable({ columns, data, @@ -376,7 +227,7 @@ const PageGateways = () => { return ( <> <Box mb={2}> - <Title text="Gateways" /> + <Title text="Legacy Gateways" /> </Box> <Grid container> <Grid item xs={12}> diff --git a/explorer-nextjs/app/network-components/mixnodes/[id]/page.tsx b/explorer-nextjs/app/network-components/mixnodes/[id]/page.tsx index 495312064de..5e03b5d1326 100644 --- a/explorer-nextjs/app/network-components/mixnodes/[id]/page.tsx +++ b/explorer-nextjs/app/network-components/mixnodes/[id]/page.tsx @@ -94,7 +94,12 @@ const PageMixnodeDetailWithState = () => { const isMobile = useIsMobile() return ( <Box component="main"> - <Title text="Mixnode Detail" /> + <Title text="Legacy Mixnode Detail" /> + <Alert variant="filled" severity="warning" sx={{ my : 2, pt: 2 }}> + <AlertTitle> + Please update to the latest <code>nym-node</code> binary and migrate your bond and delegations from the wallet + </AlertTitle> + </Alert> <Grid container spacing={2} mt={1} mb={6}> <Grid item xs={12}> {mixNodeRow && description?.data && ( diff --git a/explorer-nextjs/app/network-components/mixnodes/page.tsx b/explorer-nextjs/app/network-components/mixnodes/page.tsx index 4021e4173f9..2f2774a7f43 100644 --- a/explorer-nextjs/app/network-components/mixnodes/page.tsx +++ b/explorer-nextjs/app/network-components/mixnodes/page.tsx @@ -29,7 +29,7 @@ import { useMainContext } from '@/app/context/main' import { CopyToClipboard } from '@nymproject/react/clipboard/CopyToClipboard' import { splice } from '@/app/utils' import { currencyToString } from '@/app/utils/currency' -import { NYM_BIG_DIPPER } from '@/app/api/constants' +import { EXPLORER_FOR_ACCOUNTS } from '@/app/api/constants' import { MixnodeStatusWithAll, toMixnodeStatus, @@ -229,24 +229,6 @@ export default function MixnodesPage() { >{`${row.original.operating_cost} NYM`}</StyledLink> ), }, - { - id: 'node_performance', - accessorKey: 'node_performance', - size: 200, - header: 'Routing Score', - Header: () => ( - <CustomColumnHeading - headingTitle="Routing Score" - tooltipInfo="Mixnode's most recent score (measured in the last 15 minutes). Routing score is relative to that of the network. Each time a gateway is tested, the test packets have to go through the full path of the network (gateway + 3 nodes). If a node in the path drop packets it will affect the score of the gateway and other nodes in the test." - /> - ), - Cell: ({ row }) => ( - <StyledLink - to={`/network-components/mixnodes/${row.original.mix_id}`} - color={useGetMixNodeStatusColor(row.original.status)} - >{`${row.original.node_performance}%`}</StyledLink> - ), - }, { id: 'owner', accessorKey: 'owner', @@ -255,7 +237,7 @@ export default function MixnodesPage() { Header: () => <CustomColumnHeading headingTitle="Owner" />, Cell: ({ row }) => ( <StyledLink - to={`${NYM_BIG_DIPPER}/account/${row.original.owner}`} + to={`${EXPLORER_FOR_ACCOUNTS}/account/${row.original.owner}`} color={useGetMixNodeStatusColor(row.original.status)} target="_blank" data-testid="big-dipper-link" @@ -326,7 +308,7 @@ export default function MixnodesPage() { return ( <DelegationsProvider> <Box mb={2}> - <Title text="Mixnodes" /> + <Title text="Legacy Mixnodes" /> </Box> <Grid container> <Grid item xs={12}> diff --git a/explorer-nextjs/app/network-components/nodes/DeclaredRole.tsx b/explorer-nextjs/app/network-components/nodes/DeclaredRole.tsx new file mode 100644 index 00000000000..fac36689f2d --- /dev/null +++ b/explorer-nextjs/app/network-components/nodes/DeclaredRole.tsx @@ -0,0 +1,11 @@ +import React from "react"; +import { Chip } from "@mui/material"; + +export const DeclaredRole = ({ declared_role }: { declared_role?: any }) => ( + <> + {declared_role?.mixnode && <Chip size="small" label="Mixnode" sx={{ mr: 0.5 }} color="info" />} + {declared_role?.entry && <Chip size="small" label="Entry" sx={{ mr: 0.5 }} color="success" />} + {declared_role?.exit_nr && <Chip size="small" label="Exit NR" sx={{ mr: 0.5 }} color="warning" />} + {declared_role?.exit_ipr && <Chip size="small" label="Exit IPR" sx={{ mr: 0.5 }} color="warning" />} + </> +) \ No newline at end of file diff --git a/explorer-nextjs/app/network-components/nodes/[id]/NodeDelegationsTable.tsx b/explorer-nextjs/app/network-components/nodes/[id]/NodeDelegationsTable.tsx new file mode 100644 index 00000000000..a1b754ccce6 --- /dev/null +++ b/explorer-nextjs/app/network-components/nodes/[id]/NodeDelegationsTable.tsx @@ -0,0 +1,93 @@ +import React, {useMemo} from "react"; +import {MaterialReactTable, MRT_ColumnDef, useMaterialReactTable} from "material-react-table"; +import StyledLink from "../../../components/StyledLink"; +import {EXPLORER_FOR_ACCOUNTS} from "@/app/api/constants"; +import {splice} from "@/app/utils"; +import {humanReadableCurrencyToString} from "@/app/utils/currency"; +import {Typography} from "@mui/material"; +import {useTheme} from "@mui/material/styles"; +import WarningIcon from '@mui/icons-material/Warning'; +import { Tooltip } from '@/app/components/Tooltip' + +export const NodeDelegationsTable = ({ node }: { node: any}) => { + const columns = useMemo<MRT_ColumnDef<any>[]>(() => { + return [ + { + id: 'nym-node-delegation-data', + header: 'Nym Node Delegations', + columns: [ + { + id: 'owner', + header: 'Delegator', + accessorKey: 'owner', + size: 150, + Cell: ({ row }) => { + return ( + <StyledLink + to={`${EXPLORER_FOR_ACCOUNTS}/account/${row.original.owner || "-"}`} + target="_blank" + data-testid="bond_information.node.owner" + color="text.primary" + > + {splice(7, 29, row.original.owner)} + </StyledLink> + ) + }, + }, + { + id: 'amount', + header: 'Amount', + accessorKey: 'amount', + size: 150, + Cell: ({ row }) => ( + <>{humanReadableCurrencyToString(row.original.amount)}</> + ) + }, + { + id: 'height', + header: 'Delegated at height', + accessorKey: 'height', + size: 150, + }, + { + id: 'proxy', + header: 'From vesting account?', + accessorKey: 'proxy', + size: 250, + Cell: ({ row }) => { + if(row.original.proxy?.length) { + return ( + <VestingDelegationWarning>Please re-delegate from your main account</VestingDelegationWarning> + ) + } + } + }, + ] + } + ]; + }, []); + + const table = useMaterialReactTable({ + columns, + data: node ? node.delegations : [], + }); + + return ( + <MaterialReactTable table={table} /> + ); +} + +export const VestingDelegationWarning = ({children, plural}: { plural?: boolean, children: React.ReactNode}) => { + const theme = useTheme(); + return ( + <Tooltip + text={`${plural ? 'These delegations have' : 'This delegation has'} been made with a vesting account. All tokens are liquid, if you are the delegator, please move the tokens into your main account and make the delegation from there.`} + id="delegations" + > + <Typography fontSize="inherit" color={theme.palette.warning.main} display="flex" alignItems="center"> + <WarningIcon sx={{ mr: 0.5 }}/> + {children} + </Typography> + </Tooltip> + ); +} \ No newline at end of file diff --git a/explorer-nextjs/app/network-components/nodes/[id]/page.tsx b/explorer-nextjs/app/network-components/nodes/[id]/page.tsx new file mode 100644 index 00000000000..2f92542b3b3 --- /dev/null +++ b/explorer-nextjs/app/network-components/nodes/[id]/page.tsx @@ -0,0 +1,278 @@ +'use client' + +import * as React from 'react' +import { Alert, AlertTitle, Box, CircularProgress, Grid } from '@mui/material' +import { useParams } from 'next/navigation' +import { ColumnsType, DetailTable } from '@/app/components/DetailTable' +import { ComponentError } from '@/app/components/ComponentError' +import { ContentCard } from '@/app/components/ContentCard' +import { UptimeChart } from '@/app/components/UptimeChart' +import { + NymNodeContextProvider, + useNymNodeContext, +} from '@/app/context/node' +import { useMainContext } from '@/app/context/main' +import { Title } from '@/app/components/Title' +import Paper from "@mui/material/Paper"; +import Table from '@mui/material/Table'; +import TableBody from '@mui/material/TableBody'; +import TableCell from '@mui/material/TableCell'; +import TableContainer from '@mui/material/TableContainer'; +import TableRow from '@mui/material/TableRow'; +import {humanReadableCurrencyToString} from "@/app/utils/currency"; +import {DeclaredRole} from "@/app/network-components/nodes/DeclaredRole"; +import {NodeDelegationsTable, VestingDelegationWarning} from "@/app/network-components/nodes/[id]/NodeDelegationsTable"; + +const columns: ColumnsType[] = [ + { + field: 'identity_key', + title: 'Identity Key', + headerAlign: 'left', + width: 230, + }, + { + field: 'bond', + title: 'Bond', + headerAlign: 'left', + }, + { + field: 'host', + title: 'IP', + headerAlign: 'left', + width: 99, + }, + { + field: 'location', + title: 'Location', + headerAlign: 'left', + }, + { + field: 'owner', + title: 'Owner', + headerAlign: 'left', + }, + { + field: 'version', + title: 'Version', + headerAlign: 'left', + }, +] + +interface NodeEnrichedRowType { + node_id: number; + identity_key: string; + bond: string; + host: string; + location: string; + owner: string; + version: string; +} + +function nodeEnrichedToGridRow(node: any): NodeEnrichedRowType { + return { + node_id: node.node_id, + owner: node.bond_information?.owner || '', + identity_key: node.bond_information?.node?.identity_key || '', + location: node.location?.country_name || '', + bond: node.bond_information?.original_pledge.amount || 0, // TODO: format + host: node.bond_information?.node?.host || '', + version: node.description?.build_information?.build_version || '', + }; +} + + +/** + * Shows nym node details + */ +const PageNymNodeDetailsWithState = ({ + selectedNymNode, +}: { + selectedNymNode?: any +}) => { + const { uptimeHistory } = useNymNodeContext() + const enrichedData = React.useMemo(() => selectedNymNode ? [nodeEnrichedToGridRow(selectedNymNode)] : [], []); + + const hasVestingContractDelegations = React.useMemo(() => selectedNymNode?.delegations?.filter((d: any) => d.proxy)?.length, [selectedNymNode]); + + return ( + <Box component="main"> + <Title text="Nym Node Detail" /> + + <Grid container mt={4}> + <Grid item xs={12}> + <DetailTable + columnsData={columns} + tableName="Node detail table" + rows={enrichedData} + /> + </Grid> + </Grid> + + <Grid container mt={2} spacing={2}> + {selectedNymNode.rewarding_details && + <Grid item xs={12} md={4}> + <TableContainer component={Paper}> + <Table> + <TableBody> + <TableRow> + <TableCell colSpan={2}> + Delegations and Rewards + </TableCell> + </TableRow> + <TableRow> + <TableCell component="th" scope="row" sx={{ color: "inherit" }}> + <strong>Operator</strong> + </TableCell> + <TableCell align="right"> + {humanReadableCurrencyToString({ amount : selectedNymNode.rewarding_details.operator.split('.')[0], denom: "unym" })} + </TableCell> + </TableRow> + <TableRow> + <TableCell component="th" scope="row" sx={{ color: "inherit" }}> + <strong> + {hasVestingContractDelegations ? + <VestingDelegationWarning plural={true}> + Delegates ({selectedNymNode.rewarding_details.unique_delegations} delegates) + </VestingDelegationWarning> : + <>Delegates ({selectedNymNode.rewarding_details.unique_delegations} delegates)</> + } + </strong> + </TableCell> + <TableCell align="right"> + {humanReadableCurrencyToString({ amount : selectedNymNode.rewarding_details.delegates.split('.')[0], denom: "unym" })} + </TableCell> + </TableRow> + <TableRow> + <TableCell component="th" scope="row" sx={{ color: "inherit" }}> + <strong>Profit margin</strong> + </TableCell> + <TableCell align="right"> + {selectedNymNode.rewarding_details.cost_params.profit_margin_percent * 100}% + </TableCell> + </TableRow> + <TableRow> + <TableCell component="th" scope="row" sx={{ color: "inherit" }}> + <strong>Operator costs</strong> + </TableCell> + <TableCell align="right"> + {humanReadableCurrencyToString(selectedNymNode.rewarding_details.cost_params.interval_operating_cost)} + </TableCell> + </TableRow> + </TableBody> + </Table> + </TableContainer> + </Grid>} + + {selectedNymNode.description?.declared_role && <Grid item xs={12} md={4}> + <TableContainer component={Paper}> + <Table> + <TableBody> + <TableRow> + <TableCell colSpan={2}> + Node roles + </TableCell> + </TableRow> + <TableRow> + <TableCell> + Self declared roles + </TableCell> + <TableCell> + <DeclaredRole declared_role={selectedNymNode.description?.declared_role}/> + </TableCell> + </TableRow> + </TableBody> + </Table> + </TableContainer> + </Grid>} + </Grid> + + <Grid container spacing={2} mt={2}> + <Grid item xs={12} md={8}> + {uptimeHistory && ( + <ContentCard title="Routing Score"> + {uptimeHistory.error && ( + <ComponentError text="There was a problem retrieving routing score." /> + )} + <UptimeChart + loading={uptimeHistory.isLoading} + xLabel="Date" + yLabel="Daily average" + uptimeStory={uptimeHistory} + /> + </ContentCard> + )} + </Grid> + </Grid> + + <Box mt={2}> + <NodeDelegationsTable node={selectedNymNode}/> + </Box> + </Box> + ) +} + +/** + * Guard component to handle loading and not found states + */ +const PageNymNodeDetailGuard = () => { + const [selectedNode, setSelectedNode] = React.useState<any>() + const [isLoading, setLoading] = React.useState<boolean>(true); + const [error, setError] = React.useState<string>(); + const { fetchNodeById } = useMainContext() + const { id } = useParams() + + React.useEffect(() => { + setSelectedNode(undefined); + setLoading(true); + (async () => { + if(typeof(id) === "string") { + try { + const res = await fetchNodeById(Number.parseInt(id)); + setSelectedNode(res); + } catch(e: any) { + setError(e.message); + } + finally { + setLoading(false); + } + } + })(); + }, [id]) + + if (isLoading) { + return <CircularProgress /> + } + + // loaded, but not found + if (error) { + return ( + <Alert severity="warning"> + <AlertTitle>Nym node not found</AlertTitle> + Sorry, we could not find a node with id <code>{id || ''}</code> + </Alert> + ) + } + + return <PageNymNodeDetailsWithState selectedNymNode={selectedNode} /> +} + +/** + * Wrapper component that adds the node content based on the `id` in the address URL + */ +const PageNymNodeDetail = () => { + const { id } = useParams() + + if (!id || typeof id !== 'string') { + return ( + <Alert severity="error">Oh no! Could not find that node</Alert> + ) + } + + return ( + <NymNodeContextProvider nymNodeId={id}> + <PageNymNodeDetailGuard /> + </NymNodeContextProvider> + ) +} + +export default PageNymNodeDetail diff --git a/explorer-nextjs/app/network-components/nodes/page.tsx b/explorer-nextjs/app/network-components/nodes/page.tsx new file mode 100644 index 00000000000..75e5b97fdd0 --- /dev/null +++ b/explorer-nextjs/app/network-components/nodes/page.tsx @@ -0,0 +1,254 @@ +'use client' + +import React, { useMemo } from 'react' +import { Box, Card, Grid, Stack, Chip } from '@mui/material' +import { useTheme } from '@mui/material/styles' +import { + MRT_ColumnDef, + MaterialReactTable, + useMaterialReactTable, +} from 'material-react-table' +import { diff, gte, rcompare } from 'semver' +import { GridColDef, GridRenderCellParams } from '@mui/x-data-grid' +import { CopyToClipboard } from '@nymproject/react/clipboard/CopyToClipboard' +import { Tooltip as InfoTooltip } from '@nymproject/react/tooltip/Tooltip' +import { useMainContext } from '@/app/context/main' +import { CustomColumnHeading } from '@/app/components/CustomColumnHeading' +import { Title } from '@/app/components/Title' +import { humanReadableCurrencyToString } from '@/app/utils/currency' +import { Tooltip } from '@/app/components/Tooltip' +import { EXPLORER_FOR_ACCOUNTS } from '@/app/api/constants' +import { splice } from '@/app/utils' + +import StyledLink from '@/app/components/StyledLink' +import {DeclaredRole} from "@/app/network-components/nodes/DeclaredRole"; + +function getFlagEmoji(countryCode: string) { + const codePoints = countryCode + .toUpperCase() + .split('') + .map(char => 127397 + char.charCodeAt(0)); + return String.fromCodePoint(...codePoints); +} + +const PageNodes = () => { + const [isLoading, setLoading] = React.useState(true); + const { nodes, fetchNodes } = useMainContext() + + React.useEffect(() => { + (async () => { + try { + await fetchNodes(); + } finally { + setLoading(false); + } + })(); + }, []); + + const columns = useMemo<MRT_ColumnDef<any>[]>(() => { + return [ + { + id: 'nym-node-data', + header: 'Nym Node Data', + columns: [ + { + id: 'node_id', + header: 'Node Id', + accessorKey: 'node_id', + size: 75, + }, + { + id: 'identity_key', + header: 'Identity Key', + accessorKey: 'identity_key', + size: 250, + Cell: ({ row }) => { + return ( + <Stack direction="row" alignItems="center" gap={1}> + <CopyToClipboard + sx={{ mr: 0.5, color: 'grey.400' }} + smallIcons + value={row.original.bond_information.node.identity_key} + tooltip={`Copy identity key ${row.original.bond_information.node.identity_key} to clipboard`} + /> + <StyledLink + to={`/network-components/nodes/${row.original.node_id}`} + dataTestId="identity-link" + color="text.primary" + > + {splice(7, 29, row.original.bond_information.node.identity_key)} + </StyledLink> + </Stack> + ) + }, + }, + { + id: 'version', + header: 'Version', + accessorKey: 'description.build_information.build_version', + size: 75, + Cell: ({ row }) => { + return ( + <StyledLink + to={`/network-components/nodes/${row.original.node_id}`} + data-testid="version" + color="text.primary" + > + {row.original.description?.build_information?.build_version || "-"} + </StyledLink> + ) + }, + }, + { + id: 'contract_node_type', + header: 'Kind', + accessorKey: 'contract_node_type', + size: 150, + Cell: ({ row }) => { + return ( + <StyledLink + to={`/network-components/nodes/${row.original.node_id}`} + data-testid="contract_node_type" + color="text.primary" + > + <code>{row.original.contract_node_type || "-"}</code> + </StyledLink> + ) + }, + }, + { + id: 'declared_role', + header: 'Declare Role', + accessorKey: 'description.declared_role', + size: 250, + Cell: ({ row }) => { + return ( + <Box + sx={{ justifyContent: 'flex-start', cursor: 'pointer' }} + data-testid="declared_role-button" + > + <DeclaredRole declared_role={row.original.description?.declared_role}/> + </Box> + ) + }, + }, + { + id: 'total_stake', + header: 'Total Stake', + accessorKey: 'description.total_stake', + size: 250, + Cell: ({ row }) => { + return ( + <Box + sx={{ justifyContent: 'flex-start', cursor: 'pointer' }} + data-testid="total_stake-button" + > + {humanReadableCurrencyToString({ amount: row.original.total_stake || 0, denom: "unym" })} + </Box> + ) + }, + }, + { + id: 'location', + header: 'Location', + accessorKey: 'location.country_name', + size: 75, + Cell: ({ row }) => { + return ( + <Box + sx={{ justifyContent: 'flex-start', cursor: 'pointer' }} + data-testid="location-button" + > + <Tooltip + text={row.original.location?.country_name || "-"} + id="nym-node-location-text" + > + <Box + sx={{ + overflow: 'hidden', + whiteSpace: 'nowrap', + textOverflow: 'ellipsis', + }} + > + {row.original.location?.country_name ? <>{getFlagEmoji(row.original.location.two_letter_iso_country_code.toUpperCase())} {row.original.location.two_letter_iso_country_code}</> : <>-</> } + </Box> + </Tooltip> + </Box> + ) + }, + }, + { + id: 'host', + header: 'IP', + accessorKey: 'bond_information.node.host', + size: 150, + Cell: ({ row }) => { + return ( + <StyledLink + to={`/network-components/nodes/${row.original.node_id}`} + data-testid="host" + color="text.primary" + > + {row.original.bond_information?.node?.host || "-"} + </StyledLink> + ) + }, + }, + { + id: 'owner', + header: 'Owner', + accessorKey: 'bond_information.owner', + size: 150, + Cell: ({ row }) => { + return ( + <StyledLink + to={`${EXPLORER_FOR_ACCOUNTS}/account/${row.original.bond_information?.owner || "-"}`} + target="_blank" + data-testid="bond_information.node.owner" + color="text.primary" + > + {splice(7, 29, row.original.bond_information?.owner)} + </StyledLink> + ) + }, + }, + ], + }, + ] + }, []) + + const table = useMaterialReactTable({ + columns, + data: nodes?.data || [], + state: { + isLoading, + showLoadingOverlay: isLoading, + }, + initialState: { + isLoading: true, + showLoadingOverlay: true, + } + }) + + return ( + <> + <Box mb={2}> + <Title text="Nym Nodes" /> + </Box> + <Grid container> + <Grid item xs={12}> + <Card + sx={{ + padding: 2, + height: '100%', + }} + > + <MaterialReactTable table={table} /> + </Card> + </Grid> + </Grid> + </> + ) +} + +export default PageNodes diff --git a/explorer-nextjs/app/network-components/service-providers/page.tsx b/explorer-nextjs/app/network-components/service-providers/page.tsx deleted file mode 100644 index 7e8d9acc72c..00000000000 --- a/explorer-nextjs/app/network-components/service-providers/page.tsx +++ /dev/null @@ -1,140 +0,0 @@ -'use client' - -import React, { useMemo } from 'react' -import { - Box, - Button, - Card, - FormControl, - Grid, - ListItem, - Menu, - Typography, -} from '@mui/material' -import { TableToolbar } from '@/app/components/TableToolbar' -import { Title } from '@/app/components/Title' -import { useMainContext } from '@/app/context/main' -import { CustomColumnHeading } from '@/app/components/CustomColumnHeading' -import { - MRT_ColumnDef, - MaterialReactTable, - useMaterialReactTable, -} from 'material-react-table' -import { DirectoryServiceProvider } from '@/app/typeDefs/explorer-api' - -const SupportedApps = () => { - const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null) - const open = Boolean(anchorEl) - const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => { - setAnchorEl(event.currentTarget) - } - const handleClose = () => { - setAnchorEl(null) - } - const anchorRef = React.useRef<HTMLButtonElement>(null) - - return ( - <FormControl size="small"> - <Button - ref={anchorRef} - onClick={handleClick} - size="large" - variant="outlined" - color="inherit" - sx={{ mr: 2, textTransform: 'capitalize' }} - > - Supported Apps - </Button> - <Menu anchorEl={anchorEl} open={open} onClose={handleClose}> - <ListItem>Keybase</ListItem> - <ListItem>Telegram</ListItem> - <ListItem>Electrum</ListItem> - <ListItem>Blockstream Green</ListItem> - </Menu> - </FormControl> - ) -} - -const ServiceProviders = () => { - const { serviceProviders } = useMainContext() - - const columns = useMemo<MRT_ColumnDef<DirectoryServiceProvider>[]>(() => { - return [ - { - id: 'service-providers-data', - header: 'Service Providers Data', - columns: [ - { - id: 'address', - accessorKey: 'address', - header: 'Client ID', - size: 450, - }, - { - id: 'service_type-type', - accessorKey: 'service_type', - header: 'Type', - size: 100, - }, - { - id: 'routing_score-score', - accessorKey: 'routing_score', - header: 'Routing score', - Header() { - return ( - <CustomColumnHeading - headingTitle="Routing score" - tooltipInfo="Routing score is only displayed for the service providers that had a successful ping within the last two hours" - /> - ) - }, - Cell({ row }) { - return row.original.routing_score || '-' - }, - }, - ], - }, - ] - }, []) - - const table = useMaterialReactTable({ - columns, - data: serviceProviders?.data || [], - layoutMode: 'grid', - state: { - isLoading: serviceProviders?.isLoading, - }, - initialState: { - sorting: [ - { - id: 'routing_score', - desc: true, - }, - ], - }, - }) - - return ( - <> - <Box mb={2}> - <Title text="Service Providers" /> - </Box> - <Grid container> - <Grid item xs={12}> - <Card - sx={{ - padding: 2, - }} - > - <> - <TableToolbar childrenBefore={<SupportedApps />} /> - <MaterialReactTable table={table} /> - </> - </Card> - </Grid> - </Grid> - </> - ) -} - -export default ServiceProviders diff --git a/explorer-nextjs/app/page.tsx b/explorer-nextjs/app/page.tsx index 1ff7dd36139..2c6f9f781c1 100644 --- a/explorer-nextjs/app/page.tsx +++ b/explorer-nextjs/app/page.tsx @@ -13,7 +13,7 @@ import { GatewaysSVG } from '@/app/icons/GatewaysSVG' import { ValidatorsSVG } from '@/app/icons/ValidatorsSVG' import { ContentCard } from '@/app/components/ContentCard' import { WorldMap } from '@/app/components/WorldMap' -import { BIG_DIPPER } from '@/app/api/constants' +import { BLOCK_EXPLORER_BASE_URL } from '@/app/api/constants' import { formatNumber } from '@/app/utils' import { useMainContext } from './context/main' import { useRouter } from 'next/navigation' @@ -42,71 +42,59 @@ const PageOverview = () => { <> <Grid item xs={12} md={4}> <StatsCard - onClick={() => router.push('/network-components/mixnodes')} - title="Mixnodes" + onClick={() => router.push('/network-components/nodes')} + title="Nodes" icon={<MixnodesSVG />} - count={summaryOverview.data?.mixnodes.count || ''} - errorMsg={summaryOverview?.error} - /> - </Grid> - <Grid item xs={12} md={4}> - <StatsCard - onClick={() => - router.push('/network-components/mixnodes?status=active') - } - title="Active nodes" - icon={<Icons.Mixnodes.Status.Active />} - color={ - theme.palette.nym.networkExplorer.mixnodes.status.active - } - count={summaryOverview.data?.mixnodes.activeset.active} - errorMsg={summaryOverview?.error} - /> - </Grid> - <Grid item xs={12} md={4}> - <StatsCard - onClick={() => - router.push('/network-components/mixnodes?status=standby') - } - title="Standby nodes" - color={ - theme.palette.nym.networkExplorer.mixnodes.status.standby - } - icon={<Icons.Mixnodes.Status.Standby />} - count={summaryOverview.data?.mixnodes.activeset.standby} + count={summaryOverview.data?.nymnodes?.count || ''} errorMsg={summaryOverview?.error} /> </Grid> </> )} - {gateways && ( + {summaryOverview && ( <Grid item xs={12} md={4}> <StatsCard - onClick={() => router.push('/network-components/gateways')} - title="Gateways" - count={gateways?.data?.length || ''} - errorMsg={gateways?.error} + onClick={() => router.push('/network-components/nodes')} + title="Mixnodes" + count={summaryOverview.data?.nymnodes?.roles?.mixnode || ''} icon={<GatewaysSVG />} /> </Grid> )} - {serviceProviders && ( + {summaryOverview && ( <Grid item xs={12} md={4}> <StatsCard - onClick={() => - router.push('/network-components/service-providers') - } - title="Service providers" - icon={<PeopleAlt />} - count={serviceProviders.data?.length} - errorMsg={summaryOverview?.error} + onClick={() => router.push('/network-components/nodes')} + title="Entry Gateways" + count={summaryOverview.data?.nymnodes?.roles?.entry || ''} + icon={<GatewaysSVG />} + /> + </Grid> + )} + {summaryOverview && ( + <Grid item xs={12} md={4}> + <StatsCard + onClick={() => router.push('/network-components/nodes')} + title="Exit Gateways" + count={summaryOverview.data?.nymnodes?.roles?.exit_ipr || ''} + icon={<GatewaysSVG />} + /> + </Grid> + )} + {summaryOverview && ( + <Grid item xs={12} md={4}> + <StatsCard + onClick={() => router.push('/network-components/nodes')} + title="SOCKS5 Network Requesters" + count={summaryOverview.data?.nymnodes?.roles?.exit_nr || ''} + icon={<GatewaysSVG />} /> </Grid> )} {validators && ( <Grid item xs={12} md={4}> <StatsCard - onClick={() => window.open(`${BIG_DIPPER}/validators`)} + onClick={() => window.open(`${BLOCK_EXPLORER_BASE_URL}/validators`)} title="Validators" count={validators?.data?.count || ''} errorMsg={validators?.error} @@ -117,7 +105,7 @@ const PageOverview = () => { {block?.data && ( <Grid item xs={12}> <Link - href={`${BIG_DIPPER}/blocks`} + href={`${BLOCK_EXPLORER_BASE_URL}/blocks`} target="_blank" rel="noreferrer" underline="none" diff --git a/explorer-nextjs/app/typeDefs/explorer-api.ts b/explorer-nextjs/app/typeDefs/explorer-api.ts index 9319c33952e..b15490008d7 100644 --- a/explorer-nextjs/app/typeDefs/explorer-api.ts +++ b/explorer-nextjs/app/typeDefs/explorer-api.ts @@ -20,6 +20,15 @@ export interface SummaryOverviewResponse { validators: { count: number; }; + nymnodes: { + count: number; + roles: { + mixnode: number; + entry: number; + exit_nr: number; + exit_ipr: number; + }; + }; } export interface MixNode { @@ -156,7 +165,15 @@ export interface LocatedGateway { location?: Location; } -export type GatewayResponse = GatewayBond[]; +export type GatewayResponse = LocatedGateway[]; + +export interface NymNodeReportResponse { + identity: string; + owner: string; + most_recent: number; + last_hour: number; + last_day: number; +} export interface GatewayReportResponse { identity: string; diff --git a/explorer-nextjs/app/utils/currency.ts b/explorer-nextjs/app/utils/currency.ts index ad6ed1f1656..07bdc890de7 100644 --- a/explorer-nextjs/app/utils/currency.ts +++ b/explorer-nextjs/app/utils/currency.ts @@ -31,6 +31,16 @@ export const currencyToString = ({ amount, dp, denom = DENOM }: { amount: string return `${toDisplay(printableAmount, dp)} ${printableDenom}`; }; +function addThousandsSeparator(value: string) { + return value.toString().replace(/\B(?=(\d{3})+(?!\d))/g, " "); +} + +export const humanReadableCurrencyToString = ({ amount, denom }: { amount: string, denom: string }) => { + const str = currencyToString({ amount, denom, dp: 2 }); + const parts = str.split('.'); + return [addThousandsSeparator(parts[0]), parts[1]].join('.'); +} + export const stakingCurrencyToString = (amount: string, denom: string = DENOM_STAKING) => printableCoin({ amount, diff --git a/explorer-nextjs/package.json b/explorer-nextjs/package.json index 24e6656a35a..05ed83fcf23 100644 --- a/explorer-nextjs/package.json +++ b/explorer-nextjs/package.json @@ -17,7 +17,8 @@ "react-error-boundary": "^4.0.13", "material-react-table": "^2.12.1", "@mui/x-date-pickers": "7.1.1", - "@mui/x-data-grid": "7.1.1" + "@mui/x-data-grid": "7.1.1", + "@mui/x-charts": "^7.22.3" }, "devDependencies": { "@types/node": "^20", diff --git a/package.json b/package.json index 12d3f610462..d89f2f48a49 100644 --- a/package.json +++ b/package.json @@ -15,10 +15,7 @@ "explorer", "explorer-nextjs", "types", - "clients/validator", - "sdk/typescript/packages/**", - "sdk/typescript/examples/**", - "sdk/typescript/codegen/**" + "clients/validator" ], "scripts": { "nuke": "npx rimraf **/node_modules node_modules", diff --git a/yarn.lock b/yarn.lock index 40513f47f47..7f26c95e1cb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1219,6 +1219,13 @@ dependencies: regenerator-runtime "^0.14.0" +"@babel/runtime@^7.25.7", "@babel/runtime@^7.26.0": + version "7.26.0" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.26.0.tgz#8600c2f595f277c60815256418b85356a65173c1" + integrity sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw== + dependencies: + regenerator-runtime "^0.14.0" + "@babel/runtime@~7.5.4": version "7.5.5" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.5.5.tgz#74fba56d35efbeca444091c7850ccd494fd2f132" @@ -3095,6 +3102,11 @@ resolved "https://registry.yarnpkg.com/@mui/types/-/types-7.2.15.tgz#dadd232fe9a70be0d526630675dff3b110f30b53" integrity sha512-nbo7yPhtKJkdf9kcVOF8JZHPZTmqXjJ/tI0bdWgHg5tp9AnIN4Y7f7wm9T+0SyGYJk76+GYZ8Q5XaTYAsUHN0Q== +"@mui/types@^7.2.19": + version "7.2.19" + resolved "https://registry.yarnpkg.com/@mui/types/-/types-7.2.19.tgz#c941954dd24393fdce5f07830d44440cf4ab6c80" + integrity sha512-6XpZEM/Q3epK9RN8ENoXuygnqUQxE+siN/6rGRi2iwJPgBUR25mphYQ9ZI87plGh58YoZ5pp40bFvKYOCDJ3tA== + "@mui/utils@^5.10.3", "@mui/utils@^5.15.14", "@mui/utils@^5.7.0": version "5.15.14" resolved "https://registry.yarnpkg.com/@mui/utils/-/utils-5.15.14.tgz#e414d7efd5db00bfdc875273a40c0a89112ade3a" @@ -3117,6 +3129,53 @@ prop-types "^15.8.1" react-is "^18.3.1" +"@mui/utils@^5.16.6 || ^6.0.0": + version "6.1.8" + resolved "https://registry.yarnpkg.com/@mui/utils/-/utils-6.1.8.tgz#ae07cad4f6099eeb43dbc71267b4a96304ba3982" + integrity sha512-O2DWb1kz8hiANVcR7Z4gOB3SvPPsSQGUmStpyBDzde6dJIfBzgV9PbEQOBZd3EBsd1pB+Uv1z5LAJAbymmawrA== + dependencies: + "@babel/runtime" "^7.26.0" + "@mui/types" "^7.2.19" + "@types/prop-types" "^15.7.13" + clsx "^2.1.1" + prop-types "^15.8.1" + react-is "^18.3.1" + +"@mui/x-charts-vendor@7.20.0": + version "7.20.0" + resolved "https://registry.yarnpkg.com/@mui/x-charts-vendor/-/x-charts-vendor-7.20.0.tgz#b5858b91da0bde4f9c31f5360d05ade0b6eb5e31" + integrity sha512-pzlh7z/7KKs5o0Kk0oPcB+sY0+Dg7Q7RzqQowDQjpy5Slz6qqGsgOB5YUzn0L+2yRmvASc4Pe0914Ao3tMBogg== + dependencies: + "@babel/runtime" "^7.25.7" + "@types/d3-color" "^3.1.3" + "@types/d3-delaunay" "^6.0.4" + "@types/d3-interpolate" "^3.0.4" + "@types/d3-scale" "^4.0.8" + "@types/d3-shape" "^3.1.6" + "@types/d3-time" "^3.0.3" + d3-color "^3.1.0" + d3-delaunay "^6.0.4" + d3-interpolate "^3.0.1" + d3-scale "^4.0.2" + d3-shape "^3.2.0" + d3-time "^3.1.0" + delaunator "^5.0.1" + robust-predicates "^3.0.2" + +"@mui/x-charts@^7.22.3": + version "7.22.3" + resolved "https://registry.yarnpkg.com/@mui/x-charts/-/x-charts-7.22.3.tgz#bb8cb23ab368147634e7b9deae5225b9b93869ef" + integrity sha512-w23+AwIK86bpNWkuHewyQwOKi1wYbLDzrvUEqvZ9KVYzZvnqpJmbTKideX1pLVgSNt0On8NDXytzCntV48Nobw== + dependencies: + "@babel/runtime" "^7.25.7" + "@mui/utils" "^5.16.6 || ^6.0.0" + "@mui/x-charts-vendor" "7.20.0" + "@mui/x-internals" "7.21.0" + "@react-spring/rafz" "^9.7.5" + "@react-spring/web" "^9.7.5" + clsx "^2.1.1" + prop-types "^15.8.1" + "@mui/x-data-grid@7.1.1": version "7.1.1" resolved "https://registry.yarnpkg.com/@mui/x-data-grid/-/x-data-grid-7.1.1.tgz#ed4b852bf03c86d39bb4d35eacc35d5d0312f7ed" @@ -3162,6 +3221,14 @@ "@babel/runtime" "^7.24.8" "@mui/utils" "^5.16.5" +"@mui/x-internals@7.21.0": + version "7.21.0" + resolved "https://registry.yarnpkg.com/@mui/x-internals/-/x-internals-7.21.0.tgz#daca984059015b27efdb47bb44dc7ff4a6816673" + integrity sha512-94YNyZ0BhK5Z+Tkr90RKf47IVCW8R/1MvdUhh6MCQg6sZa74jsX+x+gEZ4kzuCqOsuyTyxikeQ8vVuCIQiP7UQ== + dependencies: + "@babel/runtime" "^7.25.7" + "@mui/utils" "^5.16.6 || ^6.0.0" + "@mui/x-tree-view@^7.11.1": version "7.11.1" resolved "https://registry.yarnpkg.com/@mui/x-tree-view/-/x-tree-view-7.11.1.tgz#77748013f368a9bd5f1e5e03adf3d6a788fb0f76" @@ -4306,6 +4373,51 @@ resolved "https://registry.yarnpkg.com/@react-icons/all-files/-/all-files-4.1.0.tgz#477284873a0821928224b6fc84c62d2534d6650b" integrity sha512-hxBI2UOuVaI3O/BhQfhtb4kcGn9ft12RWAFVMUeNjqqhLsHvFtzIkFaptBJpFDANTKoDfdVoHTKZDlwKCACbMQ== +"@react-spring/animated@~9.7.5": + version "9.7.5" + resolved "https://registry.yarnpkg.com/@react-spring/animated/-/animated-9.7.5.tgz#eb0373aaf99b879736b380c2829312dae3b05f28" + integrity sha512-Tqrwz7pIlsSDITzxoLS3n/v/YCUHQdOIKtOJf4yL6kYVSDTSmVK1LI1Q3M/uu2Sx4X3pIWF3xLUhlsA6SPNTNg== + dependencies: + "@react-spring/shared" "~9.7.5" + "@react-spring/types" "~9.7.5" + +"@react-spring/core@~9.7.5": + version "9.7.5" + resolved "https://registry.yarnpkg.com/@react-spring/core/-/core-9.7.5.tgz#72159079f52c1c12813d78b52d4f17c0bf6411f7" + integrity sha512-rmEqcxRcu7dWh7MnCcMXLvrf6/SDlSokLaLTxiPlAYi11nN3B5oiCUAblO72o+9z/87j2uzxa2Inm8UbLjXA+w== + dependencies: + "@react-spring/animated" "~9.7.5" + "@react-spring/shared" "~9.7.5" + "@react-spring/types" "~9.7.5" + +"@react-spring/rafz@^9.7.5", "@react-spring/rafz@~9.7.5": + version "9.7.5" + resolved "https://registry.yarnpkg.com/@react-spring/rafz/-/rafz-9.7.5.tgz#ee7959676e7b5d6a3813e8c17d5e50df98b95df9" + integrity sha512-5ZenDQMC48wjUzPAm1EtwQ5Ot3bLIAwwqP2w2owG5KoNdNHpEJV263nGhCeKKmuA3vG2zLLOdu3or6kuDjA6Aw== + +"@react-spring/shared@~9.7.5": + version "9.7.5" + resolved "https://registry.yarnpkg.com/@react-spring/shared/-/shared-9.7.5.tgz#6d513622df6ad750bbbd4dedb4ca0a653ec92073" + integrity sha512-wdtoJrhUeeyD/PP/zo+np2s1Z820Ohr/BbuVYv+3dVLW7WctoiN7std8rISoYoHpUXtbkpesSKuPIw/6U1w1Pw== + dependencies: + "@react-spring/rafz" "~9.7.5" + "@react-spring/types" "~9.7.5" + +"@react-spring/types@~9.7.5": + version "9.7.5" + resolved "https://registry.yarnpkg.com/@react-spring/types/-/types-9.7.5.tgz#e5dd180f3ed985b44fd2cd2f32aa9203752ef3e8" + integrity sha512-HVj7LrZ4ReHWBimBvu2SKND3cDVUPWKLqRTmWe/fNY6o1owGOX0cAHbdPDTMelgBlVbrTKrre6lFkhqGZErK/g== + +"@react-spring/web@^9.7.5": + version "9.7.5" + resolved "https://registry.yarnpkg.com/@react-spring/web/-/web-9.7.5.tgz#7d7782560b3a6fb9066b52824690da738605de80" + integrity sha512-lmvqGwpe+CSttsWNZVr+Dg62adtKhauGwLyGE/RRyZ8AAMLgb9x3NDMA5RMElXo+IMyTkPp7nxTB8ZQlmhb6JQ== + dependencies: + "@react-spring/animated" "~9.7.5" + "@react-spring/core" "~9.7.5" + "@react-spring/shared" "~9.7.5" + "@react-spring/types" "~9.7.5" + "@react-stately/calendar@^3.4.4": version "3.4.4" resolved "https://registry.yarnpkg.com/@react-stately/calendar/-/calendar-3.4.4.tgz#6e926058ddc8bc9f506c35eaf8a8a04859cb81df" @@ -6330,7 +6442,7 @@ resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-3.2.1.tgz#1f6658e3d2006c4fceac53fde464166859f8b8c5" integrity sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg== -"@types/d3-color@*": +"@types/d3-color@*", "@types/d3-color@^3.1.3": version "3.1.3" resolved "https://registry.yarnpkg.com/@types/d3-color/-/d3-color-3.1.3.tgz#368c961a18de721da8200e80bf3943fb53136af2" integrity sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A== @@ -6340,6 +6452,11 @@ resolved "https://registry.yarnpkg.com/@types/d3-color/-/d3-color-1.4.5.tgz#23bb1afda325549c6314ab60aa2aa28c4c6b1c37" integrity sha512-5sNP3DmtSnSozxcjqmzQKsDOuVJXZkceo1KJScDc1982kk/TS9mTPc6lpli1gTu1MIBF1YWutpHpjucNWcIj5g== +"@types/d3-delaunay@^6.0.4": + version "6.0.4" + resolved "https://registry.yarnpkg.com/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz#185c1a80cc807fdda2a3fe960f7c11c4a27952e1" + integrity sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw== + "@types/d3-dsv@*": version "3.0.7" resolved "https://registry.yarnpkg.com/@types/d3-dsv/-/d3-dsv-3.0.7.tgz#0a351f996dc99b37f4fa58b492c2d1c04e3dac17" @@ -6378,7 +6495,7 @@ dependencies: "@types/d3-color" "^1" -"@types/d3-interpolate@^3.0.1": +"@types/d3-interpolate@^3.0.1", "@types/d3-interpolate@^3.0.4": version "3.0.4" resolved "https://registry.yarnpkg.com/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz#412b90e84870285f2ff8a846c6eb60344f12a41c" integrity sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA== @@ -6390,7 +6507,7 @@ resolved "https://registry.yarnpkg.com/@types/d3-path/-/d3-path-3.1.0.tgz#2b907adce762a78e98828f0b438eaca339ae410a" integrity sha512-P2dlU/q51fkOc/Gfl3Ul9kicV7l+ra934qBFXCFhrZMOL6du1TM0pm1ThYvENukyOn5h9v+yMJ9Fn5JK4QozrQ== -"@types/d3-scale@^4.0.1", "@types/d3-scale@^4.0.2": +"@types/d3-scale@^4.0.1", "@types/d3-scale@^4.0.2", "@types/d3-scale@^4.0.8": version "4.0.8" resolved "https://registry.yarnpkg.com/@types/d3-scale/-/d3-scale-4.0.8.tgz#d409b5f9dcf63074464bf8ddfb8ee5a1f95945bb" integrity sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ== @@ -6402,7 +6519,7 @@ resolved "https://registry.yarnpkg.com/@types/d3-selection/-/d3-selection-1.4.6.tgz#3e6056117b19d8bb6c729c872ca7234622099fb6" integrity sha512-0MhJ/LzJe6/vQVxiYJnvNq5CD/MF6Qy0dLp4BEQ6Dz8oOaB0EMXfx1GGeBFSW+3VzgjaUrxK6uECDQj9VLa/Mg== -"@types/d3-shape@^3.1.0": +"@types/d3-shape@^3.1.0", "@types/d3-shape@^3.1.6": version "3.1.6" resolved "https://registry.yarnpkg.com/@types/d3-shape/-/d3-shape-3.1.6.tgz#65d40d5a548f0a023821773e39012805e6e31a72" integrity sha512-5KKk5aKGu2I+O6SONMYSNflgiP0WfZIQvVUMan50wHsLG1G94JlxEVnCpQARfTtzytuY0p/9PXXZb3I7giofIA== @@ -6414,6 +6531,11 @@ resolved "https://registry.yarnpkg.com/@types/d3-time/-/d3-time-3.0.3.tgz#3c186bbd9d12b9d84253b6be6487ca56b54f88be" integrity sha512-2p6olUZ4w3s+07q3Tm2dbiMZy5pCDfYwtLXXHUnVzXgQlZ/OyPtUz6OL382BkOuGlLXqfT+wqv8Fw2v8/0geBw== +"@types/d3-time@^3.0.3": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@types/d3-time/-/d3-time-3.0.4.tgz#8472feecd639691450dd8000eb33edd444e1323f" + integrity sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g== + "@types/d3-timer@^3.0.0": version "3.0.2" resolved "https://registry.yarnpkg.com/@types/d3-timer/-/d3-timer-3.0.2.tgz#70bbda77dc23aa727413e22e214afa3f0e852f70" @@ -6703,6 +6825,11 @@ resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.12.tgz#12bb1e2be27293c1406acb6af1c3f3a1481d98c6" integrity sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q== +"@types/prop-types@^15.7.13": + version "15.7.13" + resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.13.tgz#2af91918ee12d9d32914feb13f5326658461b451" + integrity sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA== + "@types/qrcode.react@^1.0.2": version "1.0.5" resolved "https://registry.yarnpkg.com/@types/qrcode.react/-/qrcode.react-1.0.5.tgz#d4ddcacee8f34d22a663029a230c5f0ab908cfb7" @@ -10107,11 +10234,18 @@ d3-array@^2.5.0: resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-2.0.0.tgz#8d625cab42ed9b8f601a1760a389f7ea9189d62e" integrity sha512-SPXi0TSKPD4g9tw0NMZFnR95XVgUZiBH+uUTqQuDu1OsE2zomHU7ho0FISciaPvosimixwHFl3WHLGabv6dDgQ== -"d3-color@1 - 3": +"d3-color@1 - 3", d3-color@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-3.1.0.tgz#395b2833dfac71507f12ac2f7af23bf819de24e2" integrity sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA== +d3-delaunay@^6.0.4: + version "6.0.4" + resolved "https://registry.yarnpkg.com/d3-delaunay/-/d3-delaunay-6.0.4.tgz#98169038733a0a5babbeda55054f795bb9e4a58b" + integrity sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A== + dependencies: + delaunator "5" + "d3-dispatch@1 - 2": version "2.0.0" resolved "https://registry.yarnpkg.com/d3-dispatch/-/d3-dispatch-2.0.0.tgz#8a18e16f76dd3fcaef42163c97b926aa9b55e7cf" @@ -10182,7 +10316,7 @@ d3-selection@2, d3-selection@^2.0.0: resolved "https://registry.yarnpkg.com/d3-selection/-/d3-selection-2.0.0.tgz#94a11638ea2141b7565f883780dabc7ef6a61066" integrity sha512-XoGGqhLUN/W14NmaqcO/bb1nqjDAw5WtSYb2X8wiuQWvSZUsUVYsOSkOybUrNvcBjaywBdYPy03eXHMXjk9nZA== -d3-shape@^3.1.0: +d3-shape@^3.1.0, d3-shape@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-3.2.0.tgz#a1a839cbd9ba45f28674c69d7f855bcf91dfc6a5" integrity sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA== @@ -10196,7 +10330,7 @@ d3-shape@^3.1.0: dependencies: d3-time "1 - 3" -"d3-time@1 - 3", "d3-time@2.1.1 - 3", d3-time@^3.0.0: +"d3-time@1 - 3", "d3-time@2.1.1 - 3", d3-time@^3.0.0, d3-time@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-3.1.0.tgz#9310db56e992e3c0175e1ef385e545e48a9bb5c7" integrity sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q== @@ -10489,6 +10623,13 @@ del@^4.1.1: pify "^4.0.1" rimraf "^2.6.3" +delaunator@5, delaunator@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/delaunator/-/delaunator-5.0.1.tgz#39032b08053923e924d6094fe2cde1a99cc51278" + integrity sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw== + dependencies: + robust-predicates "^3.0.2" + delay@^4.4.0: version "4.4.1" resolved "https://registry.yarnpkg.com/delay/-/delay-4.4.1.tgz#6e02d02946a1b6ab98b39262ced965acba2ac4d1" @@ -15438,6 +15579,11 @@ lru-cache@^7.4.4, lru-cache@^7.5.1, lru-cache@^7.7.1: resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.18.3.tgz#f793896e0fd0e954a59dfdd82f0773808df6aa89" integrity sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA== +lucide-react@^0.453.0: + version "0.453.0" + resolved "https://registry.yarnpkg.com/lucide-react/-/lucide-react-0.453.0.tgz#d37909a45a29d89680383a202ee861224b05ba6a" + integrity sha512-kL+RGZCcJi9BvJtzg2kshO192Ddy9hv3ij+cPrVPWSRzgCWCVazoQJxOjAwgK53NomL07HB7GPHW120FimjNhQ== + lunr@^2.3.9: version "2.3.9" resolved "https://registry.yarnpkg.com/lunr/-/lunr-2.3.9.tgz#18b123142832337dd6e964df1a5a7707b25d35e1" @@ -19067,6 +19213,11 @@ ripemd160@^2.0.0, ripemd160@^2.0.1, ripemd160@^2.0.2: hash-base "^3.0.0" inherits "^2.0.1" +robust-predicates@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/robust-predicates/-/robust-predicates-3.0.2.tgz#d5b28528c4824d20fc48df1928d41d9efa1ad771" + integrity sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg== + rollup-plugin-dts@^5.0.0, rollup-plugin-dts@^5.2.0: version "5.3.1" resolved "https://registry.yarnpkg.com/rollup-plugin-dts/-/rollup-plugin-dts-5.3.1.tgz#c2841269a3a5cb986b7791b0328e6a178eba108f" From 6eddc913f49b1333309f95f6eaaafda8059ae200 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20H=C3=A4ggblad?= <jon.haggblad@gmail.com> Date: Thu, 5 Dec 2024 11:34:44 +0100 Subject: [PATCH 08/18] Derive serialize for UserAgent (#5210) (#5217) --- common/http-api-client/src/user_agent.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/common/http-api-client/src/user_agent.rs b/common/http-api-client/src/user_agent.rs index 1cdbf458257..d47cb570c4b 100644 --- a/common/http-api-client/src/user_agent.rs +++ b/common/http-api-client/src/user_agent.rs @@ -5,8 +5,9 @@ use std::fmt; use http::HeaderValue; use nym_bin_common::build_information::{BinaryBuildInformation, BinaryBuildInformationOwned}; +use serde::{Deserialize, Serialize}; -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] pub struct UserAgent { pub application: String, pub version: String, From 585d752c83864e36bec93b294a8988d817405e99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bogdan-=C8=98tefan=20Neac=C5=9Fu?= <bogdan@nymtech.net> Date: Thu, 5 Dec 2024 17:43:43 +0200 Subject: [PATCH 09/18] Extend raw ws fd for gateway client (#5218) (#5220) --- common/client-libs/gateway-client/src/socket_state.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/common/client-libs/gateway-client/src/socket_state.rs b/common/client-libs/gateway-client/src/socket_state.rs index 942f6506148..6dcd055c9ee 100644 --- a/common/client-libs/gateway-client/src/socket_state.rs +++ b/common/client-libs/gateway-client/src/socket_state.rs @@ -46,7 +46,8 @@ pub(crate) fn ws_fd(_conn: &WsConn) -> Option<RawFd> { #[cfg(unix)] match _conn.get_ref() { MaybeTlsStream::Plain(stream) => Some(stream.as_raw_fd()), - &_ => None, + MaybeTlsStream::Rustls(tls_stream) => Some(tls_stream.as_raw_fd()), + _ => None, } #[cfg(not(unix))] None From b481da9c55bf8500dbf69e5abe1689ae8a026b0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= <jedrzej.stuczynski@gmail.com> Date: Thu, 5 Dec 2024 16:18:14 +0000 Subject: [PATCH 10/18] nym-api NMv1 adjustments (#5209) * ignore legacy nodes for test route selection and bias selection with existing score * feature: dont keep persistent GatewayClient inside NMv1 (#5211) * removed overly complex logic for requesting mutex permits for packet processing * dont keep persistent gateway connections. instead make them on demand * clippy --- .../gateway-client/src/bandwidth.rs | 4 +- .../gateway-client/src/client/mod.rs | 24 +- .../gateway-client/src/socket_state.rs | 5 + common/crypto/src/asymmetric/identity/mod.rs | 9 + .../src/network_monitor/gateways_reader.rs | 20 +- nym-api/src/network_monitor/mod.rs | 55 +-- .../monitor/gateway_client_handle.rs | 54 +++ .../monitor/gateway_clients_cache.rs | 102 ------ .../monitor/gateways_pinger.rs | 165 --------- nym-api/src/network_monitor/monitor/mod.rs | 31 +- .../src/network_monitor/monitor/preparer.rs | 112 +++--- .../src/network_monitor/monitor/processor.rs | 211 +++++------ .../src/network_monitor/monitor/receiver.rs | 6 +- nym-api/src/network_monitor/monitor/sender.rs | 339 +++++++----------- nym-api/src/nym_contract_cache/cache/mod.rs | 8 - nym-api/src/support/cli/run.rs | 3 +- nym-api/src/support/config/mod.rs | 16 +- 17 files changed, 407 insertions(+), 757 deletions(-) create mode 100644 nym-api/src/network_monitor/monitor/gateway_client_handle.rs delete mode 100644 nym-api/src/network_monitor/monitor/gateway_clients_cache.rs delete mode 100644 nym-api/src/network_monitor/monitor/gateways_pinger.rs diff --git a/common/client-libs/gateway-client/src/bandwidth.rs b/common/client-libs/gateway-client/src/bandwidth.rs index 25e9a44394d..9fd43765bdd 100644 --- a/common/client-libs/gateway-client/src/bandwidth.rs +++ b/common/client-libs/gateway-client/src/bandwidth.rs @@ -87,8 +87,10 @@ impl ClientBandwidth { if remaining < 0 { tracing::warn!("OUT OF BANDWIDTH. remaining: {remaining_bi2}"); - } else { + } else if remaining < 1_000_000 { tracing::info!("remaining bandwidth: {remaining_bi2}"); + } else { + tracing::debug!("remaining bandwidth: {remaining_bi2}"); } self.inner diff --git a/common/client-libs/gateway-client/src/client/mod.rs b/common/client-libs/gateway-client/src/client/mod.rs index 6c1d9fa0b28..6cb7b83f020 100644 --- a/common/client-libs/gateway-client/src/client/mod.rs +++ b/common/client-libs/gateway-client/src/client/mod.rs @@ -139,6 +139,10 @@ impl<C, St> GatewayClient<C, St> { self.gateway_identity } + pub fn shared_key(&self) -> Option<Arc<SharedGatewayKey>> { + self.shared_key.clone() + } + pub fn ws_fd(&self) -> Option<RawFd> { match &self.connection { SocketState::Available(conn) => ws_fd(conn.as_ref()), @@ -408,7 +412,7 @@ impl<C, St> GatewayClient<C, St> { } Some(_) => { - info!("the gateway is using exactly the same (or older) protocol version as we are. We're good to continue!"); + debug!("the gateway is using exactly the same (or older) protocol version as we are. We're good to continue!"); Ok(()) } } @@ -992,24 +996,6 @@ impl<C, St> GatewayClient<C, St> { } Ok(()) } - - #[deprecated(note = "this method does not deal with upgraded keys for legacy clients")] - pub async fn authenticate_and_start( - &mut self, - ) -> Result<AuthenticationResponse, GatewayClientError> - where - C: DkgQueryClient + Send + Sync, - St: CredentialStorage, - <St as CredentialStorage>::StorageError: Send + Sync + 'static, - { - let shared_key = self.perform_initial_authentication().await?; - self.claim_initial_bandwidth().await?; - - // this call is NON-blocking - self.start_listening_for_mixnet_messages()?; - - Ok(shared_key) - } } // type alias for an ease of use diff --git a/common/client-libs/gateway-client/src/socket_state.rs b/common/client-libs/gateway-client/src/socket_state.rs index 6dcd055c9ee..319a7bb624d 100644 --- a/common/client-libs/gateway-client/src/socket_state.rs +++ b/common/client-libs/gateway-client/src/socket_state.rs @@ -111,6 +111,11 @@ impl PartiallyDelegatedRouter { } }; + if self.stream_return.is_canceled() { + // nothing to do, receiver has been dropped + return; + } + let return_res = match ret { Err(err) => self.stream_return.send(Err(err)), Ok(_) => { diff --git a/common/crypto/src/asymmetric/identity/mod.rs b/common/crypto/src/asymmetric/identity/mod.rs index 4b51aa2f641..a432b0c806d 100644 --- a/common/crypto/src/asymmetric/identity/mod.rs +++ b/common/crypto/src/asymmetric/identity/mod.rs @@ -6,6 +6,7 @@ use ed25519_dalek::{Signer, SigningKey}; pub use ed25519_dalek::{Verifier, PUBLIC_KEY_LENGTH, SECRET_KEY_LENGTH, SIGNATURE_LENGTH}; use nym_pemstore::traits::{PemStorableKey, PemStorableKeyPair}; use std::fmt::{self, Debug, Display, Formatter}; +use std::hash::{Hash, Hasher}; use std::str::FromStr; use thiserror::Error; use zeroize::{Zeroize, ZeroizeOnDrop}; @@ -122,6 +123,14 @@ impl PemStorableKeyPair for KeyPair { #[derive(Copy, Clone, Eq, PartialEq)] pub struct PublicKey(ed25519_dalek::VerifyingKey); +impl Hash for PublicKey { + fn hash<H: Hasher>(&self, state: &mut H) { + // each public key has unique bytes representation which can be used + // for the hash implementation + self.to_bytes().hash(state) + } +} + impl Display for PublicKey { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { Display::fmt(&self.to_base58_string(), f) diff --git a/nym-api/src/network_monitor/gateways_reader.rs b/nym-api/src/network_monitor/gateways_reader.rs index 01bec7c52a6..bc3eeaa44cb 100644 --- a/nym-api/src/network_monitor/gateways_reader.rs +++ b/nym-api/src/network_monitor/gateways_reader.rs @@ -2,9 +2,8 @@ // SPDX-License-Identifier: GPL-3.0-only use futures::Stream; -use nym_crypto::asymmetric::identity; +use nym_crypto::asymmetric::{ed25519, identity}; use nym_gateway_client::{AcknowledgementReceiver, MixnetMessageReceiver}; -use nym_mixnet_contract_common::IdentityKey; use std::pin::Pin; use std::task::{Context, Poll}; use tokio_stream::StreamMap; @@ -15,8 +14,8 @@ pub(crate) enum GatewayMessages { } pub(crate) struct GatewaysReader { - ack_map: StreamMap<IdentityKey, AcknowledgementReceiver>, - stream_map: StreamMap<IdentityKey, MixnetMessageReceiver>, + ack_map: StreamMap<ed25519::PublicKey, AcknowledgementReceiver>, + stream_map: StreamMap<ed25519::PublicKey, MixnetMessageReceiver>, } impl GatewaysReader { @@ -33,19 +32,18 @@ impl GatewaysReader { message_receiver: MixnetMessageReceiver, ack_receiver: AcknowledgementReceiver, ) { - let channel_id = id.to_string(); - self.stream_map.insert(channel_id.clone(), message_receiver); - self.ack_map.insert(channel_id, ack_receiver); + self.stream_map.insert(id, message_receiver); + self.ack_map.insert(id, ack_receiver); } - pub fn remove_receivers(&mut self, id: &str) { - self.stream_map.remove(id); - self.ack_map.remove(id); + pub fn remove_receivers(&mut self, id: ed25519::PublicKey) { + self.stream_map.remove(&id); + self.ack_map.remove(&id); } } impl Stream for GatewaysReader { - type Item = (IdentityKey, GatewayMessages); + type Item = (ed25519::PublicKey, GatewayMessages); fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> { match Pin::new(&mut self.ack_map).poll_next(cx) { diff --git a/nym-api/src/network_monitor/mod.rs b/nym-api/src/network_monitor/mod.rs index 699498b2872..a7f9391fe28 100644 --- a/nym-api/src/network_monitor/mod.rs +++ b/nym-api/src/network_monitor/mod.rs @@ -12,10 +12,12 @@ use crate::network_monitor::monitor::sender::PacketSender; use crate::network_monitor::monitor::summary_producer::SummaryProducer; use crate::network_monitor::monitor::Monitor; use crate::node_describe_cache::DescribedNodes; +use crate::node_status_api::NodeStatusCache; use crate::nym_contract_cache::cache::NymContractCache; use crate::storage::NymApiStorage; use crate::support::caching::cache::SharedCache; -use crate::support::{config, nyxd}; +use crate::support::config::Config; +use crate::support::nyxd; use futures::channel::mpsc; use nym_bandwidth_controller::BandwidthController; use nym_credential_storage::persistent_storage::PersistentStorage; @@ -35,9 +37,10 @@ pub(crate) mod test_route; pub(crate) const ROUTE_TESTING_TEST_NONCE: u64 = 0; pub(crate) fn setup<'a>( - config: &'a config::NetworkMonitor, + config: &'a Config, nym_contract_cache: &NymContractCache, described_cache: SharedCache<DescribedNodes>, + node_status_cache: NodeStatusCache, storage: &NymApiStorage, nyxd_client: nyxd::Client, ) -> NetworkMonitorBuilder<'a> { @@ -47,24 +50,27 @@ pub(crate) fn setup<'a>( storage.to_owned(), nym_contract_cache.clone(), described_cache, + node_status_cache, ) } pub(crate) struct NetworkMonitorBuilder<'a> { - config: &'a config::NetworkMonitor, + config: &'a Config, nyxd_client: nyxd::Client, node_status_storage: NymApiStorage, contract_cache: NymContractCache, described_cache: SharedCache<DescribedNodes>, + node_status_cache: NodeStatusCache, } impl<'a> NetworkMonitorBuilder<'a> { pub(crate) fn new( - config: &'a config::NetworkMonitor, + config: &'a Config, nyxd_client: nyxd::Client, node_status_storage: NymApiStorage, contract_cache: NymContractCache, described_cache: SharedCache<DescribedNodes>, + node_status_cache: NodeStatusCache, ) -> Self { NetworkMonitorBuilder { config, @@ -72,10 +78,11 @@ impl<'a> NetworkMonitorBuilder<'a> { node_status_storage, contract_cache, described_cache, + node_status_cache, } } - pub(crate) async fn build<R: MessageReceiver + Send + 'static>( + pub(crate) async fn build<R: MessageReceiver + Send + Sync + 'static>( self, ) -> NetworkMonitorRunnables<R> { // TODO: those keys change constant throughout the whole execution of the monitor. @@ -94,7 +101,8 @@ impl<'a> NetworkMonitorBuilder<'a> { let packet_preparer = new_packet_preparer( self.contract_cache, self.described_cache, - self.config.debug.per_node_test_packets, + self.node_status_cache, + self.config.network_monitor.debug.per_node_test_packets, Arc::clone(&ack_key), *identity_keypair.public_key(), *encryption_keypair.public_key(), @@ -103,7 +111,11 @@ impl<'a> NetworkMonitorBuilder<'a> { let bandwidth_controller = { BandwidthController::new( nym_credential_storage::initialise_persistent_storage( - &self.config.storage_paths.credentials_database_path, + &self + .config + .network_monitor + .storage_paths + .credentials_database_path, ) .await, self.nyxd_client.clone(), @@ -114,9 +126,7 @@ impl<'a> NetworkMonitorBuilder<'a> { self.config, gateway_status_update_sender, Arc::clone(&identity_keypair), - self.config.debug.gateway_sending_rate, bandwidth_controller, - self.config.debug.disabled_credentials_mode, ); let received_processor = new_received_processor( @@ -124,14 +134,15 @@ impl<'a> NetworkMonitorBuilder<'a> { Arc::clone(&encryption_keypair), ack_key, ); - let summary_producer = new_summary_producer(self.config.debug.per_node_test_packets); + let summary_producer = + new_summary_producer(self.config.network_monitor.debug.per_node_test_packets); let packet_receiver = new_packet_receiver( gateway_status_update_receiver, received_processor_sender_channel, ); let monitor = Monitor::new( - self.config, + &self.config.network_monitor, packet_preparer, packet_sender, received_processor, @@ -147,12 +158,12 @@ impl<'a> NetworkMonitorBuilder<'a> { } } -pub(crate) struct NetworkMonitorRunnables<R: MessageReceiver + Send + 'static> { +pub(crate) struct NetworkMonitorRunnables<R: MessageReceiver + Send + Sync + 'static> { monitor: Monitor<R>, packet_receiver: PacketReceiver, } -impl<R: MessageReceiver + Send + 'static> NetworkMonitorRunnables<R> { +impl<R: MessageReceiver + Send + Sync + 'static> NetworkMonitorRunnables<R> { // TODO: note, that is not exactly doing what we want, because when // `ReceivedProcessor` is constructed, it already spawns a future // this needs to be refactored! @@ -169,6 +180,7 @@ impl<R: MessageReceiver + Send + 'static> NetworkMonitorRunnables<R> { fn new_packet_preparer( contract_cache: NymContractCache, described_cache: SharedCache<DescribedNodes>, + node_status_cache: NodeStatusCache, per_node_test_packets: usize, ack_key: Arc<AckKey>, self_public_identity: identity::PublicKey, @@ -177,6 +189,7 @@ fn new_packet_preparer( PacketPreparer::new( contract_cache, described_cache, + node_status_cache, per_node_test_packets, ack_key, self_public_identity, @@ -185,22 +198,16 @@ fn new_packet_preparer( } fn new_packet_sender( - config: &config::NetworkMonitor, + config: &Config, gateways_status_updater: GatewayClientUpdateSender, local_identity: Arc<identity::KeyPair>, - max_sending_rate: usize, bandwidth_controller: BandwidthController<nyxd::Client, PersistentStorage>, - disabled_credentials_mode: bool, ) -> PacketSender { PacketSender::new( + config, gateways_status_updater, local_identity, - config.debug.gateway_response_timeout, - config.debug.gateway_connection_timeout, - config.debug.max_concurrent_gateway_clients, - max_sending_rate, bandwidth_controller, - disabled_credentials_mode, ) } @@ -227,10 +234,11 @@ fn new_packet_receiver( // TODO: 1) does it still have to have separate builder or could we get rid of it now? // TODO: 2) how do we make it non-async as other 'start' methods? -pub(crate) async fn start<R: MessageReceiver + Send + 'static>( - config: &config::NetworkMonitor, +pub(crate) async fn start<R: MessageReceiver + Send + Sync + 'static>( + config: &Config, nym_contract_cache: &NymContractCache, described_cache: SharedCache<DescribedNodes>, + node_status_cache: NodeStatusCache, storage: &NymApiStorage, nyxd_client: nyxd::Client, shutdown: &TaskManager, @@ -239,6 +247,7 @@ pub(crate) async fn start<R: MessageReceiver + Send + 'static>( config, nym_contract_cache, described_cache, + node_status_cache, storage, nyxd_client, ); diff --git a/nym-api/src/network_monitor/monitor/gateway_client_handle.rs b/nym-api/src/network_monitor/monitor/gateway_client_handle.rs new file mode 100644 index 00000000000..6e3f157efb9 --- /dev/null +++ b/nym-api/src/network_monitor/monitor/gateway_client_handle.rs @@ -0,0 +1,54 @@ +// Copyright 2021 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: GPL-3.0-only + +use crate::network_monitor::monitor::receiver::{GatewayClientUpdate, GatewayClientUpdateSender}; +use crate::support::nyxd; +use nym_credential_storage::persistent_storage::PersistentStorage; +use nym_gateway_client::GatewayClient; +use std::ops::{Deref, DerefMut}; +use tracing::warn; + +pub(crate) struct GatewayClientHandle { + client: GatewayClient<nyxd::Client, PersistentStorage>, + gateways_status_updater: GatewayClientUpdateSender, +} + +impl GatewayClientHandle { + pub(crate) fn new( + client: GatewayClient<nyxd::Client, PersistentStorage>, + gateways_status_updater: GatewayClientUpdateSender, + ) -> Self { + GatewayClientHandle { + client, + gateways_status_updater, + } + } +} + +impl Drop for GatewayClientHandle { + fn drop(&mut self) { + if self + .gateways_status_updater + .unbounded_send(GatewayClientUpdate::Disconnect( + self.client.gateway_identity(), + )) + .is_err() + { + warn!("fail to cleanly shutdown gateway connection") + } + } +} + +impl Deref for GatewayClientHandle { + type Target = GatewayClient<nyxd::Client, PersistentStorage>; + + fn deref(&self) -> &Self::Target { + &self.client + } +} + +impl DerefMut for GatewayClientHandle { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.client + } +} diff --git a/nym-api/src/network_monitor/monitor/gateway_clients_cache.rs b/nym-api/src/network_monitor/monitor/gateway_clients_cache.rs deleted file mode 100644 index c791f9a6af8..00000000000 --- a/nym-api/src/network_monitor/monitor/gateway_clients_cache.rs +++ /dev/null @@ -1,102 +0,0 @@ -// Copyright 2021 - Nym Technologies SA <contact@nymtech.net> -// SPDX-License-Identifier: GPL-3.0-only - -use crate::support::nyxd; -use nym_credential_storage::persistent_storage::PersistentStorage; -use nym_crypto::asymmetric::identity::PUBLIC_KEY_LENGTH; -use nym_gateway_client::GatewayClient; -use std::collections::HashMap; -use std::sync::Arc; -use tokio::sync::{Mutex, MutexGuard, TryLockError}; - -pub(crate) struct GatewayClientHandle(Arc<GatewayClientHandleInner>); - -struct GatewayClientHandleInner { - client: Mutex<Option<GatewayClient<nyxd::Client, PersistentStorage>>>, - raw_identity: [u8; PUBLIC_KEY_LENGTH], -} - -pub(crate) struct UnlockedGatewayClientHandle<'a>( - MutexGuard<'a, Option<GatewayClient<nyxd::Client, PersistentStorage>>>, -); - -impl GatewayClientHandle { - pub(crate) fn new(gateway_client: GatewayClient<nyxd::Client, PersistentStorage>) -> Self { - GatewayClientHandle(Arc::new(GatewayClientHandleInner { - raw_identity: gateway_client.gateway_identity().to_bytes(), - client: Mutex::new(Some(gateway_client)), - })) - } - - pub(crate) fn ptr_eq(&self, other: &Self) -> bool { - Arc::ptr_eq(&self.0, &other.0) - } - - // this could have also been achieved with a normal #[derive(Clone)] but I prefer to be explicit about it, - // because clippy would suggest some potentially confusing 'simplifications' regarding clone - pub(crate) fn clone_data_pointer(&self) -> Self { - GatewayClientHandle(Arc::clone(&self.0)) - } - - pub(crate) fn raw_identity(&self) -> [u8; PUBLIC_KEY_LENGTH] { - self.0.raw_identity - } - - pub(crate) async fn is_invalid(&self) -> bool { - self.0.client.lock().await.is_none() - } - - pub(crate) async fn lock_client(&self) -> UnlockedGatewayClientHandle<'_> { - UnlockedGatewayClientHandle(self.0.client.lock().await) - } - - pub(crate) fn lock_client_unchecked(&self) -> UnlockedGatewayClientHandle<'_> { - UnlockedGatewayClientHandle(self.0.client.try_lock().unwrap()) - } - - pub(crate) fn try_lock_client(&self) -> Result<UnlockedGatewayClientHandle<'_>, TryLockError> { - self.0.client.try_lock().map(UnlockedGatewayClientHandle) - } -} - -impl UnlockedGatewayClientHandle<'_> { - pub(crate) fn get_mut_unchecked( - &mut self, - ) -> &mut GatewayClient<nyxd::Client, PersistentStorage> { - self.0.as_mut().unwrap() - } - - pub(crate) fn inner_mut( - &mut self, - ) -> Option<&mut GatewayClient<nyxd::Client, PersistentStorage>> { - self.0.as_mut() - } - - pub(crate) fn invalidate(&mut self) { - *self.0 = None - } -} - -pub(crate) type GatewayClientsMap = HashMap<[u8; PUBLIC_KEY_LENGTH], GatewayClientHandle>; - -#[derive(Clone)] -pub(crate) struct ActiveGatewayClients { - // there is no point in using an RwLock here as there will only ever be two readers here and both - // potentially need write access. - // A BiLock would have been slightly better than a normal Mutex since it's optimised for two - // owners, but it's behind `unstable` feature flag in futures and it would be a headache if the API - // changed. - inner: Arc<Mutex<GatewayClientsMap>>, -} - -impl ActiveGatewayClients { - pub(crate) fn new() -> Self { - ActiveGatewayClients { - inner: Arc::new(Mutex::new(HashMap::new())), - } - } - - pub(crate) async fn lock(&self) -> MutexGuard<'_, GatewayClientsMap> { - self.inner.lock().await - } -} diff --git a/nym-api/src/network_monitor/monitor/gateways_pinger.rs b/nym-api/src/network_monitor/monitor/gateways_pinger.rs deleted file mode 100644 index ed09c2d0906..00000000000 --- a/nym-api/src/network_monitor/monitor/gateways_pinger.rs +++ /dev/null @@ -1,165 +0,0 @@ -// Copyright 2021 - Nym Technologies SA <contact@nymtech.net> -// SPDX-License-Identifier: GPL-3.0-only - -use crate::network_monitor::monitor::gateway_clients_cache::ActiveGatewayClients; -use crate::network_monitor::monitor::receiver::{GatewayClientUpdate, GatewayClientUpdateSender}; -use nym_crypto::asymmetric::identity; -use nym_crypto::asymmetric::identity::PUBLIC_KEY_LENGTH; -use nym_task::TaskClient; -use std::time::Duration; -use tokio::time::{sleep, Instant}; -use tracing::{debug, info, trace, warn}; - -// TODO: should it perhaps be moved to config along other timeout values? -const PING_TIMEOUT: Duration = Duration::from_secs(3); - -pub(crate) struct GatewayPinger { - gateway_clients: ActiveGatewayClients, - gateways_status_updater: GatewayClientUpdateSender, - pinging_interval: Duration, -} - -impl GatewayPinger { - pub(crate) fn new( - gateway_clients: ActiveGatewayClients, - gateways_status_updater: GatewayClientUpdateSender, - pinging_interval: Duration, - ) -> Self { - GatewayPinger { - gateway_clients, - gateways_status_updater, - pinging_interval, - } - } - - fn notify_connection_failure(&self, raw_gateway_id: [u8; PUBLIC_KEY_LENGTH]) { - // if this unwrap failed it means something extremely weird is going on - // and we got some solar flare bitflip type of corruption - let gateway_key = identity::PublicKey::from_bytes(&raw_gateway_id) - .expect("failed to recover gateways public key from valid bytes"); - - // remove the gateway listener channels - self.gateways_status_updater - .unbounded_send(GatewayClientUpdate::Failure(gateway_key)) - .expect("packet receiver seems to have died!"); - } - - async fn ping_and_cleanup_all_gateways(&self) { - info!("Pinging all active gateways"); - - let lock_acquire_start = Instant::now(); - let active_gateway_clients_guard = self.gateway_clients.lock().await; - trace!( - "Acquiring lock took {:?}", - Instant::now().duration_since(lock_acquire_start) - ); - - if active_gateway_clients_guard.is_empty() { - debug!("no gateways to ping"); - return; - } - - // don't keep the guard the entire time - clone all Arcs and drop it - // - // this clippy warning is a false positive as we cannot get rid of the collect by moving - // everything into a single iterator as it would require us to hold the lock the entire time - // and that is exactly what we want to avoid - #[allow(clippy::needless_collect)] - let active_gateway_clients = active_gateway_clients_guard - .iter() - .map(|(_, handle)| handle.clone_data_pointer()) - .collect::<Vec<_>>(); - drop(active_gateway_clients_guard); - - let ping_start = Instant::now(); - - let mut clients_to_purge = Vec::new(); - - // since we don't need to wait for response, we can just ping all gateways sequentially - // if it becomes problem later on, we can adjust it. - for client_handle in active_gateway_clients.into_iter() { - trace!( - "Pinging: {}", - identity::PublicKey::from_bytes(&client_handle.raw_identity()) - .unwrap() - .to_base58_string() - ); - // if we fail to obtain the lock it means the client is being currently used to send messages - // and hence we don't need to ping it to keep connection alive - if let Ok(mut unlocked_handle) = client_handle.try_lock_client() { - if let Some(active_client) = unlocked_handle.inner_mut() { - match tokio::time::timeout(PING_TIMEOUT, active_client.send_ping_message()) - .await - { - Err(_timeout) => { - warn!( - "we timed out trying to ping {} - assuming the connection is dead.", - active_client.gateway_identity().to_base58_string(), - ); - clients_to_purge.push(client_handle.raw_identity()); - } - Ok(Err(err)) => { - warn!( - "failed to send ping message to gateway {} - {} - assuming the connection is dead.", - active_client.gateway_identity().to_base58_string(), - err, - ); - clients_to_purge.push(client_handle.raw_identity()); - } - _ => {} - } - } else { - clients_to_purge.push(client_handle.raw_identity()); - } - } - } - - info!( - "Purging {} gateways, acquiring lock", - clients_to_purge.len() - ); - // purge all dead connections - // reacquire the guard - let lock_acquire_start = Instant::now(); - let mut active_gateway_clients_guard = self.gateway_clients.lock().await; - info!( - "Acquiring lock took {:?}", - Instant::now().duration_since(lock_acquire_start) - ); - - for gateway_id in clients_to_purge.into_iter() { - if let Some(removed_handle) = active_gateway_clients_guard.remove(&gateway_id) { - if !removed_handle.is_invalid().await { - info!("Handle is invalid, purging"); - // it was not invalidated by the packet sender meaning it probably was some unbonded node - // that was never cleared - self.notify_connection_failure(gateway_id); - } - info!("Handle is not invalid, not purged") - } - } - - let ping_end = Instant::now(); - let time_taken = ping_end.duration_since(ping_start); - info!("Pinging all active gateways took {:?}", time_taken); - } - - pub(crate) async fn run(&self, mut shutdown: TaskClient) { - while !shutdown.is_shutdown() { - tokio::select! { - _ = sleep(self.pinging_interval) => { - tokio::select! { - biased; - _ = shutdown.recv() => { - trace!("GatewaysPinger: Received shutdown"); - } - _ = self.ping_and_cleanup_all_gateways() => (), - } - } - _ = shutdown.recv() => { - trace!("GatewaysPinger: Received shutdown"); - } - } - } - } -} diff --git a/nym-api/src/network_monitor/monitor/mod.rs b/nym-api/src/network_monitor/monitor/mod.rs index a5ea5ab818d..1f659b86461 100644 --- a/nym-api/src/network_monitor/monitor/mod.rs +++ b/nym-api/src/network_monitor/monitor/mod.rs @@ -17,15 +17,14 @@ use std::collections::{HashMap, HashSet}; use tokio::time::{sleep, Duration, Instant}; use tracing::{debug, error, info, trace}; -pub(crate) mod gateway_clients_cache; -pub(crate) mod gateways_pinger; +pub(crate) mod gateway_client_handle; pub(crate) mod preparer; pub(crate) mod processor; pub(crate) mod receiver; pub(crate) mod sender; pub(crate) mod summary_producer; -pub(super) struct Monitor<R: MessageReceiver + Send + 'static> { +pub(super) struct Monitor<R: MessageReceiver + Send + Sync + 'static> { test_nonce: u64, packet_preparer: PacketPreparer, packet_sender: PacketSender, @@ -33,7 +32,6 @@ pub(super) struct Monitor<R: MessageReceiver + Send + 'static> { summary_producer: SummaryProducer, node_status_storage: NymApiStorage, run_interval: Duration, - gateway_ping_interval: Duration, packet_delivery_timeout: Duration, /// Number of test packets sent via each "random" route to verify whether they work correctly. @@ -49,7 +47,7 @@ pub(super) struct Monitor<R: MessageReceiver + Send + 'static> { packet_type: PacketType, } -impl<R: MessageReceiver + Send> Monitor<R> { +impl<R: MessageReceiver + Send + Sync> Monitor<R> { pub(super) fn new( config: &config::NetworkMonitor, packet_preparer: PacketPreparer, @@ -67,7 +65,6 @@ impl<R: MessageReceiver + Send> Monitor<R> { summary_producer, node_status_storage, run_interval: config.debug.run_interval, - gateway_ping_interval: config.debug.gateway_ping_interval, packet_delivery_timeout: config.debug.packet_delivery_timeout, route_test_packets: config.debug.route_test_packets, test_routes: config.debug.test_routes, @@ -149,12 +146,15 @@ impl<R: MessageReceiver + Send> Monitor<R> { packets.push(gateway_packets); } - self.received_processor.set_route_test_nonce().await; - self.packet_sender.send_packets(packets).await; + self.received_processor.set_route_test_nonce(); + let gateway_clients = self.packet_sender.send_packets(packets).await; // give the packets some time to traverse the network sleep(self.packet_delivery_timeout).await; + // start all the disconnections in the background + drop(gateway_clients); + let received = self.received_processor.return_received().await; let mut results = self.analyse_received_test_route_packets(&received); @@ -211,7 +211,7 @@ impl<R: MessageReceiver + Send> Monitor<R> { // the actual target let candidates = match self .packet_preparer - .prepare_test_routes(remaining * 2, &mut blacklist) + .prepare_test_routes(remaining * 2) .await { Some(candidates) => candidates, @@ -261,12 +261,11 @@ impl<R: MessageReceiver + Send> Monitor<R> { .flat_map(|packets| packets.packets.iter()) .count(); - self.received_processor - .set_new_test_nonce(self.test_nonce) - .await; + self.received_processor.set_new_test_nonce(self.test_nonce); info!("Sending packets to all gateways..."); - self.packet_sender + let gateway_clients = self + .packet_sender .send_packets(prepared_packets.packets) .await; @@ -278,6 +277,9 @@ impl<R: MessageReceiver + Send> Monitor<R> { // give the packets some time to traverse the network sleep(self.packet_delivery_timeout).await; + // start all the disconnections in the background + drop(gateway_clients); + let received = self.received_processor.return_received().await; let total_received = received.len(); info!("Test routes: {:#?}", routes); @@ -329,9 +331,6 @@ impl<R: MessageReceiver + Send> Monitor<R> { .wait_for_validator_cache_initial_values(self.minimum_test_routes) .await; - self.packet_sender - .spawn_gateways_pinger(self.gateway_ping_interval, shutdown.clone()); - let mut run_interval = tokio::time::interval(self.run_interval); while !shutdown.is_shutdown() { tokio::select! { diff --git a/nym-api/src/network_monitor/monitor/preparer.rs b/nym-api/src/network_monitor/monitor/preparer.rs index 04fdceebefb..50c3be80baa 100644 --- a/nym-api/src/network_monitor/monitor/preparer.rs +++ b/nym-api/src/network_monitor/monitor/preparer.rs @@ -4,10 +4,12 @@ use crate::network_monitor::monitor::sender::GatewayPackets; use crate::network_monitor::test_route::TestRoute; use crate::node_describe_cache::{DescribedNodes, NodeDescriptionTopologyExt}; +use crate::node_status_api::NodeStatusCache; use crate::nym_contract_cache::cache::{CachedRewardedSet, NymContractCache}; use crate::support::caching::cache::SharedCache; use nym_api_requests::legacy::{LegacyGatewayBondWithId, LegacyMixNodeBondWithLayer}; -use nym_api_requests::models::NymNodeDescription; +use nym_api_requests::models::{NodeAnnotation, NymNodeDescription}; +use nym_contracts_common::NaiveFloat; use nym_crypto::asymmetric::{encryption, identity}; use nym_mixnet_contract_common::{LegacyMixLayer, NodeId}; use nym_node_tester_utils::node::TestableNode; @@ -21,7 +23,7 @@ use nym_topology::mix::MixnodeConversionError; use nym_topology::{gateway, mix}; use rand::prelude::SliceRandom; use rand::{rngs::ThreadRng, thread_rng, Rng}; -use std::collections::{HashMap, HashSet}; +use std::collections::HashMap; use std::fmt::{self, Display, Formatter}; use std::sync::Arc; use std::time::Duration; @@ -77,6 +79,7 @@ pub(crate) struct PreparedPackets { pub(crate) struct PacketPreparer { contract_cache: NymContractCache, described_cache: SharedCache<DescribedNodes>, + node_status_cache: NodeStatusCache, /// Number of test packets sent to each node per_node_test_packets: usize, @@ -94,6 +97,7 @@ impl PacketPreparer { pub(crate) fn new( contract_cache: NymContractCache, described_cache: SharedCache<DescribedNodes>, + node_status_cache: NodeStatusCache, per_node_test_packets: usize, ack_key: Arc<AckKey>, self_public_identity: identity::PublicKey, @@ -102,6 +106,7 @@ impl PacketPreparer { PacketPreparer { contract_cache, described_cache, + node_status_cache, per_node_test_packets, ack_key, self_public_identity, @@ -205,20 +210,6 @@ impl PacketPreparer { (mixnodes, gateways) } - async fn filtered_legacy_mixnodes_and_gateways( - &self, - ) -> ( - Vec<LegacyMixNodeBondWithLayer>, - Vec<LegacyGatewayBondWithId>, - ) { - info!("Obtaining network topology..."); - - let mixnodes = self.contract_cache.legacy_mixnodes_filtered_basic().await; - let gateways = self.contract_cache.legacy_gateways_filtered().await; - - (mixnodes, gateways) - } - pub(crate) fn try_parse_mix_bond( &self, bond: &LegacyMixNodeBondWithLayer, @@ -276,58 +267,50 @@ impl PacketPreparer { parse_bond(gateway).map_err(|_| identity) } - fn layered_mixes<'a, R: Rng>( + fn to_legacy_layered_mixes<'a, R: Rng>( &self, rng: &mut R, - blacklist: &mut HashSet<NodeId>, rewarded_set: &CachedRewardedSet, - legacy_mixnodes: Vec<LegacyMixNodeBondWithLayer>, + node_statuses: &HashMap<NodeId, NodeAnnotation>, mixing_nym_nodes: impl Iterator<Item = &'a NymNodeDescription> + 'a, - ) -> HashMap<LegacyMixLayer, Vec<mix::LegacyNode>> { + ) -> HashMap<LegacyMixLayer, Vec<(mix::LegacyNode, f64)>> { let mut layered_mixes = HashMap::new(); - for mix in legacy_mixnodes { - let layer = mix.layer; - let layer_mixes = layered_mixes.entry(layer).or_insert_with(Vec::new); - let Ok(parsed_node) = self.try_parse_mix_bond(&mix) else { - blacklist.insert(mix.mix_id); - continue; - }; - layer_mixes.push(parsed_node) - } for mixing_nym_node in mixing_nym_nodes { let Some(parsed_node) = self.nym_node_to_legacy_mix(rng, rewarded_set, mixing_nym_node) else { continue; }; + // if the node is not present, default to 0.5 + let weight = node_statuses + .get(&mixing_nym_node.node_id) + .map(|node| node.last_24h_performance.naive_to_f64()) + .unwrap_or(0.5); let layer = parsed_node.layer; let layer_mixes = layered_mixes.entry(layer).or_insert_with(Vec::new); - layer_mixes.push(parsed_node) + layer_mixes.push((parsed_node, weight)) } layered_mixes } - fn all_gateways<'a>( + fn to_legacy_gateway_nodes<'a>( &self, - blacklist: &mut HashSet<NodeId>, - legacy_gateways: Vec<LegacyGatewayBondWithId>, + node_statuses: &HashMap<NodeId, NodeAnnotation>, gateway_capable_nym_nodes: impl Iterator<Item = &'a NymNodeDescription> + 'a, - ) -> Vec<gateway::LegacyNode> { + ) -> Vec<(gateway::LegacyNode, f64)> { let mut gateways = Vec::new(); - for gateway in legacy_gateways { - let Ok(parsed_node) = self.try_parse_gateway_bond(&gateway) else { - blacklist.insert(gateway.node_id); - continue; - }; - gateways.push(parsed_node) - } for gateway_capable_node in gateway_capable_nym_nodes { let Some(parsed_node) = self.nym_node_to_legacy_gateway(gateway_capable_node) else { continue; }; - gateways.push(parsed_node) + // if the node is not present, default to 0.5 + let weight = node_statuses + .get(&gateway_capable_node.node_id) + .map(|node| node.last_24h_performance.naive_to_f64()) + .unwrap_or(0.5); + gateways.push((parsed_node, weight)) } gateways @@ -337,15 +320,11 @@ impl PacketPreparer { // if failed to get parsed => onto the blacklist they go // if generated fewer than n, blacklist will be updated by external function with correctly generated // routes so that they wouldn't be reused - pub(crate) async fn prepare_test_routes( - &self, - n: usize, - blacklist: &mut HashSet<NodeId>, - ) -> Option<Vec<TestRoute>> { - let (legacy_mixnodes, legacy_gateways) = self.filtered_legacy_mixnodes_and_gateways().await; + pub(crate) async fn prepare_test_routes(&self, n: usize) -> Option<Vec<TestRoute>> { let rewarded_set = self.contract_cache.rewarded_set().await?; let descriptions = self.described_cache.get().await.ok()?; + let statuses = self.node_status_cache.node_annotations().await?; let mixing_nym_nodes = descriptions.mixing_nym_nodes(); // last I checked `gatewaying` wasn't a word : ) @@ -353,15 +332,10 @@ impl PacketPreparer { let mut rng = thread_rng(); - // separate mixes into layers for easier selection - let layered_mixes = self.layered_mixes( - &mut rng, - blacklist, - &rewarded_set, - legacy_mixnodes, - mixing_nym_nodes, - ); - let gateways = self.all_gateways(blacklist, legacy_gateways, gateway_capable_nym_nodes); + // separate mixes into layers for easier selection alongside the selection weights + let layered_mixes = + self.to_legacy_layered_mixes(&mut rng, &rewarded_set, &statuses, mixing_nym_nodes); + let gateways = self.to_legacy_gateway_nodes(&statuses, gateway_capable_nym_nodes); // get all nodes from each layer... let l1 = layered_mixes.get(&LegacyMixLayer::One)?; @@ -369,10 +343,26 @@ impl PacketPreparer { let l3 = layered_mixes.get(&LegacyMixLayer::Three)?; // try to choose n nodes from each of them (+ gateways)... - let rand_l1 = l1.choose_multiple(&mut rng, n).collect::<Vec<_>>(); - let rand_l2 = l2.choose_multiple(&mut rng, n).collect::<Vec<_>>(); - let rand_l3 = l3.choose_multiple(&mut rng, n).collect::<Vec<_>>(); - let rand_gateways = gateways.choose_multiple(&mut rng, n).collect::<Vec<_>>(); + let rand_l1 = l1 + .choose_multiple_weighted(&mut rng, n, |item| item.1) + .ok()? + .map(|node| node.0.clone()) + .collect::<Vec<_>>(); + let rand_l2 = l2 + .choose_multiple_weighted(&mut rng, n, |item| item.1) + .ok()? + .map(|node| node.0.clone()) + .collect::<Vec<_>>(); + let rand_l3 = l3 + .choose_multiple_weighted(&mut rng, n, |item| item.1) + .ok()? + .map(|node| node.0.clone()) + .collect::<Vec<_>>(); + let rand_gateways = gateways + .choose_multiple_weighted(&mut rng, n, |item| item.1) + .ok()? + .map(|node| node.0.clone()) + .collect::<Vec<_>>(); // the unwrap on `min()` is fine as we know the iterator is not empty let most_available = *[ diff --git a/nym-api/src/network_monitor/monitor/processor.rs b/nym-api/src/network_monitor/monitor/processor.rs index 0008bfa901e..7a65115d78d 100644 --- a/nym-api/src/network_monitor/monitor/processor.rs +++ b/nym-api/src/network_monitor/monitor/processor.rs @@ -5,17 +5,19 @@ use crate::network_monitor::gateways_reader::GatewayMessages; use crate::network_monitor::test_packet::{NodeTestMessage, NymApiTestMessageExt}; use crate::network_monitor::ROUTE_TESTING_TEST_NONCE; use futures::channel::mpsc; -use futures::lock::{Mutex, MutexGuard}; -use futures::{SinkExt, StreamExt}; +use futures::lock::Mutex; +use futures::StreamExt; use nym_crypto::asymmetric::encryption; use nym_node_tester_utils::error::NetworkTestingError; use nym_node_tester_utils::processor::TestPacketProcessor; use nym_sphinx::acknowledgements::AckKey; use nym_sphinx::receiver::{MessageReceiver, MessageRecoveryError}; use std::mem; +use std::ops::Deref; +use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::Arc; use thiserror::Error; -use tracing::{debug, error, trace, warn}; +use tracing::{error, trace, warn}; pub(crate) type ReceivedProcessorSender = mpsc::UnboundedSender<GatewayMessages>; pub(crate) type ReceivedProcessorReceiver = mpsc::UnboundedReceiver<GatewayMessages>; @@ -37,51 +39,71 @@ enum ProcessingError { ReceivedOutsideTestRun, } -// we can't use Notify due to possible edge case where both notification are consumed at once -enum LockPermit { - Release, - Free, +#[derive(Clone)] +struct SharedProcessorData { + inner: Arc<SharedProcessorDataInner>, } -struct ReceivedProcessorInner<R: MessageReceiver> { - /// Nonce of the current test run indicating which packets should get rejected. - test_nonce: Option<u64>, +impl SharedProcessorData { + async fn reset_run_information(&self) -> Vec<NodeTestMessage> { + self.test_nonce.store(u64::MAX, Ordering::SeqCst); + let mut guard = self.received_packets.lock().await; + mem::take(&mut *guard) + } +} - /// Channel for receiving packets/messages from the gateway clients - packets_receiver: ReceivedProcessorReceiver, +impl Deref for SharedProcessorData { + type Target = SharedProcessorDataInner; + fn deref(&self) -> &Self::Target { + &self.inner + } +} - test_processor: TestPacketProcessor<NymApiTestMessageExt, R>, +struct SharedProcessorDataInner { + /// Nonce of the current test run indicating which packets should get rejected. + test_nonce: AtomicU64, /// Vector containing all received (and decrypted) packets in the current test run. // TODO: perhaps a different structure would be better here - received_packets: Vec<NodeTestMessage>, + received_packets: Mutex<Vec<NodeTestMessage>>, } -impl<R: MessageReceiver> ReceivedProcessorInner<R> { - fn on_received_data(&mut self, raw_message: Vec<u8>) -> Result<(), ProcessingError> { +struct ReceiverTask<R: MessageReceiver> { + shared: SharedProcessorData, + packets_receiver: ReceivedProcessorReceiver, + test_processor: TestPacketProcessor<NymApiTestMessageExt, R>, +} + +impl<R> ReceiverTask<R> +where + R: MessageReceiver, +{ + async fn on_received_data(&mut self, raw_message: Vec<u8>) -> Result<(), ProcessingError> { // if the nonce is none it means the packet was received during the 'waiting' for the // next test run - if self.test_nonce.is_none() { + let test_nonce = self.shared.test_nonce.load(Ordering::SeqCst); + if test_nonce == u64::MAX { return Err(ProcessingError::ReceivedOutsideTestRun); } let test_msg = self.test_processor.process_mixnet_message(raw_message)?; - if test_msg.ext.test_nonce != self.test_nonce.unwrap() { + if test_msg.ext.test_nonce != test_nonce { return Err(ProcessingError::NonMatchingNonce { received: test_msg.ext.test_nonce, - expected: self.test_nonce.unwrap(), + expected: test_nonce, }); } - self.received_packets.push(test_msg); + self.shared.received_packets.lock().await.push(test_msg); Ok(()) } fn on_received_ack(&mut self, raw_ack: Vec<u8>) -> Result<(), ProcessingError> { // if the nonce is none it means the packet was received during the 'waiting' for the // next test run - if self.test_nonce.is_none() { + let test_nonce = self.shared.test_nonce.load(Ordering::SeqCst); + if test_nonce == u64::MAX { return Err(ProcessingError::ReceivedOutsideTestRun); } @@ -92,11 +114,11 @@ impl<R: MessageReceiver> ReceivedProcessorInner<R> { Ok(()) } - fn on_received(&mut self, messages: GatewayMessages) { + async fn on_received(&mut self, messages: GatewayMessages) { match messages { GatewayMessages::Data(data_msgs) => { for raw in data_msgs { - if let Err(err) = self.on_received_data(raw) { + if let Err(err) = self.on_received_data(raw).await { warn!(target: "Monitor", "failed to process received gateway message: {err}") } } @@ -110,137 +132,64 @@ impl<R: MessageReceiver> ReceivedProcessorInner<R> { } } } - - fn finish_run(&mut self) -> Vec<NodeTestMessage> { - self.test_nonce = None; - mem::take(&mut self.received_packets) - } } -pub(crate) struct ReceivedProcessor<R: MessageReceiver> { - permit_changer: Option<mpsc::Sender<LockPermit>>, - inner: Arc<Mutex<ReceivedProcessorInner<R>>>, +pub struct ReceivedProcessor<R: MessageReceiver> { + shared: SharedProcessorData, + receiver_task: Option<ReceiverTask<R>>, } -impl<R: MessageReceiver + Send + 'static> ReceivedProcessor<R> { +impl<R> ReceivedProcessor<R> +where + R: MessageReceiver, +{ pub(crate) fn new( packets_receiver: ReceivedProcessorReceiver, client_encryption_keypair: Arc<encryption::KeyPair>, ack_key: Arc<AckKey>, ) -> Self { - let inner: Arc<Mutex<ReceivedProcessorInner<R>>> = - Arc::new(Mutex::new(ReceivedProcessorInner { - test_nonce: None, - packets_receiver, - test_processor: TestPacketProcessor::new(client_encryption_keypair, ack_key), - received_packets: Vec::new(), - })); + let shared_data = SharedProcessorData { + inner: Arc::new(SharedProcessorDataInner { + test_nonce: AtomicU64::new(u64::MAX), + received_packets: Default::default(), + }), + }; ReceivedProcessor { - permit_changer: None, - inner, + shared: shared_data.clone(), + receiver_task: Some(ReceiverTask { + shared: shared_data, + packets_receiver, + test_processor: TestPacketProcessor::new(client_encryption_keypair, ack_key), + }), } } - pub(crate) fn start_receiving(&mut self) { - let inner = Arc::clone(&self.inner); - - // TODO: perhaps it should be using 0 size instead? - let (permit_sender, mut permit_receiver) = mpsc::channel(1); - self.permit_changer = Some(permit_sender); + pub(crate) fn start_receiving(&mut self) + where + R: Sync + Send + 'static, + { + let mut receiver_task = self + .receiver_task + .take() + .expect("network monitor has already started the receiver task!"); tokio::spawn(async move { - while let Some(permit) = wait_for_permit(&mut permit_receiver, &inner).await { - receive_or_release_permit(&mut permit_receiver, permit).await; - } - - async fn receive_or_release_permit<Q: MessageReceiver>( - permit_receiver: &mut mpsc::Receiver<LockPermit>, - mut inner: MutexGuard<'_, ReceivedProcessorInner<Q>>, - ) { - loop { - tokio::select! { - permit_receiver = permit_receiver.next() => match permit_receiver { - Some(LockPermit::Release) => return, - Some(LockPermit::Free) => error!("somehow we got notification that the lock is free to take while we already hold it!"), - None => return, - }, - messages = inner.packets_receiver.next() => match messages { - Some(messages) => inner.on_received(messages), - None => return, - }, - } - } - } - - // // this lint really looks like a false positive because when lifetimes are elided, - // // the compiler can't figure out appropriate lifetime bounds - // #[allow(clippy::needless_lifetimes)] - async fn wait_for_permit<'a: 'b, 'b, P: MessageReceiver>( - permit_receiver: &'b mut mpsc::Receiver<LockPermit>, - inner: &'a Mutex<ReceivedProcessorInner<P>>, - ) -> Option<MutexGuard<'a, ReceivedProcessorInner<P>>> { - loop { - match permit_receiver.next().await { - // we should only ever get this on the very first run - Some(LockPermit::Release) => debug!( - "somehow got request to drop our lock permit while we do not hold it!" - ), - Some(LockPermit::Free) => return Some(inner.lock().await), - None => return None, - } - } + while let Some(messages) = receiver_task.packets_receiver.next().await { + receiver_task.on_received(messages).await } }); } - pub(super) async fn set_route_test_nonce(&mut self) { - self.set_new_test_nonce(ROUTE_TESTING_TEST_NONCE).await + pub(super) fn set_route_test_nonce(&self) { + self.set_new_test_nonce(ROUTE_TESTING_TEST_NONCE) } - pub(super) async fn set_new_test_nonce(&mut self, test_nonce: u64) { - // ask for the lock back - self.permit_changer - .as_mut() - .expect("ReceivedProcessor hasn't started receiving!") - .send(LockPermit::Release) - .await - .expect("processing task has died!"); - let mut inner = self.inner.lock().await; - - inner.test_nonce = Some(test_nonce); - - // give the permit back - drop(inner); - self.permit_changer - .as_mut() - .expect("ReceivedProcessor hasn't started receiving!") - .send(LockPermit::Free) - .await - .expect("processing task has died!"); + pub(super) fn set_new_test_nonce(&self, test_nonce: u64) { + self.shared.test_nonce.store(test_nonce, Ordering::SeqCst); } - pub(super) async fn return_received(&mut self) -> Vec<NodeTestMessage> { - // ask for the lock back - self.permit_changer - .as_mut() - .expect("ReceivedProcessor hasn't started receiving!") - .send(LockPermit::Release) - .await - .expect("processing task has died!"); - let mut inner = self.inner.lock().await; - - let received = inner.finish_run(); - - // give the permit back - drop(inner); - self.permit_changer - .as_mut() - .expect("ReceivedProcessor hasn't started receiving!") - .send(LockPermit::Free) - .await - .expect("processing task has died!"); - - received + pub(super) async fn return_received(&self) -> Vec<NodeTestMessage> { + self.shared.reset_run_information().await } } diff --git a/nym-api/src/network_monitor/monitor/receiver.rs b/nym-api/src/network_monitor/monitor/receiver.rs index d6dcbf4b855..91a8c678110 100644 --- a/nym-api/src/network_monitor/monitor/receiver.rs +++ b/nym-api/src/network_monitor/monitor/receiver.rs @@ -14,7 +14,7 @@ pub(crate) type GatewayClientUpdateSender = mpsc::UnboundedSender<GatewayClientU pub(crate) type GatewayClientUpdateReceiver = mpsc::UnboundedReceiver<GatewayClientUpdate>; pub(crate) enum GatewayClientUpdate { - Failure(identity::PublicKey), + Disconnect(identity::PublicKey), New( identity::PublicKey, (MixnetMessageReceiver, AcknowledgementReceiver), @@ -45,8 +45,8 @@ impl PacketReceiver { self.gateways_reader .add_receivers(id, message_receiver, ack_receiver); } - GatewayClientUpdate::Failure(id) => { - self.gateways_reader.remove_receivers(&id.to_string()); + GatewayClientUpdate::Disconnect(id) => { + self.gateways_reader.remove_receivers(id); } } } diff --git a/nym-api/src/network_monitor/monitor/sender.rs b/nym-api/src/network_monitor/monitor/sender.rs index ac69a0bc45b..c1140a55ac4 100644 --- a/nym-api/src/network_monitor/monitor/sender.rs +++ b/nym-api/src/network_monitor/monitor/sender.rs @@ -1,35 +1,34 @@ // Copyright 2021-2023 - Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: GPL-3.0-only -use crate::network_monitor::monitor::gateway_clients_cache::{ - ActiveGatewayClients, GatewayClientHandle, -}; -use crate::network_monitor::monitor::gateways_pinger::GatewayPinger; +use crate::network_monitor::monitor::gateway_client_handle::GatewayClientHandle; use crate::network_monitor::monitor::receiver::{GatewayClientUpdate, GatewayClientUpdateSender}; +use crate::support::config::Config; use crate::support::nyxd; +use dashmap::DashMap; use futures::channel::mpsc; use futures::stream::{self, FuturesUnordered, StreamExt}; use futures::task::Context; use futures::{Future, Stream}; use nym_bandwidth_controller::BandwidthController; use nym_credential_storage::persistent_storage::PersistentStorage; -use nym_crypto::asymmetric::identity::{self, PUBLIC_KEY_LENGTH}; +use nym_crypto::asymmetric::ed25519; use nym_gateway_client::client::config::GatewayClientConfig; use nym_gateway_client::client::GatewayConfig; use nym_gateway_client::error::GatewayClientError; use nym_gateway_client::{ - AcknowledgementReceiver, GatewayClient, MixnetMessageReceiver, PacketRouter, + AcknowledgementReceiver, GatewayClient, MixnetMessageReceiver, PacketRouter, SharedGatewayKey, }; use nym_sphinx::forwarding::packet::MixPacket; -use nym_task::TaskClient; use pin_project::pin_project; +use sqlx::__rt::timeout; use std::mem; use std::num::NonZeroUsize; use std::pin::Pin; use std::sync::Arc; use std::task::Poll; use std::time::Duration; -use tracing::{debug, info, trace, warn}; +use tracing::{debug, error, info, trace, warn}; const TIME_CHUNK_SIZE: Duration = Duration::from_millis(50); @@ -39,7 +38,7 @@ pub(crate) struct GatewayPackets { pub(crate) clients_address: String, /// Public key of the target gateway. - pub(crate) pub_key: identity::PublicKey, + pub(crate) pub_key: ed25519::PublicKey, /// All the packets that are going to get sent to the gateway. pub(crate) packets: Vec<MixPacket>, @@ -48,7 +47,7 @@ pub(crate) struct GatewayPackets { impl GatewayPackets { pub(crate) fn new( clients_address: String, - pub_key: identity::PublicKey, + pub_key: ed25519::PublicKey, packets: Vec<MixPacket>, ) -> Self { GatewayPackets { @@ -66,7 +65,7 @@ impl GatewayPackets { } } - pub(crate) fn empty(clients_address: String, pub_key: identity::PublicKey) -> Self { + pub(crate) fn empty(clients_address: String, pub_key: ed25519::PublicKey) -> Self { GatewayPackets { clients_address, pub_key, @@ -89,96 +88,63 @@ impl GatewayPackets { // struct consisting of all external data required to construct a fresh gateway client struct FreshGatewayClientData { gateways_status_updater: GatewayClientUpdateSender, - local_identity: Arc<identity::KeyPair>, + local_identity: Arc<ed25519::KeyPair>, gateway_response_timeout: Duration, bandwidth_controller: BandwidthController<nyxd::Client, PersistentStorage>, disabled_credentials_mode: bool, + gateways_key_cache: DashMap<ed25519::PublicKey, Arc<SharedGatewayKey>>, } impl FreshGatewayClientData { - fn notify_connection_failure( - self: Arc<FreshGatewayClientData>, - raw_gateway_id: [u8; PUBLIC_KEY_LENGTH], - ) { - // if this unwrap failed it means something extremely weird is going on - // and we got some solar flare bitflip type of corruption - let gateway_key = identity::PublicKey::from_bytes(&raw_gateway_id) - .expect("failed to recover gateways public key from valid bytes"); - - // remove the gateway listener channels - self.gateways_status_updater - .unbounded_send(GatewayClientUpdate::Failure(gateway_key)) - .expect("packet receiver seems to have died!"); - } - fn notify_new_connection( self: Arc<FreshGatewayClientData>, - gateway_id: identity::PublicKey, - gateway_channels: Option<(MixnetMessageReceiver, AcknowledgementReceiver)>, + gateway_id: ed25519::PublicKey, + gateway_channels: (MixnetMessageReceiver, AcknowledgementReceiver), ) { - self.gateways_status_updater - .unbounded_send(GatewayClientUpdate::New( - gateway_id, - gateway_channels.expect("we created a new client, yet the channels are a None!"), - )) - .expect("packet receiver seems to have died!") + if self + .gateways_status_updater + .unbounded_send(GatewayClientUpdate::New(gateway_id, gateway_channels)) + .is_err() + { + error!("packet receiver seems to have died!") + } } } pub(crate) struct PacketSender { - // TODO: this has a potential long-term issue. If we keep those clients cached between runs, - // malicious gateways could figure out which traffic comes from the network monitor and always - // forward that traffic while dropping the rest. However, at the current stage such sophisticated - // behaviour is unlikely. - active_gateway_clients: ActiveGatewayClients, - fresh_gateway_client_data: Arc<FreshGatewayClientData>, gateway_connection_timeout: Duration, + gateway_bandwidth_claim_timeout: Duration, max_concurrent_clients: usize, max_sending_rate: usize, } impl PacketSender { - // at this point I'm not entirely sure how to deal with this warning without - // some considerable refactoring - #[allow(clippy::too_many_arguments)] pub(crate) fn new( + config: &Config, gateways_status_updater: GatewayClientUpdateSender, - local_identity: Arc<identity::KeyPair>, - gateway_response_timeout: Duration, - gateway_connection_timeout: Duration, - max_concurrent_clients: usize, - max_sending_rate: usize, + local_identity: Arc<ed25519::KeyPair>, bandwidth_controller: BandwidthController<nyxd::Client, PersistentStorage>, - disabled_credentials_mode: bool, ) -> Self { PacketSender { - active_gateway_clients: ActiveGatewayClients::new(), fresh_gateway_client_data: Arc::new(FreshGatewayClientData { gateways_status_updater, local_identity, - gateway_response_timeout, + gateway_response_timeout: config.network_monitor.debug.gateway_response_timeout, bandwidth_controller, - disabled_credentials_mode, + disabled_credentials_mode: config.network_monitor.debug.disabled_credentials_mode, + gateways_key_cache: Default::default(), }), - gateway_connection_timeout, - max_concurrent_clients, - max_sending_rate, + gateway_connection_timeout: config.network_monitor.debug.gateway_connection_timeout, + gateway_bandwidth_claim_timeout: config + .network_monitor + .debug + .gateway_bandwidth_claim_timeout, + max_concurrent_clients: config.network_monitor.debug.max_concurrent_gateway_clients, + max_sending_rate: config.network_monitor.debug.gateway_sending_rate, } } - pub(crate) fn spawn_gateways_pinger(&self, pinging_interval: Duration, shutdown: TaskClient) { - let gateway_pinger = GatewayPinger::new( - self.active_gateway_clients.clone(), - self.fresh_gateway_client_data - .gateways_status_updater - .clone(), - pinging_interval, - ); - - tokio::spawn(async move { gateway_pinger.run(shutdown).await }); - } - fn new_gateway_client_handle( config: GatewayConfig, fresh_gateway_client_data: &FreshGatewayClientData, @@ -190,8 +156,6 @@ impl PacketSender { let task_client = nym_task::TaskClient::dummy().named(format!("gateway-{}", config.gateway_identity)); - // TODO: future optimization: if we're remaking client for a gateway to which we used to be connected in the past, - // use old shared keys let (message_sender, message_receiver) = mpsc::unbounded(); // currently we do not care about acks at all, but we must keep the channel alive @@ -204,13 +168,18 @@ impl PacketSender { task_client.fork("packet-router"), ); + let shared_keys = fresh_gateway_client_data + .gateways_key_cache + .get(&config.gateway_identity) + .map(|k| k.value().clone()); + let gateway_client = GatewayClient::new( GatewayClientConfig::new_default() .with_disabled_credentials_mode(fresh_gateway_client_data.disabled_credentials_mode) .with_response_timeout(fresh_gateway_client_data.gateway_response_timeout), config, Arc::clone(&fresh_gateway_client_data.local_identity), - None, + shared_keys, gateway_packet_router, Some(fresh_gateway_client_data.bandwidth_controller.clone()), nym_statistics_common::clients::ClientStatsSender::new(None), @@ -218,7 +187,10 @@ impl PacketSender { ); ( - GatewayClientHandle::new(gateway_client), + GatewayClientHandle::new( + gateway_client, + fresh_gateway_client_data.gateways_status_updater.clone(), + ), (message_receiver, ack_receiver), ) } @@ -228,11 +200,11 @@ impl PacketSender { mut mix_packets: Vec<MixPacket>, max_sending_rate: usize, ) -> Result<(), GatewayClientError> { - let gateway_id = client.gateway_identity().to_base58_string(); + let gateway_id = client.gateway_identity(); + info!( - "Got {} packets to send to gateway {}", + "Got {} packets to send to gateway {gateway_id}", mix_packets.len(), - gateway_id ); if mix_packets.len() <= max_sending_rate { @@ -282,47 +254,79 @@ impl PacketSender { Ok(()) } + async fn client_startup( + connection_timeout: Duration, + bandwidth_claim_timeout: Duration, + client: &mut GatewayClientHandle, + ) -> Option<Arc<SharedGatewayKey>> { + let gateway_identity = client.gateway_identity(); + + // 1. attempt to authenticate + let shared_key = + match timeout(connection_timeout, client.perform_initial_authentication()).await { + Err(_timeout) => { + warn!("timed out while trying to authenticate with gateway {gateway_identity}"); + return None; + } + Ok(Err(err)) => { + warn!("failed to authenticate with gateway ({gateway_identity}): {err}"); + return None; + } + Ok(Ok(res)) => res.initial_shared_key, + }; + + // 2. maybe claim bandwidth + match timeout(bandwidth_claim_timeout, client.claim_initial_bandwidth()).await { + Err(_timeout) => { + warn!("timed out while trying to claim initial bandwidth with gateway {gateway_identity}"); + return None; + } + Ok(Err(err)) => { + warn!("failed to claim bandwidth with gateway ({gateway_identity}): {err}"); + return None; + } + Ok(Ok(_)) => (), + } + + // 3. start internal listener + if let Err(err) = client.start_listening_for_mixnet_messages() { + warn!("failed to start message listener for {gateway_identity}: {err}"); + return None; + } + + Some(shared_key) + } + async fn create_new_gateway_client_handle_and_authenticate( config: GatewayConfig, fresh_gateway_client_data: &FreshGatewayClientData, gateway_connection_timeout: Duration, + gateway_bandwidth_claim_timeout: Duration, ) -> Option<( GatewayClientHandle, (MixnetMessageReceiver, AcknowledgementReceiver), )> { let gateway_identity = config.gateway_identity; - let (new_client, (message_receiver, ack_receiver)) = + let (mut new_client, (message_receiver, ack_receiver)) = Self::new_gateway_client_handle(config, fresh_gateway_client_data); - // Put this in timeout in case the gateway has incorrectly set their ulimit and our connection - // gets stuck in their TCP queue and just hangs on our end but does not terminate - // (an actual bug we experienced) - // - // Note: locking the client in unchecked manner is fine here as we just created the lock - // and it wasn't shared with anyone, therefore we're the only one holding reference to it - // and hence it's impossible to fail to obtain the permit. - let mut unlocked_client = new_client.lock_client_unchecked(); - - // SAFETY: it's fine to use the deprecated method here as we're creating brand new clients each time, - // and there's no need to deal with any key upgrades - #[allow(deprecated)] - match tokio::time::timeout( + match Self::client_startup( gateway_connection_timeout, - unlocked_client.get_mut_unchecked().authenticate_and_start(), + gateway_bandwidth_claim_timeout, + &mut new_client, ) .await { - Ok(Ok(_)) => { - drop(unlocked_client); + Some(shared_key) => { + fresh_gateway_client_data + .gateways_key_cache + .insert(gateway_identity, shared_key); Some((new_client, (message_receiver, ack_receiver))) } - Ok(Err(err)) => { - warn!("failed to authenticate with new gateway ({gateway_identity}): {err}",); - // we failed to create a client, can't do much here - None - } - Err(_) => { - warn!("timed out while trying to authenticate with new gateway {gateway_identity}",); + None => { + fresh_gateway_client_data + .gateways_key_cache + .remove(&gateway_identity); None } } @@ -345,123 +349,63 @@ impl PacketSender { // than just concurrently? async fn send_gateway_packets( gateway_connection_timeout: Duration, + gateway_bandwidth_claim_timeout: Duration, packets: GatewayPackets, fresh_gateway_client_data: Arc<FreshGatewayClientData>, - client: Option<GatewayClientHandle>, max_sending_rate: usize, ) -> Option<GatewayClientHandle> { - let existing_client = client.is_some(); - - // Note that in the worst case scenario we will only wait for a second or two to obtain the lock - // as other possibly entity holding the lock (the gateway pinger) is attempting to send - // the ping messages with a maximum timeout. - let (client, gateway_channels) = if let Some(client) = client { - if client.is_invalid().await { - warn!("Our existing client was invalid - two test runs happened back to back without cleanup"); - return None; - } - (client, None) - } else { - let (client, gateway_channels) = - Self::create_new_gateway_client_handle_and_authenticate( - packets.gateway_config(), - &fresh_gateway_client_data, - gateway_connection_timeout, - ) - .await?; - (client, Some(gateway_channels)) - }; + let (mut client, gateway_channels) = + Self::create_new_gateway_client_handle_and_authenticate( + packets.gateway_config(), + &fresh_gateway_client_data, + gateway_connection_timeout, + gateway_bandwidth_claim_timeout, + ) + .await?; + + let identity = client.gateway_identity(); let estimated_time = Duration::from_secs_f64(packets.packets.len() as f64 / max_sending_rate as f64); // give some leeway let timeout = estimated_time * 3; - let mut guard = client.lock_client().await; - let unwrapped_client = guard.get_mut_unchecked(); - - if let Err(err) = Self::check_remaining_bandwidth(unwrapped_client).await { - warn!( - "Failed to claim additional bandwidth for {} - {err}", - unwrapped_client.gateway_identity().to_base58_string(), - ); - if existing_client { - guard.invalidate(); - fresh_gateway_client_data.notify_connection_failure(packets.pub_key.to_bytes()); - } + if let Err(err) = Self::check_remaining_bandwidth(&mut client).await { + warn!("Failed to claim additional bandwidth for {identity}: {err}",); return None; } match tokio::time::timeout( timeout, - Self::attempt_to_send_packets(unwrapped_client, packets.packets, max_sending_rate), + Self::attempt_to_send_packets(&mut client, packets.packets, max_sending_rate), ) .await { Err(_timeout) => { - warn!( - "failed to send packets to {} - we timed out", - packets.pub_key.to_base58_string(), - ); - // if this was a fresh client, there's no need to do anything as it was never - // registered to get read - if existing_client { - guard.invalidate(); - fresh_gateway_client_data.notify_connection_failure(packets.pub_key.to_bytes()); - } + warn!("failed to send packets to {identity} - we timed out",); return None; } Ok(Err(err)) => { - warn!( - "failed to send packets to {} - {:?}", - packets.pub_key.to_base58_string(), - err - ); - // if this was a fresh client, there's no need to do anything as it was never - // registered to get read - if existing_client { - guard.invalidate(); - fresh_gateway_client_data.notify_connection_failure(packets.pub_key.to_bytes()); - } + warn!("failed to send packets to {identity}: {err}",); return None; } Ok(Ok(_)) => { - if !existing_client { - fresh_gateway_client_data - .notify_new_connection(packets.pub_key, gateway_channels); - } + fresh_gateway_client_data.notify_new_connection(identity, gateway_channels) } } - drop(guard); Some(client) } - // point of this is to basically insert handles of fresh clients that didn't exist here before - async fn merge_client_handles(&self, handles: Vec<GatewayClientHandle>) { - let mut guard = self.active_gateway_clients.lock().await; - for handle in handles { - let raw_identity = handle.raw_identity(); - if let Some(existing) = guard.get(&raw_identity) { - if !handle.ptr_eq(existing) { - panic!("Duplicate client detected!") - } - - if handle.is_invalid().await { - guard.remove(&raw_identity); - } - } else { - // client never existed -> just insert it - guard.insert(raw_identity, handle); - } - } - } - - pub(super) async fn send_packets(&mut self, packets: Vec<GatewayPackets>) { + pub(super) async fn send_packets( + &mut self, + packets: Vec<GatewayPackets>, + ) -> Vec<GatewayClientHandle> { // we know that each of the elements in the packets array will only ever access a single, // unique element from the existing clients let gateway_connection_timeout = self.gateway_connection_timeout; + let gateway_bandwidth_claim_timeout = self.gateway_bandwidth_claim_timeout; let max_concurrent_clients = if self.max_concurrent_clients > 0 { Some(self.max_concurrent_clients) } else { @@ -469,41 +413,24 @@ impl PacketSender { }; let max_sending_rate = self.max_sending_rate; - let guard = self.active_gateway_clients.lock().await; - // this clippy warning is a false positive as we cannot get rid of the collect by moving - // everything into a single iterator as it would require us to hold the lock the entire time - // and that is exactly what we want to avoid - #[allow(clippy::needless_collect)] let stream_data = packets .into_iter() - .map(|packets| { - let existing_client = guard - .get(&packets.pub_key.to_bytes()) - .map(|client| client.clone_data_pointer()); - ( - packets, - Arc::clone(&self.fresh_gateway_client_data), - existing_client, - ) - }) + .map(|packets| (packets, Arc::clone(&self.fresh_gateway_client_data))) .collect::<Vec<_>>(); - // drop the guard immediately so that the other task (gateway pinger) would not need to wait until - // we're done sending packets (note: without this drop, we wouldn't be able to ping gateways that - // we're not interacting with right now) - drop(guard); - // can't chain it all nicely together as there's no adapter method defined on Stream directly // for ForEachConcurrentClientUse - let used_clients = ForEachConcurrentClientUse::new( + // + // we need to keep clients alive until the test finishes so that we could keep receiving + ForEachConcurrentClientUse::new( stream::iter(stream_data.into_iter()), max_concurrent_clients, - |(packets, fresh_data, client)| async move { + |(packets, fresh_data)| async move { Self::send_gateway_packets( gateway_connection_timeout, + gateway_bandwidth_claim_timeout, packets, fresh_data, - client, max_sending_rate, ) .await @@ -512,9 +439,7 @@ impl PacketSender { .await .into_iter() .flatten() - .collect(); - - self.merge_client_handles(used_clients).await; + .collect() } } diff --git a/nym-api/src/nym_contract_cache/cache/mod.rs b/nym-api/src/nym_contract_cache/cache/mod.rs index eeb9b2a7c28..ed5db5cb3c9 100644 --- a/nym-api/src/nym_contract_cache/cache/mod.rs +++ b/nym-api/src/nym_contract_cache/cache/mod.rs @@ -219,14 +219,6 @@ impl NymContractCache { .into_inner() } - pub async fn legacy_mixnodes_filtered_basic(&self) -> Vec<LegacyMixNodeBondWithLayer> { - self.legacy_mixnodes_filtered() - .await - .into_iter() - .map(|bond| bond.bond_information) - .collect() - } - pub async fn legacy_mixnodes_all_basic(&self) -> Vec<LegacyMixNodeBondWithLayer> { self.legacy_mixnodes_all() .await diff --git a/nym-api/src/support/cli/run.rs b/nym-api/src/support/cli/run.rs index 6436f19be15..56863c8ff28 100644 --- a/nym-api/src/support/cli/run.rs +++ b/nym-api/src/support/cli/run.rs @@ -256,9 +256,10 @@ async fn start_nym_api_tasks_axum(config: &Config) -> anyhow::Result<ShutdownHan // if the monitoring is enabled if config.network_monitor.enabled { network_monitor::start::<SphinxMessageReceiver>( - &config.network_monitor, + config, &nym_contract_cache_state, described_nodes_cache.clone(), + node_status_cache_state.clone(), &storage, nyxd_client.clone(), &task_manager, diff --git a/nym-api/src/support/config/mod.rs b/nym-api/src/support/config/mod.rs index 5a07c223bee..e93a880a090 100644 --- a/nym-api/src/support/config/mod.rs +++ b/nym-api/src/support/config/mod.rs @@ -38,13 +38,12 @@ const DEFAULT_GATEWAY_SENDING_RATE: usize = 200; const DEFAULT_MAX_CONCURRENT_GATEWAY_CLIENTS: usize = 50; const DEFAULT_PACKET_DELIVERY_TIMEOUT: Duration = Duration::from_secs(20); const DEFAULT_MONITOR_RUN_INTERVAL: Duration = Duration::from_secs(15 * 60); -const DEFAULT_GATEWAY_PING_INTERVAL: Duration = Duration::from_secs(60); // Set this to a high value for now, so that we don't risk sporadic timeouts that might cause // bought bandwidth tokens to not have time to be spent; Once we remove the gateway from the // bandwidth bridging protocol, we can come back to a smaller timeout value const DEFAULT_GATEWAY_RESPONSE_TIMEOUT: Duration = Duration::from_secs(5 * 60); -// This timeout value should be big enough to accommodate an initial bandwidth acquirement -const DEFAULT_GATEWAY_CONNECTION_TIMEOUT: Duration = Duration::from_secs(2 * 60); +const DEFAULT_GATEWAY_CONNECTION_TIMEOUT: Duration = Duration::from_secs(15); +const DEFAULT_GATEWAY_BANDWIDTH_CLAIM_TIMEOUT: Duration = Duration::from_secs(2 * 60); const DEFAULT_TEST_ROUTES: usize = 3; const DEFAULT_MINIMUM_TEST_ROUTES: usize = 1; @@ -323,11 +322,6 @@ pub struct NetworkMonitorDebug { #[serde(with = "humantime_serde")] pub run_interval: Duration, - /// Specifies interval at which we should be sending ping packets to all active gateways - /// in order to keep the websocket connections alive. - #[serde(with = "humantime_serde")] - pub gateway_ping_interval: Duration, - /// Specifies maximum rate (in packets per second) of test packets being sent to gateway pub gateway_sending_rate: usize, @@ -343,6 +337,10 @@ pub struct NetworkMonitorDebug { #[serde(with = "humantime_serde")] pub gateway_connection_timeout: Duration, + /// Maximum allowed time for the gateway bandwidth claim to get resolved + #[serde(with = "humantime_serde")] + pub gateway_bandwidth_claim_timeout: Duration, + /// Specifies the duration the monitor is going to wait after sending all measurement /// packets before declaring nodes unreachable. #[serde(with = "humantime_serde")] @@ -370,11 +368,11 @@ impl Default for NetworkMonitorDebug { min_gateway_reliability: DEFAULT_MIN_GATEWAY_RELIABILITY, disabled_credentials_mode: true, run_interval: DEFAULT_MONITOR_RUN_INTERVAL, - gateway_ping_interval: DEFAULT_GATEWAY_PING_INTERVAL, gateway_sending_rate: DEFAULT_GATEWAY_SENDING_RATE, max_concurrent_gateway_clients: DEFAULT_MAX_CONCURRENT_GATEWAY_CLIENTS, gateway_response_timeout: DEFAULT_GATEWAY_RESPONSE_TIMEOUT, gateway_connection_timeout: DEFAULT_GATEWAY_CONNECTION_TIMEOUT, + gateway_bandwidth_claim_timeout: DEFAULT_GATEWAY_BANDWIDTH_CLAIM_TIMEOUT, packet_delivery_timeout: DEFAULT_PACKET_DELIVERY_TIMEOUT, test_routes: DEFAULT_TEST_ROUTES, minimum_test_routes: DEFAULT_MINIMUM_TEST_ROUTES, From 80d1a24164aa4e3338015dfd091c329d5568f155 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= <jedrzej.stuczynski@gmail.com> Date: Thu, 5 Dec 2024 16:50:34 +0000 Subject: [PATCH 11/18] dont consider legacy nodes for rewarded set selection (#5215) * dont consider legacy nodes for rewarded set selection * removed dead imports --- nym-api/src/epoch_operations/mod.rs | 9 +-- .../rewarded_set_assignment.rs | 73 +------------------ 2 files changed, 5 insertions(+), 77 deletions(-) diff --git a/nym-api/src/epoch_operations/mod.rs b/nym-api/src/epoch_operations/mod.rs index fe4fd5792d6..0a1f8bb566b 100644 --- a/nym-api/src/epoch_operations/mod.rs +++ b/nym-api/src/epoch_operations/mod.rs @@ -120,14 +120,9 @@ impl EpochAdvancer { let epoch_end = interval.current_epoch_end(); - let legacy_mixnodes = self.nym_contract_cache.legacy_mixnodes_filtered().await; - let legacy_gateways = self.nym_contract_cache.legacy_gateways_filtered().await; - - // TODO: for the purposes of rewarding, this might have to grab some pre-filtered nodes instead, - // such as ones that use up to date version or have correct 'peanut' score let nym_nodes = self.nym_contract_cache.nym_nodes().await; - if legacy_mixnodes.is_empty() && legacy_gateways.is_empty() && nym_nodes.is_empty() { + if nym_nodes.is_empty() { // that's a bit weird, but ok warn!("there don't seem to be any nodes on the network!") } @@ -160,7 +155,7 @@ impl EpochAdvancer { // note: those operations don't really have to be atomic, so it's fine to send them // as separate transactions self.reconcile_epoch_events().await?; - self.update_rewarded_set_and_advance_epoch(&legacy_mixnodes, &legacy_gateways, &nym_nodes) + self.update_rewarded_set_and_advance_epoch(&nym_nodes) .await?; info!("Purging old node statuses from the storage..."); diff --git a/nym-api/src/epoch_operations/rewarded_set_assignment.rs b/nym-api/src/epoch_operations/rewarded_set_assignment.rs index 58a62dc27c0..c2fda62dd61 100644 --- a/nym-api/src/epoch_operations/rewarded_set_assignment.rs +++ b/nym-api/src/epoch_operations/rewarded_set_assignment.rs @@ -5,8 +5,6 @@ use crate::epoch_operations::error::RewardingError; use crate::epoch_operations::helpers::stake_to_f64; use crate::EpochAdvancer; use cosmwasm_std::Decimal; -use nym_api_requests::legacy::{LegacyGatewayBondWithId, LegacyMixNodeDetailsWithLayer}; -use nym_mixnet_contract_common::helpers::IntoBaseDecimal; use nym_mixnet_contract_common::reward_params::{Performance, RewardedSetParams}; use nym_mixnet_contract_common::{EpochState, NodeId, NymNodeDetails, RewardedSet}; use rand::prelude::SliceRandom; @@ -204,8 +202,6 @@ impl EpochAdvancer { async fn attach_performance_to_eligible_nodes( &self, - legacy_mixnodes: &[LegacyMixNodeDetailsWithLayer], - legacy_gateways: &[LegacyGatewayBondWithId], nym_nodes: &[NymNodeDetails], ) -> Vec<NodeWithStakeAndPerformance> { let mut with_performance = Vec::new(); @@ -218,62 +214,6 @@ impl EpochAdvancer { return Vec::new(); }; - for mix in legacy_mixnodes { - let node_id = mix.mix_id(); - let total_stake = mix.total_stake(); - - let Some(annotation) = status_cache.get(&node_id) else { - debug!("couldn't find annotation for legacy mixnode {node_id}"); - continue; - }; - - if mix.bond_information.proxy.is_some() { - debug!("legacy mixnode {node_id} is using vested tokens"); - continue; - } - - let performance = annotation.detailed_performance.to_rewarding_performance(); - debug!( - "legacy mixnode {}: stake: {total_stake}, performance: {performance}", - mix.mix_id() - ); - - with_performance.push(NodeWithStakeAndPerformance { - node_id: mix.mix_id(), - available_roles: vec![AvailableRole::Mix], - total_stake, - performance, - }) - } - for gateway in legacy_gateways { - let node_id = gateway.node_id; - let total_stake = gateway - .bond - .pledge_amount - .amount - .into_base_decimal() - .unwrap_or_default(); - - let Some(annotation) = status_cache.get(&node_id) else { - debug!("couldn't find annotation for legacy gateway {node_id}"); - continue; - }; - - let performance = annotation.detailed_performance.to_rewarding_performance(); - - debug!( - "legacy gateway {}: stake: {total_stake}, performance: {performance}", - gateway.node_id - ); - - with_performance.push(NodeWithStakeAndPerformance { - node_id: gateway.node_id, - available_roles: vec![AvailableRole::EntryGateway], - total_stake, - performance, - }) - } - for nym_node in nym_nodes { let node_id = nym_node.node_id(); let total_stake = nym_node.total_stake(); @@ -283,7 +223,7 @@ impl EpochAdvancer { }; let Some(annotation) = status_cache.get(&node_id) else { - debug!("couldn't find annotation for nym-node gateway {node_id}"); + debug!("couldn't find annotation for nym-node {node_id}"); continue; }; @@ -319,8 +259,6 @@ impl EpochAdvancer { pub(super) async fn update_rewarded_set_and_advance_epoch( &self, - legacy_mixnodes: &[LegacyMixNodeDetailsWithLayer], - legacy_gateways: &[LegacyGatewayBondWithId], nym_nodes: &[NymNodeDetails], ) -> Result<(), RewardingError> { let epoch_status = self.nyxd_client.get_current_epoch_status().await?; @@ -333,13 +271,8 @@ impl EpochAdvancer { } info!("attempting to assign the rewarded set for the upcoming epoch..."); - let nodes_with_performance = self - .attach_performance_to_eligible_nodes( - legacy_mixnodes, - legacy_gateways, - nym_nodes, - ) - .await; + let nodes_with_performance = + self.attach_performance_to_eligible_nodes(nym_nodes).await; if let Err(err) = self ._update_rewarded_set_and_advance_epoch(nodes_with_performance) From 4b055a9bf016037cef101bd682b45261bc2c4544 Mon Sep 17 00:00:00 2001 From: benedettadavico <benedetta.davico@gmail.com> Date: Thu, 5 Dec 2024 18:13:13 +0100 Subject: [PATCH 12/18] bumping nym-node version --- Cargo.lock | 2 +- nym-node/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0a16ff38eba..f4157361ab8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5954,7 +5954,7 @@ dependencies = [ [[package]] name = "nym-node" -version = "1.1.12" +version = "1.1.13" dependencies = [ "anyhow", "bip39", diff --git a/nym-node/Cargo.toml b/nym-node/Cargo.toml index 92dec65feae..2653ac5f846 100644 --- a/nym-node/Cargo.toml +++ b/nym-node/Cargo.toml @@ -3,7 +3,7 @@ [package] name = "nym-node" -version = "1.1.12" +version = "1.1.13" authors.workspace = true repository.workspace = true homepage.workspace = true From d07e293cb552278efd1bae1ccf0f9ff3fb58beae Mon Sep 17 00:00:00 2001 From: benedettadavico <benedetta.davico@gmail.com> Date: Fri, 6 Dec 2024 11:34:21 +0100 Subject: [PATCH 13/18] amend nym-node version --- Cargo.lock | 2 +- nym-node/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f4157361ab8..e8abb6090fc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5954,7 +5954,7 @@ dependencies = [ [[package]] name = "nym-node" -version = "1.1.13" +version = "1.2.0" dependencies = [ "anyhow", "bip39", diff --git a/nym-node/Cargo.toml b/nym-node/Cargo.toml index 2653ac5f846..897a70cb0c4 100644 --- a/nym-node/Cargo.toml +++ b/nym-node/Cargo.toml @@ -3,7 +3,7 @@ [package] name = "nym-node" -version = "1.1.13" +version = "1.2.0" authors.workspace = true repository.workspace = true homepage.workspace = true From 210cc5286e0cdca529b586fcdd76b524a4530e17 Mon Sep 17 00:00:00 2001 From: Tommy Verrall <60836166+tommyv1987@users.noreply.github.com> Date: Fri, 6 Dec 2024 17:29:08 +0100 Subject: [PATCH 14/18] Update Cargo.toml amend version back to 13 --- nym-node/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nym-node/Cargo.toml b/nym-node/Cargo.toml index 897a70cb0c4..2653ac5f846 100644 --- a/nym-node/Cargo.toml +++ b/nym-node/Cargo.toml @@ -3,7 +3,7 @@ [package] name = "nym-node" -version = "1.2.0" +version = "1.1.13" authors.workspace = true repository.workspace = true homepage.workspace = true From 675e5a0305d4a09b6eb29f64845491447c1b3825 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= <jedrzej.stuczynski@gmail.com> Date: Fri, 6 Dec 2024 17:21:21 +0000 Subject: [PATCH 15/18] removed semver filtering (#5224) --- Cargo.lock | 1 - clients/native/src/commands/run.rs | 29 +------ clients/native/src/error.rs | 3 - clients/socks5/src/commands/run.rs | 34 +------- clients/socks5/src/error.rs | 3 - common/bin-common/Cargo.toml | 3 +- common/bin-common/src/lib.rs | 1 - common/bin-common/src/version_checker/mod.rs | 78 ------------------- .../client-core/src/client/base_client/mod.rs | 7 +- .../topology_control/geo_aware_provider.rs | 12 +-- .../topology_control/nym_api_provider.rs | 14 +--- common/client-core/src/init/helpers.rs | 6 +- .../validator-client/src/client.rs | 53 +++---------- .../validator-client/src/nym_api/mod.rs | 46 +---------- common/mixnode-common/src/verloc/mod.rs | 13 ---- common/topology/src/filter.rs | 55 ------------- common/topology/src/gateway.rs | 9 +-- common/topology/src/lib.rs | 15 ---- common/topology/src/mix.rs | 9 +-- common/wasm/client-core/src/helpers.rs | 4 +- gateway/src/node/helpers.rs | 1 - gateway/src/node/mod.rs | 2 +- mixnode/src/node/mod.rs | 2 +- .../nym_nodes/handlers/unstable/helpers.rs | 20 ----- .../src/nym_nodes/handlers/unstable/mod.rs | 2 + .../nym_nodes/handlers/unstable/skimmed.rs | 39 ++-------- nym-node-status-api/src/monitor/mod.rs | 2 +- nym-wallet/Cargo.lock | 1 - .../examples/custom_topology_provider.rs | 4 +- .../nym-sdk/examples/geo_topology_provider.rs | 1 - .../authenticator/src/cli/mod.rs | 30 +------ .../authenticator/src/cli/request.rs | 7 +- .../authenticator/src/cli/run.rs | 11 +-- .../authenticator/src/cli/sign.rs | 7 +- service-providers/authenticator/src/error.rs | 3 - .../ip-packet-router/src/cli/mod.rs | 30 +------ .../ip-packet-router/src/cli/run.rs | 8 +- .../ip-packet-router/src/cli/sign.rs | 7 +- .../ip-packet-router/src/error.rs | 3 - .../network-requester/src/cli/mod.rs | 29 ------- .../network-requester/src/cli/run.rs | 8 +- .../network-requester/src/cli/sign.rs | 7 +- .../network-requester/src/error.rs | 3 - .../src/manager/local_client.rs | 2 +- 44 files changed, 53 insertions(+), 571 deletions(-) delete mode 100644 common/bin-common/src/version_checker/mod.rs delete mode 100644 common/topology/src/filter.rs diff --git a/Cargo.lock b/Cargo.lock index e8abb6090fc..9e09a9533ed 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4674,7 +4674,6 @@ dependencies = [ "opentelemetry-jaeger", "pretty_env_logger", "schemars", - "semver 1.0.23", "serde", "serde_json", "tracing-opentelemetry", diff --git a/clients/native/src/commands/run.rs b/clients/native/src/commands/run.rs index eac8271a95b..c2e5c22a0fc 100644 --- a/clients/native/src/commands/run.rs +++ b/clients/native/src/commands/run.rs @@ -3,13 +3,10 @@ use crate::commands::try_load_current_config; use crate::{ - client::{config::Config, SocketClient}, + client::SocketClient, commands::{override_config, OverrideConfig}, - error::ClientError, }; use clap::Args; -use log::*; -use nym_bin_common::version_checker::is_minor_version_compatible; use nym_client_core::cli_helpers::client_run::CommonClientRunArgs; use std::error::Error; use std::net::IpAddr; @@ -48,36 +45,12 @@ impl From<Run> for OverrideConfig { } } -// this only checks compatibility between config the binary. It does not take into consideration -// network version. It might do so in the future. -fn version_check(cfg: &Config) -> bool { - let binary_version = env!("CARGO_PKG_VERSION"); - let config_version = &cfg.base.client.version; - if binary_version == config_version { - true - } else { - warn!("The native-client binary has different version than what is specified in config file! {} and {}", binary_version, config_version); - if is_minor_version_compatible(binary_version, config_version) { - info!("but they are still semver compatible. However, consider running the `upgrade` command"); - true - } else { - error!("and they are semver incompatible! - please run the `upgrade` command before attempting `run` again"); - false - } - } -} - pub(crate) async fn execute(args: Run) -> Result<(), Box<dyn Error + Send + Sync>> { eprintln!("Starting client {}...", args.common_args.id); let mut config = try_load_current_config(&args.common_args.id).await?; config = override_config(config, OverrideConfig::from(args.clone())); - if !version_check(&config) { - error!("failed the local version check"); - return Err(Box::new(ClientError::FailedLocalVersionCheck)); - } - SocketClient::new(config, args.common_args.custom_mixnet) .run_socket_forever() .await diff --git a/clients/native/src/error.rs b/clients/native/src/error.rs index 23e30121f55..d2e559f2ea7 100644 --- a/clients/native/src/error.rs +++ b/clients/native/src/error.rs @@ -17,9 +17,6 @@ pub enum ClientError { #[error("Failed to validate the loaded config")] ConfigValidationFailure, - #[error("Failed local version check, client and config mismatch")] - FailedLocalVersionCheck, - #[error("Attempted to start the client in invalid socket mode")] InvalidSocketMode, diff --git a/clients/socks5/src/commands/run.rs b/clients/socks5/src/commands/run.rs index 15857d986ec..3dcbe1dfd43 100644 --- a/clients/socks5/src/commands/run.rs +++ b/clients/socks5/src/commands/run.rs @@ -2,14 +2,8 @@ // SPDX-License-Identifier: Apache-2.0 use crate::commands::try_load_current_config; -use crate::config::Config; -use crate::{ - commands::{override_config, OverrideConfig}, - error::Socks5ClientError, -}; +use crate::commands::{override_config, OverrideConfig}; use clap::Args; -use log::*; -use nym_bin_common::version_checker::is_minor_version_compatible; use nym_client_core::cli_helpers::client_run::CommonClientRunArgs; use nym_client_core::client::base_client::storage::OnDiskPersistent; use nym_client_core::client::topology_control::geo_aware_provider::CountryGroup; @@ -82,38 +76,12 @@ fn validate_country_group(s: &str) -> Result<CountryGroup, String> { } } -// this only checks compatibility between config the binary. It does not take into consideration -// network version. It might do so in the future. -fn version_check(cfg: &Config) -> bool { - let binary_version = env!("CARGO_PKG_VERSION"); - let config_version = &cfg.core.base.client.version; - if binary_version == config_version { - true - } else { - warn!( - "The socks5-client binary has different version than what is specified in config file! {binary_version} and {config_version}", - ); - if is_minor_version_compatible(binary_version, config_version) { - info!("but they are still semver compatible. However, consider running the `upgrade` command"); - true - } else { - error!("and they are semver incompatible! - please run the `upgrade` command before attempting `run` again"); - false - } - } -} - pub(crate) async fn execute(args: Run) -> Result<(), Box<dyn std::error::Error + Send + Sync>> { eprintln!("Starting client {}...", args.common_args.id); let mut config = try_load_current_config(&args.common_args.id).await?; config = override_config(config, OverrideConfig::from(args.clone())); - if !version_check(&config) { - error!("failed the local version check"); - return Err(Box::new(Socks5ClientError::FailedLocalVersionCheck)); - } - let storage = OnDiskPersistent::from_paths(config.storage_paths.common_paths, &config.core.base.debug) .await?; diff --git a/clients/socks5/src/error.rs b/clients/socks5/src/error.rs index a2f4d8379a5..3255e694007 100644 --- a/clients/socks5/src/error.rs +++ b/clients/socks5/src/error.rs @@ -14,9 +14,6 @@ pub enum Socks5ClientError { #[error("Failed to validate the loaded config")] ConfigValidationFailure, - #[error("Failed local version check, client and config mismatch")] - FailedLocalVersionCheck, - #[error("Fail to bind address")] FailToBindAddress, diff --git a/common/bin-common/Cargo.toml b/common/bin-common/Cargo.toml index 11a2c76f2b9..78e8e6ca937 100644 --- a/common/bin-common/Cargo.toml +++ b/common/bin-common/Cargo.toml @@ -15,7 +15,6 @@ const-str = { workspace = true } log = { workspace = true } pretty_env_logger = { workspace = true } schemars = { workspace = true, features = ["preserve_order"], optional = true } -semver.workspace = true serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true, optional = true } @@ -44,5 +43,5 @@ tracing = [ "tracing-opentelemetry", "opentelemetry", ] -clap = [ "dep:clap", "dep:clap_complete", "dep:clap_complete_fig" ] +clap = ["dep:clap", "dep:clap_complete", "dep:clap_complete_fig"] models = [] diff --git a/common/bin-common/src/lib.rs b/common/bin-common/src/lib.rs index 1c6c42e6ac0..9353302b2f8 100644 --- a/common/bin-common/src/lib.rs +++ b/common/bin-common/src/lib.rs @@ -3,7 +3,6 @@ pub mod build_information; pub mod logging; -pub mod version_checker; #[cfg(feature = "clap")] pub mod completions; diff --git a/common/bin-common/src/version_checker/mod.rs b/common/bin-common/src/version_checker/mod.rs deleted file mode 100644 index 921c33b9d96..00000000000 --- a/common/bin-common/src/version_checker/mod.rs +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright 2021 - Nym Technologies SA <contact@nymtech.net> -// SPDX-License-Identifier: Apache-2.0 - -pub use semver::Version; - -/// Checks if the version is minor version compatible. -/// -/// Checks whether given `version` is compatible with a given semantic version requirement `req` -/// according to major-minor semver rules. The semantic version requirement can be passed as a full, -/// concrete version number, because that's what we'll have in our Cargo.toml files (e.g. 0.3.2). -/// The patch number in the requirement gets dropped and replaced with a wildcard (0.3.*) as all -/// minor versions should be compatible with each other. -pub fn is_minor_version_compatible(version: &str, req: &str) -> bool { - let expected_version = match Version::parse(version) { - Ok(v) => v, - Err(_) => return false, - }; - let req_version = match Version::parse(req) { - Ok(v) => v, - Err(_) => return false, - }; - - expected_version.major == req_version.major && expected_version.minor == req_version.minor -} - -pub fn parse_version(raw_version: &str) -> Result<Version, semver::Error> { - Version::parse(raw_version) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn version_0_3_0_is_compatible_with_requirement_0_3_x() { - assert!(is_minor_version_compatible("0.3.0", "0.3.2")); - } - - #[test] - fn version_0_3_1_is_compatible_with_minimum_requirement_0_3_x() { - assert!(is_minor_version_compatible("0.3.1", "0.3.2")); - } - - #[test] - fn version_0_3_2_is_compatible_with_minimum_requirement_0_3_x() { - assert!(is_minor_version_compatible("0.3.2", "0.3.0")); - } - - #[test] - fn version_0_2_0_is_not_compatible_with_requirement_0_3_x() { - assert!(!is_minor_version_compatible("0.2.0", "0.3.2")); - } - - #[test] - fn version_0_4_0_is_not_compatible_with_requirement_0_3_x() { - assert!(!is_minor_version_compatible("0.4.0", "0.3.2")); - } - - #[test] - fn version_1_3_2_is_not_compatible_with_requirement_0_3_x() { - assert!(!is_minor_version_compatible("1.3.2", "0.3.2")); - } - - #[test] - fn version_0_4_0_rc_1_is_compatible_with_version_0_4_0_rc_1() { - assert!(is_minor_version_compatible("0.4.0-rc.1", "0.4.0-rc.1")); - } - - #[test] - fn returns_false_on_foo_version() { - assert!(!is_minor_version_compatible("foo", "0.3.2")); - } - - #[test] - fn returns_false_on_bar_version() { - assert!(!is_minor_version_compatible("0.3.2", "bar")); - } -} diff --git a/common/client-core/src/client/base_client/mod.rs b/common/client-core/src/client/base_client/mod.rs index b91e5323769..856ddd94973 100644 --- a/common/client-core/src/client/base_client/mod.rs +++ b/common/client-core/src/client/base_client/mod.rs @@ -514,15 +514,10 @@ where min_gateway_performance: config_topology.minimum_gateway_performance, }, nym_api_urls, - env!("CARGO_PKG_VERSION").to_string(), user_agent, )), config::TopologyStructure::GeoAware(group_by) => { - Box::new(GeoAwareTopologyProvider::new( - nym_api_urls, - env!("CARGO_PKG_VERSION").to_string(), - group_by, - )) + Box::new(GeoAwareTopologyProvider::new(nym_api_urls, group_by)) } }) } diff --git a/common/client-core/src/client/topology_control/geo_aware_provider.rs b/common/client-core/src/client/topology_control/geo_aware_provider.rs index 7e961bb8d23..d3fabd9a938 100644 --- a/common/client-core/src/client/topology_control/geo_aware_provider.rs +++ b/common/client-core/src/client/topology_control/geo_aware_provider.rs @@ -85,15 +85,10 @@ fn check_layer_integrity(topology: NymTopology) -> Result<(), ()> { pub struct GeoAwareTopologyProvider { validator_client: nym_validator_client::client::NymApiClient, filter_on: GroupBy, - client_version: String, } impl GeoAwareTopologyProvider { - pub fn new( - mut nym_api_urls: Vec<Url>, - client_version: String, - filter_on: GroupBy, - ) -> GeoAwareTopologyProvider { + pub fn new(mut nym_api_urls: Vec<Url>, filter_on: GroupBy) -> GeoAwareTopologyProvider { log::info!( "Creating geo-aware topology provider with filter on {}", filter_on @@ -105,14 +100,13 @@ impl GeoAwareTopologyProvider { nym_api_urls[0].clone(), ), filter_on, - client_version, } } async fn get_topology(&self) -> Option<NymTopology> { let mixnodes = match self .validator_client - .get_all_basic_active_mixing_assigned_nodes(Some(self.client_version.clone())) + .get_all_basic_active_mixing_assigned_nodes() .await { Err(err) => { @@ -124,7 +118,7 @@ impl GeoAwareTopologyProvider { let gateways = match self .validator_client - .get_all_basic_entry_assigned_nodes(Some(self.client_version.clone())) + .get_all_basic_entry_assigned_nodes() .await { Err(err) => { diff --git a/common/client-core/src/client/topology_control/nym_api_provider.rs b/common/client-core/src/client/topology_control/nym_api_provider.rs index 7734ea74610..3b87086f59a 100644 --- a/common/client-core/src/client/topology_control/nym_api_provider.rs +++ b/common/client-core/src/client/topology_control/nym_api_provider.rs @@ -35,18 +35,11 @@ pub struct NymApiTopologyProvider { validator_client: nym_validator_client::client::NymApiClient, nym_api_urls: Vec<Url>, - - client_version: String, currently_used_api: usize, } impl NymApiTopologyProvider { - pub fn new( - config: Config, - mut nym_api_urls: Vec<Url>, - client_version: String, - user_agent: Option<UserAgent>, - ) -> Self { + pub fn new(config: Config, mut nym_api_urls: Vec<Url>, user_agent: Option<UserAgent>) -> Self { nym_api_urls.shuffle(&mut thread_rng()); let validator_client = if let Some(user_agent) = user_agent { @@ -62,7 +55,6 @@ impl NymApiTopologyProvider { config, validator_client, nym_api_urls, - client_version, currently_used_api: 0, } } @@ -99,7 +91,7 @@ impl NymApiTopologyProvider { async fn get_current_compatible_topology(&mut self) -> Option<NymTopology> { let mixnodes = match self .validator_client - .get_all_basic_active_mixing_assigned_nodes(Some(self.client_version.clone())) + .get_all_basic_active_mixing_assigned_nodes() .await { Err(err) => { @@ -111,7 +103,7 @@ impl NymApiTopologyProvider { let gateways = match self .validator_client - .get_all_basic_entry_assigned_nodes(Some(self.client_version.clone())) + .get_all_basic_entry_assigned_nodes() .await { Err(err) => { diff --git a/common/client-core/src/init/helpers.rs b/common/client-core/src/init/helpers.rs index 3f6a390bd0c..60c692df9f7 100644 --- a/common/client-core/src/init/helpers.rs +++ b/common/client-core/src/init/helpers.rs @@ -94,7 +94,7 @@ pub async fn current_gateways<R: Rng>( log::debug!("Fetching list of gateways from: {nym_api}"); - let gateways = client.get_all_basic_entry_assigned_nodes(None).await?; + let gateways = client.get_all_basic_entry_assigned_nodes().await?; log::debug!("Found {} gateways", gateways.len()); log::trace!("Gateways: {:#?}", gateways); @@ -121,9 +121,7 @@ pub async fn current_mixnodes<R: Rng>( log::trace!("Fetching list of mixnodes from: {nym_api}"); - let mixnodes = client - .get_all_basic_active_mixing_assigned_nodes(None) - .await?; + let mixnodes = client.get_all_basic_active_mixing_assigned_nodes().await?; let valid_mixnodes = mixnodes .iter() .filter_map(|mixnode| mixnode.try_into().ok()) diff --git a/common/client-libs/validator-client/src/client.rs b/common/client-libs/validator-client/src/client.rs index 93db55e6dd3..cae61b0d07a 100644 --- a/common/client-libs/validator-client/src/client.rs +++ b/common/client-libs/validator-client/src/client.rs @@ -358,34 +358,19 @@ impl NymApiClient { } #[deprecated(note = "use get_all_basic_active_mixing_assigned_nodes instead")] - pub async fn get_basic_mixnodes( - &self, - semver_compatibility: Option<String>, - ) -> Result<Vec<SkimmedNode>, ValidatorClientError> { - Ok(self - .nym_api - .get_basic_mixnodes(semver_compatibility) - .await? - .nodes) + pub async fn get_basic_mixnodes(&self) -> Result<Vec<SkimmedNode>, ValidatorClientError> { + Ok(self.nym_api.get_basic_mixnodes().await?.nodes) } #[deprecated(note = "use get_all_basic_entry_assigned_nodes instead")] - pub async fn get_basic_gateways( - &self, - semver_compatibility: Option<String>, - ) -> Result<Vec<SkimmedNode>, ValidatorClientError> { - Ok(self - .nym_api - .get_basic_gateways(semver_compatibility) - .await? - .nodes) + pub async fn get_basic_gateways(&self) -> Result<Vec<SkimmedNode>, ValidatorClientError> { + Ok(self.nym_api.get_basic_gateways().await?.nodes) } /// retrieve basic information for nodes are capable of operating as an entry gateway /// this includes legacy gateways and nym-nodes pub async fn get_all_basic_entry_assigned_nodes( &self, - semver_compatibility: Option<String>, ) -> Result<Vec<SkimmedNode>, ValidatorClientError> { // TODO: deal with paging in macro or some helper function or something, because it's the same pattern everywhere let mut page = 0; @@ -394,12 +379,7 @@ impl NymApiClient { loop { let mut res = self .nym_api - .get_basic_entry_assigned_nodes( - semver_compatibility.clone(), - false, - Some(page), - None, - ) + .get_basic_entry_assigned_nodes(false, Some(page), None) .await?; nodes.append(&mut res.nodes.data); @@ -417,7 +397,6 @@ impl NymApiClient { /// this includes legacy mixnodes and nym-nodes pub async fn get_all_basic_active_mixing_assigned_nodes( &self, - semver_compatibility: Option<String>, ) -> Result<Vec<SkimmedNode>, ValidatorClientError> { // TODO: deal with paging in macro or some helper function or something, because it's the same pattern everywhere let mut page = 0; @@ -426,12 +405,7 @@ impl NymApiClient { loop { let mut res = self .nym_api - .get_basic_active_mixing_assigned_nodes( - semver_compatibility.clone(), - false, - Some(page), - None, - ) + .get_basic_active_mixing_assigned_nodes(false, Some(page), None) .await?; nodes.append(&mut res.nodes.data); @@ -449,7 +423,6 @@ impl NymApiClient { /// this includes legacy mixnodes and nym-nodes pub async fn get_all_basic_mixing_capable_nodes( &self, - semver_compatibility: Option<String>, ) -> Result<Vec<SkimmedNode>, ValidatorClientError> { // TODO: deal with paging in macro or some helper function or something, because it's the same pattern everywhere let mut page = 0; @@ -458,12 +431,7 @@ impl NymApiClient { loop { let mut res = self .nym_api - .get_basic_mixing_capable_nodes( - semver_compatibility.clone(), - false, - Some(page), - None, - ) + .get_basic_mixing_capable_nodes(false, Some(page), None) .await?; nodes.append(&mut res.nodes.data); @@ -478,10 +446,7 @@ impl NymApiClient { } /// retrieve basic information for all bonded nodes on the network - pub async fn get_all_basic_nodes( - &self, - semver_compatibility: Option<String>, - ) -> Result<Vec<SkimmedNode>, ValidatorClientError> { + pub async fn get_all_basic_nodes(&self) -> Result<Vec<SkimmedNode>, ValidatorClientError> { // TODO: deal with paging in macro or some helper function or something, because it's the same pattern everywhere let mut page = 0; let mut nodes = Vec::new(); @@ -489,7 +454,7 @@ impl NymApiClient { loop { let mut res = self .nym_api - .get_basic_nodes(semver_compatibility.clone(), false, Some(page), None) + .get_basic_nodes(false, Some(page), None) .await?; nodes.append(&mut res.nodes.data); 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 f7c13682670..ebfb85ba97c 100644 --- a/common/client-libs/validator-client/src/nym_api/mod.rs +++ b/common/client-libs/validator-client/src/nym_api/mod.rs @@ -205,16 +205,7 @@ pub trait NymApiClientExt: ApiClient { #[deprecated] #[tracing::instrument(level = "debug", skip_all)] - async fn get_basic_mixnodes( - &self, - semver_compatibility: Option<String>, - ) -> Result<CachedNodesResponse<SkimmedNode>, NymAPIError> { - let params = if let Some(semver_compatibility) = &semver_compatibility { - vec![("semver_compatibility", semver_compatibility.as_str())] - } else { - vec![] - }; - + async fn get_basic_mixnodes(&self) -> Result<CachedNodesResponse<SkimmedNode>, NymAPIError> { self.get_json( &[ routes::API_VERSION, @@ -223,23 +214,14 @@ pub trait NymApiClientExt: ApiClient { "mixnodes", "skimmed", ], - ¶ms, + NO_PARAMS, ) .await } #[deprecated] #[instrument(level = "debug", skip(self))] - async fn get_basic_gateways( - &self, - semver_compatibility: Option<String>, - ) -> Result<CachedNodesResponse<SkimmedNode>, NymAPIError> { - let params = if let Some(semver_compatibility) = &semver_compatibility { - vec![("semver_compatibility", semver_compatibility.as_str())] - } else { - vec![] - }; - + async fn get_basic_gateways(&self) -> Result<CachedNodesResponse<SkimmedNode>, NymAPIError> { self.get_json( &[ routes::API_VERSION, @@ -248,7 +230,7 @@ pub trait NymApiClientExt: ApiClient { "gateways", "skimmed", ], - ¶ms, + NO_PARAMS, ) .await } @@ -258,17 +240,12 @@ pub trait NymApiClientExt: ApiClient { #[instrument(level = "debug", skip(self))] async fn get_basic_entry_assigned_nodes( &self, - semver_compatibility: Option<String>, no_legacy: bool, page: Option<u32>, per_page: Option<u32>, ) -> Result<PaginatedCachedNodesResponse<SkimmedNode>, NymAPIError> { let mut params = Vec::new(); - if let Some(arg) = &semver_compatibility { - params.push(("semver_compatibility", arg.clone())) - } - if no_legacy { params.push(("no_legacy", "true".to_string())) } @@ -300,17 +277,12 @@ pub trait NymApiClientExt: ApiClient { #[instrument(level = "debug", skip(self))] async fn get_basic_active_mixing_assigned_nodes( &self, - semver_compatibility: Option<String>, no_legacy: bool, page: Option<u32>, per_page: Option<u32>, ) -> Result<PaginatedCachedNodesResponse<SkimmedNode>, NymAPIError> { let mut params = Vec::new(); - if let Some(arg) = &semver_compatibility { - params.push(("semver_compatibility", arg.clone())) - } - if no_legacy { params.push(("no_legacy", "true".to_string())) } @@ -342,17 +314,12 @@ pub trait NymApiClientExt: ApiClient { #[instrument(level = "debug", skip(self))] async fn get_basic_mixing_capable_nodes( &self, - semver_compatibility: Option<String>, no_legacy: bool, page: Option<u32>, per_page: Option<u32>, ) -> Result<PaginatedCachedNodesResponse<SkimmedNode>, NymAPIError> { let mut params = Vec::new(); - if let Some(arg) = &semver_compatibility { - params.push(("semver_compatibility", arg.clone())) - } - if no_legacy { params.push(("no_legacy", "true".to_string())) } @@ -382,17 +349,12 @@ pub trait NymApiClientExt: ApiClient { #[instrument(level = "debug", skip(self))] async fn get_basic_nodes( &self, - semver_compatibility: Option<String>, no_legacy: bool, page: Option<u32>, per_page: Option<u32>, ) -> Result<PaginatedCachedNodesResponse<SkimmedNode>, NymAPIError> { let mut params = Vec::new(); - if let Some(arg) = &semver_compatibility { - params.push(("semver_compatibility", arg.clone())) - } - if no_legacy { params.push(("no_legacy", "true".to_string())) } diff --git a/common/mixnode-common/src/verloc/mod.rs b/common/mixnode-common/src/verloc/mod.rs index 7bcea71c9d8..1ac4cc8957d 100644 --- a/common/mixnode-common/src/verloc/mod.rs +++ b/common/mixnode-common/src/verloc/mod.rs @@ -6,7 +6,6 @@ use crate::verloc::sender::{PacketSender, TestedNode}; use futures::stream::FuturesUnordered; use futures::StreamExt; use log::*; -use nym_bin_common::version_checker::{self, parse_version}; use nym_crypto::asymmetric::identity; use nym_network_defaults::mainnet::NYM_API; use nym_node_http_api::state::metrics::{SharedVerlocStats, VerlocNodeResult}; @@ -30,9 +29,6 @@ pub(crate) mod measurement; pub(crate) mod packet; pub(crate) mod sender; -// TODO: MUST BE UPDATED BEFORE ACTUAL RELEASE!! -pub const MINIMUM_NODE_VERSION: &str = "0.10.1"; - // by default all of those are overwritten by config data from mixnodes directly const DEFAULT_VERLOC_PORT: u16 = 1790; const DEFAULT_PACKETS_PER_NODE: usize = 100; @@ -45,9 +41,6 @@ const DEFAULT_RETRY_TIMEOUT: Duration = Duration::from_secs(60 * 30); #[derive(Clone, Debug)] pub struct Config { - /// Minimum semver version of a node (gateway or mixnode) that is capable of replying to echo packets. - minimum_compatible_node_version: version_checker::Version, - /// Socket address of this node on which it will be listening for the measurement packets. listening_address: SocketAddr, @@ -91,11 +84,6 @@ impl ConfigBuilder { Self::default() } - pub fn minimum_compatible_node_version(mut self, version: version_checker::Version) -> Self { - self.0.minimum_compatible_node_version = version; - self - } - pub fn listening_address(mut self, listening_address: SocketAddr) -> Self { self.0.listening_address = listening_address; self @@ -154,7 +142,6 @@ impl ConfigBuilder { impl Default for ConfigBuilder { fn default() -> Self { ConfigBuilder(Config { - minimum_compatible_node_version: parse_version(MINIMUM_NODE_VERSION).unwrap(), listening_address: format!("[::]:{DEFAULT_VERLOC_PORT}").parse().unwrap(), packets_per_node: DEFAULT_PACKETS_PER_NODE, packet_timeout: DEFAULT_PACKET_TIMEOUT, diff --git a/common/topology/src/filter.rs b/common/topology/src/filter.rs deleted file mode 100644 index baf76225f8d..00000000000 --- a/common/topology/src/filter.rs +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright 2021 - Nym Technologies SA <contact@nymtech.net> -// SPDX-License-Identifier: Apache-2.0 - -use nym_bin_common::version_checker; -use std::collections::{BTreeMap, HashMap}; -use std::hash::Hash; - -pub trait Versioned: Clone { - fn version(&self) -> String; -} - -pub trait VersionFilterable<T> { - #[must_use] - fn filter_by_version(&self, expected_version: &str) -> Self; -} - -impl<T> VersionFilterable<T> for Vec<T> -where - T: Versioned, -{ - fn filter_by_version(&self, expected_version: &str) -> Self { - self.iter() - .filter(|node| { - version_checker::is_minor_version_compatible(&node.version(), expected_version) - }) - .cloned() - .collect() - } -} - -impl<T, K, V> VersionFilterable<T> for HashMap<K, V> -where - K: Eq + Hash + Clone, - V: VersionFilterable<T>, - T: Versioned, -{ - fn filter_by_version(&self, expected_version: &str) -> Self { - self.iter() - .map(|(k, v)| (k.clone(), v.filter_by_version(expected_version))) - .collect() - } -} - -impl<T, K, V> VersionFilterable<T> for BTreeMap<K, V> -where - K: Eq + Ord + Clone, - V: VersionFilterable<T>, - T: Versioned, -{ - fn filter_by_version(&self, expected_version: &str) -> Self { - self.iter() - .map(|(k, v)| (k.clone(), v.filter_by_version(expected_version))) - .collect() - } -} diff --git a/common/topology/src/gateway.rs b/common/topology/src/gateway.rs index e6f4981560f..545f47dd211 100644 --- a/common/topology/src/gateway.rs +++ b/common/topology/src/gateway.rs @@ -1,7 +1,7 @@ // Copyright 2021 - Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: Apache-2.0 -use crate::{filter, NetworkAddress, NodeVersion}; +use crate::{NetworkAddress, NodeVersion}; use nym_api_requests::nym_nodes::SkimmedNode; use nym_crypto::asymmetric::{encryption, identity}; use nym_mixnet_contract_common::NodeId; @@ -126,13 +126,6 @@ impl fmt::Display for LegacyNode { } } -impl filter::Versioned for LegacyNode { - fn version(&self) -> String { - // TODO: return semver instead - self.version.to_string() - } -} - impl<'a> From<&'a LegacyNode> for SphinxNode { fn from(node: &'a LegacyNode) -> Self { let node_address_bytes = NymNodeRoutingAddress::from(node.mix_host) diff --git a/common/topology/src/lib.rs b/common/topology/src/lib.rs index 6e89cf27535..4133a8ae0e3 100644 --- a/common/topology/src/lib.rs +++ b/common/topology/src/lib.rs @@ -4,7 +4,6 @@ #![allow(unknown_lints)] // clippy::to_string_trait_impl is not on stable as of 1.77 -use crate::filter::VersionFilterable; pub use error::NymTopologyError; use log::{debug, info, warn}; use nym_api_requests::nym_nodes::{CachedNodesResponse, SkimmedNode}; @@ -25,7 +24,6 @@ use std::str::FromStr; use ::serde::{Deserialize, Deserializer, Serialize, Serializer}; pub mod error; -pub mod filter; pub mod gateway; pub mod mix; pub mod random_route_provider; @@ -465,19 +463,6 @@ impl NymTopology { Ok(()) } - - #[must_use] - pub fn filter_system_version(&self, expected_version: &str) -> Self { - self.filter_node_versions(expected_version) - } - - #[must_use] - pub fn filter_node_versions(&self, expected_mix_version: &str) -> Self { - NymTopology { - mixes: self.mixes.filter_by_version(expected_mix_version), - gateways: self.gateways.clone(), - } - } } #[cfg(feature = "serializable")] diff --git a/common/topology/src/mix.rs b/common/topology/src/mix.rs index 170f1000a55..40c61cff4b4 100644 --- a/common/topology/src/mix.rs +++ b/common/topology/src/mix.rs @@ -1,7 +1,7 @@ // Copyright 2021 - Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: Apache-2.0 -use crate::{filter, NetworkAddress, NodeVersion}; +use crate::{NetworkAddress, NodeVersion}; use nym_api_requests::nym_nodes::{NodeRole, SkimmedNode}; use nym_crypto::asymmetric::{encryption, identity}; pub use nym_mixnet_contract_common::LegacyMixLayer; @@ -89,13 +89,6 @@ impl LegacyNode { } } -impl filter::Versioned for LegacyNode { - fn version(&self) -> String { - // TODO: return semver instead - self.version.to_string() - } -} - impl<'a> From<&'a LegacyNode> for SphinxNode { fn from(node: &'a LegacyNode) -> Self { let node_address_bytes = NymNodeRoutingAddress::from(node.mix_host) diff --git a/common/wasm/client-core/src/helpers.rs b/common/wasm/client-core/src/helpers.rs index c403e9a8dea..eee064e2cc2 100644 --- a/common/wasm/client-core/src/helpers.rs +++ b/common/wasm/client-core/src/helpers.rs @@ -68,9 +68,9 @@ pub async fn current_network_topology_async( let api_client = NymApiClient::new(url); let mixnodes = api_client - .get_all_basic_active_mixing_assigned_nodes(None) + .get_all_basic_active_mixing_assigned_nodes() .await?; - let gateways = api_client.get_all_basic_entry_assigned_nodes(None).await?; + let gateways = api_client.get_all_basic_entry_assigned_nodes().await?; Ok(NymTopology::from_basic(&mixnodes, &gateways).into()) } diff --git a/gateway/src/node/helpers.rs b/gateway/src/node/helpers.rs index d87b02afed0..944102d887f 100644 --- a/gateway/src/node/helpers.rs +++ b/gateway/src/node/helpers.rs @@ -31,7 +31,6 @@ impl GatewayTopologyProvider { min_gateway_performance: 0, }, nym_api_url, - env!("CARGO_PKG_VERSION").to_string(), Some(user_agent), ), cache_ttl, diff --git a/gateway/src/node/mod.rs b/gateway/src/node/mod.rs index 90e60f5bfc1..644c3672922 100644 --- a/gateway/src/node/mod.rs +++ b/gateway/src/node/mod.rs @@ -540,7 +540,7 @@ impl<St> Gateway<St> { // as opposed to the validator API for api_url in self.config.get_nym_api_endpoints() { let client = nym_validator_client::NymApiClient::new(api_url.clone()); - match client.get_all_basic_nodes(None).await { + match client.get_all_basic_nodes().await { Ok(nodes) => { return nodes.iter().any(|node| { &node.ed25519_identity_pubkey == self.identity_keypair.public_key() diff --git a/mixnode/src/node/mod.rs b/mixnode/src/node/mod.rs index 9e6358de57f..261953dbd41 100644 --- a/mixnode/src/node/mod.rs +++ b/mixnode/src/node/mod.rs @@ -160,7 +160,7 @@ impl MixNode { // as opposed to the validator API for api_url in self.config.get_nym_api_endpoints() { let client = nym_validator_client::NymApiClient::new(api_url.clone()); - match client.get_all_basic_nodes(None).await { + match client.get_all_basic_nodes().await { Ok(nodes) => { return nodes.iter().any(|node| { &node.ed25519_identity_pubkey == self.identity_keypair.public_key() diff --git a/nym-api/src/nym_nodes/handlers/unstable/helpers.rs b/nym-api/src/nym_nodes/handlers/unstable/helpers.rs index 4f7a20155e4..38c981a97a2 100644 --- a/nym-api/src/nym_nodes/handlers/unstable/helpers.rs +++ b/nym-api/src/nym_nodes/handlers/unstable/helpers.rs @@ -5,13 +5,10 @@ use nym_api_requests::models::{ GatewayBondAnnotated, MalformedNodeBond, MixNodeBondAnnotated, OffsetDateTimeJsonSchemaWrapper, }; use nym_api_requests::nym_nodes::{NodeRole, SkimmedNode}; -use nym_bin_common::version_checker; use nym_mixnet_contract_common::reward_params::Performance; use time::OffsetDateTime; pub(crate) trait LegacyAnnotation { - fn version(&self) -> &str; - fn performance(&self) -> Performance; fn identity(&self) -> &str; @@ -20,10 +17,6 @@ pub(crate) trait LegacyAnnotation { } impl LegacyAnnotation for MixNodeBondAnnotated { - fn version(&self) -> &str { - self.version() - } - fn performance(&self) -> Performance { self.node_performance.last_24h } @@ -38,10 +31,6 @@ impl LegacyAnnotation for MixNodeBondAnnotated { } impl LegacyAnnotation for GatewayBondAnnotated { - fn version(&self) -> &str { - self.version() - } - fn performance(&self) -> Performance { self.node_performance.last_24h } @@ -60,12 +49,3 @@ pub(crate) fn refreshed_at( ) -> OffsetDateTimeJsonSchemaWrapper { iter.into_iter().min().unwrap().into() } - -pub(crate) fn semver(requirement: &Option<String>, declared: &str) -> bool { - if let Some(semver_compat) = requirement.as_ref() { - if !version_checker::is_minor_version_compatible(declared, semver_compat) { - return false; - } - } - true -} diff --git a/nym-api/src/nym_nodes/handlers/unstable/mod.rs b/nym-api/src/nym_nodes/handlers/unstable/mod.rs index a9110a599de..88d0338e6f4 100644 --- a/nym-api/src/nym_nodes/handlers/unstable/mod.rs +++ b/nym-api/src/nym_nodes/handlers/unstable/mod.rs @@ -80,6 +80,7 @@ struct NodesParamsWithRole { #[param(inline)] role: Option<NodeRoleQueryParam>, + #[allow(dead_code)] semver_compatibility: Option<String>, no_legacy: Option<bool>, page: Option<u32>, @@ -88,6 +89,7 @@ struct NodesParamsWithRole { #[derive(Debug, Deserialize, utoipa::IntoParams)] struct NodesParams { + #[allow(dead_code)] semver_compatibility: Option<String>, no_legacy: Option<bool>, page: Option<u32>, diff --git a/nym-api/src/nym_nodes/handlers/unstable/skimmed.rs b/nym-api/src/nym_nodes/handlers/unstable/skimmed.rs index a4aba882cfe..fda8d8bad26 100644 --- a/nym-api/src/nym_nodes/handlers/unstable/skimmed.rs +++ b/nym-api/src/nym_nodes/handlers/unstable/skimmed.rs @@ -4,7 +4,7 @@ use crate::node_describe_cache::DescribedNodes; use crate::node_status_api::models::{AxumErrorResponse, AxumResult}; use crate::nym_contract_cache::cache::CachedRewardedSet; -use crate::nym_nodes::handlers::unstable::helpers::{refreshed_at, semver, LegacyAnnotation}; +use crate::nym_nodes::handlers::unstable::helpers::{refreshed_at, LegacyAnnotation}; use crate::nym_nodes::handlers::unstable::{NodesParams, NodesParamsWithRole}; use crate::support::caching::Cache; use crate::support::http::state::AppState; @@ -25,7 +25,6 @@ pub type PaginatedSkimmedNodes = AxumResult<Json<PaginatedCachedNodesResponse<Sk /// Given all relevant caches, build part of response for JUST Nym Nodes fn build_nym_nodes_response<'a, NI>( rewarded_set: &CachedRewardedSet, - required_semver: &Option<String>, nym_nodes_subset: NI, annotations: &HashMap<NodeId, NodeAnnotation>, active_only: bool, @@ -37,11 +36,6 @@ where for nym_node in nym_nodes_subset { let node_id = nym_node.node_id; - // if we have wrong version, ignore - if !semver(required_semver, nym_node.version()) { - continue; - } - let role: NodeRole = rewarded_set.role(node_id).into(); // if the role is inactive, see if our filter allows it @@ -61,7 +55,6 @@ where /// Given all relevant caches, add appropriate legacy nodes to the part of the response fn add_legacy<LN>( nodes: &mut Vec<SkimmedNode>, - required_semver: &Option<String>, rewarded_set: &CachedRewardedSet, describe_cache: &DescribedNodes, annotated_legacy_nodes: &HashMap<NodeId, LN>, @@ -70,11 +63,6 @@ fn add_legacy<LN>( LN: LegacyAnnotation, { for (node_id, legacy) in annotated_legacy_nodes.iter() { - // if we have wrong version, ignore - if !semver(required_semver, legacy.version()) { - continue; - } - let role: NodeRole = rewarded_set.role(*node_id).into(); // if the role is inactive, see if our filter allows it @@ -121,7 +109,6 @@ where // TODO: implement it let _ = query_params.per_page; let _ = query_params.page; - let semver_compatibility = query_params.semver_compatibility; // 1. get the rewarded set let rewarded_set = state.rewarded_set().await?; @@ -134,13 +121,8 @@ where let describe_cache = state.describe_nodes_cache_data().await?; // 4. start building the response - let mut nodes = build_nym_nodes_response( - &rewarded_set, - &semver_compatibility, - nym_nodes_subset, - &annotations, - active_only, - ); + let mut nodes = + build_nym_nodes_response(&rewarded_set, nym_nodes_subset, &annotations, active_only); // 5. if we allow legacy nodes, repeat the procedure for them, otherwise return just nym-nodes if let Some(true) = query_params.no_legacy { @@ -162,7 +144,6 @@ where let annotated_legacy_nodes = annotated_legacy_nodes_getter(state).await?; add_legacy( &mut nodes, - &semver_compatibility, &rewarded_set, &describe_cache, &annotated_legacy_nodes, @@ -239,14 +220,13 @@ pub(super) async fn deprecated_mixnodes_basic( async fn nodes_basic( state: State<AppState>, - Query(query_params): Query<NodesParams>, + Query(_query_params): Query<NodesParams>, active_only: bool, ) -> PaginatedSkimmedNodes { // unfortunately we have to build the response semi-manually here as we need to add two sources of legacy nodes // 1. grab all relevant described nym-nodes let rewarded_set = state.rewarded_set().await?; - let semver_compatibility = &query_params.semver_compatibility; let describe_cache = state.describe_nodes_cache_data().await?; let all_nym_nodes = describe_cache.all_nym_nodes(); @@ -254,18 +234,12 @@ async fn nodes_basic( let legacy_mixnodes = state.legacy_mixnode_annotations().await?; let legacy_gateways = state.legacy_gateways_annotations().await?; - let mut nodes = build_nym_nodes_response( - &rewarded_set, - semver_compatibility, - all_nym_nodes, - &annotations, - active_only, - ); + let mut nodes = + build_nym_nodes_response(&rewarded_set, all_nym_nodes, &annotations, active_only); // add legacy gateways to the response add_legacy( &mut nodes, - semver_compatibility, &rewarded_set, &describe_cache, &legacy_gateways, @@ -275,7 +249,6 @@ async fn nodes_basic( // add legacy mixnodes to the response add_legacy( &mut nodes, - semver_compatibility, &rewarded_set, &describe_cache, &legacy_mixnodes, diff --git a/nym-node-status-api/src/monitor/mod.rs b/nym-node-status-api/src/monitor/mod.rs index d0d1c5f6388..22c2482e6ef 100644 --- a/nym-node-status-api/src/monitor/mod.rs +++ b/nym-node-status-api/src/monitor/mod.rs @@ -125,7 +125,7 @@ async fn run( log_gw_in_explorer_not_api(explorer_gateways.as_slice(), gateways.as_slice()); let all_skimmed_nodes = api_client - .get_all_basic_nodes(None) + .get_all_basic_nodes() .await .log_error("get_all_basic_nodes")?; diff --git a/nym-wallet/Cargo.lock b/nym-wallet/Cargo.lock index 81fa6e2a7b2..6d1ec90fd49 100644 --- a/nym-wallet/Cargo.lock +++ b/nym-wallet/Cargo.lock @@ -3104,7 +3104,6 @@ dependencies = [ "log", "pretty_env_logger", "schemars", - "semver", "serde", "utoipa", "vergen", diff --git a/sdk/rust/nym-sdk/examples/custom_topology_provider.rs b/sdk/rust/nym-sdk/examples/custom_topology_provider.rs index 3a617b6b781..df5f4ef5a65 100644 --- a/sdk/rust/nym-sdk/examples/custom_topology_provider.rs +++ b/sdk/rust/nym-sdk/examples/custom_topology_provider.rs @@ -21,7 +21,7 @@ impl MyTopologyProvider { async fn get_topology(&self) -> NymTopology { let mixnodes = self .validator_client - .get_all_basic_active_mixing_assigned_nodes(None) + .get_all_basic_active_mixing_assigned_nodes() .await .unwrap(); @@ -35,7 +35,7 @@ impl MyTopologyProvider { let gateways = self .validator_client - .get_all_basic_entry_assigned_nodes(None) + .get_all_basic_entry_assigned_nodes() .await .unwrap(); diff --git a/sdk/rust/nym-sdk/examples/geo_topology_provider.rs b/sdk/rust/nym-sdk/examples/geo_topology_provider.rs index e57de3f1ada..3b327d7377d 100644 --- a/sdk/rust/nym-sdk/examples/geo_topology_provider.rs +++ b/sdk/rust/nym-sdk/examples/geo_topology_provider.rs @@ -22,7 +22,6 @@ async fn main() { // We filter on the version of the mixnodes. Be prepared to manually update // this to keep this example working, as we can't (currently) fetch to current // latest version. - "1.1.31".to_string(), group_by, ); diff --git a/service-providers/authenticator/src/cli/mod.rs b/service-providers/authenticator/src/cli/mod.rs index e8d6e1d1230..08aca6f720e 100644 --- a/service-providers/authenticator/src/cli/mod.rs +++ b/service-providers/authenticator/src/cli/mod.rs @@ -8,8 +8,8 @@ use nym_authenticator::{ config::{helpers::try_upgrade_config, BaseClientConfig, Config}, error::AuthenticatorError, }; +use nym_bin_common::bin_info; use nym_bin_common::completions::{fig_generate, ArgShell}; -use nym_bin_common::{bin_info, version_checker}; use nym_client_core::cli_helpers::CliClient; use std::sync::OnceLock; @@ -171,31 +171,3 @@ async fn try_load_current_config(id: &str) -> Result<Config, AuthenticatorError> Ok(config) } - -// this only checks compatibility between config the binary. It does not take into consideration -// network version. It might do so in the future. -fn version_check(cfg: &Config) -> bool { - let binary_version = env!("CARGO_PKG_VERSION"); - let config_version = &cfg.base.client.version; - if binary_version == config_version { - true - } else { - log::warn!( - "The native-client binary has different version than what is specified \ - in config file! {binary_version} and {config_version}", - ); - if version_checker::is_minor_version_compatible(binary_version, config_version) { - log::info!( - "but they are still semver compatible. \ - However, consider running the `upgrade` command" - ); - true - } else { - log::error!( - "and they are semver incompatible! - \ - please run the `upgrade` command before attempting `run` again" - ); - false - } - } -} diff --git a/service-providers/authenticator/src/cli/request.rs b/service-providers/authenticator/src/cli/request.rs index eca46a1892b..0a8dfe3cd55 100644 --- a/service-providers/authenticator/src/cli/request.rs +++ b/service-providers/authenticator/src/cli/request.rs @@ -1,9 +1,9 @@ // Copyright 2024 - Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: Apache-2.0 +use crate::cli::try_load_current_config; use crate::cli::AuthenticatorError; use crate::cli::{override_config, OverrideConfig}; -use crate::cli::{try_load_current_config, version_check}; use clap::{Args, Subcommand}; use nym_authenticator_requests::latest::{ registration::{ClientMac, FinalMessage, GatewayClient, InitMessage, IpPair}, @@ -96,11 +96,6 @@ pub(crate) async fn execute(args: &Request) -> Result<(), AuthenticatorError> { let mut config = try_load_current_config(&args.common_args.id).await?; config = override_config(config, OverrideConfig::from(args.clone())); - if !version_check(&config) { - log::error!("failed the local version check"); - return Err(AuthenticatorError::FailedLocalVersionCheck); - } - let shutdown = TaskHandle::default(); let mixnet_client = nym_authenticator::mixnet_client::create_mixnet_client( &config.base, diff --git a/service-providers/authenticator/src/cli/run.rs b/service-providers/authenticator/src/cli/run.rs index 42f9e9ab0e0..2d1b56b2da2 100644 --- a/service-providers/authenticator/src/cli/run.rs +++ b/service-providers/authenticator/src/cli/run.rs @@ -1,13 +1,10 @@ // Copyright 2024 - Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: Apache-2.0 -use std::sync::Arc; - use crate::cli::peer_handler::DummyHandler; +use crate::cli::try_load_current_config; use crate::cli::{override_config, OverrideConfig}; -use crate::cli::{try_load_current_config, version_check}; use clap::Args; -use log::error; use nym_authenticator::error::AuthenticatorError; use nym_client_core::cli_helpers::client_run::CommonClientRunArgs; use nym_crypto::asymmetric::x25519::KeyPair; @@ -15,6 +12,7 @@ use nym_gateway_storage::PersistentStorage; use nym_task::TaskHandle; use nym_wireguard::WireguardGatewayData; use rand::rngs::OsRng; +use std::sync::Arc; #[allow(clippy::struct_excessive_bools)] #[derive(Args, Clone)] @@ -38,11 +36,6 @@ pub(crate) async fn execute(args: &Run) -> Result<(), AuthenticatorError> { config = override_config(config, OverrideConfig::from(args.clone())); log::debug!("Using config: {:#?}", config); - if !version_check(&config) { - error!("failed the local version check"); - return Err(AuthenticatorError::FailedLocalVersionCheck); - } - log::info!("Starting authenticator service provider"); let (wireguard_gateway_data, peer_rx) = WireguardGatewayData::new( config.authenticator.clone().into(), diff --git a/service-providers/authenticator/src/cli/sign.rs b/service-providers/authenticator/src/cli/sign.rs index 5d8306ca840..9e1de18bf05 100644 --- a/service-providers/authenticator/src/cli/sign.rs +++ b/service-providers/authenticator/src/cli/sign.rs @@ -1,7 +1,7 @@ // Copyright 2024 - Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: Apache-2.0 -use crate::cli::{try_load_current_config, version_check}; +use crate::cli::try_load_current_config; use clap::Args; use nym_authenticator::error::AuthenticatorError; use nym_bin_common::output_format::OutputFormat; @@ -57,11 +57,6 @@ fn print_signed_contract_msg( pub(crate) async fn execute(args: &Sign) -> Result<(), AuthenticatorError> { let config = try_load_current_config(&args.id).await?; - if !version_check(&config) { - log::error!("Failed the local version check"); - return Err(AuthenticatorError::FailedLocalVersionCheck); - } - let key_store = OnDiskKeys::new(config.storage_paths.common_paths.keys); let identity_keypair = key_store.load_identity_keypair().map_err(|source| { AuthenticatorError::ClientCoreError(ClientCoreError::KeyStoreError { diff --git a/service-providers/authenticator/src/error.rs b/service-providers/authenticator/src/error.rs index 2f272e4d165..5326313bc75 100644 --- a/service-providers/authenticator/src/error.rs +++ b/service-providers/authenticator/src/error.rs @@ -23,9 +23,6 @@ pub enum AuthenticatorError { #[error("received too short packet")] ShortPacket, - #[error("failed local version check, client and config mismatch")] - FailedLocalVersionCheck, - #[error("failed to connect to mixnet: {source}")] FailedToConnectToMixnet { source: nym_sdk::Error }, diff --git a/service-providers/ip-packet-router/src/cli/mod.rs b/service-providers/ip-packet-router/src/cli/mod.rs index ee746f48c96..11c2a0e049f 100644 --- a/service-providers/ip-packet-router/src/cli/mod.rs +++ b/service-providers/ip-packet-router/src/cli/mod.rs @@ -1,8 +1,8 @@ use crate::cli::ecash::Ecash; use clap::{CommandFactory, Parser, Subcommand}; use log::error; +use nym_bin_common::bin_info; use nym_bin_common::completions::{fig_generate, ArgShell}; -use nym_bin_common::{bin_info, version_checker}; use nym_client_core::cli_helpers::CliClient; use nym_ip_packet_router::config::helpers::try_upgrade_config; use nym_ip_packet_router::config::{BaseClientConfig, Config}; @@ -167,31 +167,3 @@ async fn try_load_current_config(id: &str) -> Result<Config, IpPacketRouterError Ok(config) } - -// this only checks compatibility between config the binary. It does not take into consideration -// network version. It might do so in the future. -fn version_check(cfg: &Config) -> bool { - let binary_version = env!("CARGO_PKG_VERSION"); - let config_version = &cfg.base.client.version; - if binary_version == config_version { - true - } else { - log::warn!( - "The native-client binary has different version than what is specified \ - in config file! {binary_version} and {config_version}", - ); - if version_checker::is_minor_version_compatible(binary_version, config_version) { - log::info!( - "but they are still semver compatible. \ - However, consider running the `upgrade` command" - ); - true - } else { - log::error!( - "and they are semver incompatible! - \ - please run the `upgrade` command before attempting `run` again" - ); - false - } - } -} diff --git a/service-providers/ip-packet-router/src/cli/run.rs b/service-providers/ip-packet-router/src/cli/run.rs index 32d97efddcc..3e4010cb326 100644 --- a/service-providers/ip-packet-router/src/cli/run.rs +++ b/service-providers/ip-packet-router/src/cli/run.rs @@ -1,7 +1,6 @@ +use crate::cli::try_load_current_config; use crate::cli::{override_config, OverrideConfig}; -use crate::cli::{try_load_current_config, version_check}; use clap::Args; -use log::error; use nym_client_core::cli_helpers::client_run::CommonClientRunArgs; use nym_ip_packet_router::error::IpPacketRouterError; @@ -27,11 +26,6 @@ pub(crate) async fn execute(args: &Run) -> Result<(), IpPacketRouterError> { config = override_config(config, OverrideConfig::from(args.clone())); log::debug!("Using config: {:#?}", config); - if !version_check(&config) { - error!("failed the local version check"); - return Err(IpPacketRouterError::FailedLocalVersionCheck); - } - log::info!("Starting ip packet router service provider"); let mut server = nym_ip_packet_router::IpPacketRouter::new(config); if let Some(custom_mixnet) = &args.common_args.custom_mixnet { diff --git a/service-providers/ip-packet-router/src/cli/sign.rs b/service-providers/ip-packet-router/src/cli/sign.rs index 968ce83536f..cb4aea63bd5 100644 --- a/service-providers/ip-packet-router/src/cli/sign.rs +++ b/service-providers/ip-packet-router/src/cli/sign.rs @@ -1,4 +1,4 @@ -use crate::cli::{try_load_current_config, version_check}; +use crate::cli::try_load_current_config; use clap::Args; use nym_bin_common::output_format::OutputFormat; use nym_client_core::client::key_manager::persistence::OnDiskKeys; @@ -54,11 +54,6 @@ fn print_signed_contract_msg( pub(crate) async fn execute(args: &Sign) -> Result<(), IpPacketRouterError> { let config = try_load_current_config(&args.id).await?; - if !version_check(&config) { - log::error!("Failed the local version check"); - return Err(IpPacketRouterError::FailedLocalVersionCheck); - } - let key_store = OnDiskKeys::new(config.storage_paths.common_paths.keys); let identity_keypair = key_store.load_identity_keypair().map_err(|source| { IpPacketRouterError::ClientCoreError(ClientCoreError::KeyStoreError { diff --git a/service-providers/ip-packet-router/src/error.rs b/service-providers/ip-packet-router/src/error.rs index 6a7e5393d7f..28e21e6edd1 100644 --- a/service-providers/ip-packet-router/src/error.rs +++ b/service-providers/ip-packet-router/src/error.rs @@ -23,9 +23,6 @@ pub enum IpPacketRouterError { #[error("failed to validate the loaded config")] ConfigValidationFailure, - #[error("failed local version check, client and config mismatch")] - FailedLocalVersionCheck, - #[error("failed to setup mixnet client: {source}")] FailedToSetupMixnetClient { source: nym_sdk::Error }, diff --git a/service-providers/network-requester/src/cli/mod.rs b/service-providers/network-requester/src/cli/mod.rs index 750575f2fbe..c647913ea6e 100644 --- a/service-providers/network-requester/src/cli/mod.rs +++ b/service-providers/network-requester/src/cli/mod.rs @@ -11,7 +11,6 @@ use clap::{CommandFactory, Parser, Subcommand}; use log::error; use nym_bin_common::bin_info; use nym_bin_common::completions::{fig_generate, ArgShell}; -use nym_bin_common::version_checker; use nym_client_core::cli_helpers::CliClient; use nym_config::OptionalSet; use std::sync::OnceLock; @@ -187,34 +186,6 @@ async fn try_load_current_config(id: &str) -> Result<Config, NetworkRequesterErr Ok(config) } -// this only checks compatibility between config the binary. It does not take into consideration -// network version. It might do so in the future. -fn version_check(cfg: &Config) -> bool { - let binary_version = env!("CARGO_PKG_VERSION"); - let config_version = &cfg.base.client.version; - if binary_version == config_version { - true - } else { - log::warn!( - "The native-client binary has different version than what is specified \ - in config file! {binary_version} and {config_version}", - ); - if version_checker::is_minor_version_compatible(binary_version, config_version) { - log::info!( - "but they are still semver compatible. \ - However, consider running the `upgrade` command" - ); - true - } else { - log::error!( - "and they are semver incompatible! - \ - please run the `upgrade` command before attempting `run` again" - ); - false - } - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/service-providers/network-requester/src/cli/run.rs b/service-providers/network-requester/src/cli/run.rs index 19806b13b82..6a061dd7114 100644 --- a/service-providers/network-requester/src/cli/run.rs +++ b/service-providers/network-requester/src/cli/run.rs @@ -1,13 +1,12 @@ // Copyright 2023 - Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: GPL-3.0-only -use crate::cli::{try_load_current_config, version_check}; +use crate::cli::try_load_current_config; use crate::{ cli::{override_config, OverrideConfig}, error::NetworkRequesterError, }; use clap::Args; -use log::error; use nym_client_core::cli_helpers::client_run::CommonClientRunArgs; #[allow(clippy::struct_excessive_bools)] @@ -58,11 +57,6 @@ pub(crate) async fn execute(args: &Run) -> Result<(), NetworkRequesterError> { ); } - if !version_check(&config) { - error!("failed the local version check"); - return Err(NetworkRequesterError::FailedLocalVersionCheck); - } - log::info!("Starting socks5 service provider"); let mut server = crate::core::NRServiceProviderBuilder::new(config); if let Some(custom_mixnet) = &args.common_args.custom_mixnet { diff --git a/service-providers/network-requester/src/cli/sign.rs b/service-providers/network-requester/src/cli/sign.rs index 9e43de6009c..769c5c5b138 100644 --- a/service-providers/network-requester/src/cli/sign.rs +++ b/service-providers/network-requester/src/cli/sign.rs @@ -1,7 +1,7 @@ // Copyright 2023 - Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: GPL-3.0-only -use crate::cli::{try_load_current_config, version_check}; +use crate::cli::try_load_current_config; use crate::error::NetworkRequesterError; use clap::Args; use nym_bin_common::output_format::OutputFormat; @@ -57,11 +57,6 @@ fn print_signed_contract_msg( pub(crate) async fn execute(args: &Sign) -> Result<(), NetworkRequesterError> { let config = try_load_current_config(&args.id).await?; - if !version_check(&config) { - log::error!("Failed the local version check"); - return Err(NetworkRequesterError::FailedLocalVersionCheck); - } - let key_store = OnDiskKeys::new(config.storage_paths.common_paths.keys); let identity_keypair = key_store.load_identity_keypair().map_err(|source| { NetworkRequesterError::ClientCoreError(ClientCoreError::KeyStoreError { diff --git a/service-providers/network-requester/src/error.rs b/service-providers/network-requester/src/error.rs index 52a3cd8108b..8b15743f2bf 100644 --- a/service-providers/network-requester/src/error.rs +++ b/service-providers/network-requester/src/error.rs @@ -29,9 +29,6 @@ pub enum NetworkRequesterError { #[error("Failed to validate the loaded config")] ConfigValidationFailure, - #[error("failed local version check, client and config mismatch")] - FailedLocalVersionCheck, - #[error("failed to setup mixnet client: {source}")] FailedToSetupMixnetClient { source: nym_sdk::Error }, diff --git a/tools/internal/testnet-manager/src/manager/local_client.rs b/tools/internal/testnet-manager/src/manager/local_client.rs index fac5e6828a6..7e43756b562 100644 --- a/tools/internal/testnet-manager/src/manager/local_client.rs +++ b/tools/internal/testnet-manager/src/manager/local_client.rs @@ -96,7 +96,7 @@ impl NetworkManager { let wait_fut = async { let inner_fut = async { loop { - let mut nodes = match api_client.get_all_basic_nodes(None).await { + let mut nodes = match api_client.get_all_basic_nodes().await { Ok(nodes) => nodes, Err(err) => { ctx.println(format!( From 5454b3602227b8532264800c1f240bbacec61962 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= <jedrzej.stuczynski@gmail.com> Date: Mon, 9 Dec 2024 14:33:34 +0000 Subject: [PATCH 16/18] Further config score adjustments (#5225) * wip * changed minor/patch weights and introduced full release chain history for more accurate calculations * clippy * updated contract schema * added nym-api endpoint for current rewarded set nodes * added nym-api endpoint for internal config score data * guard mixnet contract against decreasing semver * fixed config score calculation if there are skipped versions --- .../contract_traits/mixnet_query_client.rs | 36 +- .../mixnet-contract/src/config_score.rs | 673 ++++++++++++++++++ .../mixnet-contract/src/error.rs | 3 + .../mixnet-contract/src/events.rs | 10 +- .../mixnet-contract/src/lib.rs | 2 + .../mixnet-contract/src/msg.rs | 15 + .../mixnet-contract/src/types.rs | 154 +--- .../mixnet/schema/nym-mixnet-contract.json | 304 +++++++- contracts/mixnet/schema/raw/execute.json | 8 +- contracts/mixnet/schema/raw/instantiate.json | 8 +- contracts/mixnet/schema/raw/migrate.json | 8 +- contracts/mixnet/schema/raw/query.json | 48 ++ ...ponse_to_get_current_nym_node_version.json | 105 +++ ...ponse_to_get_nym_node_version_history.json | 113 +++ .../schema/raw/response_to_get_state.json | 7 +- .../raw/response_to_get_state_params.json | 7 +- contracts/mixnet/src/constants.rs | 3 + contracts/mixnet/src/contract.rs | 25 +- .../src/mixnet_contract_settings/queries.rs | 40 +- .../src/mixnet_contract_settings/storage.rs | 107 ++- .../mixnet_contract_settings/transactions.rs | 30 +- contracts/mixnet/src/queued_migrations.rs | 13 +- nym-api/nym-api-requests/src/models.rs | 132 ++++ .../src/node_status_api/cache/node_sets.rs | 57 +- .../src/node_status_api/cache/refresher.rs | 6 +- nym-api/src/node_status_api/handlers/mod.rs | 31 +- nym-api/src/nym_contract_cache/cache/data.rs | 40 +- nym-api/src/nym_contract_cache/cache/mod.rs | 25 +- .../src/nym_contract_cache/cache/refresher.rs | 21 +- nym-api/src/nym_nodes/handlers/mod.rs | 26 +- nym-api/src/support/caching/cache.rs | 9 + nym-api/src/support/http/router.rs | 4 +- nym-api/src/support/nyxd/mod.rs | 10 +- 33 files changed, 1761 insertions(+), 319 deletions(-) create mode 100644 common/cosmwasm-smart-contracts/mixnet-contract/src/config_score.rs create mode 100644 contracts/mixnet/schema/raw/response_to_get_current_nym_node_version.json create mode 100644 contracts/mixnet/schema/raw/response_to_get_nym_node_version_history.json diff --git a/common/client-libs/validator-client/src/nyxd/contract_traits/mixnet_query_client.rs b/common/client-libs/validator-client/src/nyxd/contract_traits/mixnet_query_client.rs index 433112e1575..2e1095b3b8b 100644 --- a/common/client-libs/validator-client/src/nyxd/contract_traits/mixnet_query_client.rs +++ b/common/client-libs/validator-client/src/nyxd/contract_traits/mixnet_query_client.rs @@ -26,10 +26,11 @@ use nym_mixnet_contract_common::{ reward_params::{Performance, RewardingParams}, rewarding::{EstimatedCurrentEpochRewardResponse, PendingRewardResponse}, ContractBuildInformation, ContractState, ContractStateParams, CurrentIntervalResponse, - Delegation, EpochEventId, EpochStatus, GatewayBond, GatewayBondResponse, - GatewayOwnershipResponse, IdentityKey, IdentityKeyRef, IntervalEventId, MixNodeBond, - MixNodeDetails, MixOwnershipResponse, MixnodeDetailsByIdentityResponse, MixnodeDetailsResponse, - NodeId, NumberOfPendingEventsResponse, NymNodeBond, NymNodeDetails, + CurrentNymNodeVersionResponse, Delegation, EpochEventId, EpochStatus, GatewayBond, + GatewayBondResponse, GatewayOwnershipResponse, HistoricalNymNodeVersionEntry, IdentityKey, + IdentityKeyRef, IntervalEventId, MixNodeBond, MixNodeDetails, MixOwnershipResponse, + MixnodeDetailsByIdentityResponse, MixnodeDetailsResponse, NodeId, + NumberOfPendingEventsResponse, NymNodeBond, NymNodeDetails, NymNodeVersionHistoryResponse, PagedAllDelegationsResponse, PagedDelegatorDelegationsResponse, PagedGatewayResponse, PagedMixnodeBondsResponse, PagedNodeDelegationsResponse, PendingEpochEvent, PendingEpochEventResponse, PendingEpochEventsResponse, PendingIntervalEvent, @@ -71,6 +72,22 @@ pub trait MixnetQueryClient { .await } + async fn get_nym_node_version_history_paged( + &self, + start_after: Option<u32>, + limit: Option<u32>, + ) -> Result<NymNodeVersionHistoryResponse, NyxdError> { + self.query_mixnet_contract(MixnetQueryMsg::GetNymNodeVersionHistory { limit, start_after }) + .await + } + + async fn get_current_nym_node_version( + &self, + ) -> Result<CurrentNymNodeVersionResponse, NyxdError> { + self.query_mixnet_contract(MixnetQueryMsg::GetCurrentNymNodeVersion {}) + .await + } + async fn get_mixnet_contract_state(&self) -> Result<ContractState, NyxdError> { self.query_mixnet_contract(MixnetQueryMsg::GetState {}) .await @@ -638,6 +655,12 @@ pub trait PagedMixnetQueryClient: MixnetQueryClient { ) -> Result<Vec<PendingIntervalEvent>, NyxdError> { collect_paged!(self, get_pending_interval_events_paged, events) } + + async fn get_full_nym_node_version_history( + &self, + ) -> Result<Vec<HistoricalNymNodeVersionEntry>, NyxdError> { + collect_paged!(self, get_nym_node_version_history_paged, history) + } } #[async_trait] @@ -724,6 +747,7 @@ where mod tests { use super::*; use crate::nyxd::contract_traits::tests::IgnoreValue; + use nym_mixnet_contract_common::QueryMsg; // it's enough that this compiles and clippy is happy about it #[allow(dead_code)] @@ -924,6 +948,10 @@ mod tests { MixnetQueryMsg::GetRewardedSetMetadata {} => { client.get_rewarded_set_metadata().ignore() } + QueryMsg::GetCurrentNymNodeVersion {} => client.get_current_nym_node_version().ignore(), + QueryMsg::GetNymNodeVersionHistory { limit, start_after } => client + .get_nym_node_version_history_paged(start_after, limit) + .ignore(), } } } diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/config_score.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/config_score.rs new file mode 100644 index 00000000000..a5d8ed44f44 --- /dev/null +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/config_score.rs @@ -0,0 +1,673 @@ +// Copyright 2024 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: Apache-2.0 + +use cosmwasm_schema::cw_serde; +use cosmwasm_std::Decimal; +use std::cmp::Ordering; +use std::ops::{Add, Sub}; + +#[cw_serde] +pub struct HistoricalNymNodeVersion { + /// Version of the nym node that is going to be used for determining the version score of a node. + /// note: value stored here is pre-validated `semver::Version` + pub semver: String, + + /// Block height of when this version has been added to the contract + pub introduced_at_height: u64, + + /// The absolute version difference as compared against the first version introduced into the contract. + pub difference_since_genesis: TotalVersionDifference, +} + +impl HistoricalNymNodeVersion { + pub fn genesis(semver: String, height: u64) -> HistoricalNymNodeVersion { + HistoricalNymNodeVersion { + semver, + introduced_at_height: height, + difference_since_genesis: Default::default(), + } + } + + // SAFETY: the value stored in the contract is always valid + // if you manually construct that struct with invalid value, it's on you. + #[allow(clippy::unwrap_used)] + pub fn semver_unchecked(&self) -> semver::Version { + self.semver.parse().unwrap() + } + + /// Return [`TotalVersionDifference`] for a new release version that is going to be pushed right after this one + /// this function cannot be called against 2 arbitrary versions + #[inline] + pub fn cumulative_difference_since_genesis( + &self, + new_version: &semver::Version, + ) -> TotalVersionDifference { + let self_semver = self.semver_unchecked(); + let mut new_absolute = self.difference_since_genesis; + if new_version.major > self_semver.major { + new_absolute.major += (new_version.major - self_semver.major) as u32 + } else if new_version.minor > self_semver.minor { + new_absolute.minor += (new_version.minor - self_semver.minor) as u32 + } else if new_version.patch > self_semver.patch { + new_absolute.patch += (new_version.patch - self_semver.patch) as u32 + } else if new_version.pre != self_semver.pre { + new_absolute.prerelease += 1 + } + new_absolute + } + + pub fn relative_difference(&self, other: &Self) -> TotalVersionDifference { + if self.difference_since_genesis > other.difference_since_genesis { + self.difference_since_genesis - other.difference_since_genesis + } else { + other.difference_since_genesis - self.difference_since_genesis + } + } + + pub fn difference_against_legacy( + &self, + legacy_version: &semver::Version, + ) -> TotalVersionDifference { + let current = self.semver_unchecked(); + let major_diff = (current.major as i64 - legacy_version.major as i64).unsigned_abs() as u32; + let minor_diff = (current.minor as i64 - legacy_version.minor as i64).unsigned_abs() as u32; + let patch_diff = (current.patch as i64 - legacy_version.patch as i64).unsigned_abs() as u32; + let prerelease_diff = if current.pre == legacy_version.pre { + 0 + } else { + 1 + }; + + let mut diff = TotalVersionDifference::default(); + // if there's a major increase, ignore minor and patch and treat it as 0 + if major_diff != 0 { + diff.major += major_diff; + return diff; + } + + // if there's a minor increase, ignore patch and treat is as 0 + if minor_diff != 0 { + diff.minor += minor_diff; + return diff; + } + + diff.patch = patch_diff; + diff.prerelease = prerelease_diff; + diff + } +} + +#[cw_serde] +#[derive(Default, Copy, PartialOrd, Ord, Eq)] +pub struct TotalVersionDifference { + pub major: u32, + pub minor: u32, + pub patch: u32, + pub prerelease: u32, +} + +impl Add for TotalVersionDifference { + type Output = TotalVersionDifference; + fn add(self, rhs: TotalVersionDifference) -> Self::Output { + TotalVersionDifference { + major: self.major.add(rhs.major), + minor: self.minor.add(rhs.minor), + patch: self.patch.add(rhs.patch), + prerelease: self.prerelease.add(rhs.prerelease), + } + } +} + +impl Sub for TotalVersionDifference { + type Output = TotalVersionDifference; + fn sub(self, rhs: TotalVersionDifference) -> Self::Output { + TotalVersionDifference { + major: self.major.saturating_sub(rhs.major), + minor: self.minor.saturating_sub(rhs.minor), + patch: self.patch.saturating_sub(rhs.patch), + prerelease: self.prerelease.saturating_sub(rhs.prerelease), + } + } +} + +#[cw_serde] +pub struct HistoricalNymNodeVersionEntry { + /// The unique, ordered, id of this particular entry + pub id: u32, + + /// Data associated with this particular version + pub version_information: HistoricalNymNodeVersion, +} + +impl From<(u32, HistoricalNymNodeVersion)> for HistoricalNymNodeVersionEntry { + fn from((id, version_information): (u32, HistoricalNymNodeVersion)) -> Self { + HistoricalNymNodeVersionEntry { + id, + version_information, + } + } +} + +impl PartialOrd for HistoricalNymNodeVersionEntry { + fn partial_cmp(&self, other: &Self) -> Option<Ordering> { + // we only care about id for the purposes of ordering as they should have unique data + self.id.partial_cmp(&other.id) + } +} + +#[cw_serde] +pub struct NymNodeVersionHistoryResponse { + pub history: Vec<HistoricalNymNodeVersionEntry>, + + /// Field indicating paging information for the following queries if the caller wishes to get further entries. + pub start_next_after: Option<u32>, +} + +#[cw_serde] +pub struct CurrentNymNodeVersionResponse { + pub version: Option<HistoricalNymNodeVersionEntry>, +} + +#[cw_serde] +pub struct ConfigScoreParams { + /// Defines weights for calculating numbers of versions behind the current release. + pub version_weights: OutdatedVersionWeights, + + /// Defines the parameters of the formula for calculating the version score + pub version_score_formula_params: VersionScoreFormulaParams, +} + +/// Defines weights for calculating numbers of versions behind the current release. +#[cw_serde] +#[derive(Copy)] +pub struct OutdatedVersionWeights { + pub major: u32, + pub minor: u32, + pub patch: u32, + pub prerelease: u32, +} + +fn is_one_semver_difference(this: &semver::Version, other: &semver::Version) -> bool { + let major_diff = (this.major as i64 - other.major as i64).unsigned_abs() as u32; + let minor_diff = (this.minor as i64 - other.minor as i64).unsigned_abs() as u32; + let patch_diff = (this.patch as i64 - other.patch as i64).unsigned_abs() as u32; + let prerelease_diff = if this.pre == other.pre { 0 } else { 1 }; + + if major_diff == 1 { + return true; + } + + if major_diff == 0 && minor_diff == 1 { + return true; + } + + if major_diff == 0 && minor_diff == 0 && patch_diff == 1 { + return true; + } + + prerelease_diff == 1 +} + +impl OutdatedVersionWeights { + pub fn difference_to_versions_behind_factor(&self, diff: TotalVersionDifference) -> u32 { + diff.major * self.major + + diff.minor * self.minor + + diff.patch * self.patch + + diff.prerelease * self.prerelease + } + + // INVARIANT: release chain is sorted + // do NOT call this method directly from inside the contract. it's too inefficient + // it relies on some external caching. + pub fn versions_behind_factor( + &self, + node_version: &semver::Version, + release_chain: &[HistoricalNymNodeVersionEntry], + ) -> u32 { + let Some(latest) = release_chain.last() else { + return 0; + }; + + let latest_semver = latest.version_information.semver_unchecked(); + + // if you're more recent than the latest, you get the benefit of the doubt, the release might have not yet been commited to the chain + // but only if you're only a single semver ahead, otherwise you get penalty equivalent of being major version behind for cheating + if node_version > &latest_semver { + return if is_one_semver_difference(node_version, &latest_semver) { + 0 + } else { + self.major + }; + } + + // find your position in the release chain, if we fail, we assume that the node comes from before the changes were introduced + // in which case we simply calculate the absolute difference between the genesis entry and add up the total difference + let version_diff = match release_chain + .iter() + .rfind(|h| &h.version_information.semver_unchecked() <= node_version) + { + Some(h) => { + // first chain entry that is smaller (or equal) to the provided node version + // now, calculate the difference to the genesis version and ultimately against the current head + let diff_since_genesis = if h.version_information.semver == node_version.to_string() + { + h.version_information.difference_since_genesis + } else { + h.version_information + .cumulative_difference_since_genesis(node_version) + }; + latest.version_information.difference_since_genesis - diff_since_genesis + } + None => { + // SAFETY: since we managed to get 'last' entry, it means the release chain is not empty, + // so we must be able to obtain the first entry + #[allow(clippy::unwrap_used)] + let genesis = release_chain.first().unwrap(); + + let difference_from_genesis = genesis + .version_information + .difference_against_legacy(node_version); + difference_from_genesis + latest.version_information.difference_since_genesis + } + }; + + self.difference_to_versions_behind_factor(version_diff) + } +} + +impl Default for OutdatedVersionWeights { + fn default() -> Self { + OutdatedVersionWeights { + major: 100, + minor: 10, + patch: 1, + prerelease: 1, + } + } +} + +/// Given the formula of version_score = penalty ^ (versions_behind_factor ^ penalty_scaling) +/// define the relevant parameters +#[cw_serde] +#[derive(Copy)] +pub struct VersionScoreFormulaParams { + pub penalty: Decimal, + pub penalty_scaling: Decimal, +} + +impl Default for VersionScoreFormulaParams { + fn default() -> Self { + #[allow(clippy::unwrap_used)] + VersionScoreFormulaParams { + penalty: "0.995".parse().unwrap(), + penalty_scaling: "1.65".parse().unwrap(), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::ops::Deref; + + // simple wrapper for tests + struct ReleaseChain { + inner: Vec<HistoricalNymNodeVersionEntry>, + } + + impl Deref for ReleaseChain { + type Target = [HistoricalNymNodeVersionEntry]; + fn deref(&self) -> &Self::Target { + self.inner.deref() + } + } + + impl ReleaseChain { + fn new(initial: &str) -> Self { + ReleaseChain { + inner: vec![HistoricalNymNodeVersionEntry { + id: 0, + version_information: HistoricalNymNodeVersion { + semver: initial.to_string(), + introduced_at_height: 123, + difference_since_genesis: TotalVersionDifference::default(), + }, + }], + } + } + + fn with_release(mut self, raw: &str) -> Self { + self.push_new(raw); + self + } + + fn push_new(&mut self, raw: &str) { + let latest = self.inner.last().unwrap(); + let new_version: semver::Version = raw.parse().unwrap(); + + let new_absolute = latest + .version_information + .cumulative_difference_since_genesis(&new_version); + + self.inner.push(HistoricalNymNodeVersionEntry { + id: latest.id + 1, + version_information: HistoricalNymNodeVersion { + semver: new_version.to_string(), + introduced_at_height: latest.version_information.introduced_at_height + 1, + difference_since_genesis: new_absolute, + }, + }) + } + } + + #[test] + fn versions_behind_factor() { + // helper to compact the parsing + fn s(raw: &str) -> semver::Version { + raw.parse().unwrap() + } + + let weights = OutdatedVersionWeights::default(); + + // no releases: + let res = weights.versions_behind_factor(&s("1.1.13"), &[]); + assert_eq!(0, res); + + // ############################### + // single released version (1.1.13) + // ############################### + let mut release_chain = ReleaseChain::new("1.1.13"); + + // "legacy" versions + let res = weights.versions_behind_factor(&s("1.0.12"), &release_chain); + assert_eq!(10, res); + let res = weights.versions_behind_factor(&s("1.0.4"), &release_chain); + assert_eq!(10, res); + let res = weights.versions_behind_factor(&s("1.0.1"), &release_chain); + assert_eq!(10, res); + let res = weights.versions_behind_factor(&s("0.1.12"), &release_chain); + assert_eq!(100, res); + + let res = weights.versions_behind_factor(&s("1.1.12"), &release_chain); + assert_eq!(1, res); + let res = weights.versions_behind_factor(&s("1.1.11"), &release_chain); + assert_eq!(2, res); + let res = weights.versions_behind_factor(&s("1.1.9"), &release_chain); + assert_eq!(4, res); + + // current version + let res = weights.versions_behind_factor(&s("1.1.13"), &release_chain); + assert_eq!(0, res); + + // "ahead" versions + let res = weights.versions_behind_factor(&s("1.1.14"), &release_chain); + assert_eq!(0, res); + let res = weights.versions_behind_factor(&s("1.2.0"), &release_chain); + assert_eq!(0, res); + let res = weights.versions_behind_factor(&s("2.0.0"), &release_chain); + assert_eq!(0, res); + + // cheating ahead: + let res = weights.versions_behind_factor(&s("1.1.15"), &release_chain); + assert_eq!(100, res); + let res = weights.versions_behind_factor(&s("1.3.0"), &release_chain); + assert_eq!(100, res); + let res = weights.versions_behind_factor(&s("3.0.0"), &release_chain); + assert_eq!(100, res); + + // ############################### + // small patch release chain (1.1.13 => 1.1.14 => 1.1.15 => 1.1.16) + // ############################### + release_chain.push_new("1.1.14"); + release_chain.push_new("1.1.15"); + release_chain.push_new("1.1.16"); + + // "legacy" versions + let res = weights.versions_behind_factor(&s("1.0.12"), &release_chain); + assert_eq!(13, res); + let res = weights.versions_behind_factor(&s("1.0.4"), &release_chain); + assert_eq!(13, res); + let res = weights.versions_behind_factor(&s("1.0.1"), &release_chain); + assert_eq!(13, res); + let res = weights.versions_behind_factor(&s("0.1.12"), &release_chain); + assert_eq!(103, res); + + let res = weights.versions_behind_factor(&s("1.1.12"), &release_chain); + assert_eq!(4, res); + let res = weights.versions_behind_factor(&s("1.1.11"), &release_chain); + assert_eq!(5, res); + let res = weights.versions_behind_factor(&s("1.1.9"), &release_chain); + assert_eq!(7, res); + + // current version + let res = weights.versions_behind_factor(&s("1.1.16"), &release_chain); + assert_eq!(0, res); + + // present in the chain + let res = weights.versions_behind_factor(&s("1.1.15"), &release_chain); + assert_eq!(1, res); + let res = weights.versions_behind_factor(&s("1.1.14"), &release_chain); + assert_eq!(2, res); + let res = weights.versions_behind_factor(&s("1.1.13"), &release_chain); + assert_eq!(3, res); + + // "ahead" versions + let res = weights.versions_behind_factor(&s("1.1.17"), &release_chain); + assert_eq!(0, res); + let res = weights.versions_behind_factor(&s("1.2.0"), &release_chain); + assert_eq!(0, res); + let res = weights.versions_behind_factor(&s("2.0.0"), &release_chain); + assert_eq!(0, res); + + // cheating ahead: + let res = weights.versions_behind_factor(&s("1.1.18"), &release_chain); + assert_eq!(100, res); + let res = weights.versions_behind_factor(&s("1.3.0"), &release_chain); + assert_eq!(100, res); + let res = weights.versions_behind_factor(&s("3.0.0"), &release_chain); + assert_eq!(100, res); + + // ############################### + // small minor release chain (1.2.0 => 1.3.0 => 1.4.0) + // ############################### + let release_chain = ReleaseChain::new("1.2.0") + .with_release("1.3.0") + .with_release("1.4.0"); + + // "legacy" versions + let res = weights.versions_behind_factor(&s("1.0.12"), &release_chain); + assert_eq!(40, res); + let res = weights.versions_behind_factor(&s("1.0.4"), &release_chain); + assert_eq!(40, res); + let res = weights.versions_behind_factor(&s("1.0.1"), &release_chain); + assert_eq!(40, res); + let res = weights.versions_behind_factor(&s("0.1.12"), &release_chain); + assert_eq!(120, res); + + let res = weights.versions_behind_factor(&s("1.1.12"), &release_chain); + assert_eq!(30, res); + let res = weights.versions_behind_factor(&s("1.1.11"), &release_chain); + assert_eq!(30, res); + let res = weights.versions_behind_factor(&s("1.1.9"), &release_chain); + assert_eq!(30, res); + + // current version + let res = weights.versions_behind_factor(&s("1.4.0"), &release_chain); + assert_eq!(0, res); + + // present in the chain + let res = weights.versions_behind_factor(&s("1.2.0"), &release_chain); + assert_eq!(20, res); + let res = weights.versions_behind_factor(&s("1.3.0"), &release_chain); + assert_eq!(10, res); + + // weird in between + let res = weights.versions_behind_factor(&s("1.2.1"), &release_chain); + assert_eq!(20, res); + let res = weights.versions_behind_factor(&s("1.3.3"), &release_chain); + assert_eq!(10, res); + + // "ahead" versions + let res = weights.versions_behind_factor(&s("1.4.1"), &release_chain); + assert_eq!(0, res); + let res = weights.versions_behind_factor(&s("1.5.0"), &release_chain); + assert_eq!(0, res); + let res = weights.versions_behind_factor(&s("2.0.0"), &release_chain); + assert_eq!(0, res); + + // cheating ahead: + let res = weights.versions_behind_factor(&s("1.4.2"), &release_chain); + assert_eq!(100, res); + let res = weights.versions_behind_factor(&s("1.6.0"), &release_chain); + assert_eq!(100, res); + let res = weights.versions_behind_factor(&s("3.0.0"), &release_chain); + assert_eq!(100, res); + + // ############################### + // mixed release chain (1.1.13 => 1.2.0 => 1.2.1 => 1.3.0 => 1.3.1 => 1.3.2 => 1.4.0) + // ############################### + let release_chain = ReleaseChain::new("1.1.13") + .with_release("1.2.0") + .with_release("1.2.1") + .with_release("1.3.0") + .with_release("1.3.1-importantpre") + .with_release("1.3.1") + .with_release("1.3.2") + .with_release("1.4.0"); + + // "legacy" versions + let res = weights.versions_behind_factor(&s("1.0.12"), &release_chain); + assert_eq!(44, res); + let res = weights.versions_behind_factor(&s("1.0.4"), &release_chain); + assert_eq!(44, res); + let res = weights.versions_behind_factor(&s("1.0.1"), &release_chain); + assert_eq!(44, res); + let res = weights.versions_behind_factor(&s("0.1.12"), &release_chain); + assert_eq!(134, res); + + let res = weights.versions_behind_factor(&s("1.1.12"), &release_chain); + assert_eq!(35, res); + let res = weights.versions_behind_factor(&s("1.1.11"), &release_chain); + assert_eq!(36, res); + let res = weights.versions_behind_factor(&s("1.1.9"), &release_chain); + assert_eq!(38, res); + + // current version + let res = weights.versions_behind_factor(&s("1.4.0"), &release_chain); + assert_eq!(0, res); + + // present in the chain + let res = weights.versions_behind_factor(&s("1.1.13"), &release_chain); + assert_eq!(34, res); + let res = weights.versions_behind_factor(&s("1.2.0"), &release_chain); + assert_eq!(24, res); + let res = weights.versions_behind_factor(&s("1.2.1"), &release_chain); + assert_eq!(23, res); + let res = weights.versions_behind_factor(&s("1.3.0"), &release_chain); + assert_eq!(13, res); + let res = weights.versions_behind_factor(&s("1.3.1-importantpre"), &release_chain); + assert_eq!(12, res); + let res = weights.versions_behind_factor(&s("1.3.1"), &release_chain); + assert_eq!(11, res); + let res = weights.versions_behind_factor(&s("1.3.2"), &release_chain); + assert_eq!(10, res); + + // weird in between + let res = weights.versions_behind_factor(&s("1.2.3"), &release_chain); + assert_eq!(21, res); + let res = weights.versions_behind_factor(&s("1.3.69"), &release_chain); + assert_eq!(10, res); + + // "ahead" versions + let res = weights.versions_behind_factor(&s("1.4.1"), &release_chain); + assert_eq!(0, res); + let res = weights.versions_behind_factor(&s("1.5.0"), &release_chain); + assert_eq!(0, res); + let res = weights.versions_behind_factor(&s("2.0.0"), &release_chain); + assert_eq!(0, res); + + // cheating ahead: + let res = weights.versions_behind_factor(&s("1.4.2"), &release_chain); + assert_eq!(100, res); + let res = weights.versions_behind_factor(&s("1.6.0"), &release_chain); + assert_eq!(100, res); + let res = weights.versions_behind_factor(&s("3.0.0"), &release_chain); + assert_eq!(100, res); + + // ############################### + // skipped patch chain (1.1.13 => 1.2.0 => 1.2.1 => 1.2.4 => [1.3.0]) + // ############################### + let mut release_chain = ReleaseChain::new("1.1.13") + .with_release("1.2.0") + .with_release("1.2.1") + .with_release("1.2.4"); + + // current + let res = weights.versions_behind_factor(&s("1.2.4"), &release_chain); + assert_eq!(0, res); + + // on 'skipped' version + let res = weights.versions_behind_factor(&s("1.2.2"), &release_chain); + assert_eq!(2, res); + + // on version before the skip + let res = weights.versions_behind_factor(&s("1.2.1"), &release_chain); + assert_eq!(3, res); + + release_chain.push_new("1.3.0"); + // current + let res = weights.versions_behind_factor(&s("1.3.0"), &release_chain); + assert_eq!(0, res); + + // on 'skipped' version + let res = weights.versions_behind_factor(&s("1.2.2"), &release_chain); + assert_eq!(12, res); + + // on version before the skip + let res = weights.versions_behind_factor(&s("1.2.1"), &release_chain); + assert_eq!(13, res); + + // ############################### + // skipped minor chain (1.1.13 => 1.2.0 => 1.2.1 => 1.4.0 => [1.5.0]) + // ############################### + let mut release_chain = ReleaseChain::new("1.1.13") + .with_release("1.2.0") + .with_release("1.2.1") + .with_release("1.4.0"); + + // current + let res = weights.versions_behind_factor(&s("1.4.0"), &release_chain); + assert_eq!(0, res); + + // on 'skipped' version + let res = weights.versions_behind_factor(&s("1.3.0"), &release_chain); + assert_eq!(10, res); + + // on version before the skip + let res = weights.versions_behind_factor(&s("1.2.1"), &release_chain); + assert_eq!(20, res); + + let res = weights.versions_behind_factor(&s("1.2.0"), &release_chain); + assert_eq!(21, res); + + release_chain.push_new("1.5.0"); + + // current + let res = weights.versions_behind_factor(&s("1.5.0"), &release_chain); + assert_eq!(0, res); + + let res = weights.versions_behind_factor(&s("1.4.0"), &release_chain); + assert_eq!(10, res); + + // on 'skipped' version + let res = weights.versions_behind_factor(&s("1.3.0"), &release_chain); + assert_eq!(20, res); + + // on version before the skip + let res = weights.versions_behind_factor(&s("1.2.1"), &release_chain); + assert_eq!(30, res); + + let res = weights.versions_behind_factor(&s("1.2.0"), &release_chain); + assert_eq!(31, res); + } +} diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/error.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/error.rs index c34686151d2..e8349d4eb0e 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/error.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/error.rs @@ -275,6 +275,9 @@ pub enum MixnetContractError { #[error("the provided nym-node version is not a valid semver. got: {provided}")] InvalidNymNodeSemver { provided: String }, + + #[error("the provided nym-node version is not greater than the current one. got: {provided}. current: {current}")] + NonIncreasingSemver { provided: String, current: String }, } impl MixnetContractError { diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/events.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/events.rs index e45f7a38fef..b29fba4bd06 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/events.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/events.rs @@ -141,6 +141,7 @@ pub const NEW_INTERVAL_OPERATING_COST_RANGE_KEY: &str = "new_interval_operating_ pub const NEW_VERSION_WEIGHTS_RANGE_KEY: &str = "new_version_weights_range"; pub const NEW_VERSION_SCORE_FORMULA_PARAMS_KEY: &str = "new_version_score_formula_params"; pub const NYM_NODE_CURRENT_SEMVER_KEY: &str = "new_current_semver"; +pub const NYM_NODE_CURRENT_SEMVER_ID_KEY: &str = "new_current_semver_id"; pub const OLD_REWARDING_VALIDATOR_ADDRESS_KEY: &str = "old_rewarding_validator_address"; pub const NEW_REWARDING_VALIDATOR_ADDRESS_KEY: &str = "new_rewarding_validator_address"; @@ -481,12 +482,6 @@ pub fn new_settings_update_event(update: &ContractStateParamsUpdate) -> Event { // check for config score params updates if let Some(config_score_update) = &update.config_score_params { - if let Some(current_nym_node_semver) = &config_score_update.current_nym_node_semver { - event.attributes.push(attr( - NYM_NODE_CURRENT_SEMVER_KEY, - current_nym_node_semver.to_string(), - )) - } if let Some(version_weights) = &config_score_update.version_weights { event.attributes.push(attr( NEW_VERSION_WEIGHTS_RANGE_KEY, @@ -506,9 +501,10 @@ pub fn new_settings_update_event(update: &ContractStateParamsUpdate) -> Event { event } -pub fn new_update_nym_node_semver_event(new_version: &str) -> Event { +pub fn new_update_nym_node_semver_event(new_version: &str, new_id: u32) -> Event { Event::new(MixnetEventType::NymNodeSemverUpdate) .add_attribute(NYM_NODE_CURRENT_SEMVER_KEY, new_version) + .add_attribute(NYM_NODE_CURRENT_SEMVER_ID_KEY, new_id.to_string()) } pub fn new_not_found_node_operator_rewarding_event(interval: Interval, node_id: NodeId) -> Event { diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/lib.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/lib.rs index fca9f6bfcf8..4f9d7a088fb 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/lib.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/lib.rs @@ -5,6 +5,7 @@ #![warn(clippy::unwrap_used)] #![warn(clippy::todo)] +mod config_score; pub mod constants; pub mod delegation; pub mod error; @@ -21,6 +22,7 @@ pub mod rewarding; pub mod signing_types; pub mod types; +pub use config_score::*; pub use constants::*; pub use contracts_common::types::*; pub use cosmwasm_std::{Addr, Coin, Decimal, Fraction}; diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs index 529bf53cc2f..214e7e95dfa 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs @@ -25,6 +25,7 @@ use std::time::Duration; #[cfg(feature = "schema")] use crate::{ + config_score::{CurrentNymNodeVersionResponse, NymNodeVersionHistoryResponse}, delegation::{ NodeDelegationResponse, PagedAllDelegationsResponse, PagedDelegatorDelegationsResponse, PagedNodeDelegationsResponse, @@ -423,6 +424,20 @@ pub enum QueryMsg { #[cfg_attr(feature = "schema", returns(ContractState))] GetState {}, + /// Get the current expected version of a Nym Node. + #[cfg_attr(feature = "schema", returns(CurrentNymNodeVersionResponse))] + GetCurrentNymNodeVersion {}, + + /// Get the version history of Nym Node. + #[cfg_attr(feature = "schema", returns(NymNodeVersionHistoryResponse))] + GetNymNodeVersionHistory { + /// Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default. + limit: Option<u32>, + + /// Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response. + start_after: Option<u32>, + }, + /// Gets the current parameters used for reward calculation. #[cfg_attr(feature = "schema", returns(RewardingParams))] GetRewardingParams {}, diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/types.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/types.rs index 2a2cd296370..eb5f972e4b5 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/types.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/types.rs @@ -1,11 +1,12 @@ // Copyright 2021-2023 - Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: Apache-2.0 +use crate::config_score::{ConfigScoreParams, OutdatedVersionWeights, VersionScoreFormulaParams}; use crate::nym_node::Role; use contracts_common::Percent; use cosmwasm_schema::cw_serde; +use cosmwasm_std::Coin; use cosmwasm_std::{Addr, Uint128}; -use cosmwasm_std::{Coin, Decimal}; use std::fmt::{Display, Formatter}; // type aliases for better reasoning about available data @@ -221,163 +222,14 @@ impl OperatorsParamsUpdate { } } -#[cw_serde] -pub struct ConfigScoreParams { - /// Current version of the nym node that is going to be used for determining the version score of a node. - /// note: value stored here is pre-validated `semver::Version` - pub current_nym_node_semver: String, - - /// Defines weights for calculating numbers of versions behind the current release. - pub version_weights: OutdatedVersionWeights, - - /// Defines the parameters of the formula for calculating the version score - pub version_score_formula_params: VersionScoreFormulaParams, -} - -impl ConfigScoreParams { - // SAFETY: the value stored in the contract is always valid - #[allow(clippy::unwrap_used)] - pub fn unchecked_nym_node_version(&self) -> semver::Version { - self.current_nym_node_semver.parse().unwrap() - } - - pub fn versions_behind(&self, node_semver: &semver::Version) -> u32 { - let expected = self.unchecked_nym_node_version(); - - let major_diff = (node_semver.major as i64 - expected.major as i64).unsigned_abs() as u32; - let minor_diff = (node_semver.minor as i64 - expected.minor as i64).unsigned_abs() as u32; - let patch_diff = (node_semver.patch as i64 - expected.patch as i64).unsigned_abs() as u32; - let prerelease_diff = if node_semver.pre == expected.pre { - 0 - } else { - 1 - }; - - // if you're a major version behind, ignore minor and patch and treat it as 0 - if major_diff != 0 { - return major_diff * self.version_weights.major - + expected.minor as u32 * self.version_weights.minor - + expected.patch as u32 * self.version_weights.patch - + prerelease_diff * self.version_weights.prerelease; - } - - // if you're minor version behind, ignore patch and treat is as 0 - if minor_diff != 0 { - return minor_diff * self.version_weights.minor - + expected.patch as u32 * self.version_weights.patch - + prerelease_diff * self.version_weights.prerelease; - } - - patch_diff * self.version_weights.patch + prerelease_diff * self.version_weights.prerelease - } -} - -/// Defines weights for calculating numbers of versions behind the current release. -#[cw_serde] -#[derive(Copy)] -pub struct OutdatedVersionWeights { - pub major: u32, - pub minor: u32, - pub patch: u32, - pub prerelease: u32, -} - -impl Default for OutdatedVersionWeights { - fn default() -> Self { - OutdatedVersionWeights { - major: 100, - minor: 1, - patch: 1, - prerelease: 1, - } - } -} - -/// Given the formula of version_score = penalty ^ (num_versions_behind ^ penalty_scaling) -/// define the relevant parameters -#[cw_serde] -#[derive(Copy)] -pub struct VersionScoreFormulaParams { - pub penalty: Decimal, - pub penalty_scaling: Decimal, -} - -impl Default for VersionScoreFormulaParams { - fn default() -> Self { - #[allow(clippy::unwrap_used)] - VersionScoreFormulaParams { - penalty: "0.8".parse().unwrap(), - penalty_scaling: "2.0".parse().unwrap(), - } - } -} - #[cw_serde] pub struct ConfigScoreParamsUpdate { - pub current_nym_node_semver: Option<String>, pub version_weights: Option<OutdatedVersionWeights>, pub version_score_formula_params: Option<VersionScoreFormulaParams>, } impl ConfigScoreParamsUpdate { pub fn contains_updates(&self) -> bool { - self.current_nym_node_semver.is_some() - || self.version_weights.is_some() - || self.version_score_formula_params.is_some() - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn versions_behind() { - let weights = OutdatedVersionWeights { - major: 100, - minor: 1, - patch: 1, - prerelease: 1, - }; - let c = ConfigScoreParams { - current_nym_node_semver: "2.2.3".to_string(), - version_weights: weights, - version_score_formula_params: Default::default(), - }; - - // 1 patch behind - assert_eq!(1, c.versions_behind(&"2.2.2".parse().unwrap())); - - // 1 minor behind - assert_eq!(4, c.versions_behind(&"2.1.0".parse().unwrap())); - assert_eq!(4, c.versions_behind(&"2.1.15".parse().unwrap())); - - // 2 patch behind - assert_eq!(2, c.versions_behind(&"2.2.1".parse().unwrap())); - - // 2 minor behind - assert_eq!(5, c.versions_behind(&"2.0.0".parse().unwrap())); - assert_eq!(5, c.versions_behind(&"2.0.123".parse().unwrap())); - - // lying about being 1 patch AHEAD (you're still penalised as if behind) - assert_eq!(1, c.versions_behind(&"2.2.4".parse().unwrap())); - - // major behind - assert_eq!(105, c.versions_behind(&"1.0.0".parse().unwrap())); - assert_eq!(105, c.versions_behind(&"1.2.0".parse().unwrap())); - assert_eq!(105, c.versions_behind(&"1.0.123".parse().unwrap())); - - // different prerelease - let c = ConfigScoreParams { - current_nym_node_semver: "1.2.3-important-patch".to_string(), - version_weights: weights, - version_score_formula_params: Default::default(), - }; - assert_eq!(1, c.versions_behind(&"1.2.3".parse().unwrap())); - - // different prerelease + patch - assert_eq!(2, c.versions_behind(&"1.2.2".parse().unwrap())); - - assert_eq!(5, c.versions_behind(&"1.1.0".parse().unwrap())); + self.version_weights.is_some() || self.version_score_formula_params.is_some() } } diff --git a/contracts/mixnet/schema/nym-mixnet-contract.json b/contracts/mixnet/schema/nym-mixnet-contract.json index 306816df999..3549011058f 100644 --- a/contracts/mixnet/schema/nym-mixnet-contract.json +++ b/contracts/mixnet/schema/nym-mixnet-contract.json @@ -60,8 +60,8 @@ }, "version_score_params": { "default": { - "penalty": "0.8", - "penalty_scaling": "2" + "penalty": "0.995", + "penalty_scaling": "1.65" }, "allOf": [ { @@ -72,7 +72,7 @@ "version_score_weights": { "default": { "major": 100, - "minor": 1, + "minor": 10, "patch": 1, "prerelease": 1 }, @@ -261,7 +261,7 @@ "type": "string" }, "VersionScoreFormulaParams": { - "description": "Given the formula of version_score = penalty ^ (num_versions_behind ^ penalty_scaling) define the relevant parameters", + "description": "Given the formula of version_score = penalty ^ (versions_behind_factor ^ penalty_scaling) define the relevant parameters", "type": "object", "required": [ "penalty", @@ -1308,12 +1308,6 @@ "ConfigScoreParamsUpdate": { "type": "object", "properties": { - "current_nym_node_semver": { - "type": [ - "string", - "null" - ] - }, "version_score_formula_params": { "anyOf": [ { @@ -1938,7 +1932,7 @@ "type": "string" }, "VersionScoreFormulaParams": { - "description": "Given the formula of version_score = penalty ^ (num_versions_behind ^ penalty_scaling) define the relevant parameters", + "description": "Given the formula of version_score = penalty ^ (versions_behind_factor ^ penalty_scaling) define the relevant parameters", "type": "object", "required": [ "penalty", @@ -2043,6 +2037,54 @@ }, "additionalProperties": false }, + { + "description": "Get the current expected version of a Nym Node.", + "type": "object", + "required": [ + "get_current_nym_node_version" + ], + "properties": { + "get_current_nym_node_version": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Get the version history of Nym Node.", + "type": "object", + "required": [ + "get_nym_node_version_history" + ], + "properties": { + "get_nym_node_version_history": { + "type": "object", + "properties": { + "limit": { + "description": "Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "description": "Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, { "description": "Gets the current parameters used for reward calculation.", "type": "object", @@ -3443,8 +3485,8 @@ }, "version_score_params": { "default": { - "penalty": "0.8", - "penalty_scaling": "2" + "penalty": "0.995", + "penalty_scaling": "1.65" }, "allOf": [ { @@ -3455,7 +3497,7 @@ "version_score_weights": { "default": { "major": 100, - "minor": 1, + "minor": 10, "patch": 1, "prerelease": 1 }, @@ -3512,7 +3554,7 @@ "additionalProperties": false }, "VersionScoreFormulaParams": { - "description": "Given the formula of version_score = penalty ^ (num_versions_behind ^ penalty_scaling) define the relevant parameters", + "description": "Given the formula of version_score = penalty ^ (versions_behind_factor ^ penalty_scaling) define the relevant parameters", "type": "object", "required": [ "penalty", @@ -4180,6 +4222,111 @@ } } }, + "get_current_nym_node_version": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "CurrentNymNodeVersionResponse", + "type": "object", + "properties": { + "version": { + "anyOf": [ + { + "$ref": "#/definitions/HistoricalNymNodeVersionEntry" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "definitions": { + "HistoricalNymNodeVersion": { + "type": "object", + "required": [ + "difference_since_genesis", + "introduced_at_height", + "semver" + ], + "properties": { + "difference_since_genesis": { + "description": "The absolute version difference as compared against the first version introduced into the contract.", + "allOf": [ + { + "$ref": "#/definitions/TotalVersionDifference" + } + ] + }, + "introduced_at_height": { + "description": "Block height of when this version has been added to the contract", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "semver": { + "description": "Version of the nym node that is going to be used for determining the version score of a node. note: value stored here is pre-validated `semver::Version`", + "type": "string" + } + }, + "additionalProperties": false + }, + "HistoricalNymNodeVersionEntry": { + "type": "object", + "required": [ + "id", + "version_information" + ], + "properties": { + "id": { + "description": "The unique, ordered, id of this particular entry", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "version_information": { + "description": "Data associated with this particular version", + "allOf": [ + { + "$ref": "#/definitions/HistoricalNymNodeVersion" + } + ] + } + }, + "additionalProperties": false + }, + "TotalVersionDifference": { + "type": "object", + "required": [ + "major", + "minor", + "patch", + "prerelease" + ], + "properties": { + "major": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "minor": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "patch": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "prerelease": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + } + }, "get_delegation_details": { "$schema": "http://json-schema.org/draft-07/schema#", "title": "NodeDelegationResponse", @@ -7119,6 +7266,119 @@ } } }, + "get_nym_node_version_history": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "NymNodeVersionHistoryResponse", + "type": "object", + "required": [ + "history" + ], + "properties": { + "history": { + "type": "array", + "items": { + "$ref": "#/definitions/HistoricalNymNodeVersionEntry" + } + }, + "start_next_after": { + "description": "Field indicating paging information for the following queries if the caller wishes to get further entries.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false, + "definitions": { + "HistoricalNymNodeVersion": { + "type": "object", + "required": [ + "difference_since_genesis", + "introduced_at_height", + "semver" + ], + "properties": { + "difference_since_genesis": { + "description": "The absolute version difference as compared against the first version introduced into the contract.", + "allOf": [ + { + "$ref": "#/definitions/TotalVersionDifference" + } + ] + }, + "introduced_at_height": { + "description": "Block height of when this version has been added to the contract", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "semver": { + "description": "Version of the nym node that is going to be used for determining the version score of a node. note: value stored here is pre-validated `semver::Version`", + "type": "string" + } + }, + "additionalProperties": false + }, + "HistoricalNymNodeVersionEntry": { + "type": "object", + "required": [ + "id", + "version_information" + ], + "properties": { + "id": { + "description": "The unique, ordered, id of this particular entry", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "version_information": { + "description": "Data associated with this particular version", + "allOf": [ + { + "$ref": "#/definitions/HistoricalNymNodeVersion" + } + ] + } + }, + "additionalProperties": false + }, + "TotalVersionDifference": { + "type": "object", + "required": [ + "major", + "minor", + "patch", + "prerelease" + ], + "properties": { + "major": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "minor": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "patch": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "prerelease": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + } + }, "get_nym_nodes_detailed_paged": { "$schema": "http://json-schema.org/draft-07/schema#", "title": "PagedNymNodeDetailsResponse", @@ -10554,15 +10814,10 @@ "ConfigScoreParams": { "type": "object", "required": [ - "current_nym_node_semver", "version_score_formula_params", "version_weights" ], "properties": { - "current_nym_node_semver": { - "description": "Current version of the nym node that is going to be used for determining the version score of a node. note: value stored here is pre-validated `semver::Version`", - "type": "string" - }, "version_score_formula_params": { "description": "Defines the parameters of the formula for calculating the version score", "allOf": [ @@ -10752,7 +11007,7 @@ "type": "string" }, "VersionScoreFormulaParams": { - "description": "Given the formula of version_score = penalty ^ (num_versions_behind ^ penalty_scaling) define the relevant parameters", + "description": "Given the formula of version_score = penalty ^ (versions_behind_factor ^ penalty_scaling) define the relevant parameters", "type": "object", "required": [ "penalty", @@ -10826,15 +11081,10 @@ "ConfigScoreParams": { "type": "object", "required": [ - "current_nym_node_semver", "version_score_formula_params", "version_weights" ], "properties": { - "current_nym_node_semver": { - "description": "Current version of the nym node that is going to be used for determining the version score of a node. note: value stored here is pre-validated `semver::Version`", - "type": "string" - }, "version_score_formula_params": { "description": "Defines the parameters of the formula for calculating the version score", "allOf": [ @@ -10988,7 +11238,7 @@ "type": "string" }, "VersionScoreFormulaParams": { - "description": "Given the formula of version_score = penalty ^ (num_versions_behind ^ penalty_scaling) define the relevant parameters", + "description": "Given the formula of version_score = penalty ^ (versions_behind_factor ^ penalty_scaling) define the relevant parameters", "type": "object", "required": [ "penalty", diff --git a/contracts/mixnet/schema/raw/execute.json b/contracts/mixnet/schema/raw/execute.json index 2c042ee7054..367c6483b97 100644 --- a/contracts/mixnet/schema/raw/execute.json +++ b/contracts/mixnet/schema/raw/execute.json @@ -1027,12 +1027,6 @@ "ConfigScoreParamsUpdate": { "type": "object", "properties": { - "current_nym_node_semver": { - "type": [ - "string", - "null" - ] - }, "version_score_formula_params": { "anyOf": [ { @@ -1657,7 +1651,7 @@ "type": "string" }, "VersionScoreFormulaParams": { - "description": "Given the formula of version_score = penalty ^ (num_versions_behind ^ penalty_scaling) define the relevant parameters", + "description": "Given the formula of version_score = penalty ^ (versions_behind_factor ^ penalty_scaling) define the relevant parameters", "type": "object", "required": [ "penalty", diff --git a/contracts/mixnet/schema/raw/instantiate.json b/contracts/mixnet/schema/raw/instantiate.json index cbd3717aebe..7ab76571c79 100644 --- a/contracts/mixnet/schema/raw/instantiate.json +++ b/contracts/mixnet/schema/raw/instantiate.json @@ -56,8 +56,8 @@ }, "version_score_params": { "default": { - "penalty": "0.8", - "penalty_scaling": "2" + "penalty": "0.995", + "penalty_scaling": "1.65" }, "allOf": [ { @@ -68,7 +68,7 @@ "version_score_weights": { "default": { "major": 100, - "minor": 1, + "minor": 10, "patch": 1, "prerelease": 1 }, @@ -257,7 +257,7 @@ "type": "string" }, "VersionScoreFormulaParams": { - "description": "Given the formula of version_score = penalty ^ (num_versions_behind ^ penalty_scaling) define the relevant parameters", + "description": "Given the formula of version_score = penalty ^ (versions_behind_factor ^ penalty_scaling) define the relevant parameters", "type": "object", "required": [ "penalty", diff --git a/contracts/mixnet/schema/raw/migrate.json b/contracts/mixnet/schema/raw/migrate.json index ee125fae2fc..3844bce3087 100644 --- a/contracts/mixnet/schema/raw/migrate.json +++ b/contracts/mixnet/schema/raw/migrate.json @@ -17,8 +17,8 @@ }, "version_score_params": { "default": { - "penalty": "0.8", - "penalty_scaling": "2" + "penalty": "0.995", + "penalty_scaling": "1.65" }, "allOf": [ { @@ -29,7 +29,7 @@ "version_score_weights": { "default": { "major": 100, - "minor": 1, + "minor": 10, "patch": 1, "prerelease": 1 }, @@ -86,7 +86,7 @@ "additionalProperties": false }, "VersionScoreFormulaParams": { - "description": "Given the formula of version_score = penalty ^ (num_versions_behind ^ penalty_scaling) define the relevant parameters", + "description": "Given the formula of version_score = penalty ^ (versions_behind_factor ^ penalty_scaling) define the relevant parameters", "type": "object", "required": [ "penalty", diff --git a/contracts/mixnet/schema/raw/query.json b/contracts/mixnet/schema/raw/query.json index 90d0477a303..fdf631ebce2 100644 --- a/contracts/mixnet/schema/raw/query.json +++ b/contracts/mixnet/schema/raw/query.json @@ -85,6 +85,54 @@ }, "additionalProperties": false }, + { + "description": "Get the current expected version of a Nym Node.", + "type": "object", + "required": [ + "get_current_nym_node_version" + ], + "properties": { + "get_current_nym_node_version": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Get the version history of Nym Node.", + "type": "object", + "required": [ + "get_nym_node_version_history" + ], + "properties": { + "get_nym_node_version_history": { + "type": "object", + "properties": { + "limit": { + "description": "Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "description": "Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, { "description": "Gets the current parameters used for reward calculation.", "type": "object", diff --git a/contracts/mixnet/schema/raw/response_to_get_current_nym_node_version.json b/contracts/mixnet/schema/raw/response_to_get_current_nym_node_version.json new file mode 100644 index 00000000000..c075132d9e4 --- /dev/null +++ b/contracts/mixnet/schema/raw/response_to_get_current_nym_node_version.json @@ -0,0 +1,105 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "CurrentNymNodeVersionResponse", + "type": "object", + "properties": { + "version": { + "anyOf": [ + { + "$ref": "#/definitions/HistoricalNymNodeVersionEntry" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "definitions": { + "HistoricalNymNodeVersion": { + "type": "object", + "required": [ + "difference_since_genesis", + "introduced_at_height", + "semver" + ], + "properties": { + "difference_since_genesis": { + "description": "The absolute version difference as compared against the first version introduced into the contract.", + "allOf": [ + { + "$ref": "#/definitions/TotalVersionDifference" + } + ] + }, + "introduced_at_height": { + "description": "Block height of when this version has been added to the contract", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "semver": { + "description": "Version of the nym node that is going to be used for determining the version score of a node. note: value stored here is pre-validated `semver::Version`", + "type": "string" + } + }, + "additionalProperties": false + }, + "HistoricalNymNodeVersionEntry": { + "type": "object", + "required": [ + "id", + "version_information" + ], + "properties": { + "id": { + "description": "The unique, ordered, id of this particular entry", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "version_information": { + "description": "Data associated with this particular version", + "allOf": [ + { + "$ref": "#/definitions/HistoricalNymNodeVersion" + } + ] + } + }, + "additionalProperties": false + }, + "TotalVersionDifference": { + "type": "object", + "required": [ + "major", + "minor", + "patch", + "prerelease" + ], + "properties": { + "major": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "minor": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "patch": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "prerelease": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + } +} diff --git a/contracts/mixnet/schema/raw/response_to_get_nym_node_version_history.json b/contracts/mixnet/schema/raw/response_to_get_nym_node_version_history.json new file mode 100644 index 00000000000..d85073662bc --- /dev/null +++ b/contracts/mixnet/schema/raw/response_to_get_nym_node_version_history.json @@ -0,0 +1,113 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "NymNodeVersionHistoryResponse", + "type": "object", + "required": [ + "history" + ], + "properties": { + "history": { + "type": "array", + "items": { + "$ref": "#/definitions/HistoricalNymNodeVersionEntry" + } + }, + "start_next_after": { + "description": "Field indicating paging information for the following queries if the caller wishes to get further entries.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false, + "definitions": { + "HistoricalNymNodeVersion": { + "type": "object", + "required": [ + "difference_since_genesis", + "introduced_at_height", + "semver" + ], + "properties": { + "difference_since_genesis": { + "description": "The absolute version difference as compared against the first version introduced into the contract.", + "allOf": [ + { + "$ref": "#/definitions/TotalVersionDifference" + } + ] + }, + "introduced_at_height": { + "description": "Block height of when this version has been added to the contract", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "semver": { + "description": "Version of the nym node that is going to be used for determining the version score of a node. note: value stored here is pre-validated `semver::Version`", + "type": "string" + } + }, + "additionalProperties": false + }, + "HistoricalNymNodeVersionEntry": { + "type": "object", + "required": [ + "id", + "version_information" + ], + "properties": { + "id": { + "description": "The unique, ordered, id of this particular entry", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "version_information": { + "description": "Data associated with this particular version", + "allOf": [ + { + "$ref": "#/definitions/HistoricalNymNodeVersion" + } + ] + } + }, + "additionalProperties": false + }, + "TotalVersionDifference": { + "type": "object", + "required": [ + "major", + "minor", + "patch", + "prerelease" + ], + "properties": { + "major": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "minor": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "patch": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "prerelease": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + } +} diff --git a/contracts/mixnet/schema/raw/response_to_get_state.json b/contracts/mixnet/schema/raw/response_to_get_state.json index 1161f1b6ae3..8d688360f71 100644 --- a/contracts/mixnet/schema/raw/response_to_get_state.json +++ b/contracts/mixnet/schema/raw/response_to_get_state.json @@ -76,15 +76,10 @@ "ConfigScoreParams": { "type": "object", "required": [ - "current_nym_node_semver", "version_score_formula_params", "version_weights" ], "properties": { - "current_nym_node_semver": { - "description": "Current version of the nym node that is going to be used for determining the version score of a node. note: value stored here is pre-validated `semver::Version`", - "type": "string" - }, "version_score_formula_params": { "description": "Defines the parameters of the formula for calculating the version score", "allOf": [ @@ -274,7 +269,7 @@ "type": "string" }, "VersionScoreFormulaParams": { - "description": "Given the formula of version_score = penalty ^ (num_versions_behind ^ penalty_scaling) define the relevant parameters", + "description": "Given the formula of version_score = penalty ^ (versions_behind_factor ^ penalty_scaling) define the relevant parameters", "type": "object", "required": [ "penalty", diff --git a/contracts/mixnet/schema/raw/response_to_get_state_params.json b/contracts/mixnet/schema/raw/response_to_get_state_params.json index 4a5bf9d2dcd..47da7893a61 100644 --- a/contracts/mixnet/schema/raw/response_to_get_state_params.json +++ b/contracts/mixnet/schema/raw/response_to_get_state_params.json @@ -54,15 +54,10 @@ "ConfigScoreParams": { "type": "object", "required": [ - "current_nym_node_semver", "version_score_formula_params", "version_weights" ], "properties": { - "current_nym_node_semver": { - "description": "Current version of the nym node that is going to be used for determining the version score of a node. note: value stored here is pre-validated `semver::Version`", - "type": "string" - }, "version_score_formula_params": { "description": "Defines the parameters of the formula for calculating the version score", "allOf": [ @@ -216,7 +211,7 @@ "type": "string" }, "VersionScoreFormulaParams": { - "description": "Given the formula of version_score = penalty ^ (num_versions_behind ^ penalty_scaling) define the relevant parameters", + "description": "Given the formula of version_score = penalty ^ (versions_behind_factor ^ penalty_scaling) define the relevant parameters", "type": "object", "required": [ "penalty", diff --git a/contracts/mixnet/src/constants.rs b/contracts/mixnet/src/constants.rs index 3d2d1651138..50cf7c2541f 100644 --- a/contracts/mixnet/src/constants.rs +++ b/contracts/mixnet/src/constants.rs @@ -71,6 +71,9 @@ pub const LAST_INTERVAL_EVENT_ID_KEY: &str = "lie"; pub const ADMIN_STORAGE_KEY: &str = "admin"; pub const CONTRACT_STATE_KEY: &str = "state"; +pub const VERSION_HISTORY_ID_COUNTER_KEY: &str = "vhid"; +pub const VERSION_HISTORY_NAMESPACE: &str = "vh"; + pub const NYMNODE_ROLES_ASSIGNMENT_NAMESPACE: &str = "roles"; pub const NYMNODE_REWARDED_SET_METADATA_NAMESPACE: &str = "roles_metadata"; pub const NYMNODE_ACTIVE_ROLE_ASSIGNMENT_KEY: &str = "active_roles"; diff --git a/contracts/mixnet/src/contract.rs b/contracts/mixnet/src/contract.rs index 86a701978b0..3526e586368 100644 --- a/contracts/mixnet/src/contract.rs +++ b/contracts/mixnet/src/contract.rs @@ -48,7 +48,6 @@ fn default_initial_state( }, config_score_params: ConfigScoreParams { - current_nym_node_semver: msg.current_nym_node_version.clone(), version_weights: msg.version_score_weights, version_score_formula_params: msg.version_score_params, }, @@ -101,7 +100,13 @@ pub fn instantiate( starting_interval, rewarding_validator_address, )?; - mixnet_params_storage::initialise_storage(deps.branch(), state, info.sender)?; + mixnet_params_storage::initialise_storage( + deps.branch(), + &env, + state, + info.sender, + msg.current_nym_node_version, + )?; RewardingStorage::new().initialise(deps.storage, reward_params)?; nymnodes_storage::initialise_storage(deps.storage)?; cw2::set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; @@ -138,6 +143,7 @@ pub fn execute( ExecuteMsg::UpdateCurrentNymNodeSemver { current_version } => { crate::mixnet_contract_settings::transactions::try_update_current_nym_node_semver( deps, + env, info, current_version, ) @@ -334,6 +340,16 @@ pub fn query( QueryMsg::GetState {} => { to_binary(&crate::mixnet_contract_settings::queries::query_contract_state(deps)?) } + QueryMsg::GetCurrentNymNodeVersion {} => to_binary( + &crate::mixnet_contract_settings::queries::query_current_nym_node_version(deps)?, + ), + QueryMsg::GetNymNodeVersionHistory { limit, start_after } => to_binary( + &crate::mixnet_contract_settings::queries::query_nym_node_version_history_paged( + deps, + start_after, + limit, + )?, + ), QueryMsg::Admin {} => to_binary(&crate::mixnet_contract_settings::queries::query_admin( deps, )?), @@ -587,7 +603,7 @@ pub fn query( #[entry_point] pub fn migrate( mut deps: DepsMut<'_>, - _env: Env, + env: Env, msg: MigrateMsg, ) -> Result<Response, MixnetContractError> { set_build_information!(deps.storage)?; @@ -596,7 +612,7 @@ pub fn migrate( let skip_state_updates = msg.unsafe_skip_state_updates.unwrap_or(false); if !skip_state_updates { - crate::queued_migrations::add_config_score_params(deps.branch(), &msg)?; + crate::queued_migrations::add_config_score_params(deps.branch(), env, &msg)?; } // due to circular dependency on contract addresses (i.e. mixnet contract requiring vesting contract address @@ -693,7 +709,6 @@ mod tests { }, }, config_score_params: ConfigScoreParams { - current_nym_node_semver: "1.1.10".to_string(), version_weights: Default::default(), version_score_formula_params: Default::default(), }, diff --git a/contracts/mixnet/src/mixnet_contract_settings/queries.rs b/contracts/mixnet/src/mixnet_contract_settings/queries.rs index 4ece720fcd2..5963dbf0a2b 100644 --- a/contracts/mixnet/src/mixnet_contract_settings/queries.rs +++ b/contracts/mixnet/src/mixnet_contract_settings/queries.rs @@ -3,9 +3,14 @@ use super::storage; use crate::mixnet_contract_settings::storage::ADMIN; -use cosmwasm_std::{Deps, StdResult}; +use cosmwasm_std::{Deps, Order, StdResult}; use cw_controllers::AdminResponse; -use mixnet_contract_common::{ContractBuildInformation, ContractState, ContractStateParams}; +use cw_storage_plus::Bound; +use mixnet_contract_common::error::MixnetContractError; +use mixnet_contract_common::{ + ContractBuildInformation, ContractState, ContractStateParams, CurrentNymNodeVersionResponse, + HistoricalNymNodeVersionEntry, NymNodeVersionHistoryResponse, +}; use nym_contracts_common::get_build_information; pub(crate) fn query_admin(deps: Deps<'_>) -> StdResult<AdminResponse> { @@ -32,6 +37,36 @@ pub(crate) fn query_contract_version() -> ContractBuildInformation { get_build_information!() } +pub(crate) fn query_nym_node_version_history_paged( + deps: Deps<'_>, + start_after: Option<u32>, + limit: Option<u32>, +) -> StdResult<NymNodeVersionHistoryResponse> { + let limit = limit.unwrap_or(100).min(200) as usize; + let start = start_after.map(Bound::exclusive); + + let history = storage::NymNodeVersionHistory::new() + .version_history + .range(deps.storage, start, None, Order::Ascending) + .take(limit) + .map(|r| r.map(Into::<HistoricalNymNodeVersionEntry>::into)) + .collect::<StdResult<Vec<_>>>()?; + + let start_next_after = history.last().map(|entry| entry.id); + + Ok(NymNodeVersionHistoryResponse { + history, + start_next_after, + }) +} + +pub(crate) fn query_current_nym_node_version( + deps: Deps<'_>, +) -> Result<CurrentNymNodeVersionResponse, MixnetContractError> { + let version = storage::NymNodeVersionHistory::new().current_version(deps.storage)?; + Ok(CurrentNymNodeVersionResponse { version }) +} + #[cfg(test)] pub(crate) mod tests { use super::*; @@ -59,7 +94,6 @@ pub(crate) mod tests { interval_operating_cost: Default::default(), }, config_score_params: ConfigScoreParams { - current_nym_node_semver: "1.1.10".to_string(), version_weights: Default::default(), version_score_formula_params: Default::default(), }, diff --git a/contracts/mixnet/src/mixnet_contract_settings/storage.rs b/contracts/mixnet/src/mixnet_contract_settings/storage.rs index 09bab3490cc..6201972e51a 100644 --- a/contracts/mixnet/src/mixnet_contract_settings/storage.rs +++ b/contracts/mixnet/src/mixnet_contract_settings/storage.rs @@ -1,19 +1,108 @@ // Copyright 2021-2023 - Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: Apache-2.0 -use crate::constants::{ADMIN_STORAGE_KEY, CONTRACT_STATE_KEY}; -use cosmwasm_std::{Addr, DepsMut, Storage}; -use cosmwasm_std::{Coin, StdResult}; +use crate::constants::{ + ADMIN_STORAGE_KEY, CONTRACT_STATE_KEY, VERSION_HISTORY_ID_COUNTER_KEY, + VERSION_HISTORY_NAMESPACE, +}; +use cosmwasm_std::Coin; +use cosmwasm_std::{Addr, DepsMut, Env, Storage}; use cw_controllers::Admin; -use cw_storage_plus::Item; +use cw_storage_plus::{Item, Map}; use mixnet_contract_common::error::MixnetContractError; use mixnet_contract_common::{ - ContractState, ContractStateParams, OperatingCostRange, ProfitMarginRange, + ContractState, ContractStateParams, HistoricalNymNodeVersion, HistoricalNymNodeVersionEntry, + OperatingCostRange, ProfitMarginRange, }; +use std::str::FromStr; pub(crate) const CONTRACT_STATE: Item<'_, ContractState> = Item::new(CONTRACT_STATE_KEY); pub(crate) const ADMIN: Admin = Admin::new(ADMIN_STORAGE_KEY); +pub(crate) struct NymNodeVersionHistory<'a> { + pub(crate) id_counter: Item<'a, u32>, + pub(crate) version_history: Map<'a, u32, HistoricalNymNodeVersion>, +} + +impl NymNodeVersionHistory<'_> { + #[allow(clippy::new_without_default)] + pub const fn new() -> Self { + Self { + id_counter: Item::new(VERSION_HISTORY_ID_COUNTER_KEY), + version_history: Map::new(VERSION_HISTORY_NAMESPACE), + } + } + + fn next_id(&self, storage: &mut dyn Storage) -> Result<u32, MixnetContractError> { + let next = self.id_counter.may_load(storage)?.unwrap_or_default(); + self.id_counter.save(storage, &next)?; + Ok(next) + } + + pub fn current_version( + &self, + storage: &dyn Storage, + ) -> Result<Option<HistoricalNymNodeVersionEntry>, MixnetContractError> { + let Some(current_id) = self.id_counter.may_load(storage)? else { + return Ok(None); + }; + let version_information = self.version_history.load(storage, current_id)?; + Ok(Some(HistoricalNymNodeVersionEntry { + id: current_id, + version_information, + })) + } + + pub fn insert_new( + &self, + storage: &mut dyn Storage, + entry: HistoricalNymNodeVersion, + ) -> Result<u32, MixnetContractError> { + let next_id = self.next_id(storage)?; + self.version_history.save(storage, next_id, &entry)?; + Ok(next_id) + } + + pub fn try_insert_new( + &self, + storage: &mut dyn Storage, + env: &Env, + raw_semver: &str, + ) -> Result<u32, MixnetContractError> { + let Ok(new_semver) = semver::Version::from_str(raw_semver) else { + return Err(MixnetContractError::InvalidNymNodeSemver { + provided: raw_semver.to_string(), + }); + }; + + let Some(current) = self.current_version(storage)? else { + // treat this as genesis + let genesis = + HistoricalNymNodeVersion::genesis(raw_semver.to_string(), env.block.height); + return self.insert_new(storage, genesis); + }; + + let current_semver = current.version_information.semver_unchecked(); + if new_semver <= current_semver { + // make sure the new semver is strictly more recent than the current head + return Err(MixnetContractError::NonIncreasingSemver { + provided: raw_semver.to_string(), + current: current.version_information.semver, + }); + } + + let diff = current + .version_information + .cumulative_difference_since_genesis(&new_semver); + let entry = HistoricalNymNodeVersion { + semver: raw_semver.to_string(), + introduced_at_height: env.block.height, + difference_since_genesis: diff, + }; + self.insert_new(storage, entry) + } +} + pub fn rewarding_validator_address(storage: &dyn Storage) -> Result<Addr, MixnetContractError> { Ok(CONTRACT_STATE .load(storage) @@ -71,9 +160,13 @@ pub(crate) fn state_params( pub(crate) fn initialise_storage( deps: DepsMut<'_>, + env: &Env, initial_state: ContractState, initial_admin: Addr, -) -> StdResult<()> { + initial_nymnode_version: String, +) -> Result<(), MixnetContractError> { CONTRACT_STATE.save(deps.storage, &initial_state)?; - ADMIN.set(deps, Some(initial_admin)) + NymNodeVersionHistory::new().try_insert_new(deps.storage, env, &initial_nymnode_version)?; + ADMIN.set(deps, Some(initial_admin))?; + Ok(()) } diff --git a/contracts/mixnet/src/mixnet_contract_settings/transactions.rs b/contracts/mixnet/src/mixnet_contract_settings/transactions.rs index 78ddab97b9c..d3c3a211f1a 100644 --- a/contracts/mixnet/src/mixnet_contract_settings/transactions.rs +++ b/contracts/mixnet/src/mixnet_contract_settings/transactions.rs @@ -3,16 +3,15 @@ use super::storage; use crate::mixnet_contract_settings::storage::ADMIN; -use cosmwasm_std::MessageInfo; use cosmwasm_std::Response; use cosmwasm_std::{DepsMut, StdResult}; +use cosmwasm_std::{Env, MessageInfo}; use mixnet_contract_common::error::MixnetContractError; use mixnet_contract_common::events::{ new_rewarding_validator_address_update_event, new_settings_update_event, new_update_nym_node_semver_event, }; use mixnet_contract_common::ContractStateParamsUpdate; -use std::str::FromStr; pub fn try_update_contract_admin( mut deps: DepsMut<'_>, @@ -92,15 +91,6 @@ pub(crate) fn try_update_contract_settings( // check for config score params updates if let Some(config_score_update) = update.config_score_params { - // if semver is to be updated - validate the provided value - if let Some(current_nym_node_semver) = config_score_update.current_nym_node_semver { - if semver::Version::from_str(¤t_nym_node_semver).is_err() { - return Err(MixnetContractError::InvalidNymNodeSemver { - provided: current_nym_node_semver, - }); - } - state.params.config_score_params.current_nym_node_semver = current_nym_node_semver - } if let Some(version_weights) = config_score_update.version_weights { state.params.config_score_params.version_weights = version_weights } @@ -119,23 +109,19 @@ pub(crate) fn try_update_contract_settings( pub(crate) fn try_update_current_nym_node_semver( deps: DepsMut<'_>, + env: Env, info: MessageInfo, current_version: String, ) -> Result<Response, MixnetContractError> { - let mut state = storage::CONTRACT_STATE.load(deps.storage)?; ADMIN.assert_admin(deps.as_ref(), &info.sender)?; - let response = Response::new().add_event(new_update_nym_node_semver_event(¤t_version)); - - if semver::Version::from_str(¤t_version).is_err() { - return Err(MixnetContractError::InvalidNymNodeSemver { - provided: current_version, - }); - } + let new_id = storage::NymNodeVersionHistory::new().try_insert_new( + deps.storage, + &env, + ¤t_version, + )?; - state.params.config_score_params.current_nym_node_semver = current_version; - storage::CONTRACT_STATE.save(deps.storage, &state)?; - Ok(response) + Ok(Response::new().add_event(new_update_nym_node_semver_event(¤t_version, new_id))) } #[cfg(test)] diff --git a/contracts/mixnet/src/queued_migrations.rs b/contracts/mixnet/src/queued_migrations.rs index 3381cac47e6..476d484d809 100644 --- a/contracts/mixnet/src/queued_migrations.rs +++ b/contracts/mixnet/src/queued_migrations.rs @@ -4,7 +4,8 @@ mod config_score_params { use crate::constants::CONTRACT_STATE_KEY; use crate::mixnet_contract_settings::storage as mixnet_params_storage; - use cosmwasm_std::{Addr, Coin, DepsMut}; + use crate::mixnet_contract_settings::storage::NymNodeVersionHistory; + use cosmwasm_std::{Addr, Coin, DepsMut, Env}; use cw_storage_plus::Item; use mixnet_contract_common::error::MixnetContractError; use mixnet_contract_common::{ @@ -16,6 +17,7 @@ mod config_score_params { pub(crate) fn add_config_score_params( deps: DepsMut<'_>, + env: Env, msg: &MigrateMsg, ) -> Result<(), MixnetContractError> { if semver::Version::from_str(&msg.current_nym_node_semver).is_err() { @@ -62,7 +64,6 @@ mod config_score_params { interval_operating_cost: old_state.params.interval_operating_cost, }, config_score_params: ConfigScoreParams { - current_nym_node_semver: msg.current_nym_node_semver.to_string(), version_weights: msg.version_score_weights, version_score_formula_params: msg.version_score_params, }, @@ -70,6 +71,14 @@ mod config_score_params { }; mixnet_params_storage::CONTRACT_STATE.save(deps.storage, &new_state)?; + + // initialise the version chain + NymNodeVersionHistory::new().try_insert_new( + deps.storage, + &env, + &msg.current_nym_node_semver, + )?; + Ok(()) } } diff --git a/nym-api/nym-api-requests/src/models.rs b/nym-api/nym-api-requests/src/models.rs index 8465905b629..3b9c759620b 100644 --- a/nym-api/nym-api-requests/src/models.rs +++ b/nym-api/nym-api-requests/src/models.rs @@ -1340,6 +1340,138 @@ impl NodeRefreshBody { } } +#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] +pub struct RewardedSetResponse { + pub entry_gateways: Vec<NodeId>, + + pub exit_gateways: Vec<NodeId>, + + pub layer1: Vec<NodeId>, + + pub layer2: Vec<NodeId>, + + pub layer3: Vec<NodeId>, + + pub standby: Vec<NodeId>, +} + +pub use config_score::*; +pub mod config_score { + use nym_contracts_common::NaiveFloat; + use serde::{Deserialize, Serialize}; + use std::cmp::Ordering; + use utoipa::ToSchema; + + #[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] + pub struct ConfigScoreDataResponse { + pub parameters: ConfigScoreParams, + pub version_history: Vec<HistoricalNymNodeVersionEntry>, + } + + #[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema, PartialEq)] + pub struct HistoricalNymNodeVersionEntry { + /// The unique, ordered, id of this particular entry + pub id: u32, + + /// Data associated with this particular version + pub version_information: HistoricalNymNodeVersion, + } + + impl PartialOrd for HistoricalNymNodeVersionEntry { + fn partial_cmp(&self, other: &Self) -> Option<Ordering> { + // we only care about id for the purposes of ordering as they should have unique data + self.id.partial_cmp(&other.id) + } + } + + impl From<nym_mixnet_contract_common::HistoricalNymNodeVersionEntry> + for HistoricalNymNodeVersionEntry + { + fn from(value: nym_mixnet_contract_common::HistoricalNymNodeVersionEntry) -> Self { + HistoricalNymNodeVersionEntry { + id: value.id, + version_information: value.version_information.into(), + } + } + } + + #[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema, PartialEq)] + pub struct HistoricalNymNodeVersion { + /// Version of the nym node that is going to be used for determining the version score of a node. + /// note: value stored here is pre-validated `semver::Version` + pub semver: String, + + /// Block height of when this version has been added to the contract + pub introduced_at_height: u64, + // for now ignore that field. it will give nothing useful to the users + // pub difference_since_genesis: TotalVersionDifference, + } + + impl From<nym_mixnet_contract_common::HistoricalNymNodeVersion> for HistoricalNymNodeVersion { + fn from(value: nym_mixnet_contract_common::HistoricalNymNodeVersion) -> Self { + HistoricalNymNodeVersion { + semver: value.semver, + introduced_at_height: value.introduced_at_height, + } + } + } + + #[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] + pub struct ConfigScoreParams { + /// Defines weights for calculating numbers of versions behind the current release. + pub version_weights: OutdatedVersionWeights, + + /// Defines the parameters of the formula for calculating the version score + pub version_score_formula_params: VersionScoreFormulaParams, + } + + /// Defines weights for calculating numbers of versions behind the current release. + #[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] + pub struct OutdatedVersionWeights { + pub major: u32, + pub minor: u32, + pub patch: u32, + pub prerelease: u32, + } + + /// Given the formula of version_score = penalty ^ (versions_behind_factor ^ penalty_scaling) + /// define the relevant parameters + #[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] + pub struct VersionScoreFormulaParams { + pub penalty: f64, + pub penalty_scaling: f64, + } + + impl From<nym_mixnet_contract_common::ConfigScoreParams> for ConfigScoreParams { + fn from(value: nym_mixnet_contract_common::ConfigScoreParams) -> Self { + ConfigScoreParams { + version_weights: value.version_weights.into(), + version_score_formula_params: value.version_score_formula_params.into(), + } + } + } + + impl From<nym_mixnet_contract_common::OutdatedVersionWeights> for OutdatedVersionWeights { + fn from(value: nym_mixnet_contract_common::OutdatedVersionWeights) -> Self { + OutdatedVersionWeights { + major: value.major, + minor: value.minor, + patch: value.patch, + prerelease: value.prerelease, + } + } + } + + impl From<nym_mixnet_contract_common::VersionScoreFormulaParams> for VersionScoreFormulaParams { + fn from(value: nym_mixnet_contract_common::VersionScoreFormulaParams) -> Self { + VersionScoreFormulaParams { + penalty: value.penalty.naive_to_f64(), + penalty_scaling: value.penalty_scaling.naive_to_f64(), + } + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/nym-api/src/node_status_api/cache/node_sets.rs b/nym-api/src/node_status_api/cache/node_sets.rs index 0ea8820ab52..e03dd2807da 100644 --- a/nym-api/src/node_status_api/cache/node_sets.rs +++ b/nym-api/src/node_status_api/cache/node_sets.rs @@ -5,6 +5,7 @@ use crate::node_describe_cache::DescribedNodes; use crate::node_status_api::helpers::RewardedSetStatus; use crate::node_status_api::models::Uptime; use crate::node_status_api::reward_estimate::{compute_apy_from_reward, compute_reward_estimate}; +use crate::nym_contract_cache::cache::data::ConfigScoreData; use crate::nym_contract_cache::cache::CachedRewardedSet; use crate::support::storage::NymApiStorage; use nym_api_requests::legacy::{LegacyGatewayBondWithId, LegacyMixNodeDetailsWithLayer}; @@ -14,7 +15,7 @@ use nym_api_requests::models::{ MixNodeBondAnnotated, NodeAnnotation, NodePerformance, NymNodeDescription, RoutingScore, }; use nym_contracts_common::NaiveFloat; -use nym_mixnet_contract_common::{ConfigScoreParams, Interval, NodeId}; +use nym_mixnet_contract_common::{Interval, NodeId, VersionScoreFormulaParams}; use nym_mixnet_contract_common::{NymNodeDetails, RewardingParams}; use nym_topology::NetworkAddress; use std::collections::{HashMap, HashSet}; @@ -90,24 +91,37 @@ async fn get_routing_score( RoutingScore::new(score as f64) } +fn versions_behind_factor_to_config_score( + versions_behind: u32, + params: VersionScoreFormulaParams, +) -> f64 { + let penalty = params.penalty.naive_to_f64(); + let scaling = params.penalty_scaling.naive_to_f64(); + + // version_score = penalty ^ (num_versions_behind ^ penalty_scaling) + penalty.powf((versions_behind as f64).powf(scaling)) +} + fn calculate_config_score( - config_score_params: &ConfigScoreParams, + config_score_data: &ConfigScoreData, described_data: Option<&NymNodeDescription>, ) -> ConfigScore { let Some(described) = described_data else { return ConfigScore::unavailable(); }; - let Ok(reported_semver) = described - .description - .build_information - .build_version - .parse::<semver::Version>() - else { + let node_version = &described.description.build_information.build_version; + let Ok(reported_semver) = node_version.parse::<semver::Version>() else { return ConfigScore::bad_semver(); }; + let versions_behind = config_score_data + .config_score_params + .version_weights + .versions_behind_factor( + &reported_semver, + &config_score_data.nym_node_version_history, + ); - let versions_behind = config_score_params.versions_behind(&reported_semver); let runs_nym_node = described.description.build_information.binary_name == "nym-node"; let accepted_terms_and_conditions = described .description @@ -117,17 +131,12 @@ fn calculate_config_score( let version_score = if !runs_nym_node || !accepted_terms_and_conditions { 0. } else { - let penalty = config_score_params - .version_score_formula_params - .penalty - .naive_to_f64(); - let scaling = config_score_params - .version_score_formula_params - .penalty_scaling - .naive_to_f64(); - - // version_score = penalty ^ (num_versions_behind ^ penalty_scaling) - penalty.powf((versions_behind as f64).powf(scaling)) + versions_behind_factor_to_config_score( + versions_behind, + config_score_data + .config_score_params + .version_score_formula_params, + ) }; ConfigScore::new( @@ -285,7 +294,7 @@ pub(crate) async fn annotate_legacy_gateways_with_details( #[allow(clippy::too_many_arguments)] pub(crate) async fn produce_node_annotations( storage: &NymApiStorage, - config_score_params: &ConfigScoreParams, + config_score_data: &ConfigScoreData, legacy_mixnodes: &[LegacyMixNodeDetailsWithLayer], legacy_gateways: &[LegacyGatewayBondWithId], nym_nodes: &[NymNodeDetails], @@ -301,7 +310,7 @@ pub(crate) async fn produce_node_annotations( let routing_score = get_routing_score(storage, node_id, LegacyMixnode, current_interval).await; let config_score = - calculate_config_score(config_score_params, described_nodes.get_node(&node_id)); + calculate_config_score(config_score_data, described_nodes.get_node(&node_id)); let performance = routing_score.score * config_score.score; // map it from 0-1 range into 0-100 @@ -327,7 +336,7 @@ pub(crate) async fn produce_node_annotations( let routing_score = get_routing_score(storage, node_id, LegacyGateway, current_interval).await; let config_score = - calculate_config_score(config_score_params, described_nodes.get_node(&node_id)); + calculate_config_score(config_score_data, described_nodes.get_node(&node_id)); let performance = routing_score.score * config_score.score; // map it from 0-1 range into 0-100 @@ -352,7 +361,7 @@ pub(crate) async fn produce_node_annotations( let node_id = nym_node.node_id(); let routing_score = get_routing_score(storage, node_id, NymNode, current_interval).await; let config_score = - calculate_config_score(config_score_params, described_nodes.get_node(&node_id)); + calculate_config_score(config_score_data, described_nodes.get_node(&node_id)); let performance = routing_score.score * config_score.score; // map it from 0-1 range into 0-100 diff --git a/nym-api/src/node_status_api/cache/refresher.rs b/nym-api/src/node_status_api/cache/refresher.rs index 1d1771d7bbc..b9f4aed5075 100644 --- a/nym-api/src/node_status_api/cache/refresher.rs +++ b/nym-api/src/node_status_api/cache/refresher.rs @@ -144,9 +144,9 @@ impl NodeStatusCacheRefresher { let rewarded_set = self.contract_cache.rewarded_set_owned().await; let gateway_bonds = self.contract_cache.legacy_gateways_all().await; let nym_nodes = self.contract_cache.nym_nodes().await; - let config_score_params = self + let config_score_data = self .contract_cache - .config_score_params() + .config_score_data_owned() .await .into_inner() .ok_or(NodeStatusCacheError::SourceDataMissing)?; @@ -181,7 +181,7 @@ impl NodeStatusCacheRefresher { // Create annotated data let node_annotations = produce_node_annotations( &self.storage, - &config_score_params, + &config_score_data, &mixnode_details, &gateway_bonds, &nym_nodes, diff --git a/nym-api/src/node_status_api/handlers/mod.rs b/nym-api/src/node_status_api/handlers/mod.rs index f42cd59d841..b720294af94 100644 --- a/nym-api/src/node_status_api/handlers/mod.rs +++ b/nym-api/src/node_status_api/handlers/mod.rs @@ -1,8 +1,13 @@ // Copyright 2021-2024 - Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: GPL-3.0-only +use crate::node_status_api::models::AxumResult; +use crate::support::caching::cache::UninitialisedCache; use crate::support::http::state::AppState; -use axum::Router; +use axum::extract::State; +use axum::routing::get; +use axum::{Json, Router}; +use nym_api_requests::models::ConfigScoreDataResponse; use nym_mixnet_contract_common::NodeId; use serde::Deserialize; use utoipa::IntoParams; @@ -11,7 +16,7 @@ pub(crate) mod network_monitor; pub(crate) mod unstable; pub(crate) mod without_monitor; -pub(crate) fn node_status_routes(network_monitor: bool) -> Router<AppState> { +pub(crate) fn status_routes(network_monitor: bool) -> Router<AppState> { // in the minimal variant we would not have access to endpoints relying on existence // of the network monitor and the associated storage let without_network_monitor = without_monitor::mandatory_routes(); @@ -23,6 +28,7 @@ pub(crate) fn node_status_routes(network_monitor: bool) -> Router<AppState> { } else { without_network_monitor } + .route("/config-score-details", get(config_score_details)) } #[derive(Deserialize, IntoParams)] @@ -30,3 +36,24 @@ pub(crate) fn node_status_routes(network_monitor: bool) -> Router<AppState> { struct MixIdParam { mix_id: NodeId, } + +#[utoipa::path( + tag = "Status", + get, + path = "/config-score-details", + context_path = "/v1/status", + responses( + (status = 200, body = ConfigScoreDataResponse) + ), +)] +async fn config_score_details( + State(state): State<AppState>, +) -> AxumResult<Json<ConfigScoreDataResponse>> { + let data = state + .nym_contract_cache() + .maybe_config_score_data_owned() + .await + .ok_or(UninitialisedCache)?; + + Ok(Json(data.into_inner().into())) +} diff --git a/nym-api/src/nym_contract_cache/cache/data.rs b/nym-api/src/nym_contract_cache/cache/data.rs index 3bfe058f729..d259109f988 100644 --- a/nym-api/src/nym_contract_cache/cache/data.rs +++ b/nym-api/src/nym_contract_cache/cache/data.rs @@ -3,10 +3,12 @@ use crate::support::caching::Cache; use nym_api_requests::legacy::{LegacyGatewayBondWithId, LegacyMixNodeDetailsWithLayer}; +use nym_api_requests::models::{ConfigScoreDataResponse, RewardedSetResponse}; use nym_contracts_common::ContractBuildInformation; use nym_mixnet_contract_common::nym_node::Role; use nym_mixnet_contract_common::{ - ConfigScoreParams, Interval, NodeId, NymNodeDetails, RewardedSet, RewardingParams, + ConfigScoreParams, HistoricalNymNodeVersionEntry, Interval, NodeId, NymNodeDetails, + RewardedSet, RewardingParams, }; use nym_validator_client::nyxd::AccountId; use std::collections::{HashMap, HashSet}; @@ -52,6 +54,19 @@ impl From<CachedRewardedSet> for RewardedSet { } } +impl From<&CachedRewardedSet> for RewardedSetResponse { + fn from(value: &CachedRewardedSet) -> Self { + RewardedSetResponse { + entry_gateways: value.entry_gateways.iter().copied().collect(), + exit_gateways: value.exit_gateways.iter().copied().collect(), + layer1: value.layer1.iter().copied().collect(), + layer2: value.layer2.iter().copied().collect(), + layer3: value.layer3.iter().copied().collect(), + standby: value.standby.iter().copied().collect(), + } + } +} + impl CachedRewardedSet { pub(crate) fn role(&self, node_id: NodeId) -> Option<Role> { if self.entry_gateways.contains(&node_id) { @@ -112,6 +127,25 @@ impl CachedRewardedSet { } } +#[derive(Clone)] +pub(crate) struct ConfigScoreData { + pub(crate) config_score_params: ConfigScoreParams, + pub(crate) nym_node_version_history: Vec<HistoricalNymNodeVersionEntry>, +} + +impl From<ConfigScoreData> for ConfigScoreDataResponse { + fn from(value: ConfigScoreData) -> Self { + ConfigScoreDataResponse { + parameters: value.config_score_params.into(), + version_history: value + .nym_node_version_history + .into_iter() + .map(Into::into) + .collect(), + } + } +} + pub(crate) struct ContractCacheData { pub(crate) legacy_mixnodes: Cache<Vec<LegacyMixNodeDetailsWithLayer>>, pub(crate) legacy_gateways: Cache<Vec<LegacyGatewayBondWithId>>, @@ -123,7 +157,7 @@ pub(crate) struct ContractCacheData { pub(crate) legacy_mixnodes_blacklist: Cache<HashSet<NodeId>>, pub(crate) legacy_gateways_blacklist: Cache<HashSet<NodeId>>, - pub(crate) config_score_params: Cache<Option<ConfigScoreParams>>, + pub(crate) config_score_data: Cache<Option<ConfigScoreData>>, pub(crate) current_reward_params: Cache<Option<RewardingParams>>, pub(crate) current_interval: Cache<Option<Interval>>, @@ -143,7 +177,7 @@ impl ContractCacheData { current_interval: Cache::default(), current_reward_params: Cache::default(), contracts_info: Cache::default(), - config_score_params: Default::default(), + config_score_data: Default::default(), } } } diff --git a/nym-api/src/nym_contract_cache/cache/mod.rs b/nym-api/src/nym_contract_cache/cache/mod.rs index ed5db5cb3c9..1738901550e 100644 --- a/nym-api/src/nym_contract_cache/cache/mod.rs +++ b/nym-api/src/nym_contract_cache/cache/mod.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::node_describe_cache::RefreshData; -use crate::nym_contract_cache::cache::data::CachedContractsInfo; +use crate::nym_contract_cache::cache::data::{CachedContractsInfo, ConfigScoreData}; use crate::support::caching::Cache; use data::ContractCacheData; use nym_api_requests::legacy::{ @@ -11,7 +11,8 @@ use nym_api_requests::legacy::{ use nym_api_requests::models::MixnodeStatus; use nym_crypto::asymmetric::ed25519; use nym_mixnet_contract_common::{ - ConfigScoreParams, Interval, NodeId, NymNodeDetails, RewardedSet, RewardingParams, + ConfigScoreParams, HistoricalNymNodeVersionEntry, Interval, NodeId, NymNodeDetails, + RewardedSet, RewardingParams, }; use std::{ collections::HashSet, @@ -25,7 +26,7 @@ use tokio::sync::{RwLock, RwLockReadGuard}; use tokio::time; use tracing::{debug, error}; -mod data; +pub(crate) mod data; pub(crate) mod refresher; pub(crate) use self::data::CachedRewardedSet; @@ -81,19 +82,23 @@ impl NymContractCache { nym_nodes: Vec<NymNodeDetails>, rewarded_set: RewardedSet, config_score_params: ConfigScoreParams, + nym_node_version_history: Vec<HistoricalNymNodeVersionEntry>, rewarding_params: RewardingParams, current_interval: Interval, nym_contracts_info: CachedContractsInfo, ) { match time::timeout(Duration::from_millis(100), self.inner.write()).await { Ok(mut cache) => { + let config_score_data = ConfigScoreData { + config_score_params, + nym_node_version_history, + }; + cache.legacy_mixnodes.unchecked_update(mixnodes); cache.legacy_gateways.unchecked_update(gateways); cache.nym_nodes.unchecked_update(nym_nodes); cache.rewarded_set.unchecked_update(rewarded_set); - cache - .config_score_params - .unchecked_update(config_score_params); + cache.config_score_data.unchecked_update(config_score_data); cache .current_reward_params .unchecked_update(Some(rewarding_params)); @@ -269,8 +274,12 @@ impl NymContractCache { .unwrap_or_default() } - pub async fn config_score_params(&self) -> Cache<Option<ConfigScoreParams>> { - self.get_owned(|cache| cache.config_score_params.clone_cache()) + pub async fn maybe_config_score_data_owned(&self) -> Option<Cache<ConfigScoreData>> { + self.config_score_data_owned().await.transpose() + } + + pub async fn config_score_data_owned(&self) -> Cache<Option<ConfigScoreData>> { + self.get_owned(|cache| cache.config_score_data.clone_cache()) .await .unwrap_or_default() } diff --git a/nym-api/src/nym_contract_cache/cache/refresher.rs b/nym-api/src/nym_contract_cache/cache/refresher.rs index 64d6672517f..6681ac669e7 100644 --- a/nym-api/src/nym_contract_cache/cache/refresher.rs +++ b/nym-api/src/nym_contract_cache/cache/refresher.rs @@ -178,6 +178,7 @@ impl NymContractCacheRefresher { } let config_score_params = self.nyxd_client.get_config_score_params().await?; + let nym_node_version_history = self.nyxd_client.get_nym_node_version_history().await?; let contract_info = self.get_nym_contracts_info().await?; info!( @@ -194,6 +195,7 @@ impl NymContractCacheRefresher { nym_nodes, rewarded_set, config_score_params, + nym_node_version_history, rewarding_params, current_interval, contract_info, @@ -214,25 +216,6 @@ impl NymContractCacheRefresher { .unwrap_or_default() } - // fn collect_rewarded_and_active_set_details( - // all_mixnodes: &[MixNodeDetails], - // rewarded_set_nodes: RewardedSet, - // ) -> (Vec<MixNodeDetails>, Vec<MixNodeDetails>) { - // let mut active_set = Vec::new(); - // let mut rewarded_set = Vec::new(); - // - // for mix in all_mixnodes { - // if let Some(status) = rewarded_set_nodes.get(&mix.mix_id()) { - // rewarded_set.push(mix.clone()); - // if status.is_active() { - // active_set.push(mix.clone()) - // } - // } - // } - // - // (rewarded_set, active_set) - // } - pub(crate) async fn run(&self, mut shutdown: TaskClient) { let mut interval = time::interval(self.caching_interval); while !shutdown.is_shutdown() { diff --git a/nym-api/src/nym_nodes/handlers/mod.rs b/nym-api/src/nym_nodes/handlers/mod.rs index 42cfee3c86c..a3646e8b50b 100644 --- a/nym-api/src/nym_nodes/handlers/mod.rs +++ b/nym-api/src/nym_nodes/handlers/mod.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::node_status_api::models::{AxumErrorResponse, AxumResult}; +use crate::support::caching::cache::UninitialisedCache; use crate::support::http::helpers::{NodeIdParam, PaginationRequest}; use crate::support::http::state::AppState; use axum::extract::{Path, Query, State}; @@ -9,7 +10,8 @@ use axum::routing::{get, post}; use axum::{Json, Router}; use nym_api_requests::models::{ AnnotationResponse, NodeDatePerformanceResponse, NodePerformanceResponse, NodeRefreshBody, - NoiseDetails, NymNodeDescription, PerformanceHistoryResponse, UptimeHistoryResponse, + NoiseDetails, NymNodeDescription, PerformanceHistoryResponse, RewardedSetResponse, + UptimeHistoryResponse, }; use nym_api_requests::pagination::{PaginatedResponse, Pagination}; use nym_contracts_common::NaiveFloat; @@ -17,6 +19,7 @@ use nym_mixnet_contract_common::reward_params::Performance; use nym_mixnet_contract_common::NymNodeDetails; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +use std::ops::Deref; use std::time::Duration; use time::{Date, OffsetDateTime}; use utoipa::{IntoParams, ToSchema}; @@ -42,6 +45,27 @@ pub(crate) fn nym_node_routes() -> Router<AppState> { ) // to make it compatible with all the explorers that were used to using 0-100 values .route("/uptime-history/:node_id", get(get_node_uptime_history)) + .route("/rewarded-set", get(rewarded_set)) +} + +#[utoipa::path( + tag = "Nym Nodes", + get, + request_body = NodeRefreshBody, + path = "/rewarded-set", + context_path = "/v1/nym-nodes", + responses( + (status = 200, body = RewardedSetResponse) + ), +)] +async fn rewarded_set(State(state): State<AppState>) -> AxumResult<Json<RewardedSetResponse>> { + let cached_rewarded_set = state + .nym_contract_cache() + .rewarded_set() + .await + .ok_or(UninitialisedCache)?; + + Ok(Json(cached_rewarded_set.deref().deref().into())) } #[utoipa::path( diff --git a/nym-api/src/support/caching/cache.rs b/nym-api/src/support/caching/cache.rs index 405f6022a3c..da746399f54 100644 --- a/nym-api/src/support/caching/cache.rs +++ b/nym-api/src/support/caching/cache.rs @@ -103,6 +103,15 @@ pub struct Cache<T> { as_at: OffsetDateTime, } +impl<T> Cache<Option<T>> { + pub(crate) fn transpose(self) -> Option<Cache<T>> { + self.value.map(|value| Cache { + value, + as_at: self.as_at, + }) + } +} + impl<T> Cache<T> { // ugh. I hate to expose it, but it'd have broken pre-existing code pub(crate) fn new(value: T) -> Self { diff --git a/nym-api/src/support/http/router.rs b/nym-api/src/support/http/router.rs index a6c0390ab9f..4c291fbc587 100644 --- a/nym-api/src/support/http/router.rs +++ b/nym-api/src/support/http/router.rs @@ -4,7 +4,7 @@ use crate::circulating_supply_api::handlers::circulating_supply_routes; use crate::ecash::api_routes::handlers::ecash_routes; use crate::network::handlers::nym_network_routes; -use crate::node_status_api::handlers::node_status_routes; +use crate::node_status_api::handlers::status_routes; use crate::nym_contract_cache::handlers::nym_contract_cache_routes; use crate::nym_nodes::handlers::legacy::legacy_nym_node_routes; use crate::nym_nodes::handlers::nym_node_routes; @@ -59,7 +59,7 @@ impl RouterBuilder { .merge(nym_contract_cache_routes()) .merge(legacy_nym_node_routes()) .nest("/circulating-supply", circulating_supply_routes()) - .nest("/status", node_status_routes(network_monitor)) + .nest("/status", status_routes(network_monitor)) .nest("/network", nym_network_routes()) .nest("/api-status", status::handlers::api_status_routes()) .nest("/nym-nodes", nym_node_routes()) diff --git a/nym-api/src/support/nyxd/mod.rs b/nym-api/src/support/nyxd/mod.rs index 5e129058b94..cb97f758340 100644 --- a/nym-api/src/support/nyxd/mod.rs +++ b/nym-api/src/support/nyxd/mod.rs @@ -29,8 +29,8 @@ use nym_mixnet_contract_common::mixnode::MixNodeDetails; use nym_mixnet_contract_common::nym_node::Role; use nym_mixnet_contract_common::reward_params::RewardingParams; use nym_mixnet_contract_common::{ - ConfigScoreParams, CurrentIntervalResponse, EpochStatus, ExecuteMsg, GatewayBond, IdentityKey, - NymNodeDetails, RewardedSet, RoleAssignment, + ConfigScoreParams, CurrentIntervalResponse, EpochStatus, ExecuteMsg, GatewayBond, + HistoricalNymNodeVersionEntry, IdentityKey, NymNodeDetails, RewardedSet, RoleAssignment, }; use nym_validator_client::coconut::EcashApiError; use nym_validator_client::nyxd::contract_traits::mixnet_query_client::MixnetQueryClientExt; @@ -232,6 +232,12 @@ impl Client { .map(|state| state.config_score_params) } + pub(crate) async fn get_nym_node_version_history( + &self, + ) -> Result<Vec<HistoricalNymNodeVersionEntry>, NyxdError> { + nyxd_query!(self, get_full_nym_node_version_history().await) + } + pub(crate) async fn get_current_interval(&self) -> Result<CurrentIntervalResponse, NyxdError> { nyxd_query!(self, get_current_interval_details().await) } From 10933ff8f12fd00901f7d6cfde912ffce62828d8 Mon Sep 17 00:00:00 2001 From: benedettadavico <benedetta.davico@gmail.com> Date: Mon, 9 Dec 2024 16:58:55 +0100 Subject: [PATCH 17/18] update node version to 1.2.0 again --- nym-node/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nym-node/Cargo.toml b/nym-node/Cargo.toml index 2653ac5f846..897a70cb0c4 100644 --- a/nym-node/Cargo.toml +++ b/nym-node/Cargo.toml @@ -3,7 +3,7 @@ [package] name = "nym-node" -version = "1.1.13" +version = "1.2.0" authors.workspace = true repository.workspace = true homepage.workspace = true From 96f99bb9e4e750ee0ef8fe0b54e07bb671c876c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= <jedrzej.stuczynski@gmail.com> Date: Tue, 10 Dec 2024 10:37:04 +0000 Subject: [PATCH 18/18] bugfix: added explicit openapi servers to account for route prefixes (#5237) --- nym-api/src/support/http/openapi.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/nym-api/src/support/http/openapi.rs b/nym-api/src/support/http/openapi.rs index 876bf35cb78..99713e16507 100644 --- a/nym-api/src/support/http/openapi.rs +++ b/nym-api/src/support/http/openapi.rs @@ -17,6 +17,11 @@ use utoipauto::utoipauto; #[derive(OpenApi)] #[openapi( info(title = "Nym API"), + servers( + (url = "/api", description = "Main Nym Api Server"), + (url = "/", description = "Auxiliary Nym Api Instances"), + (url = "/", description = "Local Development Server") + ), tags(), components(schemas( models::CirculatingSupplyResponse,