diff --git a/common/client-libs/validator-client/src/nyxd/mod.rs b/common/client-libs/validator-client/src/nyxd/mod.rs index 86c0f347ae..7ecaacae92 100644 --- a/common/client-libs/validator-client/src/nyxd/mod.rs +++ b/common/client-libs/validator-client/src/nyxd/mod.rs @@ -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 { Ok(Config { @@ -315,6 +325,10 @@ impl NyxdClient { pub fn get_nym_contracts(&self) -> TypedNymContracts { self.config.contracts.clone() } + + pub fn get_chain_details(&self) -> ChainDetails { + self.config.chain_details.clone() + } } impl NymContractsProvider for NyxdClient { diff --git a/nym-api/nym-api-requests/src/models/described/v2.rs b/nym-api/nym-api-requests/src/models/described/v2.rs index f02346368d..465663dc2f 100644 --- a/nym-api/nym-api-requests/src/models/described/v2.rs +++ b/nym-api/nym-api-requests/src/models/described/v2.rs @@ -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] { diff --git a/nym-api/nym-api-requests/src/models/described/v3.rs b/nym-api/nym-api-requests/src/models/described/v3.rs index 3a47e0cb4b..fb0a208fe4 100644 --- a/nym-api/nym-api-requests/src/models/described/v3.rs +++ b/nym-api/nym-api-requests/src/models/described/v3.rs @@ -1,12 +1,250 @@ // Copyright 2026 - Nym Technologies SA // 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 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 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, + + #[serde(default)] + pub ip_packet_router: Option, + + #[serde(default)] + pub authenticator: Option, + + #[serde(default)] + pub wireguard: Option, + + // for now we only care about their ws/wss situation, nothing more + pub mixnet_websockets: WebSocketsV3, + + #[serde(default)] + pub lewes_protocol: Option, +} + +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 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, /// On-chain address of this node + #[serde(default)] pub address: Option, #[serde(default)] diff --git a/nym-api/nym-api-requests/src/models/node_status.rs b/nym-api/nym-api-requests/src/models/node_status.rs index 58e55a057c..a45aa01aec 100644 --- a/nym-api/nym-api-requests/src/models/node_status.rs +++ b/nym-api/nym-api-requests/src/models/node_status.rs @@ -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, + #[schema(value_type = Option)] + pub on_chain_balance: Option, + pub detailed_performance: DetailedNodePerformanceV2, } @@ -449,7 +444,7 @@ impl From 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 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, + 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, } -#[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, diff --git a/nym-api/src/network_monitor/monitor/preparer.rs b/nym-api/src/network_monitor/monitor/preparer.rs index edbbcda458..5b71db29c3 100644 --- a/nym-api/src/network_monitor/monitor/preparer.rs +++ b/nym-api/src/network_monitor/monitor/preparer.rs @@ -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, - mixing_nym_nodes: impl Iterator + 'a, + mixing_nym_nodes: impl Iterator + 'a, ) -> HashMap> { let mut layered_mixes = HashMap::new(); @@ -246,7 +246,7 @@ impl PacketPreparer { &self, current_rotation_id: u32, node_statuses: &HashMap, - gateway_capable_nym_nodes: impl Iterator + 'a, + gateway_capable_nym_nodes: impl Iterator + '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 { description.try_to_topology_node(current_rotation_id).ok() } diff --git a/nym-api/src/node_describe_cache/cache.rs b/nym-api/src/node_describe_cache/cache.rs index bf82eb29b2..93d833b403 100644 --- a/nym-api/src/node_describe_cache/cache.rs +++ b/nym-api/src/node_describe_cache/cache.rs @@ -1,8 +1,8 @@ // Copyright 2025 - Nym Technologies SA // 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, + pub(crate) nodes: HashMap, pub(crate) addresses_cache: HashMap, } 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 { + pub fn all_nodes(&self) -> impl Iterator { self.nodes.values() } - pub fn all_nym_nodes(&self) -> impl Iterator { + pub fn all_nym_nodes(&self) -> impl Iterator { 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 { + pub fn mixing_nym_nodes(&self) -> impl Iterator { 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 { + pub fn entry_capable_nym_nodes(&self) -> impl Iterator { 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 { + pub fn exit_capable_nym_nodes(&self) -> impl Iterator { 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()) } diff --git a/nym-api/src/node_describe_cache/mod.rs b/nym-api/src/node_describe_cache/mod.rs index 38f170b9b6..9523411b2d 100644 --- a/nym-api/src/node_describe_cache/mod.rs +++ b/nym-api/src/node_describe_cache/mod.rs @@ -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 { + // 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() + } +} diff --git a/nym-api/src/node_describe_cache/query_helpers.rs b/nym-api/src/node_describe_cache/query_helpers.rs index 3f1eecdb81..ca6b3fc90d 100644 --- a/nym-api/src/node_describe_cache/query_helpers.rs +++ b/nym-api/src/node_describe_cache/query_helpers.rs @@ -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, NymNodeApiClientError> { +) -> Result, 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 Future for NodeDescribedInfoMegaFuture where F1: Future>, - F2: Future>, + F2: Future>, F3: Future, - F4: Future>, - F5: Future, NodeDescribeCacheError>>, - F6: Future>, - F7: Future>, - F8: Future>, - F9: Future>, + F4: Future>, + F5: Future, NodeDescribeCacheError>>, + F6: Future>, + F7: Future>, + F8: Future>, + F9: Future>, { type Output = Result; @@ -215,15 +215,15 @@ where struct ResolvedNodeDescribedInfo { build_info: Result, - roles: Result, + roles: Result, // TODO: in the future make it return a Result as well. auxiliary_details: NymNodeAuxiliaryDetailsV3, - websockets: Result, - network_requester: Result, NodeDescribeCacheError>, - ipr: Option, - authenticator: Option, - wireguard: Option, - lewes_protocol: Option, + websockets: Result, + network_requester: Result, NodeDescribeCacheError>, + ipr: Option, + authenticator: Option, + wireguard: Option, + lewes_protocol: Option, } 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, - pub(crate) ipr: Option, - pub(crate) authenticator: Option, - pub(crate) wireguard: Option, - pub(crate) lewes_protocol: Option, + pub(crate) websockets: WebSocketsV3, + pub(crate) network_requester: Option, + pub(crate) ipr: Option, + pub(crate) authenticator: Option, + pub(crate) wireguard: Option, + pub(crate) lewes_protocol: Option, } impl UnwrappedResolvedNodeDescribedInfo { pub(crate) fn into_node_description( self, - host_info: impl Into, - ) -> NymNodeDataV2 { - NymNodeDataV2 { + host_info: impl Into, + ) -> 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, } } diff --git a/nym-api/src/node_describe_cache/refresh.rs b/nym-api/src/node_describe_cache/refresh.rs index 3fd279e464..a81871ff39 100644 --- a/nym-api/src/node_describe_cache/refresh.rs +++ b/nym-api/src/node_describe_cache/refresh.rs @@ -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, } @@ -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, expected_identity: ed25519::PublicKey, - node_type: DescribedNodeTypeV2, + node_type: DescribedNodeTypeV3, node_id: NodeId, port: Option, ) -> Self { @@ -55,7 +55,7 @@ impl RefreshData { self.node_id } - pub(crate) async fn try_refresh(self, allow_all_ips: bool) -> Option { + pub(crate) async fn try_refresh(self, allow_all_ips: bool) -> Option { 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 { +) -> Result { 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, diff --git a/nym-api/src/node_status_api/cache/config_score.rs b/nym-api/src/node_status_api/cache/config_score.rs index d69dc7a4bd..246534d5ec 100644 --- a/nym-api/src/node_status_api/cache/config_score.rs +++ b/nym-api/src/node_status_api/cache/config_score.rs @@ -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) -> 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, +) -> 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::() 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, ) } diff --git a/nym-api/src/node_status_api/cache/mod.rs b/nym-api/src/node_status_api/cache/mod.rs index 9f41f176ef..1a81402083 100644 --- a/nym-api/src/node_status_api/cache/mod.rs +++ b/nym-api/src/node_status_api/cache/mod.rs @@ -103,4 +103,16 @@ impl NodeStatusCache { ) -> Result>, UninitialisedCache> { self.get(|c| &c.node_annotations).await } + + async fn node_balances( + &self, + ) -> Result>, NodeStatusCacheError> { + Ok(self + .cache() + .await? + .node_annotations + .iter() + .map(|(node_id, annotation)| (*node_id, annotation.on_chain_balance.clone())) + .collect::>()) + } } diff --git a/nym-api/src/node_status_api/cache/refresher.rs b/nym-api/src/node_status_api/cache/refresher.rs index c677cf946a..eaa4b05b5f 100644 --- a/nym-api/src/node_status_api/cache/refresher.rs +++ b/nym-api/src/node_status_api/cache/refresher.rs @@ -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, + // 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, @@ -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, contract_cache_listener: CacheNotificationWatcher, @@ -85,9 +107,14 @@ impl NodeStatusCacheRefresher { performance_provider: Box, 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>, 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::>::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>, ) -> HashMap { 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) diff --git a/nym-api/src/node_status_api/mod.rs b/nym-api/src/node_status_api/mod.rs index 00bb136e8c..66d16da149 100644 --- a/nym-api/src/node_status_api/mod.rs +++ b/nym-api/src/node_status_api/mod.rs @@ -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, 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 }); diff --git a/nym-api/src/nym_nodes/handlers/v2.rs b/nym-api/src/nym_nodes/handlers/v2.rs index 8a4ec985b7..3f3660e697 100644 --- a/nym-api/src/nym_nodes/handlers/v2.rs +++ b/nym-api/src/nym_nodes/handlers/v2.rs @@ -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 { @@ -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::>(); + // convert description to V2 + let descriptions = cache + .all_nodes() + .map(|d| d.clone().into()) + .collect::>(); 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, - Query(output): Query, + Query(output): Query, State(state): State, ) -> AxumResult> { let annotations = state.node_status_cache().node_annotations().await?; diff --git a/nym-api/src/support/cli/run.rs b/nym-api/src/support/cli/run.rs index 850a809e1a..68cce2f348 100644 --- a/nym-api/src/support/cli/run.rs +++ b/nym-api/src/support/cli/run.rs @@ -322,6 +322,7 @@ async fn start_nym_api_tasks(mut config: Config) -> anyhow::Result anyhow::Result 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, } } } diff --git a/nym-api/src/support/nyxd/mod.rs b/nym-api/src/support/nyxd/mod.rs index e93e24ad59..58eb9fe001 100644 --- a/nym-api/src/support/nyxd/mod.rs +++ b/nym-api/src/support/nyxd/mod.rs @@ -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 { nyxd_query!( self, diff --git a/nym-api/src/unstable_routes/v2/nym_nodes/semi_skimmed/mod.rs b/nym-api/src/unstable_routes/v2/nym_nodes/semi_skimmed/mod.rs index 56bd864b9a..bfd29979b5 100644 --- a/nym-api/src/unstable_routes/v2/nym_nodes/semi_skimmed/mod.rs +++ b/nym-api/src/unstable_routes/v2/nym_nodes/semi_skimmed/mod.rs @@ -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 where - NI: Iterator + 'a, + NI: Iterator + '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(); diff --git a/nym-api/src/unstable_routes/v2/nym_nodes/skimmed/helpers.rs b/nym-api/src/unstable_routes/v2/nym_nodes/skimmed/helpers.rs index 1a60756cd9..83100a9655 100644 --- a/nym-api/src/unstable_routes/v2/nym_nodes/skimmed/helpers.rs +++ b/nym-api/src/unstable_routes/v2/nym_nodes/skimmed/helpers.rs @@ -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 where - NI: Iterator + 'a, + NI: Iterator + '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 + 'a, + NI: Iterator + 'a, { // TODO: implement it let _ = query_params.per_page; diff --git a/nym-api/src/unstable_routes/v3/nym_nodes/semi_skimmed/mod.rs b/nym-api/src/unstable_routes/v3/nym_nodes/semi_skimmed/mod.rs index 7d3fa34463..7cff93b3c8 100644 --- a/nym-api/src/unstable_routes/v3/nym_nodes/semi_skimmed/mod.rs +++ b/nym-api/src/unstable_routes/v3/nym_nodes/semi_skimmed/mod.rs @@ -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, + nym_nodes: impl Iterator, annotations: &HashMap, current_key_rotation: u32, ) -> Vec { @@ -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();