diff --git a/nym-api/src/nym_contract_cache/handlers.rs b/nym-api/src/nym_contract_cache/handlers.rs index d57f1317c4..00f02e1fe2 100644 --- a/nym-api/src/nym_contract_cache/handlers.rs +++ b/nym-api/src/nym_contract_cache/handlers.rs @@ -6,10 +6,12 @@ use crate::node_status_api::helpers::{ _get_rewarded_set_legacy_mixnodes_detailed, }; use crate::support::http::state::AppState; +use crate::support::legacy_helpers::{to_legacy_gateway, to_legacy_mixnode}; use axum::extract::State; use axum::{Json, Router}; use nym_api_requests::legacy::LegacyMixNodeDetailsWithLayer; use nym_api_requests::models::MixNodeBondAnnotated; +use nym_mixnet_contract_common::reward_params::Performance; use nym_mixnet_contract_common::{reward_params::RewardingParams, GatewayBond, Interval, NodeId}; use std::collections::HashSet; @@ -58,11 +60,47 @@ pub(crate) fn nym_contract_cache_routes() -> Router { )] #[deprecated] async fn get_mixnodes(State(state): State) -> Json> { - state - .nym_contract_cache() - .legacy_mixnodes_filtered() - .await - .into() + let mut out = state.nym_contract_cache().legacy_mixnodes_filtered().await; + + let Ok(describe_cache) = state.described_nodes_cache.get().await else { + return Json(out); + }; + + let Some(migrated_nymnodes) = state.nym_contract_cache().all_cached_nym_nodes().await else { + return Json(out); + }; + + let Ok(annotations) = state.node_annotations().await else { + return Json(out); + }; + + // safety: valid percentage value + #[allow(clippy::unwrap_used)] + let p50 = Performance::from_percentage_value(50).unwrap(); + + for nym_node in &**migrated_nymnodes { + // if we can't get it self-described data, ignore it + let Some(description) = describe_cache.get_description(&nym_node.node_id()) else { + continue; + }; + // if the node hasn't declared it can be a mixnode, ignore it + if !description.declared_role.mixnode { + continue; + } + // if we don't have annotation for this node, ignore it + let Some(annotation) = annotations.get(&nym_node.node_id()) else { + continue; + }; + // equivalent of legacy mixnode being blacklisted + if annotation.last_24h_performance < p50 { + continue; + } + + let node = to_legacy_mixnode(nym_node, description); + out.push(node); + } + + Json(out) } // DEPRECATED: this endpoint now lives in `node_status_api`. Once all consumers are updated, @@ -97,15 +135,54 @@ async fn get_mixnodes_detailed(State(state): State) -> Json) -> Json> { - Json( - state - .nym_contract_cache() - .legacy_gateways_filtered() - .await - .into_iter() - .map(Into::into) - .collect(), - ) + // legacy + let mut out: Vec = state + .nym_contract_cache() + .legacy_gateways_filtered() + .await + .into_iter() + .map(Into::into) + .collect(); + + let Ok(describe_cache) = state.described_nodes_cache.get().await else { + return Json(out); + }; + + let Some(migrated_nymnodes) = state.nym_contract_cache().all_cached_nym_nodes().await else { + return Json(out); + }; + + let Ok(annotations) = state.node_annotations().await else { + return Json(out); + }; + + // safety: valid percentage value + #[allow(clippy::unwrap_used)] + let p50 = Performance::from_percentage_value(50).unwrap(); + + for nym_node in &**migrated_nymnodes { + // if we can't get it self-described data, ignore it + let Some(description) = describe_cache.get_description(&nym_node.node_id()) else { + continue; + }; + // if the node hasn't declared it can be a gateway, ignore it + if !description.declared_role.entry { + continue; + } + // if we don't have annotation for this node, ignore it + let Some(annotation) = annotations.get(&nym_node.node_id()) else { + continue; + }; + // equivalent of legacy gateway being blacklisted + if annotation.last_24h_performance < p50 { + continue; + } + + let node = to_legacy_gateway(nym_node, description); + out.push(node); + } + + Json(out) } #[utoipa::path( @@ -166,12 +243,59 @@ async fn get_rewarded_set_detailed( )] #[deprecated] async fn get_active_set(State(state): State) -> Json> { - state + let mut out = state .nym_contract_cache() .legacy_v1_active_set_mixnodes() .await - .clone() - .into() + .clone(); + + let Some(rewarded_set) = state.nym_contract_cache().rewarded_set().await else { + return Json(out); + }; + + let Ok(describe_cache) = state.described_nodes_cache.get().await else { + return Json(out); + }; + + let Some(migrated_nymnodes) = state.nym_contract_cache().all_cached_nym_nodes().await else { + return Json(out); + }; + + let Ok(annotations) = state.node_annotations().await else { + return Json(out); + }; + + // safety: valid percentage value + #[allow(clippy::unwrap_used)] + let p50 = Performance::from_percentage_value(50).unwrap(); + + for nym_node in &**migrated_nymnodes { + // if we can't get it self-described data, ignore it + let Some(description) = describe_cache.get_description(&nym_node.node_id()) else { + continue; + }; + // if the node hasn't declared it can be a mixnode, ignore it + if !description.declared_role.mixnode { + continue; + } + // if we don't have annotation for this node, ignore it + let Some(annotation) = annotations.get(&nym_node.node_id()) else { + continue; + }; + // equivalent of legacy mixnode being blacklisted + if annotation.last_24h_performance < p50 { + continue; + } + // if the node is not in the active set, ignore it + if !rewarded_set.is_active_mixnode(&nym_node.node_id()) { + continue; + } + + let node = to_legacy_mixnode(nym_node, description); + out.push(node); + } + + Json(out) } // DEPRECATED: this endpoint now lives in `node_status_api`. Once all consumers are updated, diff --git a/nym-api/src/nym_nodes/handlers/legacy.rs b/nym-api/src/nym_nodes/handlers/legacy.rs index cae94d5254..3dd6a51f74 100644 --- a/nym-api/src/nym_nodes/handlers/legacy.rs +++ b/nym-api/src/nym_nodes/handlers/legacy.rs @@ -2,8 +2,10 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::support::http::state::AppState; +use crate::support::legacy_helpers::{to_legacy_gateway, to_legacy_mixnode}; use axum::extract::State; use axum::{Json, Router}; +use nym_api_requests::legacy::LegacyMixNodeBondWithLayer; use nym_api_requests::models::{LegacyDescribedGateway, LegacyDescribedMixNode}; // we want to mark the routes as deprecated in swagger, but still expose them @@ -34,25 +36,44 @@ async fn get_gateways_described( ) -> Json> { let contract_cache = state.nym_contract_cache(); let describe_cache = state.described_nodes_cache(); - let gateways = contract_cache.legacy_gateways_filtered().await; - if gateways.is_empty() { - return Json(Vec::new()); - } + + // legacy + let legacy = contract_cache.legacy_gateways_filtered().await; // if the self describe cache is unavailable, well, don't attach describe data and only return legacy gateways - let Ok(self_descriptions) = describe_cache.get().await else { - return Json(gateways.into_iter().map(Into::into).collect()); + let Ok(describe_cache) = describe_cache.get().await else { + return Json(legacy.into_iter().map(Into::into).collect()); }; - Json( - gateways - .into_iter() - .map(|bond| LegacyDescribedGateway { - self_described: self_descriptions.get_description(&bond.node_id).cloned(), - bond: bond.bond, - }) - .collect(), - ) + let migrated_nymnodes = state.nym_contract_cache().nym_nodes().await; + let mut out = Vec::new(); + + for legacy_bond in legacy { + out.push(LegacyDescribedGateway { + self_described: describe_cache + .get_description(&legacy_bond.node_id) + .cloned(), + bond: legacy_bond.bond, + }) + } + + for nym_node in migrated_nymnodes { + // we ALWAYS need description to set legacy fields + let Some(description) = describe_cache.get_description(&nym_node.node_id()) else { + continue; + }; + // if the node hasn't declared it can be a gateway, ignore it + if !description.declared_role.entry { + continue; + } + + out.push(LegacyDescribedGateway { + bond: to_legacy_gateway(&nym_node, description), + self_described: Some(description.clone()), + }) + } + + Json(out) } #[utoipa::path( @@ -70,28 +91,43 @@ async fn get_mixnodes_described( let contract_cache = state.nym_contract_cache(); let describe_cache = state.described_nodes_cache(); - let mixnodes = contract_cache + let legacy: Vec = contract_cache .legacy_mixnodes_filtered() .await .into_iter() .map(|m| m.bond_information) .collect::>(); - if mixnodes.is_empty() { - return Json(Vec::new()); - } - // if the self describe cache is unavailable, well, don't attach describe data - let Ok(self_descriptions) = describe_cache.get().await else { - return Json(mixnodes.into_iter().map(Into::into).collect()); + // if the self describe cache is unavailable, well, don't attach describe data and only return legacy mixnodes + let Ok(describe_cache) = describe_cache.get().await else { + return Json(legacy.into_iter().map(Into::into).collect()); }; - Json( - mixnodes - .into_iter() - .map(|bond| LegacyDescribedMixNode { - self_described: self_descriptions.get_description(&bond.mix_id).cloned(), - bond, - }) - .collect(), - ) + let migrated_nymnodes = state.nym_contract_cache().nym_nodes().await; + let mut out = Vec::new(); + + for legacy_bond in legacy { + out.push(LegacyDescribedMixNode { + self_described: describe_cache.get_description(&legacy_bond.mix_id).cloned(), + bond: legacy_bond, + }) + } + + for nym_node in migrated_nymnodes { + // we ALWAYS need description to set legacy fields + let Some(description) = describe_cache.get_description(&nym_node.node_id()) else { + continue; + }; + // if the node hasn't declared it can be a gateway, ignore it + if !description.declared_role.mixnode { + continue; + } + + out.push(LegacyDescribedMixNode { + bond: to_legacy_mixnode(&nym_node, description).bond_information, + self_described: Some(description.clone()), + }) + } + + Json(out) } diff --git a/nym-api/src/support/legacy_helpers.rs b/nym-api/src/support/legacy_helpers.rs new file mode 100644 index 0000000000..d5135c7643 --- /dev/null +++ b/nym-api/src/support/legacy_helpers.rs @@ -0,0 +1,85 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use nym_api_requests::legacy::{LegacyMixNodeBondWithLayer, LegacyMixNodeDetailsWithLayer}; +use nym_api_requests::models::NymNodeData; +use nym_config::defaults::DEFAULT_NYM_NODE_HTTP_PORT; +use nym_crypto::aes::cipher::crypto_common::rand_core::OsRng; +use nym_mixnet_contract_common::mixnode::LegacyPendingMixNodeChanges; +use nym_mixnet_contract_common::{ + Gateway, GatewayBond, LegacyMixLayer, MixNode, MixNodeBond, NymNodeDetails, +}; +use rand::prelude::SliceRandom; + +pub(crate) fn to_legacy_mixnode( + nym_node: &NymNodeDetails, + description: &NymNodeData, +) -> LegacyMixNodeDetailsWithLayer { + let layer_choices = [ + LegacyMixLayer::One, + LegacyMixLayer::Two, + LegacyMixLayer::Three, + ]; + let mut rng = OsRng; + + // slap a random layer on it because legacy clients don't understand a concept of layerless mixnodes + // SAFETY: the slice is not empty so the unwrap is fine + #[allow(clippy::unwrap_used)] + let layer = layer_choices.choose(&mut rng).copied().unwrap(); + + LegacyMixNodeDetailsWithLayer { + bond_information: LegacyMixNodeBondWithLayer { + bond: MixNodeBond { + mix_id: nym_node.node_id(), + owner: nym_node.bond_information.owner.clone(), + original_pledge: nym_node.bond_information.original_pledge.clone(), + mix_node: MixNode { + host: nym_node.bond_information.node.host.clone(), + mix_port: description.mix_port(), + verloc_port: description.verloc_port(), + http_api_port: nym_node + .bond_information + .node + .custom_http_port + .unwrap_or(DEFAULT_NYM_NODE_HTTP_PORT), + sphinx_key: description.host_information.keys.x25519.to_base58_string(), + identity_key: nym_node.bond_information.node.identity_key.clone(), + version: description.build_information.build_version.clone(), + }, + proxy: None, + bonding_height: nym_node.bond_information.bonding_height, + is_unbonding: nym_node.bond_information.is_unbonding, + }, + layer, + }, + rewarding_details: nym_node.rewarding_details.clone(), + pending_changes: LegacyPendingMixNodeChanges { + pledge_change: nym_node.pending_changes.pledge_change, + }, + } +} + +pub(crate) fn to_legacy_gateway( + nym_node: &NymNodeDetails, + description: &NymNodeData, +) -> GatewayBond { + GatewayBond { + pledge_amount: nym_node.bond_information.original_pledge.clone(), + owner: nym_node.bond_information.owner.clone(), + block_height: nym_node.bond_information.bonding_height, + gateway: Gateway { + host: nym_node.bond_information.node.host.clone(), + mix_port: description.mix_port(), + clients_port: description.mixnet_websockets.ws_port, + location: description + .auxiliary_details + .location + .map(|c| c.to_string()) + .unwrap_or_default(), + sphinx_key: description.host_information.keys.x25519.to_base58_string(), + identity_key: nym_node.bond_information.node.identity_key.clone(), + version: description.build_information.build_version.clone(), + }, + proxy: None, + } +} diff --git a/nym-api/src/support/mod.rs b/nym-api/src/support/mod.rs index 28abaf3b94..a1773f9804 100644 --- a/nym-api/src/support/mod.rs +++ b/nym-api/src/support/mod.rs @@ -7,3 +7,5 @@ pub(crate) mod config; pub(crate) mod http; pub(crate) mod nyxd; pub(crate) mod storage; + +pub(crate) mod legacy_helpers;