using node's account balance for config score field

This commit is contained in:
Jędrzej Stuczyński
2026-05-26 16:37:57 +01:00
parent b9d96f337a
commit 60261a0fa6
20 changed files with 655 additions and 172 deletions
@@ -102,6 +102,16 @@ pub struct Config {
pub(crate) simulated_gas_multiplier: f32,
}
impl Config {
pub fn chain_details(&self) -> &ChainDetails {
&self.chain_details
}
pub fn contracts(&self) -> &TypedNymContracts {
&self.contracts
}
}
impl Config {
pub fn try_from_nym_network_details(details: &NymNetworkDetails) -> Result<Self, NyxdError> {
Ok(Config {
@@ -315,6 +325,10 @@ impl<C, S> NyxdClient<C, S> {
pub fn get_nym_contracts(&self) -> TypedNymContracts {
self.config.contracts.clone()
}
pub fn get_chain_details(&self) -> ChainDetails {
self.config.chain_details.clone()
}
}
impl<C, S> NymContractsProvider for NyxdClient<C, S> {
@@ -8,9 +8,7 @@ use crate::models::described::type_translation::{
};
use crate::models::described::v1::{DescribedNodeTypeV1, NymNodeDataV1, NymNodeDescriptionV1};
use crate::models::{BinaryBuildInformationOwned, OffsetDateTimeJsonSchemaWrapper};
use crate::nym_nodes::{
BasicEntryInformation, NodeRole, SemiSkimmedNodeV1, SemiSkimmedNodeV3, SkimmedNodeV1,
};
use crate::nym_nodes::{BasicEntryInformation, NodeRole, SkimmedNodeV1};
use nym_crypto::asymmetric::{ed25519, x25519};
use nym_mixnet_contract_common::reward_params::Performance;
use nym_mixnet_contract_common::NodeId;
@@ -119,44 +117,6 @@ impl NymNodeDescriptionV2 {
performance,
}
}
pub fn to_semi_skimmed_node(
&self,
current_rotation_id: u32,
role: NodeRole,
performance: Performance,
) -> SemiSkimmedNodeV1 {
let skimmed_node = self.to_skimmed_node(current_rotation_id, role, performance);
SemiSkimmedNodeV1 {
basic: skimmed_node,
x25519_noise_versioned_key: self
.description
.host_information
.keys
.x25519_versioned_noise,
}
}
pub fn to_semi_skimmed_node_v3(
&self,
current_rotation_id: u32,
role: NodeRole,
performance: Performance,
) -> SemiSkimmedNodeV3 {
let skimmed_node = self.to_skimmed_node(current_rotation_id, role, performance);
SemiSkimmedNodeV3 {
basic: skimmed_node,
noise_key: self
.description
.host_information
.keys
.x25519_versioned_noise,
build_version: self.description.build_information.build_version.clone(),
lp: self.description.lewes_protocol.clone(),
}
}
}
// to whoever is thinking of modifying this struct.
@@ -276,10 +236,10 @@ pub fn mock_nym_node_description(seed: u64) -> NymNodeDescriptionV2 {
let mut rng = u64_seeded_rng(seed);
let ed25519 = ed25519::KeyPair::new(&mut rng);
let ed25519 = nym_crypto::asymmetric::ed25519::KeyPair::new(&mut rng);
// just reuse the same x25519 key for everything - this is just a data mock
let x25519 = x25519::KeyPair::new(&mut rng);
let x25519 = nym_crypto::asymmetric::x25519::KeyPair::new(&mut rng);
let mut dummy_kems = std::collections::BTreeMap::new();
for kem in [LPKEM::McEliece, LPKEM::McEliece] {
@@ -1,12 +1,250 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use crate::models::described::v2::{AnnouncePortsV2, NymNodeAuxiliaryDetailsV2};
use crate::models::described::type_translation::LewesProtocolDetailsV1;
use crate::models::described::v1::NymNodeDescriptionV1;
use crate::models::described::v2::{
AnnouncePortsV2, AuthenticatorDetailsV2, DeclaredRolesV2, DescribedNodeTypeV2,
HostInformationV2, HostKeysV2, IpPacketRouterDetailsV2, NetworkRequesterDetailsV2,
NymNodeAuxiliaryDetailsV2, NymNodeDataV2, NymNodeDescriptionV2, SphinxKeyV2,
VersionedNoiseKeyV2, WebSocketsV2, WireguardDetailsV2,
};
use crate::models::{BinaryBuildInformationOwned, OffsetDateTimeJsonSchemaWrapper};
use crate::nym_nodes::{
BasicEntryInformation, NodeRole, SemiSkimmedNodeV1, SemiSkimmedNodeV3, SkimmedNodeV1,
};
use celes::Country;
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 serde::{Deserialize, Serialize};
use tracing::warn;
use utoipa::ToSchema;
// no changes for the following types
pub type AnnouncePortsV3 = AnnouncePortsV2;
pub type HostInformationV3 = HostInformationV2;
pub type DeclaredRolesV3 = DeclaredRolesV2;
pub type NetworkRequesterDetailsV3 = NetworkRequesterDetailsV2;
pub type IpPacketRouterDetailsV3 = IpPacketRouterDetailsV2;
pub type AuthenticatorDetailsV3 = AuthenticatorDetailsV2;
pub type WireguardDetailsV3 = WireguardDetailsV2;
pub type WebSocketsV3 = WebSocketsV2;
pub type DescribedNodeTypeV3 = DescribedNodeTypeV2;
pub type HostKeysV3 = HostKeysV2;
pub type SphinxKeyV3 = SphinxKeyV2;
pub type VersionedNoiseKeyV3 = VersionedNoiseKeyV2;
pub type LewesProtocolDetailsV3 = LewesProtocolDetailsV1;
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub struct NymNodeDescriptionV3 {
#[schema(value_type = u32)]
pub node_id: NodeId,
pub contract_node_type: DescribedNodeTypeV3,
pub description: NymNodeDataV3,
}
impl NymNodeDescriptionV3 {
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,
) -> SkimmedNodeV1 {
let keys = &self.description.host_information.keys;
let entry = if self.description.declared_role.entry {
Some(self.entry_information())
} else {
None
};
SkimmedNodeV1 {
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,
) -> SemiSkimmedNodeV1 {
let skimmed_node = self.to_skimmed_node(current_rotation_id, role, performance);
SemiSkimmedNodeV1 {
basic: skimmed_node,
x25519_noise_versioned_key: self
.description
.host_information
.keys
.x25519_versioned_noise,
}
}
pub fn to_semi_skimmed_node_v3(
&self,
current_rotation_id: u32,
role: NodeRole,
performance: Performance,
) -> SemiSkimmedNodeV3 {
let skimmed_node = self.to_skimmed_node(current_rotation_id, role, performance);
SemiSkimmedNodeV3 {
basic: skimmed_node,
noise_key: self
.description
.host_information
.keys
.x25519_versioned_noise,
build_version: self.description.build_information.build_version.clone(),
lp: self.description.lewes_protocol.clone(),
}
}
}
impl From<NymNodeDescriptionV3> for NymNodeDescriptionV2 {
fn from(value: NymNodeDescriptionV3) -> Self {
NymNodeDescriptionV2 {
node_id: value.node_id,
contract_node_type: value.contract_node_type,
description: value.description.into(),
}
}
}
impl From<NymNodeDescriptionV3> for NymNodeDescriptionV1 {
fn from(value: NymNodeDescriptionV3) -> Self {
NymNodeDescriptionV1 {
node_id: value.node_id,
contract_node_type: value.contract_node_type,
description: NymNodeDataV2::from(value.description).into(),
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub struct NymNodeDataV3 {
#[serde(default)]
pub last_polled: OffsetDateTimeJsonSchemaWrapper,
pub host_information: HostInformationV3,
#[serde(default)]
pub declared_role: DeclaredRolesV3,
#[serde(default)]
pub auxiliary_details: NymNodeAuxiliaryDetailsV3,
// TODO: do we really care about ALL build info or just the version?
pub build_information: BinaryBuildInformationOwned,
#[serde(default)]
pub network_requester: Option<NetworkRequesterDetailsV3>,
#[serde(default)]
pub ip_packet_router: Option<IpPacketRouterDetailsV3>,
#[serde(default)]
pub authenticator: Option<AuthenticatorDetailsV3>,
#[serde(default)]
pub wireguard: Option<WireguardDetailsV3>,
// for now we only care about their ws/wss situation, nothing more
pub mixnet_websockets: WebSocketsV3,
#[serde(default)]
pub lewes_protocol: Option<LewesProtocolDetailsV3>,
}
impl NymNodeDataV3 {
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)
}
}
impl From<NymNodeDataV3> for NymNodeDataV2 {
fn from(data: NymNodeDataV3) -> Self {
NymNodeDataV2 {
last_polled: data.last_polled,
host_information: data.host_information,
declared_role: data.declared_role,
auxiliary_details: data.auxiliary_details.into(),
build_information: data.build_information,
network_requester: data.network_requester,
ip_packet_router: data.ip_packet_router,
authenticator: data.authenticator,
wireguard: data.wireguard,
mixnet_websockets: data.mixnet_websockets,
lewes_protocol: data.lewes_protocol,
}
}
}
#[derive(
Clone, Debug, Default, Serialize, Deserialize, schemars::JsonSchema, ToSchema, PartialEq,
@@ -19,6 +257,7 @@ pub struct NymNodeAuxiliaryDetailsV3 {
pub location: Option<Country>,
/// On-chain address of this node
#[serde(default)]
pub address: Option<String>,
#[serde(default)]
@@ -2,9 +2,9 @@
// SPDX-License-Identifier: Apache-2.0
use crate::helpers::PlaceholderJsonSchemaImpl;
use crate::models::DisplayRole;
use crate::models::{CoinSchema, DisplayRole};
use crate::pagination::PaginatedResponse;
use cosmwasm_std::Decimal;
use cosmwasm_std::{Coin, Decimal};
use nym_contracts_common::{IdentityKey, NaiveFloat};
use nym_crypto::asymmetric::ed25519;
use nym_crypto::asymmetric::ed25519::serde_helpers::bs58_ed25519_pubkey;
@@ -419,18 +419,13 @@ pub struct NodeAnnotationV1 {
pub detailed_performance: DetailedNodePerformanceV1,
}
#[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/NodeAnnotationV2.ts"
)
)]
#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, ToSchema)]
pub struct NodeAnnotationV2 {
pub current_role: Option<DisplayRole>,
#[schema(value_type = Option<CoinSchema>)]
pub on_chain_balance: Option<Coin>,
pub detailed_performance: DetailedNodePerformanceV2,
}
@@ -449,7 +444,7 @@ impl From<NodeAnnotationV2> for NodeAnnotationV1 {
detailed_performance: DetailedNodePerformanceV1 {
performance_score: value.detailed_performance.performance_score,
routing_score: value.detailed_performance.routing_score,
config_score: value.detailed_performance.config_score,
config_score: value.detailed_performance.config_score.into(),
},
}
}
@@ -470,14 +465,14 @@ pub struct DetailedNodePerformanceV1 {
pub performance_score: f64,
pub routing_score: RoutingScore,
pub config_score: ConfigScore,
pub config_score: ConfigScoreV1,
}
impl DetailedNodePerformanceV1 {
pub fn new(
performance_score: f64,
routing_score: RoutingScore,
config_score: ConfigScore,
config_score: ConfigScoreV1,
) -> DetailedNodePerformanceV1 {
Self {
performance_score,
@@ -508,7 +503,7 @@ pub struct DetailedNodePerformanceV2 {
pub performance_score: f64,
pub routing_score: RoutingScore,
pub config_score: ConfigScore,
pub config_score: ConfigScoreV2,
pub stress_testing_score: StressTestingScore,
}
@@ -516,7 +511,7 @@ impl DetailedNodePerformanceV2 {
pub fn new(
performance_score: f64,
routing_score: RoutingScore,
config_score: ConfigScore,
config_score: ConfigScoreV2,
stress_testing_score: StressTestingScore,
) -> DetailedNodePerformanceV2 {
Self {
@@ -588,10 +583,13 @@ impl StressTestingScore {
#[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")
ts(
export,
export_to = "ts-packages/types/src/types/rust/ConfigScoreV2.ts"
)
)]
#[non_exhaustive]
pub struct ConfigScore {
pub struct ConfigScoreV2 {
/// Total score after taking all the criteria into consideration
pub score: f64,
@@ -599,45 +597,111 @@ pub struct ConfigScore {
pub self_described_api_available: bool,
pub accepted_terms_and_conditions: bool,
pub runs_nym_node_binary: bool,
/// Describes the node is capable of sending chain/contract transactions
pub chain_interaction_capabilities: ChainInteractionCapabilities,
}
impl ConfigScore {
impl ConfigScoreV2 {
pub fn new(
score: f64,
versions_behind: u32,
accepted_terms_and_conditions: bool,
runs_nym_node_binary: bool,
) -> ConfigScore {
chain_interaction_capabilities: ChainInteractionCapabilities,
) -> ConfigScoreV2 {
Self {
score,
versions_behind: Some(versions_behind),
self_described_api_available: true,
accepted_terms_and_conditions,
runs_nym_node_binary,
chain_interaction_capabilities,
}
}
pub fn bad_semver() -> ConfigScore {
ConfigScore {
pub fn bad_semver() -> ConfigScoreV2 {
ConfigScoreV2 {
score: 0.0,
versions_behind: None,
self_described_api_available: true,
accepted_terms_and_conditions: false,
runs_nym_node_binary: false,
chain_interaction_capabilities: Default::default(),
}
}
pub fn unavailable() -> ConfigScore {
ConfigScore {
pub fn unavailable() -> ConfigScoreV2 {
ConfigScoreV2 {
score: 0.0,
versions_behind: None,
self_described_api_available: false,
accepted_terms_and_conditions: false,
runs_nym_node_binary: false,
chain_interaction_capabilities: Default::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/ChainInteractionCapabilities.ts"
)
)]
pub struct ChainInteractionCapabilities {
pub has_sufficient_tokens: bool,
pub is_fee_grant_grantee: bool,
}
impl ChainInteractionCapabilities {
pub fn new(has_sufficient_tokens: bool, is_fee_grant_grantee: bool) -> Self {
Self {
has_sufficient_tokens,
is_fee_grant_grantee,
}
}
pub fn can_send_transactions(&self) -> bool {
self.has_sufficient_tokens || self.is_fee_grant_grantee
}
}
impl From<ConfigScoreV2> for ConfigScoreV1 {
fn from(score_v2: ConfigScoreV2) -> ConfigScoreV1 {
ConfigScoreV1 {
score: score_v2.score,
versions_behind: score_v2.versions_behind,
self_described_api_available: score_v2.self_described_api_available,
accepted_terms_and_conditions: score_v2.accepted_terms_and_conditions,
runs_nym_node_binary: score_v2.runs_nym_node_binary,
}
}
}
#[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/ConfigScoreV1.ts"
)
)]
#[non_exhaustive]
pub struct ConfigScoreV1 {
/// 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,
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, ToSchema)]
#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))]
#[cfg_attr(
@@ -653,15 +717,7 @@ pub struct AnnotationResponseV1 {
pub annotation: Option<NodeAnnotationV1>,
}
#[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/AnnotationResponseV2.ts"
)
)]
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, ToSchema)]
pub struct AnnotationResponseV2 {
#[schema(value_type = u32)]
pub node_id: NodeId,
@@ -8,7 +8,7 @@ use crate::node_describe_cache::cache::DescribedNodes;
use crate::node_describe_cache::NodeDescriptionTopologyExt;
use crate::node_status_api::NodeStatusCache;
use crate::support::caching::cache::SharedCache;
use nym_api_requests::models::described::v2::NymNodeDescriptionV2;
use nym_api_requests::models::described::v3::NymNodeDescriptionV3;
use nym_api_requests::models::NodeAnnotationV2;
use nym_crypto::asymmetric::{ed25519, x25519};
use nym_mixnet_contract_common::{LegacyMixLayer, NodeId};
@@ -206,7 +206,7 @@ impl PacketPreparer {
rng: &mut R,
current_rotation_id: u32,
node_statuses: &HashMap<NodeId, NodeAnnotationV2>,
mixing_nym_nodes: impl Iterator<Item = &'a NymNodeDescriptionV2> + 'a,
mixing_nym_nodes: impl Iterator<Item = &'a NymNodeDescriptionV3> + 'a,
) -> HashMap<LegacyMixLayer, Vec<(RoutingNode, f64)>> {
let mut layered_mixes = HashMap::new();
@@ -246,7 +246,7 @@ impl PacketPreparer {
&self,
current_rotation_id: u32,
node_statuses: &HashMap<NodeId, NodeAnnotationV2>,
gateway_capable_nym_nodes: impl Iterator<Item = &'a NymNodeDescriptionV2> + 'a,
gateway_capable_nym_nodes: impl Iterator<Item = &'a NymNodeDescriptionV3> + 'a,
) -> Vec<(RoutingNode, f64)> {
let mut gateways = Vec::new();
@@ -401,7 +401,7 @@ impl PacketPreparer {
fn nym_node_to_routing_node(
&self,
current_rotation_id: u32,
description: &NymNodeDescriptionV2,
description: &NymNodeDescriptionV3,
) -> Option<RoutingNode> {
description.try_to_topology_node(current_rotation_id).ok()
}
+15 -15
View File
@@ -1,8 +1,8 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use nym_api_requests::models::described::v2::{
DescribedNodeTypeV2, NymNodeDataV2, NymNodeDescriptionV2,
use nym_api_requests::models::described::v3::{
DescribedNodeTypeV3, NymNodeDataV3, NymNodeDescriptionV3,
};
use nym_mixnet_contract_common::NodeId;
use serde::{Deserialize, Serialize};
@@ -11,54 +11,54 @@ use std::net::IpAddr;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DescribedNodes {
pub(crate) nodes: HashMap<NodeId, NymNodeDescriptionV2>,
pub(crate) nodes: HashMap<NodeId, NymNodeDescriptionV3>,
pub(crate) addresses_cache: HashMap<IpAddr, NodeId>,
}
impl DescribedNodes {
pub fn force_update(&mut self, node: NymNodeDescriptionV2) {
pub fn force_update(&mut self, node: NymNodeDescriptionV3) {
for ip in &node.description.host_information.ip_address {
self.addresses_cache.insert(*ip, node.node_id);
}
self.nodes.insert(node.node_id, node);
}
pub fn get_description(&self, node_id: &NodeId) -> Option<&NymNodeDataV2> {
pub fn get_description(&self, node_id: &NodeId) -> Option<&NymNodeDataV3> {
self.nodes.get(node_id).map(|n| &n.description)
}
pub fn get_node(&self, node_id: &NodeId) -> Option<&NymNodeDescriptionV2> {
pub fn get_node(&self, node_id: &NodeId) -> Option<&NymNodeDescriptionV3> {
self.nodes.get(node_id)
}
pub fn all_nodes(&self) -> impl Iterator<Item = &NymNodeDescriptionV2> {
pub fn all_nodes(&self) -> impl Iterator<Item = &NymNodeDescriptionV3> {
self.nodes.values()
}
pub fn all_nym_nodes(&self) -> impl Iterator<Item = &NymNodeDescriptionV2> {
pub fn all_nym_nodes(&self) -> impl Iterator<Item = &NymNodeDescriptionV3> {
self.nodes
.values()
.filter(|n| n.contract_node_type == DescribedNodeTypeV2::NymNode)
.filter(|n| n.contract_node_type == DescribedNodeTypeV3::NymNode)
}
pub fn mixing_nym_nodes(&self) -> impl Iterator<Item = &NymNodeDescriptionV2> {
pub fn mixing_nym_nodes(&self) -> impl Iterator<Item = &NymNodeDescriptionV3> {
self.nodes
.values()
.filter(|n| n.contract_node_type == DescribedNodeTypeV2::NymNode)
.filter(|n| n.contract_node_type == DescribedNodeTypeV3::NymNode)
.filter(|n| n.description.declared_role.mixnode)
}
pub fn entry_capable_nym_nodes(&self) -> impl Iterator<Item = &NymNodeDescriptionV2> {
pub fn entry_capable_nym_nodes(&self) -> impl Iterator<Item = &NymNodeDescriptionV3> {
self.nodes
.values()
.filter(|n| n.contract_node_type == DescribedNodeTypeV2::NymNode)
.filter(|n| n.contract_node_type == DescribedNodeTypeV3::NymNode)
.filter(|n| n.description.declared_role.entry)
}
pub fn exit_capable_nym_nodes(&self) -> impl Iterator<Item = &NymNodeDescriptionV2> {
pub fn exit_capable_nym_nodes(&self) -> impl Iterator<Item = &NymNodeDescriptionV3> {
self.nodes
.values()
.filter(|n| n.contract_node_type == DescribedNodeTypeV2::NymNode)
.filter(|n| n.contract_node_type == DescribedNodeTypeV3::NymNode)
.filter(|n| n.description.declared_role.can_operate_exit_gateway())
}
+13
View File
@@ -4,6 +4,7 @@
use crate::support::caching::cache::UninitialisedCache;
use nym_api_requests::models::described::v1::NymNodeDescriptionV1;
use nym_api_requests::models::described::v2::NymNodeDescriptionV2;
use nym_api_requests::models::described::v3::NymNodeDescriptionV3;
use nym_mixnet_contract_common::NodeId;
use nym_node_requests::api::client::NymNodeApiClientError;
use nym_topology::node::RoutingNodeError;
@@ -73,3 +74,15 @@ impl NodeDescriptionTopologyExt for NymNodeDescriptionV2 {
.try_into()
}
}
impl NodeDescriptionTopologyExt for NymNodeDescriptionV3 {
fn try_to_topology_node(
&self,
current_rotation_id: u32,
) -> Result<RoutingNode, RoutingNodeError> {
// for the purposes of routing, performance is completely ignored,
// so add dummy value and piggyback on existing conversion
(&self.to_skimmed_node(current_rotation_id, Default::default(), Default::default()))
.try_into()
}
}
@@ -4,12 +4,12 @@
use crate::node_describe_cache::NodeDescribeCacheError;
use futures::future::{maybe_done, MaybeDone};
use futures::{FutureExt, TryFutureExt};
use nym_api_requests::models::described::type_translation::LewesProtocolDetailsV1;
use nym_api_requests::models::described::v2::{
AuthenticatorDetailsV2, DeclaredRolesV2, HostInformationV2, IpPacketRouterDetailsV2,
NetworkRequesterDetailsV2, NymNodeDataV2, WebSocketsV2, WireguardDetailsV2,
};
use nym_api_requests::models::described::v3::NymNodeAuxiliaryDetailsV3;
use nym_api_requests::models::described::v3::{
AuthenticatorDetailsV3, DeclaredRolesV3, HostInformationV3, IpPacketRouterDetailsV3,
LewesProtocolDetailsV3, NetworkRequesterDetailsV3, NymNodeDataV3, WebSocketsV3,
WireguardDetailsV3,
};
use nym_bin_common::build_information::BinaryBuildInformationOwned;
use nym_config::defaults::mainnet;
use nym_mixnet_contract_common::NodeId;
@@ -24,14 +24,14 @@ use tracing::debug;
async fn network_requester_future(
client: &Client,
) -> Result<Option<NetworkRequesterDetailsV2>, NymNodeApiClientError> {
) -> Result<Option<NetworkRequesterDetailsV3>, NymNodeApiClientError> {
let Ok(nr) = client.get_network_requester().await else {
return Ok(None);
};
client.get_exit_policy().await.map(|exit_policy| {
let uses_nym_exit_policy = exit_policy.upstream_source == mainnet::EXIT_POLICY_URL;
Some(NetworkRequesterDetailsV2 {
Some(NetworkRequesterDetailsV3 {
address: nr.address,
uses_exit_policy: exit_policy.enabled && uses_nym_exit_policy,
})
@@ -125,14 +125,14 @@ impl<F1, F2, F3, F4, F5, F6, F7, F8, F9> Future
for NodeDescribedInfoMegaFuture<F1, F2, F3, F4, F5, F6, F7, F8, F9>
where
F1: Future<Output = Result<BinaryBuildInformationOwned, NodeDescribeCacheError>>,
F2: Future<Output = Result<DeclaredRolesV2, NodeDescribeCacheError>>,
F2: Future<Output = Result<DeclaredRolesV3, NodeDescribeCacheError>>,
F3: Future<Output = NymNodeAuxiliaryDetailsV3>,
F4: Future<Output = Result<WebSocketsV2, NodeDescribeCacheError>>,
F5: Future<Output = Result<Option<NetworkRequesterDetailsV2>, NodeDescribeCacheError>>,
F6: Future<Output = Option<IpPacketRouterDetailsV2>>,
F7: Future<Output = Option<AuthenticatorDetailsV2>>,
F8: Future<Output = Option<WireguardDetailsV2>>,
F9: Future<Output = Option<LewesProtocolDetailsV1>>,
F4: Future<Output = Result<WebSocketsV3, NodeDescribeCacheError>>,
F5: Future<Output = Result<Option<NetworkRequesterDetailsV3>, NodeDescribeCacheError>>,
F6: Future<Output = Option<IpPacketRouterDetailsV3>>,
F7: Future<Output = Option<AuthenticatorDetailsV3>>,
F8: Future<Output = Option<WireguardDetailsV3>>,
F9: Future<Output = Option<LewesProtocolDetailsV3>>,
{
type Output = Result<UnwrappedResolvedNodeDescribedInfo, NodeDescribeCacheError>;
@@ -215,15 +215,15 @@ where
struct ResolvedNodeDescribedInfo {
build_info: Result<BinaryBuildInformationOwned, NodeDescribeCacheError>,
roles: Result<DeclaredRolesV2, NodeDescribeCacheError>,
roles: Result<DeclaredRolesV3, NodeDescribeCacheError>,
// TODO: in the future make it return a Result as well.
auxiliary_details: NymNodeAuxiliaryDetailsV3,
websockets: Result<WebSocketsV2, NodeDescribeCacheError>,
network_requester: Result<Option<NetworkRequesterDetailsV2>, NodeDescribeCacheError>,
ipr: Option<IpPacketRouterDetailsV2>,
authenticator: Option<AuthenticatorDetailsV2>,
wireguard: Option<WireguardDetailsV2>,
lewes_protocol: Option<LewesProtocolDetailsV1>,
websockets: Result<WebSocketsV3, NodeDescribeCacheError>,
network_requester: Result<Option<NetworkRequesterDetailsV3>, NodeDescribeCacheError>,
ipr: Option<IpPacketRouterDetailsV3>,
authenticator: Option<AuthenticatorDetailsV3>,
wireguard: Option<WireguardDetailsV3>,
lewes_protocol: Option<LewesProtocolDetailsV3>,
}
impl ResolvedNodeDescribedInfo {
@@ -245,22 +245,22 @@ impl ResolvedNodeDescribedInfo {
#[derive(Debug)]
pub(crate) struct UnwrappedResolvedNodeDescribedInfo {
pub(crate) build_info: BinaryBuildInformationOwned,
pub(crate) roles: DeclaredRolesV2,
pub(crate) roles: DeclaredRolesV3,
pub(crate) auxiliary_details: NymNodeAuxiliaryDetailsV3,
pub(crate) websockets: WebSocketsV2,
pub(crate) network_requester: Option<NetworkRequesterDetailsV2>,
pub(crate) ipr: Option<IpPacketRouterDetailsV2>,
pub(crate) authenticator: Option<AuthenticatorDetailsV2>,
pub(crate) wireguard: Option<WireguardDetailsV2>,
pub(crate) lewes_protocol: Option<LewesProtocolDetailsV1>,
pub(crate) websockets: WebSocketsV3,
pub(crate) network_requester: Option<NetworkRequesterDetailsV3>,
pub(crate) ipr: Option<IpPacketRouterDetailsV3>,
pub(crate) authenticator: Option<AuthenticatorDetailsV3>,
pub(crate) wireguard: Option<WireguardDetailsV3>,
pub(crate) lewes_protocol: Option<LewesProtocolDetailsV3>,
}
impl UnwrappedResolvedNodeDescribedInfo {
pub(crate) fn into_node_description(
self,
host_info: impl Into<HostInformationV2>,
) -> NymNodeDataV2 {
NymNodeDataV2 {
host_info: impl Into<HostInformationV3>,
) -> NymNodeDataV3 {
NymNodeDataV3 {
host_information: host_info.into(),
last_polled: OffsetDateTime::now_utc().into(),
build_information: self.build_info,
@@ -270,7 +270,7 @@ impl UnwrappedResolvedNodeDescribedInfo {
wireguard: self.wireguard,
lewes_protocol: self.lewes_protocol,
mixnet_websockets: self.websockets,
auxiliary_details: self.auxiliary_details.into(),
auxiliary_details: self.auxiliary_details,
declared_role: self.roles,
}
}
+7 -7
View File
@@ -3,7 +3,7 @@
use crate::node_describe_cache::query_helpers::query_for_described_data;
use crate::node_describe_cache::NodeDescribeCacheError;
use nym_api_requests::models::described::v2::{DescribedNodeTypeV2, NymNodeDescriptionV2};
use nym_api_requests::models::described::v3::{DescribedNodeTypeV3, NymNodeDescriptionV3};
use nym_bin_common::bin_info;
use nym_crypto::asymmetric::ed25519;
use nym_mixnet_contract_common::{NodeId, NymNodeDetails};
@@ -15,7 +15,7 @@ pub(crate) struct RefreshData {
host: String,
node_id: NodeId,
expected_identity: ed25519::PublicKey,
node_type: DescribedNodeTypeV2,
node_type: DescribedNodeTypeV3,
port: Option<u16>,
}
@@ -27,7 +27,7 @@ impl<'a> TryFrom<&'a NymNodeDetails> for RefreshData {
Ok(RefreshData::new(
&node.bond_information.node.host,
node.bond_information.identity().parse()?,
DescribedNodeTypeV2::NymNode,
DescribedNodeTypeV3::NymNode,
node.node_id(),
node.bond_information.node.custom_http_port,
))
@@ -38,7 +38,7 @@ impl RefreshData {
pub fn new(
host: impl Into<String>,
expected_identity: ed25519::PublicKey,
node_type: DescribedNodeTypeV2,
node_type: DescribedNodeTypeV3,
node_id: NodeId,
port: Option<u16>,
) -> Self {
@@ -55,7 +55,7 @@ impl RefreshData {
self.node_id
}
pub(crate) async fn try_refresh(self, allow_all_ips: bool) -> Option<NymNodeDescriptionV2> {
pub(crate) async fn try_refresh(self, allow_all_ips: bool) -> Option<NymNodeDescriptionV3> {
match try_get_description(self, allow_all_ips).await {
Ok(description) => Some(description),
Err(err) => {
@@ -69,7 +69,7 @@ impl RefreshData {
async fn try_get_description(
data: RefreshData,
allow_all_ips: bool,
) -> Result<NymNodeDescriptionV2, NodeDescribeCacheError> {
) -> Result<NymNodeDescriptionV3, NodeDescribeCacheError> {
let client = NymNodeApiClientRetriever::new(bin_info!())
.with_expected_identity(Some(data.expected_identity.to_base58_string()))
.with_verify_host_information()
@@ -101,7 +101,7 @@ async fn try_get_description(
let node_info = query_for_described_data(&client.client, data.node_id).await?;
let description = node_info.into_node_description(host_info.data);
Ok(NymNodeDescriptionV2 {
Ok(NymNodeDescriptionV3 {
node_id: data.node_id,
contract_node_type: data.node_type,
description,
+31 -7
View File
@@ -2,10 +2,12 @@
// SPDX-License-Identifier: GPL-3.0-only
use crate::mixnet_contract_cache::cache::data::ConfigScoreData;
use nym_api_requests::models::described::v2::NymNodeDescriptionV2;
use nym_api_requests::models::ConfigScore;
use cosmwasm_std::Coin;
use nym_api_requests::models::described::v3::NymNodeDescriptionV3;
use nym_api_requests::models::{ChainInteractionCapabilities, ConfigScoreV2};
use nym_contracts_common::NaiveFloat;
use nym_mixnet_contract_common::VersionScoreFormulaParams;
use tracing::warn;
fn versions_behind_factor_to_config_score(
versions_behind: u32,
@@ -18,17 +20,29 @@ fn versions_behind_factor_to_config_score(
penalty.powf((versions_behind as f64).powf(scaling))
}
fn has_sufficient_tokens(minimum_balance: &Coin, chain_balance: &Option<Coin>) -> bool {
let Some(chain_balance) = chain_balance else {
return false;
};
if chain_balance.denom != minimum_balance.denom {
return false;
}
chain_balance.amount >= minimum_balance.amount
}
pub(crate) fn calculate_config_score(
minimum_balance: &Coin,
config_score_data: &ConfigScoreData,
described_data: Option<&NymNodeDescriptionV2>,
) -> ConfigScore {
described_data: Option<&NymNodeDescriptionV3>,
chain_balance: &Option<Coin>,
) -> ConfigScoreV2 {
let Some(described) = described_data else {
return ConfigScore::unavailable();
return ConfigScoreV2::unavailable();
};
let node_version = &described.description.build_information.build_version;
let Ok(reported_semver) = node_version.parse::<semver::Version>() else {
return ConfigScore::bad_semver();
return ConfigScoreV2::bad_semver();
};
let versions_behind = config_score_data
.config_score_params
@@ -55,10 +69,20 @@ pub(crate) fn calculate_config_score(
)
};
ConfigScore::new(
let TODO = "";
warn!("unimplemented check for feegrant");
let chain_interaction = ChainInteractionCapabilities {
has_sufficient_tokens: has_sufficient_tokens(minimum_balance, chain_balance),
// TODO: implement this
is_fee_grant_grantee: false,
};
ConfigScoreV2::new(
version_score,
versions_behind,
accepted_terms_and_conditions,
runs_nym_node,
chain_interaction,
)
}
+12
View File
@@ -103,4 +103,16 @@ impl NodeStatusCache {
) -> Result<RwLockReadGuard<'_, HashMap<NodeId, NodeAnnotationV2>>, UninitialisedCache> {
self.get(|c| &c.node_annotations).await
}
async fn node_balances(
&self,
) -> Result<HashMap<NodeId, Option<cosmwasm_std::Coin>>, NodeStatusCacheError> {
Ok(self
.cache()
.await?
.node_annotations
.iter()
.map(|(node_id, annotation)| (*node_id, annotation.on_chain_balance.clone()))
.collect::<HashMap<_, _>>())
}
}
+123 -9
View File
@@ -11,46 +11,67 @@ use crate::node_status_api::cache::config_score::calculate_config_score;
use crate::support::caching::cache::SharedCache;
use crate::support::caching::refresher::RefreshRequester;
use crate::support::caching::CacheNotificationWatcher;
use crate::support::nyxd::Client;
use crate::{
mixnet_contract_cache::cache::MixnetContractCache,
node_status_api::cache::NodeStatusCacheError, support::caching::CacheNotification,
};
use ::time::OffsetDateTime;
use nym_api_requests::models::{DetailedNodePerformanceV2, NodeAnnotationV2, NymNodeDescriptionV2};
use cosmwasm_std::Coin;
use futures::StreamExt;
use nym_api_requests::models::described::v3::NymNodeDescriptionV3;
use nym_api_requests::models::{DetailedNodePerformanceV2, NodeAnnotationV2};
use nym_mixnet_contract_common::{NodeId, NymNodeDetails};
use nym_task::ShutdownToken;
use nym_topology::CachedEpochRewardedSet;
use nym_validator_client::nyxd::{AccountId, CosmWasmClient};
use nym_validator_client::QueryHttpRpcNyxdClient;
use std::collections::HashMap;
use std::path::PathBuf;
use std::str::FromStr;
use std::time::Duration;
use tokio::time;
use tokio::time::Instant;
use tracing::{error, info, trace, warn};
pub(crate) struct NodeStatusCacheConfig {
pub(crate) minimum_on_chain_balance: Coin,
pub(crate) balance_retrieval_concurrency: usize,
/// Indicates how often should the chain balances of known nodes be refreshed.
/// (it is an overkill to do it every single iteration)
pub(crate) chain_balances_refresh_interval: Duration,
pub(crate) fallback_caching_interval: Duration,
/// Specify whether external stress testing data should be used for calculating node performance
/// score used for rewarding and active set selection
/// note: this can only be enabled if use_performance_contract_data is set to false!
pub use_stress_testing_data: bool,
pub(crate) use_stress_testing_data: bool,
/// If `use_stress_testing_data` is set to true, this specifies the minimum % of nodes,
/// that must have their stress data available in the `stress_testing_data_period`,
/// in order to include that metric in performance calculation.
/// This is done to protect against Network Monitor failures and not receiving any data.
pub minimum_available_stress_testing_results: f32,
pub(crate) minimum_available_stress_testing_results: f32,
/// If use_stress_testing_data is enabled, specifies the weight of the stress testing score in the overall performance score.
pub stress_testing_score_weight: f64,
pub(crate) stress_testing_score_weight: f64,
}
// Long running task responsible for keeping the node status cache up-to-date.
pub struct NodeStatusCacheRefresher {
config: NodeStatusCacheConfig,
/// Indicates the last time chain balances of known nodes were refreshed.
last_refreshed_chain_balances: Option<Instant>,
// Main stored data
cache: NodeStatusCache,
/// Query client for retrieving blockchain data
query_client: QueryHttpRpcNyxdClient,
// Sources for when refreshing data
mixnet_contract_cache: MixnetContractCache,
described_cache: SharedCache<DescribedNodes>,
@@ -75,9 +96,10 @@ pub struct NodeStatusCacheRefresher {
impl NodeStatusCacheRefresher {
#[allow(clippy::too_many_arguments)]
pub(crate) fn new(
pub(crate) async fn new(
cache: NodeStatusCache,
config: NodeStatusCacheConfig,
chain_client: &Client,
contract_cache: MixnetContractCache,
described_cache: SharedCache<DescribedNodes>,
contract_cache_listener: CacheNotificationWatcher,
@@ -85,9 +107,14 @@ impl NodeStatusCacheRefresher {
performance_provider: Box<dyn NodePerformanceProvider + Send + Sync>,
on_disk_file: PathBuf,
) -> Self {
// due to the number of queries required, create an explicit query instance
// of our nyxd client to avoid potentially blocking tasks requiring signing access
let query_client = chain_client.query_client().await;
Self {
cache,
config,
last_refreshed_chain_balances: None,
mixnet_contract_cache: contract_cache,
described_cache,
mixnet_contract_cache_listener: contract_cache_listener,
@@ -95,6 +122,7 @@ impl NodeStatusCacheRefresher {
refresh_requester: Default::default(),
on_disk_file,
performance_provider,
query_client,
}
}
@@ -182,7 +210,7 @@ impl NodeStatusCacheRefresher {
}
async fn maybe_refresh(
&self,
&mut self,
fallback_interval: &mut time::Interval,
last_updated: &mut OffsetDateTime,
) {
@@ -202,6 +230,63 @@ impl NodeStatusCacheRefresher {
fallback_interval.reset();
}
// SAFETY: unwrap is fine as if the mutex got poisoned we'd be experiencing some UB anyway
#[allow(clippy::unwrap_used)]
async fn retrieve_balances(
&self,
nodes: &DescribedNodes,
) -> Result<HashMap<NodeId, Option<Coin>>, NodeStatusCacheError> {
let denom = self.config.minimum_on_chain_balance.denom.clone();
// create an iterator of node ids with valid associated account addresses
let to_check = nodes.nodes.values().filter_map(|n| {
n.description
.auxiliary_details
.address
.as_ref()
.and_then(|addr| {
AccountId::from_str(addr)
.inspect_err(|_| {
warn!("node {} has provided an invalid account address", n.node_id)
})
.ok()
.map(|account_id| (n.node_id, account_id))
})
});
// note: we use `for_each_concurrent` rather than `stream::iter(..).buffer_unordered(..)`.
// The latter yields a `Stream` whose `Send` bound gets over-generalised once chained into
// `collect()`, tripping "implementation of `Send` is not general enough" (rust-lang/rust#102211)
let concurrency = self.config.balance_retrieval_concurrency.max(1);
// std Mutex is fine because we don't hold it across await points
let balances = std::sync::Mutex::new(HashMap::<NodeId, Option<Coin>>::new());
futures::stream::iter(to_check)
.for_each_concurrent(concurrency, |(node_id, account_id)| {
let denom = denom.clone();
let query_client = &self.query_client;
let balances = &balances;
async move {
match query_client.get_balance(&account_id, denom).await {
Ok(balance) => {
balances
.lock()
.unwrap()
.insert(node_id, balance.map(Into::into));
}
Err(err) => {
warn!(node_id, %err, "failed to retrieve node balance");
}
}
}
})
.await;
let balances = balances.into_inner().unwrap();
Ok(balances)
}
#[allow(clippy::too_many_arguments)]
pub(crate) async fn produce_node_annotations(
&self,
config_score_data: &ConfigScoreData,
@@ -210,12 +295,14 @@ impl NodeStatusCacheRefresher {
nym_nodes: &[NymNodeDetails],
rewarded_set: &CachedEpochRewardedSet,
described_nodes: &DescribedNodes,
balances: HashMap<NodeId, Option<Coin>>,
) -> HashMap<NodeId, NodeAnnotationV2> {
let mut annotations = HashMap::new();
if nym_nodes.is_empty() {
return annotations;
}
let minimum_balance = &self.config.minimum_on_chain_balance;
let use_stress_testing_scores = self.config.use_stress_testing_data;
let threshold = self.config.minimum_available_stress_testing_results;
@@ -248,8 +335,15 @@ impl NodeStatusCacheRefresher {
let node_id = nym_node.node_id();
let described = described_nodes.get_node(&node_id);
let routing_score = routing_scores.get_or_log(node_id);
let config_score = calculate_config_score(config_score_data, described);
let stress_testing_score = stress_testing_scores.get_or_log(node_id);
let on_chain_balance = balances.get(&node_id).unwrap_or(&None).clone();
let config_score = calculate_config_score(
minimum_balance,
config_score_data,
described,
&on_chain_balance,
);
// a node only takes the stress-testing component if it is actually stress-tested (i.e.
// it is a mixnode); gateways have no stress data and must not be penalised for it.
@@ -266,6 +360,7 @@ impl NodeStatusCacheRefresher {
node_id,
NodeAnnotationV2 {
current_role: rewarded_set.role(node_id).map(|r| r.into()),
on_chain_balance,
detailed_performance: DetailedNodePerformanceV2::new(
performance,
routing_score,
@@ -279,9 +374,16 @@ impl NodeStatusCacheRefresher {
annotations
}
fn should_refresh_balances(&self) -> bool {
let Some(last_refresh) = self.last_refreshed_chain_balances else {
return true;
};
last_refresh.elapsed() > self.config.chain_balances_refresh_interval
}
/// Refreshes the node status cache by fetching the latest data from the contract cache
#[allow(deprecated)]
async fn refresh(&self) -> Result<(), NodeStatusCacheError> {
async fn refresh(&mut self) -> Result<(), NodeStatusCacheError> {
info!("Updating node status cache");
// Fetch contract cache data to work with
@@ -313,6 +415,17 @@ impl NodeStatusCacheRefresher {
)
.await?;
// decide whether to refresh cache of node balances
let balances = if self.should_refresh_balances() {
let balances = self.retrieve_balances(&described).await?;
self.last_refreshed_chain_balances = Some(Instant::now());
balances
} else {
// use the currently cached values instead
self.cache.node_balances().await?
};
// Create annotated data
let node_annotations = self
.produce_node_annotations(
@@ -322,6 +435,7 @@ impl NodeStatusCacheRefresher {
&nym_nodes,
&rewarded_set,
&described,
balances,
)
.await;
@@ -354,7 +468,7 @@ impl NodeStatusCacheRefresher {
/// Today only mixnodes are stress-tested; when gateway stress testing lands, widen this predicate
/// (e.g. to also accept `entry`/`exit` capable nodes) and nothing else in the scoring path needs
/// to change.
fn stress_test_eligible(described: Option<&NymNodeDescriptionV2>) -> bool {
fn stress_test_eligible(described: Option<&NymNodeDescriptionV3>) -> bool {
described
.map(|n| n.description.declared_role.mixnode)
.unwrap_or(false)
+23 -2
View File
@@ -8,11 +8,13 @@ use crate::node_status_api::cache::refresher::NodeStatusCacheConfig;
use crate::support::caching::cache::SharedCache;
use crate::support::caching::refresher::RefreshRequester;
use crate::support::config;
use crate::support::nyxd::Client;
use crate::{
mixnet_contract_cache::cache::MixnetContractCache,
support::{self},
};
pub(crate) use cache::NodeStatusCache;
use cosmwasm_std::Coin;
use nym_task::ShutdownManager;
use std::path::PathBuf;
use std::time::Duration;
@@ -34,8 +36,9 @@ pub(crate) const ONE_DAY: Duration = Duration::from_secs(86400);
/// It is primarily refreshed in-sync with the contract cache and described, however provide a fallback
/// caching interval that is twice the nym contract cache
#[allow(clippy::too_many_arguments)]
pub(crate) fn start_cache_refresh(
pub(crate) async fn start_cache_refresh(
config: &config::Config,
chain_client: &Client,
nym_contract_cache_state: &MixnetContractCache,
described_cache: &SharedCache<DescribedNodes>,
node_status_cache_state: &NodeStatusCache,
@@ -45,7 +48,23 @@ pub(crate) fn start_cache_refresh(
on_disk_file: PathBuf,
shutdown_manager: &ShutdownManager,
) -> RefreshRequester {
let denom = chain_client.chain_details().await.mix_denom.base;
let minimum_on_chain_balance = Coin::new(
config.node_status_api.debug.minimum_on_chain_balance_amount,
denom,
);
let config = NodeStatusCacheConfig {
minimum_on_chain_balance,
balance_retrieval_concurrency: config
.node_status_api
.debug
.node_balance_retrieval_concurrency,
chain_balances_refresh_interval: config
.node_status_api
.debug
.chain_balance_refresh_interval,
fallback_caching_interval: config.node_status_api.debug.caching_interval,
use_stress_testing_data: config.performance_provider.debug.use_stress_testing_data,
minimum_available_stress_testing_results: config
@@ -61,13 +80,15 @@ pub(crate) fn start_cache_refresh(
let mut nym_api_cache_refresher = NodeStatusCacheRefresher::new(
node_status_cache_state.to_owned(),
config,
chain_client,
nym_contract_cache_state.to_owned(),
described_cache.clone(),
nym_contract_cache_listener,
described_cache_cache_listener,
performance_provider,
on_disk_file,
);
)
.await;
let refresh_requester = nym_api_cache_refresher.refresh_requester();
let shutdown_listener = shutdown_manager.clone_shutdown_token();
tokio::spawn(async move { nym_api_cache_refresher.run(shutdown_listener).await });
+8 -5
View File
@@ -10,7 +10,7 @@ use axum::Router;
use nym_api_requests::models::described::v2::NymNodeDescriptionV2;
use nym_api_requests::models::AnnotationResponseV2;
use nym_api_requests::pagination::{PaginatedResponse, Pagination};
use nym_http_api_common::{FormattedResponse, OutputParams};
use nym_http_api_common::{FormattedResponse, OutputParamsV2};
use tower_http::compression::CompressionLayer;
pub(crate) fn routes() -> Router<AppState> {
@@ -43,7 +43,11 @@ async fn get_described_nodes(
let output = pagination.output.unwrap_or_default();
let cache = state.described_nodes_cache.get().await?;
let descriptions = cache.all_nodes().cloned().collect::<Vec<_>>();
// convert description to V2
let descriptions = cache
.all_nodes()
.map(|d| d.clone().into())
.collect::<Vec<_>>();
Ok(output.to_response(PaginatedResponse {
pagination: Pagination {
@@ -64,14 +68,13 @@ async fn get_described_nodes(
(status = 200, content(
(AnnotationResponseV2 = "application/json"),
(AnnotationResponseV2 = "application/yaml"),
(AnnotationResponseV2 = "application/bincode")
))
),
params(NodeIdParam, OutputParams),
params(NodeIdParam, OutputParamsV2),
)]
async fn get_node_annotation(
Path(NodeIdParam { node_id }): Path<NodeIdParam>,
Query(output): Query<OutputParams>,
Query(output): Query<OutputParamsV2>,
State(state): State<AppState>,
) -> AxumResult<FormattedResponse<AnnotationResponseV2>> {
let annotations = state.node_status_cache().node_annotations().await?;
+3 -1
View File
@@ -322,6 +322,7 @@ async fn start_nym_api_tasks(mut config: Config) -> anyhow::Result<ShutdownManag
let node_status_cache_refresh_requester = node_status_api::start_cache_refresh(
&config,
&nyxd_client,
&mixnet_contract_cache_state,
&described_nodes_cache,
&node_status_cache_state,
@@ -330,7 +331,8 @@ async fn start_nym_api_tasks(mut config: Config) -> anyhow::Result<ShutdownManag
describe_cache_watcher,
annotations_path,
&shutdown_manager,
);
)
.await;
node_families_cache_refresher.start(shutdown_manager.clone_shutdown_token());
+19 -2
View File
@@ -50,7 +50,6 @@ const DEFAULT_MINIMUM_TEST_ROUTES: usize = 1;
const DEFAULT_ROUTE_TEST_PACKETS: usize = 1000;
const DEFAULT_PER_NODE_TEST_PACKETS: usize = 3;
const DEFAULT_NODE_STATUS_CACHE_REFRESH_INTERVAL: Duration = Duration::from_secs(305);
const DEFAULT_MIXNET_CACHE_REFRESH_INTERVAL: Duration = Duration::from_secs(150);
const DEFAULT_NODE_FAMILIES_CACHE_REFRESH_INTERVAL: Duration = Duration::from_secs(600);
@@ -722,12 +721,30 @@ pub struct NodeStatusAPIDebug {
// port: u16,
#[serde(with = "humantime_serde")]
pub caching_interval: Duration,
// base amount (in unym)
pub minimum_on_chain_balance_amount: u128,
pub node_balance_retrieval_concurrency: usize,
#[serde(with = "humantime_serde")]
pub chain_balance_refresh_interval: Duration,
}
impl NodeStatusAPIDebug {
const DEFAULT_NODE_STATUS_CACHE_REFRESH_INTERVAL: Duration = Duration::from_secs(305);
const DEFAULT_NODE_BALANCE_RETRIEVAL_CONCURRENCY: usize = 8;
const DEFAULT_CHAIN_BALANCE_REFRESH_INTERVAL: Duration = Duration::from_secs(24 * 60 * 60); // once a day is more than enough
const DEFAULT_CHAIN_BALANCE_REFRESH_THRESHOLD: u128 = 1_000000; // 1 nym is enough for all tx fees for quite some time
}
impl Default for NodeStatusAPIDebug {
fn default() -> Self {
NodeStatusAPIDebug {
caching_interval: DEFAULT_NODE_STATUS_CACHE_REFRESH_INTERVAL,
caching_interval: Self::DEFAULT_NODE_STATUS_CACHE_REFRESH_INTERVAL,
minimum_on_chain_balance_amount: Self::DEFAULT_CHAIN_BALANCE_REFRESH_THRESHOLD,
node_balance_retrieval_concurrency: Self::DEFAULT_NODE_BALANCE_RETRIEVAL_CONCURRENCY,
chain_balance_refresh_interval: Self::DEFAULT_CHAIN_BALANCE_REFRESH_INTERVAL,
}
}
}
+9 -1
View File
@@ -20,7 +20,7 @@ use nym_coconut_dkg_common::{
types::{EncodedBTEPublicKeyWithProof, Epoch},
verification_key::{ContractVKShare, VerificationKeyShare},
};
use nym_config::defaults::NymNetworkDetails;
use nym_config::defaults::{ChainDetails, NymNetworkDetails};
use nym_dkg::Threshold;
use nym_ecash_contract_common::blacklist::BlacklistedAccountResponse;
use nym_ecash_contract_common::deposit::{DepositId, DepositResponse};
@@ -137,6 +137,10 @@ impl Client {
})
}
pub(crate) async fn query_client(&self) -> QueryHttpRpcNyxdClient {
nyxd_query!(self, clone_query_client())
}
pub(crate) async fn read(&self) -> RwLockReadGuard<'_, ClientInner> {
self.inner.read().await
}
@@ -176,6 +180,10 @@ impl Client {
nyxd_query!(self, get_nym_contracts())
}
pub(crate) async fn chain_details(&self) -> ChainDetails {
nyxd_query!(self, get_chain_details())
}
pub(crate) async fn get_ecash_contract_address(&self) -> Result<AccountId, EcashError> {
nyxd_query!(
self,
@@ -6,7 +6,7 @@ use crate::support::http::state::AppState;
use crate::unstable_routes::helpers::refreshed_at;
use crate::unstable_routes::v2::nym_nodes::helpers::NodesParamsWithRole;
use axum::extract::{Query, State};
use nym_api_requests::models::described::v2::NymNodeDescriptionV2;
use nym_api_requests::models::described::v3::NymNodeDescriptionV3;
use nym_api_requests::models::{
NodeAnnotationV1, NodeAnnotationV2, OffsetDateTimeJsonSchemaWrapper,
};
@@ -30,7 +30,7 @@ fn build_nym_nodes_response<'a, NI>(
active_only: bool,
) -> Vec<SemiSkimmedNodeV1>
where
NI: Iterator<Item = &'a NymNodeDescriptionV2> + 'a,
NI: Iterator<Item = &'a NymNodeDescriptionV3> + 'a,
{
let mut nodes = Vec::new();
for nym_node in nym_nodes_subset {
@@ -47,7 +47,7 @@ where
// but in that case just use 0 performance
let annotation: NodeAnnotationV1 = annotations
.get(&node_id)
.copied()
.cloned()
.unwrap_or_default()
.into();
@@ -6,7 +6,7 @@ use crate::unstable_routes::helpers::refreshed_at;
use crate::unstable_routes::v2::nym_nodes::helpers::NodesParams;
use crate::unstable_routes::v2::nym_nodes::skimmed::PaginatedSkimmedNodes;
use axum::extract::{Query, State};
use nym_api_requests::models::described::v2::NymNodeDescriptionV2;
use nym_api_requests::models::described::v3::NymNodeDescriptionV3;
use nym_api_requests::models::{
NodeAnnotationV1, NodeAnnotationV2, OffsetDateTimeJsonSchemaWrapper,
};
@@ -26,7 +26,7 @@ fn build_nym_nodes_response<'a, NI>(
active_only: bool,
) -> Vec<SkimmedNodeV1>
where
NI: Iterator<Item = &'a NymNodeDescriptionV2> + 'a,
NI: Iterator<Item = &'a NymNodeDescriptionV3> + 'a,
{
let mut nodes = Vec::new();
for nym_node in nym_nodes_subset {
@@ -43,7 +43,7 @@ where
// but in that case just use 0 performance
let annotation: NodeAnnotationV1 = annotations
.get(&node_id)
.copied()
.cloned()
.unwrap_or_default()
.into();
@@ -96,7 +96,7 @@ pub(crate) async fn build_skimmed_nodes_response<'a, NI>(
) -> PaginatedSkimmedNodes
where
// iterator returning relevant subset of nym-nodes (like mixing nym-nodes, entries, etc.)
NI: Iterator<Item = &'a NymNodeDescriptionV2> + 'a,
NI: Iterator<Item = &'a NymNodeDescriptionV3> + 'a,
{
// TODO: implement it
let _ = query_params.per_page;
@@ -5,7 +5,7 @@ use crate::node_status_api::models::AxumResult;
use crate::support::http::state::AppState;
use crate::unstable_routes::helpers::refreshed_at;
use axum::extract::{Query, State};
use nym_api_requests::models::described::v2::NymNodeDescriptionV2;
use nym_api_requests::models::described::v3::NymNodeDescriptionV3;
use nym_api_requests::models::{
NodeAnnotationV1, NodeAnnotationV2, OffsetDateTimeJsonSchemaWrapper,
};
@@ -22,7 +22,7 @@ pub type PaginatedSemiSkimmedNodes =
fn build_response<'a>(
rewarded_set: &CachedEpochRewardedSet,
nym_nodes: impl Iterator<Item = &'a NymNodeDescriptionV2>,
nym_nodes: impl Iterator<Item = &'a NymNodeDescriptionV3>,
annotations: &HashMap<NodeId, NodeAnnotationV2>,
current_key_rotation: u32,
) -> Vec<SemiSkimmedNodeV3> {
@@ -36,7 +36,7 @@ fn build_response<'a>(
// but in that case just use 0 performance
let annotation: NodeAnnotationV1 = annotations
.get(&node_id)
.copied()
.cloned()
.unwrap_or_default()
.into();