LP: announced KEM key hashes (#6349)

* announce KEM key hashes and use generated value within LpStateMachine

* added digest of remote KEM key into LpSession

* changed  constructor to LpSession to take explicit key materials for local and remote

this makes it easier to change keys required by each party without having to change all the interfaces everywhere again

* extended the changes to LpStateMachine constructor

* modify the interface to LpRegistrationHandler and LpListener

* gateway probe fixes

* temp nym-lp-client fixes

* review nits

* remove network test

* introduced v2/nym-nodes/described endpoint for returning nodes description alongside LP data

* missed V1 -> V2 description replacements

* removed deprecated call within mix-fetch

* use old v1 call in network stats
This commit is contained in:
Jędrzej Stuczyński
2026-01-22 14:29:33 +00:00
committed by GitHub
parent 7462926bcf
commit c1ddcc75cf
75 changed files with 2930 additions and 2334 deletions
+9 -6
View File
@@ -12,10 +12,13 @@ documentation.workspace = true
[dependencies]
bs58 = { workspace = true }
celes = { workspace = true } # country codes
cosmrs = { workspace = true }
cosmwasm-std = { workspace = true }
schemars = { workspace = true, features = ["preserve_order"] }
serde = { workspace = true, features = ["derive"] }
strum = { workspace = true, features = ["derive"] }
strum_macros = { workspace = true }
humantime-serde = { workspace = true }
hex = { workspace = true }
serde_json = { workspace = true }
@@ -35,11 +38,11 @@ nym-serde-helpers = { workspace = true, features = ["bs58", "base64", "date"] }
nym-credentials-interface = { workspace = true }
nym-crypto = { workspace = true, features = ["serde", "asymmetric"] }
nym-config = { workspace = true }
nym-ecash-time = { workspace = true }
nym-compact-ecash = { workspace = true }
nym-contracts-common = { workspace = true , features = ["naive_float"] }
nym-mixnet-contract-common = { workspace = true , features = ["utoipa"] }
nym-config = { workspace = true }
nym-ecash-time = { workspace = true }
nym-compact-ecash = { workspace = true }
nym-contracts-common = { workspace = true, features = ["naive_float"] }
nym-mixnet-contract-common = { workspace = true, features = ["utoipa"] }
nym-coconut-dkg-common = { workspace = true }
nym-node-requests = { workspace = true, default-features = false, features = ["openapi"] }
nym-noise-keys = { workspace = true }
@@ -51,7 +54,7 @@ nym-ecash-signer-check-types = { workspace = true }
[dev-dependencies]
rand_chacha = { workspace = true }
nym-crypto = { workspace = true, features = ["rand"] }
nym-test-utils = { workspace = true }
[features]
default = []
@@ -1,418 +0,0 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::models::{BinaryBuildInformationOwned, OffsetDateTimeJsonSchemaWrapper};
use crate::nym_nodes::{BasicEntryInformation, NodeRole, SemiSkimmedNode, SkimmedNode};
use nym_crypto::asymmetric::ed25519::serde_helpers::bs58_ed25519_pubkey;
use nym_crypto::asymmetric::x25519::serde_helpers::bs58_x25519_pubkey;
use nym_crypto::asymmetric::{ed25519, x25519};
use nym_mixnet_contract_common::reward_params::Performance;
use nym_mixnet_contract_common::NodeId;
use nym_network_defaults::{
DEFAULT_MIX_LISTENING_PORT, DEFAULT_VERLOC_LISTENING_PORT, WG_METADATA_PORT, WG_TUNNEL_PORT,
};
use nym_node_requests::api::v1::authenticator::models::Authenticator;
use nym_node_requests::api::v1::gateway::models::Wireguard;
use nym_node_requests::api::v1::ip_packet_router::models::IpPacketRouter;
use nym_node_requests::api::v1::lewes_protocol::models::LewesProtocol;
use nym_node_requests::api::v1::node::models::{AuxiliaryDetails, NodeRoles};
use nym_noise_keys::VersionedNoiseKey;
use serde::{Deserialize, Serialize};
use std::net::IpAddr;
use tracing::warn;
use utoipa::ToSchema;
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub struct HostInformation {
#[schema(value_type = Vec<String>)]
pub ip_address: Vec<IpAddr>,
pub hostname: Option<String>,
pub keys: HostKeys,
}
impl From<nym_node_requests::api::v1::node::models::HostInformation> for HostInformation {
fn from(value: nym_node_requests::api::v1::node::models::HostInformation) -> Self {
HostInformation {
ip_address: value.ip_address,
hostname: value.hostname,
keys: value.keys.into(),
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub struct HostKeys {
#[serde(with = "bs58_ed25519_pubkey")]
#[schemars(with = "String")]
#[schema(value_type = String)]
pub ed25519: ed25519::PublicKey,
#[deprecated(note = "use the current_x25519_sphinx_key with explicit rotation information")]
#[serde(with = "bs58_x25519_pubkey")]
#[schemars(with = "String")]
#[schema(value_type = String)]
pub x25519: x25519::PublicKey,
pub current_x25519_sphinx_key: SphinxKey,
#[serde(default)]
pub pre_announced_x25519_sphinx_key: Option<SphinxKey>,
#[serde(default)]
pub x25519_versioned_noise: Option<VersionedNoiseKey>,
}
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub struct SphinxKey {
pub rotation_id: u32,
#[serde(with = "bs58_x25519_pubkey")]
#[schemars(with = "String")]
#[schema(value_type = String)]
pub public_key: x25519::PublicKey,
}
impl From<nym_node_requests::api::v1::node::models::SphinxKey> for SphinxKey {
fn from(value: nym_node_requests::api::v1::node::models::SphinxKey) -> Self {
SphinxKey {
rotation_id: value.rotation_id,
public_key: value.public_key,
}
}
}
impl From<nym_node_requests::api::v1::node::models::HostKeys> for HostKeys {
fn from(value: nym_node_requests::api::v1::node::models::HostKeys) -> Self {
HostKeys {
ed25519: value.ed25519_identity,
x25519: value.x25519_sphinx,
current_x25519_sphinx_key: value.primary_x25519_sphinx_key.into(),
pre_announced_x25519_sphinx_key: value.pre_announced_x25519_sphinx_key.map(Into::into),
x25519_versioned_noise: value.x25519_versioned_noise,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub struct WebSockets {
pub ws_port: u16,
pub wss_port: Option<u16>,
}
impl From<nym_node_requests::api::v1::gateway::models::WebSockets> for WebSockets {
fn from(value: nym_node_requests::api::v1::gateway::models::WebSockets) -> Self {
WebSockets {
ws_port: value.ws_port,
wss_port: value.wss_port,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub struct NoiseDetails {
pub key: VersionedNoiseKey,
pub mixnet_port: u16,
#[schema(value_type = Vec<String>)]
pub ip_addresses: Vec<IpAddr>,
}
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub struct NymNodeDescription {
#[schema(value_type = u32)]
pub node_id: NodeId,
pub contract_node_type: DescribedNodeType,
pub description: NymNodeData,
}
impl NymNodeDescription {
pub fn version(&self) -> &str {
&self.description.build_information.build_version
}
pub fn entry_information(&self) -> BasicEntryInformation {
BasicEntryInformation {
hostname: self.description.host_information.hostname.clone(),
ws_port: self.description.mixnet_websockets.ws_port,
wss_port: self.description.mixnet_websockets.wss_port,
}
}
pub fn ed25519_identity_key(&self) -> ed25519::PublicKey {
self.description.host_information.keys.ed25519
}
pub fn current_sphinx_key(&self, current_rotation_id: u32) -> x25519::PublicKey {
let keys = &self.description.host_information.keys;
if keys.current_x25519_sphinx_key.rotation_id == u32::MAX {
// legacy case (i.e. node doesn't support rotation)
return keys.current_x25519_sphinx_key.public_key;
}
if current_rotation_id == keys.current_x25519_sphinx_key.rotation_id {
// it's the 'current' key
return keys.current_x25519_sphinx_key.public_key;
}
if let Some(pre_announced) = &keys.pre_announced_x25519_sphinx_key {
if pre_announced.rotation_id == current_rotation_id {
return pre_announced.public_key;
}
}
warn!(
"unexpected key rotation {current_rotation_id} for node {}",
self.node_id
);
// this should never be reached, but just in case, return the fallback option
keys.current_x25519_sphinx_key.public_key
}
pub fn to_skimmed_node(
&self,
current_rotation_id: u32,
role: NodeRole,
performance: Performance,
) -> SkimmedNode {
let keys = &self.description.host_information.keys;
let entry = if self.description.declared_role.entry {
Some(self.entry_information())
} else {
None
};
SkimmedNode {
node_id: self.node_id,
ed25519_identity_pubkey: keys.ed25519,
ip_addresses: self.description.host_information.ip_address.clone(),
mix_port: self.description.mix_port(),
x25519_sphinx_pubkey: self.current_sphinx_key(current_rotation_id),
// we can't use the declared roles, we have to take whatever was provided in the contract.
// why? say this node COULD operate as an exit, but it might be the case the contract decided
// to assign it an ENTRY role only. we have to use that one instead.
role,
supported_roles: self.description.declared_role,
entry,
performance,
}
}
pub fn to_semi_skimmed_node(
&self,
current_rotation_id: u32,
role: NodeRole,
performance: Performance,
) -> SemiSkimmedNode {
let skimmed_node = self.to_skimmed_node(current_rotation_id, role, performance);
SemiSkimmedNode {
basic: skimmed_node,
x25519_noise_versioned_key: self
.description
.host_information
.keys
.x25519_versioned_noise,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
#[serde(rename_all = "snake_case")]
#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))]
#[cfg_attr(
feature = "generate-ts",
ts(
export,
export_to = "ts-packages/types/src/types/rust/DescribedNodeType.ts"
)
)]
pub enum DescribedNodeType {
LegacyMixnode,
LegacyGateway,
NymNode,
}
impl DescribedNodeType {
pub fn is_nym_node(&self) -> bool {
matches!(self, DescribedNodeType::NymNode)
}
}
#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))]
#[cfg_attr(
feature = "generate-ts",
ts(
export,
export_to = "ts-packages/types/src/types/rust/DeclaredRoles.ts"
)
)]
pub struct DeclaredRoles {
pub mixnode: bool,
pub entry: bool,
pub exit_nr: bool,
pub exit_ipr: bool,
}
impl DeclaredRoles {
pub fn can_operate_exit_gateway(&self) -> bool {
self.exit_ipr && self.exit_nr
}
}
impl From<NodeRoles> for DeclaredRoles {
fn from(value: NodeRoles) -> Self {
DeclaredRoles {
mixnode: value.mixnode_enabled,
entry: value.gateway_enabled,
exit_nr: value.gateway_enabled && value.network_requester_enabled,
exit_ipr: value.gateway_enabled && value.ip_packet_router_enabled,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub struct NetworkRequesterDetails {
/// address of the embedded network requester
pub address: String,
/// flag indicating whether this network requester uses the exit policy rather than the deprecated allow list
pub uses_exit_policy: bool,
}
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub struct IpPacketRouterDetails {
/// address of the embedded ip packet router
pub address: String,
}
// works for current simple case.
impl From<IpPacketRouter> for IpPacketRouterDetails {
fn from(value: IpPacketRouter) -> Self {
IpPacketRouterDetails {
address: value.address,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub struct AuthenticatorDetails {
/// address of the embedded authenticator
pub address: String,
}
// works for current simple case.
impl From<Authenticator> for AuthenticatorDetails {
fn from(value: Authenticator) -> Self {
AuthenticatorDetails {
address: value.address,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub struct WireguardDetails {
// NOTE: the port field is deprecated in favour of tunnel_port
pub port: u16,
#[serde(default = "default_tunnel_port")]
pub tunnel_port: u16,
#[serde(default = "default_metadata_port")]
pub metadata_port: u16,
pub public_key: String,
}
fn default_tunnel_port() -> u16 {
WG_TUNNEL_PORT
}
fn default_metadata_port() -> u16 {
WG_METADATA_PORT
}
// works for current simple case.
impl From<Wireguard> for WireguardDetails {
fn from(value: Wireguard) -> Self {
WireguardDetails {
port: value.port,
tunnel_port: value.tunnel_port,
metadata_port: value.metadata_port,
public_key: value.public_key,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub struct LewesProtocolDetails {
/// Helper field that specifies whether the LP listener(s) is enabled on this node.
/// It is directly controlled by the node's role (i.e. it is enabled if it supports 'entry' mode)
pub enabled: bool,
/// LP TCP control address (default: 41264) for establishing LP sessions
pub control_port: u16,
/// LP UDP data address (default: 51264) for Sphinx packets wrapped in LP
pub data_port: u16,
}
impl From<LewesProtocol> for LewesProtocolDetails {
fn from(value: LewesProtocol) -> Self {
LewesProtocolDetails {
enabled: value.enabled,
control_port: value.control_port,
data_port: value.data_port,
}
}
}
// this struct is getting quite bloated...
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub struct NymNodeData {
#[serde(default)]
pub last_polled: OffsetDateTimeJsonSchemaWrapper,
pub host_information: HostInformation,
#[serde(default)]
pub declared_role: DeclaredRoles,
#[serde(default)]
pub auxiliary_details: AuxiliaryDetails,
// TODO: do we really care about ALL build info or just the version?
pub build_information: BinaryBuildInformationOwned,
#[serde(default)]
pub network_requester: Option<NetworkRequesterDetails>,
#[serde(default)]
pub ip_packet_router: Option<IpPacketRouterDetails>,
#[serde(default)]
pub authenticator: Option<AuthenticatorDetails>,
#[serde(default)]
pub wireguard: Option<WireguardDetails>,
#[serde(default)]
pub lewes_protocol: Option<LewesProtocolDetails>,
// for now we only care about their ws/wss situation, nothing more
pub mixnet_websockets: WebSockets,
}
impl NymNodeData {
pub fn mix_port(&self) -> u16 {
self.auxiliary_details
.announce_ports
.mix_port
.unwrap_or(DEFAULT_MIX_LISTENING_PORT)
}
pub fn verloc_port(&self) -> u16 {
self.auxiliary_details
.announce_ports
.verloc_port
.unwrap_or(DEFAULT_VERLOC_LISTENING_PORT)
}
}
@@ -0,0 +1,26 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use nym_noise_keys::VersionedNoiseKeyV1;
use serde::{Deserialize, Serialize};
use std::net::IpAddr;
use utoipa::ToSchema;
pub mod type_translation;
pub mod v1;
pub mod v2;
// don't break existing imports
pub use type_translation::*;
pub use v1::*;
pub use v2::*;
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub struct NoiseDetails {
pub key: VersionedNoiseKeyV1,
pub mixnet_port: u16,
#[schema(value_type = Vec<String>)]
pub ip_addresses: Vec<IpAddr>,
}
@@ -0,0 +1,426 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
//! This redefines relevant types present within nym-node-requests for the purposes of this crate
//! and defines required conversion methods
use celes::Country;
use nym_crypto::asymmetric::ed25519::serde_helpers::bs58_ed25519_pubkey;
use nym_crypto::asymmetric::x25519::serde_helpers::bs58_x25519_pubkey;
use nym_crypto::asymmetric::{ed25519, x25519};
use nym_network_defaults::{WG_METADATA_PORT, WG_TUNNEL_PORT};
use nym_noise_keys::VersionedNoiseKeyV1;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::net::IpAddr;
use strum_macros::{Display, EnumString};
use utoipa::ToSchema;
// to whoever is thinking of modifying this struct.
// you MUST NOT change its structure in any way - adding, removing or changing fields
// otherwise, it will break old clients as bincode serialisation is not backwards compatible
// even if you put `#[serde(default)]` all over the place
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema, PartialEq)]
pub struct HostInformationV1 {
#[schema(value_type = Vec<String>)]
pub ip_address: Vec<IpAddr>,
pub hostname: Option<String>,
pub keys: HostKeysV1,
}
// to whoever is thinking of modifying this struct.
// you MUST NOT change its structure in any way - adding, removing or changing fields
// otherwise, it will break old clients as bincode serialisation is not backwards compatible
// even if you put `#[serde(default)]` all over the place
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema, PartialEq)]
pub struct HostKeysV1 {
#[serde(with = "bs58_ed25519_pubkey")]
#[schemars(with = "String")]
#[schema(value_type = String)]
pub ed25519: ed25519::PublicKey,
#[deprecated(note = "use the current_x25519_sphinx_key with explicit rotation information")]
#[serde(with = "bs58_x25519_pubkey")]
#[schemars(with = "String")]
#[schema(value_type = String)]
pub x25519: x25519::PublicKey,
pub current_x25519_sphinx_key: SphinxKeyV1,
#[serde(default)]
pub pre_announced_x25519_sphinx_key: Option<SphinxKeyV1>,
#[serde(default)]
pub x25519_versioned_noise: Option<VersionedNoiseKeyV1>,
}
// to whoever is thinking of modifying this struct.
// you MUST NOT change its structure in any way - adding, removing or changing fields
// otherwise, it will break old clients as bincode serialisation is not backwards compatible
// even if you put `#[serde(default)]` all over the place
#[derive(
Clone, Copy, Debug, Default, Serialize, Deserialize, schemars::JsonSchema, ToSchema, PartialEq,
)]
pub struct AnnouncePortsV1 {
pub verloc_port: Option<u16>,
pub mix_port: Option<u16>,
}
// to whoever is thinking of modifying this struct.
// you MUST NOT change its structure in any way - adding, removing or changing fields
// otherwise, it will break old clients as bincode serialisation is not backwards compatible
// even if you put `#[serde(default)]` all over the place
#[derive(
Clone, Copy, Debug, Default, Serialize, Deserialize, schemars::JsonSchema, ToSchema, PartialEq,
)]
pub struct AuxiliaryDetailsV1 {
/// Optional ISO 3166 alpha-2 two-letter country code of the node's **physical** location
#[schema(example = "PL", value_type = Option<String>)]
#[schemars(with = "Option<String>")]
#[schemars(length(equal = 2))]
pub location: Option<Country>,
#[serde(default)]
pub announce_ports: AnnouncePortsV1,
/// Specifies whether this node operator has agreed to the terms and conditions
/// as defined at <https://nymtech.net/terms-and-conditions/operators/v1.0.0>
// make sure to include the default deserialisation as this field hasn't existed when the struct was first created
#[serde(default)]
pub accepted_operator_terms_and_conditions: bool,
}
// to whoever is thinking of modifying this struct.
// you MUST NOT change its structure in any way - adding, removing or changing fields
// otherwise, it will break old clients as bincode serialisation is not backwards compatible
// even if you put `#[serde(default)]` all over the place
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema, PartialEq)]
pub struct SphinxKeyV1 {
pub rotation_id: u32,
#[serde(with = "bs58_x25519_pubkey")]
#[schemars(with = "String")]
#[schema(value_type = String)]
pub public_key: x25519::PublicKey,
}
// to whoever is thinking of modifying this struct.
// you MUST NOT change its structure in any way - adding, removing or changing fields
// otherwise, it will break old clients as bincode serialisation is not backwards compatible
// even if you put `#[serde(default)]` all over the place
#[derive(
Clone, Copy, Debug, Default, Serialize, Deserialize, schemars::JsonSchema, ToSchema, PartialEq,
)]
#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))]
#[cfg_attr(
feature = "generate-ts",
ts(
export,
export_to = "ts-packages/types/src/types/rust/DeclaredRoles.ts"
)
)]
pub struct DeclaredRolesV1 {
pub mixnode: bool,
pub entry: bool,
pub exit_nr: bool,
pub exit_ipr: bool,
}
impl DeclaredRolesV1 {
pub fn can_operate_exit_gateway(&self) -> bool {
self.exit_ipr && self.exit_nr
}
}
// to whoever is thinking of modifying this struct.
// you MUST NOT change its structure in any way - adding, removing or changing fields
// otherwise, it will break old clients as bincode serialisation is not backwards compatible
// even if you put `#[serde(default)]` all over the place
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema, PartialEq)]
pub struct WebSocketsV1 {
pub ws_port: u16,
pub wss_port: Option<u16>,
}
// to whoever is thinking of modifying this struct.
// you MUST NOT change its structure in any way - adding, removing or changing fields
// otherwise, it will break old clients as bincode serialisation is not backwards compatible
// even if you put `#[serde(default)]` all over the place
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema, PartialEq)]
pub struct NetworkRequesterDetailsV1 {
/// address of the embedded network requester
pub address: String,
/// flag indicating whether this network requester uses the exit policy rather than the deprecated allow list
pub uses_exit_policy: bool,
}
// to whoever is thinking of modifying this struct.
// you MUST NOT change its structure in any way - adding, removing or changing fields
// otherwise, it will break old clients as bincode serialisation is not backwards compatible
// even if you put `#[serde(default)]` all over the place
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema, PartialEq)]
pub struct IpPacketRouterDetailsV1 {
/// address of the embedded ip packet router
pub address: String,
}
// to whoever is thinking of modifying this struct.
// you MUST NOT change its structure in any way - adding, removing or changing fields
// otherwise, it will break old clients as bincode serialisation is not backwards compatible
// even if you put `#[serde(default)]` all over the place
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema, PartialEq)]
pub struct AuthenticatorDetailsV1 {
/// address of the embedded authenticator
pub address: String,
}
// to whoever is thinking of modifying this struct.
// you MUST NOT change its structure in any way - adding, removing or changing fields
// otherwise, it will break old clients as bincode serialisation is not backwards compatible
// even if you put `#[serde(default)]` all over the place
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema, PartialEq)]
pub struct WireguardDetailsV1 {
// NOTE: the port field is deprecated in favour of tunnel_port
pub port: u16,
#[serde(default = "default_tunnel_port")]
pub tunnel_port: u16,
#[serde(default = "default_metadata_port")]
pub metadata_port: u16,
pub public_key: String,
}
// to whoever is thinking of modifying this struct.
// you MUST NOT change its structure in any way - adding, removing or changing fields
// otherwise, it will break old clients as bincode serialisation is not backwards compatible
// even if you put `#[serde(default)]` all over the place
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema, PartialEq)]
pub struct LewesProtocolDetailsV1 {
/// Helper field that specifies whether the LP listener(s) is enabled on this node.
/// It is directly controlled by the node's role (i.e. it is enabled if it supports 'entry' mode)
pub enabled: bool,
/// LP TCP control address (default: 41264) for establishing LP sessions
pub control_port: u16,
/// LP UDP data address (default: 51264) for Sphinx packets wrapped in LP
pub data_port: u16,
/// Digests of the KEM keys available to this node alongside hashing algorithms used
/// for their computation.
pub kem_keys: HashMap<LPKEM, HashMap<LPHashFunction, Vec<u8>>>,
}
#[derive(
Serialize,
Deserialize,
Debug,
Clone,
Copy,
JsonSchema,
Hash,
PartialEq,
Eq,
PartialOrd,
Display,
EnumString,
ToSchema,
)]
#[strum(serialize_all = "lowercase")]
pub enum LPKEM {
MlKem768,
XWing,
X25519,
McEliece,
}
#[derive(
Serialize,
Deserialize,
Debug,
Clone,
Copy,
JsonSchema,
Hash,
PartialEq,
Eq,
PartialOrd,
Display,
EnumString,
ToSchema,
)]
#[strum(serialize_all = "lowercase")]
pub enum LPHashFunction {
Blake3,
Shake128,
Shake256,
Sha256,
}
impl From<nym_node_requests::api::v1::node::models::HostInformation> for HostInformationV1 {
fn from(value: nym_node_requests::api::v1::node::models::HostInformation) -> Self {
HostInformationV1 {
ip_address: value.ip_address,
hostname: value.hostname,
keys: value.keys.into(),
}
}
}
impl From<nym_node_requests::api::v1::node::models::SphinxKey> for SphinxKeyV1 {
fn from(value: nym_node_requests::api::v1::node::models::SphinxKey) -> Self {
SphinxKeyV1 {
rotation_id: value.rotation_id,
public_key: value.public_key,
}
}
}
impl From<nym_node_requests::api::v1::node::models::HostKeys> for HostKeysV1 {
fn from(value: nym_node_requests::api::v1::node::models::HostKeys) -> Self {
HostKeysV1 {
ed25519: value.ed25519_identity,
x25519: value.x25519_sphinx,
current_x25519_sphinx_key: value.primary_x25519_sphinx_key.into(),
pre_announced_x25519_sphinx_key: value.pre_announced_x25519_sphinx_key.map(Into::into),
x25519_versioned_noise: value.x25519_versioned_noise,
}
}
}
impl From<nym_node_requests::api::v1::node::models::AnnouncePorts> for AnnouncePortsV1 {
fn from(value: nym_node_requests::api::v1::node::models::AnnouncePorts) -> Self {
AnnouncePortsV1 {
verloc_port: value.verloc_port,
mix_port: value.mix_port,
}
}
}
impl From<nym_node_requests::api::v1::node::models::AuxiliaryDetails> for AuxiliaryDetailsV1 {
fn from(value: nym_node_requests::api::v1::node::models::AuxiliaryDetails) -> Self {
AuxiliaryDetailsV1 {
location: value.location,
announce_ports: value.announce_ports.into(),
accepted_operator_terms_and_conditions: value.accepted_operator_terms_and_conditions,
}
}
}
impl From<nym_node_requests::api::v1::node::models::NodeRoles> for DeclaredRolesV1 {
fn from(value: nym_node_requests::api::v1::node::models::NodeRoles) -> Self {
DeclaredRolesV1 {
mixnode: value.mixnode_enabled,
entry: value.gateway_enabled,
exit_nr: value.gateway_enabled && value.network_requester_enabled,
exit_ipr: value.gateway_enabled && value.ip_packet_router_enabled,
}
}
}
impl From<nym_node_requests::api::v1::gateway::models::WebSockets> for WebSocketsV1 {
fn from(value: nym_node_requests::api::v1::gateway::models::WebSockets) -> Self {
WebSocketsV1 {
ws_port: value.ws_port,
wss_port: value.wss_port,
}
}
}
// works for current simple case.
impl From<nym_node_requests::api::v1::ip_packet_router::models::IpPacketRouter>
for IpPacketRouterDetailsV1
{
fn from(value: nym_node_requests::api::v1::ip_packet_router::models::IpPacketRouter) -> Self {
IpPacketRouterDetailsV1 {
address: value.address,
}
}
}
// works for current simple case.
impl From<nym_node_requests::api::v1::authenticator::models::Authenticator>
for AuthenticatorDetailsV1
{
fn from(value: nym_node_requests::api::v1::authenticator::models::Authenticator) -> Self {
AuthenticatorDetailsV1 {
address: value.address,
}
}
}
fn default_tunnel_port() -> u16 {
WG_TUNNEL_PORT
}
fn default_metadata_port() -> u16 {
WG_METADATA_PORT
}
// works for current simple case.
impl From<nym_node_requests::api::v1::gateway::models::Wireguard> for WireguardDetailsV1 {
fn from(value: nym_node_requests::api::v1::gateway::models::Wireguard) -> Self {
WireguardDetailsV1 {
port: value.port,
tunnel_port: value.tunnel_port,
metadata_port: value.metadata_port,
public_key: value.public_key,
}
}
}
impl From<nym_node_requests::api::v1::lewes_protocol::models::LewesProtocol>
for LewesProtocolDetailsV1
{
fn from(value: nym_node_requests::api::v1::lewes_protocol::models::LewesProtocol) -> Self {
LewesProtocolDetailsV1 {
enabled: value.enabled,
control_port: value.control_port,
data_port: value.data_port,
kem_keys: value
.kem_keys
.into_iter()
.map(|(kem, digests)| {
(
kem.into(),
digests
.into_iter()
.map(|(hash_fn, digest)| (hash_fn.into(), digest))
.collect(),
)
})
.collect(),
}
}
}
impl From<nym_node_requests::api::v1::lewes_protocol::models::LPKEM> for LPKEM {
fn from(value: nym_node_requests::api::v1::lewes_protocol::models::LPKEM) -> Self {
match value {
nym_node_requests::api::v1::lewes_protocol::models::LPKEM::MlKem768 => LPKEM::MlKem768,
nym_node_requests::api::v1::lewes_protocol::models::LPKEM::XWing => LPKEM::XWing,
nym_node_requests::api::v1::lewes_protocol::models::LPKEM::X25519 => LPKEM::X25519,
nym_node_requests::api::v1::lewes_protocol::models::LPKEM::McEliece => LPKEM::McEliece,
}
}
}
impl From<nym_node_requests::api::v1::lewes_protocol::models::LPHashFunction> for LPHashFunction {
fn from(value: nym_node_requests::api::v1::lewes_protocol::models::LPHashFunction) -> Self {
match value {
nym_node_requests::api::v1::lewes_protocol::models::LPHashFunction::Blake3 => {
LPHashFunction::Blake3
}
nym_node_requests::api::v1::lewes_protocol::models::LPHashFunction::Shake128 => {
LPHashFunction::Shake128
}
nym_node_requests::api::v1::lewes_protocol::models::LPHashFunction::Shake256 => {
LPHashFunction::Shake256
}
nym_node_requests::api::v1::lewes_protocol::models::LPHashFunction::Sha256 => {
LPHashFunction::Sha256
}
}
}
}
@@ -0,0 +1,198 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::models::{
AuthenticatorDetailsV1, AuxiliaryDetailsV1, BinaryBuildInformationOwned, DeclaredRolesV1,
HostInformationV1, IpPacketRouterDetailsV1, NetworkRequesterDetailsV1,
OffsetDateTimeJsonSchemaWrapper, WebSocketsV1, WireguardDetailsV1,
};
use crate::nym_nodes::{BasicEntryInformation, NodeRole, SemiSkimmedNode, SkimmedNode};
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;
// to whoever is thinking of modifying this struct.
// you MUST NOT change its structure in any way - adding, removing or changing fields
// otherwise, it will break old clients as bincode serialisation is not backwards compatible
// even if you put `#[serde(default)]` all over the place
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub struct NymNodeDescriptionV1 {
#[schema(value_type = u32)]
pub node_id: NodeId,
pub contract_node_type: DescribedNodeTypeV1,
pub description: NymNodeDataV1,
}
impl NymNodeDescriptionV1 {
pub fn version(&self) -> &str {
&self.description.build_information.build_version
}
pub fn entry_information(&self) -> BasicEntryInformation {
BasicEntryInformation {
hostname: self.description.host_information.hostname.clone(),
ws_port: self.description.mixnet_websockets.ws_port,
wss_port: self.description.mixnet_websockets.wss_port,
}
}
pub fn ed25519_identity_key(&self) -> ed25519::PublicKey {
self.description.host_information.keys.ed25519
}
pub fn current_sphinx_key(&self, current_rotation_id: u32) -> x25519::PublicKey {
let keys = &self.description.host_information.keys;
if keys.current_x25519_sphinx_key.rotation_id == u32::MAX {
// legacy case (i.e. node doesn't support rotation)
return keys.current_x25519_sphinx_key.public_key;
}
if current_rotation_id == keys.current_x25519_sphinx_key.rotation_id {
// it's the 'current' key
return keys.current_x25519_sphinx_key.public_key;
}
if let Some(pre_announced) = &keys.pre_announced_x25519_sphinx_key {
if pre_announced.rotation_id == current_rotation_id {
return pre_announced.public_key;
}
}
warn!(
"unexpected key rotation {current_rotation_id} for node {}",
self.node_id
);
// this should never be reached, but just in case, return the fallback option
keys.current_x25519_sphinx_key.public_key
}
pub fn to_skimmed_node(
&self,
current_rotation_id: u32,
role: NodeRole,
performance: Performance,
) -> SkimmedNode {
let keys = &self.description.host_information.keys;
let entry = if self.description.declared_role.entry {
Some(self.entry_information())
} else {
None
};
SkimmedNode {
node_id: self.node_id,
ed25519_identity_pubkey: keys.ed25519,
ip_addresses: self.description.host_information.ip_address.clone(),
mix_port: self.description.mix_port(),
x25519_sphinx_pubkey: self.current_sphinx_key(current_rotation_id),
// we can't use the declared roles, we have to take whatever was provided in the contract.
// why? say this node COULD operate as an exit, but it might be the case the contract decided
// to assign it an ENTRY role only. we have to use that one instead.
role,
supported_roles: self.description.declared_role,
entry,
performance,
}
}
pub fn to_semi_skimmed_node(
&self,
current_rotation_id: u32,
role: NodeRole,
performance: Performance,
) -> SemiSkimmedNode {
let skimmed_node = self.to_skimmed_node(current_rotation_id, role, performance);
SemiSkimmedNode {
basic: skimmed_node,
x25519_noise_versioned_key: self
.description
.host_information
.keys
.x25519_versioned_noise,
}
}
}
// to whoever is thinking of modifying this struct.
// you MUST NOT change its structure in any way - adding, removing or changing fields
// otherwise, it will break old clients as bincode serialisation is not backwards compatible
// even if you put `#[serde(default)]` all over the place
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
#[serde(rename_all = "snake_case")]
#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))]
#[cfg_attr(
feature = "generate-ts",
ts(
export,
export_to = "ts-packages/types/src/types/rust/DescribedNodeType.ts"
)
)]
pub enum DescribedNodeTypeV1 {
LegacyMixnode,
LegacyGateway,
NymNode,
}
impl DescribedNodeTypeV1 {
pub fn is_nym_node(&self) -> bool {
matches!(self, DescribedNodeTypeV1::NymNode)
}
}
// to whoever is thinking of modifying this struct.
// you MUST NOT change its structure in any way - adding, removing or changing fields
// otherwise, it will break old clients as bincode serialisation is not backwards compatible
// even if you put `#[serde(default)]` all over the place
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub struct NymNodeDataV1 {
#[serde(default)]
pub last_polled: OffsetDateTimeJsonSchemaWrapper,
pub host_information: HostInformationV1,
#[serde(default)]
pub declared_role: DeclaredRolesV1,
#[serde(default)]
pub auxiliary_details: AuxiliaryDetailsV1,
// TODO: do we really care about ALL build info or just the version?
pub build_information: BinaryBuildInformationOwned,
#[serde(default)]
pub network_requester: Option<NetworkRequesterDetailsV1>,
#[serde(default)]
pub ip_packet_router: Option<IpPacketRouterDetailsV1>,
#[serde(default)]
pub authenticator: Option<AuthenticatorDetailsV1>,
#[serde(default)]
pub wireguard: Option<WireguardDetailsV1>,
// for now we only care about their ws/wss situation, nothing more
pub mixnet_websockets: WebSocketsV1,
}
impl NymNodeDataV1 {
pub fn mix_port(&self) -> u16 {
self.auxiliary_details
.announce_ports
.mix_port
.unwrap_or(DEFAULT_MIX_LISTENING_PORT)
}
pub fn verloc_port(&self) -> u16 {
self.auxiliary_details
.announce_ports
.verloc_port
.unwrap_or(DEFAULT_VERLOC_LISTENING_PORT)
}
}
@@ -0,0 +1,343 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::models::{
AuthenticatorDetailsV1, AuxiliaryDetailsV1, BinaryBuildInformationOwned, DeclaredRolesV1,
DescribedNodeTypeV1, HostInformationV1, HostKeysV1, IpPacketRouterDetailsV1,
LewesProtocolDetailsV1, NetworkRequesterDetailsV1, NymNodeDataV1, NymNodeDescriptionV1,
OffsetDateTimeJsonSchemaWrapper, SphinxKeyV1, WebSocketsV1, WireguardDetailsV1,
};
use crate::nym_nodes::{BasicEntryInformation, NodeRole, SemiSkimmedNode, SkimmedNode};
use nym_crypto::asymmetric::{ed25519, x25519};
use nym_mixnet_contract_common::reward_params::Performance;
use nym_mixnet_contract_common::NodeId;
use nym_network_defaults::{DEFAULT_MIX_LISTENING_PORT, DEFAULT_VERLOC_LISTENING_PORT};
use nym_noise_keys::VersionedNoiseKeyV1;
use serde::{Deserialize, Serialize};
use tracing::warn;
use utoipa::ToSchema;
// no changes for the following types
pub type HostInformationV2 = HostInformationV1;
pub type DeclaredRolesV2 = DeclaredRolesV1;
pub type AuxiliaryDetailsV2 = AuxiliaryDetailsV1;
pub type NetworkRequesterDetailsV2 = NetworkRequesterDetailsV1;
pub type IpPacketRouterDetailsV2 = IpPacketRouterDetailsV1;
pub type AuthenticatorDetailsV2 = AuthenticatorDetailsV1;
pub type WireguardDetailsV2 = WireguardDetailsV1;
pub type WebSocketsV2 = WebSocketsV1;
pub type DescribedNodeTypeV2 = DescribedNodeTypeV1;
pub type HostKeysV2 = HostKeysV1;
pub type SphinxKeyV2 = SphinxKeyV1;
pub type VersionedNoiseKeyV2 = VersionedNoiseKeyV1;
// to whoever is thinking of modifying this struct.
// you MUST NOT change its structure in any way - adding, removing or changing fields
// otherwise, it will break old clients as bincode serialisation is not backwards compatible
// even if you put `#[serde(default)]` all over the place
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub struct NymNodeDescriptionV2 {
#[schema(value_type = u32)]
pub node_id: NodeId,
pub contract_node_type: DescribedNodeTypeV2,
pub description: NymNodeDataV2,
}
impl NymNodeDescriptionV2 {
pub fn version(&self) -> &str {
&self.description.build_information.build_version
}
pub fn entry_information(&self) -> BasicEntryInformation {
BasicEntryInformation {
hostname: self.description.host_information.hostname.clone(),
ws_port: self.description.mixnet_websockets.ws_port,
wss_port: self.description.mixnet_websockets.wss_port,
}
}
pub fn ed25519_identity_key(&self) -> ed25519::PublicKey {
self.description.host_information.keys.ed25519
}
pub fn current_sphinx_key(&self, current_rotation_id: u32) -> x25519::PublicKey {
let keys = &self.description.host_information.keys;
if keys.current_x25519_sphinx_key.rotation_id == u32::MAX {
// legacy case (i.e. node doesn't support rotation)
return keys.current_x25519_sphinx_key.public_key;
}
if current_rotation_id == keys.current_x25519_sphinx_key.rotation_id {
// it's the 'current' key
return keys.current_x25519_sphinx_key.public_key;
}
if let Some(pre_announced) = &keys.pre_announced_x25519_sphinx_key {
if pre_announced.rotation_id == current_rotation_id {
return pre_announced.public_key;
}
}
warn!(
"unexpected key rotation {current_rotation_id} for node {}",
self.node_id
);
// this should never be reached, but just in case, return the fallback option
keys.current_x25519_sphinx_key.public_key
}
pub fn to_skimmed_node(
&self,
current_rotation_id: u32,
role: NodeRole,
performance: Performance,
) -> SkimmedNode {
let keys = &self.description.host_information.keys;
let entry = if self.description.declared_role.entry {
Some(self.entry_information())
} else {
None
};
SkimmedNode {
node_id: self.node_id,
ed25519_identity_pubkey: keys.ed25519,
ip_addresses: self.description.host_information.ip_address.clone(),
mix_port: self.description.mix_port(),
x25519_sphinx_pubkey: self.current_sphinx_key(current_rotation_id),
// we can't use the declared roles, we have to take whatever was provided in the contract.
// why? say this node COULD operate as an exit, but it might be the case the contract decided
// to assign it an ENTRY role only. we have to use that one instead.
role,
supported_roles: self.description.declared_role,
entry,
performance,
}
}
pub fn to_semi_skimmed_node(
&self,
current_rotation_id: u32,
role: NodeRole,
performance: Performance,
) -> SemiSkimmedNode {
let skimmed_node = self.to_skimmed_node(current_rotation_id, role, performance);
SemiSkimmedNode {
basic: skimmed_node,
x25519_noise_versioned_key: self
.description
.host_information
.keys
.x25519_versioned_noise,
}
}
}
// to whoever is thinking of modifying this struct.
// you MUST NOT change its structure in any way - adding, removing or changing fields
// otherwise, it will break old clients as bincode serialisation is not backwards compatible
// even if you put `#[serde(default)]` all over the place
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub struct NymNodeDataV2 {
#[serde(default)]
pub last_polled: OffsetDateTimeJsonSchemaWrapper,
pub host_information: HostInformationV2,
#[serde(default)]
pub declared_role: DeclaredRolesV2,
#[serde(default)]
pub auxiliary_details: AuxiliaryDetailsV2,
// TODO: do we really care about ALL build info or just the version?
pub build_information: BinaryBuildInformationOwned,
#[serde(default)]
pub network_requester: Option<NetworkRequesterDetailsV2>,
#[serde(default)]
pub ip_packet_router: Option<IpPacketRouterDetailsV2>,
#[serde(default)]
pub authenticator: Option<AuthenticatorDetailsV2>,
#[serde(default)]
pub wireguard: Option<WireguardDetailsV2>,
// for now we only care about their ws/wss situation, nothing more
pub mixnet_websockets: WebSocketsV2,
#[serde(default)]
pub lewes_protocol: Option<LewesProtocolDetailsV1>,
}
impl NymNodeDataV2 {
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<NymNodeDataV2> for NymNodeDataV1 {
fn from(data: NymNodeDataV2) -> Self {
NymNodeDataV1 {
last_polled: data.last_polled,
host_information: data.host_information,
declared_role: data.declared_role,
auxiliary_details: data.auxiliary_details,
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,
}
}
}
impl From<NymNodeDataV1> for NymNodeDataV2 {
fn from(data: NymNodeDataV1) -> Self {
NymNodeDataV2 {
last_polled: data.last_polled,
host_information: data.host_information,
declared_role: data.declared_role,
auxiliary_details: data.auxiliary_details,
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: Default::default(),
}
}
}
impl From<NymNodeDescriptionV2> for NymNodeDescriptionV1 {
fn from(value: NymNodeDescriptionV2) -> Self {
NymNodeDescriptionV1 {
node_id: value.node_id,
contract_node_type: value.contract_node_type,
description: value.description.into(),
}
}
}
impl From<NymNodeDescriptionV1> for NymNodeDescriptionV2 {
fn from(value: NymNodeDescriptionV1) -> Self {
NymNodeDescriptionV2 {
node_id: value.node_id,
contract_node_type: value.contract_node_type,
description: value.description.into(),
}
}
}
#[cfg(test)]
pub fn mock_nym_node_description(seed: u64) -> NymNodeDescriptionV2 {
use crate::models::{LPHashFunction, LPKEM};
use nym_test_utils::helpers::{u64_seeded_rng, RngCore};
let mut rng = u64_seeded_rng(seed);
let ed25519 = 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 mut hashes_wrapper = std::collections::HashMap::new();
let mut hashes = std::collections::HashMap::new();
hashes.insert(LPHashFunction::Sha256, vec![(seed % 256) as u8; 32]);
hashes_wrapper.insert(LPKEM::X25519, hashes);
NymNodeDescriptionV2 {
node_id: rng.next_u32(),
contract_node_type: DescribedNodeTypeV1::NymNode,
description: NymNodeDataV2 {
last_polled: time::OffsetDateTime::from_unix_timestamp(1767225600)
.unwrap()
.into(),
host_information: HostInformationV2 {
ip_address: vec![
std::net::IpAddr::V4(std::net::Ipv4Addr::new(1, 2, 3, (seed % 255) as u8)),
],
hostname: Some(format!("my-awesome-node-{seed}.com")),
keys: HostKeysV2 {
ed25519: *ed25519.public_key(),
x25519: *x25519.public_key(),
current_x25519_sphinx_key: SphinxKeyV2 {
rotation_id: 123,
public_key: *x25519.public_key(),
},
pre_announced_x25519_sphinx_key: None,
x25519_versioned_noise: Some(VersionedNoiseKeyV2 {
supported_version: nym_noise_keys::NoiseVersion::V1,
x25519_pubkey: *x25519.public_key(),
}),
},
},
declared_role: DeclaredRolesV2 {
mixnode: false,
entry: true,
exit_nr: true,
exit_ipr: true,
},
auxiliary_details: AuxiliaryDetailsV2 {
location: Some(celes::Country::switzerland()),
announce_ports: Default::default(),
accepted_operator_terms_and_conditions: true,
},
build_information: BinaryBuildInformationOwned {
binary_name: "dummy-node".to_string(),
build_timestamp: "2021-02-23T20:14:46.558472672+00:00".to_string(),
build_version: "0.1.0-9-g46f83e1".to_string(),
commit_sha: "46f83e112520533338245862d366f6a02cef07d4".to_string(),
commit_timestamp: "2021-02-23T08:08:02-05:00".to_string(),
commit_branch: "master".to_string(),
rustc_version: "1.52.0-nightly".to_string(),
rustc_channel: "nightly".to_string(),
cargo_profile: "release".to_string(),
cargo_triple: "wasm32-unknown-unknown".to_string(),
},
network_requester: Some(NetworkRequesterDetailsV2 {
address: "FhtkzizQg2JbZ19kGkRKXdjV2QnFbT5ww88ZAKaD4nkF.7Remi4UVYzn1yL3qYtEcQBGh6tzTYxMdYB4uqyHVc5Z4@62F81C9GrHDRja9WCqozemRFSzFPMecY85MbGwn6efve".to_string(),
uses_exit_policy: true,
}),
ip_packet_router: Some(IpPacketRouterDetailsV2 {
address: "FhtkzizQg2JbZ19kGkRKXdjV2QnFbT5ww88ZAKaD4nkF.7Remi4UVYzn1yL3qYtEcQBGh6tzTYxMdYB4uqyHVc5Z4@62F81C9GrHDRja9WCqozemRFSzFPMecY85MbGwn6efve".to_string(),
}),
authenticator: Some(AuthenticatorDetailsV2 {
address: "FhtkzizQg2JbZ19kGkRKXdjV2QnFbT5ww88ZAKaD4nkF.7Remi4UVYzn1yL3qYtEcQBGh6tzTYxMdYB4uqyHVc5Z4@62F81C9GrHDRja9WCqozemRFSzFPMecY85MbGwn6efve".to_string(),
}),
wireguard: Some(WireguardDetailsV2 {
port: 123,
tunnel_port: 234,
metadata_port: 456,
public_key: x25519.public_key().to_base58_string(),
}),
lewes_protocol: Some(LewesProtocolDetailsV1 {
enabled: true,
control_port: 1234,
data_port: 2345,
kem_keys: hashes_wrapper,
}),
mixnet_websockets: WebSocketsV2 {
ws_port: 9000,
wss_port: None,
},
},
}
}
+1 -1
View File
@@ -31,7 +31,7 @@ pub use schema_helpers::*;
pub use nym_mixnet_contract_common::{EpochId, KeyRotationId, KeyRotationState};
pub use nym_node_requests::api::v1::node::models::BinaryBuildInformationOwned;
pub use nym_noise_keys::VersionedNoiseKey;
pub use nym_noise_keys::VersionedNoiseKeyV1;
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
pub struct RequestError {
+5 -5
View File
@@ -1,7 +1,7 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::models::{DeclaredRoles, NymNodeData, OffsetDateTimeJsonSchemaWrapper};
use crate::models::{DeclaredRolesV1, NymNodeDataV1, OffsetDateTimeJsonSchemaWrapper};
use crate::pagination::{PaginatedResponse, Pagination};
use nym_crypto::asymmetric::ed25519::serde_helpers::bs58_ed25519_pubkey;
use nym_crypto::asymmetric::x25519::serde_helpers::bs58_x25519_pubkey;
@@ -9,7 +9,7 @@ use nym_crypto::asymmetric::{ed25519, x25519};
use nym_mixnet_contract_common::nym_node::Role;
use nym_mixnet_contract_common::reward_params::Performance;
use nym_mixnet_contract_common::{EpochId, Interval, NodeId};
use nym_noise_keys::VersionedNoiseKey;
use nym_noise_keys::VersionedNoiseKeyV1;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::net::IpAddr;
@@ -254,7 +254,7 @@ pub struct SkimmedNode {
// needed for the purposes of sending appropriate test packets
#[serde(default)]
pub supported_roles: DeclaredRoles,
pub supported_roles: DeclaredRolesV1,
pub entry: Option<BasicEntryInformation>,
@@ -278,7 +278,7 @@ impl SkimmedNode {
pub struct SemiSkimmedNode {
pub basic: SkimmedNode,
pub x25519_noise_versioned_key: Option<VersionedNoiseKey>,
pub x25519_noise_versioned_key: Option<VersionedNoiseKeyV1>,
// pub location:
}
@@ -287,7 +287,7 @@ pub struct FullFatNode {
pub expanded: SemiSkimmedNode,
// kinda temporary for now to make as few changes as possible for now
pub self_described: Option<NymNodeData>,
pub self_described: Option<NymNodeDataV1>,
}
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, ToSchema)]
@@ -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::{NodeAnnotation, NymNodeDescription};
use nym_api_requests::models::{NodeAnnotation, NymNodeDescriptionV2};
use nym_contracts_common::NaiveFloat;
use nym_crypto::asymmetric::{ed25519, x25519};
use nym_mixnet_contract_common::{LegacyMixLayer, NodeId};
@@ -179,7 +179,7 @@ impl PacketPreparer {
rng: &mut R,
current_rotation_id: u32,
node_statuses: &HashMap<NodeId, NodeAnnotation>,
mixing_nym_nodes: impl Iterator<Item = &'a NymNodeDescription> + 'a,
mixing_nym_nodes: impl Iterator<Item = &'a NymNodeDescriptionV2> + 'a,
) -> HashMap<LegacyMixLayer, Vec<(RoutingNode, f64)>> {
let mut layered_mixes = HashMap::new();
@@ -206,7 +206,7 @@ impl PacketPreparer {
&self,
current_rotation_id: u32,
node_statuses: &HashMap<NodeId, NodeAnnotation>,
gateway_capable_nym_nodes: impl Iterator<Item = &'a NymNodeDescription> + 'a,
gateway_capable_nym_nodes: impl Iterator<Item = &'a NymNodeDescriptionV2> + 'a,
) -> Vec<(RoutingNode, f64)> {
let mut gateways = Vec::new();
@@ -361,7 +361,7 @@ impl PacketPreparer {
fn nym_node_to_routing_node(
&self,
current_rotation_id: u32,
description: &NymNodeDescription,
description: &NymNodeDescriptionV2,
) -> Option<RoutingNode> {
description.try_to_topology_node(current_rotation_id).ok()
}
+14 -14
View File
@@ -1,61 +1,61 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use nym_api_requests::models::{DescribedNodeType, NymNodeData, NymNodeDescription};
use nym_api_requests::models::{DescribedNodeTypeV2, NymNodeDataV2, NymNodeDescriptionV2};
use nym_mixnet_contract_common::NodeId;
use std::collections::HashMap;
use std::net::IpAddr;
#[derive(Debug, Clone)]
pub struct DescribedNodes {
pub(crate) nodes: HashMap<NodeId, NymNodeDescription>,
pub(crate) nodes: HashMap<NodeId, NymNodeDescriptionV2>,
pub(crate) addresses_cache: HashMap<IpAddr, NodeId>,
}
impl DescribedNodes {
pub fn force_update(&mut self, node: NymNodeDescription) {
pub fn force_update(&mut self, node: NymNodeDescriptionV2) {
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<&NymNodeData> {
pub fn get_description(&self, node_id: &NodeId) -> Option<&NymNodeDataV2> {
self.nodes.get(node_id).map(|n| &n.description)
}
pub fn get_node(&self, node_id: &NodeId) -> Option<&NymNodeDescription> {
pub fn get_node(&self, node_id: &NodeId) -> Option<&NymNodeDescriptionV2> {
self.nodes.get(node_id)
}
pub fn all_nodes(&self) -> impl Iterator<Item = &NymNodeDescription> {
pub fn all_nodes(&self) -> impl Iterator<Item = &NymNodeDescriptionV2> {
self.nodes.values()
}
pub fn all_nym_nodes(&self) -> impl Iterator<Item = &NymNodeDescription> {
pub fn all_nym_nodes(&self) -> impl Iterator<Item = &NymNodeDescriptionV2> {
self.nodes
.values()
.filter(|n| n.contract_node_type == DescribedNodeType::NymNode)
.filter(|n| n.contract_node_type == DescribedNodeTypeV2::NymNode)
}
pub fn mixing_nym_nodes(&self) -> impl Iterator<Item = &NymNodeDescription> {
pub fn mixing_nym_nodes(&self) -> impl Iterator<Item = &NymNodeDescriptionV2> {
self.nodes
.values()
.filter(|n| n.contract_node_type == DescribedNodeType::NymNode)
.filter(|n| n.contract_node_type == DescribedNodeTypeV2::NymNode)
.filter(|n| n.description.declared_role.mixnode)
}
pub fn entry_capable_nym_nodes(&self) -> impl Iterator<Item = &NymNodeDescription> {
pub fn entry_capable_nym_nodes(&self) -> impl Iterator<Item = &NymNodeDescriptionV2> {
self.nodes
.values()
.filter(|n| n.contract_node_type == DescribedNodeType::NymNode)
.filter(|n| n.contract_node_type == DescribedNodeTypeV2::NymNode)
.filter(|n| n.description.declared_role.entry)
}
pub fn exit_capable_nym_nodes(&self) -> impl Iterator<Item = &NymNodeDescription> {
pub fn exit_capable_nym_nodes(&self) -> impl Iterator<Item = &NymNodeDescriptionV2> {
self.nodes
.values()
.filter(|n| n.contract_node_type == DescribedNodeType::NymNode)
.filter(|n| n.contract_node_type == DescribedNodeTypeV2::NymNode)
.filter(|n| n.description.declared_role.can_operate_exit_gateway())
}
+14 -2
View File
@@ -2,7 +2,7 @@
// SPDX-License-Identifier: GPL-3.0-only
use crate::support::caching::cache::UninitialisedCache;
use nym_api_requests::models::NymNodeDescription;
use nym_api_requests::models::{NymNodeDescriptionV1, NymNodeDescriptionV2};
use nym_config::defaults::DEFAULT_NYM_NODE_HTTP_PORT;
use nym_mixnet_contract_common::NodeId;
use nym_node_requests::api::client::NymNodeApiClientError;
@@ -68,7 +68,19 @@ pub(crate) trait NodeDescriptionTopologyExt {
) -> Result<RoutingNode, RoutingNodeError>;
}
impl NodeDescriptionTopologyExt for NymNodeDescription {
impl NodeDescriptionTopologyExt for NymNodeDescriptionV1 {
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()
}
}
impl NodeDescriptionTopologyExt for NymNodeDescriptionV2 {
fn try_to_topology_node(
&self,
current_rotation_id: u32,
@@ -5,14 +5,14 @@ use crate::node_describe_cache::NodeDescribeCacheError;
use futures::future::{maybe_done, MaybeDone};
use futures::{FutureExt, TryFutureExt};
use nym_api_requests::models::{
AuthenticatorDetails, DeclaredRoles, HostInformation, IpPacketRouterDetails,
LewesProtocolDetails, NetworkRequesterDetails, NymNodeData, WebSockets, WireguardDetails,
AuthenticatorDetailsV2, AuxiliaryDetailsV2, DeclaredRolesV2, HostInformationV2,
IpPacketRouterDetailsV2, LewesProtocolDetailsV1, NetworkRequesterDetailsV2, NymNodeDataV2,
WebSocketsV2, WireguardDetailsV2,
};
use nym_bin_common::build_information::BinaryBuildInformationOwned;
use nym_config::defaults::mainnet;
use nym_mixnet_contract_common::NodeId;
use nym_node_requests::api::client::{NymNodeApiClientError, NymNodeApiClientExt};
use nym_node_requests::api::v1::node::models::AuxiliaryDetails;
use nym_node_requests::api::Client;
use pin_project::pin_project;
use std::future::Future;
@@ -23,14 +23,14 @@ use tracing::debug;
async fn network_requester_future(
client: &Client,
) -> Result<Option<NetworkRequesterDetails>, NymNodeApiClientError> {
) -> Result<Option<NetworkRequesterDetailsV2>, 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(NetworkRequesterDetails {
Some(NetworkRequesterDetailsV2 {
address: nr.address,
uses_exit_policy: exit_policy.enabled && uses_nym_exit_policy,
})
@@ -55,7 +55,8 @@ pub(crate) async fn query_for_described_data(
// old nym-nodes will not have this field, so use the default instead
debug!("could not obtain auxiliary details of node {node_id}: {err} is it running an old version?")
})
.unwrap_or_else(|_| AuxiliaryDetails::default()),
.ok_into()
.unwrap_or_else(|_| AuxiliaryDetailsV2::default()),
client.get_mixnet_websockets().ok_into().map_err(map_query_err),
network_requester_future(client).map_err(map_query_err),
// `ok_into` ultimately calls `IpPacketRouter::into` to transform it into `IpPacketRouterDetails`
@@ -112,14 +113,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<DeclaredRoles, NodeDescribeCacheError>>,
F3: Future<Output = AuxiliaryDetails>,
F4: Future<Output = Result<WebSockets, NodeDescribeCacheError>>,
F5: Future<Output = Result<Option<NetworkRequesterDetails>, NodeDescribeCacheError>>,
F6: Future<Output = Option<IpPacketRouterDetails>>,
F7: Future<Output = Option<AuthenticatorDetails>>,
F8: Future<Output = Option<WireguardDetails>>,
F9: Future<Output = Option<LewesProtocolDetails>>,
F2: Future<Output = Result<DeclaredRolesV2, NodeDescribeCacheError>>,
F3: Future<Output = AuxiliaryDetailsV2>,
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>>,
{
type Output = Result<UnwrappedResolvedNodeDescribedInfo, NodeDescribeCacheError>;
@@ -202,15 +203,15 @@ where
struct ResolvedNodeDescribedInfo {
build_info: Result<BinaryBuildInformationOwned, NodeDescribeCacheError>,
roles: Result<DeclaredRoles, NodeDescribeCacheError>,
roles: Result<DeclaredRolesV2, NodeDescribeCacheError>,
// TODO: in the future make it return a Result as well.
auxiliary_details: AuxiliaryDetails,
websockets: Result<WebSockets, NodeDescribeCacheError>,
network_requester: Result<Option<NetworkRequesterDetails>, NodeDescribeCacheError>,
ipr: Option<IpPacketRouterDetails>,
authenticator: Option<AuthenticatorDetails>,
wireguard: Option<WireguardDetails>,
lewes_protocol: Option<LewesProtocolDetails>,
auxiliary_details: AuxiliaryDetailsV2,
websockets: Result<WebSocketsV2, NodeDescribeCacheError>,
network_requester: Result<Option<NetworkRequesterDetailsV2>, NodeDescribeCacheError>,
ipr: Option<IpPacketRouterDetailsV2>,
authenticator: Option<AuthenticatorDetailsV2>,
wireguard: Option<WireguardDetailsV2>,
lewes_protocol: Option<LewesProtocolDetailsV1>,
}
impl ResolvedNodeDescribedInfo {
@@ -232,22 +233,22 @@ impl ResolvedNodeDescribedInfo {
#[derive(Debug)]
pub(crate) struct UnwrappedResolvedNodeDescribedInfo {
pub(crate) build_info: BinaryBuildInformationOwned,
pub(crate) roles: DeclaredRoles,
pub(crate) auxiliary_details: AuxiliaryDetails,
pub(crate) websockets: WebSockets,
pub(crate) network_requester: Option<NetworkRequesterDetails>,
pub(crate) ipr: Option<IpPacketRouterDetails>,
pub(crate) authenticator: Option<AuthenticatorDetails>,
pub(crate) wireguard: Option<WireguardDetails>,
pub(crate) lewes_protocol: Option<LewesProtocolDetails>,
pub(crate) roles: DeclaredRolesV2,
pub(crate) auxiliary_details: AuxiliaryDetailsV2,
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>,
}
impl UnwrappedResolvedNodeDescribedInfo {
pub(crate) fn into_node_description(
self,
host_info: impl Into<HostInformation>,
) -> NymNodeData {
NymNodeData {
host_info: impl Into<HostInformationV2>,
) -> NymNodeDataV2 {
NymNodeDataV2 {
host_information: host_info.into(),
last_polled: OffsetDateTime::now_utc().into(),
build_information: self.build_info,
+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::{DescribedNodeType, NymNodeDescription};
use nym_api_requests::models::{DescribedNodeTypeV2, NymNodeDescriptionV2};
use nym_bin_common::bin_info;
use nym_config::defaults::DEFAULT_NYM_NODE_HTTP_PORT;
use nym_crypto::asymmetric::ed25519;
@@ -18,7 +18,7 @@ pub(crate) struct RefreshData {
host: String,
node_id: NodeId,
expected_identity: ed25519::PublicKey,
node_type: DescribedNodeType,
node_type: DescribedNodeTypeV2,
port: Option<u16>,
}
@@ -30,7 +30,7 @@ impl<'a> TryFrom<&'a NymNodeDetails> for RefreshData {
Ok(RefreshData::new(
&node.bond_information.node.host,
node.bond_information.identity().parse()?,
DescribedNodeType::NymNode,
DescribedNodeTypeV2::NymNode,
node.node_id(),
node.bond_information.node.custom_http_port,
))
@@ -41,7 +41,7 @@ impl RefreshData {
pub fn new(
host: impl Into<String>,
expected_identity: ed25519::PublicKey,
node_type: DescribedNodeType,
node_type: DescribedNodeTypeV2,
node_id: NodeId,
port: Option<u16>,
) -> Self {
@@ -58,7 +58,7 @@ impl RefreshData {
self.node_id
}
pub(crate) async fn try_refresh(self, allow_all_ips: bool) -> Option<NymNodeDescription> {
pub(crate) async fn try_refresh(self, allow_all_ips: bool) -> Option<NymNodeDescriptionV2> {
match try_get_description(self, allow_all_ips).await {
Ok(description) => Some(description),
Err(err) => {
@@ -124,7 +124,7 @@ async fn try_get_client(
async fn try_get_description(
data: RefreshData,
allow_all_ips: bool,
) -> Result<NymNodeDescription, NodeDescribeCacheError> {
) -> Result<NymNodeDescriptionV2, NodeDescribeCacheError> {
let client = try_get_client(&data.host, data.node_id, data.port).await?;
let map_query_err = |err| NodeDescribeCacheError::ApiFailure {
@@ -158,7 +158,7 @@ async fn try_get_description(
let node_info = query_for_described_data(&client, data.node_id).await?;
let description = node_info.into_node_description(host_info.data);
Ok(NymNodeDescription {
Ok(NymNodeDescriptionV2 {
node_id: data.node_id,
contract_node_type: data.node_type,
description,
+2 -2
View File
@@ -2,7 +2,7 @@
// SPDX-License-Identifier: GPL-3.0-only
use crate::mixnet_contract_cache::cache::data::ConfigScoreData;
use nym_api_requests::models::{ConfigScore, NymNodeDescription};
use nym_api_requests::models::{ConfigScore, NymNodeDescriptionV2};
use nym_contracts_common::NaiveFloat;
use nym_mixnet_contract_common::VersionScoreFormulaParams;
@@ -19,7 +19,7 @@ fn versions_behind_factor_to_config_score(
pub(crate) fn calculate_config_score(
config_score_data: &ConfigScoreData,
described_data: Option<&NymNodeDescription>,
described_data: Option<&NymNodeDescriptionV2>,
) -> ConfigScore {
let Some(described) = described_data else {
return ConfigScore::unavailable();
+2 -492
View File
@@ -1,495 +1,5 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use crate::node_status_api::models::{AxumErrorResponse, AxumResult};
use crate::support::http::helpers::{NodeIdParam, PaginationRequest};
use crate::support::http::state::AppState;
use axum::extract::{Path, Query, State};
use axum::routing::{get, post};
use axum::{Json, Router};
use nym_api_requests::models::{
AnnotationResponse, NodeDatePerformanceResponse, NodePerformanceResponse, NodeRefreshBody,
NoiseDetails, NymNodeDescription, PerformanceHistoryResponse, RewardedSetResponse,
StakeSaturationResponse, UptimeHistoryResponse,
};
use nym_api_requests::pagination::{PaginatedResponse, Pagination};
use nym_contracts_common::NaiveFloat;
use nym_http_api_common::{FormattedResponse, Output, OutputParams};
use nym_mixnet_contract_common::reward_params::Performance;
use nym_mixnet_contract_common::NymNodeDetails;
use serde::{Deserialize, Serialize};
use std::time::Duration;
use time::{Date, OffsetDateTime};
use tower_http::compression::CompressionLayer;
use utoipa::{IntoParams, ToSchema};
pub(crate) fn nym_node_routes() -> Router<AppState> {
Router::new()
.route("/refresh-described", post(refresh_described))
.route("/noise", get(nodes_noise))
.route("/bonded", get(get_bonded_nodes))
.route("/described", get(get_described_nodes))
.route("/annotation/:node_id", get(get_node_annotation))
.route("/performance/:node_id", get(get_current_node_performance))
.route("/stake-saturation/:node_id", get(get_node_stake_saturation))
.route(
"/historical-performance/:node_id",
get(get_historical_performance),
)
.route(
"/performance-history/:node_id",
get(get_node_performance_history),
)
// to make it compatible with all the explorers that were used to using 0-100 values
.route("/uptime-history/:node_id", get(get_node_uptime_history))
.route("/rewarded-set", get(rewarded_set))
.layer(CompressionLayer::new())
}
#[utoipa::path(
tag = "Nym Nodes",
get,
path = "/rewarded-set",
context_path = "/v1/nym-nodes",
responses(
(status = 200, content(
(RewardedSetResponse = "application/json"),
(RewardedSetResponse = "application/yaml"),
(RewardedSetResponse = "application/bincode")
))
),
params(OutputParams)
)]
async fn rewarded_set(
Query(output): Query<OutputParams>,
State(state): State<AppState>,
) -> AxumResult<FormattedResponse<RewardedSetResponse>> {
let output = output.output.unwrap_or_default();
let rewarded_set = state.nym_contract_cache().rewarded_set_owned().await?;
Ok(output.to_response(nym_mixnet_contract_common::EpochRewardedSet::from(rewarded_set).into()))
}
#[utoipa::path(
tag = "Nym Nodes",
post,
request_body = NodeRefreshBody,
path = "/refresh-described",
context_path = "/v1/nym-nodes",
)]
async fn refresh_described(
State(state): State<AppState>,
Json(request_body): Json<NodeRefreshBody>,
) -> AxumResult<Json<()>> {
let Some(refresh_data) = state
.nym_contract_cache()
.get_node_refresh_data(request_body.node_identity)
.await
else {
return Err(AxumErrorResponse::not_found(format!(
"node with identity {} does not seem to exist",
request_body.node_identity
)));
};
if !request_body.verify_signature() {
return Err(AxumErrorResponse::unauthorised("invalid request signature"));
}
if request_body.is_stale() {
return Err(AxumErrorResponse::bad_request("the request is stale"));
}
let node_id = refresh_data.node_id();
if let Some(last) = state.forced_refresh.last_refreshed(node_id).await {
// max 1 refresh a minute
let minute_ago = OffsetDateTime::now_utc() - Duration::from_secs(60);
if last > minute_ago {
return Err(AxumErrorResponse::too_many(
"already refreshed node in the last minute",
));
}
}
// to make sure you can't ddos the endpoint while a request is in progress
state.forced_refresh.set_last_refreshed(node_id).await;
let allow_all_ips = state.forced_refresh.allow_all_ip_addresses;
if let Some(updated_data) = refresh_data.try_refresh(allow_all_ips).await {
let Ok(mut describe_cache) = state.described_nodes_cache.write().await else {
return Err(AxumErrorResponse::service_unavailable());
};
describe_cache.get_mut().force_update(updated_data)
} else {
return Err(AxumErrorResponse::unprocessable_entity(
"failed to refresh node description",
));
}
Ok(Json(()))
}
#[utoipa::path(
tag = "Nym Nodes",
get,
path = "/noise",
context_path = "/v1/nym-nodes",
responses(
(status = 200, content(
(PaginatedResponse<NoiseDetails> = "application/json"),
(PaginatedResponse<NoiseDetails> = "application/yaml"),
(PaginatedResponse<NoiseDetails> = "application/bincode")
))
),
params(PaginationRequest)
)]
async fn nodes_noise(
State(state): State<AppState>,
Query(pagination): Query<PaginationRequest>,
) -> AxumResult<FormattedResponse<PaginatedResponse<NoiseDetails>>> {
// TODO: implement it
let _ = pagination;
let output = pagination.output.unwrap_or_default();
let describe_cache = state.describe_nodes_cache_data().await?;
let nodes = describe_cache
.all_nodes()
.filter_map(|n| {
n.description
.host_information
.keys
.x25519_versioned_noise
.map(|noise_key| (noise_key, n))
})
.map(|(noise_key, node)| NoiseDetails {
key: noise_key,
mixnet_port: node.description.mix_port(),
ip_addresses: node.description.host_information.ip_address.clone(),
})
.collect::<Vec<_>>();
let total = nodes.len();
Ok(output.to_response(PaginatedResponse {
pagination: Pagination {
total,
page: 0,
size: total,
},
data: nodes,
}))
}
#[utoipa::path(
tag = "Nym Nodes",
get,
path = "/bonded",
context_path = "/v1/nym-nodes",
responses(
(status = 200, content(
(PaginatedResponse<NymNodeDetails> = "application/json"),
(PaginatedResponse<NymNodeDetails> = "application/yaml"),
(PaginatedResponse<NymNodeDetails> = "application/bincode")
))
),
params(PaginationRequest)
)]
async fn get_bonded_nodes(
State(state): State<AppState>,
Query(pagination): Query<PaginationRequest>,
) -> FormattedResponse<PaginatedResponse<NymNodeDetails>> {
// TODO: implement it
let _ = pagination;
let output = pagination.output.unwrap_or_default();
let details = state.nym_contract_cache().nym_nodes().await;
let total = details.len();
output.to_response(PaginatedResponse {
pagination: Pagination {
total,
page: 0,
size: total,
},
data: details,
})
}
#[utoipa::path(
tag = "Nym Nodes",
get,
path = "/described",
context_path = "/v1/nym-nodes",
responses(
(status = 200, content(
(PaginatedResponse<NymNodeDescription> = "application/json"),
(PaginatedResponse<NymNodeDescription> = "application/yaml"),
(PaginatedResponse<NymNodeDescription> = "application/bincode")
))
),
params(PaginationRequest)
)]
async fn get_described_nodes(
State(state): State<AppState>,
Query(pagination): Query<PaginationRequest>,
) -> AxumResult<FormattedResponse<PaginatedResponse<NymNodeDescription>>> {
// TODO: implement it
let _ = pagination;
let output = pagination.output.unwrap_or_default();
let cache = state.described_nodes_cache.get().await?;
let descriptions = cache.all_nodes().cloned().collect::<Vec<_>>();
Ok(output.to_response(PaginatedResponse {
pagination: Pagination {
total: descriptions.len(),
page: 0,
size: descriptions.len(),
},
data: descriptions,
}))
}
#[utoipa::path(
tag = "Nym Nodes",
get,
path = "/annotation/{node_id}",
context_path = "/v1/nym-nodes",
responses(
(status = 200, content(
(AnnotationResponse = "application/json"),
(AnnotationResponse = "application/yaml"),
(AnnotationResponse = "application/bincode")
))
),
params(NodeIdParam, OutputParams),
)]
async fn get_node_annotation(
Path(NodeIdParam { node_id }): Path<NodeIdParam>,
Query(output): Query<OutputParams>,
State(state): State<AppState>,
) -> AxumResult<FormattedResponse<AnnotationResponse>> {
let output = output.output.unwrap_or_default();
let annotations = state
.node_status_cache
.node_annotations()
.await
.ok_or_else(AxumErrorResponse::internal)?;
Ok(output.to_response(AnnotationResponse {
node_id,
annotation: annotations.get(&node_id).cloned(),
}))
}
#[utoipa::path(
tag = "Nym Nodes",
get,
path = "/performance/{node_id}",
context_path = "/v1/nym-nodes",
responses(
(status = 200, content(
(NodePerformanceResponse = "application/json"),
(NodePerformanceResponse = "application/yaml"),
(NodePerformanceResponse = "application/bincode")
))
),
params(NodeIdParam, OutputParams),
)]
async fn get_current_node_performance(
Path(NodeIdParam { node_id }): Path<NodeIdParam>,
Query(output): Query<OutputParams>,
State(state): State<AppState>,
) -> AxumResult<FormattedResponse<NodePerformanceResponse>> {
let output = output.output.unwrap_or_default();
let annotations = state
.node_status_cache
.node_annotations()
.await
.ok_or_else(AxumErrorResponse::internal)?;
Ok(output.to_response(NodePerformanceResponse {
node_id,
performance: annotations
.get(&node_id)
.map(|n| n.last_24h_performance.naive_to_f64()),
}))
}
#[utoipa::path(
tag = "Nym Nodes",
get,
path = "/stake-saturation/{node_id}",
context_path = "/v1/nym-nodes",
responses(
(status = 200, content(
(StakeSaturationResponse = "application/json"),
(StakeSaturationResponse = "application/yaml"),
(StakeSaturationResponse = "application/bincode")
))
),
params(NodeIdParam, OutputParams),
)]
async fn get_node_stake_saturation(
Path(NodeIdParam { node_id }): Path<NodeIdParam>,
Query(output): Query<OutputParams>,
State(state): State<AppState>,
) -> AxumResult<FormattedResponse<StakeSaturationResponse>> {
let output = output.get_output();
let contract_cache = state.nym_contract_cache();
let Some(node) = contract_cache.nym_node(node_id).await? else {
return Err(AxumErrorResponse::not_found("nym node bond not found"));
};
let rewarding_params = contract_cache.interval_reward_params().await?;
let as_at = contract_cache.cache_timestamp().await;
Ok(output.to_response(StakeSaturationResponse {
saturation: node.rewarding_details.bond_saturation(&rewarding_params),
uncapped_saturation: node
.rewarding_details
.uncapped_bond_saturation(&rewarding_params),
as_at: as_at.unix_timestamp(),
}))
}
// todo; probably extract it to requests crate
#[derive(Debug, Serialize, Deserialize, Copy, Clone, IntoParams, ToSchema)]
#[into_params(parameter_in = Query)]
pub(crate) struct DateQuery {
#[schema(value_type = String, example = "1970-01-01")]
pub(crate) date: Date,
pub(crate) output: Option<Output>,
}
#[utoipa::path(
tag = "Nym Nodes",
get,
path = "/historical-performance/{node_id}",
context_path = "/v1/nym-nodes",
responses(
(status = 200, content(
(NodeDatePerformanceResponse = "application/json"),
(NodeDatePerformanceResponse = "application/yaml"),
(NodeDatePerformanceResponse = "application/bincode")
))
),
params(DateQuery, NodeIdParam)
)]
async fn get_historical_performance(
Path(NodeIdParam { node_id }): Path<NodeIdParam>,
Query(DateQuery { date, output }): Query<DateQuery>,
State(state): State<AppState>,
) -> AxumResult<FormattedResponse<NodeDatePerformanceResponse>> {
let output = output.unwrap_or_default();
let uptime = state
.storage()
.get_historical_node_uptime_on(node_id, date)
.await?;
Ok(output.to_response(NodeDatePerformanceResponse {
node_id,
date,
performance: uptime.and_then(|u| {
Performance::from_percentage_value(u.uptime as u64)
.map(|p| p.naive_to_f64())
.ok()
}),
}))
}
#[utoipa::path(
tag = "Nym Nodes",
get,
path = "/performance-history/{node_id}",
context_path = "/v1/nym-nodes",
responses(
(status = 200, content(
(PerformanceHistoryResponse = "application/json"),
(PerformanceHistoryResponse = "application/yaml"),
(PerformanceHistoryResponse = "application/bincode")
))
),
params(PaginationRequest, NodeIdParam)
)]
async fn get_node_performance_history(
Path(NodeIdParam { node_id }): Path<NodeIdParam>,
State(state): State<AppState>,
Query(pagination): Query<PaginationRequest>,
) -> AxumResult<FormattedResponse<PerformanceHistoryResponse>> {
// TODO: implement it
let _ = pagination;
let output = pagination.output.unwrap_or_default();
let history = state
.storage()
.get_node_uptime_history(node_id)
.await?
.into_iter()
.filter_map(|u| u.try_into().ok())
.collect::<Vec<_>>();
let total = history.len();
Ok(output.to_response(PerformanceHistoryResponse {
node_id,
history: PaginatedResponse {
pagination: Pagination {
total,
page: 0,
size: total,
},
data: history,
},
}))
}
#[utoipa::path(
tag = "Nym Nodes",
get,
path = "/uptime-history/{node_id}",
context_path = "/v1/nym-nodes",
responses(
(status = 200, content(
(PerformanceHistoryResponse = "application/json"),
(PerformanceHistoryResponse = "application/yaml"),
(PerformanceHistoryResponse = "application/bincode")
))
),
params(PaginationRequest, NodeIdParam)
)]
async fn get_node_uptime_history(
Path(NodeIdParam { node_id }): Path<NodeIdParam>,
State(state): State<AppState>,
Query(pagination): Query<PaginationRequest>,
) -> AxumResult<FormattedResponse<UptimeHistoryResponse>> {
// TODO: implement it
let _ = pagination;
let output = pagination.output.unwrap_or_default();
let history = state
.storage()
.get_node_uptime_history(node_id)
.await?
.into_iter()
.filter_map(|u| u.try_into().ok())
.collect::<Vec<_>>();
let total = history.len();
Ok(output.to_response(UptimeHistoryResponse {
node_id,
history: PaginatedResponse {
pagination: Pagination {
total,
page: 0,
size: total,
},
data: history,
},
}))
}
pub mod v1;
pub mod v2;
+502
View File
@@ -0,0 +1,502 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use crate::node_status_api::models::{AxumErrorResponse, AxumResult};
use crate::support::http::helpers::{NodeIdParam, PaginationRequest};
use crate::support::http::state::AppState;
use axum::extract::{Path, Query, State};
use axum::routing::{get, post};
use axum::{Json, Router};
use nym_api_requests::models::{
AnnotationResponse, NodeDatePerformanceResponse, NodePerformanceResponse, NodeRefreshBody,
NoiseDetails, NymNodeDescriptionV1, PerformanceHistoryResponse, RewardedSetResponse,
StakeSaturationResponse, UptimeHistoryResponse,
};
use nym_api_requests::pagination::{PaginatedResponse, Pagination};
use nym_contracts_common::NaiveFloat;
use nym_http_api_common::{FormattedResponse, Output, OutputParams};
use nym_mixnet_contract_common::reward_params::Performance;
use nym_mixnet_contract_common::NymNodeDetails;
use serde::{Deserialize, Serialize};
use std::time::Duration;
use time::{Date, OffsetDateTime};
use tower_http::compression::CompressionLayer;
use utoipa::{IntoParams, ToSchema};
#[allow(deprecated)]
pub(crate) fn routes() -> Router<AppState> {
Router::new()
.route("/refresh-described", post(refresh_described))
.route("/noise", get(nodes_noise))
.route("/bonded", get(get_bonded_nodes))
.route("/described", get(get_described_nodes))
.route("/annotation/:node_id", get(get_node_annotation))
.route("/performance/:node_id", get(get_current_node_performance))
.route("/stake-saturation/:node_id", get(get_node_stake_saturation))
.route(
"/historical-performance/:node_id",
get(get_historical_performance),
)
.route(
"/performance-history/:node_id",
get(get_node_performance_history),
)
// to make it compatible with all the explorers that were used to using 0-100 values
.route("/uptime-history/:node_id", get(get_node_uptime_history))
.route("/rewarded-set", get(rewarded_set))
.layer(CompressionLayer::new())
}
#[utoipa::path(
tag = "Nym Nodes",
get,
path = "/rewarded-set",
context_path = "/v1/nym-nodes",
responses(
(status = 200, content(
(RewardedSetResponse = "application/json"),
(RewardedSetResponse = "application/yaml"),
(RewardedSetResponse = "application/bincode")
))
),
params(OutputParams)
)]
async fn rewarded_set(
Query(output): Query<OutputParams>,
State(state): State<AppState>,
) -> AxumResult<FormattedResponse<RewardedSetResponse>> {
let output = output.output.unwrap_or_default();
let rewarded_set = state.nym_contract_cache().rewarded_set_owned().await?;
Ok(output.to_response(nym_mixnet_contract_common::EpochRewardedSet::from(rewarded_set).into()))
}
#[utoipa::path(
tag = "Nym Nodes",
post,
request_body = NodeRefreshBody,
path = "/refresh-described",
context_path = "/v1/nym-nodes",
)]
async fn refresh_described(
State(state): State<AppState>,
Json(request_body): Json<NodeRefreshBody>,
) -> AxumResult<Json<()>> {
let Some(refresh_data) = state
.nym_contract_cache()
.get_node_refresh_data(request_body.node_identity)
.await
else {
return Err(AxumErrorResponse::not_found(format!(
"node with identity {} does not seem to exist",
request_body.node_identity
)));
};
if !request_body.verify_signature() {
return Err(AxumErrorResponse::unauthorised("invalid request signature"));
}
if request_body.is_stale() {
return Err(AxumErrorResponse::bad_request("the request is stale"));
}
let node_id = refresh_data.node_id();
if let Some(last) = state.forced_refresh.last_refreshed(node_id).await {
// max 1 refresh a minute
let minute_ago = OffsetDateTime::now_utc() - Duration::from_secs(60);
if last > minute_ago {
return Err(AxumErrorResponse::too_many(
"already refreshed node in the last minute",
));
}
}
// to make sure you can't ddos the endpoint while a request is in progress
state.forced_refresh.set_last_refreshed(node_id).await;
let allow_all_ips = state.forced_refresh.allow_all_ip_addresses;
if let Some(updated_data) = refresh_data.try_refresh(allow_all_ips).await {
let Ok(mut describe_cache) = state.described_nodes_cache.write().await else {
return Err(AxumErrorResponse::service_unavailable());
};
describe_cache.get_mut().force_update(updated_data)
} else {
return Err(AxumErrorResponse::unprocessable_entity(
"failed to refresh node description",
));
}
Ok(Json(()))
}
#[utoipa::path(
tag = "Nym Nodes",
get,
path = "/noise",
context_path = "/v1/nym-nodes",
responses(
(status = 200, content(
(PaginatedResponse<NoiseDetails> = "application/json"),
(PaginatedResponse<NoiseDetails> = "application/yaml"),
(PaginatedResponse<NoiseDetails> = "application/bincode")
))
),
params(PaginationRequest)
)]
async fn nodes_noise(
State(state): State<AppState>,
Query(pagination): Query<PaginationRequest>,
) -> AxumResult<FormattedResponse<PaginatedResponse<NoiseDetails>>> {
// TODO: implement it
let _ = pagination;
let output = pagination.output.unwrap_or_default();
let describe_cache = state.describe_nodes_cache_data().await?;
let nodes = describe_cache
.all_nodes()
.filter_map(|n| {
n.description
.host_information
.keys
.x25519_versioned_noise
.map(|noise_key| (noise_key, n))
})
.map(|(noise_key, node)| NoiseDetails {
key: noise_key,
mixnet_port: node.description.mix_port(),
ip_addresses: node.description.host_information.ip_address.clone(),
})
.collect::<Vec<_>>();
let total = nodes.len();
Ok(output.to_response(PaginatedResponse {
pagination: Pagination {
total,
page: 0,
size: total,
},
data: nodes,
}))
}
#[utoipa::path(
tag = "Nym Nodes",
get,
path = "/bonded",
context_path = "/v1/nym-nodes",
responses(
(status = 200, content(
(PaginatedResponse<NymNodeDetails> = "application/json"),
(PaginatedResponse<NymNodeDetails> = "application/yaml"),
(PaginatedResponse<NymNodeDetails> = "application/bincode")
))
),
params(PaginationRequest)
)]
async fn get_bonded_nodes(
State(state): State<AppState>,
Query(pagination): Query<PaginationRequest>,
) -> FormattedResponse<PaginatedResponse<NymNodeDetails>> {
// TODO: implement it
let _ = pagination;
let output = pagination.output.unwrap_or_default();
let details = state.nym_contract_cache().nym_nodes().await;
let total = details.len();
output.to_response(PaginatedResponse {
pagination: Pagination {
total,
page: 0,
size: total,
},
data: details,
})
}
#[utoipa::path(
tag = "Nym Nodes",
get,
path = "/described",
context_path = "/v1/nym-nodes",
responses(
(status = 200, content(
(PaginatedResponse<NymNodeDescriptionV1> = "application/json"),
(PaginatedResponse<NymNodeDescriptionV1> = "application/yaml"),
(PaginatedResponse<NymNodeDescriptionV1> = "application/bincode")
))
),
params(PaginationRequest)
)]
#[deprecated(note = "use '/v2/nym-nodes/described' instead")]
async fn get_described_nodes(
State(state): State<AppState>,
Query(pagination): Query<PaginationRequest>,
) -> AxumResult<FormattedResponse<PaginatedResponse<NymNodeDescriptionV1>>> {
// TODO: implement it
let _ = pagination;
let output = pagination.output.unwrap_or_default();
let cache = state.described_nodes_cache.get().await?;
// convert description to V1
let descriptions = cache
.all_nodes()
.map(|d| d.clone().into())
.collect::<Vec<_>>();
Ok(output.to_response(PaginatedResponse {
pagination: Pagination {
total: descriptions.len(),
page: 0,
size: descriptions.len(),
},
data: descriptions,
}))
}
#[utoipa::path(
tag = "Nym Nodes",
get,
path = "/annotation/{node_id}",
context_path = "/v1/nym-nodes",
responses(
(status = 200, content(
(AnnotationResponse = "application/json"),
(AnnotationResponse = "application/yaml"),
(AnnotationResponse = "application/bincode")
))
),
params(NodeIdParam, OutputParams),
)]
async fn get_node_annotation(
Path(NodeIdParam { node_id }): Path<NodeIdParam>,
Query(output): Query<OutputParams>,
State(state): State<AppState>,
) -> AxumResult<FormattedResponse<AnnotationResponse>> {
let output = output.output.unwrap_or_default();
let annotations = state
.node_status_cache
.node_annotations()
.await
.ok_or_else(AxumErrorResponse::internal)?;
Ok(output.to_response(AnnotationResponse {
node_id,
annotation: annotations.get(&node_id).cloned(),
}))
}
#[utoipa::path(
tag = "Nym Nodes",
get,
path = "/performance/{node_id}",
context_path = "/v1/nym-nodes",
responses(
(status = 200, content(
(NodePerformanceResponse = "application/json"),
(NodePerformanceResponse = "application/yaml"),
(NodePerformanceResponse = "application/bincode")
))
),
params(NodeIdParam, OutputParams),
)]
async fn get_current_node_performance(
Path(NodeIdParam { node_id }): Path<NodeIdParam>,
Query(output): Query<OutputParams>,
State(state): State<AppState>,
) -> AxumResult<FormattedResponse<NodePerformanceResponse>> {
let output = output.output.unwrap_or_default();
let annotations = state
.node_status_cache
.node_annotations()
.await
.ok_or_else(AxumErrorResponse::internal)?;
Ok(output.to_response(NodePerformanceResponse {
node_id,
performance: annotations
.get(&node_id)
.map(|n| n.last_24h_performance.naive_to_f64()),
}))
}
#[utoipa::path(
tag = "Nym Nodes",
get,
path = "/stake-saturation/{node_id}",
context_path = "/v1/nym-nodes",
responses(
(status = 200, content(
(StakeSaturationResponse = "application/json"),
(StakeSaturationResponse = "application/yaml"),
(StakeSaturationResponse = "application/bincode")
))
),
params(NodeIdParam, OutputParams),
)]
async fn get_node_stake_saturation(
Path(NodeIdParam { node_id }): Path<NodeIdParam>,
Query(output): Query<OutputParams>,
State(state): State<AppState>,
) -> AxumResult<FormattedResponse<StakeSaturationResponse>> {
let output = output.get_output();
let contract_cache = state.nym_contract_cache();
let Some(node) = contract_cache.nym_node(node_id).await? else {
return Err(AxumErrorResponse::not_found("nym node bond not found"));
};
let rewarding_params = contract_cache.interval_reward_params().await?;
let as_at = contract_cache.cache_timestamp().await;
Ok(output.to_response(StakeSaturationResponse {
saturation: node.rewarding_details.bond_saturation(&rewarding_params),
uncapped_saturation: node
.rewarding_details
.uncapped_bond_saturation(&rewarding_params),
as_at: as_at.unix_timestamp(),
}))
}
// todo; probably extract it to requests crate
#[derive(Debug, Serialize, Deserialize, Copy, Clone, IntoParams, ToSchema)]
#[into_params(parameter_in = Query)]
pub(crate) struct DateQuery {
#[schema(value_type = String, example = "1970-01-01")]
pub(crate) date: Date,
pub(crate) output: Option<Output>,
}
#[utoipa::path(
tag = "Nym Nodes",
get,
path = "/historical-performance/{node_id}",
context_path = "/v1/nym-nodes",
responses(
(status = 200, content(
(NodeDatePerformanceResponse = "application/json"),
(NodeDatePerformanceResponse = "application/yaml"),
(NodeDatePerformanceResponse = "application/bincode")
))
),
params(DateQuery, NodeIdParam)
)]
async fn get_historical_performance(
Path(NodeIdParam { node_id }): Path<NodeIdParam>,
Query(DateQuery { date, output }): Query<DateQuery>,
State(state): State<AppState>,
) -> AxumResult<FormattedResponse<NodeDatePerformanceResponse>> {
let output = output.unwrap_or_default();
let uptime = state
.storage()
.get_historical_node_uptime_on(node_id, date)
.await?;
Ok(output.to_response(NodeDatePerformanceResponse {
node_id,
date,
performance: uptime.and_then(|u| {
Performance::from_percentage_value(u.uptime as u64)
.map(|p| p.naive_to_f64())
.ok()
}),
}))
}
#[utoipa::path(
tag = "Nym Nodes",
get,
path = "/performance-history/{node_id}",
context_path = "/v1/nym-nodes",
responses(
(status = 200, content(
(PerformanceHistoryResponse = "application/json"),
(PerformanceHistoryResponse = "application/yaml"),
(PerformanceHistoryResponse = "application/bincode")
))
),
params(PaginationRequest, NodeIdParam)
)]
async fn get_node_performance_history(
Path(NodeIdParam { node_id }): Path<NodeIdParam>,
State(state): State<AppState>,
Query(pagination): Query<PaginationRequest>,
) -> AxumResult<FormattedResponse<PerformanceHistoryResponse>> {
// TODO: implement it
let _ = pagination;
let output = pagination.output.unwrap_or_default();
let history = state
.storage()
.get_node_uptime_history(node_id)
.await?
.into_iter()
.filter_map(|u| u.try_into().ok())
.collect::<Vec<_>>();
let total = history.len();
Ok(output.to_response(PerformanceHistoryResponse {
node_id,
history: PaginatedResponse {
pagination: Pagination {
total,
page: 0,
size: total,
},
data: history,
},
}))
}
#[utoipa::path(
tag = "Nym Nodes",
get,
path = "/uptime-history/{node_id}",
context_path = "/v1/nym-nodes",
responses(
(status = 200, content(
(PerformanceHistoryResponse = "application/json"),
(PerformanceHistoryResponse = "application/yaml"),
(PerformanceHistoryResponse = "application/bincode")
))
),
params(PaginationRequest, NodeIdParam)
)]
async fn get_node_uptime_history(
Path(NodeIdParam { node_id }): Path<NodeIdParam>,
State(state): State<AppState>,
Query(pagination): Query<PaginationRequest>,
) -> AxumResult<FormattedResponse<UptimeHistoryResponse>> {
// TODO: implement it
let _ = pagination;
let output = pagination.output.unwrap_or_default();
let history = state
.storage()
.get_node_uptime_history(node_id)
.await?
.into_iter()
.filter_map(|u| u.try_into().ok())
.collect::<Vec<_>>();
let total = history.len();
Ok(output.to_response(UptimeHistoryResponse {
node_id,
history: PaginatedResponse {
pagination: Pagination {
total,
page: 0,
size: total,
},
data: history,
},
}))
}
+54
View File
@@ -0,0 +1,54 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use crate::node_status_api::models::AxumResult;
use crate::support::http::helpers::PaginationRequest;
use crate::support::http::state::AppState;
use axum::extract::{Query, State};
use axum::routing::get;
use axum::Router;
use nym_api_requests::models::NymNodeDescriptionV2;
use nym_api_requests::pagination::{PaginatedResponse, Pagination};
use nym_http_api_common::FormattedResponse;
use tower_http::compression::CompressionLayer;
pub(crate) fn routes() -> Router<AppState> {
Router::new()
.route("/described", get(get_described_nodes))
.layer(CompressionLayer::new())
}
#[utoipa::path(
tag = "Nym Nodes",
get,
path = "/described",
context_path = "/v2/nym-nodes",
responses(
(status = 200, content(
(PaginatedResponse<NymNodeDescriptionV2> = "application/json"),
(PaginatedResponse<NymNodeDescriptionV2> = "application/yaml"),
(PaginatedResponse<NymNodeDescriptionV2> = "application/bincode")
))
),
params(PaginationRequest)
)]
async fn get_described_nodes(
State(state): State<AppState>,
Query(pagination): Query<PaginationRequest>,
) -> AxumResult<FormattedResponse<PaginatedResponse<NymNodeDescriptionV2>>> {
// TODO: implement it
let _ = pagination;
let output = pagination.output.unwrap_or_default();
let cache = state.described_nodes_cache.get().await?;
let descriptions = cache.all_nodes().cloned().collect::<Vec<_>>();
Ok(output.to_response(PaginatedResponse {
pagination: Pagination {
total: descriptions.len(),
page: 0,
size: descriptions.len(),
},
data: descriptions,
}))
}
+8 -4
View File
@@ -6,12 +6,11 @@ use crate::ecash::api_routes::handlers::ecash_routes;
use crate::mixnet_contract_cache::handlers::{epoch_routes, legacy_nodes_routes};
use crate::network::handlers::nym_network_routes;
use crate::node_status_api::handlers::status_routes;
use crate::nym_nodes::handlers::nym_node_routes;
use crate::status;
use crate::support::http::openapi::ApiDoc;
use crate::support::http::state::AppState;
use crate::unstable_routes::v1::unstable_routes_v1;
use crate::unstable_routes::v2::unstable_routes_v2;
use crate::{nym_nodes, status};
use anyhow::anyhow;
use axum::response::Redirect;
use axum::routing::get;
@@ -61,12 +60,17 @@ impl RouterBuilder {
.nest("/status", status_routes(network_monitor))
.nest("/network", nym_network_routes())
.nest("/api-status", status::handlers::api_status_routes())
.nest("/nym-nodes", nym_node_routes())
.nest("/nym-nodes", nym_nodes::handlers::v1::routes())
.nest("/ecash", ecash_routes())
.nest("/unstable", unstable_routes_v1())
.nest("/legacy", legacy_nodes_routes()), // CORS layer needs to be "outside" of routes
)
.nest("/v2", Router::new().nest("/unstable", unstable_routes_v2()));
.nest(
"/v2",
Router::new()
.nest("/unstable", unstable_routes_v2())
.nest("/nym-nodes", nym_nodes::handlers::v2::routes()),
);
Self {
unfinished_router: default_routes,
@@ -7,7 +7,7 @@ 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::{
NodeAnnotation, NymNodeDescription, OffsetDateTimeJsonSchemaWrapper,
NodeAnnotation, NymNodeDescriptionV2, OffsetDateTimeJsonSchemaWrapper,
};
use nym_api_requests::nym_nodes::{NodeRole, PaginatedCachedNodesResponseV2, SemiSkimmedNode};
use nym_api_requests::pagination::PaginatedResponse;
@@ -29,7 +29,7 @@ fn build_nym_nodes_response<'a, NI>(
active_only: bool,
) -> Vec<SemiSkimmedNode>
where
NI: Iterator<Item = &'a NymNodeDescription> + 'a,
NI: Iterator<Item = &'a NymNodeDescriptionV2> + 'a,
{
let mut nodes = Vec::new();
for nym_node in nym_nodes_subset {
@@ -7,7 +7,7 @@ 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::{
NodeAnnotation, NymNodeDescription, OffsetDateTimeJsonSchemaWrapper,
NodeAnnotation, NymNodeDescriptionV2, OffsetDateTimeJsonSchemaWrapper,
};
use nym_api_requests::nym_nodes::{NodeRole, PaginatedCachedNodesResponseV2, SkimmedNode};
use nym_http_api_common::Output;
@@ -25,7 +25,7 @@ fn build_nym_nodes_response<'a, NI>(
active_only: bool,
) -> Vec<SkimmedNode>
where
NI: Iterator<Item = &'a NymNodeDescription> + 'a,
NI: Iterator<Item = &'a NymNodeDescriptionV2> + 'a,
{
let mut nodes = Vec::new();
for nym_node in nym_nodes_subset {
@@ -91,7 +91,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 NymNodeDescription> + 'a,
NI: Iterator<Item = &'a NymNodeDescriptionV2> + 'a,
{
// TODO: implement it
let _ = query_params.per_page;