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:
committed by
GitHub
parent
e418c7587a
commit
dc59149a5d
Generated
+33
-10
@@ -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
@@ -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>,
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
@@ -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 })
|
||||
}
|
||||
@@ -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
@@ -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 = []
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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::*;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 {}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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()),
|
||||
))
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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();
|
||||
|
||||
@@ -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
@@ -0,0 +1,2 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
+11
@@ -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
@@ -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 })
|
||||
}
|
||||
}
|
||||
@@ -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()),
|
||||
))
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Generated
+17
-35
@@ -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),
|
||||
))
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user