squashed feature/ecash-liveness-check (#5890) (#5890)

delay to gruyere

chore: delay to Feta

added threshold information to the response

nym api test clippy

bugfixes and endpoint improvements

expose results on api endpoints

wip: making nym api monitor network signers

added fallback legacy queries to get basic support idea

refactored the code to expose bool-only methods for status

ecash-signer-check lib for obtaining basic ecash signer information
This commit is contained in:
Jędrzej Stuczyński
2025-08-05 12:28:42 +01:00
committed by GitHub
parent e418c7587a
commit dc59149a5d
55 changed files with 4191 additions and 2297 deletions
Generated
+33 -10
View File
@@ -4900,13 +4900,11 @@ dependencies = [
"anyhow",
"async-trait",
"axum 0.7.9",
"axum-extra",
"axum-test 16.4.1",
"bincode",
"bip39",
"bs58",
"cfg-if",
"chrono",
"clap",
"console-subscriber",
"cosmwasm-std",
@@ -4915,13 +4913,9 @@ dependencies = [
"cw3",
"cw4",
"dashmap",
"dirs",
"dotenv",
"futures",
"getset",
"humantime-serde",
"itertools 0.14.0",
"k256",
"moka",
"nym-api-requests 0.1.0",
"nym-bandwidth-controller",
@@ -4936,12 +4930,11 @@ dependencies = [
"nym-crypto 0.4.0",
"nym-dkg",
"nym-ecash-contract-common 0.1.0",
"nym-ecash-signer-check",
"nym-ecash-time 0.1.0",
"nym-gateway-client",
"nym-http-api-common 0.1.0",
"nym-inclusion-probability",
"nym-mixnet-contract-common 0.6.0",
"nym-multisig-contract-common 0.1.0",
"nym-node-requests 0.1.0",
"nym-node-tester-utils",
"nym-pemstore 0.3.0",
@@ -4953,7 +4946,6 @@ dependencies = [
"nym-topology 0.1.0",
"nym-types",
"nym-validator-client 0.1.0",
"nym-vesting-contract-common 0.7.0",
"pin-project",
"rand 0.8.5",
"rand_chacha 0.3.1",
@@ -4989,14 +4981,15 @@ dependencies = [
"cosmrs",
"cosmwasm-std",
"ecdsa",
"getset",
"hex",
"humantime-serde",
"nym-coconut-dkg-common 0.1.0",
"nym-compact-ecash 0.1.0",
"nym-config 0.1.0",
"nym-contracts-common 0.5.0",
"nym-credentials-interface 0.1.0",
"nym-crypto 0.4.0",
"nym-ecash-signer-check-types",
"nym-ecash-time 0.1.0",
"nym-mixnet-contract-common 0.6.0",
"nym-network-defaults 0.1.0",
@@ -5901,6 +5894,36 @@ dependencies = [
"thiserror 2.0.12",
]
[[package]]
name = "nym-ecash-signer-check"
version = "0.1.0"
dependencies = [
"futures",
"nym-ecash-signer-check-types",
"nym-network-defaults 0.1.0",
"nym-validator-client 0.1.0",
"semver 1.0.26",
"thiserror 2.0.12",
"tokio",
"tracing",
"url",
]
[[package]]
name = "nym-ecash-signer-check-types"
version = "0.1.0"
dependencies = [
"nym-coconut-dkg-common 0.1.0",
"nym-crypto 0.4.0",
"semver 1.0.26",
"serde",
"thiserror 2.0.12",
"time",
"tracing",
"url",
"utoipa",
]
[[package]]
name = "nym-ecash-time"
version = "0.1.0"
+4 -1
View File
@@ -39,7 +39,8 @@ members = [
"common/cosmwasm-smart-contracts/ecash-contract",
"common/cosmwasm-smart-contracts/group-contract",
"common/cosmwasm-smart-contracts/mixnet-contract",
"common/cosmwasm-smart-contracts/multisig-contract", "common/cosmwasm-smart-contracts/nym-performance-contract",
"common/cosmwasm-smart-contracts/multisig-contract",
"common/cosmwasm-smart-contracts/nym-performance-contract",
"common/cosmwasm-smart-contracts/nym-pool-contract",
"common/cosmwasm-smart-contracts/vesting-contract",
"common/credential-storage",
@@ -49,6 +50,8 @@ members = [
"common/credentials-interface",
"common/crypto",
"common/dkg",
"common/ecash-signer-check",
"common/ecash-signer-check-types",
"common/ecash-time",
"common/execute",
"common/exit-policy",
@@ -6,16 +6,18 @@ use crate::nym_api::routes::{ecash, CORE_STATUS_COUNT, SINCE_ARG};
use async_trait::async_trait;
use nym_api_requests::ecash::models::{
AggregatedCoinIndicesSignatureResponse, AggregatedExpirationDateSignatureResponse,
BatchRedeemTicketsBody, EcashBatchTicketRedemptionResponse, EcashTicketVerificationResponse,
IssuedTicketbooksChallengeCommitmentRequest, IssuedTicketbooksChallengeCommitmentResponse,
IssuedTicketbooksDataRequest, IssuedTicketbooksDataResponse, IssuedTicketbooksForCountResponse,
IssuedTicketbooksForResponse, VerifyEcashTicketBody,
BatchRedeemTicketsBody, EcashBatchTicketRedemptionResponse, EcashSignerStatusResponse,
EcashTicketVerificationResponse, IssuedTicketbooksChallengeCommitmentRequest,
IssuedTicketbooksChallengeCommitmentResponse, IssuedTicketbooksDataRequest,
IssuedTicketbooksDataResponse, IssuedTicketbooksForCountResponse, IssuedTicketbooksForResponse,
VerifyEcashTicketBody,
};
use nym_api_requests::ecash::VerificationKeyResponse;
use nym_api_requests::models::{
AnnotationResponse, ApiHealthResponse, BinaryBuildInformationOwned, ChainStatusResponse,
KeyRotationInfoResponse, LegacyDescribedMixNode, NodePerformanceResponse, NodeRefreshBody,
NymNodeDescription, PerformanceHistoryResponse, RewardedSetResponse,
AnnotationResponse, ApiHealthResponse, BinaryBuildInformationOwned, ChainBlocksStatusResponse,
ChainStatusResponse, KeyRotationInfoResponse, LegacyDescribedMixNode, NodePerformanceResponse,
NodeRefreshBody, NymNodeDescription, PerformanceHistoryResponse, RewardedSetResponse,
SignerInformationResponse,
};
use nym_api_requests::nym_nodes::{
NodesByAddressesRequestBody, NodesByAddressesResponse, PaginatedCachedNodesResponseV1,
@@ -1331,6 +1333,22 @@ pub trait NymApiClientExt: ApiClient {
.await
}
async fn get_chain_blocks_status(&self) -> Result<ChainBlocksStatusResponse, NymAPIError> {
self.get_json("/v1/network/chain-blocks-status", NO_PARAMS)
.await
}
#[instrument(level = "debug", skip(self))]
async fn get_signer_status(&self) -> Result<EcashSignerStatusResponse, NymAPIError> {
self.get_json("/v1/ecash/signer-status", NO_PARAMS).await
}
#[instrument(level = "debug", skip(self))]
async fn get_signer_information(&self) -> Result<SignerInformationResponse, NymAPIError> {
self.get_json("/v1/api-status/signer-information", NO_PARAMS)
.await
}
#[instrument(level = "debug", skip(self))]
async fn get_key_rotation_info(&self) -> Result<KeyRotationInfoResponse, NymAPIError> {
self.get_json(
@@ -8,11 +8,11 @@ use crate::nyxd::CosmWasmClient;
use async_trait::async_trait;
use cosmrs::AccountId;
use cosmwasm_std::Addr;
use nym_coconut_dkg_common::dealer::RegisteredDealerDetails;
use nym_coconut_dkg_common::types::{ChunkIndex, NodeIndex, StateAdvanceResponse};
use serde::Deserialize;
use tracing::trace;
use nym_coconut_dkg_common::dealer::RegisteredDealerDetails;
pub use nym_coconut_dkg_common::{
dealer::{DealerDetailsResponse, PagedDealerIndexResponse, PagedDealerResponse},
dealing::{
@@ -21,7 +21,9 @@ pub use nym_coconut_dkg_common::{
},
msg::QueryMsg as DkgQueryMsg,
types::{DealerDetails, DealingIndex, Epoch, EpochId, EpochState, State},
verification_key::{ContractVKShare, PagedVKSharesResponse, VkShareResponse},
verification_key::{
ContractVKShare, PagedVKSharesResponse, VerificationKeyShare, VkShareResponse,
},
};
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
@@ -139,12 +139,22 @@ impl NyxdClient<HttpClient> {
})
}
pub fn connect_with_network_details<U>(
endpoint: U,
network_details: NymNetworkDetails,
) -> Result<QueryHttpRpcNyxdClient, NyxdError>
where
U: TryInto<HttpClientUrl, Error = TendermintRpcError>,
{
let config = Config::try_from_nym_network_details(&network_details)?;
Self::connect(config, endpoint)
}
pub fn connect_to_default_env<U>(endpoint: U) -> Result<QueryHttpRpcNyxdClient, NyxdError>
where
U: TryInto<HttpClientUrl, Error = TendermintRpcError>,
{
let config = Config::try_from_nym_network_details(&NymNetworkDetails::new_from_env())?;
Self::connect(config, endpoint)
Self::connect_with_network_details(endpoint, NymNetworkDetails::new_from_env())
}
}
@@ -0,0 +1,27 @@
[package]
name = "nym-ecash-signer-check-types"
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]
semver = { workspace = true }
serde = { workspace = true, features = ["derive"] }
url = { workspace = true }
thiserror = { workspace = true }
time = { workspace = true }
tracing = { workspace = true }
utoipa = { workspace = true }
nym-coconut-dkg-common = { path = "../cosmwasm-smart-contracts/coconut-dkg" }
nym-crypto = { path = "../crypto", features = ["asymmetric"] }
[lints]
workspace = true
@@ -0,0 +1,97 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use nym_coconut_dkg_common::dealer::DealerDetails;
use nym_coconut_dkg_common::verification_key::{ContractVKShare, VerificationKeyShare};
use nym_crypto::asymmetric::ed25519;
use nym_crypto::asymmetric::ed25519::Ed25519RecoveryError;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use url::Url;
use utoipa::ToSchema;
#[derive(Debug, Error)]
pub enum MalformedDealer {
#[error("dealer at {dealer_url} has provided invalid ed25519 pubkey: {source}")]
InvalidDealerPubkey {
dealer_url: String,
source: Ed25519RecoveryError,
},
#[error("dealer at {dealer_url} has provided invalid announce url: {source}")]
InvalidDealerAddress {
dealer_url: String,
source: url::ParseError,
},
}
#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]
pub struct RawDealerInformation {
pub announce_address: String,
pub owner_address: String,
pub node_index: u64,
pub public_key: String,
pub verification_key_share: Option<VerificationKeyShare>,
pub share_verified: bool,
}
impl RawDealerInformation {
pub fn new(
dealer_details: &DealerDetails,
contract_share: Option<&ContractVKShare>,
) -> RawDealerInformation {
RawDealerInformation {
announce_address: dealer_details.announce_address.clone(),
owner_address: dealer_details.address.to_string(),
node_index: dealer_details.assigned_index,
public_key: dealer_details.ed25519_identity.clone(),
verification_key_share: contract_share.map(|s| s.share.clone()),
share_verified: contract_share.map(|s| s.verified).unwrap_or(false),
}
}
pub fn parse(&self) -> Result<DealerInformation, MalformedDealer> {
Ok(DealerInformation {
announce_address: self.announce_address.parse().map_err(|source| {
MalformedDealer::InvalidDealerAddress {
dealer_url: self.announce_address.clone(),
source,
}
})?,
owner_address: self.owner_address.clone(),
node_index: self.node_index,
public_key: self.public_key.parse().map_err(|source| {
MalformedDealer::InvalidDealerPubkey {
dealer_url: self.announce_address.clone(),
source,
}
})?,
verification_key_share: self.verification_key_share.clone(),
share_verified: self.share_verified,
})
}
}
#[derive(Debug)]
pub struct DealerInformation {
pub announce_address: Url,
pub owner_address: String,
pub node_index: u64,
pub public_key: ed25519::PublicKey,
// no need to parse it into the full type as it doesn't get us anything
pub verification_key_share: Option<VerificationKeyShare>,
pub share_verified: bool,
}
impl From<DealerInformation> for RawDealerInformation {
fn from(d: DealerInformation) -> Self {
RawDealerInformation {
announce_address: d.announce_address.to_string(),
owner_address: d.owner_address,
node_index: d.node_index,
public_key: d.public_key.to_base58_string(),
verification_key_share: d.verification_key_share,
share_verified: d.share_verified,
}
}
}
@@ -0,0 +1,127 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use nym_coconut_dkg_common::types::EpochId;
use nym_coconut_dkg_common::verification_key::VerificationKeyShare;
use nym_crypto::asymmetric::ed25519;
use std::time::Duration;
use time::OffsetDateTime;
use tracing::{debug, warn};
pub trait Verifiable {
fn verify_signature(&self, pub_key: &ed25519::PublicKey) -> bool;
}
pub trait TimestampedResponse {
fn timestamp(&self) -> OffsetDateTime;
}
pub trait LegacyChainResponse {
fn chain_synced(&self, now: OffsetDateTime, stall_threshold: Duration) -> bool;
}
pub trait ChainResponse: Verifiable + TimestampedResponse {
fn chain_synced(&self) -> bool;
fn chain_available(
&self,
pub_key: &ed25519::PublicKey,
now: OffsetDateTime,
stale_response_threshold: Duration,
) -> bool {
if !self.verify_signature(pub_key) {
warn!("failed signature verification on chain status response");
return false;
}
// we rely on information provided from the api itself AS LONG AS it's not too outdated
if self.timestamp() + stale_response_threshold < now {
return false;
}
self.chain_synced()
}
}
pub trait LegacySignerResponse {
fn signer_identity(&self) -> &str;
fn signer_verification_key(&self) -> &Option<String>;
fn unprovable_signing_available(
&self,
pub_key: &ed25519::PublicKey,
expected_verification_key: Option<VerificationKeyShare>,
share_verified: bool,
) -> bool {
if self.signer_identity() != pub_key.to_base58_string() {
warn!("mismatched identity key on the legacy response");
return false;
}
// the contract share hasn't been verified yet, so we're probably in the middle of DKG
// thus if there's a bit of desync in the state, it's fine
if !share_verified {
return true;
}
if self.signer_verification_key() != &expected_verification_key {
warn!("mismatched [ecash] verification key on the legacy response");
return false;
}
true
}
}
pub trait SignerResponse: Verifiable + TimestampedResponse {
fn has_signing_keys(&self) -> bool;
fn signer_disabled(&self) -> bool;
fn is_ecash_signer(&self) -> bool;
fn dkg_ecash_epoch_id(&self) -> EpochId;
fn provable_signing_available(
&self,
pub_key: &ed25519::PublicKey,
dkg_epoch_id: EpochId,
now: OffsetDateTime,
stale_response_threshold: Duration,
) -> bool {
if !self.verify_signature(pub_key) {
warn!("failed signature verification on chain status response");
return false;
}
// we rely on information provided from the api itself AS LONG AS it's not too outdated
if self.timestamp() + stale_response_threshold < now {
return false;
}
if !self.has_signing_keys() {
debug!("missing signing keys");
return false;
}
if self.signer_disabled() {
debug!("signer functionalities explicitly disabled");
return false;
}
if !self.is_ecash_signer() {
debug!("signer doesn't recognise it's a signer for this epoch");
return false;
}
if dkg_epoch_id != self.dkg_ecash_epoch_id() {
debug!(
"mismatched dkg epoch id. current: {dkg_epoch_id}, signer's: {}",
self.dkg_ecash_epoch_id()
);
return false;
}
true
}
}
@@ -0,0 +1,6 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
pub mod dealer_information;
pub mod helper_traits;
pub mod status;
@@ -0,0 +1,303 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::dealer_information::RawDealerInformation;
use crate::helper_traits::{
ChainResponse, LegacyChainResponse, LegacySignerResponse, SignerResponse,
};
use nym_coconut_dkg_common::types::EpochId;
use nym_coconut_dkg_common::verification_key::VerificationKeyShare;
use nym_crypto::asymmetric::ed25519;
use serde::{Deserialize, Serialize};
use std::time::Duration;
use time::OffsetDateTime;
use utoipa::ToSchema;
pub(crate) const CHAIN_STALL_THRESHOLD: Duration = Duration::from_secs(5 * 60);
pub(crate) const STALE_RESPONSE_THRESHOLD: Duration = Duration::from_secs(5 * 60);
// the reason for generics is not to remove duplication of code,
// but because without them, we'd be having problems with circular dependencies,
// i.e. nym-api-requests depending on ecash-signer-check-types and
// ecash-signer-check-types needing nym-api-requests
#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]
pub enum Status<L, T> {
/// The API, even though it reports correct version, did not response to the status query
Unreachable,
/// The API is running an outdated version that does not expose the required endpoint
Outdated,
/// Response to the legacy (unsigned) status query
ReachableLegacy { response: Box<L> },
/// Response to the current (signed) status query
Reachable { response: Box<T> },
}
impl<L, T> Status<L, T>
where
L: LegacyChainResponse,
T: ChainResponse,
{
pub fn chain_available(&self, pub_key: ed25519::PublicKey) -> bool {
let now = OffsetDateTime::now_utc();
match self {
Status::Unreachable | Status::Outdated => false,
Status::ReachableLegacy { response } => {
response.chain_synced(now, CHAIN_STALL_THRESHOLD)
}
Status::Reachable { response } => {
response.chain_available(&pub_key, now, STALE_RESPONSE_THRESHOLD)
}
}
}
pub fn chain_provably_stalled(&self, pub_key: ed25519::PublicKey) -> bool {
let now = OffsetDateTime::now_utc();
match self {
Status::Unreachable | Status::Outdated | Status::ReachableLegacy { .. } => false,
Status::Reachable { response } => {
!response.chain_available(&pub_key, now, STALE_RESPONSE_THRESHOLD)
}
}
}
pub fn chain_unprovably_stalled(&self) -> bool {
let now = OffsetDateTime::now_utc();
match self {
Status::Unreachable | Status::Outdated | Status::Reachable { .. } => false,
Status::ReachableLegacy { response } => {
!response.chain_synced(now, CHAIN_STALL_THRESHOLD)
}
}
}
}
impl<L, T> Status<L, T>
where
L: LegacySignerResponse,
T: SignerResponse,
{
pub fn signing_available(
&self,
pub_key: ed25519::PublicKey,
dkg_epoch_id: u64,
expected_verification_key: Option<VerificationKeyShare>,
share_verified: bool,
) -> bool {
let now = OffsetDateTime::now_utc();
match self {
Status::Unreachable | Status::Outdated => false,
Status::ReachableLegacy { response } => response.unprovable_signing_available(
&pub_key,
expected_verification_key,
share_verified,
),
Status::Reachable { response } => response.provable_signing_available(
&pub_key,
dkg_epoch_id,
now,
STALE_RESPONSE_THRESHOLD,
),
}
}
pub fn signing_provably_unavailable(
&self,
pub_key: ed25519::PublicKey,
dkg_epoch_id: EpochId,
) -> bool {
let now = OffsetDateTime::now_utc();
match self {
Status::Unreachable | Status::Outdated | Status::ReachableLegacy { .. } => false,
Status::Reachable { response } => !response.provable_signing_available(
&pub_key,
dkg_epoch_id,
now,
STALE_RESPONSE_THRESHOLD,
),
}
}
pub fn signing_unprovably_unavailable(
&self,
pub_key: ed25519::PublicKey,
expected_verification_key: Option<VerificationKeyShare>,
share_verified: bool,
) -> bool {
match self {
Status::Unreachable | Status::Outdated | Status::Reachable { .. } => false,
Status::ReachableLegacy { response } => !response.unprovable_signing_available(
&pub_key,
expected_verification_key,
share_verified,
),
}
}
}
#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]
pub struct SignerResult<LS, TS, LC, TC> {
pub dkg_epoch_id: u64,
pub information: RawDealerInformation,
pub status: SignerStatus<LS, TS, LC, TC>,
}
impl<LS, TS, LC, TC> SignerResult<LS, TS, LC, TC> {
pub fn signer_unreachable(&self) -> bool {
matches!(self.status, SignerStatus::Unreachable)
}
pub fn malformed_details(&self) -> bool {
self.information.parse().is_err()
}
}
impl<LS, TS, LC, TC> SignerResult<LS, TS, LC, TC>
where
LC: LegacyChainResponse,
TC: ChainResponse,
{
pub fn unknown_chain_status(&self) -> bool {
let Ok(_) = self.information.parse() else {
return true;
};
if let SignerStatus::Tested { .. } = &self.status {
return false;
}
true
}
pub fn chain_available(&self) -> bool {
let Ok(parsed_info) = self.information.parse() else {
return false;
};
let SignerStatus::Tested { result } = &self.status else {
return false;
};
result
.local_chain_status
.chain_available(parsed_info.public_key)
}
pub fn chain_provably_stalled(&self) -> bool {
let Ok(parsed_info) = self.information.parse() else {
return false;
};
let SignerStatus::Tested { result } = &self.status else {
return false;
};
result
.local_chain_status
.chain_provably_stalled(parsed_info.public_key)
}
pub fn chain_unprovably_stalled(&self) -> bool {
let SignerStatus::Tested { result } = &self.status else {
return false;
};
result.local_chain_status.chain_unprovably_stalled()
}
}
impl<LS, TS, LC, TC> SignerResult<LS, TS, LC, TC>
where
LS: LegacySignerResponse,
TS: SignerResponse,
{
pub fn unknown_signing_status(&self) -> bool {
let Ok(_) = self.information.parse() else {
return true;
};
if let SignerStatus::Tested { .. } = &self.status {
return false;
}
true
}
pub fn signing_available(&self) -> bool {
let Ok(parsed_info) = self.information.parse() else {
return false;
};
let SignerStatus::Tested { result } = &self.status else {
return false;
};
result.signing_status.signing_available(
parsed_info.public_key,
self.dkg_epoch_id,
parsed_info.verification_key_share,
parsed_info.share_verified,
)
}
pub fn signing_provably_unavailable(&self) -> bool {
let Ok(parsed_info) = self.information.parse() else {
return false;
};
let SignerStatus::Tested { result } = &self.status else {
return false;
};
result
.signing_status
.signing_provably_unavailable(parsed_info.public_key, self.dkg_epoch_id)
}
pub fn signing_unprovably_unavailable(&self) -> bool {
let Ok(parsed_info) = self.information.parse() else {
return false;
};
let SignerStatus::Tested { result } = &self.status else {
return false;
};
result.signing_status.signing_unprovably_unavailable(
parsed_info.public_key,
parsed_info.verification_key_share,
parsed_info.share_verified,
)
}
}
#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]
pub enum SignerStatus<LS, TS, LC, TC> {
Unreachable,
ProvidedInvalidDetails,
Tested {
result: SignerTestResult<LS, TS, LC, TC>,
},
}
impl<LS, TS, LC, TC> SignerStatus<LS, TS, LC, TC> {
pub fn with_details(
self,
information: impl Into<RawDealerInformation>,
dkg_epoch_id: u64,
) -> SignerResult<LS, TS, LC, TC> {
SignerResult {
dkg_epoch_id,
status: self,
information: information.into(),
}
}
}
#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]
pub struct SignerTestResult<LS, TS, LC, TC> {
pub reported_version: String,
pub signing_status: Status<LS, TS>,
pub local_chain_status: Status<LC, TC>,
}
+27
View File
@@ -0,0 +1,27 @@
[package]
name = "nym-ecash-signer-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]
futures = { workspace = true }
thiserror = { workspace = true }
semver = { workspace = true }
tokio = { workspace = true, features = ["time"] }
tracing = { workspace = true }
url = { workspace = true }
nym-validator-client = { path = "../client-libs/validator-client" }
nym-network-defaults = { path = "../network-defaults" }
nym-ecash-signer-check-types = { path = "../ecash-signer-check-types" }
[lints]
workspace = true
@@ -0,0 +1,225 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::{LocalChainStatus, SigningStatus, TypedSignerResult};
use nym_ecash_signer_check_types::dealer_information::RawDealerInformation;
use nym_ecash_signer_check_types::status::{SignerStatus, SignerTestResult};
use nym_validator_client::client::NymApiClientExt;
use nym_validator_client::models::BinaryBuildInformationOwned;
use nym_validator_client::nyxd::contract_traits::dkg_query_client::{
ContractVKShare, DealerDetails,
};
use nym_validator_client::NymApiClient;
use std::time::Duration;
use tracing::{error, warn};
use url::Url;
pub(crate) mod chain_status {
// Dorina
pub(crate) const MINIMUM_VERSION_LEGACY: semver::Version = semver::Version::new(1, 1, 51);
// Gruyere
pub(crate) const MINIMUM_VERSION: semver::Version = semver::Version::new(1, 1, 64);
}
pub(crate) mod signing_status {
// Magura (possibly earlier)
pub(crate) const MINIMUM_LEGACY_VERSION: semver::Version = semver::Version::new(1, 1, 46);
// Gruyere
pub(crate) const MINIMUM_VERSION: semver::Version = semver::Version::new(1, 1, 64);
}
struct ClientUnderTest {
api_client: NymApiClient,
build_info: Option<BinaryBuildInformationOwned>,
}
impl ClientUnderTest {
pub(crate) fn new(api_url: &Url) -> Self {
ClientUnderTest {
api_client: NymApiClient::new(api_url.clone()),
build_info: None,
}
}
pub(crate) async fn try_retrieve_build_information(&mut self) -> bool {
match tokio::time::timeout(
Duration::from_secs(5),
self.api_client.nym_api.build_information(),
)
.await
{
Ok(Ok(build_information)) => {
self.build_info = Some(build_information);
true
}
Ok(Err(err)) => {
warn!("{}: failed to retrieve build information: {err}. the signer is most likely down", self.api_client.api_url());
false
}
Err(_timeout) => {
warn!(
"{}: timed out while attempting to retrieve build information",
self.api_client.api_url()
);
false
}
}
}
pub(crate) fn version(&self) -> Option<semver::Version> {
self.build_info.as_ref().and_then(|build_info| {
build_info
.build_version
.parse()
.inspect_err(|err| {
error!(
"ecash signer '{}' reports invalid version {}: {err}",
self.api_client.api_url(),
build_info.build_version
)
})
.ok()
})
}
pub(crate) fn supports_legacy_signing_status_query(&self) -> bool {
let Some(version) = self.version() else {
return false;
};
version >= signing_status::MINIMUM_LEGACY_VERSION
}
pub(crate) fn supports_signing_status_query(&self) -> bool {
let Some(version) = self.version() else {
return false;
};
version >= signing_status::MINIMUM_VERSION
}
pub(crate) fn supports_chain_status_query(&self) -> bool {
let Some(version) = self.version() else {
return false;
};
version >= chain_status::MINIMUM_VERSION
}
pub(crate) fn supports_legacy_chain_status_query(&self) -> bool {
let Some(version) = self.version() else {
return false;
};
version >= chain_status::MINIMUM_VERSION_LEGACY
}
pub(crate) async fn check_local_chain(&self) -> LocalChainStatus {
// check if it at least supports legacy query
if !self.supports_legacy_chain_status_query() {
return LocalChainStatus::Outdated;
}
// check if it supports the current query
if self.supports_chain_status_query() {
return match self.api_client.nym_api.get_chain_blocks_status().await {
Ok(status) => LocalChainStatus::Reachable {
response: Box::new(status),
},
Err(err) => {
warn!(
"{}: failed to retrieve local chain status: {err}",
self.api_client.api_url()
);
LocalChainStatus::Unreachable
}
};
}
// fallback to the legacy query
match self.api_client.nym_api.get_chain_status().await {
Ok(status) => LocalChainStatus::ReachableLegacy {
response: Box::new(status),
},
Err(err) => {
warn!(
"{}: failed to retrieve [legacy] local chain status: {err}",
self.api_client.api_url()
);
LocalChainStatus::Unreachable
}
}
}
pub(crate) async fn check_signing_status(&self) -> SigningStatus {
// check if it at least supports legacy query
if !self.supports_legacy_signing_status_query() {
return SigningStatus::Outdated;
}
// check if it supports the current query
if self.supports_signing_status_query() {
return match self.api_client.nym_api.get_signer_status().await {
Ok(response) => SigningStatus::Reachable {
response: Box::new(response),
},
Err(err) => {
warn!(
"{}: failed to retrieve signer chain status: {err}",
self.api_client.api_url()
);
SigningStatus::Unreachable
}
};
}
// fallback to the legacy query
match self.api_client.nym_api.get_signer_information().await {
Ok(status) => SigningStatus::ReachableLegacy {
response: Box::new(status),
},
Err(err) => {
warn!(
"{}: failed to retrieve [legacy] signer chain status: {err}",
self.api_client.api_url()
);
// NOTE: this might equally mean the signing is disabled
SigningStatus::Unreachable
}
}
}
}
pub(crate) async fn check_client(
dealer_details: DealerDetails,
dkg_epoch: u64,
contract_share: Option<&ContractVKShare>,
) -> TypedSignerResult {
let dealer_information = RawDealerInformation::new(&dealer_details, contract_share);
// 7. attempt to construct client instances out of them
let Ok(parsed_information) = dealer_information.parse() else {
return SignerStatus::ProvidedInvalidDetails.with_details(dealer_information, dkg_epoch);
};
let mut client = ClientUnderTest::new(&parsed_information.announce_address);
// 8. check basic connection status - can you retrieve build information?
if !client.try_retrieve_build_information().await {
return SignerStatus::Unreachable.with_details(dealer_information, dkg_epoch);
}
// 9. check perceived chain status
let local_chain_status = client.check_local_chain().await;
// 10. check signer status
let signing_status = client.check_signing_status().await;
SignerStatus::Tested {
result: SignerTestResult {
reported_version: client.version().map(|v| v.to_string()).unwrap_or_default(),
signing_status,
local_chain_status,
},
}
.with_details(dealer_information, dkg_epoch)
}
@@ -0,0 +1,80 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::SignerCheckError;
use nym_crypto::asymmetric::ed25519;
use nym_validator_client::nyxd::contract_traits::dkg_query_client::{
ContractVKShare, DealerDetails, VerificationKeyShare,
};
use url::Url;
#[derive(Debug)]
pub struct RawDealerInformation {
pub announce_address: String,
pub owner_address: String,
pub node_index: u64,
pub public_key: String,
pub verification_key_share: Option<VerificationKeyShare>,
pub share_verified: bool,
}
impl RawDealerInformation {
pub fn new(
dealer_details: &DealerDetails,
contract_share: Option<&ContractVKShare>,
) -> RawDealerInformation {
RawDealerInformation {
announce_address: dealer_details.announce_address.clone(),
owner_address: dealer_details.address.to_string(),
node_index: dealer_details.assigned_index,
public_key: dealer_details.ed25519_identity.clone(),
verification_key_share: contract_share.map(|s| s.share.clone()),
share_verified: contract_share.map(|s| s.verified).unwrap_or(false),
}
}
pub fn parse(&self) -> Result<DealerInformation, SignerCheckError> {
Ok(DealerInformation {
announce_address: self.announce_address.parse().map_err(|source| {
SignerCheckError::InvalidDealerAddress {
dealer_url: self.announce_address.clone(),
source,
}
})?,
owner_address: self.owner_address.clone(),
node_index: self.node_index,
public_key: self.announce_address.parse().map_err(|source| {
SignerCheckError::InvalidDealerPubkey {
dealer_url: self.announce_address.clone(),
source,
}
})?,
verification_key_share: self.verification_key_share.clone(),
share_verified: self.share_verified,
})
}
}
#[derive(Debug)]
pub struct DealerInformation {
pub announce_address: Url,
pub owner_address: String,
pub node_index: u64,
pub public_key: ed25519::PublicKey,
// no need to parse it into the full type as it doesn't get us anything
pub verification_key_share: Option<VerificationKeyShare>,
pub share_verified: bool,
}
impl From<DealerInformation> for RawDealerInformation {
fn from(d: DealerInformation) -> Self {
RawDealerInformation {
announce_address: d.announce_address.to_string(),
owner_address: d.owner_address,
node_index: d.node_index,
public_key: d.public_key.to_base58_string(),
verification_key_share: d.verification_key_share,
share_verified: d.share_verified,
}
}
}
+24
View File
@@ -0,0 +1,24 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use nym_validator_client::nyxd::error::NyxdError;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum SignerCheckError {
#[error("failed to connect to nyxd chain due to invalid connection details: {source}")]
InvalidNyxdConnectionDetails { source: NyxdError },
#[error("failed to query the DKG contract: {source}")]
DKGContractQueryFailure { source: NyxdError },
}
impl SignerCheckError {
pub fn invalid_nyxd_connection_details(source: NyxdError) -> Self {
Self::InvalidNyxdConnectionDetails { source }
}
pub fn dkg_contract_query_failure(source: NyxdError) -> Self {
Self::DKGContractQueryFailure { source }
}
}
+94
View File
@@ -0,0 +1,94 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::client_check::check_client;
use futures::stream::{FuturesUnordered, StreamExt};
use nym_network_defaults::NymNetworkDetails;
use nym_validator_client::nyxd::contract_traits::{DkgQueryClient, PagedDkgQueryClient};
use nym_validator_client::QueryHttpRpcNyxdClient;
use std::collections::HashMap;
use url::Url;
pub use error::SignerCheckError;
use nym_ecash_signer_check_types::status::{SignerResult, Status};
use nym_validator_client::ecash::models::EcashSignerStatusResponse;
use nym_validator_client::models::{
ChainBlocksStatusResponse, ChainStatusResponse, SignerInformationResponse,
};
mod client_check;
pub mod error;
pub type TypedSignerResult = SignerResult<
SignerInformationResponse,
EcashSignerStatusResponse,
ChainStatusResponse,
ChainBlocksStatusResponse,
>;
pub type LocalChainStatus = Status<ChainStatusResponse, ChainBlocksStatusResponse>;
pub type SigningStatus = Status<SignerInformationResponse, EcashSignerStatusResponse>;
pub struct SignersTestResult {
pub threshold: Option<u64>,
pub results: Vec<TypedSignerResult>,
}
pub async fn check_signers(
rpc_endpoint: Url,
// details such as denoms, prefixes, etc.
network_details: NymNetworkDetails,
) -> Result<SignersTestResult, SignerCheckError> {
// 1. create nyx client instance
let client = QueryHttpRpcNyxdClient::connect_with_network_details(
rpc_endpoint.as_str(),
network_details,
)
.map_err(SignerCheckError::invalid_nyxd_connection_details)?;
check_signers_with_client(&client).await
}
pub async fn check_signers_with_client<C>(client: &C) -> Result<SignersTestResult, SignerCheckError>
where
C: DkgQueryClient + Sync,
{
// 2. retrieve current dkg epoch
let dkg_epoch = client
.get_current_epoch()
.await
.map_err(SignerCheckError::dkg_contract_query_failure)?;
// 3. retrieve the dkg threshold as reference point
let threshold = client
.get_epoch_threshold(dkg_epoch.epoch_id)
.await
.map_err(SignerCheckError::dkg_contract_query_failure)?;
// 4. retrieve information on current DKG dealers (i.e. eligible signers)
let dealers = client
.get_all_current_dealers()
.await
.map_err(SignerCheckError::dkg_contract_query_failure)?;
// 5. retrieve their published keys (if available)
let shares: HashMap<_, _> = client
.get_all_verification_key_shares(dkg_epoch.epoch_id)
.await
.map_err(SignerCheckError::dkg_contract_query_failure)?
.into_iter()
.map(|share| (share.node_index, share))
.collect();
// 6. for each dealer attempt to perform the checks
let results = dealers
.into_iter()
.map(|d| {
let share = shares.get(&d.assigned_index);
check_client(d, dkg_epoch.epoch_id, share)
})
.collect::<FuturesUnordered<_>>()
.collect::<Vec<_>>()
.await;
Ok(SignersTestResult { threshold, results })
}
+73
View File
@@ -0,0 +1,73 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::chain_status::LocalChainStatus;
use crate::dealer_information::RawDealerInformation;
use crate::signing_status::SigningStatus;
use std::time::Duration;
pub(crate) const STALE_RESPONSE_THRESHOLD: Duration = Duration::from_secs(5 * 60);
#[derive(Debug)]
pub struct SignerResult {
pub dkg_epoch_id: u64,
pub information: RawDealerInformation,
pub status: SignerStatus,
}
impl SignerResult {
pub fn chain_available(&self) -> bool {
let Ok(parsed_info) = self.information.parse() else {
return false;
};
let SignerStatus::Tested { result } = &self.status else {
return false;
};
result.local_chain_status.available(parsed_info.public_key)
}
pub fn signer_available(&self) -> bool {
let Ok(parsed_info) = self.information.parse() else {
return false;
};
let SignerStatus::Tested { result } = &self.status else {
return false;
};
result.signing_status.available(
parsed_info.public_key,
self.dkg_epoch_id,
parsed_info.verification_key_share,
parsed_info.share_verified,
)
}
}
#[derive(Debug)]
pub enum SignerStatus {
Unreachable,
ProvidedInvalidDetails,
Tested { result: SignerTestResult },
}
impl SignerStatus {
pub fn with_details(
self,
information: impl Into<RawDealerInformation>,
dkg_epoch_id: u64,
) -> SignerResult {
SignerResult {
dkg_epoch_id,
status: self,
information: information.into(),
}
}
}
#[derive(Debug)]
pub struct SignerTestResult {
pub reported_version: String,
pub signing_status: SigningStatus,
pub local_chain_status: LocalChainStatus,
}
@@ -146,6 +146,12 @@ pub struct OutputParams {
pub output: Option<Output>,
}
impl OutputParams {
pub fn get_output(&self) -> Output {
self.output.unwrap_or_default()
}
}
impl Output {
pub fn to_response<T: Serialize>(self, data: T) -> FormattedResponse<T> {
match self {
+3 -22
View File
@@ -16,17 +16,12 @@ async-trait = { workspace = true }
bs58 = { workspace = true }
bip39 = { workspace = true }
bincode.workspace = true
console-subscriber = { workspace = true, optional = true } # nym-api needs to be built with RUSTFLAGS="--cfg tokio_unstable"
cfg-if = { workspace = true }
clap = { workspace = true, features = ["cargo", "derive", "env"] }
console-subscriber = { workspace = true, optional = true } # validator-api needs to be built with RUSTFLAGS="--cfg tokio_unstable"
dashmap = { workspace = true }
dirs = { workspace = true }
futures = { workspace = true }
itertools = { workspace = true }
humantime-serde = { workspace = true }
k256 = { workspace = true, features = [
"ecdsa-core",
] } # needed for the Verifier trait; pull whatever version is used by other dependencies
moka = { workspace = true }
pin-project = { workspace = true }
rand = { workspace = true }
@@ -51,7 +46,6 @@ tendermint = { workspace = true }
ts-rs = { workspace = true, optional = true }
anyhow = { workspace = true }
getset = { workspace = true }
sqlx = { workspace = true, features = [
"runtime-tokio-rustls",
@@ -66,30 +60,19 @@ zeroize = { workspace = true }
# for axum server
axum = { workspace = true, features = ["tokio"] }
axum-extra = { workspace = true, features = ["typed-header"] }
tower-http = { workspace = true, features = ["cors", "trace", "compression-br", "compression-deflate", "compression-gzip", "compression-zstd"] }
utoipa = { workspace = true, features = ["axum_extras", "time"] }
utoipauto = { workspace = true }
utoipa-swagger-ui = { workspace = true, features = ["axum"] }
tracing = { workspace = true }
## ephemera-specific
#actix-web = "4"
#array-bytes = "6.0.0"
#chrono = { version = "0.4.24", default-features = false, features = ["clock"] }
#futures-util = "0.3.25"
#serde_derive = "1.0.149"
#uuid = { version = "1.3.0", features = ["serde", "v4"] }
## internal
#ephemera = { path = "../ephemera" }
nym-bandwidth-controller = { path = "../common/bandwidth-controller" }
nym-ecash-contract-common = { path = "../common/cosmwasm-smart-contracts/ecash-contract" }
nym-ecash-time = { path = "../common/ecash-time", features = ["expiration"] }
nym-coconut-dkg-common = { path = "../common/cosmwasm-smart-contracts/coconut-dkg" }
nym-compact-ecash = { path = "../common/nym_offline_compact_ecash" }
nym-credentials-interface = { path = "../common/credentials-interface" }
#nym-ephemera-common = { path = "../common/cosmwasm-smart-contracts/ephemera" }
nym-config = { path = "../common/config" }
cosmwasm-std = { workspace = true }
nym-credential-storage = { path = "../common/credential-storage", features = [
@@ -102,11 +85,8 @@ cw3 = { workspace = true }
cw4 = { workspace = true }
nym-dkg = { path = "../common/dkg", features = ["cw-types"] }
nym-gateway-client = { path = "../common/client-libs/gateway-client" }
nym-inclusion-probability = { path = "../common/inclusion-probability" }
nym-mixnet-contract-common = { path = "../common/cosmwasm-smart-contracts/mixnet-contract", features = ["utoipa"] }
nym-vesting-contract-common = { path = "../common/cosmwasm-smart-contracts/vesting-contract" }
nym-contracts-common = { path = "../common/cosmwasm-smart-contracts/contracts-common", features = ["naive_float", "utoipa"] }
nym-multisig-contract-common = { path = "../common/cosmwasm-smart-contracts/multisig-contract" }
nym-sphinx = { path = "../common/nymsphinx" }
nym-pemstore = { path = "../common/pemstore" }
nym-task = { path = "../common/task" }
@@ -121,7 +101,8 @@ nym-http-api-common = { path = "../common/http-api-common", features = ["utoipa"
nym-serde-helpers = { path = "../common/serde-helpers", features = ["date"] }
nym-ticketbooks-merkle = { path = "../common/ticketbooks-merkle" }
nym-statistics-common = { path = "../common/statistics" }
chrono.workspace = true
nym-ecash-signer-check = { path = "../common/ecash-signer-check" }
[features]
no-reward = []
+4 -2
View File
@@ -10,7 +10,6 @@ license.workspace = true
bs58 = { workspace = true }
cosmrs = { workspace = true }
cosmwasm-std = { workspace = true }
getset = { workspace = true }
schemars = { workspace = true, features = ["preserve_order"] }
serde = { workspace = true, features = ["derive"] }
humantime-serde = { workspace = true }
@@ -37,10 +36,13 @@ nym-ecash-time = { path = "../../common/ecash-time" }
nym-compact-ecash = { path = "../../common/nym_offline_compact_ecash" }
nym-contracts-common = { path = "../../common/cosmwasm-smart-contracts/contracts-common", features = ["naive_float"] }
nym-mixnet-contract-common = { path = "../../common/cosmwasm-smart-contracts/mixnet-contract", features = ["utoipa"] }
nym-coconut-dkg-common = { path = "../../common/cosmwasm-smart-contracts/coconut-dkg" }
nym-node-requests = { path = "../../nym-node/nym-node-requests", default-features = false, features = ["openapi"] }
nym-noise-keys = { path = "../../common/nymnoise/keys"}
nym-noise-keys = { path = "../../common/nymnoise/keys" }
nym-network-defaults = { path = "../../common/network-defaults" }
nym-ticketbooks-merkle = { path = "../../common/ticketbooks-merkle" }
nym-ecash-signer-check-types = { path = "../../common/ecash-signer-check-types" }
[dev-dependencies]
rand_chacha = { workspace = true }
+28 -70
View File
@@ -1,7 +1,9 @@
// Copyright 2023-2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::signable::SignedMessage;
use cosmrs::AccountId;
use nym_coconut_dkg_common::types::EpochId;
use nym_compact_ecash::scheme::coin_indices_signatures::AnnotatedCoinIndexSignature;
use nym_compact_ecash::scheme::expiration_date_signatures::AnnotatedExpirationDateSignature;
use nym_compact_ecash::utils::try_deserialize_g1_projective;
@@ -12,14 +14,13 @@ use nym_credentials_interface::{
VerificationKeyAuth, WithdrawalRequest,
};
use nym_crypto::asymmetric::ed25519;
use nym_crypto::asymmetric::ed25519::serde_helpers::bs58_ed25519_signature;
use nym_ticketbooks_merkle::{IssuedTicketbook, IssuedTicketbooksFullMerkleProof};
use serde::{Deserialize, Serialize};
use sha2::Digest;
use std::collections::BTreeMap;
use std::ops::Deref;
use thiserror::Error;
use time::Date;
use time::{Date, OffsetDateTime};
use utoipa::ToSchema;
#[derive(Serialize, Deserialize, Clone, ToSchema)]
@@ -541,74 +542,6 @@ pub struct CommitedDeposit {
pub merkle_index: usize,
}
//
//
// make sure only our types can implement this trait (to ensure infallible serialisation)
mod private {
use crate::ecash::models::{
IssuedTicketbooksChallengeCommitmentRequestBody,
IssuedTicketbooksChallengeCommitmentResponseBody, IssuedTicketbooksDataRequestBody,
IssuedTicketbooksDataResponseBody, IssuedTicketbooksForResponseBody,
};
pub trait Sealed {}
// requests
impl Sealed for IssuedTicketbooksChallengeCommitmentRequestBody {}
impl Sealed for IssuedTicketbooksDataRequestBody {}
// responses
impl Sealed for IssuedTicketbooksChallengeCommitmentResponseBody {}
impl Sealed for IssuedTicketbooksForResponseBody {}
impl Sealed for IssuedTicketbooksDataResponseBody {}
}
// the trait is not public as it's only defined on types that are guaranteed to not panic when serialised
pub trait SignableMessageBody: Serialize + private::Sealed {
fn sign(self, key: &ed25519::PrivateKey) -> SignedMessage<Self>
where
Self: Sized,
{
let signature = key.sign(self.plaintext());
SignedMessage {
body: self,
signature,
}
}
fn plaintext(&self) -> Vec<u8> {
#[allow(clippy::unwrap_used)]
// SAFETY: all types that implement this trait have valid serialisations
serde_json::to_vec(&self).unwrap()
}
}
impl<T> SignableMessageBody for T where T: Serialize + private::Sealed {}
#[derive(Clone, Serialize, Deserialize, Debug, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct SignedMessage<T> {
pub body: T,
#[schema(value_type = String)]
#[serde(with = "bs58_ed25519_signature")]
pub signature: ed25519::Signature,
}
impl<T> SignedMessage<T> {
pub fn verify_signature(&self, pub_key: &ed25519::PublicKey) -> bool
where
T: SignableMessageBody,
{
let plaintext = self.body.plaintext();
if plaintext.is_empty() {
return false;
}
pub_key.verify(&plaintext, &self.signature).is_ok()
}
}
pub type IssuedTicketbooksDataRequest = SignedMessage<IssuedTicketbooksDataRequestBody>;
pub type IssuedTicketbooksChallengeCommitmentRequest =
SignedMessage<IssuedTicketbooksChallengeCommitmentRequestBody>;
@@ -826,6 +759,31 @@ pub struct IssuedTicketbooksForCount {
pub count: u32,
}
pub type EcashSignerStatusResponse = SignedMessage<EcashSignerStatusResponseBody>;
#[derive(Serialize, Deserialize, ToSchema, Clone, Debug)]
#[serde(rename_all = "camelCase")]
// includes all pre-requisites for successful (assuming valid request) `/blind-sign`
pub struct EcashSignerStatusResponseBody {
#[serde(with = "time::serde::rfc3339")]
#[schema(value_type = String)]
pub current_time: OffsetDateTime,
/// Current, perceived, dkg epoch id
pub dkg_ecash_epoch_id: EpochId,
/// Flag indicating whether the operator has explicitly disabled signer functionalities in the api
pub signer_disabled: bool,
/// Flag indicating whether this api thinks it's a valid ecash signer for the current epoch
pub is_ecash_signer: bool,
/// Flag indicating whether this api thinks it has valid signing keys.
/// It might be a valid signer that's not disabled, but the keys might have accidentally been
/// removed due to invalid data migration.
pub has_signing_keys: bool,
}
#[cfg(test)]
mod tests {
use super::*;
+1
View File
@@ -11,6 +11,7 @@ pub mod legacy;
pub mod models;
pub mod nym_nodes;
pub mod pagination;
pub mod signable;
// The response type we fetch from the network details endpoint.
#[derive(Clone, Debug, Serialize, Deserialize)]
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,68 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use serde::{Deserialize, Serialize};
use std::time::Duration;
use utoipa::ToSchema;
#[derive(Clone, Copy, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub struct ApiHealthResponse {
pub status: ApiStatus,
#[serde(default)]
pub chain_status: ChainStatus,
pub uptime: u64,
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
#[serde(rename_all = "lowercase")]
pub enum ApiStatus {
Up,
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Default, schemars::JsonSchema, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum ChainStatus {
Synced,
#[default]
Unknown,
Stalled {
#[serde(
serialize_with = "humantime_serde::serialize",
deserialize_with = "humantime_serde::deserialize"
)]
approximate_amount: Duration,
},
}
impl ChainStatus {
pub fn is_synced(&self) -> bool {
matches!(self, ChainStatus::Synced)
}
}
impl ApiHealthResponse {
pub fn new_healthy(uptime: Duration) -> Self {
ApiHealthResponse {
status: ApiStatus::Up,
chain_status: ChainStatus::Synced,
uptime: uptime.as_secs(),
}
}
}
impl ApiStatus {
pub fn is_up(&self) -> bool {
matches!(self, ApiStatus::Up)
}
}
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub struct SignerInformationResponse {
pub cosmos_address: String,
pub identity: String,
pub announce_address: String,
pub verification_key: Option<String>,
}
@@ -0,0 +1,19 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use super::CoinSchema;
use cosmwasm_std::Coin;
use serde::{Deserialize, Serialize};
use utoipa::{ToResponse, ToSchema};
#[derive(Clone, Serialize, Deserialize, schemars::JsonSchema, ToSchema, ToResponse)]
pub struct CirculatingSupplyResponse {
#[schema(value_type = CoinSchema)]
pub total_supply: Coin,
#[schema(value_type = CoinSchema)]
pub mixmining_reserve: Coin,
#[schema(value_type = CoinSchema)]
pub vesting_tokens: Coin,
#[schema(value_type = CoinSchema)]
pub circulating_supply: Coin,
}
@@ -0,0 +1,375 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::models::{BinaryBuildInformationOwned, OffsetDateTimeJsonSchemaWrapper};
use crate::nym_nodes::{BasicEntryInformation, NodeRole, SemiSkimmedNode, SkimmedNode};
use nym_crypto::asymmetric::ed25519::serde_helpers::bs58_ed25519_pubkey;
use nym_crypto::asymmetric::x25519::serde_helpers::bs58_x25519_pubkey;
use nym_crypto::asymmetric::{ed25519, x25519};
use nym_mixnet_contract_common::reward_params::Performance;
use nym_mixnet_contract_common::NodeId;
use nym_network_defaults::{DEFAULT_MIX_LISTENING_PORT, DEFAULT_VERLOC_LISTENING_PORT};
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, NodeRoles};
use nym_noise_keys::VersionedNoiseKey;
use serde::{Deserialize, Serialize};
use std::net::IpAddr;
use tracing::warn;
use utoipa::ToSchema;
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub struct HostInformation {
#[schema(value_type = Vec<String>)]
pub ip_address: Vec<IpAddr>,
pub hostname: Option<String>,
pub keys: HostKeys,
}
impl From<nym_node_requests::api::v1::node::models::HostInformation> for HostInformation {
fn from(value: nym_node_requests::api::v1::node::models::HostInformation) -> Self {
HostInformation {
ip_address: value.ip_address,
hostname: value.hostname,
keys: value.keys.into(),
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub struct HostKeys {
#[serde(with = "bs58_ed25519_pubkey")]
#[schemars(with = "String")]
#[schema(value_type = String)]
pub ed25519: ed25519::PublicKey,
#[deprecated(note = "use the current_x25519_sphinx_key with explicit rotation information")]
#[serde(with = "bs58_x25519_pubkey")]
#[schemars(with = "String")]
#[schema(value_type = String)]
pub x25519: x25519::PublicKey,
pub current_x25519_sphinx_key: SphinxKey,
#[serde(default)]
pub pre_announced_x25519_sphinx_key: Option<SphinxKey>,
#[serde(default)]
pub x25519_versioned_noise: Option<VersionedNoiseKey>,
}
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub struct SphinxKey {
pub rotation_id: u32,
#[serde(with = "bs58_x25519_pubkey")]
#[schemars(with = "String")]
#[schema(value_type = String)]
pub public_key: x25519::PublicKey,
}
impl From<nym_node_requests::api::v1::node::models::SphinxKey> for SphinxKey {
fn from(value: nym_node_requests::api::v1::node::models::SphinxKey) -> Self {
SphinxKey {
rotation_id: value.rotation_id,
public_key: value.public_key,
}
}
}
impl From<nym_node_requests::api::v1::node::models::HostKeys> for HostKeys {
fn from(value: nym_node_requests::api::v1::node::models::HostKeys) -> Self {
HostKeys {
ed25519: value.ed25519_identity,
x25519: value.x25519_sphinx,
current_x25519_sphinx_key: value.primary_x25519_sphinx_key.into(),
pre_announced_x25519_sphinx_key: value.pre_announced_x25519_sphinx_key.map(Into::into),
x25519_versioned_noise: value.x25519_versioned_noise,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub struct WebSockets {
pub ws_port: u16,
pub wss_port: Option<u16>,
}
impl From<nym_node_requests::api::v1::gateway::models::WebSockets> for WebSockets {
fn from(value: nym_node_requests::api::v1::gateway::models::WebSockets) -> Self {
WebSockets {
ws_port: value.ws_port,
wss_port: value.wss_port,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub struct NoiseDetails {
pub key: VersionedNoiseKey,
pub mixnet_port: u16,
#[schema(value_type = Vec<String>)]
pub ip_addresses: Vec<IpAddr>,
}
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub struct NymNodeDescription {
#[schema(value_type = u32)]
pub node_id: NodeId,
pub contract_node_type: DescribedNodeType,
pub description: NymNodeData,
}
impl NymNodeDescription {
pub fn version(&self) -> &str {
&self.description.build_information.build_version
}
pub fn entry_information(&self) -> BasicEntryInformation {
BasicEntryInformation {
hostname: self.description.host_information.hostname.clone(),
ws_port: self.description.mixnet_websockets.ws_port,
wss_port: self.description.mixnet_websockets.wss_port,
}
}
pub fn ed25519_identity_key(&self) -> ed25519::PublicKey {
self.description.host_information.keys.ed25519
}
pub fn current_sphinx_key(&self, current_rotation_id: u32) -> x25519::PublicKey {
let keys = &self.description.host_information.keys;
if keys.current_x25519_sphinx_key.rotation_id == u32::MAX {
// legacy case (i.e. node doesn't support rotation)
return keys.current_x25519_sphinx_key.public_key;
}
if current_rotation_id == keys.current_x25519_sphinx_key.rotation_id {
// it's the 'current' key
return keys.current_x25519_sphinx_key.public_key;
}
if let Some(pre_announced) = &keys.pre_announced_x25519_sphinx_key {
if pre_announced.rotation_id == current_rotation_id {
return pre_announced.public_key;
}
}
warn!(
"unexpected key rotation {current_rotation_id} for node {}",
self.node_id
);
// this should never be reached, but just in case, return the fallback option
keys.current_x25519_sphinx_key.public_key
}
pub fn to_skimmed_node(
&self,
current_rotation_id: u32,
role: NodeRole,
performance: Performance,
) -> SkimmedNode {
let keys = &self.description.host_information.keys;
let entry = if self.description.declared_role.entry {
Some(self.entry_information())
} else {
None
};
SkimmedNode {
node_id: self.node_id,
ed25519_identity_pubkey: keys.ed25519,
ip_addresses: self.description.host_information.ip_address.clone(),
mix_port: self.description.mix_port(),
x25519_sphinx_pubkey: self.current_sphinx_key(current_rotation_id),
// we can't use the declared roles, we have to take whatever was provided in the contract.
// why? say this node COULD operate as an exit, but it might be the case the contract decided
// to assign it an ENTRY role only. we have to use that one instead.
role,
supported_roles: self.description.declared_role,
entry,
performance,
}
}
pub fn to_semi_skimmed_node(
&self,
current_rotation_id: u32,
role: NodeRole,
performance: Performance,
) -> SemiSkimmedNode {
let skimmed_node = self.to_skimmed_node(current_rotation_id, role, performance);
SemiSkimmedNode {
basic: skimmed_node,
x25519_noise_versioned_key: self
.description
.host_information
.keys
.x25519_versioned_noise,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
#[serde(rename_all = "snake_case")]
#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))]
#[cfg_attr(
feature = "generate-ts",
ts(
export,
export_to = "ts-packages/types/src/types/rust/DescribedNodeType.ts"
)
)]
pub enum DescribedNodeType {
LegacyMixnode,
LegacyGateway,
NymNode,
}
impl DescribedNodeType {
pub fn is_nym_node(&self) -> bool {
matches!(self, DescribedNodeType::NymNode)
}
}
#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))]
#[cfg_attr(
feature = "generate-ts",
ts(
export,
export_to = "ts-packages/types/src/types/rust/DeclaredRoles.ts"
)
)]
pub struct DeclaredRoles {
pub mixnode: bool,
pub entry: bool,
pub exit_nr: bool,
pub exit_ipr: bool,
}
impl DeclaredRoles {
pub fn can_operate_exit_gateway(&self) -> bool {
self.exit_ipr && self.exit_nr
}
}
impl From<NodeRoles> for DeclaredRoles {
fn from(value: NodeRoles) -> Self {
DeclaredRoles {
mixnode: value.mixnode_enabled,
entry: value.gateway_enabled,
exit_nr: value.gateway_enabled && value.network_requester_enabled,
exit_ipr: value.gateway_enabled && value.ip_packet_router_enabled,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub struct NetworkRequesterDetails {
/// address of the embedded network requester
pub address: String,
/// flag indicating whether this network requester uses the exit policy rather than the deprecated allow list
pub uses_exit_policy: bool,
}
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub struct IpPacketRouterDetails {
/// address of the embedded ip packet router
pub address: String,
}
// works for current simple case.
impl From<IpPacketRouter> for IpPacketRouterDetails {
fn from(value: IpPacketRouter) -> Self {
IpPacketRouterDetails {
address: value.address,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub struct AuthenticatorDetails {
/// address of the embedded authenticator
pub address: String,
}
// works for current simple case.
impl From<Authenticator> for AuthenticatorDetails {
fn from(value: Authenticator) -> Self {
AuthenticatorDetails {
address: value.address,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub struct WireguardDetails {
pub port: u16,
pub public_key: String,
}
// works for current simple case.
impl From<Wireguard> for WireguardDetails {
fn from(value: Wireguard) -> Self {
WireguardDetails {
port: value.port,
public_key: value.public_key,
}
}
}
// this struct is getting quite bloated...
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub struct NymNodeData {
#[serde(default)]
pub last_polled: OffsetDateTimeJsonSchemaWrapper,
pub host_information: HostInformation,
#[serde(default)]
pub declared_role: DeclaredRoles,
#[serde(default)]
pub auxiliary_details: AuxiliaryDetails,
// TODO: do we really care about ALL build info or just the version?
pub build_information: BinaryBuildInformationOwned,
#[serde(default)]
pub network_requester: Option<NetworkRequesterDetails>,
#[serde(default)]
pub ip_packet_router: Option<IpPacketRouterDetails>,
#[serde(default)]
pub authenticator: Option<AuthenticatorDetails>,
#[serde(default)]
pub wireguard: Option<WireguardDetails>,
// for now we only care about their ws/wss situation, nothing more
pub mixnet_websockets: WebSockets,
}
impl NymNodeData {
pub fn mix_port(&self) -> u16 {
self.auxiliary_details
.announce_ports
.mix_port
.unwrap_or(DEFAULT_MIX_LISTENING_PORT)
}
pub fn verloc_port(&self) -> u16 {
self.auxiliary_details
.announce_ports
.verloc_port
.unwrap_or(DEFAULT_VERLOC_LISTENING_PORT)
}
}
@@ -0,0 +1,364 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use super::{CoinSchema, DeclaredRoles};
use crate::legacy::{
LegacyGatewayBondWithId, LegacyMixNodeBondWithLayer, LegacyMixNodeDetailsWithLayer,
};
use crate::models::{NodePerformance, NymNodeData, StakeSaturation};
use crate::nym_nodes::{BasicEntryInformation, NodeRole, SemiSkimmedNode, SkimmedNode};
use cosmwasm_std::{Addr, Coin, Decimal};
use nym_contracts_common::Percent;
use nym_mixnet_contract_common::reward_params::Performance;
use nym_mixnet_contract_common::rewarding::RewardEstimate;
use nym_mixnet_contract_common::{GatewayBond, Interval, MixNode, NodeId, RewardingParams};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::fmt;
use std::net::IpAddr;
use std::time::Duration;
use thiserror::Error;
use utoipa::{IntoParams, ToSchema};
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub struct LegacyDescribedGateway {
pub bond: GatewayBond,
pub self_described: Option<NymNodeData>,
}
impl From<LegacyGatewayBondWithId> for LegacyDescribedGateway {
fn from(bond: LegacyGatewayBondWithId) -> Self {
LegacyDescribedGateway {
bond: bond.bond,
self_described: None,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub struct LegacyDescribedMixNode {
pub bond: LegacyMixNodeBondWithLayer,
pub self_described: Option<NymNodeData>,
}
impl From<LegacyMixNodeBondWithLayer> for LegacyDescribedMixNode {
fn from(bond: LegacyMixNodeBondWithLayer) -> Self {
LegacyDescribedMixNode {
bond,
self_described: None,
}
}
}
#[deprecated]
#[derive(Clone, Serialize, schemars::JsonSchema, ToSchema)]
pub struct InclusionProbability {
#[schema(value_type = u32)]
pub mix_id: NodeId,
pub in_active: f64,
pub in_reserve: f64,
}
#[deprecated]
#[derive(Clone, Serialize, schemars::JsonSchema, ToSchema)]
pub struct AllInclusionProbabilitiesResponse {
pub inclusion_probabilities: Vec<InclusionProbability>,
pub samples: u64,
pub elapsed: Duration,
pub delta_max: f64,
pub delta_l2: f64,
pub as_at: i64,
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, ToSchema)]
#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))]
#[cfg_attr(
feature = "generate-ts",
ts(
export,
export_to = "ts-packages/types/src/types/rust/SelectionChance.ts"
)
)]
#[deprecated]
pub enum SelectionChance {
High,
Good,
Low,
}
impl From<f64> for SelectionChance {
fn from(p: f64) -> SelectionChance {
match p {
p if p >= 0.7 => SelectionChance::High,
p if p >= 0.3 => SelectionChance::Good,
_ => SelectionChance::Low,
}
}
}
impl From<Decimal> for SelectionChance {
fn from(p: Decimal) -> Self {
match p {
p if p >= Decimal::from_ratio(70u32, 100u32) => SelectionChance::High,
p if p >= Decimal::from_ratio(30u32, 100u32) => SelectionChance::Good,
_ => SelectionChance::Low,
}
}
}
impl fmt::Display for SelectionChance {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
SelectionChance::High => write!(f, "High"),
SelectionChance::Good => write!(f, "Good"),
SelectionChance::Low => write!(f, "Low"),
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, ToSchema)]
#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))]
#[cfg_attr(
feature = "generate-ts",
ts(
export,
export_to = "ts-packages/types/src/types/rust/InclusionProbabilityResponse.ts"
)
)]
#[deprecated]
pub struct InclusionProbabilityResponse {
pub in_active: SelectionChance,
pub in_reserve: SelectionChance,
}
impl fmt::Display for InclusionProbabilityResponse {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"in_active: {}, in_reserve: {}",
self.in_active, self.in_reserve
)
}
}
#[derive(Debug, Serialize, Deserialize, JsonSchema, ToSchema, IntoParams)]
pub struct ComputeRewardEstParam {
#[schema(value_type = Option<String>)]
#[param(value_type = Option<String>)]
pub performance: Option<Performance>,
pub active_in_rewarded_set: Option<bool>,
pub pledge_amount: Option<u64>,
pub total_delegation: Option<u64>,
#[schema(value_type = Option<CoinSchema>)]
#[param(value_type = Option<CoinSchema>)]
pub interval_operating_cost: Option<Coin>,
#[schema(value_type = Option<String>)]
#[param(value_type = Option<String>)]
pub profit_margin_percent: Option<Percent>,
}
#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))]
#[cfg_attr(
feature = "generate-ts",
ts(
export,
export_to = "ts-packages/types/src/types/rust/RewardEstimationResponse.ts"
)
)]
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, ToSchema)]
pub struct RewardEstimationResponse {
pub estimation: RewardEstimate,
pub reward_params: RewardingParams,
pub epoch: Interval,
#[cfg_attr(feature = "generate-ts", ts(type = "number"))]
pub as_at: i64,
}
impl MixNodeBondAnnotated {
pub fn mix_node(&self) -> &MixNode {
&self.mixnode_details.bond_information.mix_node
}
pub fn mix_id(&self) -> NodeId {
self.mixnode_details.mix_id()
}
pub fn identity_key(&self) -> &str {
self.mixnode_details.bond_information.identity()
}
pub fn owner(&self) -> &Addr {
self.mixnode_details.bond_information.owner()
}
pub fn version(&self) -> &str {
&self.mixnode_details.bond_information.mix_node.version
}
pub fn try_to_skimmed_node(&self, role: NodeRole) -> Result<SkimmedNode, MalformedNodeBond> {
Ok(SkimmedNode {
node_id: self.mix_id(),
ed25519_identity_pubkey: self
.identity_key()
.parse()
.map_err(|_| MalformedNodeBond::InvalidEd25519Key)?,
ip_addresses: self.ip_addresses.clone(),
mix_port: self.mix_node().mix_port,
x25519_sphinx_pubkey: self
.mix_node()
.sphinx_key
.parse()
.map_err(|_| MalformedNodeBond::InvalidX25519Key)?,
role,
supported_roles: DeclaredRoles {
mixnode: true,
entry: false,
exit_nr: false,
exit_ipr: false,
},
entry: None,
performance: self.node_performance.last_24h,
})
}
pub fn try_to_semi_skimmed_node(
&self,
role: NodeRole,
) -> Result<SemiSkimmedNode, MalformedNodeBond> {
let skimmed_node = self.try_to_skimmed_node(role)?;
Ok(SemiSkimmedNode {
basic: skimmed_node,
x25519_noise_versioned_key: None, // legacy node won't ever support Noise
})
}
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, ToSchema)]
pub struct GatewayBondAnnotated {
pub gateway_bond: LegacyGatewayBondWithId,
#[serde(default)]
pub self_described: Option<GatewayDescription>,
// NOTE: the performance field is deprecated in favour of node_performance
#[schema(value_type = String)]
pub performance: Performance,
pub node_performance: NodePerformance,
pub blacklisted: bool,
#[serde(default)]
#[schema(value_type = Vec<String>)]
pub ip_addresses: Vec<IpAddr>,
}
impl GatewayBondAnnotated {
pub fn version(&self) -> &str {
&self.gateway_bond.gateway.version
}
pub fn identity(&self) -> &String {
self.gateway_bond.bond.identity()
}
pub fn owner(&self) -> &Addr {
self.gateway_bond.bond.owner()
}
pub fn try_to_skimmed_node(&self, role: NodeRole) -> Result<SkimmedNode, MalformedNodeBond> {
Ok(SkimmedNode {
node_id: self.gateway_bond.node_id,
ip_addresses: self.ip_addresses.clone(),
ed25519_identity_pubkey: self
.gateway_bond
.gateway
.identity_key
.parse()
.map_err(|_| MalformedNodeBond::InvalidEd25519Key)?,
mix_port: self.gateway_bond.bond.gateway.mix_port,
x25519_sphinx_pubkey: self
.gateway_bond
.gateway
.sphinx_key
.parse()
.map_err(|_| MalformedNodeBond::InvalidX25519Key)?,
role,
supported_roles: DeclaredRoles {
mixnode: false,
entry: true,
exit_nr: false,
exit_ipr: false,
},
entry: Some(BasicEntryInformation {
hostname: None,
ws_port: self.gateway_bond.bond.gateway.clients_port,
wss_port: None,
}),
performance: self.node_performance.last_24h,
})
}
pub fn try_to_semi_skimmed_node(
&self,
role: NodeRole,
) -> Result<SemiSkimmedNode, MalformedNodeBond> {
let skimmed_node = self.try_to_skimmed_node(role)?;
Ok(SemiSkimmedNode {
basic: skimmed_node,
x25519_noise_versioned_key: None, // legacy node won't ever support Noise
})
}
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, ToSchema)]
pub struct GatewayDescription {
// for now only expose what we need. this struct will evolve in the future (or be incorporated into nym-node properly)
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, ToSchema)]
#[schema(title = "LegacyMixNodeDetailsWithLayer")]
pub struct LegacyMixNodeDetailsWithLayerSchema {
/// Basic bond information of this mixnode, such as owner address, original pledge, etc.
#[schema(example = "unimplemented schema")]
pub bond_information: String,
/// Details used for computation of rewarding related data.
#[schema(example = "unimplemented schema")]
pub rewarding_details: String,
/// Adjustments to the mixnode that are ought to happen during future epoch transitions.
#[schema(example = "unimplemented schema")]
pub pending_changes: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, ToSchema)]
pub struct MixNodeBondAnnotated {
#[schema(value_type = LegacyMixNodeDetailsWithLayerSchema)]
pub mixnode_details: LegacyMixNodeDetailsWithLayer,
#[schema(value_type = String)]
pub stake_saturation: StakeSaturation,
#[schema(value_type = String)]
pub uncapped_stake_saturation: StakeSaturation,
// NOTE: the performance field is deprecated in favour of node_performance
#[schema(value_type = String)]
pub performance: Performance,
pub node_performance: NodePerformance,
#[schema(value_type = String)]
pub estimated_operator_apy: Decimal,
#[schema(value_type = String)]
pub estimated_delegators_apy: Decimal,
pub blacklisted: bool,
// a rather temporary thing until we query self-described endpoints of mixnodes
#[serde(default)]
#[schema(value_type = Vec<String>)]
pub ip_addresses: Vec<IpAddr>,
}
#[derive(Debug, Error)]
pub enum MalformedNodeBond {
#[error("the associated ed25519 identity key is malformed")]
InvalidEd25519Key,
#[error("the associated x25519 sphinx key is malformed")]
InvalidX25519Key,
}
@@ -0,0 +1,242 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use nym_mixnet_contract_common::nym_node::Role;
use nym_mixnet_contract_common::{EpochId, KeyRotationId, KeyRotationState, NodeId};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::cmp::Ordering;
use std::time::Duration;
use time::OffsetDateTime;
use tracing::warn;
use utoipa::ToSchema;
#[derive(Clone, Copy, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub struct KeyRotationInfoResponse {
#[serde(flatten)]
pub details: KeyRotationDetails,
// helper field that holds calculated data based on the `details` field
// this is to expose the information in a format more easily accessible by humans
// without having to do any calculations
pub progress: KeyRotationProgressInfo,
}
impl From<KeyRotationDetails> for KeyRotationInfoResponse {
fn from(details: KeyRotationDetails) -> Self {
KeyRotationInfoResponse {
details,
progress: KeyRotationProgressInfo {
current_key_rotation_id: details.current_key_rotation_id(),
current_rotation_starting_epoch: details.current_rotation_starting_epoch_id(),
current_rotation_ending_epoch: details.current_rotation_starting_epoch_id()
+ details.key_rotation_state.validity_epochs
- 1,
},
}
}
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub struct KeyRotationProgressInfo {
pub current_key_rotation_id: u32,
pub current_rotation_starting_epoch: u32,
pub current_rotation_ending_epoch: u32,
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub struct KeyRotationDetails {
pub key_rotation_state: KeyRotationState,
#[schema(value_type = u32)]
pub current_absolute_epoch_id: EpochId,
#[serde(with = "time::serde::rfc3339")]
#[schemars(with = "String")]
#[schema(value_type = String)]
pub current_epoch_start: OffsetDateTime,
pub epoch_duration: Duration,
}
impl KeyRotationDetails {
pub fn current_key_rotation_id(&self) -> u32 {
self.key_rotation_state
.key_rotation_id(self.current_absolute_epoch_id)
}
pub fn next_rotation_starting_epoch_id(&self) -> EpochId {
self.key_rotation_state
.next_rotation_starting_epoch_id(self.current_absolute_epoch_id)
}
pub fn current_rotation_starting_epoch_id(&self) -> EpochId {
self.key_rotation_state
.current_rotation_starting_epoch_id(self.current_absolute_epoch_id)
}
fn current_epoch_progress(&self, now: OffsetDateTime) -> f32 {
let elapsed = (now - self.current_epoch_start).as_seconds_f32();
elapsed / self.epoch_duration.as_secs_f32()
}
pub fn is_epoch_stuck(&self) -> bool {
let now = OffsetDateTime::now_utc();
let progress = self.current_epoch_progress(now);
if progress > 1. {
let into_next = 1. - progress;
// if epoch hasn't progressed for more than 20% of its duration, mark is as stuck
if into_next > 0.2 {
let diff_time =
Duration::from_secs_f32(into_next * self.epoch_duration.as_secs_f32());
let expected_epoch_end = self.current_epoch_start + self.epoch_duration;
warn!("the current epoch is expected to have been over by {expected_epoch_end}. it's already {} overdue!", humantime_serde::re::humantime::format_duration(diff_time));
return true;
}
}
false
}
// based on the current **TIME**, determine what's the expected current rotation id
pub fn expected_current_rotation_id(&self) -> KeyRotationId {
let now = OffsetDateTime::now_utc();
let current_end = now + self.epoch_duration;
if now < current_end {
return self
.key_rotation_state
.key_rotation_id(self.current_absolute_epoch_id);
}
let diff = now - current_end;
let passed_epochs = diff / self.epoch_duration;
let expected_current_epoch = self.current_absolute_epoch_id + passed_epochs.floor() as u32;
self.key_rotation_state
.key_rotation_id(expected_current_epoch)
}
pub fn until_next_rotation(&self) -> Option<Duration> {
let current_epoch_progress = self.current_epoch_progress(OffsetDateTime::now_utc());
if current_epoch_progress > 1. {
return None;
}
let next_rotation_epoch = self.next_rotation_starting_epoch_id();
let full_remaining =
(next_rotation_epoch - self.current_absolute_epoch_id).checked_add(1)?;
let epochs_until_next_rotation = (1. - current_epoch_progress) + full_remaining as f32;
Some(Duration::from_secs_f32(
epochs_until_next_rotation * self.epoch_duration.as_secs_f32(),
))
}
pub fn epoch_start_time(&self, absolute_epoch_id: EpochId) -> OffsetDateTime {
match absolute_epoch_id.cmp(&self.current_absolute_epoch_id) {
Ordering::Less => {
let diff = self.current_absolute_epoch_id - absolute_epoch_id;
self.current_epoch_start - diff * self.epoch_duration
}
Ordering::Equal => self.current_epoch_start,
Ordering::Greater => {
let diff = absolute_epoch_id - self.current_absolute_epoch_id;
self.current_epoch_start + diff * self.epoch_duration
}
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub struct RewardedSetResponse {
#[serde(default)]
#[schema(value_type = u32)]
pub epoch_id: EpochId,
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>,
}
impl From<RewardedSetResponse> for nym_mixnet_contract_common::EpochRewardedSet {
fn from(res: RewardedSetResponse) -> Self {
nym_mixnet_contract_common::EpochRewardedSet {
epoch_id: res.epoch_id,
assignment: nym_mixnet_contract_common::RewardedSet {
entry_gateways: res.entry_gateways,
exit_gateways: res.exit_gateways,
layer1: res.layer1,
layer2: res.layer2,
layer3: res.layer3,
standby: res.standby,
},
}
}
}
impl From<nym_mixnet_contract_common::EpochRewardedSet> for RewardedSetResponse {
fn from(r: nym_mixnet_contract_common::EpochRewardedSet) -> Self {
RewardedSetResponse {
epoch_id: r.epoch_id,
entry_gateways: r.assignment.entry_gateways,
exit_gateways: r.assignment.exit_gateways,
layer1: r.assignment.layer1,
layer2: r.assignment.layer2,
layer3: r.assignment.layer3,
standby: r.assignment.standby,
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, JsonSchema, ToSchema)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))]
#[cfg_attr(
feature = "generate-ts",
ts(export, export_to = "ts-packages/types/src/types/rust/DisplayRole.ts")
)]
pub enum DisplayRole {
EntryGateway,
Layer1,
Layer2,
Layer3,
ExitGateway,
Standby,
}
impl From<Role> for DisplayRole {
fn from(role: Role) -> Self {
match role {
Role::EntryGateway => DisplayRole::EntryGateway,
Role::Layer1 => DisplayRole::Layer1,
Role::Layer2 => DisplayRole::Layer2,
Role::Layer3 => DisplayRole::Layer3,
Role::ExitGateway => DisplayRole::ExitGateway,
Role::Standby => DisplayRole::Standby,
}
}
}
impl From<DisplayRole> for Role {
fn from(role: DisplayRole) -> Self {
match role {
DisplayRole::EntryGateway => Role::EntryGateway,
DisplayRole::Layer1 => Role::Layer1,
DisplayRole::Layer2 => Role::Layer2,
DisplayRole::Layer3 => Role::Layer3,
DisplayRole::ExitGateway => Role::ExitGateway,
DisplayRole::Standby => Role::Standby,
}
}
}
@@ -0,0 +1,62 @@
// Copyright 2022-2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
#![allow(deprecated)]
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::fmt;
use std::fmt::{Display, Formatter};
pub mod api_status;
pub mod circulating_supply;
pub mod described;
pub mod legacy;
pub mod mixnet;
pub mod network;
pub mod network_monitor;
pub mod node_status;
pub mod schema_helpers;
// don't break existing imports
pub use api_status::*;
pub use circulating_supply::*;
pub use described::*;
pub use legacy::*;
pub use mixnet::*;
pub use network::*;
pub use network_monitor::*;
pub use node_status::*;
pub use schema_helpers::*;
pub use nym_mixnet_contract_common::{EpochId, KeyRotationId, KeyRotationState};
pub use nym_node_requests::api::v1::node::models::BinaryBuildInformationOwned;
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
pub struct RequestError {
message: String,
}
impl RequestError {
pub fn new<S: Into<String>>(msg: S) -> Self {
RequestError {
message: msg.into(),
}
}
pub fn message(&self) -> &str {
&self.message
}
pub fn empty() -> Self {
Self {
message: String::new(),
}
}
}
impl Display for RequestError {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
Display::fmt(&self.message, f)
}
}
@@ -0,0 +1,562 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::ecash::models::EcashSignerStatusResponse;
use crate::models::tendermint_types::{BlockHeader, BlockId};
use crate::models::{ChainStatus, SignerInformationResponse};
use crate::signable::SignedMessage;
use nym_coconut_dkg_common::types::EpochId;
use nym_crypto::asymmetric::ed25519::PublicKey;
use nym_ecash_signer_check_types::helper_traits::{
ChainResponse, LegacyChainResponse, LegacySignerResponse, SignerResponse, TimestampedResponse,
Verifiable,
};
use nym_ecash_signer_check_types::status::SignerResult;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::time::Duration;
use time::OffsetDateTime;
use utoipa::ToSchema;
pub type ChainBlocksStatusResponse = SignedMessage<ChainBlocksStatusResponseBody>;
pub type SignersStatusResponse = SignedMessage<SignersStatusResponseBody>;
pub type DetailedSignersStatusResponse = SignedMessage<DetailedSignersStatusResponseBody>;
#[derive(Serialize, Deserialize, ToSchema, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct SignersStatusResponseBody {
#[serde(with = "time::serde::rfc3339")]
#[schema(value_type = String)]
pub as_at: OffsetDateTime,
pub overview: SignersStatusOverview,
pub results: Vec<MinimalSignerResult>,
}
pub type TypedSignerResult = SignerResult<
SignerInformationResponse,
EcashSignerStatusResponse,
ChainStatusResponse,
ChainBlocksStatusResponse,
>;
#[derive(Serialize, Deserialize, ToSchema, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct MinimalSignerResult {
pub announce_address: String,
pub owner_address: String,
pub node_index: u64,
pub public_key: String,
pub local_chain_working: bool,
pub credential_issuance_available: bool,
}
impl From<&TypedSignerResult> for MinimalSignerResult {
fn from(result: &TypedSignerResult) -> MinimalSignerResult {
MinimalSignerResult {
announce_address: result.information.announce_address.clone(),
owner_address: result.information.owner_address.clone(),
node_index: result.information.node_index,
public_key: result.information.public_key.clone(),
local_chain_working: result.chain_available(),
credential_issuance_available: result.signing_available(),
}
}
}
#[derive(Serialize, Deserialize, ToSchema, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct DetailedSignersStatusResponseBody {
#[serde(with = "time::serde::rfc3339")]
#[schema(value_type = String)]
pub as_at: OffsetDateTime,
pub overview: SignersStatusOverview,
pub details: Vec<TypedSignerResult>,
}
#[derive(Serialize, Deserialize, ToSchema, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct SignersStatusOverview {
#[schema(value_type = Option<u64>)]
pub epoch_id: Option<EpochId>,
pub signing_threshold: Option<u64>,
pub threshold_available: Option<bool>,
pub total_signers: usize,
pub unreachable_signers: usize,
pub malformed_signers: usize,
// unreachable or outdated
pub unknown_local_chain_status: usize,
pub working_local_chain: usize,
// i.e. provided signature
pub provably_stalled_local_chain: usize,
pub unprovably_stalled_local_chain: usize,
// unreachable or outdated
pub unknown_credential_issuance_status: usize,
pub working_credential_issuance: usize,
// i.e. provided signature
pub provably_unavailable_credential_issuance: usize,
pub unprovably_unavailable_credential_issuance: usize,
}
impl SignersStatusOverview {
pub fn new(results: &[TypedSignerResult], signing_threshold: Option<u64>) -> Self {
let epoch_id = results.first().map(|r| r.dkg_epoch_id);
let mut unreachable_signers = 0;
let mut malformed_signers = 0;
let mut unknown_local_chain_status = 0;
let mut working_local_chain = 0;
let mut provably_stalled_local_chain = 0;
let mut unprovably_stalled_local_chain = 0;
let mut unknown_credential_issuance_status = 0;
let mut working_credential_issuance = 0;
let mut provably_unavailable_credential_issuance = 0;
let mut unprovably_unavailable_credential_issuance = 0;
for result in results {
if result.signer_unreachable() {
unreachable_signers += 1;
}
if result.malformed_details() {
malformed_signers += 1;
}
if result.unknown_chain_status() {
unknown_local_chain_status += 1;
}
if result.chain_available() {
working_local_chain += 1;
}
if result.chain_provably_stalled() {
provably_stalled_local_chain += 1;
}
if result.chain_unprovably_stalled() {
unprovably_stalled_local_chain += 1;
}
if result.unknown_signing_status() {
unknown_credential_issuance_status += 1;
}
if result.signing_available() {
working_credential_issuance += 1;
}
if result.signing_provably_unavailable() {
provably_unavailable_credential_issuance += 1;
}
if result.signing_unprovably_unavailable() {
unprovably_unavailable_credential_issuance += 1;
}
}
SignersStatusOverview {
epoch_id,
signing_threshold,
threshold_available: signing_threshold.map(|threshold| {
(working_local_chain as u64) >= threshold
&& (working_credential_issuance as u64) >= threshold
}),
total_signers: results.len(),
unreachable_signers,
malformed_signers,
unknown_local_chain_status,
working_local_chain,
provably_stalled_local_chain,
unprovably_stalled_local_chain,
unknown_credential_issuance_status,
working_credential_issuance,
provably_unavailable_credential_issuance,
unprovably_unavailable_credential_issuance,
}
}
}
#[derive(Serialize, Deserialize, ToSchema, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct ChainBlocksStatusResponseBody {
#[serde(with = "time::serde::rfc3339")]
#[schema(value_type = String)]
pub current_time: OffsetDateTime,
pub latest_cached_block: Option<DetailedChainStatus>,
// explicit indication of THIS signer whether it thinks the chain is stalled
pub chain_status: ChainStatus,
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, ToSchema)]
pub struct ChainStatusResponse {
pub connected_nyxd: String,
pub status: DetailedChainStatus,
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, ToSchema)]
pub struct DetailedChainStatus {
pub abci: crate::models::tendermint_types::AbciInfo,
pub latest_block: BlockInfo,
}
impl DetailedChainStatus {
pub fn stall_status(&self, now: OffsetDateTime, threshold: Duration) -> ChainStatus {
let block_time: OffsetDateTime = self.latest_block.block.header.time.into();
let diff = now - block_time;
if diff > threshold {
ChainStatus::Stalled {
approximate_amount: diff.unsigned_abs(),
}
} else {
ChainStatus::Synced
}
}
}
#[derive(Clone, Debug, 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, Debug, 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, Debug, 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(),
}
}
}
}
// implement required traits for the signer responses
impl LegacyChainResponse for ChainStatusResponse {
fn chain_synced(&self, now: OffsetDateTime, stall_threshold: Duration) -> bool {
self.status.stall_status(now, stall_threshold).is_synced()
}
}
impl Verifiable for ChainBlocksStatusResponse {
fn verify_signature(&self, pub_key: &PublicKey) -> bool {
self.verify_signature(pub_key)
}
}
impl TimestampedResponse for ChainBlocksStatusResponse {
fn timestamp(&self) -> OffsetDateTime {
self.body.current_time
}
}
impl ChainResponse for ChainBlocksStatusResponse {
fn chain_synced(&self) -> bool {
self.body.chain_status.is_synced()
}
}
impl LegacySignerResponse for SignerInformationResponse {
fn signer_identity(&self) -> &str {
&self.identity
}
fn signer_verification_key(&self) -> &Option<String> {
&self.verification_key
}
}
impl Verifiable for EcashSignerStatusResponse {
fn verify_signature(&self, pub_key: &PublicKey) -> bool {
self.verify_signature(pub_key)
}
}
impl TimestampedResponse for EcashSignerStatusResponse {
fn timestamp(&self) -> OffsetDateTime {
self.body.current_time
}
}
impl SignerResponse for EcashSignerStatusResponse {
fn has_signing_keys(&self) -> bool {
self.body.has_signing_keys
}
fn signer_disabled(&self) -> bool {
self.body.signer_disabled
}
fn is_ecash_signer(&self) -> bool {
self.body.is_ecash_signer
}
fn dkg_ecash_epoch_id(&self) -> EpochId {
self.body.dkg_ecash_epoch_id
}
}
@@ -0,0 +1,74 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::pagination::PaginatedResponse;
use nym_mixnet_contract_common::NodeId;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use utoipa::ToSchema;
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, Default, ToSchema)]
pub struct TestNode {
pub node_id: Option<u32>,
pub identity_key: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub struct TestRoute {
pub gateway: TestNode,
pub layer1: TestNode,
pub layer2: TestNode,
pub layer3: TestNode,
}
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub struct PartialTestResult {
pub monitor_run_id: i64,
pub timestamp: i64,
pub overall_reliability_for_all_routes_in_monitor_run: Option<u8>,
pub test_routes: TestRoute,
}
pub type MixnodeTestResultResponse = PaginatedResponse<PartialTestResult>;
pub type GatewayTestResultResponse = PaginatedResponse<PartialTestResult>;
#[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<u8, usize>,
pub gateway_results: BTreeMap<u8, usize>,
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, ToSchema)]
#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))]
#[cfg_attr(
feature = "generate-ts",
ts(
export,
export_to = "ts-packages/types/src/types/rust/MixnodeCoreStatusResponse.ts"
)
)]
pub struct MixnodeCoreStatusResponse {
pub mix_id: NodeId,
pub count: i64,
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, ToSchema)]
#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))]
#[cfg_attr(
feature = "generate-ts",
ts(
export,
export_to = "ts-packages/types/src/types/rust/GatewayCoreStatusResponse.ts"
)
)]
pub struct GatewayCoreStatusResponse {
pub identity: String,
pub count: i64,
}
@@ -0,0 +1,587 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::helpers::PlaceholderJsonSchemaImpl;
use crate::pagination::PaginatedResponse;
use cosmwasm_std::Decimal;
use nym_contracts_common::{IdentityKey, NaiveFloat};
use nym_crypto::asymmetric::ed25519;
use nym_crypto::asymmetric::ed25519::serde_helpers::bs58_ed25519_pubkey;
use nym_mixnet_contract_common::reward_params::Performance;
use nym_mixnet_contract_common::NodeId;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::time::Duration;
use time::{Date, OffsetDateTime};
use utoipa::ToSchema;
use crate::models::DisplayRole;
pub use config_score::*;
pub type StakeSaturation = Decimal;
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, ToSchema)]
#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))]
#[cfg_attr(
feature = "generate-ts",
ts(
export,
export_to = "ts-packages/types/src/types/rust/StakeSaturationResponse.ts"
)
)]
pub struct StakeSaturationResponse {
#[cfg_attr(feature = "generate-ts", ts(type = "string"))]
#[schema(value_type = String)]
pub saturation: StakeSaturation,
#[cfg_attr(feature = "generate-ts", ts(type = "string"))]
#[schema(value_type = String)]
pub uncapped_saturation: StakeSaturation,
pub as_at: i64,
}
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(),
}
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub struct NodeRefreshBody {
#[serde(with = "bs58_ed25519_pubkey")]
#[schemars(with = "String")]
#[schema(value_type = String)]
pub node_identity: ed25519::PublicKey,
// a poor man's nonce
pub request_timestamp: i64,
#[schemars(with = "PlaceholderJsonSchemaImpl")]
#[schema(value_type = String)]
pub signature: ed25519::Signature,
}
impl NodeRefreshBody {
pub fn plaintext(node_identity: ed25519::PublicKey, request_timestamp: i64) -> Vec<u8> {
node_identity
.to_bytes()
.into_iter()
.chain(request_timestamp.to_be_bytes())
.chain(b"describe-cache-refresh-request".iter().copied())
.collect()
}
pub fn new(private_key: &ed25519::PrivateKey) -> Self {
let node_identity = private_key.public_key();
let request_timestamp = OffsetDateTime::now_utc().unix_timestamp();
let signature = private_key.sign(Self::plaintext(node_identity, request_timestamp));
NodeRefreshBody {
node_identity,
request_timestamp,
signature,
}
}
pub fn verify_signature(&self) -> bool {
self.node_identity
.verify(
Self::plaintext(self.node_identity, self.request_timestamp),
&self.signature,
)
.is_ok()
}
pub fn is_stale(&self) -> bool {
let Ok(encoded) = OffsetDateTime::from_unix_timestamp(self.request_timestamp) else {
return true;
};
let now = OffsetDateTime::now_utc();
if encoded > now {
return true;
}
if (encoded + Duration::from_secs(30)) < now {
return true;
}
false
}
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, ToSchema)]
pub struct UptimeResponse {
#[schema(value_type = u32)]
pub mix_id: NodeId,
// The same as node_performance.last_24h. Legacy
pub avg_uptime: u8,
pub node_performance: NodePerformance,
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, ToSchema)]
pub struct GatewayUptimeResponse {
pub identity: String,
// The same as node_performance.last_24h. Legacy
pub avg_uptime: u8,
pub node_performance: NodePerformance,
}
type Uptime = u8;
#[derive(Clone, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub struct MixnodeStatusReportResponse {
pub mix_id: NodeId,
pub identity: IdentityKey,
pub owner: String,
#[schema(value_type = u8)]
pub most_recent: Uptime,
#[schema(value_type = u8)]
pub last_hour: Uptime,
#[schema(value_type = u8)]
pub last_day: Uptime,
}
#[derive(Clone, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub struct GatewayStatusReportResponse {
pub identity: String,
pub owner: String,
#[schema(value_type = u8)]
pub most_recent: Uptime,
#[schema(value_type = u8)]
pub last_hour: Uptime,
#[schema(value_type = u8)]
pub last_day: Uptime,
}
#[derive(Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))]
#[cfg_attr(
feature = "generate-ts",
ts(
export,
export_to = "ts-packages/types/src/types/rust/PerformanceHistoryResponse.ts"
)
)]
pub struct PerformanceHistoryResponse {
#[schema(value_type = u32)]
pub node_id: NodeId,
pub history: PaginatedResponse<HistoricalPerformanceResponse>,
}
#[derive(Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))]
#[cfg_attr(
feature = "generate-ts",
ts(
export,
export_to = "ts-packages/types/src/types/rust/UptimeHistoryResponse.ts"
)
)]
pub struct UptimeHistoryResponse {
#[schema(value_type = u32)]
pub node_id: NodeId,
pub history: PaginatedResponse<HistoricalUptimeResponse>,
}
#[derive(Clone, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))]
#[cfg_attr(
feature = "generate-ts",
ts(
export,
export_to = "ts-packages/types/src/types/rust/HistoricalUptimeResponse.ts"
)
)]
pub struct HistoricalUptimeResponse {
#[schema(value_type = String, example = "1970-01-01")]
#[schemars(with = "String")]
#[cfg_attr(feature = "generate-ts", ts(type = "string"))]
pub date: Date,
pub uptime: Uptime,
}
#[derive(Clone, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))]
#[cfg_attr(
feature = "generate-ts",
ts(
export,
export_to = "ts-packages/types/src/types/rust/HistoricalPerformanceResponse.ts"
)
)]
pub struct HistoricalPerformanceResponse {
#[schema(value_type = String, example = "1970-01-01")]
#[schemars(with = "String")]
#[cfg_attr(feature = "generate-ts", ts(type = "string"))]
pub date: Date,
pub performance: f64,
}
#[derive(Clone, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub struct OldHistoricalUptimeResponse {
pub date: String,
#[schema(value_type = u8)]
pub uptime: Uptime,
}
#[derive(Clone, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub struct MixnodeUptimeHistoryResponse {
pub mix_id: NodeId,
pub identity: String,
pub owner: String,
pub history: Vec<OldHistoricalUptimeResponse>,
}
#[derive(Clone, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub struct GatewayUptimeHistoryResponse {
pub identity: String,
pub owner: String,
pub history: Vec<OldHistoricalUptimeResponse>,
}
#[derive(
Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, ToSchema, Default,
)]
#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))]
#[cfg_attr(
feature = "generate-ts",
ts(
export,
export_to = "ts-packages/types/src/types/rust/MixnodeStatus.ts"
)
)]
#[serde(rename_all = "snake_case")]
pub enum MixnodeStatus {
Active, // in both the active set and the rewarded set
Standby, // only in the rewarded set
Inactive, // in neither the rewarded set nor the active set, but is bonded
#[default]
NotFound, // doesn't even exist in the bonded set
}
impl MixnodeStatus {
pub fn is_active(&self) -> bool {
*self == MixnodeStatus::Active
}
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, ToSchema)]
#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))]
#[cfg_attr(
feature = "generate-ts",
ts(
export,
export_to = "ts-packages/types/src/types/rust/MixnodeStatusResponse.ts"
)
)]
pub struct MixnodeStatusResponse {
pub status: MixnodeStatus,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, ToSchema)]
pub struct NodePerformance {
#[schema(value_type = String)]
pub most_recent: Performance,
#[schema(value_type = String)]
pub last_hour: Performance,
#[schema(value_type = String)]
pub last_24h: Performance,
}
// imo for now there's no point in exposing more than that,
// nym-api shouldn't be calculating apy or stake saturation for you.
// it should just return its own metrics (performance) and then you can do with it as you wish
#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema, ToSchema)]
#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))]
#[cfg_attr(
feature = "generate-ts",
ts(
export,
export_to = "ts-packages/types/src/types/rust/NodeAnnotation.ts"
)
)]
pub struct NodeAnnotation {
#[cfg_attr(feature = "generate-ts", ts(type = "string"))]
// legacy
#[schema(value_type = String)]
pub last_24h_performance: Performance,
pub current_role: Option<DisplayRole>,
pub detailed_performance: DetailedNodePerformance,
}
#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema, ToSchema)]
#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))]
#[cfg_attr(
feature = "generate-ts",
ts(
export,
export_to = "ts-packages/types/src/types/rust/DetailedNodePerformance.ts"
)
)]
#[non_exhaustive]
pub struct DetailedNodePerformance {
/// routing_score * config_score
pub performance_score: f64,
pub routing_score: RoutingScore,
pub config_score: ConfigScore,
}
impl DetailedNodePerformance {
pub fn new(
performance_score: f64,
routing_score: RoutingScore,
config_score: ConfigScore,
) -> DetailedNodePerformance {
Self {
performance_score,
routing_score,
config_score,
}
}
pub fn to_rewarding_performance(&self) -> Performance {
Performance::naive_try_from_f64(self.performance_score).unwrap_or_default()
}
}
#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema, ToSchema)]
#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))]
#[cfg_attr(
feature = "generate-ts",
ts(export, export_to = "ts-packages/types/src/types/rust/RoutingScore.ts")
)]
#[non_exhaustive]
pub struct RoutingScore {
/// Total score after taking all the criteria into consideration
pub score: f64,
}
impl RoutingScore {
pub fn new(score: f64) -> RoutingScore {
Self { score }
}
pub const fn zero() -> RoutingScore {
RoutingScore { score: 0.0 }
}
pub fn legacy_performance(&self) -> Performance {
Performance::naive_try_from_f64(self.score).unwrap_or_default()
}
}
#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema, ToSchema)]
#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))]
#[cfg_attr(
feature = "generate-ts",
ts(export, export_to = "ts-packages/types/src/types/rust/ConfigScore.ts")
)]
#[non_exhaustive]
pub struct ConfigScore {
/// Total score after taking all the criteria into consideration
pub score: f64,
pub versions_behind: Option<u32>,
pub self_described_api_available: bool,
pub accepted_terms_and_conditions: bool,
pub runs_nym_node_binary: bool,
}
impl ConfigScore {
pub fn new(
score: f64,
versions_behind: u32,
accepted_terms_and_conditions: bool,
runs_nym_node_binary: bool,
) -> ConfigScore {
Self {
score,
versions_behind: Some(versions_behind),
self_described_api_available: true,
accepted_terms_and_conditions,
runs_nym_node_binary,
}
}
pub fn bad_semver() -> ConfigScore {
ConfigScore {
score: 0.0,
versions_behind: None,
self_described_api_available: true,
accepted_terms_and_conditions: false,
runs_nym_node_binary: false,
}
}
pub fn unavailable() -> ConfigScore {
ConfigScore {
score: 0.0,
versions_behind: None,
self_described_api_available: false,
accepted_terms_and_conditions: false,
runs_nym_node_binary: false,
}
}
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, ToSchema)]
#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))]
#[cfg_attr(
feature = "generate-ts",
ts(
export,
export_to = "ts-packages/types/src/types/rust/AnnotationResponse.ts"
)
)]
pub struct AnnotationResponse {
#[schema(value_type = u32)]
pub node_id: NodeId,
pub annotation: Option<NodeAnnotation>,
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, ToSchema)]
#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))]
#[cfg_attr(
feature = "generate-ts",
ts(
export,
export_to = "ts-packages/types/src/types/rust/NodePerformanceResponse.ts"
)
)]
pub struct NodePerformanceResponse {
#[schema(value_type = u32)]
pub node_id: NodeId,
pub performance: Option<f64>,
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, ToSchema)]
#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))]
#[cfg_attr(
feature = "generate-ts",
ts(
export,
export_to = "ts-packages/types/src/types/rust/NodeDatePerformanceResponse.ts"
)
)]
pub struct NodeDatePerformanceResponse {
#[schema(value_type = u32)]
pub node_id: NodeId,
#[schema(value_type = String, example = "1970-01-01")]
#[schemars(with = "String")]
#[cfg_attr(feature = "generate-ts", ts(type = "string"))]
pub date: Date,
pub performance: Option<f64>,
}
@@ -0,0 +1,128 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::helpers::unix_epoch;
use cosmwasm_std::Uint128;
use schemars::schema::{InstanceType, Schema, SchemaObject};
use schemars::{JsonSchema, SchemaGenerator};
use serde::{Deserialize, Deserializer, Serialize};
use std::fmt;
use std::fmt::{Display, Formatter};
use std::ops::{Deref, DerefMut};
use time::OffsetDateTime;
use utoipa::ToSchema;
#[derive(ToSchema)]
#[schema(title = "Coin")]
pub struct CoinSchema {
pub denom: String,
#[schema(value_type = String)]
pub amount: Uint128,
}
pub fn de_rfc3339_or_default<'de, D>(deserializer: D) -> Result<OffsetDateTime, D::Error>
where
D: Deserializer<'de>,
{
Ok(time::serde::rfc3339::deserialize(deserializer).unwrap_or_else(|_| unix_epoch()))
}
// for all intents and purposes it's just OffsetDateTime, but we need JsonSchema...
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, ToSchema)]
pub struct OffsetDateTimeJsonSchemaWrapper(
#[serde(
default = "unix_epoch",
with = "crate::helpers::overengineered_offset_date_time_serde"
)]
#[schema(inline)]
pub OffsetDateTime,
);
impl Default for OffsetDateTimeJsonSchemaWrapper {
fn default() -> Self {
OffsetDateTimeJsonSchemaWrapper(unix_epoch())
}
}
impl Display for OffsetDateTimeJsonSchemaWrapper {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
Display::fmt(&self.0, f)
}
}
impl From<OffsetDateTimeJsonSchemaWrapper> for OffsetDateTime {
fn from(value: OffsetDateTimeJsonSchemaWrapper) -> Self {
value.0
}
}
impl From<OffsetDateTime> for OffsetDateTimeJsonSchemaWrapper {
fn from(value: OffsetDateTime) -> Self {
OffsetDateTimeJsonSchemaWrapper(value)
}
}
impl Deref for OffsetDateTimeJsonSchemaWrapper {
type Target = OffsetDateTime;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for OffsetDateTimeJsonSchemaWrapper {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
// implementation taken from: https://github.com/GREsau/schemars/pull/207
impl JsonSchema for OffsetDateTimeJsonSchemaWrapper {
fn is_referenceable() -> bool {
false
}
fn schema_name() -> String {
"DateTime".into()
}
fn json_schema(_: &mut SchemaGenerator) -> Schema {
SchemaObject {
instance_type: Some(InstanceType::String.into()),
format: Some("date-time".into()),
..Default::default()
}
.into()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
#[test]
fn offset_date_time_json_schema_wrapper_serde_backwards_compat() {
let mut dummy = OffsetDateTimeJsonSchemaWrapper::default();
dummy.0 += Duration::from_millis(1);
let ser = serde_json::to_string(&dummy).unwrap();
assert_eq!("\"1970-01-01 00:00:00.001 +00:00:00\"", ser);
let human_readable = "\"2024-05-23 07:41:02.756283766 +00:00:00\"";
let rfc3339 = "\"2002-10-02T15:00:00Z\"";
let rfc3339_offset = "\"2002-10-02T10:00:00-05:00\"";
let de = serde_json::from_str::<OffsetDateTimeJsonSchemaWrapper>(human_readable).unwrap();
assert_eq!(de.0.unix_timestamp(), 1716450062);
let de = serde_json::from_str::<OffsetDateTimeJsonSchemaWrapper>(rfc3339).unwrap();
assert_eq!(de.0.unix_timestamp(), 1033570800);
let de = serde_json::from_str::<OffsetDateTimeJsonSchemaWrapper>(rfc3339_offset).unwrap();
assert_eq!(de.0.unix_timestamp(), 1033570800);
let de = serde_json::from_str::<OffsetDateTimeJsonSchemaWrapper>("\"nonsense\"").unwrap();
assert_eq!(de.0.unix_timestamp(), 0);
}
}
+75
View File
@@ -0,0 +1,75 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use nym_crypto::asymmetric::ed25519;
use nym_crypto::asymmetric::ed25519::serde_helpers::bs58_ed25519_signature;
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
// the trait is not public as it's only defined on types that are guaranteed to not panic when serialised
pub trait SignableMessageBody: Serialize + sealed::Sealed {
fn sign(self, key: &ed25519::PrivateKey) -> SignedMessage<Self>
where
Self: Sized,
{
let signature = key.sign(self.plaintext());
SignedMessage {
body: self,
signature,
}
}
fn plaintext(&self) -> Vec<u8> {
#[allow(clippy::unwrap_used)]
// SAFETY: all types that implement this trait have valid serialisations
serde_json::to_vec(&self).unwrap()
}
}
impl<T> SignableMessageBody for T where T: Serialize + sealed::Sealed {}
#[derive(Clone, Serialize, Deserialize, Debug, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct SignedMessage<T> {
pub body: T,
#[schema(value_type = String)]
#[serde(with = "bs58_ed25519_signature")]
pub signature: ed25519::Signature,
}
impl<T> SignedMessage<T> {
pub fn verify_signature(&self, pub_key: &ed25519::PublicKey) -> bool
where
T: SignableMessageBody,
{
let plaintext = self.body.plaintext();
if plaintext.is_empty() {
return false;
}
pub_key.verify(&plaintext, &self.signature).is_ok()
}
}
// make sure only our types can implement this trait (to ensure infallible serialisation)
pub(crate) mod sealed {
use crate::ecash::models::*;
use crate::models::{
ChainBlocksStatusResponseBody, DetailedSignersStatusResponseBody, SignersStatusResponseBody,
};
pub trait Sealed {}
// requests
impl Sealed for IssuedTicketbooksChallengeCommitmentRequestBody {}
impl Sealed for IssuedTicketbooksDataRequestBody {}
// responses
impl Sealed for IssuedTicketbooksChallengeCommitmentResponseBody {}
impl Sealed for IssuedTicketbooksForResponseBody {}
impl Sealed for IssuedTicketbooksDataResponseBody {}
impl Sealed for EcashSignerStatusResponseBody {}
impl Sealed for ChainBlocksStatusResponseBody {}
impl Sealed for SignersStatusResponseBody {}
impl Sealed for DetailedSignersStatusResponseBody {}
}
+3
View File
@@ -4,8 +4,10 @@
use crate::ecash::api_routes::aggregation::aggregation_routes;
use crate::ecash::api_routes::issued::issued_routes;
use crate::ecash::api_routes::partial_signing::partial_signing_routes;
use crate::ecash::api_routes::signer_status::signer_status;
use crate::ecash::api_routes::spending::spending_routes;
use crate::support::http::state::AppState;
use axum::routing::get;
use axum::Router;
pub(crate) fn ecash_routes() -> Router<AppState> {
@@ -14,4 +16,5 @@ pub(crate) fn ecash_routes() -> Router<AppState> {
.merge(issued_routes())
.merge(partial_signing_routes())
.merge(spending_routes())
.route("/signer-status", get(signer_status))
}
+2 -1
View File
@@ -11,8 +11,9 @@ use nym_api_requests::ecash::models::{
IssuedTicketbooksChallengeCommitmentRequest, IssuedTicketbooksChallengeCommitmentResponse,
IssuedTicketbooksCountResponse, IssuedTicketbooksDataRequest, IssuedTicketbooksDataResponse,
IssuedTicketbooksForCountResponse, IssuedTicketbooksForResponse,
IssuedTicketbooksOnCountResponse, SignableMessageBody,
IssuedTicketbooksOnCountResponse,
};
use nym_api_requests::signable::SignableMessageBody;
use nym_http_api_common::{FormattedResponse, OutputParams};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
+1
View File
@@ -6,4 +6,5 @@ pub(crate) mod handlers;
mod helpers;
pub(crate) mod issued;
pub(crate) mod partial_signing;
pub(crate) mod signer_status;
pub(crate) mod spending;
@@ -0,0 +1,45 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use crate::ecash::state::EcashState;
use crate::node_status_api::models::ApiResult;
use axum::extract::{Query, State};
use nym_api_requests::ecash::models::{EcashSignerStatusResponse, EcashSignerStatusResponseBody};
use nym_api_requests::signable::SignableMessageBody;
use nym_http_api_common::{FormattedResponse, OutputParams};
use std::sync::Arc;
use time::OffsetDateTime;
#[utoipa::path(
tag = "Ecash",
get,
path = "/signer-status",
context_path = "/v1/ecash",
responses(
(status = 200, content(
(EcashSignerStatusResponse = "application/json"),
(EcashSignerStatusResponse = "application/yaml"),
(EcashSignerStatusResponse = "application/bincode")
)),
),
params(OutputParams)
)]
pub(crate) async fn signer_status(
Query(params): Query<OutputParams>,
State(state): State<Arc<EcashState>>,
) -> ApiResult<FormattedResponse<EcashSignerStatusResponse>> {
let output = params.get_output();
let dkg_ecash_epoch_id = state.current_dkg_epoch().await?;
Ok(output.to_response(
EcashSignerStatusResponseBody {
current_time: OffsetDateTime::now_utc(),
dkg_ecash_epoch_id,
signer_disabled: state.local.explicitly_disabled,
is_ecash_signer: state.is_dkg_signer(dkg_ecash_epoch_id).await?,
has_signing_keys: state.ecash_signing_key().await.is_ok(),
}
.sign(state.local.identity_keypair.private_key()),
))
}
+16 -9
View File
@@ -50,7 +50,6 @@ use nym_validator_client::nyxd::AccountId;
use nym_validator_client::EcashApiClient;
use rand::{thread_rng, RngCore};
use std::collections::HashMap;
use std::ops::Deref;
use time::{Date, OffsetDateTime};
use tokio::sync::{RwLockReadGuard, RwLockWriteGuard};
use tokio::task::JoinHandle;
@@ -162,14 +161,11 @@ impl EcashState {
}
}
/// Ensures that this nym-api is one of ecash signers for the current epoch
pub(crate) async fn ensure_signer(&self) -> Result<()> {
if self.local.explicitly_disabled {
return Err(EcashError::NotASigner);
}
let epoch_id = self.aux.current_epoch().await?;
pub(crate) async fn current_dkg_epoch(&self) -> Result<EpochId> {
self.aux.current_epoch().await
}
pub(crate) async fn is_dkg_signer(&self, epoch_id: EpochId) -> Result<bool> {
let is_epoch_signer = self
.local
.active_signer
@@ -183,8 +179,19 @@ impl EcashState {
Ok(ecash_signers.iter().any(|c| c.cosmos_address == address))
})
.await?;
Ok(*is_epoch_signer)
}
if !is_epoch_signer.deref() {
/// Ensures that this nym-api is one of ecash signers for the current epoch
pub(crate) async fn ensure_signer(&self) -> Result<()> {
if self.local.explicitly_disabled {
return Err(EcashError::NotASigner);
}
let epoch_id = self.current_dkg_epoch().await?;
let is_epoch_signer = self.is_dkg_signer(epoch_id).await?;
if !is_epoch_signer {
return Err(EcashError::NotASigner);
}
+3 -1
View File
@@ -32,9 +32,10 @@ use cw3::{Proposal, ProposalResponse, Vote, VoteInfo, VoteResponse, Votes};
use cw4::{Cw4Contract, MemberResponse};
use nym_api_requests::ecash::models::{
IssuedTicketbooksChallengeCommitmentRequestBody, IssuedTicketbooksChallengeCommitmentResponse,
IssuedTicketbooksForResponse, SignableMessageBody,
IssuedTicketbooksForResponse,
};
use nym_api_requests::ecash::{BlindSignRequestBody, BlindedSignatureResponse};
use nym_api_requests::signable::SignableMessageBody;
use nym_coconut_dkg_common::dealer::{
DealerDetails, DealerDetailsResponse, DealerType, RegisteredDealerDetails,
};
@@ -1277,6 +1278,7 @@ impl TestFixture {
AppState {
nyxd_client,
chain_status_cache: ChainStatusCache::new(Duration::from_secs(42)),
ecash_signers_cache: Default::default(),
address_info_cache: AddressInfoCache::new(Duration::from_secs(42), 1000),
forced_refresh: ForcedRefresh::new(true),
mixnet_contract_cache: MixnetContractCache::new(),
+3 -3
View File
@@ -8,7 +8,6 @@ use ::nym_config::defaults::setup_env;
use clap::Parser;
use mixnet_contract_cache::cache::MixnetContractCache;
use node_status_api::NodeStatusCache;
use nym_bin_common::logging::setup_tracing_logger;
use support::nyxd;
use tracing::{info, trace};
@@ -23,6 +22,7 @@ pub(crate) mod node_describe_cache;
mod node_performance;
pub(crate) mod node_status_api;
pub(crate) mod nym_nodes;
mod signers_cache;
mod status;
pub(crate) mod support;
mod unstable_routes;
@@ -32,10 +32,10 @@ async fn main() -> Result<(), anyhow::Error> {
cfg_if::cfg_if! {if #[cfg(feature = "console-subscriber")] {
// instrument tokio console subscriber needs RUSTFLAGS="--cfg tokio_unstable" at build time
console_subscriber::init();
} else {
nym_bin_common::logging::setup_tracing_logger();
}}
setup_tracing_logger();
info!("Starting nym api...");
let args = cli::Cli::parse();
+62 -6
View File
@@ -3,13 +3,19 @@
use crate::network::models::{ContractInformation, NetworkDetails};
use crate::node_status_api::models::AxumResult;
use crate::signers_cache::handlers::signers_routes;
use crate::support::config::CHAIN_STALL_THRESHOLD;
use crate::support::http::state::AppState;
use axum::extract::{Query, State};
use axum::Router;
use nym_api_requests::models::ChainStatusResponse;
use nym_api_requests::models::{
ChainBlocksStatusResponse, ChainBlocksStatusResponseBody, ChainStatus, ChainStatusResponse,
};
use nym_api_requests::signable::SignableMessageBody;
use nym_contracts_common::ContractBuildInformation;
use nym_http_api_common::{FormattedResponse, OutputParams};
use std::collections::HashMap;
use time::OffsetDateTime;
use tower_http::compression::CompressionLayer;
use utoipa::ToSchema;
@@ -17,18 +23,24 @@ pub(crate) fn nym_network_routes() -> Router<AppState> {
Router::new()
.route("/details", axum::routing::get(network_details))
.route("/chain-status", axum::routing::get(chain_status))
.route(
"/chain-blocks-status",
axum::routing::get(chain_blocks_status),
)
.route("/nym-contracts", axum::routing::get(nym_contracts))
.route(
"/nym-contracts-detailed",
axum::routing::get(nym_contracts_detailed),
)
.nest("/signers", signers_routes())
.layer(CompressionLayer::new())
}
#[utoipa::path(
tag = "network",
get,
path = "/v1/network/details",
context_path = "/v1/network",
path = "/details",
responses(
(status = 200, content(
(NetworkDetails = "application/json"),
@@ -50,7 +62,8 @@ async fn network_details(
#[utoipa::path(
tag = "network",
get,
path = "/v1/network/chain-status",
context_path = "/v1/network",
path = "/chain-status",
responses(
(status = 200, content(
(ChainStatusResponse = "application/json"),
@@ -79,6 +92,47 @@ async fn chain_status(
}))
}
#[utoipa::path(
tag = "network",
get,
context_path = "/v1/network",
path = "/chain-blocks-status",
responses(
(status = 200, content(
(ChainBlocksStatusResponse = "application/json"),
(ChainBlocksStatusResponse = "application/yaml"),
(ChainBlocksStatusResponse = "application/bincode")
))
),
params(OutputParams)
)]
async fn chain_blocks_status(
Query(params): Query<OutputParams>,
State(state): State<AppState>,
) -> FormattedResponse<ChainBlocksStatusResponse> {
let output = params.get_output();
let current_time = OffsetDateTime::now_utc();
let latest_cached_block = state
.chain_status_cache
.get_or_refresh(&state.nyxd_client)
.await
.ok();
let chain_status = latest_cached_block
.as_ref()
.map(|detailed| detailed.stall_status(current_time, CHAIN_STALL_THRESHOLD))
.unwrap_or(ChainStatus::Unknown);
output.to_response(
ChainBlocksStatusResponseBody {
current_time,
latest_cached_block,
chain_status,
}
.sign(state.private_signing_key()),
)
}
// it's used for schema generation so dead_code is fine
#[allow(dead_code)]
#[derive(ToSchema)]
@@ -90,7 +144,7 @@ pub(crate) struct ContractVersionSchemaResponse {
/// version is any string that this implementation knows. It may be simple counter "1", "2".
/// or semantic version on release tags "v0.7.0", or some custom feature flag list.
/// the only code that needs to understand the version parsing is code that knows how to
/// migrate from the given contract (and is tied to it's implementation somehow)
/// migrate from the given contract (and is tied to its implementation somehow)
pub version: String,
}
@@ -104,7 +158,8 @@ pub struct ContractInformationContractVersion {
#[utoipa::path(
tag = "network",
get,
path = "/v1/network/nym-contracts",
context_path = "/v1/network",
path = "/nym-contracts",
responses(
(status = 200, content(
(HashMap<String, ContractInformationContractVersion> = "application/json"),
@@ -151,7 +206,8 @@ pub struct ContractInformationBuildInformation {
#[utoipa::path(
tag = "network",
get,
path = "/v1/network/nym-contracts-detailed",
context_path = "/v1/network",
path = "/nym-contracts-detailed",
responses(
(status = 200, content(
(HashMap<String, ContractInformationBuildInformation> = "application/json"),
+2
View File
@@ -0,0 +1,2 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
+11
View File
@@ -0,0 +1,11 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use nym_ecash_signer_check::SignersTestResult;
pub(crate) mod data;
pub(crate) mod refresher;
pub(crate) struct SignersCacheData {
pub(crate) signers_results: SignersTestResult,
}
+33
View File
@@ -0,0 +1,33 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use crate::signers_cache::cache::SignersCacheData;
use crate::support::caching::refresher::CacheItemProvider;
use crate::support::nyxd::Client;
use async_trait::async_trait;
use nym_ecash_signer_check::{check_signers_with_client, SignerCheckError};
pub(crate) struct SignersCacheDataProvider {
nyxd_client: Client,
}
#[async_trait]
impl CacheItemProvider for SignersCacheDataProvider {
type Item = SignersCacheData;
type Error = SignerCheckError;
async fn try_refresh(&mut self) -> Result<Option<Self::Item>, Self::Error> {
self.refresh().await.map(Some)
}
}
impl SignersCacheDataProvider {
pub(crate) fn new(nyxd_client: Client) -> Self {
SignersCacheDataProvider { nyxd_client }
}
async fn refresh(&self) -> Result<SignersCacheData, SignerCheckError> {
let signers_results = check_signers_with_client(&self.nyxd_client).await?;
Ok(SignersCacheData { signers_results })
}
}
+95
View File
@@ -0,0 +1,95 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use crate::node_status_api::models::ApiResult;
use crate::support::http::state::AppState;
use axum::extract::{Query, State};
use axum::routing::get;
use axum::Router;
use nym_api_requests::models::{
DetailedSignersStatusResponse, DetailedSignersStatusResponseBody, SignersStatusOverview,
SignersStatusResponse, SignersStatusResponseBody,
};
use nym_api_requests::signable::SignableMessageBody;
use nym_http_api_common::{FormattedResponse, OutputParams};
pub(crate) fn signers_routes() -> Router<AppState> {
Router::new()
.route("/status", get(signers_status))
.route("/status-detailed", get(signers_status_detailed))
}
#[utoipa::path(
tag = "network",
get,
context_path = "/v1/network/signers",
path = "/status",
responses(
(status = 200, content(
(SignersStatusResponse = "application/json"),
(SignersStatusResponse = "application/yaml"),
(SignersStatusResponse = "application/bincode")
))
),
params(OutputParams)
)]
pub(crate) async fn signers_status(
Query(params): Query<OutputParams>,
State(state): State<AppState>,
) -> ApiResult<FormattedResponse<SignersStatusResponse>> {
let output = params.get_output();
let cached = state.ecash_signers_cache.get().await?;
let as_at = cached.timestamp();
Ok(output.to_response(
SignersStatusResponseBody {
as_at,
overview: SignersStatusOverview::new(
&cached.signers_results.results,
cached.signers_results.threshold,
),
results: cached
.signers_results
.results
.iter()
.map(Into::into)
.collect(),
}
.sign(state.private_signing_key()),
))
}
#[utoipa::path(
tag = "network",
get,
context_path = "/v1/network/signers",
path = "/status-detailed",
responses(
(status = 200, content(
(DetailedSignersStatusResponse = "application/json"),
(DetailedSignersStatusResponse = "application/yaml"),
(DetailedSignersStatusResponse = "application/bincode")
))
),
params(OutputParams)
)]
pub(crate) async fn signers_status_detailed(
Query(params): Query<OutputParams>,
State(state): State<AppState>,
) -> ApiResult<FormattedResponse<DetailedSignersStatusResponse>> {
let output = params.get_output();
let cached = state.ecash_signers_cache.get().await?;
let as_at = cached.timestamp();
Ok(output.to_response(
DetailedSignersStatusResponseBody {
as_at,
overview: SignersStatusOverview::new(
&cached.signers_results.results,
cached.signers_results.threshold,
),
details: cached.signers_results.results.clone(),
}
.sign(state.private_signing_key()),
))
}
+30
View File
@@ -0,0 +1,30 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use crate::signers_cache::cache::refresher::SignersCacheDataProvider;
use crate::signers_cache::cache::SignersCacheData;
use crate::support::caching::cache::SharedCache;
use crate::support::caching::refresher::CacheRefresher;
use crate::support::{config, nyxd};
use nym_task::TaskManager;
pub(crate) mod cache;
pub(crate) mod handlers;
pub(crate) fn start_refresher(
config: &config::SignersCache,
nyxd_client: nyxd::Client,
task_manager: &TaskManager,
) -> SharedCache<SignersCacheData> {
let refresher = CacheRefresher::new(
SignersCacheDataProvider::new(nyxd_client),
config.debug.refresh_interval,
)
.named("signers-cache-refresher");
let shared_cache = refresher.get_shared_cache();
refresher.start_with_delay(
task_manager.subscribe_named("signers-cache-refresher"),
config.debug.refresher_start_delay,
);
shared_cache
}
+2 -12
View File
@@ -3,6 +3,7 @@
use crate::node_status_api::models::{AxumErrorResponse, AxumResult};
use crate::status::ApiStatusState;
use crate::support::config::CHAIN_STALL_THRESHOLD;
use crate::support::http::state::AppState;
use axum::extract::{Query, State};
use axum::Router;
@@ -12,7 +13,6 @@ use nym_api_requests::models::{
use nym_bin_common::build_information::BinaryBuildInformationOwned;
use nym_compact_ecash::Base58;
use nym_http_api_common::{FormattedResponse, OutputParams};
use std::time::Duration;
use time::OffsetDateTime;
pub(crate) fn api_status_routes() -> Router<AppState> {
@@ -42,8 +42,6 @@ async fn health(
Query(output): Query<OutputParams>,
State(state): State<AppState>,
) -> FormattedResponse<ApiHealthResponse> {
const CHAIN_STALL_THRESHOLD: Duration = Duration::from_secs(5 * 60);
let output = output.output.unwrap_or_default();
let uptime = state.api_status.startup_time.elapsed();
@@ -54,15 +52,7 @@ async fn health(
{
Ok(res) => {
let now = OffsetDateTime::now_utc();
let block_time: OffsetDateTime = res.latest_block.block.header.time.into();
let diff = now - block_time;
if diff > CHAIN_STALL_THRESHOLD {
ChainStatus::Stalled {
approximate_amount: diff.unsigned_abs(),
}
} else {
ChainStatus::Synced
}
res.stall_status(now, CHAIN_STALL_THRESHOLD)
}
Err(_) => ChainStatus::Unknown,
};
+27
View File
@@ -60,6 +60,11 @@ pub(crate) trait CacheItemProvider {
async fn try_refresh(&mut self) -> Result<Option<Self::Item>, Self::Error>;
}
// Generics explanation:
// T: the actual type held in the cache
// E: Error type associated with refresh failure
// S: data type retrieved during update operation. it must be convertible into T
// (so that initial state could be established or when no `custom_fn` is set)
impl<T, E, S> CacheRefresher<T, E, S>
where
E: std::error::Error,
@@ -107,6 +112,8 @@ where
}
}
/// Rather than performing default behaviour of overwriting all existing values in the cache,
/// provide a custom update function that will define the update behaviour.
#[must_use]
pub(crate) fn with_update_fn(
mut self,
@@ -259,6 +266,26 @@ where
tokio::spawn(async move { self.run(task_client).await });
}
pub fn start_with_delay(mut self, mut task_client: TaskClient, delay: Duration)
where
T: Send + Sync + 'static,
E: Send + Sync + 'static,
S: Send + Sync + 'static,
{
tokio::spawn(async move {
let sleep = tokio::time::sleep(delay);
tokio::select! {
biased;
_ = task_client.recv() => {
trace!("{}: Received shutdown", self.name);
return
}
_ = sleep => {},
}
self.run(task_client).await
});
}
pub fn start_with_watcher(self, task_client: TaskClient) -> CacheUpdateWatcher
where
T: Send + Sync + 'static,
+9 -1
View File
@@ -34,7 +34,7 @@ use crate::support::storage::NymApiStorage;
use crate::unstable_routes::v1::account::cache::AddressInfoCache;
use crate::{
ecash, epoch_operations, mixnet_contract_cache, network_monitor, node_describe_cache,
node_performance, node_status_api,
node_performance, node_status_api, signers_cache,
};
use anyhow::{bail, Context};
use nym_config::defaults::NymNetworkDetails;
@@ -202,10 +202,18 @@ async fn start_nym_api_tasks(config: &Config) -> anyhow::Result<ShutdownHandles>
None
};
// check if signers cache is enabled, and if so, start the refresher
let ecash_signers_cache = if config.signers_cache.enabled {
signers_cache::start_refresher(&config.signers_cache, nyxd_client.clone(), &task_manager)
} else {
SharedCache::new()
};
ecash_state.spawn_background_cleaner();
let router = router.with_state(AppState {
nyxd_client: nyxd_client.clone(),
chain_status_cache: ChainStatusCache::new(DEFAULT_CHAIN_STATUS_CACHE_TTL),
ecash_signers_cache,
address_info_cache: AddressInfoCache::new(
config.address_cache.time_to_live,
config.address_cache.capacity,
+48 -1
View File
@@ -63,12 +63,17 @@ pub(crate) const DEFAULT_NODE_DESCRIBE_CACHE_INTERVAL: Duration = Duration::from
pub(crate) const DEFAULT_NODE_DESCRIBE_BATCH_SIZE: usize = 50;
// TODO: make it configurable
pub(crate) const DEFAULT_CHAIN_STATUS_CACHE_TTL: Duration = Duration::from_secs(60);
pub(crate) const DEFAULT_CHAIN_STATUS_CACHE_TTL: Duration = Duration::from_secs(30);
pub(crate) const CHAIN_STALL_THRESHOLD: Duration = Duration::from_secs(5 * 60);
// contract info is changed very infrequently (essentially once per release cycle)
// so this default is more than enough
pub(crate) const DEFAULT_CONTRACT_DETAILS_CACHE_TTL: Duration = Duration::from_secs(60 * 60);
pub(crate) const DEFAULT_NODE_SIGNERS_CACHE_REFRESH_INTERVAL: Duration = Duration::from_secs(600);
pub(crate) const DEFAULT_NODE_SIGNERS_CACHE_REFRESHER_START_DELAY: Duration =
Duration::from_secs(30);
const DEFAULT_MONITOR_THRESHOLD: u8 = 60;
const DEFAULT_MIN_MIXNODE_RELIABILITY: u8 = 50;
const DEFAULT_MIN_GATEWAY_RELIABILITY: u8 = 20;
@@ -126,6 +131,9 @@ pub struct Config {
pub rewarding: Rewarding,
#[serde(default)]
pub signers_cache: SignersCache,
#[serde(alias = "coconut_signer")]
pub ecash_signer: EcashSigner,
@@ -151,6 +159,7 @@ impl Config {
describe_cache: Default::default(),
contracts_info_cache: Default::default(),
rewarding: Default::default(),
signers_cache: Default::default(),
ecash_signer: EcashSigner::new_default(id.as_ref()),
address_cache: Default::default(),
}
@@ -413,6 +422,44 @@ impl Default for PerformanceProviderDebug {
}
}
#[derive(Debug, Deserialize, PartialEq, Eq, Serialize)]
pub struct SignersCache {
pub enabled: bool,
pub debug: SignersCacheDebug,
}
impl Default for SignersCache {
fn default() -> Self {
SignersCache {
enabled: true,
debug: Default::default(),
}
}
}
#[derive(Debug, Deserialize, PartialEq, Eq, Serialize)]
pub struct SignersCacheDebug {
// TODO: make it into a decaying function so that if multiple signers are down,
// the refresh interval would decrease
#[serde(with = "humantime_serde")]
pub refresh_interval: Duration,
// give it some time so that the actual api on THIS singer could start
// and it wouldn't self-report itself as being down
#[serde(with = "humantime_serde")]
pub refresher_start_delay: Duration,
}
impl Default for SignersCacheDebug {
fn default() -> Self {
SignersCacheDebug {
refresh_interval: DEFAULT_NODE_SIGNERS_CACHE_REFRESH_INTERVAL,
refresher_start_delay: DEFAULT_NODE_SIGNERS_CACHE_REFRESHER_START_DELAY,
}
}
}
#[derive(Debug, PartialEq, Eq)]
pub struct AddressCacheConfig {
pub time_to_live: Duration,
+18
View File
@@ -8,6 +8,7 @@ use crate::node_describe_cache::cache::DescribedNodes;
use crate::node_status_api::handlers::unstable;
use crate::node_status_api::models::AxumErrorResponse;
use crate::node_status_api::NodeStatusCache;
use crate::signers_cache::cache::SignersCacheData;
use crate::status::ApiStatusState;
use crate::support::caching::cache::SharedCache;
use crate::support::caching::Cache;
@@ -20,6 +21,7 @@ use crate::unstable_routes::v1::account::cache::AddressInfoCache;
use crate::unstable_routes::v1::account::models::NyxAccountDetails;
use axum::extract::FromRef;
use nym_api_requests::models::{GatewayBondAnnotated, MixNodeBondAnnotated, NodeAnnotation};
use nym_crypto::asymmetric::ed25519;
use nym_mixnet_contract_common::NodeId;
use nym_topology::CachedEpochRewardedSet;
use std::collections::HashMap;
@@ -41,6 +43,10 @@ pub(crate) struct AppState {
/// Note, it is not updated on every request. It follows the embedded ttl.
pub(crate) chain_status_cache: ChainStatusCache,
/// Holds cached state of the statuses of all [ecash] signers on the network -
/// their perceived chain statuses and signing capabilities.
pub(crate) ecash_signers_cache: SharedCache<SignersCacheData>,
/// Holds mapping between a nyx address and tokens/delegations it holds
pub(crate) address_info_cache: AddressInfoCache,
@@ -100,7 +106,19 @@ impl FromRef<AppState> for MixnetContractCache {
}
}
impl FromRef<AppState> for SharedCache<SignersCacheData> {
fn from_ref(app_state: &AppState) -> Self {
app_state.ecash_signers_cache.clone()
}
}
impl AppState {
pub(crate) fn private_signing_key(&self) -> &ed25519::PrivateKey {
// even though we have to go through ecash state, the key is always available
// (moving it would involve some refactoring that's not worth it now)
self.ecash_state.local.identity_keypair.private_key()
}
pub(crate) fn nym_contract_cache(&self) -> &MixnetContractCache {
&self.mixnet_contract_cache
}
+2 -2
View File
@@ -1,5 +1,5 @@
use crate::utils::{base_url, get_any_node_id, make_request, test_client, validate_json_response};
use chrono::Utc;
use time::OffsetDateTime;
#[tokio::test]
async fn test_get_bonded_nodes() -> Result<(), String> {
@@ -83,7 +83,7 @@ async fn test_get_annotation_for_node() -> Result<(), String> {
#[tokio::test]
async fn test_get_historical_performance() -> Result<(), String> {
let id = get_any_node_id().await?;
let date = Utc::now().date_naive().to_string();
let date = OffsetDateTime::now_utc().date().to_string();
let url = format!("{}/v1/nym-nodes/historical-performance/{}", base_url()?, id);
let res = test_client()
.get(&url)
@@ -15,9 +15,9 @@ use nym_validator_client::ecash::models::{
CommitedDeposit, DepositId, IssuedTicketbooksChallengeCommitmentRequestBody,
IssuedTicketbooksChallengeCommitmentResponse, IssuedTicketbooksDataRequestBody,
IssuedTicketbooksDataResponse, IssuedTicketbooksDataResponseBody, IssuedTicketbooksForResponse,
SignableMessageBody, SignedMessage,
};
use nym_validator_client::nyxd::AccountId;
use nym_validator_client::signable::{SignableMessageBody, SignedMessage};
use rand::distributions::{Distribution, WeightedIndex};
use rand::prelude::SliceRandom;
use rand::thread_rng;
+17 -35
View File
@@ -2541,18 +2541,6 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "getset"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3586f256131df87204eb733da72e3d3eb4f343c639f4b7be279ac7c48baeafe"
dependencies = [
"proc-macro-error2",
"proc-macro2",
"quote",
"syn 2.0.100",
]
[[package]]
name = "ghash"
version = "0.5.1"
@@ -4020,14 +4008,15 @@ dependencies = [
"cosmrs",
"cosmwasm-std",
"ecdsa",
"getset",
"hex",
"humantime-serde",
"nym-coconut-dkg-common",
"nym-compact-ecash",
"nym-config",
"nym-contracts-common",
"nym-credentials-interface",
"nym-crypto",
"nym-ecash-signer-check-types",
"nym-ecash-time",
"nym-mixnet-contract-common",
"nym-network-defaults",
@@ -4170,6 +4159,21 @@ dependencies = [
"thiserror 2.0.12",
]
[[package]]
name = "nym-ecash-signer-check-types"
version = "0.1.0"
dependencies = [
"nym-coconut-dkg-common",
"nym-crypto",
"semver",
"serde",
"thiserror 2.0.12",
"time",
"tracing",
"url",
"utoipa",
]
[[package]]
name = "nym-ecash-time"
version = "0.1.0"
@@ -5413,28 +5417,6 @@ dependencies = [
"version_check",
]
[[package]]
name = "proc-macro-error-attr2"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5"
dependencies = [
"proc-macro2",
"quote",
]
[[package]]
name = "proc-macro-error2"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802"
dependencies = [
"proc-macro-error-attr2",
"proc-macro2",
"quote",
"syn 2.0.100",
]
[[package]]
name = "proc-macro-hack"
version = "0.5.20+deprecated"
@@ -412,10 +412,10 @@ impl NetworkManager {
let mut receivers = Vec::new();
for signer in &ctx.ecash_signers {
// send 101nym to the admin
// send 250nym to the admin
receivers.push((
signer.data.cosmos_account.address.clone(),
admin.mix_coins(101_000000),
admin.mix_coins(250_000000),
))
}