diff --git a/Cargo.lock b/Cargo.lock index 7fef1012b5f..6173507dd7a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4890,6 +4890,7 @@ dependencies = [ "serde_json", "sha2 0.10.8", "tendermint 0.40.1", + "tendermint-rpc", "thiserror 2.0.12", "time", "ts-rs", @@ -11241,6 +11242,24 @@ dependencies = [ "serde", ] +[[package]] +name = "validator-status-check" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap", + "comfy-table", + "nym-bin-common", + "nym-network-defaults", + "nym-validator-client", + "serde", + "serde_json", + "strum 0.26.3", + "time", + "tokio", + "tracing", +] + [[package]] name = "valuable" version = "0.1.1" diff --git a/Cargo.toml b/Cargo.toml index 1cae64acf16..69ed0c8a36e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -137,7 +137,7 @@ members = [ "tools/internal/testnet-manager", "tools/internal/testnet-manager", "tools/internal/testnet-manager/dkg-bypass-contract", - "tools/internal/testnet-manager/dkg-bypass-contract", + "tools/internal/testnet-manager/dkg-bypass-contract", "tools/internal/validator-status-check", "tools/nym-cli", "tools/nym-id-cli", "tools/nym-nr-query", @@ -446,4 +446,4 @@ dbg_macro = "deny" exit = "deny" panic = "deny" unimplemented = "deny" -unreachable = "deny" \ No newline at end of file +unreachable = "deny" 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 7011a1904a6..a9b1bc54d52 100644 --- a/common/client-libs/validator-client/src/nym_api/mod.rs +++ b/common/client-libs/validator-client/src/nym_api/mod.rs @@ -12,8 +12,9 @@ use nym_api_requests::ecash::models::{ }; use nym_api_requests::ecash::VerificationKeyResponse; use nym_api_requests::models::{ - AnnotationResponse, ApiHealthResponse, LegacyDescribedMixNode, NodePerformanceResponse, - NodeRefreshBody, NymNodeDescription, PerformanceHistoryResponse, RewardedSetResponse, + AnnotationResponse, ApiHealthResponse, BinaryBuildInformationOwned, ChainStatusResponse, + LegacyDescribedMixNode, NodePerformanceResponse, NodeRefreshBody, NymNodeDescription, + PerformanceHistoryResponse, RewardedSetResponse, }; use nym_api_requests::nym_nodes::{ NodesByAddressesRequestBody, NodesByAddressesResponse, PaginatedCachedNodesResponse, @@ -69,6 +70,19 @@ pub trait NymApiClientExt: ApiClient { .await } + #[instrument(level = "debug", skip(self))] + async fn build_information(&self) -> Result { + self.get_json( + &[ + routes::API_VERSION, + routes::API_STATUS_ROUTES, + routes::BUILD_INFORMATION, + ], + NO_PARAMS, + ) + .await + } + #[deprecated] #[instrument(level = "debug", skip(self))] async fn get_mixnodes(&self) -> Result, NymAPIError> { @@ -1043,6 +1057,15 @@ pub trait NymApiClientExt: ApiClient { ) .await } + + #[instrument(level = "debug", skip(self))] + async fn get_chain_status(&self) -> Result { + self.get_json( + &[routes::API_VERSION, routes::NETWORK, routes::CHAIN_STATUS], + NO_PARAMS, + ) + .await + } } #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] diff --git a/common/client-libs/validator-client/src/nym_api/routes.rs b/common/client-libs/validator-client/src/nym_api/routes.rs index 1dae2254f47..8787264c4a3 100644 --- a/common/client-libs/validator-client/src/nym_api/routes.rs +++ b/common/client-libs/validator-client/src/nym_api/routes.rs @@ -49,6 +49,8 @@ pub mod nym_nodes { pub const STATUS_ROUTES: &str = "status"; pub const API_STATUS_ROUTES: &str = "api-status"; pub const HEALTH: &str = "health"; +pub const BUILD_INFORMATION: &str = "build-information"; + pub const MIXNODE: &str = "mixnode"; pub const GATEWAY: &str = "gateway"; pub const NYM_NODES: &str = "nym-nodes"; @@ -70,4 +72,5 @@ pub const SUBMIT_NODE: &str = "submit-node-monitoring-results"; pub const SERVICE_PROVIDERS: &str = "services"; pub const DETAILS: &str = "details"; +pub const CHAIN_STATUS: &str = "chain-status"; pub const NETWORK: &str = "network"; diff --git a/nym-api/nym-api-requests/Cargo.toml b/nym-api/nym-api-requests/Cargo.toml index 9ca0fe6ea47..5c9ee287b66 100644 --- a/nym-api/nym-api-requests/Cargo.toml +++ b/nym-api/nym-api-requests/Cargo.toml @@ -17,6 +17,7 @@ humantime-serde = { workspace = true } serde_json = { workspace = true } sha2.workspace = true tendermint = { workspace = true } +tendermint-rpc = { workspace = true } thiserror.workspace = true time = { workspace = true, features = ["serde", "parsing", "formatting"] } ts-rs = { workspace = true, optional = true } diff --git a/nym-api/nym-api-requests/src/models.rs b/nym-api/nym-api-requests/src/models.rs index 2558613c8ca..a39b6f25c78 100644 --- a/nym-api/nym-api-requests/src/models.rs +++ b/nym-api/nym-api-requests/src/models.rs @@ -27,9 +27,7 @@ use nym_network_defaults::{DEFAULT_MIX_LISTENING_PORT, DEFAULT_VERLOC_LISTENING_ use nym_node_requests::api::v1::authenticator::models::Authenticator; use nym_node_requests::api::v1::gateway::models::Wireguard; use nym_node_requests::api::v1::ip_packet_router::models::IpPacketRouter; -use nym_node_requests::api::v1::node::models::{ - AuxiliaryDetails, BinaryBuildInformationOwned, NodeRoles, -}; +use nym_node_requests::api::v1::node::models::{AuxiliaryDetails, NodeRoles}; use schemars::gen::SchemaGenerator; use schemars::schema::{InstanceType, Schema, SchemaObject}; use schemars::JsonSchema; @@ -43,6 +41,8 @@ use thiserror::Error; use time::{Date, OffsetDateTime}; use utoipa::{IntoParams, ToResponse, ToSchema}; +pub use nym_node_requests::api::v1::node::models::BinaryBuildInformationOwned; + #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] pub struct RequestError { message: String, @@ -1215,6 +1215,7 @@ impl From for WireguardDetails { #[derive(Clone, Copy, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] pub struct ApiHealthResponse { pub status: ApiStatus, + #[serde(default)] pub chain_status: ChainStatus, pub uptime: u64, } @@ -1225,10 +1226,11 @@ pub enum ApiStatus { Up, } -#[derive(Clone, Copy, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] +#[derive(Clone, Copy, Debug, Serialize, Deserialize, Default, schemars::JsonSchema, ToSchema)] #[serde(rename_all = "snake_case")] pub enum ChainStatus { Synced, + #[default] Unknown, Stalled { #[serde( @@ -1428,7 +1430,297 @@ impl From for RewardedSetResponse } } +#[derive(Clone, Serialize, Deserialize, JsonSchema, ToSchema)] +pub struct ChainStatusResponse { + pub connected_nyxd: String, + pub status: DetailedChainStatus, +} + +#[derive(Clone, Serialize, Deserialize, JsonSchema, ToSchema)] +pub struct DetailedChainStatus { + pub abci: crate::models::tendermint_types::AbciInfo, + pub latest_block: BlockInfo, +} + +#[derive(Clone, Serialize, Deserialize, JsonSchema, ToSchema)] +pub struct BlockInfo { + pub block_id: BlockId, + pub block: FullBlockInfo, + // if necessary we might put block data here later too +} + +impl From for BlockInfo { + fn from(value: tendermint_rpc::endpoint::block::Response) -> Self { + BlockInfo { + block_id: value.block_id.into(), + block: FullBlockInfo { + header: value.block.header.into(), + }, + } + } +} + +#[derive(Clone, Serialize, Deserialize, JsonSchema, ToSchema)] +pub struct FullBlockInfo { + pub header: BlockHeader, +} + +// copy tendermint types definitions whilst deriving schema types on them and dropping unwanted fields +pub mod tendermint_types { + use schemars::JsonSchema; + use serde::{Deserialize, Serialize}; + use tendermint::abci::response::Info; + use tendermint::block::header::Version; + use tendermint::{block, Hash}; + use utoipa::ToSchema; + + #[derive(Clone, Serialize, Deserialize, JsonSchema, ToSchema)] + pub struct AbciInfo { + /// Some arbitrary information. + pub data: String, + + /// The application software semantic version. + pub version: String, + + /// The application protocol version. + pub app_version: u64, + + /// The latest block for which the app has called [`Commit`]. + pub last_block_height: u64, + + /// The latest result of [`Commit`]. + pub last_block_app_hash: String, + } + + impl From for AbciInfo { + fn from(value: Info) -> Self { + AbciInfo { + data: value.data, + version: value.version, + app_version: value.app_version, + last_block_height: value.last_block_height.value(), + last_block_app_hash: value.last_block_app_hash.to_string(), + } + } + } + + /// `Version` contains the protocol version for the blockchain and the + /// application. + /// + /// + #[derive( + Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema, ToSchema, + )] + pub struct HeaderVersion { + /// Block version + pub block: u64, + + /// App version + pub app: u64, + } + + impl From for HeaderVersion { + fn from(value: Version) -> Self { + HeaderVersion { + block: value.block, + app: value.app, + } + } + } + + /// Block identifiers which contain two distinct Merkle roots of the block, + /// as well as the number of parts in the block. + /// + /// + /// + /// Default implementation is an empty Id as defined by the Go implementation in + /// . + /// + /// If the Hash is empty in BlockId, the BlockId should be empty (encoded to None). + /// This is implemented outside of this struct. Use the Default trait to check for an empty BlockId. + /// See: + #[derive( + Serialize, + Deserialize, + Copy, + Clone, + Debug, + Default, + Hash, + Eq, + PartialEq, + PartialOrd, + Ord, + JsonSchema, + ToSchema, + )] + pub struct BlockId { + /// The block's main hash is the Merkle root of all the fields in the + /// block header. + #[schemars(with = "String")] + #[schema(value_type = String)] + pub hash: Hash, + + /// Parts header (if available) is used for secure gossipping of the block + /// during consensus. It is the Merkle root of the complete serialized block + /// cut into parts. + /// + /// PartSet is used to split a byteslice of data into parts (pieces) for + /// transmission. By splitting data into smaller parts and computing a + /// Merkle root hash on the list, you can verify that a part is + /// legitimately part of the complete data, and the part can be forwarded + /// to other peers before all the parts are known. In short, it's a fast + /// way to propagate a large file over a gossip network. + /// + /// + /// + /// PartSetHeader in protobuf is defined as never nil using the gogoproto + /// annotations. This does not translate to Rust, but we can indicate this + /// in the domain type. + pub part_set_header: PartSetHeader, + } + + impl From for BlockId { + fn from(value: block::Id) -> Self { + BlockId { + hash: value.hash, + part_set_header: value.part_set_header.into(), + } + } + } + + /// Block parts header + #[derive( + Clone, + Copy, + Debug, + Default, + Hash, + Eq, + PartialEq, + PartialOrd, + Ord, + Deserialize, + Serialize, + JsonSchema, + ToSchema, + )] + #[non_exhaustive] + pub struct PartSetHeader { + /// Number of parts in this block + pub total: u32, + + /// Hash of the parts set header, + #[schemars(with = "String")] + #[schema(value_type = String)] + pub hash: Hash, + } + + impl From for PartSetHeader { + fn from(value: block::parts::Header) -> Self { + PartSetHeader { + total: value.total, + hash: value.hash, + } + } + } + + /// Block `Header` values contain metadata about the block and about the + /// consensus, as well as commitments to the data in the current block, the + /// previous block, and the results returned by the application. + /// + /// + #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)] + pub struct BlockHeader { + /// Header version + pub version: HeaderVersion, + + /// Chain ID + pub chain_id: String, + + /// Current block height + pub height: u64, + + /// Current timestamp + #[schemars(with = "String")] + #[schema(value_type = String)] + pub time: tendermint::Time, + + /// Previous block info + pub last_block_id: Option, + + /// Commit from validators from the last block + #[schemars(with = "Option")] + #[schema(value_type = Option)] + pub last_commit_hash: Option, + + /// Merkle root of transaction hashes + #[schemars(with = "Option")] + #[schema(value_type = Option)] + pub data_hash: Option, + + /// Validators for the current block + #[schemars(with = "String")] + #[schema(value_type = String)] + pub validators_hash: Hash, + + /// Validators for the next block + #[schemars(with = "String")] + #[schema(value_type = String)] + pub next_validators_hash: Hash, + + /// Consensus params for the current block + #[schemars(with = "String")] + #[schema(value_type = String)] + pub consensus_hash: Hash, + + /// State after txs from the previous block + #[schemars(with = "String")] + #[schema(value_type = String)] + pub app_hash: Hash, + + /// Root hash of all results from the txs from the previous block + #[schemars(with = "Option")] + #[schema(value_type = Option)] + pub last_results_hash: Option, + + /// Hash of evidence included in the block + #[schemars(with = "Option")] + #[schema(value_type = Option)] + pub evidence_hash: Option, + + /// Original proposer of the block + #[serde(with = "nym_serde_helpers::hex")] + #[schemars(with = "String")] + #[schema(value_type = String)] + pub proposer_address: Vec, + } + + impl From for BlockHeader { + fn from(value: block::Header) -> Self { + BlockHeader { + version: value.version.into(), + chain_id: value.chain_id.to_string(), + height: value.height.value(), + time: value.time, + last_block_id: value.last_block_id.map(Into::into), + last_commit_hash: value.last_commit_hash, + data_hash: value.data_hash, + validators_hash: value.validators_hash, + next_validators_hash: value.next_validators_hash, + consensus_hash: value.consensus_hash, + app_hash: Hash::try_from(value.app_hash.as_bytes().to_vec()).unwrap_or_default(), + last_results_hash: value.last_results_hash, + evidence_hash: value.evidence_hash, + proposer_address: value.proposer_address.as_bytes().to_vec(), + } + } + } +} + +use crate::models::tendermint_types::{BlockHeader, BlockId}; pub use config_score::*; + pub mod config_score { use nym_contracts_common::NaiveFloat; use serde::{Deserialize, Serialize}; diff --git a/nym-api/src/network/handlers.rs b/nym-api/src/network/handlers.rs index a308a9fa824..380e78e736b 100644 --- a/nym-api/src/network/handlers.rs +++ b/nym-api/src/network/handlers.rs @@ -1,11 +1,12 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::network::models::{ChainStatusResponse, ContractInformation, NetworkDetails}; +use crate::network::models::{ContractInformation, NetworkDetails}; use crate::node_status_api::models::AxumResult; use crate::support::http::state::AppState; use axum::extract::State; use axum::{extract, Json, Router}; +use nym_api_requests::models::ChainStatusResponse; use nym_contracts_common::ContractBuildInformation; use std::collections::HashMap; use tower_http::compression::CompressionLayer; diff --git a/nym-api/src/network/models.rs b/nym-api/src/network/models.rs index 066b473921e..1819ce024d8 100644 --- a/nym-api/src/network/models.rs +++ b/nym-api/src/network/models.rs @@ -1,9 +1,7 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::network::models::tendermint_types::{AbciInfo, BlockHeader, BlockId}; use nym_config::defaults::NymNetworkDetails; -use nym_validator_client::nyxd::BlockResponse; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use utoipa::ToSchema; @@ -29,292 +27,3 @@ pub struct ContractInformation { pub(crate) address: Option, pub(crate) details: Option, } - -#[derive(Clone, Serialize, Deserialize, JsonSchema, ToSchema)] -pub struct ChainStatusResponse { - pub connected_nyxd: String, - pub status: ChainStatus, -} - -#[derive(Clone, Serialize, Deserialize, JsonSchema, ToSchema)] -pub struct ChainStatus { - pub abci: AbciInfo, - pub latest_block: BlockInfo, -} - -#[derive(Clone, Serialize, Deserialize, JsonSchema, ToSchema)] -pub struct BlockInfo { - pub block_id: BlockId, - pub block: FullBlockInfo, - // if necessary we might put block data here later too -} - -impl From for BlockInfo { - fn from(value: BlockResponse) -> Self { - BlockInfo { - block_id: value.block_id.into(), - block: FullBlockInfo { - header: value.block.header.into(), - }, - } - } -} - -#[derive(Clone, Serialize, Deserialize, JsonSchema, ToSchema)] -pub struct FullBlockInfo { - pub header: BlockHeader, -} - -// copy tendermint types definitions whilst deriving schema types on them and dropping unwanted fields -pub mod tendermint_types { - use nym_validator_client::nyxd::Hash; - use schemars::JsonSchema; - use serde::{Deserialize, Serialize}; - use tendermint::abci::response::Info; - use tendermint::block; - use tendermint::block::header::Version; - use utoipa::ToSchema; - - #[derive(Clone, Serialize, Deserialize, JsonSchema, ToSchema)] - pub struct AbciInfo { - /// Some arbitrary information. - pub data: String, - - /// The application software semantic version. - pub version: String, - - /// The application protocol version. - pub app_version: u64, - - /// The latest block for which the app has called [`Commit`]. - pub last_block_height: u64, - - /// The latest result of [`Commit`]. - pub last_block_app_hash: String, - } - - impl From for AbciInfo { - fn from(value: Info) -> Self { - AbciInfo { - data: value.data, - version: value.version, - app_version: value.app_version, - last_block_height: value.last_block_height.value(), - last_block_app_hash: value.last_block_app_hash.to_string(), - } - } - } - - /// `Version` contains the protocol version for the blockchain and the - /// application. - /// - /// - #[derive( - Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema, ToSchema, - )] - pub struct HeaderVersion { - /// Block version - pub block: u64, - - /// App version - pub app: u64, - } - - impl From for HeaderVersion { - fn from(value: Version) -> Self { - HeaderVersion { - block: value.block, - app: value.app, - } - } - } - - /// Block identifiers which contain two distinct Merkle roots of the block, - /// as well as the number of parts in the block. - /// - /// - /// - /// Default implementation is an empty Id as defined by the Go implementation in - /// . - /// - /// If the Hash is empty in BlockId, the BlockId should be empty (encoded to None). - /// This is implemented outside of this struct. Use the Default trait to check for an empty BlockId. - /// See: - #[derive( - Serialize, - Deserialize, - Copy, - Clone, - Debug, - Default, - Hash, - Eq, - PartialEq, - PartialOrd, - Ord, - JsonSchema, - ToSchema, - )] - pub struct BlockId { - /// The block's main hash is the Merkle root of all the fields in the - /// block header. - #[schemars(with = "String")] - #[schema(value_type = String)] - pub hash: Hash, - - /// Parts header (if available) is used for secure gossipping of the block - /// during consensus. It is the Merkle root of the complete serialized block - /// cut into parts. - /// - /// PartSet is used to split a byteslice of data into parts (pieces) for - /// transmission. By splitting data into smaller parts and computing a - /// Merkle root hash on the list, you can verify that a part is - /// legitimately part of the complete data, and the part can be forwarded - /// to other peers before all the parts are known. In short, it's a fast - /// way to propagate a large file over a gossip network. - /// - /// - /// - /// PartSetHeader in protobuf is defined as never nil using the gogoproto - /// annotations. This does not translate to Rust, but we can indicate this - /// in the domain type. - pub part_set_header: PartSetHeader, - } - - impl From for BlockId { - fn from(value: block::Id) -> Self { - BlockId { - hash: value.hash, - part_set_header: value.part_set_header.into(), - } - } - } - - /// Block parts header - #[derive( - Clone, - Copy, - Debug, - Default, - Hash, - Eq, - PartialEq, - PartialOrd, - Ord, - Deserialize, - Serialize, - JsonSchema, - ToSchema, - )] - #[non_exhaustive] - pub struct PartSetHeader { - /// Number of parts in this block - pub total: u32, - - /// Hash of the parts set header, - #[schemars(with = "String")] - #[schema(value_type = String)] - pub hash: Hash, - } - - impl From for PartSetHeader { - fn from(value: block::parts::Header) -> Self { - PartSetHeader { - total: value.total, - hash: value.hash, - } - } - } - - /// Block `Header` values contain metadata about the block and about the - /// consensus, as well as commitments to the data in the current block, the - /// previous block, and the results returned by the application. - /// - /// - #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)] - pub struct BlockHeader { - /// Header version - pub version: HeaderVersion, - - /// Chain ID - pub chain_id: String, - - /// Current block height - pub height: u64, - - /// Current timestamp - #[schemars(with = "String")] - #[schema(value_type = String)] - pub time: tendermint::Time, - - /// Previous block info - pub last_block_id: Option, - - /// Commit from validators from the last block - #[schemars(with = "Option")] - #[schema(value_type = Option)] - pub last_commit_hash: Option, - - /// Merkle root of transaction hashes - #[schemars(with = "Option")] - #[schema(value_type = Option)] - pub data_hash: Option, - - /// Validators for the current block - #[schemars(with = "String")] - #[schema(value_type = String)] - pub validators_hash: Hash, - - /// Validators for the next block - #[schemars(with = "String")] - #[schema(value_type = String)] - pub next_validators_hash: Hash, - - /// Consensus params for the current block - #[schemars(with = "String")] - #[schema(value_type = String)] - pub consensus_hash: Hash, - - /// State after txs from the previous block - #[schemars(with = "String")] - #[schema(value_type = String)] - pub app_hash: Hash, - - /// Root hash of all results from the txs from the previous block - #[schemars(with = "Option")] - #[schema(value_type = Option)] - pub last_results_hash: Option, - - /// Hash of evidence included in the block - #[schemars(with = "Option")] - #[schema(value_type = Option)] - pub evidence_hash: Option, - - /// Original proposer of the block - #[serde(with = "nym_serde_helpers::hex")] - #[schemars(with = "String")] - #[schema(value_type = String)] - pub proposer_address: Vec, - } - - impl From for BlockHeader { - fn from(value: block::Header) -> Self { - BlockHeader { - version: value.version.into(), - chain_id: value.chain_id.to_string(), - height: value.height.value(), - time: value.time, - last_block_id: value.last_block_id.map(Into::into), - last_commit_hash: value.last_commit_hash, - data_hash: value.data_hash, - validators_hash: value.validators_hash, - next_validators_hash: value.next_validators_hash, - consensus_hash: value.consensus_hash, - app_hash: Hash::try_from(value.app_hash.as_bytes().to_vec()).unwrap_or_default(), - last_results_hash: value.last_results_hash, - evidence_hash: value.evidence_hash, - proposer_address: value.proposer_address.as_bytes().to_vec(), - } - } - } -} diff --git a/nym-api/src/support/http/state.rs b/nym-api/src/support/http/state.rs index 6bda58cb6c6..224db98e538 100644 --- a/nym-api/src/support/http/state.rs +++ b/nym-api/src/support/http/state.rs @@ -3,7 +3,7 @@ use crate::circulating_supply_api::cache::CirculatingSupplyCache; use crate::ecash::state::EcashState; -use crate::network::models::{ChainStatus, NetworkDetails}; +use crate::network::models::NetworkDetails; use crate::node_describe_cache::DescribedNodes; use crate::node_status_api::handlers::unstable; use crate::node_status_api::models::AxumErrorResponse; @@ -15,7 +15,9 @@ use crate::support::caching::Cache; use crate::support::nyxd::Client; use crate::support::storage; use axum::extract::FromRef; -use nym_api_requests::models::{GatewayBondAnnotated, MixNodeBondAnnotated, NodeAnnotation}; +use nym_api_requests::models::{ + DetailedChainStatus, GatewayBondAnnotated, MixNodeBondAnnotated, NodeAnnotation, +}; use nym_mixnet_contract_common::NodeId; use nym_task::TaskManager; use nym_topology::CachedEpochRewardedSet; @@ -151,7 +153,7 @@ impl ChainStatusCache { struct ChainStatusCacheInner { last_refreshed_at: OffsetDateTime, - cache_value: ChainStatus, + cache_value: DetailedChainStatus, } impl ChainStatusCacheInner { @@ -167,7 +169,7 @@ impl ChainStatusCache { pub(crate) async fn get_or_refresh( &self, client: &Client, - ) -> Result { + ) -> Result { if let Some(cached) = self.check_cache().await { return Ok(cached); } @@ -175,7 +177,7 @@ impl ChainStatusCache { self.refresh(client).await } - async fn check_cache(&self) -> Option { + async fn check_cache(&self) -> Option { let guard = self.inner.read().await; let inner = guard.as_ref()?; if inner.is_valid(self.cache_ttl) { @@ -184,7 +186,7 @@ impl ChainStatusCache { None } - async fn refresh(&self, client: &Client) -> Result { + async fn refresh(&self, client: &Client) -> Result { // 1. attempt to get write lock permit let mut guard = self.inner.write().await; @@ -201,7 +203,7 @@ impl ChainStatusCache { .block_info(abci.last_block_height.value() as u32) .await?; - let status = ChainStatus { + let status = DetailedChainStatus { abci: abci.into(), latest_block: block.into(), }; diff --git a/nym-wallet/Cargo.lock b/nym-wallet/Cargo.lock index 35379a35efd..a42892c03dd 100644 --- a/nym-wallet/Cargo.lock +++ b/nym-wallet/Cargo.lock @@ -3293,6 +3293,7 @@ dependencies = [ "serde_json", "sha2 0.10.8", "tendermint 0.40.1", + "tendermint-rpc", "thiserror 2.0.11", "time", "utoipa", diff --git a/tools/internal/validator-status-check/Cargo.toml b/tools/internal/validator-status-check/Cargo.toml new file mode 100644 index 00000000000..99eb1826215 --- /dev/null +++ b/tools/internal/validator-status-check/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "validator-status-check" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true + +[dependencies] +anyhow = { workspace = true } +clap = { workspace = true, features = ["cargo", "derive"] } +comfy-table = { workspace = true } +tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } +strum = { workspace = true, features = ["derive"] } +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } +tracing = { workspace = true } +time = { workspace = true } + + +nym-validator-client = { path = "../../../common/client-libs/validator-client" } +nym-bin-common = { path = "../../../common/bin-common", features = ["output_format", "basic_tracing"] } +nym-network-defaults = { path = "../../../common/network-defaults" } + +[lints] +workspace = true diff --git a/tools/internal/validator-status-check/src/commands/build_info.rs b/tools/internal/validator-status-check/src/commands/build_info.rs new file mode 100644 index 00000000000..7b682aac122 --- /dev/null +++ b/tools/internal/validator-status-check/src/commands/build_info.rs @@ -0,0 +1,15 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use nym_bin_common::bin_info_owned; +use nym_bin_common::output_format::OutputFormat; + +#[derive(clap::Args, Debug)] +pub(crate) struct Args { + #[clap(short, long, default_value_t = OutputFormat::default())] + output: OutputFormat, +} + +pub(crate) fn execute(args: Args) { + println!("{}", args.output.format(&bin_info_owned!())) +} diff --git a/tools/internal/validator-status-check/src/commands/check_network.rs b/tools/internal/validator-status-check/src/commands/check_network.rs new file mode 100644 index 00000000000..e388e90c742 --- /dev/null +++ b/tools/internal/validator-status-check/src/commands/check_network.rs @@ -0,0 +1,73 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::helpers::{get_known_dealers, get_signer_status}; +use crate::models::SignerStatus; +use comfy_table::Table; +use nym_bin_common::output_format::OutputFormat; +use serde::{Deserialize, Serialize}; +use std::fmt::{Display, Formatter}; + +#[derive(clap::Args, Debug)] +pub(crate) struct Args { + #[clap(short, long, default_value_t = OutputFormat::default())] + output: OutputFormat, +} + +#[derive(Serialize, Deserialize)] +struct NetworkStatus { + available_api_nodes: f64, + available_rpc_nodes: f64, + detailed: Vec, +} + +impl From> for NetworkStatus { + fn from(value: Vec) -> Self { + let nodes = value.len() as f64; + let api = value.iter().filter(|s| s.api_up()).count() as f64; + let rpc = value.iter().filter(|s| s.rpc_up()).count() as f64; + + NetworkStatus { + available_api_nodes: api / nodes, + available_rpc_nodes: rpc / nodes, + detailed: value, + } + } +} + +impl Display for NetworkStatus { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + writeln!( + f, + "available signers: {:.2}%, available rpc nodes: {:.2}%", + self.available_api_nodes, self.available_rpc_nodes + )?; + + let mut table = Table::new(); + table.set_header(vec![ + "signer", + "api version", + "rpc status", + "rpc endpoint", + "abci version", + ]); + for signer in &self.detailed { + table.add_row(signer.to_table_row()); + } + write!(f, "{table}") + } +} + +pub(crate) async fn execute(args: Args) -> anyhow::Result<()> { + let dealers = get_known_dealers().await?; + + let mut signers = Vec::new(); + for dealer in dealers { + signers.push(get_signer_status(&dealer.announce_address).await) + } + + let out = args.output.format(&NetworkStatus::from(signers)); + println!("{out}"); + + Ok(()) +} diff --git a/tools/internal/validator-status-check/src/commands/check_signer.rs b/tools/internal/validator-status-check/src/commands/check_signer.rs new file mode 100644 index 00000000000..ee50e3ab046 --- /dev/null +++ b/tools/internal/validator-status-check/src/commands/check_signer.rs @@ -0,0 +1,21 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::helpers::get_signer_status; +use nym_bin_common::output_format::OutputFormat; + +#[derive(clap::Args, Debug)] +pub(crate) struct Args { + #[clap(short, long, default_value_t = OutputFormat::default())] + output: OutputFormat, + + /// api address of the specified signer + #[clap(long)] + signer: String, +} + +pub(crate) async fn execute(args: Args) -> anyhow::Result<()> { + let out = args.output.format(&get_signer_status(&args.signer).await); + println!("{out}"); + Ok(()) +} diff --git a/tools/internal/validator-status-check/src/commands/mod.rs b/tools/internal/validator-status-check/src/commands/mod.rs new file mode 100644 index 00000000000..abf0f5c8b16 --- /dev/null +++ b/tools/internal/validator-status-check/src/commands/mod.rs @@ -0,0 +1,51 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use clap::{Parser, Subcommand}; +use nym_bin_common::bin_info; +use std::path::PathBuf; +use std::sync::OnceLock; + +mod build_info; +mod check_network; +mod check_signer; + +fn pretty_build_info_static() -> &'static str { + static PRETTY_BUILD_INFORMATION: OnceLock = OnceLock::new(); + PRETTY_BUILD_INFORMATION.get_or_init(|| bin_info!().pretty_print()) +} + +#[derive(Parser, Debug)] +#[clap(author = "Nymtech", version, long_version = pretty_build_info_static(), about)] +pub struct Cli { + /// Path pointing to an env file that configures the CLI. + #[clap(short, long)] + pub(crate) config_env_file: Option, + + #[clap(subcommand)] + command: Commands, +} + +#[derive(Subcommand, Debug)] +pub(crate) enum Commands { + /// Check status of an individual signer + CheckSigner(check_signer::Args), + + /// Check status of all signers + CheckNetwork(check_network::Args), + + /// Show build information of this binary + BuildInfo(build_info::Args), +} + +impl Cli { + pub async fn execute(self) -> anyhow::Result<()> { + match self.command { + Commands::CheckSigner(args) => check_signer::execute(args).await?, + Commands::CheckNetwork(args) => check_network::execute(args).await?, + Commands::BuildInfo(args) => build_info::execute(args), + } + + Ok(()) + } +} diff --git a/tools/internal/validator-status-check/src/helpers.rs b/tools/internal/validator-status-check/src/helpers.rs new file mode 100644 index 00000000000..feb4dd488e1 --- /dev/null +++ b/tools/internal/validator-status-check/src/helpers.rs @@ -0,0 +1,39 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::models::SignerStatus; +use anyhow::bail; +use nym_network_defaults::NymNetworkDetails; +use nym_validator_client::nyxd::contract_traits::dkg_query_client::DealerDetails; +use nym_validator_client::nyxd::contract_traits::PagedDkgQueryClient; +use nym_validator_client::nyxd::Config; +use nym_validator_client::QueryHttpRpcNyxdClient; +use tracing::info; + +async fn get_query_client() -> anyhow::Result { + let network = NymNetworkDetails::new_from_env(); + + let Some(endpoint_info) = network.endpoints.first() else { + bail!("no known rpc endpoints available") + }; + + let config = Config::try_from_nym_network_details(&network)?; + Ok(QueryHttpRpcNyxdClient::connect( + config, + endpoint_info.nyxd_url.as_str(), + )?) +} + +pub(crate) async fn get_known_dealers() -> anyhow::Result> { + let client = get_query_client().await?; + Ok(client.get_all_current_dealers().await?) +} + +pub(crate) async fn get_signer_status(raw_api_endpoint: &str) -> SignerStatus { + info!("attempting to get signer status of {raw_api_endpoint}..."); + let mut status = SignerStatus::new(raw_api_endpoint.to_string()); + + status.try_update_api_version().await; + status.try_update_rpc_status().await; + status +} diff --git a/tools/internal/validator-status-check/src/main.rs b/tools/internal/validator-status-check/src/main.rs new file mode 100644 index 00000000000..668aa5dd2b6 --- /dev/null +++ b/tools/internal/validator-status-check/src/main.rs @@ -0,0 +1,24 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::commands::Cli; +use clap::Parser; +use nym_bin_common::logging::setup_tracing_logger; +use nym_network_defaults::setup_env; +use tracing::trace; + +mod commands; +mod helpers; +mod models; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + let cli = Cli::parse(); + trace!("args: {cli:#?}"); + + setup_env(cli.config_env_file.as_ref()); + setup_tracing_logger(); + + cli.execute().await?; + Ok(()) +} diff --git a/tools/internal/validator-status-check/src/models.rs b/tools/internal/validator-status-check/src/models.rs new file mode 100644 index 00000000000..12b8337f5d1 --- /dev/null +++ b/tools/internal/validator-status-check/src/models.rs @@ -0,0 +1,222 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use nym_validator_client::client::NymApiClientExt; +use nym_validator_client::NymApiClient; +use serde::{Deserialize, Serialize}; +use std::fmt::Display; +use strum::{Display, EnumProperty}; +use time::{Duration, OffsetDateTime}; +use tracing::error; + +#[derive(Serialize, Deserialize)] +pub(crate) struct SignerStatus { + api_endpoint: String, + api_version: ApiVersion, + rpc_status: RpcStatus, + used_rpc_endpoint: RpcEndpoint, + abci_version: AbciVersion, +} + +impl Display for SignerStatus { + fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { + writeln!(f, "api_endpoint: {}", self.api_endpoint)?; + writeln!(f, "api_version: {}", self.api_version)?; + writeln!(f, "rpc_status: {}", self.rpc_status)?; + writeln!(f, "used_rpc_endpoint: {}", self.used_rpc_endpoint)?; + writeln!(f, "abci_version: {}", self.abci_version)?; + Ok(()) + } +} + +impl SignerStatus { + pub(crate) fn new(api_endpoint: String) -> Self { + SignerStatus { + api_endpoint, + api_version: Default::default(), + rpc_status: Default::default(), + used_rpc_endpoint: Default::default(), + abci_version: Default::default(), + } + } + + pub(crate) fn api_up(&self) -> bool { + matches!(self.api_version, ApiVersion::Available { .. }) + } + + pub(crate) fn rpc_up(&self) -> bool { + matches!(self.rpc_status, RpcStatus::Up) + } + + fn build_api_client(&self) -> Option { + let api_endpoint = match self.api_endpoint.as_str().parse() { + Ok(endpoint) => endpoint, + Err(err) => { + error!("{} is not a valid api endpoint: {err}", self.api_endpoint); + return None; + } + }; + + Some(NymApiClient::new(api_endpoint)) + } + + pub(crate) async fn try_update_api_version(&mut self) { + let Some(client) = self.build_api_client() else { + return; + }; + match client.nym_api.build_information().await { + Ok(build_info) => { + self.api_version = ApiVersion::Available { + version: build_info.build_version, + }; + } + Err(err) => { + error!( + "failed to retrieve build information of {}: {err}", + self.api_endpoint + ) + } + } + } + + pub(crate) async fn try_update_rpc_status(&mut self) { + let Some(client) = self.build_api_client() else { + return; + }; + + match client.nym_api.get_chain_status().await { + Ok(chain_status) => { + self.used_rpc_endpoint = RpcEndpoint(chain_status.connected_nyxd); + let last_block = + OffsetDateTime::from(chain_status.status.latest_block.block.header.time); + let now = OffsetDateTime::now_utc(); + let diff = now - last_block; + if diff < Duration::minutes(2) { + self.rpc_status = RpcStatus::Up + } else { + self.rpc_status = RpcStatus::Down + } + self.abci_version = AbciVersion::Available { + version: chain_status.status.abci.version, + } + } + Err(err) => { + error!( + "failed to retrieve chain status from {}: {err}", + self.api_endpoint + ); + } + } + } + + pub(crate) fn to_table_row(&self) -> Vec { + vec![ + self.api_endpoint.to_string(), + self.api_version.as_cell(), + self.rpc_status.as_cell(), + self.used_rpc_endpoint.as_cell(), + self.abci_version.as_cell(), + ] + } +} + +#[derive(Serialize, Deserialize, Default)] +struct RpcEndpoint(String); + +impl Display for RpcEndpoint { + fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { + self.0.fmt(f) + } +} + +impl RpcEndpoint { + fn as_cell(&self) -> String { + if self.0.contains("localhost") || self.0.contains("127.0.0.1") { + format!("✅ {}", self.0) + } else if self.0.contains("nymtech") { + format!("❗ {}", self.0) + } else if self.0.is_empty() { + "⚠️ unknown".to_string() + } else { + format!("⚠️ {}", self.0) + } + } +} + +#[derive( + Clone, Default, PartialOrd, PartialEq, Ord, Eq, Display, EnumProperty, Serialize, Deserialize, +)] +#[strum(serialize_all = "snake_case")] +enum AbciVersion { + #[strum(props(emoji = "✅"))] + #[strum(to_string = "{version}")] + Available { version: String }, + + #[strum(props(emoji = "❗"))] + #[default] + Unavailable, +} + +impl AbciVersion { + // SAFETY: every variant has a `emoji` prop defined + #[allow(clippy::unwrap_used)] + fn as_cell(&self) -> String { + format!("{} {}", self.get_str("emoji").unwrap(), self) + } +} + +#[derive( + Clone, Default, PartialOrd, PartialEq, Ord, Eq, Display, EnumProperty, Serialize, Deserialize, +)] +#[strum(serialize_all = "snake_case")] +enum ApiVersion { + #[strum(props(emoji = "✅"))] + #[strum(to_string = "{version}")] + Available { version: String }, + + #[strum(props(emoji = "❗"))] + #[default] + Unavailable, +} + +impl ApiVersion { + // SAFETY: every variant has a `emoji` prop defined + #[allow(clippy::unwrap_used)] + fn as_cell(&self) -> String { + format!("{} {}", self.get_str("emoji").unwrap(), self) + } +} + +#[derive( + Copy, + Clone, + Default, + PartialOrd, + PartialEq, + Ord, + Eq, + Display, + EnumProperty, + Serialize, + Deserialize, +)] +#[strum(serialize_all = "snake_case")] +enum RpcStatus { + #[strum(props(emoji = "❗"))] + Down, + + #[strum(props(emoji = "✅"))] + Up, + + #[strum(props(emoji = "⚠️"))] + #[default] + Unknown, +} + +impl RpcStatus { + // SAFETY: every variant has a `emoji` prop defined + #[allow(clippy::unwrap_used)] + fn as_cell(&self) -> String { + format!("{} {}", self.get_str("emoji").unwrap(), self) + } +}