From c10d97372ffa32e3b6aa0d10d5f41b9e90f95a37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Wed, 25 Feb 2026 16:06:07 +0000 Subject: [PATCH] re-expose LP information on the http API --- Cargo.lock | 3 +- .../validator-client/src/client.rs | 62 ++++++------ .../validator-client/src/nym_api/mod.rs | 96 +++++++++---------- common/nym-kkt-ciphersuite/Cargo.toml | 1 + common/nym-kkt-ciphersuite/src/lib.rs | 13 ++- common/nym-kkt-context/Cargo.toml | 1 + common/nym-kkt/src/key_utils.rs | 8 +- common/nym-kkt/src/keys.rs | 16 +++- common/nym-lp-packet/Cargo.toml | 1 + common/nym-lp/src/peer.rs | 22 ++--- .../src/models/described/type_translation.rs | 74 +++++++++----- nym-api/src/nym_nodes/handlers/v1.rs | 2 +- nym-api/src/nym_nodes/handlers/v2.rs | 32 +++---- nym-node/Cargo.toml | 1 - nym-node/nym-node-requests/Cargo.toml | 2 +- nym-node/nym-node-requests/src/api/client.rs | 3 +- nym-node/nym-node-requests/src/api/mod.rs | 32 ++++--- .../src/api/v1/lewes_protocol/models.rs | 8 +- .../http/router/api/v1/lewes_protocol/mod.rs | 6 +- .../http/router/api/v1/lewes_protocol/root.rs | 17 ++-- .../router/api/v1/node/host_information.rs | 3 +- nym-node/src/node/http/router/mod.rs | 8 +- nym-node/src/node/mod.rs | 79 ++++++++------- 23 files changed, 268 insertions(+), 222 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7825a56079..cd3bf09db8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7226,7 +7226,6 @@ dependencies = [ "opentelemetry_sdk", "rand 0.8.5", "rand 0.9.2", - "rand_chacha 0.3.1", "serde", "serde_json", "sha2 0.10.9", @@ -7274,9 +7273,9 @@ dependencies = [ "nym-http-api-client", "nym-kkt-ciphersuite", "nym-noise-keys", + "nym-test-utils", "nym-upgrade-mode-check", "nym-wireguard-types", - "rand_chacha 0.3.1", "schemars 0.8.22", "serde", "serde_json", diff --git a/common/client-libs/validator-client/src/client.rs b/common/client-libs/validator-client/src/client.rs index d461157f52..43ab5808a7 100644 --- a/common/client-libs/validator-client/src/client.rs +++ b/common/client-libs/validator-client/src/client.rs @@ -20,7 +20,7 @@ use nym_api_requests::ecash::{ }; use nym_api_requests::models::{ ApiHealthResponse, GatewayCoreStatusResponse, HistoricalPerformanceResponse, - MixnodeCoreStatusResponse, NymNodeDescriptionV1, + MixnodeCoreStatusResponse, NymNodeDescriptionV1, NymNodeDescriptionV2, }; use nym_api_requests::nym_nodes::{ NodesByAddressesResponse, SemiSkimmedNodesWithMetadata, SkimmedNode, SkimmedNodesWithMetadata, @@ -273,18 +273,18 @@ impl Client { Ok(history) } - // #[deprecated(note = "use get_all_cached_described_nodes_v2 instead")] + #[deprecated(note = "use get_all_cached_described_nodes_v2 instead")] pub async fn get_all_cached_described_nodes( &self, ) -> Result, ValidatorClientError> { Ok(self.nym_api.get_all_described_nodes().await?) } - // pub async fn get_all_cached_described_nodes_v2( - // &self, - // ) -> Result, ValidatorClientError> { - // Ok(self.nym_api.get_all_described_nodes_v2().await?) - // } + pub async fn get_all_cached_described_nodes_v2( + &self, + ) -> Result, ValidatorClientError> { + Ok(self.nym_api.get_all_described_nodes_v2().await?) + } pub async fn get_all_cached_bonded_nym_nodes( &self, @@ -473,7 +473,7 @@ impl NymApiClient { Ok(self.nym_api.health().await?) } - // #[deprecated(note = "use .get_all_described_nodes_v2 instead")] + #[deprecated(note = "use .get_all_described_nodes_v2 instead")] pub async fn get_all_described_nodes( &self, ) -> Result, ValidatorClientError> { @@ -495,29 +495,29 @@ impl NymApiClient { Ok(descriptions) } - // pub async fn get_all_described_nodes_v2( - // &self, - // ) -> Result, ValidatorClientError> { - // // TODO: deal with paging in macro or some helper function or something, because it's the same pattern everywhere - // let mut page = 0; - // let mut descriptions = Vec::new(); - // - // loop { - // let mut res = self - // .nym_api - // .get_nodes_described_v2(Some(page), None) - // .await?; - // - // descriptions.append(&mut res.data); - // if descriptions.len() < res.pagination.total { - // page += 1 - // } else { - // break; - // } - // } - // - // Ok(descriptions) - // } + pub async fn get_all_described_nodes_v2( + &self, + ) -> Result, ValidatorClientError> { + // TODO: deal with paging in macro or some helper function or something, because it's the same pattern everywhere + let mut page = 0; + let mut descriptions = Vec::new(); + + loop { + let mut res = self + .nym_api + .get_nodes_described_v2(Some(page), None) + .await?; + + descriptions.append(&mut res.data); + if descriptions.len() < res.pagination.total { + page += 1 + } else { + break; + } + } + + Ok(descriptions) + } pub async fn get_all_bonded_nym_nodes( &self, diff --git a/common/client-libs/validator-client/src/nym_api/mod.rs b/common/client-libs/validator-client/src/nym_api/mod.rs index 08bcd7e7f3..cf71f706a4 100644 --- a/common/client-libs/validator-client/src/nym_api/mod.rs +++ b/common/client-libs/validator-client/src/nym_api/mod.rs @@ -17,7 +17,7 @@ use nym_api_requests::ecash::VerificationKeyResponse; use nym_api_requests::models::{ AnnotationResponse, ApiHealthResponse, BinaryBuildInformationOwned, ChainBlocksStatusResponse, ChainStatusResponse, KeyRotationInfoResponse, NodePerformanceResponse, NodeRefreshBody, - NymNodeDescriptionV1, PerformanceHistoryResponse, RewardedSetResponse, + NymNodeDescriptionV1, NymNodeDescriptionV2, PerformanceHistoryResponse, RewardedSetResponse, SignerInformationResponse, }; use nym_api_requests::nym_nodes::{ @@ -117,7 +117,7 @@ pub trait NymApiClientExt: ApiClient { } #[tracing::instrument(level = "debug", skip_all)] - // #[deprecated(note = "use .get_nodes_described_v2 instead")] + #[deprecated(note = "use .get_nodes_described_v2 instead")] async fn get_nodes_described( &self, page: Option, @@ -144,32 +144,32 @@ pub trait NymApiClientExt: ApiClient { .await } - // #[tracing::instrument(level = "debug", skip_all)] - // async fn get_nodes_described_v2( - // &self, - // page: Option, - // per_page: Option, - // ) -> Result, NymAPIError> { - // let mut params = Vec::new(); - // - // if let Some(page) = page { - // params.push(("page", page.to_string())) - // } - // - // if let Some(per_page) = per_page { - // params.push(("per_page", per_page.to_string())) - // } - // - // self.get_json( - // &[ - // routes::V2_API_VERSION, - // routes::NYM_NODES_ROUTES, - // routes::NYM_NODES_DESCRIBED, - // ], - // ¶ms, - // ) - // .await - // } + #[tracing::instrument(level = "debug", skip_all)] + async fn get_nodes_described_v2( + &self, + page: Option, + per_page: Option, + ) -> Result, NymAPIError> { + let mut params = Vec::new(); + + if let Some(page) = page { + params.push(("page", page.to_string())) + } + + if let Some(per_page) = per_page { + params.push(("per_page", per_page.to_string())) + } + + self.get_json( + &[ + routes::V2_API_VERSION, + routes::NYM_NODES_ROUTES, + routes::NYM_NODES_DESCRIBED, + ], + ¶ms, + ) + .await + } async fn get_current_rewarded_set(&self) -> Result { self.get_rewarded_set().await @@ -302,8 +302,8 @@ pub trait NymApiClientExt: ApiClient { Ok(SkimmedNodesWithMetadata::new(nodes, metadata)) } - // #[deprecated(note = "use .get_all_described_nodes_v2 instead")] - // #[allow(deprecated)] + #[deprecated(note = "use .get_all_described_nodes_v2 instead")] + #[allow(deprecated)] async fn get_all_described_nodes(&self) -> Result, NymAPIError> { // TODO: deal with paging in macro or some helper function or something, because it's the same pattern everywhere let mut page = 0; @@ -323,24 +323,24 @@ pub trait NymApiClientExt: ApiClient { Ok(descriptions) } - // async fn (&self) -> Result, NymAPIError> { - // // TODO: deal with paging in macro or some helper function or something, because it's the same pattern everywhere - // let mut page = 0; - // let mut descriptions = Vec::new(); - // - // loop { - // let mut res = self.get_nodes_described_v2(Some(page), None).await?; - // - // descriptions.append(&mut res.data); - // if descriptions.len() < res.pagination.total { - // page += 1 - // } else { - // break; - // } - // } - // - // Ok(descriptions) - // } + async fn get_all_described_nodes_v2(&self) -> Result, NymAPIError> { + // TODO: deal with paging in macro or some helper function or something, because it's the same pattern everywhere + let mut page = 0; + let mut descriptions = Vec::new(); + + loop { + let mut res = self.get_nodes_described_v2(Some(page), None).await?; + + descriptions.append(&mut res.data); + if descriptions.len() < res.pagination.total { + page += 1 + } else { + break; + } + } + + Ok(descriptions) + } #[tracing::instrument(level = "debug", skip_all)] async fn get_nym_nodes( diff --git a/common/nym-kkt-ciphersuite/Cargo.toml b/common/nym-kkt-ciphersuite/Cargo.toml index efd52f7030..030945c8cb 100644 --- a/common/nym-kkt-ciphersuite/Cargo.toml +++ b/common/nym-kkt-ciphersuite/Cargo.toml @@ -9,6 +9,7 @@ license.workspace = true rust-version.workspace = true readme.workspace = true version.workspace = true +publish = false [dependencies] thiserror = { workspace = true } diff --git a/common/nym-kkt-ciphersuite/src/lib.rs b/common/nym-kkt-ciphersuite/src/lib.rs index 52ef33ed3b..e27d48c790 100644 --- a/common/nym-kkt-ciphersuite/src/lib.rs +++ b/common/nym-kkt-ciphersuite/src/lib.rs @@ -3,7 +3,7 @@ use crate::error::KKTCiphersuiteError; use num_enum::{IntoPrimitive, TryFromPrimitive}; -use std::collections::HashMap; +use std::collections::BTreeMap; use std::fmt::Display; use strum_macros::{Display, EnumIter, EnumString}; @@ -47,12 +47,7 @@ pub mod xwing { pub const PUBLIC_KEY_LENGTH: usize = x25519::PUBLIC_KEY_LENGTH + ml_kem768::PUBLIC_KEY_LENGTH; } -pub type KEMKeyDigests = KeyDigests; - -#[deprecated] -pub type SigningKeyDigests = KeyDigests; - -pub type KeyDigests = HashMap>; +pub type KEMKeyDigests = BTreeMap>; #[derive( Clone, @@ -66,6 +61,8 @@ pub type KeyDigests = HashMap>; EnumIter, EnumString, Display, + Ord, + PartialOrd, )] #[strum(ascii_case_insensitive)] #[strum(serialize_all = "lowercase")] @@ -209,6 +206,8 @@ impl SignatureScheme { EnumString, Display, Default, + Ord, + PartialOrd, )] #[strum(ascii_case_insensitive)] #[strum(serialize_all = "lowercase")] diff --git a/common/nym-kkt-context/Cargo.toml b/common/nym-kkt-context/Cargo.toml index ef1c4f5a44..9ab1c8e123 100644 --- a/common/nym-kkt-context/Cargo.toml +++ b/common/nym-kkt-context/Cargo.toml @@ -9,6 +9,7 @@ license.workspace = true rust-version.workspace = true readme.workspace = true version.workspace = true +publish = false [dependencies] num_enum = { workspace = true } diff --git a/common/nym-kkt/src/key_utils.rs b/common/nym-kkt/src/key_utils.rs index 35295a5c0a..222f228088 100644 --- a/common/nym-kkt/src/key_utils.rs +++ b/common/nym-kkt/src/key_utils.rs @@ -3,9 +3,9 @@ use libcrux_ml_kem::mlkem768::MlKem768KeyPair; use libcrux_psq::handshake::types::DHKeyPair; -use nym_kkt_ciphersuite::{DEFAULT_HASH_LEN, HashFunction, KeyDigests}; +use nym_kkt_ciphersuite::{DEFAULT_HASH_LEN, HashFunction, KEMKeyDigests}; use rand09::{CryptoRng, RngCore}; -use std::collections::HashMap; +use std::collections::BTreeMap; pub fn generate_lp_keypair_x25519(rng: &mut R) -> DHKeyPair where @@ -38,9 +38,9 @@ pub fn hash_key_bytes( /// attempt to produce digests of the provided key using all known [HashFunction] with a default /// hash length where variable output is available -pub fn produce_key_digests(key_bytes: &[u8]) -> KeyDigests { +pub fn produce_key_digests(key_bytes: &[u8]) -> KEMKeyDigests { use strum::IntoEnumIterator; - let mut digests = HashMap::new(); + let mut digests = BTreeMap::new(); for hash in HashFunction::iter() { digests.insert(hash, hash.digest(key_bytes, DEFAULT_HASH_LEN)); } diff --git a/common/nym-kkt/src/keys.rs b/common/nym-kkt/src/keys.rs index 0a0f4fdc6c..18206de7b8 100644 --- a/common/nym-kkt/src/keys.rs +++ b/common/nym-kkt/src/keys.rs @@ -3,10 +3,12 @@ use crate::error::KKTError; use libcrux_psq::handshake::types::PQEncapsulationKey; -use nym_kkt_ciphersuite::KEM; +use nym_kkt_ciphersuite::{KEM, KEMKeyDigests}; +use std::collections::BTreeMap; use std::fmt::{Debug, Formatter}; use std::sync::Arc; +use crate::key_utils::produce_key_digests; pub use libcrux_ml_kem::mlkem768::{MlKem768KeyPair, MlKem768PrivateKey, MlKem768PublicKey}; pub use libcrux_psq::classic_mceliece as mceliece; pub use libcrux_psq::handshake::types::{DHKeyPair, DHPrivateKey, DHPublicKey}; @@ -41,6 +43,18 @@ impl KEMKeys { } } + pub fn encapsulation_keys_digests(&self) -> BTreeMap { + let mut digests = BTreeMap::new(); + + let mlkem_digests = produce_key_digests(self.ml_kem768_pk.as_slice()); + let mceliece_digests = produce_key_digests(self.mc_eliece_pk.as_ref().as_ref()); + + digests.insert(KEM::MlKem768, mlkem_digests); + digests.insert(KEM::McEliece, mceliece_digests); + + digests + } + pub fn encoded_encapsulation_key(&self, kem: KEM) -> Option<&[u8]> { match kem { KEM::McEliece => Some(self.mc_eliece_pk.as_ref().as_ref()), diff --git a/common/nym-lp-packet/Cargo.toml b/common/nym-lp-packet/Cargo.toml index 0778b19228..15c490f8ce 100644 --- a/common/nym-lp-packet/Cargo.toml +++ b/common/nym-lp-packet/Cargo.toml @@ -9,6 +9,7 @@ license.workspace = true rust-version.workspace = true readme.workspace = true version.workspace = true +publish = false [dependencies] bytes = { workspace = true } diff --git a/common/nym-lp/src/peer.rs b/common/nym-lp/src/peer.rs index 8e29b46f50..888d908100 100644 --- a/common/nym-lp/src/peer.rs +++ b/common/nym-lp/src/peer.rs @@ -4,7 +4,7 @@ use crate::LpError; use nym_crypto::asymmetric::x25519; use nym_kkt_ciphersuite::{Ciphersuite, KEM, KEMKeyDigests}; -use std::collections::HashMap; +use std::collections::BTreeMap; use std::fmt::Debug; use std::sync::Arc; @@ -46,17 +46,11 @@ impl LpLocalPeer { /// Convert this `LpLocalPeer` into a valid `LpRemotePeer` that can be used within tests #[doc(hidden)] pub fn as_remote(&self) -> LpRemotePeer { - let mut expected_kem_key_digests = HashMap::new(); - if let Some(keys) = &self.kem_keypairs { - for kem in [KEM::MlKem768, KEM::McEliece] { - expected_kem_key_digests.insert( - kem, - nym_kkt::key_utils::produce_key_digests( - keys.encoded_encapsulation_key(kem).unwrap(), - ), - ); - } - } + let expected_kem_key_digests = self + .kem_keypairs + .as_ref() + .map(|k| k.encapsulation_keys_digests()) + .unwrap_or_default(); LpRemotePeer { x25519_public: self.x25519.pk, @@ -87,7 +81,7 @@ pub struct LpRemotePeer { pub(crate) x25519_public: DHPublicKey, /// Expected digests of the remote's KEM key - pub(crate) expected_kem_key_digests: HashMap, + pub(crate) expected_kem_key_digests: BTreeMap, } impl LpRemotePeer { @@ -107,7 +101,7 @@ impl LpRemotePeer { #[must_use] pub fn with_key_digests( mut self, - expected_kem_key_digests: HashMap, + expected_kem_key_digests: BTreeMap, ) -> Self { self.expected_kem_key_digests = expected_kem_key_digests; self diff --git a/nym-api/nym-api-requests/src/models/described/type_translation.rs b/nym-api/nym-api-requests/src/models/described/type_translation.rs index 212eb475af..8fe02bdd03 100644 --- a/nym-api/nym-api-requests/src/models/described/type_translation.rs +++ b/nym-api/nym-api-requests/src/models/described/type_translation.rs @@ -5,15 +5,16 @@ //! and defines required conversion methods use celes::Country; -use nym_crypto::asymmetric::ed25519::serde_helpers::bs58_ed25519_pubkey; +use nym_crypto::asymmetric::ed25519::serde_helpers::{bs58_ed25519_pubkey, bs58_ed25519_signature}; use nym_crypto::asymmetric::x25519::serde_helpers::{bs58_dh_public_key, bs58_x25519_pubkey}; use nym_crypto::asymmetric::{ed25519, x25519}; use nym_kkt_ciphersuite::{HashFunction, SignatureScheme, KEM}; use nym_network_defaults::{WG_METADATA_PORT, WG_TUNNEL_PORT}; +use nym_node_requests::api::SignedData; use nym_noise_keys::VersionedNoiseKeyV1; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use std::collections::HashMap; +use std::collections::BTreeMap; use std::net::IpAddr; use strum_macros::{Display, EnumString}; use thiserror::Error; @@ -200,6 +201,16 @@ pub struct WireguardDetailsV1 { // even if you put `#[serde(default)]` all over the place #[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema, PartialEq)] pub struct LewesProtocolDetailsV1 { + pub content: LewesProtocolDetailsDataV1, + + #[serde(with = "bs58_ed25519_signature")] + #[schemars(with = "String")] + #[schema(value_type = String)] + pub signature: ed25519::Signature, +} + +#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema, PartialEq)] +pub struct LewesProtocolDetailsDataV1 { /// 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, @@ -219,14 +230,14 @@ pub struct LewesProtocolDetailsV1 { /// Digests of the KEM keys available to this node alongside hashing algorithms used /// for their computation. /// note: digests are hex encoded - pub kem_keys: HashMap>, + pub kem_keys: BTreeMap>, } -impl LewesProtocolDetailsV1 { +impl LewesProtocolDetailsDataV1 { fn decode_digests( - digests: &HashMap, - ) -> Result>, MalformedLPData> { - let mut kem_digests = HashMap::new(); + digests: &BTreeMap, + ) -> Result>, MalformedLPData> { + let mut kem_digests = BTreeMap::new(); for (hash_function, digest) in digests { let digest = hex::decode(digest).map_err(|source| MalformedLPData::MalformedHash { value: digest.to_string(), @@ -239,8 +250,8 @@ impl LewesProtocolDetailsV1 { pub fn kem_keys( &self, - ) -> Result>>, MalformedLPData> { - let mut kem_keys = HashMap::new(); + ) -> Result>>, MalformedLPData> { + let mut kem_keys = BTreeMap::new(); for (kem, digests) in &self.kem_keys { let kem_digests = Self::decode_digests(digests)?; kem_keys.insert((*kem).try_into()?, kem_digests); @@ -251,8 +262,8 @@ impl LewesProtocolDetailsV1 { /// Convert map of digests from `nym_node_requests` types into `nym-api-requests` types fn translate_digests( - digests: HashMap, -) -> HashMap { + digests: BTreeMap, +) -> BTreeMap { digests .into_iter() .map(|(hash_fn, digest)| (hash_fn.into(), digest)) @@ -273,6 +284,7 @@ fn translate_digests( Display, EnumString, ToSchema, + Ord, )] #[strum(serialize_all = "lowercase")] #[non_exhaustive] @@ -295,6 +307,7 @@ pub enum LPKEM { Display, EnumString, ToSchema, + Ord, )] #[strum(serialize_all = "lowercase")] #[non_exhaustive] @@ -437,20 +450,26 @@ impl From for WireguardD } } -impl From +impl From> for LewesProtocolDetailsV1 { - fn from(value: nym_node_requests::api::v1::lewes_protocol::models::LewesProtocol) -> Self { + fn from( + value: SignedData, + ) -> Self { LewesProtocolDetailsV1 { - enabled: value.enabled, - control_port: value.control_port, - data_port: value.data_port, - x25519: value.x25519, - kem_keys: value - .kem_keys - .into_iter() - .map(|(kem, digests)| (kem.into(), translate_digests(digests))) - .collect(), + signature: value.signature, + content: LewesProtocolDetailsDataV1 { + enabled: value.enabled, + control_port: value.control_port, + data_port: value.data_port, + x25519: value.x25519, + kem_keys: value + .data + .kem_keys + .into_iter() + .map(|(kem, digests)| (kem.into(), translate_digests(digests))) + .collect(), + }, } } } @@ -549,3 +568,14 @@ impl TryFrom for SignatureScheme { } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn signature_validity_after_conversion() { + // make sure the serialisation stays the same and signature is still valid + todo!() + } +} diff --git a/nym-api/src/nym_nodes/handlers/v1.rs b/nym-api/src/nym_nodes/handlers/v1.rs index 8cd5b46368..ce004731ed 100644 --- a/nym-api/src/nym_nodes/handlers/v1.rs +++ b/nym-api/src/nym_nodes/handlers/v1.rs @@ -231,7 +231,7 @@ async fn get_bonded_nodes( ), params(PaginationRequest) )] -// #[deprecated(note = "use '/v2/nym-nodes/described' instead")] +#[deprecated(note = "use '/v2/nym-nodes/described' instead")] async fn get_described_nodes( State(state): State, Query(pagination): Query, diff --git a/nym-api/src/nym_nodes/handlers/v2.rs b/nym-api/src/nym_nodes/handlers/v2.rs index 502f2c6e1c..d6306001bc 100644 --- a/nym-api/src/nym_nodes/handlers/v2.rs +++ b/nym-api/src/nym_nodes/handlers/v2.rs @@ -8,7 +8,7 @@ use axum::extract::{Query, State}; use axum::routing::get; use axum::Router; use nym_api_requests::models::NymNodeDescriptionV2; -use nym_api_requests::pagination::PaginatedResponse; +use nym_api_requests::pagination::{PaginatedResponse, Pagination}; use nym_http_api_common::FormattedResponse; use tower_http::compression::CompressionLayer; @@ -36,23 +36,19 @@ async fn get_described_nodes( State(state): State, Query(pagination): Query, ) -> AxumResult>> { - let _ = state; + // TODO: implement it let _ = pagination; - Err(AxumErrorResponse::not_implemented()) + let output = pagination.output.unwrap_or_default(); - // // 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::>(); - // - // Ok(output.to_response(PaginatedResponse { - // pagination: Pagination { - // total: descriptions.len(), - // page: 0, - // size: descriptions.len(), - // }, - // data: descriptions, - // })) + let cache = state.described_nodes_cache.get().await?; + let descriptions = cache.all_nodes().cloned().collect::>(); + + Ok(output.to_response(PaginatedResponse { + pagination: Pagination { + total: descriptions.len(), + page: 0, + size: descriptions.len(), + }, + data: descriptions, + })) } diff --git a/nym-node/Cargo.toml b/nym-node/Cargo.toml index 756e2c6780..275b4fda55 100644 --- a/nym-node/Cargo.toml +++ b/nym-node/Cargo.toml @@ -134,7 +134,6 @@ cargo_metadata = { workspace = true } [dev-dependencies] criterion = { workspace = true, features = ["async_tokio"] } -rand_chacha = { workspace = true } [features] tokio-console = ["console-subscriber", "nym-task/tokio-tracing"] diff --git a/nym-node/nym-node-requests/Cargo.toml b/nym-node/nym-node-requests/Cargo.toml index f87a71f13d..b051086220 100644 --- a/nym-node/nym-node-requests/Cargo.toml +++ b/nym-node/nym-node-requests/Cargo.toml @@ -48,7 +48,7 @@ nym-bin-common = { workspace = true, features = [ [dev-dependencies] tokio = { workspace = true, features = ["full"] } -rand_chacha = { workspace = true } +nym-test-utils = { workspace = true } nym-crypto = { workspace = true, features = ["rand"] } diff --git a/nym-node/nym-node-requests/src/api/client.rs b/nym-node/nym-node-requests/src/api/client.rs index dd014851cc..e88e846099 100644 --- a/nym-node/nym-node-requests/src/api/client.rs +++ b/nym-node/nym-node-requests/src/api/client.rs @@ -12,6 +12,7 @@ use nym_http_api_client::{ApiClient, HttpClientError}; use super::v1::gateway::models::Wireguard; use super::v1::metrics::models::SessionStats; +use crate::api::SignedLewesProtocol; use crate::api::v1::authenticator::models::Authenticator; use crate::api::v1::health::models::NodeHealth; use crate::api::v1::ip_packet_router::models::IpPacketRouter; @@ -98,7 +99,7 @@ pub trait NymNodeApiClientExt: ApiClient { .await } - async fn get_lewes_protocol(&self) -> Result { + async fn get_lewes_protocol(&self) -> Result { self.get_json_from(routes::api::v1::lp_absolute()).await } } diff --git a/nym-node/nym-node-requests/src/api/mod.rs b/nym-node/nym-node-requests/src/api/mod.rs index ba32570c2e..b884773edc 100644 --- a/nym-node/nym-node-requests/src/api/mod.rs +++ b/nym-node/nym-node-requests/src/api/mod.rs @@ -20,6 +20,7 @@ pub use client::Client; // create the type alias manually if openapi is not enabled pub type SignedHostInformation = SignedData; +pub type SignedLewesProtocol = SignedData; #[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] pub struct SignedDataHostInfo { @@ -28,11 +29,20 @@ pub struct SignedDataHostInfo { pub signature: String, } +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +pub struct SignedLewesProtocolInfo { + // #[serde(flatten)] + pub data: crate::api::v1::lewes_protocol::models::LewesProtocol, + pub signature: String, +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SignedData { // #[serde(flatten)] pub data: T, - pub signature: String, + + #[serde(with = "ed25519::bs58_ed25519_signature")] + pub signature: ed25519::Signature, } impl SignedData { @@ -42,7 +52,7 @@ impl SignedData { { let plaintext = serde_json::to_string(&data)?; - let signature = key.sign(plaintext).to_base58_string(); + let signature = key.sign(plaintext); Ok(SignedData { data, signature }) } @@ -54,11 +64,7 @@ impl SignedData { return false; }; - let Ok(signature) = ed25519::Signature::from_base58_string(&self.signature) else { - return false; - }; - - key.verify(plaintext, &signature).is_ok() + key.verify(plaintext, &self.signature).is_ok() } } @@ -72,7 +78,7 @@ impl SignedHostInformation { let legacy_v3 = SignedData { data: LegacyHostInformationV3::from(self.data.clone()), - signature: self.signature.clone(), + signature: self.signature, }; if legacy_v3.verify(&self.keys.ed25519_identity) { @@ -82,7 +88,7 @@ impl SignedHostInformation { // attempt to verify legacy signatures let legacy_v3 = SignedData { data: LegacyHostInformationV3::from(self.data.clone()), - signature: self.signature.clone(), + signature: self.signature, }; if legacy_v3.verify(&self.keys.ed25519_identity) { @@ -91,7 +97,7 @@ impl SignedHostInformation { let legacy_v2 = SignedData { data: LegacyHostInformationV2::from(legacy_v3.data), - signature: self.signature.clone(), + signature: self.signature, }; if legacy_v2.verify(&self.keys.ed25519_identity) { @@ -100,7 +106,7 @@ impl SignedHostInformation { SignedData { data: LegacyHostInformationV1::from(legacy_v2.data), - signature: self.signature.clone(), + signature: self.signature, } .verify(&self.keys.ed25519_identity) } @@ -133,11 +139,11 @@ mod tests { use crate::api::v1::node::models::{HostKeys, SphinxKey}; use nym_crypto::asymmetric::{ed25519, x25519}; use nym_noise_keys::{NoiseVersion, VersionedNoiseKeyV1}; - use rand_chacha::rand_core::SeedableRng; + use nym_test_utils::helpers::deterministic_rng; #[test] fn dummy_signed_host_verification() { - let mut rng = rand_chacha::ChaCha20Rng::from_seed([0u8; 32]); + let mut rng = deterministic_rng(); let ed22519 = ed25519::KeyPair::new(&mut rng); let x25519_sphinx = x25519::KeyPair::new(&mut rng); let x25519_sphinx2 = x25519::KeyPair::new(&mut rng); diff --git a/nym-node/nym-node-requests/src/api/v1/lewes_protocol/models.rs b/nym-node/nym-node-requests/src/api/v1/lewes_protocol/models.rs index ba99a69f80..fe1ea68bd5 100644 --- a/nym-node/nym-node-requests/src/api/v1/lewes_protocol/models.rs +++ b/nym-node/nym-node-requests/src/api/v1/lewes_protocol/models.rs @@ -5,7 +5,7 @@ use nym_crypto::asymmetric::x25519; use nym_crypto::asymmetric::x25519::serde_helpers::bs58_dh_public_key; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use std::collections::HashMap; +use std::collections::BTreeMap; use strum_macros::{Display, EnumIter, EnumString}; #[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)] @@ -30,7 +30,7 @@ pub struct LewesProtocol { /// Digests of the KEM keys available to this node alongside hashing algorithms used /// for their computation. /// note: digests are hex encoded - pub kem_keys: HashMap>, + pub kem_keys: BTreeMap>, } impl LewesProtocol { @@ -39,7 +39,7 @@ impl LewesProtocol { control_port: u16, data_port: u16, x25519: x25519::DHPublicKey, - kem_keys: HashMap>, + kem_keys: BTreeMap>, ) -> Self { LewesProtocol { enabled, @@ -69,6 +69,7 @@ impl LewesProtocol { PartialOrd, Display, EnumString, + Ord, )] #[strum(serialize_all = "lowercase")] #[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] @@ -109,6 +110,7 @@ impl From for LPKEM { Display, EnumString, EnumIter, + Ord, )] #[strum(serialize_all = "lowercase")] #[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] diff --git a/nym-node/src/node/http/router/api/v1/lewes_protocol/mod.rs b/nym-node/src/node/http/router/api/v1/lewes_protocol/mod.rs index 36d8dc785d..6e0aa38e58 100644 --- a/nym-node/src/node/http/router/api/v1/lewes_protocol/mod.rs +++ b/nym-node/src/node/http/router/api/v1/lewes_protocol/mod.rs @@ -3,13 +3,13 @@ use axum::Router; use axum::routing::get; -use nym_node_requests::api::v1::lewes_protocol::models; +use nym_node_requests::api::SignedLewesProtocol; pub mod root; -#[derive(Debug, Default, Clone)] +#[derive(Debug, Clone)] pub struct Config { - pub details: Option, + pub details: SignedLewesProtocol, } pub(crate) fn routes(config: Config) -> Router { diff --git a/nym-node/src/node/http/router/api/v1/lewes_protocol/root.rs b/nym-node/src/node/http/router/api/v1/lewes_protocol/root.rs index b056db5a90..06b2961717 100644 --- a/nym-node/src/node/http/router/api/v1/lewes_protocol/root.rs +++ b/nym-node/src/node/http/router/api/v1/lewes_protocol/root.rs @@ -4,30 +4,29 @@ use axum::extract::Query; use axum::http::StatusCode; use nym_http_api_common::{FormattedResponse, OutputParams}; -use nym_node_requests::api::v1::lewes_protocol::models::LewesProtocol; +use nym_node_requests::api::{SignedLewesProtocol, SignedLewesProtocolInfo}; /// Returns root Lewes Protocol information #[utoipa::path( get, - path = "", - context_path = "/api/v1/lewes-protocol", + path = "/lewes-protocol", + context_path = "/api/v1", tag = "Lewes Protocol", responses( (status = 501, description = "the endpoint hasn't been implemented yet"), (status = 200, content( - (LewesProtocol = "application/json"), - (LewesProtocol = "application/yaml"), - (LewesProtocol = "application/bincode") + (SignedLewesProtocolInfo = "application/json"), + (SignedLewesProtocolInfo = "application/yaml"), + (SignedLewesProtocolInfo = "application/bincode") )) ), params(OutputParams) )] pub(crate) async fn root_lewes_protocol( - config: Option, + config: SignedLewesProtocol, Query(output): Query, ) -> Result { - let config = config.ok_or(StatusCode::NOT_IMPLEMENTED)?; Ok(output.to_response(config)) } -pub type LewesProtocolResponse = FormattedResponse; +pub type LewesProtocolResponse = FormattedResponse; diff --git a/nym-node/src/node/http/router/api/v1/node/host_information.rs b/nym-node/src/node/http/router/api/v1/node/host_information.rs index 53427f0cdf..8e828f8590 100644 --- a/nym-node/src/node/http/router/api/v1/node/host_information.rs +++ b/nym-node/src/node/http/router/api/v1/node/host_information.rs @@ -16,7 +16,8 @@ use nym_node_requests::api::{SignedDataHostInfo, v1::node::models::SignedHostInf responses( (status = 200, content( (SignedDataHostInfo = "application/json"), - (SignedDataHostInfo = "application/yaml") + (SignedDataHostInfo = "application/yaml"), + (SignedDataHostInfo = "application/bincode") )) ), params(OutputParams) diff --git a/nym-node/src/node/http/router/mod.rs b/nym-node/src/node/http/router/mod.rs index dbaee0e8d5..4c3618c76f 100644 --- a/nym-node/src/node/http/router/mod.rs +++ b/nym-node/src/node/http/router/mod.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::node::http::NymNodeHttpServer; +use crate::node::http::api::v1::lewes_protocol; use crate::node::http::error::NymNodeHttpError; use crate::node::http::state::AppState; use axum::Router; @@ -9,6 +10,7 @@ use axum::response::Redirect; use axum::routing::get; use nym_bin_common::bin_info_owned; use nym_http_api_common::middleware::logging; +use nym_node_requests::api::SignedLewesProtocol; use nym_node_requests::api::v1::authenticator::models::Authenticator; use nym_node_requests::api::v1::gateway::models::{Bridges, Gateway}; use nym_node_requests::api::v1::ip_packet_router::models::IpPacketRouter; @@ -33,7 +35,7 @@ pub struct HttpServerConfig { } impl HttpServerConfig { - pub fn new() -> Self { + pub fn new(signed_lewes_protocol: SignedLewesProtocol) -> Self { HttpServerConfig { landing: Default::default(), api: api::Config { @@ -52,7 +54,9 @@ impl HttpServerConfig { network_requester: Default::default(), ip_packet_router: Default::default(), authenticator: Default::default(), - lewes_protocol: Default::default(), + lewes_protocol: lewes_protocol::Config { + details: signed_lewes_protocol, + }, }, }, } diff --git a/nym-node/src/node/mod.rs b/nym-node/src/node/mod.rs index a77536ffa9..1acf98a908 100644 --- a/nym-node/src/node/mod.rs +++ b/nym-node/src/node/mod.rs @@ -57,6 +57,8 @@ use nym_network_requester::{ }; use nym_node_metrics::NymNodeMetrics; use nym_node_metrics::events::MetricEventsSender; +use nym_node_requests::api::SignedData; +use nym_node_requests::api::v1::lewes_protocol::models::{LPHashFunction, LPKEM, LewesProtocol}; use nym_node_requests::api::v1::node::models::{AnnouncePorts, NodeDescription}; use nym_noise::config::{NoiseConfig, NoiseNetworkView}; use nym_noise_keys::VersionedNoiseKeyV1; @@ -70,6 +72,7 @@ use nym_wireguard::{WireguardGatewayData, peer_controller::PeerControlRequest}; use rand::rngs::OsRng; use rand::{CryptoRng, RngCore}; use rand09::SeedableRng; +use std::collections::BTreeMap; use std::net::SocketAddr; use std::ops::Deref; use std::path::Path; @@ -736,13 +739,11 @@ impl NymNode { self.config.gateway_tasks.lp.control_bind_address, self.config.gateway_tasks.lp.data_bind_address, ); - let lp_listener = gateway_tasks_builder + let mut lp_listener = gateway_tasks_builder .build_lp_listener(wg_peer_registrator.clone(), active_clients_store.clone()) .await?; - // make sure lp listener can be built, but do not start it - let _ = lp_listener; - // self.shutdown_tracker() - // .try_spawn_named(async move { lp_listener.run().await }, "LpListener"); + self.shutdown_tracker() + .try_spawn_named(async move { lp_listener.run().await }, "LpListener"); } else { info!("node not running in entry mode: the websocket and LP will remain closed"); } @@ -818,40 +819,24 @@ impl NymNode { Ok(()) } - // - // fn compute_kem_key_hashes(&self) -> HashMap> { - // let kem = LPKEM::X25519; - // - // let kem_key_bytes = self.entry_gateway.psq_kem_key.public_key().as_bytes(); - // - // // convert from `nym_kkt_ciphersuite` types into `nym_nodes_requests` - // let digests = produce_key_digests(kem_key_bytes.as_ref()) - // .into_iter() - // .map(|(f, digest)| (f.into(), hex::encode(&digest))) - // .collect(); - // - // let mut hashes = HashMap::new(); - // hashes.insert(kem, digests); - // hashes - // } - // - // fn compute_signing_key_hashes( - // &self, - // ) -> HashMap> { - // let scheme = LPSignatureScheme::Ed25519; - // - // let kem_key_bytes = self.ed25519_identity_keys.public_key().as_bytes(); - // - // // convert from `nym_kkt_ciphersuite` types into `nym_nodes_requests` - // let digests = produce_key_digests(kem_key_bytes.as_ref()) - // .into_iter() - // .map(|(f, digest)| (f.into(), hex::encode(&digest))) - // .collect(); - // - // let mut hashes = HashMap::new(); - // hashes.insert(scheme, digests); - // hashes - // } + + fn compute_kem_key_hashes(&self) -> BTreeMap> { + let digests = self.psq_kem_keys.encapsulation_keys_digests(); + + // convert from `nym_kkt_ciphersuite` types into `nym_nodes_requests` + digests + .into_iter() + .map(|(kem, kem_digests)| { + ( + kem.into(), + kem_digests + .into_iter() + .map(|(f, digest)| (f.into(), hex::encode(&digest))) + .collect(), + ) + }) + .collect() + } pub(crate) async fn build_http_server(&self) -> Result { let auxiliary_details = api_requests::v1::node::models::AuxiliaryDetails { @@ -926,7 +911,21 @@ impl NymNode { policy: None, }; - let mut config = HttpServerConfig::new() + let lewes_protocol = LewesProtocol { + enabled: self.modes().entry, + control_port: self.config.gateway_tasks.lp.announced_control_port(), + data_port: self.config.gateway_tasks.lp.announced_data_port(), + x25519: self.x25519_lp_keys.pk, + kem_keys: self.compute_kem_key_hashes(), + }; + + // SAFETY: the only way for this call to fail is if serialisation of LewesProtocol fails. + // however, that conversion is stable and infallible + #[allow(clippy::unwrap_used)] + let signed_lewes_protocol = + SignedData::new(lewes_protocol, self.ed25519_identity_keys.private_key()).unwrap(); + + let mut config = HttpServerConfig::new(signed_lewes_protocol) .with_landing_page_assets(self.config.http.landing_page_assets_path.as_ref()) .with_mixnode_details(mixnode_details) .with_gateway_details(gateway_details)