introduce internal tool for checking signer status (#5598)

* introduce internal tool for checking signer status

* fixed nym-api types due to moving values around

* added abci version
This commit is contained in:
Jędrzej Stuczyński
2025-03-12 11:02:03 +00:00
committed by GitHub
parent b80a4c8614
commit eca88b0fa4
18 changed files with 833 additions and 307 deletions
Generated
+19
View File
@@ -4896,6 +4896,7 @@ dependencies = [
"serde_json",
"sha2 0.10.8",
"tendermint 0.40.1",
"tendermint-rpc",
"thiserror 2.0.12",
"time",
"ts-rs",
@@ -11264,6 +11265,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"
+2 -2
View File
@@ -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",
@@ -447,4 +447,4 @@ dbg_macro = "deny"
exit = "deny"
panic = "deny"
unimplemented = "deny"
unreachable = "deny"
unreachable = "deny"
@@ -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<BinaryBuildInformationOwned, NymAPIError> {
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<Vec<MixNodeDetails>, NymAPIError> {
@@ -1043,6 +1057,15 @@ pub trait NymApiClientExt: ApiClient {
)
.await
}
#[instrument(level = "debug", skip(self))]
async fn get_chain_status(&self) -> Result<ChainStatusResponse, NymAPIError> {
self.get_json(
&[routes::API_VERSION, routes::NETWORK, routes::CHAIN_STATUS],
NO_PARAMS,
)
.await
}
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
@@ -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";
+1
View File
@@ -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 }
+296 -4
View File
@@ -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<Wireguard> 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<nym_mixnet_contract_common::EpochRewardedSet> 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<tendermint_rpc::endpoint::block::Response> 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<Info> 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.
///
/// <https://github.com/tendermint/spec/blob/d46cd7f573a2c6a2399fcab2cde981330aa63f37/spec/core/data_structures.md#version>
#[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<tendermint::block::header::Version> 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.
///
/// <https://github.com/tendermint/spec/blob/d46cd7f573a2c6a2399fcab2cde981330aa63f37/spec/core/data_structures.md#blockid>
///
/// Default implementation is an empty Id as defined by the Go implementation in
/// <https://github.com/tendermint/tendermint/blob/1635d1339c73ae6a82e062cd2dc7191b029efa14/types/block.go#L1204>.
///
/// 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: <https://github.com/informalsystems/tendermint-rs/issues/663>
#[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.
///
/// <https://github.com/tendermint/tendermint/wiki/Block-Structure#partset>
///
/// 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<block::Id> 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<tendermint::block::parts::Header> 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.
///
/// <https://github.com/tendermint/spec/blob/d46cd7f573a2c6a2399fcab2cde981330aa63f37/spec/core/data_structures.md#header>
#[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<BlockId>,
/// Commit from validators from the last block
#[schemars(with = "Option<String>")]
#[schema(value_type = Option<String>)]
pub last_commit_hash: Option<Hash>,
/// Merkle root of transaction hashes
#[schemars(with = "Option<String>")]
#[schema(value_type = Option<String>)]
pub data_hash: Option<Hash>,
/// 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<String>")]
#[schema(value_type = Option<String>)]
pub last_results_hash: Option<Hash>,
/// Hash of evidence included in the block
#[schemars(with = "Option<String>")]
#[schema(value_type = Option<String>)]
pub evidence_hash: Option<Hash>,
/// Original proposer of the block
#[serde(with = "nym_serde_helpers::hex")]
#[schemars(with = "String")]
#[schema(value_type = String)]
pub proposer_address: Vec<u8>,
}
impl From<block::Header> 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};
+2 -1
View File
@@ -1,11 +1,12 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// 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;
-291
View File
@@ -1,9 +1,7 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// 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<T> {
pub(crate) address: Option<String>,
pub(crate) details: Option<T>,
}
#[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<BlockResponse> 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<Info> 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.
///
/// <https://github.com/tendermint/spec/blob/d46cd7f573a2c6a2399fcab2cde981330aa63f37/spec/core/data_structures.md#version>
#[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<tendermint::block::header::Version> 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.
///
/// <https://github.com/tendermint/spec/blob/d46cd7f573a2c6a2399fcab2cde981330aa63f37/spec/core/data_structures.md#blockid>
///
/// Default implementation is an empty Id as defined by the Go implementation in
/// <https://github.com/tendermint/tendermint/blob/1635d1339c73ae6a82e062cd2dc7191b029efa14/types/block.go#L1204>.
///
/// 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: <https://github.com/informalsystems/tendermint-rs/issues/663>
#[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.
///
/// <https://github.com/tendermint/tendermint/wiki/Block-Structure#partset>
///
/// 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<block::Id> 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<tendermint::block::parts::Header> 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.
///
/// <https://github.com/tendermint/spec/blob/d46cd7f573a2c6a2399fcab2cde981330aa63f37/spec/core/data_structures.md#header>
#[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<BlockId>,
/// Commit from validators from the last block
#[schemars(with = "Option<String>")]
#[schema(value_type = Option<String>)]
pub last_commit_hash: Option<Hash>,
/// Merkle root of transaction hashes
#[schemars(with = "Option<String>")]
#[schema(value_type = Option<String>)]
pub data_hash: Option<Hash>,
/// 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<String>")]
#[schema(value_type = Option<String>)]
pub last_results_hash: Option<Hash>,
/// Hash of evidence included in the block
#[schemars(with = "Option<String>")]
#[schema(value_type = Option<String>)]
pub evidence_hash: Option<Hash>,
/// Original proposer of the block
#[serde(with = "nym_serde_helpers::hex")]
#[schemars(with = "String")]
#[schema(value_type = String)]
pub proposer_address: Vec<u8>,
}
impl From<block::Header> 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(),
}
}
}
}
+9 -7
View File
@@ -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<ChainStatus, AxumErrorResponse> {
) -> Result<DetailedChainStatus, AxumErrorResponse> {
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<ChainStatus> {
async fn check_cache(&self) -> Option<DetailedChainStatus> {
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<ChainStatus, AxumErrorResponse> {
async fn refresh(&self, client: &Client) -> Result<DetailedChainStatus, AxumErrorResponse> {
// 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(),
};
+1
View File
@@ -3293,6 +3293,7 @@ dependencies = [
"serde_json",
"sha2 0.10.8",
"tendermint 0.40.1",
"tendermint-rpc",
"thiserror 2.0.11",
"time",
"utoipa",
@@ -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
@@ -0,0 +1,15 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// 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!()))
}
@@ -0,0 +1,73 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// 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<SignerStatus>,
}
impl From<Vec<SignerStatus>> for NetworkStatus {
fn from(value: Vec<SignerStatus>) -> 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(())
}
@@ -0,0 +1,21 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// 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(())
}
@@ -0,0 +1,51 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// 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<String> = 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<PathBuf>,
#[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(())
}
}
@@ -0,0 +1,39 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// 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<QueryHttpRpcNyxdClient> {
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<Vec<DealerDetails>> {
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
}
@@ -0,0 +1,24 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// 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(())
}
@@ -0,0 +1,222 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// 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<NymApiClient> {
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<String> {
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)
}
}