From c1ddcc75cf4c6f7760f53fc1131a02e54c7c5ecd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Thu, 22 Jan 2026 14:29:33 +0000 Subject: [PATCH] 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 --- Cargo.lock | 6 + .../bin-common/src/build_information/mod.rs | 4 + .../validator-client/src/client.rs | 74 +-- .../validator-client/src/nym_api/mod.rs | 56 +- .../mixnet/query/query_all_gateways.rs | 2 +- .../mixnet/query/query_all_mixnodes.rs | 2 +- common/http-api-common/src/lib.rs | 4 +- common/nym-kkt/src/ciphersuite.rs | 12 +- common/nym-kkt/src/encryption.rs | 4 +- common/nym-lp/Cargo.toml | 1 + common/nym-lp/src/error.rs | 3 + common/nym-lp/src/kkt_orchestrator.rs | 25 +- common/nym-lp/src/lib.rs | 68 +- common/nym-lp/src/message.rs | 6 + common/nym-lp/src/peer.rs | 138 +++++ common/nym-lp/src/session.rs | 579 +++++------------- common/nym-lp/src/session_integration/mod.rs | 162 +---- common/nym-lp/src/session_manager.rs | 112 +--- common/nym-lp/src/state_machine.rs | 231 ++----- common/nymnoise/keys/src/lib.rs | 10 +- common/nymnoise/src/config.rs | 8 +- common/registration/src/lib.rs | 11 +- common/topology/src/node.rs | 6 +- common/verloc/src/measurements/measurer.rs | 8 +- gateway/src/node/lp_listener/handler.rs | 14 +- gateway/src/node/lp_listener/mod.rs | 14 +- gateway/src/node/lp_listener/registration.rs | 7 +- gateway/src/node/mod.rs | 21 +- integration-tests/src/lp_registration.rs | 44 +- nym-api/nym-api-requests/Cargo.toml | 15 +- .../nym-api-requests/src/models/described.rs | 418 ------------- .../src/models/described/mod.rs | 26 + .../src/models/described/type_translation.rs | 426 +++++++++++++ .../src/models/described/v1.rs | 198 ++++++ .../src/models/described/v2.rs | 343 +++++++++++ nym-api/nym-api-requests/src/models/mod.rs | 2 +- nym-api/nym-api-requests/src/nym_nodes.rs | 10 +- .../src/network_monitor/monitor/preparer.rs | 8 +- nym-api/src/node_describe_cache/cache.rs | 28 +- nym-api/src/node_describe_cache/mod.rs | 16 +- .../src/node_describe_cache/query_helpers.rs | 67 +- nym-api/src/node_describe_cache/refresh.rs | 14 +- .../src/node_status_api/cache/config_score.rs | 4 +- nym-api/src/nym_nodes/handlers/mod.rs | 494 +-------------- nym-api/src/nym_nodes/handlers/v1.rs | 502 +++++++++++++++ nym-api/src/nym_nodes/handlers/v2.rs | 54 ++ nym-api/src/support/http/router.rs | 12 +- .../v2/nym_nodes/semi_skimmed/mod.rs | 4 +- .../v2/nym_nodes/skimmed/helpers.rs | 6 +- nym-gateway-probe/src/lib.rs | 36 +- nym-gateway-probe/src/nodes.rs | 57 +- .../nym-node-status-api/src/db/models.rs | 52 +- .../src/db/queries/nym_nodes.rs | 19 +- .../nym-node-status-api/src/db/tests.rs | 12 +- .../nym-node-status-api/src/http/models.rs | 10 +- .../src/metrics_scraper/mod.rs | 12 +- .../nym-node-status-api/src/monitor/mod.rs | 8 +- nym-node/Cargo.toml | 3 +- nym-node/nym-node-requests/src/api/mod.rs | 8 +- .../src/api/v1/lewes_protocol/models.rs | 101 ++- .../src/api/v1/node/models.rs | 6 +- .../http/router/api/v1/lewes_protocol/mod.rs | 2 +- nym-node/src/node/http/state/mod.rs | 4 +- nym-node/src/node/mod.rs | 67 +- nym-registration-client/src/error.rs | 3 + nym-registration-client/src/lib.rs | 58 +- .../src/lp_client/client.rs | 67 +- .../src/lp_client/error.rs | 4 - .../src/lp_client/nested_session.rs | 66 +- nym-statistics-api/src/network_view.rs | 5 +- nym-wallet/Cargo.lock | 291 ++++++--- tools/nym-lp-client/src/client.rs | 11 +- tools/nym-lp-client/src/topology.rs | 75 +-- tools/ts-rs-cli/src/main.rs | 6 +- wasm/mix-fetch/src/helpers.rs | 2 +- 75 files changed, 2930 insertions(+), 2334 deletions(-) create mode 100644 common/nym-lp/src/peer.rs delete mode 100644 nym-api/nym-api-requests/src/models/described.rs create mode 100644 nym-api/nym-api-requests/src/models/described/mod.rs create mode 100644 nym-api/nym-api-requests/src/models/described/type_translation.rs create mode 100644 nym-api/nym-api-requests/src/models/described/v1.rs create mode 100644 nym-api/nym-api-requests/src/models/described/v2.rs create mode 100644 nym-api/src/nym_nodes/handlers/v1.rs create mode 100644 nym-api/src/nym_nodes/handlers/v2.rs diff --git a/Cargo.lock b/Cargo.lock index 9908c54241..2705d107e0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5403,6 +5403,7 @@ name = "nym-api-requests" version = "1.20.1" dependencies = [ "bs58", + "celes", "cosmrs", "cosmwasm-std", "ecdsa", @@ -5421,12 +5422,15 @@ dependencies = [ "nym-node-requests", "nym-noise-keys", "nym-serde-helpers", + "nym-test-utils", "nym-ticketbooks-merkle", "rand_chacha 0.3.1", "schemars 0.8.22", "serde", "serde_json", "sha2 0.10.9", + "strum", + "strum_macros", "tendermint", "tendermint-rpc", "thiserror 2.0.17", @@ -6828,6 +6832,7 @@ dependencies = [ "nym-crypto", "nym-kkt", "nym-lp-common", + "nym-test-utils", "parking_lot", "rand 0.8.5", "rand 0.9.2", @@ -7120,6 +7125,7 @@ dependencies = [ "nym-http-api-client", "nym-http-api-common", "nym-ip-packet-router", + "nym-kkt", "nym-metrics", "nym-mixnet-client", "nym-network-requester", diff --git a/common/bin-common/src/build_information/mod.rs b/common/bin-common/src/build_information/mod.rs index 6c5d13d4f1..5ed969e9e9 100644 --- a/common/bin-common/src/build_information/mod.rs +++ b/common/bin-common/src/build_information/mod.rs @@ -124,6 +124,10 @@ impl BinaryBuildInformation { } } +// 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, PartialEq, Eq)] #[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] #[cfg_attr(feature = "bin_info_schema", derive(schemars::JsonSchema))] diff --git a/common/client-libs/validator-client/src/client.rs b/common/client-libs/validator-client/src/client.rs index eb23ba60ca..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, NymNodeDescription, + MixnodeCoreStatusResponse, NymNodeDescriptionV1, NymNodeDescriptionV2, }; use nym_api_requests::nym_nodes::{ NodesByAddressesResponse, SemiSkimmedNodesWithMetadata, SkimmedNode, SkimmedNodesWithMetadata, @@ -273,48 +273,23 @@ impl Client { Ok(history) } - // TODO: combine with NymApiClient... + #[deprecated(note = "use get_all_cached_described_nodes_v2 instead")] pub async fn get_all_cached_described_nodes( &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(Some(page), None).await?; - - descriptions.append(&mut res.data); - if descriptions.len() < res.pagination.total { - page += 1 - } else { - break; - } - } - - Ok(descriptions) + ) -> 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?) } - // TODO: combine with NymApiClient... pub async fn get_all_cached_bonded_nym_nodes( &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 bonds = Vec::new(); - - loop { - let mut res = self.nym_api.get_nym_nodes(Some(page), None).await?; - - bonds.append(&mut res.data); - if bonds.len() < res.pagination.total { - page += 1 - } else { - break; - } - } - - Ok(bonds) + self.nym_api.get_all_bonded_nym_nodes().await } pub async fn blind_sign( @@ -498,9 +473,10 @@ impl NymApiClient { Ok(self.nym_api.health().await?) } + #[deprecated(note = "use .get_all_described_nodes_v2 instead")] pub async fn get_all_described_nodes( &self, - ) -> Result, ValidatorClientError> { + ) -> 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(); @@ -519,6 +495,30 @@ 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_bonded_nym_nodes( &self, ) -> Result, ValidatorClientError> { 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 8c549491e9..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,8 @@ use nym_api_requests::ecash::VerificationKeyResponse; use nym_api_requests::models::{ AnnotationResponse, ApiHealthResponse, BinaryBuildInformationOwned, ChainBlocksStatusResponse, ChainStatusResponse, KeyRotationInfoResponse, NodePerformanceResponse, NodeRefreshBody, - NymNodeDescription, PerformanceHistoryResponse, RewardedSetResponse, SignerInformationResponse, + NymNodeDescriptionV1, NymNodeDescriptionV2, PerformanceHistoryResponse, RewardedSetResponse, + SignerInformationResponse, }; use nym_api_requests::nym_nodes::{ NodesByAddressesRequestBody, NodesByAddressesResponse, PaginatedCachedNodesResponseV1, @@ -116,11 +117,12 @@ pub trait NymApiClientExt: ApiClient { } #[tracing::instrument(level = "debug", skip_all)] + #[deprecated(note = "use .get_nodes_described_v2 instead")] async fn get_nodes_described( &self, page: Option, per_page: Option, - ) -> Result, NymAPIError> { + ) -> Result, NymAPIError> { let mut params = Vec::new(); if let Some(page) = page { @@ -142,6 +144,33 @@ 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 + } + async fn get_current_rewarded_set(&self) -> Result { self.get_rewarded_set().await } @@ -273,7 +302,9 @@ pub trait NymApiClientExt: ApiClient { Ok(SkimmedNodesWithMetadata::new(nodes, metadata)) } - async fn get_all_described_nodes(&self) -> Result, NymAPIError> { + #[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; let mut descriptions = Vec::new(); @@ -292,6 +323,25 @@ pub trait NymApiClientExt: ApiClient { 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( &self, diff --git a/common/commands/src/validator/mixnet/query/query_all_gateways.rs b/common/commands/src/validator/mixnet/query/query_all_gateways.rs index f03fb61d28..516a533315 100644 --- a/common/commands/src/validator/mixnet/query/query_all_gateways.rs +++ b/common/commands/src/validator/mixnet/query/query_all_gateways.rs @@ -14,7 +14,7 @@ pub struct Args { } pub async fn query(args: Args, client: &QueryClientWithNyxd) { - match client.get_all_cached_described_nodes().await { + match client.get_all_cached_described_nodes_v2().await { Ok(res) => match args.identity_key { Some(identity_key) => { let node = res.iter().find(|node| { diff --git a/common/commands/src/validator/mixnet/query/query_all_mixnodes.rs b/common/commands/src/validator/mixnet/query/query_all_mixnodes.rs index ef5b4a9548..b1a81780b9 100644 --- a/common/commands/src/validator/mixnet/query/query_all_mixnodes.rs +++ b/common/commands/src/validator/mixnet/query/query_all_mixnodes.rs @@ -14,7 +14,7 @@ pub struct Args { } pub async fn query(args: Args, client: &QueryClientWithNyxd) { - match client.get_all_cached_described_nodes().await { + match client.get_all_cached_described_nodes_v2().await { Ok(res) => match args.identity_key { Some(identity_key) => { let node = res.iter().find(|node| { diff --git a/common/http-api-common/src/lib.rs b/common/http-api-common/src/lib.rs index 9ed715cde2..3ce026396e 100644 --- a/common/http-api-common/src/lib.rs +++ b/common/http-api-common/src/lib.rs @@ -11,8 +11,10 @@ pub mod response; #[cfg(feature = "output")] pub use response::*; +pub use ::bincode::Options as BincodeOptions; + // be explicit about those values because bincode uses different defaults in different places -pub fn make_bincode_serializer() -> impl ::bincode::Options { +pub fn make_bincode_serializer() -> impl BincodeOptions { use ::bincode::Options; ::bincode::DefaultOptions::new() .with_little_endian() diff --git a/common/nym-kkt/src/ciphersuite.rs b/common/nym-kkt/src/ciphersuite.rs index 7dd5a85ae3..a822afdf7d 100644 --- a/common/nym-kkt/src/ciphersuite.rs +++ b/common/nym-kkt/src/ciphersuite.rs @@ -1,14 +1,15 @@ // Copyright 2025 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use std::fmt::Display; - use libcrux_kem::{Algorithm, MlKem768PublicKey}; use nym_crypto::asymmetric::ed25519; +use std::fmt::Display; use crate::error::KKTError; -pub const HASH_LEN_256: usize = 32; +pub const DEFAULT_HASH_LEN: usize = 32; +const _: () = assert!(DEFAULT_HASH_LEN <= u8::MAX as usize); + pub const CIPHERSUITE_ENCODING_LEN: usize = 4; pub const CURVE25519_KEY_LEN: usize = 32; @@ -20,6 +21,7 @@ pub enum HashFunction { SHAKE256, SHA256, } + impl Display for HashFunction { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str(match self { @@ -172,7 +174,7 @@ impl Ciphersuite { l } } - None => HASH_LEN_256 as u8, + None => DEFAULT_HASH_LEN as u8, }; Ok(Self { hash_function, @@ -219,7 +221,7 @@ impl Ciphersuite { HashFunction::SHA256 => 3, }, match self.hash_length as usize { - HASH_LEN_256 => 0u8, + DEFAULT_HASH_LEN => 0u8, _ => self.hash_length, }, match self.signature_scheme { diff --git a/common/nym-kkt/src/encryption.rs b/common/nym-kkt/src/encryption.rs index 7bb7610622..b80abffbed 100644 --- a/common/nym-kkt/src/encryption.rs +++ b/common/nym-kkt/src/encryption.rs @@ -181,7 +181,7 @@ mod test { use crate::encryption::{decrypt_kkt_frame, encrypt_kkt_frame}; use crate::frame::{KKT_SESSION_ID_LEN, KKTFrame}; use crate::{ - ciphersuite::HASH_LEN_256, + ciphersuite::DEFAULT_HASH_LEN, encryption::{KKTSessionSecret, decrypt, encrypt}, key_utils::generate_keypair_x25519, }; @@ -209,7 +209,7 @@ mod test { fn test_encryption() { let mut rng = rng(); - let mut secret_key = [0u8; HASH_LEN_256]; + let mut secret_key = [0u8; DEFAULT_HASH_LEN]; rng.fill_bytes(&mut secret_key); let mut plaintext = vec![0; 100]; diff --git a/common/nym-lp/Cargo.toml b/common/nym-lp/Cargo.toml index 58c19b38f8..58f376b1e5 100644 --- a/common/nym-lp/Cargo.toml +++ b/common/nym-lp/Cargo.toml @@ -38,6 +38,7 @@ zeroize = { workspace = true, features = ["zeroize_derive"] } criterion = { version = "0.5", features = ["html_reports"] } rand_chacha = "0.3" nym-crypto = { path = "../crypto", features = ["rand"] } +nym-test-utils = { workspace = true } [[bench]] diff --git a/common/nym-lp/src/error.rs b/common/nym-lp/src/error.rs index 7beae8365b..290289fd0a 100644 --- a/common/nym-lp/src/error.rs +++ b/common/nym-lp/src/error.rs @@ -90,4 +90,7 @@ pub enum LpError { /// Received an LP packet with an incompatible, legacy, version #[error("incompatible LP packet version. got: {got}, lowest supported: {lowest_supported}")] IncompatibleLegacyPacketVersion { got: u8, lowest_supported: u8 }, + + #[error("attempted to create an LP responder without providing a valid KEM key")] + ResponderWithMissingKEMKey, } diff --git a/common/nym-lp/src/kkt_orchestrator.rs b/common/nym-lp/src/kkt_orchestrator.rs index f07168a933..98e3cdff71 100644 --- a/common/nym-lp/src/kkt_orchestrator.rs +++ b/common/nym-lp/src/kkt_orchestrator.rs @@ -184,6 +184,7 @@ pub fn handle_request<'a>( #[cfg(test)] mod tests { use super::*; + use crate::peer::mock_peers; use nym_kkt::ciphersuite::{HashFunction, KEM, SignatureScheme}; use nym_kkt::key_utils::{ generate_keypair_ed25519, generate_keypair_libcrux, generate_keypair_x25519, @@ -362,16 +363,8 @@ mod tests { #[test] fn test_hash_mismatch_rejected() { - let mut rng = rand09::rng(); - - // Generate Ed25519 keypairs for both parties - let initiator_ed25519_keypair = generate_keypair_ed25519(&mut rng, Some(0)); - let responder_ed25519_keypair = generate_keypair_ed25519(&mut rng, Some(1)); - - let responder_x25519 = generate_keypair_x25519(&mut rng); - - let (_, responder_kem_pk) = generate_keypair_libcrux(&mut rng, KEM::X25519).unwrap(); - let responder_kem_key = EncapsulationKey::X25519(responder_kem_pk); + let (init, resp) = mock_peers(); + let responder_kem_key = resp.encapsulate_kem_key().unwrap(); let ciphersuite = Ciphersuite::resolve_ciphersuite( KEM::X25519, @@ -386,16 +379,16 @@ mod tests { let (session_secret, context, request_data) = create_request( ciphersuite, - initiator_ed25519_keypair.private_key(), - responder_x25519.public_key(), + init.ed25519.private_key(), + resp.x25519.public_key(), ) .unwrap(); let response_data = handle_request( &request_data, - Some(initiator_ed25519_keypair.public_key()), - responder_ed25519_keypair.private_key(), - responder_x25519.private_key(), + Some(init.ed25519.public_key()), + resp.ed25519.private_key(), + resp.x25519.private_key(), &responder_kem_key, ) .unwrap(); @@ -404,7 +397,7 @@ mod tests { let result = process_response( context, &session_secret, - responder_ed25519_keypair.public_key(), + resp.ed25519.public_key(), &wrong_hash, // Wrong! &response_data, ); diff --git a/common/nym-lp/src/lib.rs b/common/nym-lp/src/lib.rs index 7e8eae8bd1..7a154cc37e 100644 --- a/common/nym-lp/src/lib.rs +++ b/common/nym-lp/src/lib.rs @@ -8,6 +8,7 @@ pub mod kkt_orchestrator; pub mod message; pub mod noise_protocol; pub mod packet; +pub mod peer; pub mod psk; pub mod replay; pub mod session; @@ -29,50 +30,20 @@ pub const NOISE_PSK_INDEX: u8 = 3; #[cfg(test)] pub fn sessions_for_tests() -> (LpSession, LpSession) { - use nym_crypto::asymmetric::{ed25519, x25519}; - use std::sync::Arc; - - let mut rng = rand::thread_rng(); - - // X25519 keypairs for Noise protocol - let keypair_1 = Arc::new(x25519::KeyPair::new(&mut rng)); - let keypair_2 = Arc::new(x25519::KeyPair::new(&mut rng)); + let (init, resp) = crate::peer::mock_peers(); // Use a fixed receiver_index for deterministic tests let receiver_index: u32 = 12345; - // Ed25519 keypairs for PSQ authentication (placeholders for testing) - let ed25519_keypair_1 = ed25519::KeyPair::from_secret([1u8; 32], 0); - let ed25519_keypair_2 = ed25519::KeyPair::from_secret([2u8; 32], 1); - - let ed25519_keypair1_pubkey = *ed25519_keypair_1.public_key(); - // Use consistent salt for deterministic tests let salt = [1u8; 32]; - // PSQ will always derive the PSK during handshake using X25519 as DHKEM + let initiator_session = + LpSession::new(receiver_index, true, init.clone(), resp.as_remote(), &salt) + .expect("Test session creation failed"); - let initiator_session = LpSession::new( - receiver_index, - true, - Arc::new(ed25519_keypair_1), - keypair_1.clone(), - ed25519_keypair_2.public_key(), - keypair_2.public_key(), - &salt, - ) - .expect("Test session creation failed"); - - let responder_session = LpSession::new( - receiver_index, - false, - Arc::new(ed25519_keypair_2), - keypair_2.clone(), - &ed25519_keypair1_pubkey, - keypair_1.public_key(), - &salt, - ) - .expect("Test session creation failed"); + let responder_session = LpSession::new(receiver_index, false, resp, init.as_remote(), &salt) + .expect("Test session creation failed"); (initiator_session, responder_session) } @@ -84,10 +55,10 @@ mod tests { use crate::session_manager::SessionManager; use crate::{LpError, sessions_for_tests}; use bytes::BytesMut; - use std::sync::Arc; // Import the new standalone functions use crate::codec::{parse_lp_packet, serialize_lp_packet}; + use crate::peer::mock_peers; #[test] fn test_replay_protection_integration() { @@ -190,19 +161,12 @@ mod tests { #[test] fn test_session_manager_integration() { - use nym_crypto::asymmetric::ed25519; - // Create session manager let local_manager = SessionManager::new(); let remote_manager = SessionManager::new(); // Generate Ed25519 keypairs for PSQ authentication - let ed25519_keypair_local = ed25519::KeyPair::from_secret([8u8; 32], 0); - let ed25519_keypair_remote = ed25519::KeyPair::from_secret([9u8; 32], 1); - - let ed25519_keypair_local_pubkey = *ed25519_keypair_local.public_key(); - let x25519_keypair_local_pubkey = ed25519_keypair_local_pubkey.to_x25519().unwrap(); - let x25519_keypair_remote_pubkey = ed25519_keypair_remote.public_key().to_x25519().unwrap(); + let (init, resp) = mock_peers(); // Use fixed receiver_index for deterministic test let receiver_index: u32 = 54321; @@ -214,23 +178,15 @@ mod tests { let _ = local_manager .create_session_state_machine( receiver_index, - Arc::new(ed25519_keypair_local), - ed25519_keypair_remote.public_key(), - &x25519_keypair_remote_pubkey, true, + init.clone(), + resp.as_remote(), &salt, ) .unwrap(); let _ = remote_manager - .create_session_state_machine( - receiver_index, - Arc::new(ed25519_keypair_remote), - &ed25519_keypair_local_pubkey, - &x25519_keypair_local_pubkey, - false, - &salt, - ) + .create_session_state_machine(receiver_index, false, resp, init.as_remote(), &salt) .unwrap(); // === Packet 1 (Counter 0 - Should succeed) === let packet1 = LpPacket { diff --git a/common/nym-lp/src/message.rs b/common/nym-lp/src/message.rs index 05d1a39c6c..8447b44684 100644 --- a/common/nym-lp/src/message.rs +++ b/common/nym-lp/src/message.rs @@ -1,6 +1,7 @@ // Copyright 2025 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use crate::peer::LpRemotePeer; use crate::{BOOTSTRAP_RECEIVER_IDX, LpError}; use bytes::{BufMut, BytesMut}; use num_enum::{IntoPrimitive, TryFromPrimitive}; @@ -109,6 +110,11 @@ impl ClientHelloData { salt: b[68..].try_into().unwrap(), }) } + + /// Attempt to construct remote peer information based on the data provided in this packet. + pub fn to_remote_peer(&self) -> LpRemotePeer { + LpRemotePeer::new(self.client_ed25519_public_key, self.client_lp_public_key) + } } #[derive(Debug, Copy, Clone, PartialEq, Eq, IntoPrimitive, TryFromPrimitive)] diff --git a/common/nym-lp/src/peer.rs b/common/nym-lp/src/peer.rs new file mode 100644 index 0000000000..02a6372ea6 --- /dev/null +++ b/common/nym-lp/src/peer.rs @@ -0,0 +1,138 @@ +// Copyright 2026 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use nym_crypto::asymmetric::{ed25519, x25519}; +use std::sync::Arc; + +/// Representation of a local Lewes Protocol peer +/// encapsulating all the known information and keys. +#[derive(Debug, Clone)] +pub struct LpLocalPeer { + /// Local Ed25519 keys for PSQ authentication + pub(crate) ed25519: Arc, + + /// Local x25519 keys (Noise static key) + pub(crate) x25519: Arc, + + /// Local KEM key used for PSQ + pub(crate) kem_psq: Option>, +} + +impl LpLocalPeer { + pub fn new(ed25519: Arc, x25519: Arc) -> Self { + LpLocalPeer { + ed25519, + x25519, + kem_psq: None, + } + } + + #[must_use] + pub fn with_kem_psq_key(mut self, key: Arc) -> Self { + self.kem_psq = Some(key); + self + } + + pub fn ed25519(&self) -> &Arc { + &self.ed25519 + } + + pub fn x25519(&self) -> &Arc { + &self.x25519 + } + + /// Convert this `LpLocalPeer` into a valid `LpRemotePeer` that can be used within tests + #[doc(hidden)] + pub fn as_remote(&self) -> LpRemotePeer { + let expected_kem_key_digest = match &self.kem_psq { + None => Vec::new(), + Some(kem_keys) => nym_kkt::key_utils::hash_key_bytes( + &nym_kkt::ciphersuite::HashFunction::Blake3, + nym_kkt::ciphersuite::DEFAULT_HASH_LEN, + kem_keys.public_key().as_bytes(), + ), + }; + LpRemotePeer { + ed25519_public: *self.ed25519.public_key(), + x25519_public: *self.x25519.public_key(), + expected_kem_key_digest, + } + } + + // this is only exposed in tests as ideally we should be storing the proper types to begin with + #[cfg(test)] + pub fn encapsulate_kem_key(&self) -> Option> { + let pk_bytes = self.kem_psq.as_ref()?.public_key().to_bytes(); + let libcrux_pk = + libcrux_kem::PublicKey::decode(libcrux_kem::Algorithm::X25519, &pk_bytes).ok()?; + + Some(nym_kkt::ciphersuite::EncapsulationKey::X25519(libcrux_pk)) + } +} + +/// Representation of a remote Lewes Protocol peer +/// encapsulating all the known information and keys. +#[derive(Debug, Clone)] +pub struct LpRemotePeer { + /// Remote Ed25519 public key for PSQ authentication + pub(crate) ed25519_public: ed25519::PublicKey, + + /// Remote X25519 public key (Noise static key) + pub(crate) x25519_public: x25519::PublicKey, + + /// Expected digest of the remote's KEM key + // TODO: this might have to be replaced by a HashMap> instead + pub(crate) expected_kem_key_digest: Vec, +} + +impl LpRemotePeer { + pub fn new(ed25519_public: ed25519::PublicKey, x25519_public: x25519::PublicKey) -> Self { + LpRemotePeer { + ed25519_public, + x25519_public, + expected_kem_key_digest: vec![], + } + } + + pub fn ed25519(&self) -> ed25519::PublicKey { + self.ed25519_public + } + + pub fn x25519(&self) -> x25519::PublicKey { + self.x25519_public + } + + #[must_use] + pub fn with_kem_key_digest(mut self, expected_kem_key_digest: Vec) -> Self { + self.expected_kem_key_digest = expected_kem_key_digest; + self + } +} + +#[cfg(test)] +pub fn mock_peer() -> LpLocalPeer { + // use deterministic rng + let mut rng = nym_test_utils::helpers::deterministic_rng(); + random_peer(&mut rng) +} + +#[cfg(test)] +pub fn random_peer(rng: &mut R) -> LpLocalPeer { + let ed25519 = Arc::new(ed25519::KeyPair::new(rng)); + let x25519 = Arc::new(ed25519.to_x25519()); + let kem_psq = Some(x25519.clone()); + + LpLocalPeer { + ed25519, + x25519, + kem_psq, + } +} + +#[cfg(test)] +pub fn mock_peers() -> (LpLocalPeer, LpLocalPeer) { + // use deterministic rng + let mut rng = nym_test_utils::helpers::deterministic_rng(); + + (random_peer(&mut rng), random_peer(&mut rng)) +} diff --git a/common/nym-lp/src/session.rs b/common/nym-lp/src/session.rs index 774f148146..024df03162 100644 --- a/common/nym-lp/src/session.rs +++ b/common/nym-lp/src/session.rs @@ -10,19 +10,19 @@ use crate::codec::OuterAeadKey; use crate::message::{EncryptedDataPayload, HandshakeData}; use crate::noise_protocol::{NoiseError, NoiseProtocol, ReadResult}; use crate::packet::LpHeader; +use crate::peer::{LpLocalPeer, LpRemotePeer}; use crate::psk::{ derive_subsession_psk, psq_initiator_create_message, psq_responder_process_message, }; use crate::replay::ReceivingKeyCounterValidator; use crate::{LpError, LpMessage, LpPacket}; -use nym_crypto::asymmetric::{ed25519, x25519}; +use nym_crypto::asymmetric::x25519; use nym_kkt::ciphersuite::{DecapsulationKey, EncapsulationKey}; use nym_kkt::encryption::KKTSessionSecret; -use nym_kkt::kkt::decrypt_kkt_response_frame; +use nym_kkt::kkt::validate_kem_response; use parking_lot::Mutex; use rand::RngCore; use snow::Builder; -use std::sync::Arc; use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; use std::time::{SystemTime, UNIX_EPOCH}; use zeroize::{Zeroize, ZeroizeOnDrop}; @@ -124,7 +124,7 @@ pub enum PSQState { NotStarted, /// Initiator has sent PSQ ciphertext and is waiting for confirmation. - /// PSK is already derived but we don't encrypt outgoing packets yet + /// PSK is already derived, but we don't encrypt outgoing packets yet /// because the responder may not have processed our message yet. InitiatorWaiting { /// The derived PSK, stored until we transition to Completed @@ -183,18 +183,13 @@ pub struct LpSession { /// This prevents transport mode operations from running with the insecure dummy `[0u8; 32]` PSK. psk_injected: AtomicBool, - // PSQ-related keys stored for handshake - /// Local Ed25519 keys for PSQ authentication - local_ed25519: Arc, + /// Representation of a local Lewes Protocol peer + /// encapsulating all the known information and keys. + local_peer: LpLocalPeer, - /// Remote Ed25519 public key for PSQ authentication - remote_ed25519_public: ed25519::PublicKey, - - /// Local x25519 keys (Noise static key) - local_x25519: Arc, - - /// Remote X25519 public key (Noise static key) - remote_x25519_public: x25519::PublicKey, + /// Representation of a remote Lewes Protocol peer + /// encapsulating all the known information and keys. + remote_peer: LpRemotePeer, /// Salt for PSK derivation salt: [u8; 32], @@ -288,7 +283,7 @@ impl LpSession { /// This is used for KKT protocol when the responder needs to send their /// KEM public key in the KKT response. pub fn local_x25519_public(&self) -> x25519::PublicKey { - *self.local_x25519.public_key() + *self.local_peer.x25519.public_key() } /// Returns the remote X25519 public key. @@ -296,7 +291,7 @@ impl LpSession { /// Used for tie-breaking in simultaneous subsession initiation. /// Lower key loses and becomes responder. pub fn remote_x25519_public(&self) -> &x25519::PublicKey { - &self.remote_x25519_public + &self.remote_peer.x25519_public } /// Returns the outer AEAD key for packet encryption/decryption. @@ -349,20 +344,21 @@ impl LpSession { /// /// * `id` - Session identifier /// * `is_initiator` - True if this side initiates the Noise handshake. - /// * `local_ed25519_keypair` - This side's Ed25519 keypair for PSQ authentication - /// * `local_x25519_keypair` - This side's X25519 keypair for Noise protocol and DHKEM - /// * `remote_ed25519_key` - Peer's Ed25519 public key for PSQ authentication - /// * `remote_x25519_key` - Peer's X25519 public key for Noise protocol and DHKEM + /// * `local_peer` - This side's LP peer's keys + /// * `remote_peer` - The remote's LP peer's keys /// * `salt` - Salt for PSK derivation pub fn new( id: u32, is_initiator: bool, - local_ed25519_keypair: Arc, - local_x25519_keypair: Arc, - remote_ed25519_key: &ed25519::PublicKey, - remote_x25519_key: &x25519::PublicKey, + local_peer: LpLocalPeer, + remote_peer: LpRemotePeer, salt: &[u8; 32], ) -> Result { + // if we're LP responder, we **must** set our kem key + if !is_initiator && local_peer.kem_psq.is_none() { + return Err(LpError::ResponderWithMissingKEMKey); + } + // XKpsk3 pattern requires remote static key known upfront (XK) // and PSK mixed at position 3. This provides forward secrecy with PSK authentication. let pattern_name = crate::NOISE_PATTERN; @@ -371,10 +367,10 @@ impl LpSession { let params = pattern_name.parse()?; let builder = Builder::new(params); - let local_key_bytes = local_x25519_keypair.private_key().as_bytes(); + let local_key_bytes = local_peer.x25519.private_key().as_bytes(); let builder = builder.local_private_key(local_key_bytes); - let remote_key_bytes = remote_x25519_key.to_bytes(); + let remote_key_bytes = remote_peer.x25519_public.to_bytes(); let builder = builder.remote_public_key(&remote_key_bytes); // Initialize with dummy PSK - real PSK will be injected via set_psk() during handshake @@ -410,10 +406,8 @@ impl LpSession { sending_counter: AtomicU64::new(0), receiving_counter: Mutex::new(ReceivingKeyCounterValidator::default()), psk_injected: AtomicBool::new(false), - local_ed25519: local_ed25519_keypair.clone(), - remote_ed25519_public: *remote_ed25519_key, - local_x25519: local_x25519_keypair, - remote_x25519_public: *remote_x25519_key, + local_peer, + remote_peer, salt: *salt, outer_aead_key: Mutex::new(None), pq_shared_secret: Mutex::new(None), @@ -503,6 +497,50 @@ impl LpSession { self.psk_handle.lock().clone() } + /// Returns the reference to the KEM Public key of the peer (if available). + pub fn get_kem_key_handle(&self) -> Result<&x25519::PublicKey, LpError> { + self.local_peer + .kem_psq + .as_ref() + .map(|kp| kp.public_key()) + .ok_or(LpError::ResponderWithMissingKEMKey) + } + + // TODO: missing Zeroize + /// Convert local KEM PSQ keys to typed EncapsulationKey / EncapsulationKey + pub fn encapsulated_kem_keys( + &self, + ) -> Result<(DecapsulationKey<'_>, EncapsulationKey<'_>), LpError> { + let kem_keys = self + .local_peer + .kem_psq + .as_ref() + .ok_or(LpError::ResponderWithMissingKEMKey)?; + + let libcrux_private_key = libcrux_kem::PrivateKey::decode( + libcrux_kem::Algorithm::X25519, + kem_keys.private_key().as_bytes(), + ) + .map_err(|e| { + LpError::KKTError(format!( + "Failed to convert X25519 private key to libcrux PrivateKey: {e:?}", + )) + })?; + let dec_key = DecapsulationKey::X25519(libcrux_private_key); + + let libcrux_public_key = libcrux_kem::PublicKey::decode( + libcrux_kem::Algorithm::X25519, + kem_keys.public_key().as_bytes(), + ) + .map_err(|e| { + LpError::KKTError(format!( + "Failed to convert X25519 public key to libcrux PublicKey: {e:?}", + )) + })?; + let enc_key = EncapsulationKey::X25519(libcrux_public_key); + Ok((dec_key, enc_key)) + } + /// Prepares a KKT (KEM Key Transfer) request message. /// /// This should be called by the initiator before starting the Noise handshake @@ -552,8 +590,8 @@ impl LpSession { match request_kem_key( &mut rng, ciphersuite, - self.local_ed25519.private_key(), - &self.remote_x25519_public, + self.local_peer.ed25519.private_key(), + &self.remote_peer.x25519_public, ) { Ok((session_secret, context, request_bytes)) => { // Store context for response validation @@ -582,29 +620,14 @@ impl LpSession { /// # Arguments /// /// * `response_bytes` - Raw KKT response message from responder - /// * `expected_key_hash` - Optional expected hash of responder's KEM key. - /// - `Some(hash)`: Full KKT validation (signature + hash) - use when directory service available - /// - `None`: Signature-only validation (hash computed from received key) - temporary mode + /// * `expected_key_hash` - Expected hash of responder's KEM key. /// /// # Returns /// /// * `Ok(())` - KKT exchange completed, KEM key stored /// * `Err(LpError)` - Signature verification failed, hash mismatch, or invalid state /// - /// # Note - /// - /// When None is passed, the function computes the hash from the received key and validates against - /// that (effectively signature-only mode). This allows easy upgrade: just pass Some(directory_hash) - /// when directory service becomes available. The full KKT protocol with hash pinning provides - /// protection against key substitution attacks. - pub fn process_kkt_response( - &self, - response_bytes: &[u8], - expected_key_hash: Option<&[u8]>, - ) -> Result<(), LpError> { - use nym_kkt::key_utils::hash_encapsulation_key; - use nym_kkt::kkt::validate_kem_response; - + pub fn process_kkt_response(&self, response_bytes: &[u8]) -> Result<(), LpError> { let mut kkt_state = self.kkt_state.lock(); // Extract context from waiting state @@ -620,33 +643,12 @@ impl LpSession { } }; - // Determine hash to validate against - let hash_for_validation: Vec; - let hash_ref = match expected_key_hash { - Some(hash) => hash, - None => { - // Signature-only mode: extract key from response and compute its hash - // This effectively bypasses hash validation while keeping signature validation - let (frame, _) = decrypt_kkt_response_frame(&session_secret, response_bytes) - .map_err(|e| { - LpError::Internal(format!("Failed to decrypt KKT response: {:?}", e)) - })?; - - hash_for_validation = hash_encapsulation_key( - &context.ciphersuite().hash_function(), - context.ciphersuite().hash_len(), - frame.body_ref(), - ); - &hash_for_validation - } - }; - // Validate response and extract KEM key let kem_pk = validate_kem_response( &mut context, &session_secret, - &self.remote_ed25519_public, - hash_ref, + &self.remote_peer.ed25519_public, + &self.remote_peer.expected_kem_key_digest, response_bytes, ) .map_err(|e| LpError::Internal(format!("KKT response validation failed: {:?}", e)))?; @@ -688,9 +690,9 @@ impl LpSession { let response_bytes = handle_kem_request( &mut rng, request_bytes, - Some(&self.remote_ed25519_public), // Verify initiator signature - self.local_ed25519.private_key(), // Sign response - self.local_x25519.private_key(), + Some(&self.remote_peer.ed25519_public), // Verify initiator signature + self.local_peer.ed25519.private_key(), // Sign response + self.local_peer.x25519.private_key(), responder_kem_pk, ) .map_err(|e| LpError::Internal(format!("KKT request handling failed: {:?}", e)))?; @@ -742,11 +744,11 @@ impl LpSession { let session_context = self.id.to_le_bytes(); let psq_result = match psq_initiator_create_message( - self.local_x25519.private_key(), - &self.remote_x25519_public, + self.local_peer.x25519.private_key(), + &self.remote_peer.x25519_public, remote_kem, - self.local_ed25519.private_key(), - self.local_ed25519.public_key(), + self.local_peer.ed25519.private_key(), + self.local_peer.ed25519.public_key(), &self.salt, &session_context, ) { @@ -879,42 +881,16 @@ impl LpSession { let psq_payload = &payload[2..2 + psq_len]; let noise_payload = &payload[2 + psq_len..]; - // Convert X25519 local keys to DecapsulationKey/EncapsulationKey (DHKEM) - let local_private_bytes = &self.local_x25519.private_key().to_bytes(); - let libcrux_private_key = libcrux_kem::PrivateKey::decode( - libcrux_kem::Algorithm::X25519, - local_private_bytes, - ) - .map_err(|e| { - LpError::KKTError(format!( - "Failed to convert X25519 private key to libcrux PrivateKey: {:?}", - e - )) - })?; - let dec_key = DecapsulationKey::X25519(libcrux_private_key); - - let local_public_key = self.local_x25519_public(); - let local_public_bytes = local_public_key.as_bytes(); - let libcrux_public_key = libcrux_kem::PublicKey::decode( - libcrux_kem::Algorithm::X25519, - local_public_bytes, - ) - .map_err(|e| { - LpError::KKTError(format!( - "Failed to convert X25519 public key to libcrux PublicKey: {:?}", - e - )) - })?; - let enc_key = EncapsulationKey::X25519(libcrux_public_key); + let (dec_key, enc_key) = self.encapsulated_kem_keys()?; // Decapsulate PSK from PSQ payload using X25519 as DHKEM let session_context = self.id.to_le_bytes(); let psq_result = match psq_responder_process_message( - self.local_x25519.private_key(), - &self.remote_x25519_public, + self.local_peer.x25519.private_key(), + &self.remote_peer.x25519_public, (&dec_key, &enc_key), - &self.remote_ed25519_public, + &self.remote_peer.ed25519_public, psq_payload, &self.salt, &session_context, @@ -1170,8 +1146,8 @@ impl LpSession { let pattern_name = "Noise_KKpsk0_25519_ChaChaPoly_SHA256"; let params = pattern_name.parse()?; - let local_key_bytes = self.local_x25519.private_key().to_bytes(); - let remote_key_bytes = self.remote_x25519_public.to_bytes(); + let local_key_bytes = self.local_peer.x25519.private_key().to_bytes(); + let remote_key_bytes = self.remote_x25519_public().to_bytes(); let builder = Builder::new(params) .local_private_key(&local_key_bytes) @@ -1188,13 +1164,10 @@ impl LpSession { index: subsession_index, noise_state: Mutex::new(NoiseProtocol::new(handshake_state)), is_initiator, - // Copy key material from parent for into_session() conversion - local_ed25519: self.local_ed25519.clone(), - remote_ed25519_public: self.remote_ed25519_public, - remote_x25519_public: self.remote_x25519_public, + local_peer: self.local_peer.clone(), + remote_peer: self.remote_peer.clone(), pq_shared_secret: PqSharedSecret::new(pq_secret), subsession_psk, - local_x25519: self.local_x25519.clone(), }) } } @@ -1224,17 +1197,14 @@ pub struct SubsessionHandshake { is_initiator: bool, // Key material inherited from parent session for into_session() conversion - /// Local Ed25519 keys (for PSQ auth if needed) - local_ed25519: Arc, + /// Representation of a local Lewes Protocol peer + /// encapsulating all the known information and keys. + local_peer: LpLocalPeer, - /// Local x25519 keys (Noise static key) - local_x25519: Arc, + /// Representation of a remote Lewes Protocol peer + /// encapsulating all the known information and keys. + remote_peer: LpRemotePeer, - /// Remote Ed25519 public key - remote_ed25519_public: ed25519::PublicKey, - - /// Remote X25519 public key (Noise static key) - remote_x25519_public: x25519::PublicKey, /// PQ shared secret inherited from parent (for creating further subsessions) pq_shared_secret: PqSharedSecret, /// Subsession PSK (for deriving outer AEAD key) @@ -1327,10 +1297,8 @@ impl SubsessionHandshake { sending_counter: AtomicU64::new(0), receiving_counter: Mutex::new(ReceivingKeyCounterValidator::new(0)), psk_injected: AtomicBool::new(true), // PSK was in KKpsk0 - local_ed25519: self.local_ed25519, - remote_ed25519_public: self.remote_ed25519_public, - local_x25519: self.local_x25519, - remote_x25519_public: self.remote_x25519_public, + local_peer: self.local_peer, + remote_peer: self.remote_peer, salt, outer_aead_key: Mutex::new(Some(outer_key)), pq_shared_secret: Mutex::new(Some(self.pq_shared_secret)), @@ -1346,54 +1314,37 @@ impl SubsessionHandshake { #[cfg(test)] mod tests { use super::*; + use crate::peer::mock_peers; use crate::{replay::ReplayError, sessions_for_tests}; + use nym_crypto::asymmetric::ed25519; use rand::thread_rng; // Helper function to generate keypairs for tests - fn generate_keypair() -> x25519::KeyPair { + fn generate_x25519_keypair() -> x25519::KeyPair { x25519::KeyPair::new(&mut thread_rng()) } // Helper function to create a session with real keys for handshake tests - fn create_handshake_test_session( - receiver_index: u32, - is_initiator: bool, - local_keys: &x25519::KeyPair, - remote_pub_key: &x25519::PublicKey, - ) -> LpSession { - use nym_crypto::asymmetric::ed25519; + fn create_handshake_test_session(receiver_index: u32, is_initiator: bool) -> LpSession { + let (keys_1, keys_2) = mock_peers(); // Create Ed25519 keypairs that correspond to initiator/responder roles // Initiator uses [1u8], Responder uses [2u8] - let (local_ed25519_seed, remote_ed25519_seed) = if is_initiator { - ([1u8; 32], [2u8; 32]) + let (local, remote) = if is_initiator { + (keys_1, keys_2.as_remote()) } else { - ([2u8; 32], [1u8; 32]) + (keys_2, keys_1.as_remote()) }; - let local_ed25519 = ed25519::KeyPair::from_secret(local_ed25519_seed, 0); - let local_x25519 = x25519::PrivateKey::from_bytes(local_keys.private_key().as_bytes()) - .unwrap() - .into(); - let remote_ed25519 = ed25519::KeyPair::from_secret(remote_ed25519_seed, 1); - let salt = [0u8; 32]; // Test salt // PSQ will derive the PSK during handshake using X25519 as DHKEM - let session = LpSession::new( - receiver_index, - is_initiator, - Arc::new(local_ed25519), - Arc::new(local_x25519), - remote_ed25519.public_key(), - remote_pub_key, - &salt, - ) - .expect("Test session creation failed"); + let session = LpSession::new(receiver_index, is_initiator, local, remote.clone(), &salt) + .expect("Test session creation failed"); // Initialize KKT state to Completed for tests (bypasses KKT exchange) // This simulates having already received the remote party's KEM key via KKT - session.set_kkt_completed_for_test(remote_pub_key); + session.set_kkt_completed_for_test(&remote.x25519_public); session } @@ -1489,21 +1440,13 @@ mod tests { #[test] fn test_prepare_handshake_message_initial_state() { - let initiator_keys = Arc::new(generate_keypair()); - let responder_keys = Arc::new(generate_keypair()); let receiver_index = 12345u32; - let initiator_session = create_handshake_test_session( - receiver_index, - true, - &initiator_keys, - responder_keys.public_key(), - ); + let initiator_session = create_handshake_test_session(receiver_index, true); let responder_session = create_handshake_test_session( receiver_index, false, - &responder_keys, - initiator_keys.public_key(), // Responder also needs initiator's key for XK + // Responder also needs initiator's key for XK ); // Initiator should have a message to send immediately (-> e) @@ -1521,22 +1464,10 @@ mod tests { #[test] fn test_process_handshake_message_first_step() { - let initiator_keys = Arc::new(generate_keypair()); - let responder_keys = Arc::new(generate_keypair()); let receiver_index = 12345u32; - let initiator_session = create_handshake_test_session( - receiver_index, - true, - &initiator_keys, - responder_keys.public_key(), - ); - let responder_session = create_handshake_test_session( - receiver_index, - false, - &responder_keys, - initiator_keys.public_key(), - ); + let initiator_session = create_handshake_test_session(receiver_index, true); + let responder_session = create_handshake_test_session(receiver_index, false); // 1. Initiator prepares the first message (-> e) let initiator_msg_result = initiator_session.prepare_handshake_message(); @@ -1567,21 +1498,8 @@ mod tests { #[test] fn test_handshake_driver_simulation() { - let initiator_keys = Arc::new(generate_keypair()); - let responder_keys = Arc::new(generate_keypair()); - - let initiator_session = create_handshake_test_session( - 12345u32, - true, - &initiator_keys, - responder_keys.public_key(), - ); - let responder_session = create_handshake_test_session( - 12345u32, - false, - &responder_keys, - initiator_keys.public_key(), - ); + let initiator_session = create_handshake_test_session(12345u32, true); + let responder_session = create_handshake_test_session(12345u32, false); let mut responder_to_initiator_msg = None; let mut rounds = 0; @@ -1662,21 +1580,9 @@ mod tests { #[test] fn test_encrypt_decrypt_after_handshake() { // --- Setup Handshake --- - let initiator_keys = Arc::new(generate_keypair()); - let responder_keys = Arc::new(generate_keypair()); - let initiator_session = create_handshake_test_session( - 12345u32, - true, - &initiator_keys, - responder_keys.public_key(), - ); - let responder_session = create_handshake_test_session( - 12345u32, - false, - &responder_keys, - initiator_keys.public_key(), - ); + let initiator_session = create_handshake_test_session(12345u32, true); + let responder_session = create_handshake_test_session(12345u32, false); // Drive handshake to completion (simplified loop from previous test) let mut i_msg = initiator_session @@ -1731,15 +1637,7 @@ mod tests { #[test] fn test_encrypt_decrypt_before_handshake() { - let initiator_keys = Arc::new(generate_keypair()); - let responder_keys = Arc::new(generate_keypair()); - - let initiator_session = create_handshake_test_session( - 12345u32, - true, - &initiator_keys, - responder_keys.public_key(), - ); + let initiator_session = create_handshake_test_session(12345u32, true); assert!(!initiator_session.is_handshake_complete()); @@ -1807,21 +1705,8 @@ mod tests { /// Test that PSQ runs during handshake and derives a PSK #[test] fn test_psq_handshake_runs_with_psk_injection() { - let initiator_keys = Arc::new(generate_keypair()); - let responder_keys = Arc::new(generate_keypair()); - - let initiator_session = create_handshake_test_session( - 12345u32, - true, - &initiator_keys, - responder_keys.public_key(), - ); - let responder_session = create_handshake_test_session( - 12345u32, - false, - &responder_keys, - initiator_keys.public_key(), - ); + let initiator_session = create_handshake_test_session(12345u32, true); + let responder_session = create_handshake_test_session(12345u32, false); // Drive the handshake let mut i_msg = initiator_session @@ -1884,8 +1769,8 @@ mod tests { fn test_x25519_to_kem_conversion() { use nym_kkt::ciphersuite::EncapsulationKey; - let initiator_keys = Arc::new(generate_keypair()); - let responder_keys = Arc::new(generate_keypair()); + let initiator_keys = generate_x25519_keypair(); + let responder_keys = generate_x25519_keypair(); // Verify we can convert X25519 public key to KEM format (as done in session.rs) let x25519_public_bytes = responder_keys.public_key().as_bytes(); @@ -1908,22 +1793,9 @@ mod tests { /// Test that PSQ actually derives a different PSK (not using dummy) #[test] fn test_psq_derived_psk_differs_from_dummy() { - let initiator_keys = Arc::new(generate_keypair()); - let responder_keys = Arc::new(generate_keypair()); - // Create sessions - they start with dummy PSK [0u8; 32] - let initiator_session = create_handshake_test_session( - 12345u32, - true, - &initiator_keys, - responder_keys.public_key(), - ); - let responder_session = create_handshake_test_session( - 12345u32, - false, - &responder_keys, - initiator_keys.public_key(), - ); + let initiator_session = create_handshake_test_session(12345u32, true); + let responder_session = create_handshake_test_session(12345u32, false); // Prepare first message (initiator runs PSQ and injects PSK) let i_msg = initiator_session @@ -1982,21 +1854,8 @@ mod tests { /// Test full end-to-end handshake with PSQ integration #[test] fn test_handshake_with_psq_end_to_end() { - let initiator_keys = Arc::new(generate_keypair()); - let responder_keys = Arc::new(generate_keypair()); - - let initiator_session = create_handshake_test_session( - 12345u32, - true, - &initiator_keys, - responder_keys.public_key(), - ); - let responder_session = create_handshake_test_session( - 12345u32, - false, - &responder_keys, - initiator_keys.public_key(), - ); + let initiator_session = create_handshake_test_session(12345u32, true); + let responder_session = create_handshake_test_session(12345u32, false); // Verify initial state assert!(!initiator_session.is_handshake_complete()); @@ -2068,22 +1927,9 @@ mod tests { /// Test that Ed25519 keys are used in PSQ authentication #[test] fn test_psq_handshake_uses_ed25519_authentication() { - let initiator_keys = Arc::new(generate_keypair()); - let responder_keys = Arc::new(generate_keypair()); - // Create sessions with explicit Ed25519 keys - let initiator_session = create_handshake_test_session( - 12345u32, - true, - &initiator_keys, - responder_keys.public_key(), - ); - let responder_session = create_handshake_test_session( - 12345u32, - false, - &responder_keys, - initiator_keys.public_key(), - ); + let initiator_session = create_handshake_test_session(12345u32, true); + let responder_session = create_handshake_test_session(12345u32, false); // Verify sessions store Ed25519 keys // (Internal verification - keys are used in PSQ calls) @@ -2122,15 +1968,8 @@ mod tests { #[test] fn test_psq_deserialization_failure() { // Test that corrupted PSQ payload causes clean abort - let responder_keys = generate_keypair(); - let initiator_keys = generate_keypair(); - let responder_session = create_handshake_test_session( - 12345u32, - false, - &responder_keys, - initiator_keys.public_key(), - ); + let responder_session = create_handshake_test_session(12345u32, false); // Create a handshake message with corrupted PSQ payload let corrupted_psq_data = vec![0xFF; 128]; // Random garbage @@ -2150,45 +1989,30 @@ mod tests { #[test] fn test_handshake_abort_on_psq_failure() { - // Test that Ed25519 auth failure causes handshake abort - let initiator_keys = Arc::new(generate_keypair()); - let responder_keys = Arc::new(generate_keypair()); + let (init, resp) = mock_peers(); + + let mut bad_resp = resp.as_remote(); + let wrong_ed25519 = ed25519::KeyPair::from_secret([99u8; 32], 99); // Different key! + bad_resp.ed25519_public = *wrong_ed25519.public_key(); + + let mut bad_init = init.as_remote(); + bad_init.ed25519_public = *wrong_ed25519.public_key(); // Create sessions with MISMATCHED Ed25519 keys // This simulates authentication failure - let initiator_ed25519 = ed25519::KeyPair::from_secret([1u8; 32], 0); - let wrong_ed25519 = ed25519::KeyPair::from_secret([99u8; 32], 99); // Different key! - let receiver_index: u32 = 55555; let salt = [0u8; 32]; - let initiator_session = LpSession::new( - receiver_index, - true, - Arc::new(initiator_ed25519), - initiator_keys.clone(), - wrong_ed25519.public_key(), // Responder expects THIS key - responder_keys.public_key(), - &salt, - ) - .unwrap(); - // Initialize KKT state for test - initiator_session.set_kkt_completed_for_test(responder_keys.public_key()); + let initiator_session = + LpSession::new(receiver_index, true, init.clone(), bad_resp, &salt).unwrap(); - let responder_ed25519 = ed25519::KeyPair::from_secret([2u8; 32], 1); - - let responder_session = LpSession::new( - receiver_index, - false, - Arc::new(responder_ed25519), - responder_keys.clone(), - wrong_ed25519.public_key(), // Expects WRONG key (not initiator's) - initiator_keys.public_key(), - &salt, - ) - .unwrap(); // Initialize KKT state for test - responder_session.set_kkt_completed_for_test(initiator_keys.public_key()); + initiator_session.set_kkt_completed_for_test(resp.x25519.public_key()); + + let responder_session = + LpSession::new(receiver_index, false, resp, bad_init, &salt).unwrap(); + // Initialize KKT state for test + responder_session.set_kkt_completed_for_test(init.x25519.public_key()); // Initiator prepares message (should succeed - signing works) let msg1 = initiator_session @@ -2212,48 +2036,24 @@ mod tests { #[test] fn test_psq_invalid_signature() { - // Test Ed25519 signature validation specifically - // Setup with matching X25519 keys but mismatched Ed25519 keys - let initiator_keys = Arc::new(generate_keypair()); - let responder_keys = Arc::new(generate_keypair()); + let (init, resp) = mock_peers(); - // Initiator uses Ed25519 key [1u8] - let initiator_ed25519 = ed25519::KeyPair::from_secret([1u8; 32], 0); - - // Responder expects Ed25519 key [99u8] (wrong!) - let wrong_ed25519_keypair = ed25519::KeyPair::from_secret([99u8; 32], 99); - let wrong_ed25519_public = wrong_ed25519_keypair.public_key(); + let mut bad_init = init.as_remote(); + let wrong_ed25519 = ed25519::KeyPair::from_secret([99u8; 32], 99); // Different key! + bad_init.ed25519_public = *wrong_ed25519.public_key(); let receiver_index: u32 = 66666; let salt = [0u8; 32]; - let initiator_session = LpSession::new( - receiver_index, - true, - Arc::new(initiator_ed25519), - initiator_keys.clone(), - wrong_ed25519_public, // This doesn't matter for initiator - responder_keys.public_key(), - &salt, - ) - .unwrap(); + let initiator_session = + LpSession::new(receiver_index, true, init.clone(), resp.as_remote(), &salt).unwrap(); // Initialize KKT state for test - initiator_session.set_kkt_completed_for_test(responder_keys.public_key()); + initiator_session.set_kkt_completed_for_test(resp.x25519.public_key()); - let responder_ed25519 = ed25519::KeyPair::from_secret([2u8; 32], 1); - - let responder_session = LpSession::new( - receiver_index, - false, - Arc::new(responder_ed25519), - responder_keys.clone(), - wrong_ed25519_public, // Responder expects WRONG key - initiator_keys.public_key(), - &salt, - ) - .unwrap(); + let responder_session = + LpSession::new(receiver_index, false, resp, bad_init, &salt).unwrap(); // Initialize KKT state for test - responder_session.set_kkt_completed_for_test(initiator_keys.public_key()); + responder_session.set_kkt_completed_for_test(init.x25519.public_key()); // Initiator creates message with valid signature (signed with [1u8]) let msg = initiator_session @@ -2279,15 +2079,7 @@ mod tests { #[test] fn test_psq_state_unchanged_on_error() { // Verify that PSQ errors leave session in clean state - let responder_keys = generate_keypair(); - let initiator_keys = generate_keypair(); - - let responder_session = create_handshake_test_session( - 12345u32, - false, - &responder_keys, - initiator_keys.public_key(), - ); + let responder_session = create_handshake_test_session(12345u32, false); // Capture initial PSQ state (should be ResponderWaiting) // (We can't directly access psq_state, but we can verify behavior) @@ -2304,12 +2096,7 @@ mod tests { // Session should still be functional - can process valid messages // Create a proper initiator to send valid message - let initiator_session = create_handshake_test_session( - 12345u32, - true, - &initiator_keys, - responder_keys.public_key(), - ); + let initiator_session = create_handshake_test_session(12345u32, true); let valid_msg = initiator_session .prepare_handshake_message() @@ -2331,16 +2118,8 @@ mod tests { // This test verifies the safety mechanism that prevents transport mode operations // from running with the dummy PSK if PSQ injection fails or is skipped. - let initiator_keys = Arc::new(generate_keypair()); - let responder_keys = Arc::new(generate_keypair()); - // Create session but don't complete handshake (no PSK injection will occur) - let session = create_handshake_test_session( - 12345u32, - true, - &initiator_keys, - responder_keys.public_key(), - ); + let session = create_handshake_test_session(12345u32, true); // Verify session was created successfully assert!(!session.is_handshake_complete()); @@ -2380,15 +2159,7 @@ mod tests { #[test] fn test_demote_sets_read_only() { - let initiator_keys = Arc::new(generate_keypair()); - let responder_keys = Arc::new(generate_keypair()); - - let session = create_handshake_test_session( - 12345u32, - true, - &initiator_keys, - responder_keys.public_key(), - ); + let session = create_handshake_test_session(12345u32, true); // Initially not read-only assert!(!session.is_read_only()); @@ -2405,21 +2176,9 @@ mod tests { #[test] fn test_encrypt_fails_after_demotion() { // --- Setup Handshake --- - let initiator_keys = Arc::new(generate_keypair()); - let responder_keys = Arc::new(generate_keypair()); - let initiator_session = create_handshake_test_session( - 12345u32, - true, - &initiator_keys, - responder_keys.public_key(), - ); - let responder_session = create_handshake_test_session( - 12345u32, - false, - &responder_keys, - initiator_keys.public_key(), - ); + let initiator_session = create_handshake_test_session(12345u32, true); + let responder_session = create_handshake_test_session(12345u32, false); // Drive handshake to completion let i_msg = initiator_session @@ -2461,21 +2220,9 @@ mod tests { #[test] fn test_decrypt_works_after_demotion() { // --- Setup Handshake --- - let initiator_keys = Arc::new(generate_keypair()); - let responder_keys = Arc::new(generate_keypair()); - let initiator_session = create_handshake_test_session( - 12345u32, - true, - &initiator_keys, - responder_keys.public_key(), - ); - let responder_session = create_handshake_test_session( - 12345u32, - false, - &responder_keys, - initiator_keys.public_key(), - ); + let initiator_session = create_handshake_test_session(12345u32, true); + let responder_session = create_handshake_test_session(12345u32, false); // Drive handshake to completion let i_msg = initiator_session diff --git a/common/nym-lp/src/session_integration/mod.rs b/common/nym-lp/src/session_integration/mod.rs index 74f4dfdfb4..833634e670 100644 --- a/common/nym-lp/src/session_integration/mod.rs +++ b/common/nym-lp/src/session_integration/mod.rs @@ -8,8 +8,6 @@ mod tests { session_manager::SessionManager, }; use bytes::BytesMut; - use nym_crypto::asymmetric::{ed25519, x25519}; - use std::sync::Arc; // Function to create a test packet - similar to how it's done in codec.rs tests fn create_test_packet( @@ -50,29 +48,7 @@ mod tests { let session_manager_2 = SessionManager::new(); // 2. Generate Ed25519 keypairs for PSQ authentication - let ed25519_keypair_a = ed25519::KeyPair::from_secret([1u8; 32], 0); - let ed25519_keypair_b = ed25519::KeyPair::from_secret([2u8; 32], 1); - - let x25519_keypair_a = ed25519_keypair_a.to_x25519(); - let x25519_keypair_b = ed25519_keypair_b.to_x25519(); - - let ed25519_pubkey_a = *ed25519_keypair_a.public_key(); - - // Derive X25519 keys from Ed25519 (needed for KKT init test) - let x25519_pub_a = ed25519_keypair_a - .public_key() - .to_x25519() - .expect("Failed to derive X25519 from Ed25519"); - let x25519_pub_b = ed25519_keypair_b - .public_key() - .to_x25519() - .expect("Failed to derive X25519 from Ed25519"); - - // Convert to LP keypair types - let lp_pub_a = x25519::PublicKey::from_bytes(x25519_pub_a.as_bytes()) - .expect("Failed to create PublicKey from bytes"); - let lp_pub_b = x25519::PublicKey::from_bytes(x25519_pub_b.as_bytes()) - .expect("Failed to create PublicKey from bytes"); + let (a, b) = mock_peers(); // Use fixed receiver_index for deterministic test let receiver_index: u32 = 100001; @@ -82,25 +58,11 @@ mod tests { // 4. Create sessions using the pre-built Noise states let peer_a_sm = session_manager_1 - .create_session_state_machine( - receiver_index, - Arc::new(ed25519_keypair_a), - ed25519_keypair_b.public_key(), - x25519_keypair_b.public_key(), - true, - &salt, - ) + .create_session_state_machine(receiver_index, true, a.clone(), b.as_remote(), &salt) .expect("Failed to create session A"); let peer_b_sm = session_manager_2 - .create_session_state_machine( - receiver_index, - Arc::new(ed25519_keypair_b), - &ed25519_pubkey_a, - x25519_keypair_a.public_key(), - false, - &salt, - ) + .create_session_state_machine(receiver_index, false, b.clone(), a.as_remote(), &salt) .expect("Failed to create session B"); // Verify session count @@ -109,10 +71,10 @@ mod tests { // Initialize KKT state for both sessions (test bypass) session_manager_1 - .init_kkt_for_test(peer_a_sm, &lp_pub_b) + .init_kkt_for_test(peer_a_sm, b.x25519.public_key()) .expect("Failed to init KKT for peer A"); session_manager_2 - .init_kkt_for_test(peer_b_sm, &lp_pub_a) + .init_kkt_for_test(peer_b_sm, a.x25519.public_key()) .expect("Failed to init KKT for peer B"); // 5. Simulate Noise Handshake (Sans-IO) @@ -510,29 +472,7 @@ mod tests { let session_manager_2 = SessionManager::new(); // 2. Generate Ed25519 keypairs for PSQ authentication - let ed25519_keypair_a = ed25519::KeyPair::from_secret([3u8; 32], 0); - let ed25519_keypair_b = ed25519::KeyPair::from_secret([4u8; 32], 1); - - let x25519_keypair_a = ed25519_keypair_a.to_x25519(); - let x25519_keypair_b = ed25519_keypair_b.to_x25519(); - - let ed25519_pubkey_a = *ed25519_keypair_a.public_key(); - - // Derive X25519 keys from Ed25519 (same as state machine does internally) - let x25519_pub_a = ed25519_keypair_a - .public_key() - .to_x25519() - .expect("Failed to derive X25519 from Ed25519"); - let x25519_pub_b = ed25519_keypair_b - .public_key() - .to_x25519() - .expect("Failed to derive X25519 from Ed25519"); - - // Convert to LP keypair types - let lp_pub_a = x25519::PublicKey::from_bytes(x25519_pub_a.as_bytes()) - .expect("Failed to create PublicKey from bytes"); - let lp_pub_b = x25519::PublicKey::from_bytes(x25519_pub_b.as_bytes()) - .expect("Failed to create PublicKey from bytes"); + let (a, b) = mock_peers(); // Use fixed receiver_index for test let receiver_index: u32 = 100002; @@ -541,32 +481,19 @@ mod tests { let salt = [43u8; 32]; let peer_a_sm = session_manager_1 - .create_session_state_machine( - receiver_index, - Arc::new(ed25519_keypair_a), - ed25519_keypair_b.public_key(), - x25519_keypair_b.public_key(), - true, - &salt, - ) - .unwrap(); + .create_session_state_machine(receiver_index, true, a.clone(), b.as_remote(), &salt) + .expect("Failed to create session A"); + let peer_b_sm = session_manager_2 - .create_session_state_machine( - receiver_index, - Arc::new(ed25519_keypair_b), - &ed25519_pubkey_a, - x25519_keypair_a.public_key(), - false, - &salt, - ) - .unwrap(); + .create_session_state_machine(receiver_index, false, b.clone(), a.as_remote(), &salt) + .expect("Failed to create session B"); // Initialize KKT state for both sessions (test bypass) session_manager_1 - .init_kkt_for_test(peer_a_sm, &lp_pub_b) + .init_kkt_for_test(peer_a_sm, b.x25519.public_key()) .expect("Failed to init KKT for peer A"); session_manager_2 - .init_kkt_for_test(peer_b_sm, &lp_pub_a) + .init_kkt_for_test(peer_b_sm, a.x25519.public_key()) .expect("Failed to init KKT for peer B"); // Drive handshake to completion (simplified) @@ -722,23 +649,7 @@ mod tests { let session_manager = SessionManager::new(); // Generate Ed25519 keypair for PSQ authentication - let ed25519_keypair_a = ed25519::KeyPair::from_secret([5u8; 32], 0); - let ed25519_keypair_b = ed25519::KeyPair::from_secret([6u8; 32], 0); - - let x25519_keypair_a = ed25519_keypair_a.to_x25519(); - let x25519_keypair_b = ed25519_keypair_b.to_x25519(); - - // Derive X25519 key from Ed25519 (same as state machine does internally) - let x25519_pub = ed25519_keypair_a - .public_key() - .to_x25519() - .expect("Failed to derive X25519 from Ed25519"); - - let keypair_a = Arc::new(ed25519_keypair_a); - - // Convert to LP keypair type (still needed for init_kkt_for_test below if used) - let _lp_pub = x25519::PublicKey::from_bytes(x25519_pub.as_bytes()) - .expect("Failed to create PublicKey from bytes"); + let (a, b) = mock_peers(); // Use fixed receiver_index for test let receiver_index: u32 = 100003; @@ -748,14 +659,7 @@ mod tests { // 2. Create a session (using real noise state) let _session = session_manager - .create_session_state_machine( - receiver_index, - keypair_a.clone(), - ed25519_keypair_b.public_key(), - x25519_keypair_b.public_key(), - true, - &salt, - ) + .create_session_state_machine(receiver_index, true, a.clone(), b.as_remote(), &salt) .expect("Failed to create session"); // 3. Try to get a non-existent session @@ -774,10 +678,9 @@ mod tests { let _temp_session = session_manager .create_session_state_machine( receiver_index_temp, - keypair_a.clone(), - ed25519_keypair_b.public_key(), - x25519_keypair_a.public_key(), true, + a.clone(), + b.as_remote(), &salt, ) .expect("Failed to create temp session"); @@ -830,13 +733,15 @@ mod tests { } // Remove unused imports if SessionManager methods are no longer direct dependencies // use crate::noise_protocol::{create_noise_state, create_noise_state_responder}; + use crate::peer::mock_peers; use crate::{ // Bring in state machine types state_machine::{LpAction, LpInput, LpStateBare}, // message::LpMessage, // LpMessage likely still needed for LpInput/LpAction // packet::{LpHeader, LpPacket, TRAILER_LEN}, // LpPacket needed for LpAction/LpInput }; - use bytes::Bytes; // Use Bytes for SendData input + use bytes::Bytes; + // Use Bytes for SendData input // Keep helper function for creating test packets if needed, // but LpAction::SendPacket should provide the packets now. @@ -856,14 +761,7 @@ mod tests { let session_manager_2 = SessionManager::new(); // 2. Generate Ed25519 keypairs for PSQ authentication - let ed25519_keypair_a = ed25519::KeyPair::from_secret([6u8; 32], 0); - let ed25519_keypair_b = ed25519::KeyPair::from_secret([7u8; 32], 1); - - let x25519_keypair_a = ed25519_keypair_a.to_x25519(); - let x25519_keypair_b = ed25519_keypair_b.to_x25519(); - - let pubkey_a = *ed25519_keypair_a.public_key(); - let pubkey_b = *ed25519_keypair_b.public_key(); + let (a, b) = mock_peers(); // Use fixed receiver_index for test let receiver_index: u32 = 100005; @@ -874,26 +772,12 @@ mod tests { // 3. Create sessions state machines assert!( session_manager_1 - .create_session_state_machine( - receiver_index, - Arc::new(ed25519_keypair_a), - &pubkey_b, - x25519_keypair_b.public_key(), - true, - &salt, - ) // Initiator + .create_session_state_machine(receiver_index, true, a.clone(), b.as_remote(), &salt,) // Initiator .is_ok() ); assert!( session_manager_2 - .create_session_state_machine( - receiver_index, - Arc::new(ed25519_keypair_b), - &pubkey_a, - x25519_keypair_a.public_key(), - false, - &salt, - ) // Responder + .create_session_state_machine(receiver_index, false, b, a.as_remote(), &salt,) // Responder .is_ok() ); diff --git a/common/nym-lp/src/session_manager.rs b/common/nym-lp/src/session_manager.rs index c669a9df41..41b28da1c7 100644 --- a/common/nym-lp/src/session_manager.rs +++ b/common/nym-lp/src/session_manager.rs @@ -6,13 +6,11 @@ //! This module implements session lifecycle management functionality, handling //! creation, retrieval, and storage of sessions. -use dashmap::DashMap; -use nym_crypto::asymmetric::{ed25519, x25519}; -use std::sync::Arc; - use crate::noise_protocol::ReadResult; +use crate::peer::{LpLocalPeer, LpRemotePeer}; use crate::state_machine::{LpAction, LpInput, LpState, LpStateBare}; use crate::{LpError, LpMessage, LpSession, LpStateMachine}; +use dashmap::DashMap; /// Manages the lifecycle of Lewes Protocol sessions. /// @@ -170,20 +168,12 @@ impl SessionManager { pub fn create_session_state_machine( &self, receiver_index: u32, - local_ed25519_keypair: Arc, - remote_ed25519_key: &ed25519::PublicKey, - remote_x25519_key: &x25519::PublicKey, is_initiator: bool, + local_peer: LpLocalPeer, + remote_peer: LpRemotePeer, salt: &[u8; 32], ) -> Result { - let sm = LpStateMachine::new( - receiver_index, - is_initiator, - local_ed25519_keypair, - remote_ed25519_key, - remote_x25519_key, - salt, - )?; + let sm = LpStateMachine::new(receiver_index, is_initiator, local_peer, remote_peer, salt)?; self.state_machines.insert(receiver_index, sm); Ok(receiver_index) @@ -202,7 +192,7 @@ impl SessionManager { pub fn init_kkt_for_test( &self, lp_id: u32, - remote_x25519_pub: &x25519::PublicKey, + remote_x25519_pub: &nym_crypto::asymmetric::x25519::PublicKey, ) -> Result<(), LpError> { self.with_state_machine(lp_id, |sm| { sm.session()?.set_kkt_completed_for_test(remote_x25519_pub); @@ -214,28 +204,21 @@ impl SessionManager { #[cfg(test)] mod tests { use super::*; - use nym_crypto::asymmetric::ed25519; + use crate::peer::{mock_peers, random_peer}; + use nym_test_utils::helpers::deterministic_rng; #[test] fn test_session_manager_get() { let manager = SessionManager::new(); - let ed25519_keypair = ed25519::KeyPair::from_secret([10u8; 32], 0); - let ed25519_keypair2 = ed25519::KeyPair::from_secret([16u8; 32], 0); - - let x25519_keypair2 = ed25519_keypair2.to_x25519(); + let mut rng = deterministic_rng(); + let local = random_peer(&mut rng); + let peer1 = random_peer(&mut rng); let salt = [47u8; 32]; let receiver_index: u32 = 1001; let sm_1_id = manager - .create_session_state_machine( - receiver_index, - Arc::new(ed25519_keypair), - ed25519_keypair2.public_key(), - x25519_keypair2.public_key(), - true, - &salt, - ) + .create_session_state_machine(receiver_index, true, local, peer1.as_remote(), &salt) .unwrap(); let retrieved = manager.state_machine_exists(sm_1_id); @@ -248,23 +231,15 @@ mod tests { #[test] fn test_session_manager_remove() { let manager = SessionManager::new(); - let ed25519_keypair = ed25519::KeyPair::from_secret([11u8; 32], 0); - let ed25519_keypair2 = ed25519::KeyPair::from_secret([16u8; 32], 0); - - let x25519_keypair2 = ed25519_keypair2.to_x25519(); + let mut rng = deterministic_rng(); + let local = random_peer(&mut rng); + let peer1 = random_peer(&mut rng); let salt = [48u8; 32]; let receiver_index: u32 = 2002; let sm_1_id = manager - .create_session_state_machine( - receiver_index, - Arc::new(ed25519_keypair), - ed25519_keypair2.public_key(), - x25519_keypair2.public_key(), - true, - &salt, - ) + .create_session_state_machine(receiver_index, true, local, peer1.as_remote(), &salt) .unwrap(); let removed = manager.remove_state_machine(sm_1_id); @@ -278,50 +253,24 @@ mod tests { #[test] fn test_multiple_sessions() { let manager = SessionManager::new(); - let ed25519_keypair_1 = ed25519::KeyPair::from_secret([12u8; 32], 0); - let ed25519_keypair_2 = ed25519::KeyPair::from_secret([13u8; 32], 1); - let ed25519_keypair_3 = ed25519::KeyPair::from_secret([14u8; 32], 2); + let mut rng = deterministic_rng(); + let local = random_peer(&mut rng); + let peer1 = random_peer(&mut rng); + let peer2 = random_peer(&mut rng); + let peer3 = random_peer(&mut rng); + let salt = [49u8; 32]; - let pubkey1 = *ed25519_keypair_1.public_key(); - let pubkey2 = *ed25519_keypair_2.public_key(); - let pubkey3 = *ed25519_keypair_3.public_key(); - - let xpubkey1 = *ed25519_keypair_1.to_x25519().public_key(); - let xpubkey2 = *ed25519_keypair_2.to_x25519().public_key(); - let xpubkey3 = *ed25519_keypair_3.to_x25519().public_key(); - let sm_1 = manager - .create_session_state_machine( - 3001, - Arc::new(ed25519_keypair_1), - &pubkey2, - &xpubkey2, - true, - &salt, - ) + .create_session_state_machine(3001, true, local.clone(), peer1.as_remote(), &salt) .unwrap(); let sm_2 = manager - .create_session_state_machine( - 3002, - Arc::new(ed25519_keypair_2), - &pubkey3, - &xpubkey3, - true, - &salt, - ) + .create_session_state_machine(3002, true, local.clone(), peer2.as_remote(), &salt) .unwrap(); let sm_3 = manager - .create_session_state_machine( - 3003, - Arc::new(ed25519_keypair_3), - &pubkey1, - &xpubkey1, - true, - &salt, - ) + .create_session_state_machine(3003, true, local.clone(), peer3.as_remote(), &salt) .unwrap(); assert_eq!(manager.session_count(), 3); @@ -338,19 +287,16 @@ mod tests { #[test] fn test_session_manager_create_session() { let manager = SessionManager::new(); - let ed25519_keypair = ed25519::KeyPair::from_secret([15u8; 32], 0); - let ed25519_keypair2 = ed25519::KeyPair::from_secret([16u8; 32], 0); + let (init, resp) = mock_peers(); + let salt = [50u8; 32]; let receiver_index: u32 = 4004; - let x25519_keypair2 = ed25519_keypair2.to_x25519(); - let sm = manager.create_session_state_machine( receiver_index, - Arc::new(ed25519_keypair), - ed25519_keypair2.public_key(), - x25519_keypair2.public_key(), true, + init, + resp.as_remote(), &salt, ); diff --git a/common/nym-lp/src/state_machine.rs b/common/nym-lp/src/state_machine.rs index fb467d213e..5d91df2b71 100644 --- a/common/nym-lp/src/state_machine.rs +++ b/common/nym-lp/src/state_machine.rs @@ -13,6 +13,7 @@ //! State machine ensures protocol steps execute in correct order. Invalid transitions //! return LpError, preventing protocol violations. +use crate::peer::{LpLocalPeer, LpRemotePeer}; use crate::{ LpError, message::{LpMessage, SubsessionKK1Data, SubsessionKK2Data, SubsessionReadyData}, @@ -21,9 +22,7 @@ use crate::{ session::{LpSession, SubsessionHandshake}, }; use bytes::BytesMut; -use nym_crypto::asymmetric::{ed25519, x25519}; use std::mem; -use std::sync::Arc; use tracing::debug; /// Represents the possible states of the Lewes Protocol connection. @@ -178,57 +177,25 @@ impl LpStateMachine { Ok(self.session()?.id()) } - /// Creates a new state machine from Ed25519 keys, internally deriving X25519 keys. - /// - /// This is the primary constructor that accepts only Ed25519 keys (identity/signing keys) - /// and internally derives the X25519 keys needed for Noise protocol and DHKEM. - /// This simplifies the API by hiding the X25519 derivation as an implementation detail. + /// Creates a new state machine from Ed25519 keys. /// /// # Arguments /// /// * `receiver_index` - Client-proposed session identifier (random 4 bytes) /// * `is_initiator` - Whether this side initiates the handshake - /// * `local_ed25519_keypair` - Ed25519 keypair for PSQ authentication and X25519 derivation - /// (from client identity key or gateway signing key) - /// * `remote_ed25519_key` - Peer's Ed25519 public key for PSQ authentication - /// * `remote_x25519_key` - Peer's x25519 public key for Noise protocol and DHKEM + /// * `local_peer` - This side's LP peer's keys + /// * `remote_peer` - The remote's LP peer's keys /// * `salt` - Fresh salt for PSK derivation (must be unique per session) - /// - /// # Errors - /// - /// Returns `LpError::Ed25519RecoveryError` if Ed25519→X25519 conversion fails for the remote key. - /// Local private key conversion cannot fail. pub fn new( receiver_index: u32, is_initiator: bool, - local_ed25519_keypair: Arc, - remote_ed25519_key: &ed25519::PublicKey, - remote_x25519_key: &x25519::PublicKey, + local_peer: LpLocalPeer, + remote_peer: LpRemotePeer, salt: &[u8; 32], ) -> Result { - // We use standard RFC 7748 conversion to derive X25519 keys from Ed25519 identity keys. - // This allows callers to provide only Ed25519 keys (which they already have for signing/identity) - // without needing to manage separate X25519 keypairs. - // - // Security: Ed25519→X25519 conversion is cryptographically sound (RFC 7748). - // The derived X25519 keys are used for: - // - Noise protocol ephemeral DH - // - PSQ ECDH baseline security (pre-quantum) - - // Convert Ed25519 keys to X25519 for Noise protocol - let local_x25519 = Arc::new(local_ed25519_keypair.to_x25519()); - // Create the session with both Ed25519 (for PSQ auth) and derived X25519 keys (for Noise) // receiver_index is client-proposed, passed through directly - let session = LpSession::new( - receiver_index, - is_initiator, - local_ed25519_keypair, - local_x25519, - remote_ed25519_key, - remote_x25519_key, - salt, - )?; + let session = LpSession::new(receiver_index, is_initiator, local_peer, remote_peer, salt)?; Ok(LpStateMachine { state: LpState::ReadyToHandshake { @@ -369,8 +336,7 @@ impl LpStateMachine { } } LpMessage::KKTResponse(kkt_response) if session.is_initiator() => { - // Initiator processes KKT response (signature-only mode with None) - match session.process_kkt_response(&kkt_response.0, None) { + match session.process_kkt_response(&kkt_response.0) { Ok(()) => { result_action = Some(Ok(LpAction::KKTComplete)); // After successful KKT, move to Handshaking @@ -1083,32 +1049,20 @@ impl LpStateMachine { #[cfg(test)] mod tests { use super::*; + use crate::peer::mock_peers; use bytes::Bytes; - use nym_crypto::asymmetric::ed25519; #[test] fn test_state_machine_init() { - // Ed25519 keypairs for PSQ authentication and X25519 derivation - let ed25519_keypair_init = ed25519::KeyPair::from_secret([16u8; 32], 0); - let ed25519_keypair_resp = ed25519::KeyPair::from_secret([17u8; 32], 1); - - let ed25519_pubkey_init = *ed25519_keypair_init.public_key(); - let x25519_pubkey_init = ed25519_pubkey_init.to_x25519().unwrap(); - let x25519_pubkey_resp = *ed25519_keypair_resp.to_x25519().public_key(); + let (init, resp) = mock_peers(); // Test salt let salt = [51u8; 32]; let receiver_index: u32 = 77777; - let initiator_sm = LpStateMachine::new( - receiver_index, - true, - Arc::new(ed25519_keypair_init), - ed25519_keypair_resp.public_key(), - &x25519_pubkey_resp, - &salt, - ); + let initiator_sm = + LpStateMachine::new(receiver_index, true, init.clone(), resp.as_remote(), &salt); assert!(initiator_sm.is_ok()); let initiator_sm = initiator_sm.unwrap(); assert!(matches!( @@ -1118,14 +1072,8 @@ mod tests { let init_session = initiator_sm.session().unwrap(); assert!(init_session.is_initiator()); - let responder_sm = LpStateMachine::new( - receiver_index, - false, - Arc::new(ed25519_keypair_resp), - &ed25519_pubkey_init, - &x25519_pubkey_init, - &salt, - ); + let responder_sm = + LpStateMachine::new(receiver_index, false, resp, init.as_remote(), &salt); assert!(responder_sm.is_ok()); let responder_sm = responder_sm.unwrap(); assert!(matches!( @@ -1141,13 +1089,7 @@ mod tests { #[test] fn test_state_machine_simplified_flow() { - // Ed25519 keypairs for PSQ authentication and X25519 derivation - let ed25519_keypair_init = ed25519::KeyPair::from_secret([18u8; 32], 0); - let ed25519_keypair_resp = ed25519::KeyPair::from_secret([19u8; 32], 1); - - let ed25519_pubkey_init = *ed25519_keypair_init.public_key(); - let x25519_pubkey_init = ed25519_pubkey_init.to_x25519().unwrap(); - let x25519_pubkey_resp = *ed25519_keypair_resp.to_x25519().public_key(); + let (init, resp) = mock_peers(); // Test salt let salt = [52u8; 32]; @@ -1157,9 +1099,8 @@ mod tests { let mut initiator = LpStateMachine::new( receiver_index, true, // is_initiator - Arc::new(ed25519_keypair_init), - ed25519_keypair_resp.public_key(), - &x25519_pubkey_resp, + init.clone(), + resp.as_remote(), &salt, ) .unwrap(); @@ -1167,9 +1108,8 @@ mod tests { let mut responder = LpStateMachine::new( receiver_index, false, // is_initiator - Arc::new(ed25519_keypair_resp), - &ed25519_pubkey_init, - &x25519_pubkey_init, + resp, + init.as_remote(), &salt, ) .unwrap(); @@ -1349,25 +1289,14 @@ mod tests { #[test] fn test_kkt_exchange_initiator_flow() { - // Ed25519 keypairs for PSQ authentication and X25519 derivation - let ed25519_keypair_init = ed25519::KeyPair::from_secret([20u8; 32], 0); - let ed25519_keypair_resp = ed25519::KeyPair::from_secret([21u8; 32], 1); - - let x25519_pubkey_init = *ed25519_keypair_init.to_x25519().public_key(); + let (init, resp) = mock_peers(); let salt = [53u8; 32]; let receiver_index: u32 = 99901; // Create initiator state machine - let mut initiator = LpStateMachine::new( - receiver_index, - true, - Arc::new(ed25519_keypair_init), - ed25519_keypair_resp.public_key(), - &x25519_pubkey_init, - &salt, - ) - .unwrap(); + let mut initiator = + LpStateMachine::new(receiver_index, true, init, resp.as_remote(), &salt).unwrap(); // Verify initial state assert!(matches!(initiator.state, LpState::ReadyToHandshake { .. })); @@ -1380,26 +1309,14 @@ mod tests { #[test] fn test_kkt_exchange_responder_flow() { - // Ed25519 keypairs for PSQ authentication and X25519 derivation - let ed25519_keypair_init = ed25519::KeyPair::from_secret([22u8; 32], 0); - let ed25519_keypair_resp = ed25519::KeyPair::from_secret([23u8; 32], 1); - - let ed25519_pubkey_init = *ed25519_keypair_init.public_key(); - let x25519_pubkey_init = ed25519_pubkey_init.to_x25519().unwrap(); + let (init, resp) = mock_peers(); let salt = [54u8; 32]; let receiver_index: u32 = 99902; // Create responder state machine - let mut responder = LpStateMachine::new( - receiver_index, - false, - Arc::new(ed25519_keypair_resp), - &ed25519_pubkey_init, - &x25519_pubkey_init, - &salt, - ) - .unwrap(); + let mut responder = + LpStateMachine::new(receiver_index, false, resp, init.as_remote(), &salt).unwrap(); // Verify initial state assert!(matches!(responder.state, LpState::ReadyToHandshake { .. })); @@ -1413,36 +1330,18 @@ mod tests { #[test] fn test_kkt_exchange_full_roundtrip() { // Ed25519 keypairs for PSQ authentication and X25519 derivation - let ed25519_keypair_init = ed25519::KeyPair::from_secret([24u8; 32], 0); - let ed25519_keypair_resp = ed25519::KeyPair::from_secret([25u8; 32], 1); - - let ed25519_pubkey_init = *ed25519_keypair_init.public_key(); - let x25519_pubkey_init = ed25519_pubkey_init.to_x25519().unwrap(); - let x25519_pubkey_resp = *ed25519_keypair_resp.to_x25519().public_key(); + let (init, resp) = mock_peers(); let salt = [55u8; 32]; let receiver_index: u32 = 99903; // Create both state machines - let mut initiator = LpStateMachine::new( - receiver_index, - true, - Arc::new(ed25519_keypair_init), - ed25519_keypair_resp.public_key(), - &x25519_pubkey_resp, - &salt, - ) - .unwrap(); + let mut initiator = + LpStateMachine::new(receiver_index, true, init.clone(), resp.as_remote(), &salt) + .unwrap(); - let mut responder = LpStateMachine::new( - receiver_index, - false, - Arc::new(ed25519_keypair_resp), - &ed25519_pubkey_init, - &x25519_pubkey_init, - &salt, - ) - .unwrap(); + let mut responder = + LpStateMachine::new(receiver_index, false, resp, init.as_remote(), &salt).unwrap(); // Step 1: Initiator starts handshake, sends KKT request let init_action = initiator.process_input(LpInput::StartHandshake); @@ -1470,6 +1369,7 @@ mod tests { // Step 4: Initiator receives KKT response, completes KKT let init_action = initiator.process_input(LpInput::ReceivePacket(kkt_response_packet)); + assert!(matches!(init_action, Some(Ok(LpAction::KKTComplete)))); // After KKT complete, initiator moves to Handshaking assert!(matches!(initiator.state, LpState::Handshaking { .. })); @@ -1477,25 +1377,15 @@ mod tests { #[test] fn test_kkt_exchange_close() { - // Ed25519 keypairs for KKT authentication - let ed25519_keypair_init = ed25519::KeyPair::from_secret([26u8; 32], 0); - let ed25519_keypair_resp = ed25519::KeyPair::from_secret([27u8; 32], 1); - - let x25519_pubkey_resp = *ed25519_keypair_resp.to_x25519().public_key(); + let (init, resp) = mock_peers(); let salt = [56u8; 32]; let receiver_index: u32 = 99904; // Create initiator state machine - let mut initiator = LpStateMachine::new( - receiver_index, - true, - Arc::new(ed25519_keypair_init), - ed25519_keypair_resp.public_key(), - &x25519_pubkey_resp, - &salt, - ) - .unwrap(); + let mut initiator = + LpStateMachine::new(receiver_index, true, init.clone(), resp.as_remote(), &salt) + .unwrap(); // Start handshake to enter KKTExchange state initiator.process_input(LpInput::StartHandshake); @@ -1509,25 +1399,15 @@ mod tests { #[test] fn test_kkt_exchange_rejects_invalid_inputs() { - // Ed25519 keypairs for KKT authentication - let ed25519_keypair_init = ed25519::KeyPair::from_secret([28u8; 32], 0); - let ed25519_keypair_resp = ed25519::KeyPair::from_secret([29u8; 32], 1); - - let x25519_pubkey_resp = *ed25519_keypair_resp.to_x25519().public_key(); + let (init, resp) = mock_peers(); let salt = [57u8; 32]; let receiver_index: u32 = 99905; // Create initiator state machine - let mut initiator = LpStateMachine::new( - receiver_index, - true, - Arc::new(ed25519_keypair_init), - ed25519_keypair_resp.public_key(), - &x25519_pubkey_resp, - &salt, - ) - .unwrap(); + let mut initiator = + LpStateMachine::new(receiver_index, true, init.clone(), resp.as_remote(), &salt) + .unwrap(); // Start handshake to enter KKTExchange state initiator.process_input(LpInput::StartHandshake); @@ -1553,39 +1433,16 @@ mod tests { /// Helper function to complete a full handshake between initiator and responder, /// returning both in Transport state ready for subsession testing. fn setup_transport_sessions() -> (LpStateMachine, LpStateMachine) { - // Use different seeds to get different X25519 keys. - // The tie-breaker compares X25519 public keys. - let ed25519_keypair_a = ed25519::KeyPair::from_secret([30u8; 32], 0); - let ed25519_keypair_b = ed25519::KeyPair::from_secret([31u8; 32], 1); - - let ed25519_pubkey_a = *ed25519_keypair_a.public_key(); - - let x25519_pubkey_a = *ed25519_keypair_a.to_x25519().public_key(); - let x25519_pubkey_b = *ed25519_keypair_b.to_x25519().public_key(); + let (a, b) = mock_peers(); let salt = [60u8; 32]; let receiver_index: u32 = 111111; // Create state machines - Alice is initiator, Bob is responder - let mut alice = LpStateMachine::new( - receiver_index, - true, - Arc::new(ed25519_keypair_a), - ed25519_keypair_b.public_key(), - &x25519_pubkey_b, - &salt, - ) - .unwrap(); + let mut alice = + LpStateMachine::new(receiver_index, true, a.clone(), b.as_remote(), &salt).unwrap(); - let mut bob = LpStateMachine::new( - receiver_index, - false, - Arc::new(ed25519_keypair_b), - &ed25519_pubkey_a, - &x25519_pubkey_a, - &salt, - ) - .unwrap(); + let mut bob = LpStateMachine::new(receiver_index, false, b, a.as_remote(), &salt).unwrap(); // --- Complete KKT Exchange --- // Alice starts handshake diff --git a/common/nymnoise/keys/src/lib.rs b/common/nymnoise/keys/src/lib.rs index b4cd70148b..59fd6cfb12 100644 --- a/common/nymnoise/keys/src/lib.rs +++ b/common/nymnoise/keys/src/lib.rs @@ -30,8 +30,14 @@ impl From for u8 { } } -#[derive(Copy, Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, utoipa::ToSchema)] -pub struct VersionedNoiseKey { +// 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( + Copy, Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, utoipa::ToSchema, PartialEq, +)] +pub struct VersionedNoiseKeyV1 { #[schemars(with = "u8")] #[schema(value_type = u8)] pub supported_version: NoiseVersion, diff --git a/common/nymnoise/src/config.rs b/common/nymnoise/src/config.rs index 6f5e087d6c..1ef2235928 100644 --- a/common/nymnoise/src/config.rs +++ b/common/nymnoise/src/config.rs @@ -10,7 +10,7 @@ use std::{ use arc_swap::ArcSwap; use nym_crypto::asymmetric::x25519; -use nym_noise_keys::{NoiseVersion, VersionedNoiseKey}; +use nym_noise_keys::{NoiseVersion, VersionedNoiseKeyV1}; use snow::params::NoiseParams; use strum_macros::{EnumIter, FromRepr}; @@ -55,7 +55,7 @@ impl NoisePattern { #[derive(Debug, Default)] struct SocketAddrToKey { - inner: ArcSwap>, + inner: ArcSwap>, } // SW NOTE : Only for phased upgrade. To remove once we decide all nodes have to support Noise @@ -78,7 +78,7 @@ impl NoiseNetworkView { } } - pub fn swap_view(&self, new: HashMap) { + pub fn swap_view(&self, new: HashMap) { let noise_support = new .iter() .map(|(s_addr, key)| (s_addr.ip(), key.supported_version)) @@ -126,7 +126,7 @@ impl NoiseConfig { self } - pub(crate) fn get_noise_key(&self, s_address: &SocketAddr) -> Option { + pub(crate) fn get_noise_key(&self, s_address: &SocketAddr) -> Option { self.network.keys.inner.load().get(s_address).copied() } diff --git a/common/registration/src/lib.rs b/common/registration/src/lib.rs index 801daa9525..37ad465765 100644 --- a/common/registration/src/lib.rs +++ b/common/registration/src/lib.rs @@ -17,13 +17,13 @@ pub use lp_messages::{ }; pub use serialisation::BincodeError; -#[derive(Debug, Clone, Copy, PartialEq)] +#[derive(Debug, Clone)] pub struct NymNode { pub identity: NodeIdentity, pub ip_address: IpAddr, pub ipr_address: Option, pub authenticator_address: Option, - pub lp_address: Option, + pub lp_data: Option, pub version: AuthenticatorVersion, } #[derive(Clone, Debug, Serialize, Deserialize)] @@ -35,6 +35,13 @@ pub struct GatewayData { pub private_ipv6: Ipv6Addr, } +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct LpData { + pub address: SocketAddr, + // TODO: modify it into a map once we know more about the PSQv2 structure + pub expected_kem_key_hash: Vec, +} + #[derive(Clone, Copy, Debug)] pub struct AssignedAddresses { pub entry_mixnet_gateway_ip: IpAddr, diff --git a/common/topology/src/node.rs b/common/topology/src/node.rs index 85cb81b51f..8100977bee 100644 --- a/common/topology/src/node.rs +++ b/common/topology/src/node.rs @@ -1,7 +1,7 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use nym_api_requests::models::DeclaredRoles; +use nym_api_requests::models::DeclaredRolesV1; use nym_api_requests::nym_nodes::SkimmedNode; use nym_crypto::asymmetric::{ed25519, x25519}; use nym_mixnet_contract_common::NodeId; @@ -35,8 +35,8 @@ pub struct SupportedRoles { pub mixnet_exit: bool, } -impl From for SupportedRoles { - fn from(value: DeclaredRoles) -> Self { +impl From for SupportedRoles { + fn from(value: DeclaredRolesV1) -> Self { SupportedRoles { mixnode: value.mixnode, mixnet_entry: value.entry, diff --git a/common/verloc/src/measurements/measurer.rs b/common/verloc/src/measurements/measurer.rs index cc83426a64..edbbfab765 100644 --- a/common/verloc/src/measurements/measurer.rs +++ b/common/verloc/src/measurements/measurer.rs @@ -9,7 +9,7 @@ use futures::StreamExt; use futures::stream::FuturesUnordered; use nym_crypto::asymmetric::ed25519; use nym_task::ShutdownToken; -use nym_validator_client::models::NymNodeDescription; +use nym_validator_client::models::NymNodeDescriptionV1; use nym_validator_client::nym_api::NymApiClientExt; use rand::prelude::SliceRandom; use rand::thread_rng; @@ -132,7 +132,7 @@ impl VerlocMeasurer { MeasurementOutcome::Done } - async fn get_list_of_nodes(&self) -> Option> { + async fn get_list_of_nodes(&self) -> Option> { let mut api_endpoints = self.config.nym_api_urls.clone(); api_endpoints.shuffle(&mut thread_rng()); for api_endpoint in api_endpoints { @@ -145,6 +145,10 @@ impl VerlocMeasurer { continue; } }; + + // for now allow usage of old endpoint for we don't need LP related data + // and the new endpoint might not be immediately deployed + #[allow(deprecated)] match client.get_all_described_nodes().await { Ok(res) => return Some(res), Err(err) => { diff --git a/gateway/src/node/lp_listener/handler.rs b/gateway/src/node/lp_listener/handler.rs index 9b7f231e27..948d62a0b6 100644 --- a/gateway/src/node/lp_listener/handler.rs +++ b/gateway/src/node/lp_listener/handler.rs @@ -342,9 +342,8 @@ where let mut state_machine = LpStateMachine::new( receiver_index, false, // responder - self.state.local_identity.clone(), - &hello_data.client_ed25519_public_key, - &hello_data.client_lp_public_key, + self.state.local_lp_peer.clone(), + hello_data.to_remote_peer(), &hello_data.salt, ) .map_err(|e| { @@ -1248,10 +1247,10 @@ mod tests { use nym_lp::codec::{parse_lp_packet, serialize_lp_packet}; use nym_lp::message::{ClientHelloData, EncryptedDataPayload, HandshakeData, LpMessage}; use nym_lp::packet::{LpHeader, LpPacket}; + use nym_lp::peer::LpLocalPeer; use std::sync::Arc; use std::time::{SystemTime, UNIX_EPOCH}; use tokio::io::{AsyncRead, AsyncReadExt, AsyncWriteExt}; - // ==================== Test Helpers ==================== /// Create a minimal test state for handler tests @@ -1282,12 +1281,17 @@ mod tests { // Create mix forwarding channel (unused in tests but required by struct) let (mix_sender, _mix_receiver) = nym_mixnet_client::forwarder::mix_forwarding_channels(); + let id_keys = Arc::new(ed25519::KeyPair::new(&mut OsRng)); + let x_keys = Arc::new(id_keys.to_x25519()); + + let lp_peer = LpLocalPeer::new(id_keys, x_keys.clone()).with_kem_psq_key(x_keys); + LpHandlerState { lp_config, ecash_verifier: Arc::new(ecash_verifier) as Arc, storage, - local_identity: Arc::new(ed25519::KeyPair::new(&mut OsRng)), + local_lp_peer: lp_peer, metrics: nym_node_metrics::NymNodeMetrics::default(), active_clients_store: ActiveClientsStore::new(), wg_peer_controller: None, diff --git a/gateway/src/node/lp_listener/mod.rs b/gateway/src/node/lp_listener/mod.rs index 0e1a948e7a..8e63293759 100644 --- a/gateway/src/node/lp_listener/mod.rs +++ b/gateway/src/node/lp_listener/mod.rs @@ -71,11 +71,14 @@ use crate::error::GatewayError; use crate::node::ActiveClientsStore; use dashmap::DashMap; use nym_config::serde_helpers::de_maybe_port; -use nym_crypto::asymmetric::ed25519; use nym_gateway_storage::GatewayStorage; use nym_lp::state_machine::LpStateMachine; +pub use nym_mixnet_client::forwarder::{ + mix_forwarding_channels, MixForwardingReceiver, MixForwardingSender, +}; use nym_node_metrics::NymNodeMetrics; use nym_task::ShutdownTracker; +pub use nym_wireguard::{PeerControlRequest, WireguardGatewayData}; use std::net::{IpAddr, Ipv6Addr, SocketAddr}; use std::sync::Arc; use std::time::Duration; @@ -83,10 +86,7 @@ use tokio::net::TcpListener; use tokio::sync::{mpsc, Semaphore}; use tracing::*; -pub use nym_mixnet_client::forwarder::{ - mix_forwarding_channels, MixForwardingReceiver, MixForwardingSender, -}; -pub use nym_wireguard::{PeerControlRequest, WireguardGatewayData}; +pub use nym_lp::peer::LpLocalPeer; mod data_handler; pub mod handler; @@ -339,8 +339,8 @@ pub struct LpHandlerState { /// Storage backend for persistence pub storage: GatewayStorage, - /// Gateway's identity keypair - pub local_identity: Arc, + /// Encapsulates all required key information of a local Lewes Protocol Peer. + pub local_lp_peer: LpLocalPeer, /// Metrics collection pub metrics: NymNodeMetrics, diff --git a/gateway/src/node/lp_listener/registration.rs b/gateway/src/node/lp_listener/registration.rs index 7ff41acc4a..c14197c405 100644 --- a/gateway/src/node/lp_listener/registration.rs +++ b/gateway/src/node/lp_listener/registration.rs @@ -346,13 +346,12 @@ pub async fn process_registration( } // Get gateway identity and derive sphinx key - let gateway_identity = state.local_identity.public_key().to_bytes(); + let ed25519_key = state.local_lp_peer.ed25519().public_key(); + let gateway_identity = ed25519_key.to_bytes(); warn!("TEMPORARY ed25519 -> x25519 conversion"); #[allow(clippy::expect_used)] - let gateway_sphinx_key = state - .local_identity - .public_key() + let gateway_sphinx_key = ed25519_key .to_x25519() .expect("valid ed25519 key should convert to x25519") .to_bytes(); diff --git a/gateway/src/node/mod.rs b/gateway/src/node/mod.rs index 418a12673d..153b8ba59f 100644 --- a/gateway/src/node/mod.rs +++ b/gateway/src/node/mod.rs @@ -16,7 +16,7 @@ use nym_credential_verification::ecash::{ use nym_credential_verification::upgrade_mode::{ UpgradeModeCheckConfig, UpgradeModeDetails, UpgradeModeState, }; -use nym_crypto::asymmetric::ed25519; +use nym_crypto::asymmetric::{ed25519, x25519}; use nym_ip_packet_router::IpPacketRouter; use nym_mixnet_client::forwarder::MixForwardingSender; use nym_network_defaults::NymNetworkDetails; @@ -46,6 +46,7 @@ pub use nym_gateway_storage::{ traits::{BandwidthGatewayStorage, InboxGatewayStorage}, GatewayStorage, }; +use nym_lp::peer::LpLocalPeer; pub use nym_sdk::{NymApiTopologyProvider, NymApiTopologyProviderConfig, UserAgent}; pub(crate) mod client_handling; @@ -92,6 +93,9 @@ pub struct GatewayTasksBuilder { /// ed25519 keypair used to assert one's identity. identity_keypair: Arc, + /// x25519 (for now, to be changed into MlKem) keypair used for the PSQ derivation + kem_psq_keys: Arc, + storage: GatewayStorage, mix_packet_sender: MixForwardingSender, @@ -120,6 +124,7 @@ impl GatewayTasksBuilder { pub fn new( config: Config, identity: Arc, + kem_psq_keys: Arc, storage: GatewayStorage, mix_packet_sender: MixForwardingSender, metrics_sender: MetricEventsSender, @@ -137,6 +142,7 @@ impl GatewayTasksBuilder { wireguard_data: None, user_agent, identity_keypair: identity, + kem_psq_keys, storage, mix_packet_sender, metrics_sender, @@ -325,10 +331,21 @@ impl GatewayTasksBuilder { .as_ref() .map(|wg_data| wg_data.inner.peer_tx().clone()); + // We use standard RFC 7748 conversion to derive X25519 keys from Ed25519 identity keys. + // This allows callers to provide only Ed25519 keys (which they already have for signing/identity) + // without needing to manage separate X25519 keypairs. + // + // Security: Ed25519→X25519 conversion is cryptographically sound (RFC 7748). + // The derived X25519 keys are used for: + // - Noise protocol ephemeral DH + // - PSQ ECDH baseline security (pre-quantum) + let x25519_keys = Arc::new(self.identity_keypair.to_x25519()); + let handler_state = lp_listener::LpHandlerState { ecash_verifier: self.ecash_manager().await?, storage: self.storage.clone(), - local_identity: Arc::clone(&self.identity_keypair), + local_lp_peer: LpLocalPeer::new(self.identity_keypair.clone(), x25519_keys) + .with_kem_psq_key(self.kem_psq_keys.clone()), metrics: self.metrics.clone(), active_clients_store, wg_peer_controller, diff --git a/integration-tests/src/lp_registration.rs b/integration-tests/src/lp_registration.rs index 8c9d7c3fdc..40533cbb1f 100644 --- a/integration-tests/src/lp_registration.rs +++ b/integration-tests/src/lp_registration.rs @@ -11,8 +11,8 @@ mod tests { use nym_gateway::GatewayError; use nym_gateway::node::lp_listener::handler::LpConnectionHandler; use nym_gateway::node::lp_listener::{ - LpDebug, LpHandlerState, MixForwardingReceiver, PeerControlRequest, WireguardGatewayData, - mix_forwarding_channels, + LpDebug, LpHandlerState, LpLocalPeer, MixForwardingReceiver, PeerControlRequest, + WireguardGatewayData, mix_forwarding_channels, }; use nym_gateway::node::{ActiveClientsStore, GatewayStorage, LpConfig}; use nym_registration_client::{LpClientError, LpRegistrationClient}; @@ -46,7 +46,7 @@ mod tests { } struct Party { - ed25519_keys: Arc, + peer: LpLocalPeer, x25519_wg_keys: Arc, socket_addr: SocketAddr, } @@ -58,10 +58,15 @@ mod tests { rng.fill_bytes(&mut ip); rng.fill_bytes(&mut port); + let ed25519_keys = Arc::new(ed25519::KeyPair::new(rng)); + let x25519_wg_keys = Arc::new(x25519::KeyPair::new(rng)); + + let lp_x25519_keys = Arc::new(ed25519_keys.to_x25519()); Party { - ed25519_keys: Arc::new(ed25519::KeyPair::new(rng)), - x25519_wg_keys: Arc::new(x25519::KeyPair::new(rng)), + peer: LpLocalPeer::new(ed25519_keys, lp_x25519_keys.clone()) + .with_kem_psq_key(lp_x25519_keys), + x25519_wg_keys, socket_addr: SocketAddr::from((ip, u16::from_le_bytes(port))), } } @@ -207,10 +212,8 @@ mod tests { // use in-memory database (no need for persistency) storage, - // reuse the same identity we just generated - local_identity: base.ed25519_keys.clone(), + local_lp_peer: base.peer.clone(), - // we don't care about metrics - all zeroes are perfectly fine metrics: Default::default(), // no clients at the beginning @@ -386,8 +389,8 @@ mod tests { let mut entry = Gateway::mock(&mut gateway_rng).await?; let mut client = LpRegistrationClient::::new_with_default_config( - client_data.base.ed25519_keys, - *entry.base.ed25519_keys.public_key(), + client_data.base.peer.ed25519().clone(), + entry.base.peer.as_remote(), entry.base.socket_addr, client_data.base.socket_addr.ip(), ); @@ -436,7 +439,7 @@ mod tests { // 6. perform registration with entry only let wg_keypair = client_data.base.x25519_wg_keys; - let gateway_identity = entry.base.ed25519_keys.public_key(); + let gateway_identity = entry.base.peer.ed25519().public_key(); let registration_result = client .register( &wg_keypair, @@ -477,8 +480,8 @@ mod tests { let mut entry = Gateway::mock(&mut gateway_rng).await?; let mut client = LpRegistrationClient::::new_with_default_config( - client_data.base.ed25519_keys, - *entry.base.ed25519_keys.public_key(), + client_data.base.peer.ed25519().clone(), + entry.base.peer.as_remote(), entry.base.socket_addr, client_data.base.socket_addr.ip(), ); @@ -502,7 +505,7 @@ mod tests { // 4. perform registration with entry only // but WITHOUT performing the handshake let wg_keypair = client_data.base.x25519_wg_keys; - let gateway_identity = entry.base.ed25519_keys.public_key(); + let gateway_identity = entry.base.peer.ed25519().public_key(); let registration_result = client .register( &wg_keypair, @@ -537,8 +540,8 @@ mod tests { let mut exit = Gateway::mock(&mut exit_rng).await?; let mut entry_client = LpRegistrationClient::::new_with_default_config( - client_data.base.ed25519_keys.clone(), - *entry.base.ed25519_keys.public_key(), + client_data.base.peer.ed25519().clone(), + entry.base.peer.as_remote(), entry.base.socket_addr, client_data.base.socket_addr.ip(), ); @@ -636,10 +639,9 @@ mod tests { // technically we should use different ephemeral keys than we had for the entry // but crypto is going to work the same let mut nested_session = NestedLpSession::new( - exit.base.ed25519_keys.public_key().to_bytes(), exit.base.socket_addr.to_string(), - client_data.base.ed25519_keys, - *exit.base.ed25519_keys.public_key(), + client_data.base.peer.ed25519().clone(), + exit.base.peer.as_remote(), ); // 13. Perform handshake and registration with exit gateway (all via entry forwarding) @@ -647,7 +649,7 @@ mod tests { .handshake_and_register( &mut entry_client, &client_data.base.x25519_wg_keys, - exit.base.ed25519_keys.public_key(), + exit.base.peer.ed25519().public_key(), &client_data.ticket_provider, TicketType::V1WireguardExit, client_data.base.socket_addr.ip(), @@ -659,7 +661,7 @@ mod tests { let entry_registration_result = entry_client .register( &client_data.base.x25519_wg_keys, - entry.base.ed25519_keys.public_key(), + entry.base.peer.ed25519().public_key(), &client_data.ticket_provider, TicketType::V1WireguardEntry, ) diff --git a/nym-api/nym-api-requests/Cargo.toml b/nym-api/nym-api-requests/Cargo.toml index 0a683ef8b8..25279ba1cb 100644 --- a/nym-api/nym-api-requests/Cargo.toml +++ b/nym-api/nym-api-requests/Cargo.toml @@ -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 = [] diff --git a/nym-api/nym-api-requests/src/models/described.rs b/nym-api/nym-api-requests/src/models/described.rs deleted file mode 100644 index 73612d7f38..0000000000 --- a/nym-api/nym-api-requests/src/models/described.rs +++ /dev/null @@ -1,418 +0,0 @@ -// Copyright 2025 - Nym Technologies SA -// 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)] - pub ip_address: Vec, - pub hostname: Option, - pub keys: HostKeys, -} - -impl From 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, - - #[serde(default)] - pub x25519_versioned_noise: Option, -} - -#[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 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 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, -} - -impl From 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)] - pub ip_addresses: Vec, -} - -#[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 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 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 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 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 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, - - #[serde(default)] - pub ip_packet_router: Option, - - #[serde(default)] - pub authenticator: Option, - - #[serde(default)] - pub wireguard: Option, - - #[serde(default)] - pub lewes_protocol: Option, - - // 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) - } -} diff --git a/nym-api/nym-api-requests/src/models/described/mod.rs b/nym-api/nym-api-requests/src/models/described/mod.rs new file mode 100644 index 0000000000..f3b38925be --- /dev/null +++ b/nym-api/nym-api-requests/src/models/described/mod.rs @@ -0,0 +1,26 @@ +// Copyright 2025 - Nym Technologies SA +// 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)] + pub ip_addresses: Vec, +} 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 new file mode 100644 index 0000000000..54c751ea46 --- /dev/null +++ b/nym-api/nym-api-requests/src/models/described/type_translation.rs @@ -0,0 +1,426 @@ +// Copyright 2026 - Nym Technologies SA +// 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)] + pub ip_address: Vec, + pub hostname: Option, + 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, + + #[serde(default)] + pub x25519_versioned_noise: Option, +} + +// 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, + pub mix_port: Option, +} + +// 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)] + #[schemars(with = "Option")] + #[schemars(length(equal = 2))] + pub location: Option, + + #[serde(default)] + pub announce_ports: AnnouncePortsV1, + + /// Specifies whether this node operator has agreed to the terms and conditions + /// as defined at + // 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, +} + +// 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>>, +} + +#[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 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 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 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 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 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 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 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 + 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 + 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 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 + 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 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 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 + } + } + } +} diff --git a/nym-api/nym-api-requests/src/models/described/v1.rs b/nym-api/nym-api-requests/src/models/described/v1.rs new file mode 100644 index 0000000000..2108422c2a --- /dev/null +++ b/nym-api/nym-api-requests/src/models/described/v1.rs @@ -0,0 +1,198 @@ +// Copyright 2026 - Nym Technologies SA +// 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, + + #[serde(default)] + pub ip_packet_router: Option, + + #[serde(default)] + pub authenticator: Option, + + #[serde(default)] + pub wireguard: Option, + + // for now we only care about their ws/wss situation, nothing more + pub mixnet_websockets: 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) + } +} diff --git a/nym-api/nym-api-requests/src/models/described/v2.rs b/nym-api/nym-api-requests/src/models/described/v2.rs new file mode 100644 index 0000000000..ac712e3d5e --- /dev/null +++ b/nym-api/nym-api-requests/src/models/described/v2.rs @@ -0,0 +1,343 @@ +// Copyright 2026 - Nym Technologies SA +// 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, + + #[serde(default)] + pub ip_packet_router: Option, + + #[serde(default)] + pub authenticator: Option, + + #[serde(default)] + pub wireguard: Option, + + // for now we only care about their ws/wss situation, nothing more + pub mixnet_websockets: WebSocketsV2, + + #[serde(default)] + pub lewes_protocol: Option, +} + +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 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 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 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 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, + }, + }, + } +} diff --git a/nym-api/nym-api-requests/src/models/mod.rs b/nym-api/nym-api-requests/src/models/mod.rs index 6d7ab23f96..c5f0026d44 100644 --- a/nym-api/nym-api-requests/src/models/mod.rs +++ b/nym-api/nym-api-requests/src/models/mod.rs @@ -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 { diff --git a/nym-api/nym-api-requests/src/nym_nodes.rs b/nym-api/nym-api-requests/src/nym_nodes.rs index ca7ffab7e8..40b088a681 100644 --- a/nym-api/nym-api-requests/src/nym_nodes.rs +++ b/nym-api/nym-api-requests/src/nym_nodes.rs @@ -1,7 +1,7 @@ // Copyright 2024 - Nym Technologies SA // 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, @@ -278,7 +278,7 @@ impl SkimmedNode { pub struct SemiSkimmedNode { pub basic: SkimmedNode, - pub x25519_noise_versioned_key: Option, + pub x25519_noise_versioned_key: Option, // 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, + pub self_described: Option, } #[derive(Clone, Serialize, Deserialize, Debug, PartialEq, ToSchema)] diff --git a/nym-api/src/network_monitor/monitor/preparer.rs b/nym-api/src/network_monitor/monitor/preparer.rs index 074a4f612f..d2d23a9715 100644 --- a/nym-api/src/network_monitor/monitor/preparer.rs +++ b/nym-api/src/network_monitor/monitor/preparer.rs @@ -8,7 +8,7 @@ use crate::node_describe_cache::cache::DescribedNodes; use crate::node_describe_cache::NodeDescriptionTopologyExt; use crate::node_status_api::NodeStatusCache; use crate::support::caching::cache::SharedCache; -use nym_api_requests::models::{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, - mixing_nym_nodes: impl Iterator + 'a, + mixing_nym_nodes: impl Iterator + 'a, ) -> HashMap> { let mut layered_mixes = HashMap::new(); @@ -206,7 +206,7 @@ impl PacketPreparer { &self, current_rotation_id: u32, node_statuses: &HashMap, - gateway_capable_nym_nodes: impl Iterator + 'a, + gateway_capable_nym_nodes: impl Iterator + 'a, ) -> Vec<(RoutingNode, f64)> { let mut gateways = Vec::new(); @@ -361,7 +361,7 @@ impl PacketPreparer { fn nym_node_to_routing_node( &self, current_rotation_id: u32, - description: &NymNodeDescription, + description: &NymNodeDescriptionV2, ) -> Option { description.try_to_topology_node(current_rotation_id).ok() } diff --git a/nym-api/src/node_describe_cache/cache.rs b/nym-api/src/node_describe_cache/cache.rs index 3f3de776d6..0e00b986d6 100644 --- a/nym-api/src/node_describe_cache/cache.rs +++ b/nym-api/src/node_describe_cache/cache.rs @@ -1,61 +1,61 @@ // Copyright 2025 - Nym Technologies SA // 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, + pub(crate) nodes: HashMap, pub(crate) addresses_cache: HashMap, } 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 { + pub fn all_nodes(&self) -> impl Iterator { self.nodes.values() } - pub fn all_nym_nodes(&self) -> impl Iterator { + pub fn all_nym_nodes(&self) -> impl Iterator { self.nodes .values() - .filter(|n| n.contract_node_type == DescribedNodeType::NymNode) + .filter(|n| n.contract_node_type == DescribedNodeTypeV2::NymNode) } - pub fn mixing_nym_nodes(&self) -> impl Iterator { + pub fn mixing_nym_nodes(&self) -> impl Iterator { self.nodes .values() - .filter(|n| n.contract_node_type == 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 { + pub fn entry_capable_nym_nodes(&self) -> impl Iterator { 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 { + pub fn exit_capable_nym_nodes(&self) -> impl Iterator { 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()) } diff --git a/nym-api/src/node_describe_cache/mod.rs b/nym-api/src/node_describe_cache/mod.rs index b72c1a9103..976904ce2c 100644 --- a/nym-api/src/node_describe_cache/mod.rs +++ b/nym-api/src/node_describe_cache/mod.rs @@ -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; } -impl NodeDescriptionTopologyExt for NymNodeDescription { +impl NodeDescriptionTopologyExt for NymNodeDescriptionV1 { + fn try_to_topology_node( + &self, + current_rotation_id: u32, + ) -> Result { + // for the purposes of routing, performance is completely ignored, + // so add dummy value and piggyback on existing conversion + (&self.to_skimmed_node(current_rotation_id, Default::default(), Default::default())) + .try_into() + } +} + +impl NodeDescriptionTopologyExt for NymNodeDescriptionV2 { fn try_to_topology_node( &self, current_rotation_id: u32, diff --git a/nym-api/src/node_describe_cache/query_helpers.rs b/nym-api/src/node_describe_cache/query_helpers.rs index bcfbdf4049..c800638efc 100644 --- a/nym-api/src/node_describe_cache/query_helpers.rs +++ b/nym-api/src/node_describe_cache/query_helpers.rs @@ -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, NymNodeApiClientError> { +) -> Result, NymNodeApiClientError> { let Ok(nr) = client.get_network_requester().await else { return Ok(None); }; client.get_exit_policy().await.map(|exit_policy| { let uses_nym_exit_policy = exit_policy.upstream_source == mainnet::EXIT_POLICY_URL; - Some(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 Future for NodeDescribedInfoMegaFuture where F1: Future>, - F2: Future>, - F3: Future, - F4: Future>, - F5: Future, NodeDescribeCacheError>>, - F6: Future>, - F7: Future>, - F8: Future>, - F9: Future>, + F2: Future>, + F3: Future, + F4: Future>, + F5: Future, NodeDescribeCacheError>>, + F6: Future>, + F7: Future>, + F8: Future>, + F9: Future>, { type Output = Result; @@ -202,15 +203,15 @@ where struct ResolvedNodeDescribedInfo { build_info: Result, - roles: Result, + roles: Result, // TODO: in the future make it return a Result as well. - auxiliary_details: AuxiliaryDetails, - websockets: Result, - network_requester: Result, NodeDescribeCacheError>, - ipr: Option, - authenticator: Option, - wireguard: Option, - lewes_protocol: Option, + auxiliary_details: AuxiliaryDetailsV2, + websockets: Result, + network_requester: Result, NodeDescribeCacheError>, + ipr: Option, + authenticator: Option, + wireguard: Option, + lewes_protocol: Option, } 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, - pub(crate) ipr: Option, - pub(crate) authenticator: Option, - pub(crate) wireguard: Option, - pub(crate) lewes_protocol: Option, + pub(crate) roles: DeclaredRolesV2, + pub(crate) auxiliary_details: AuxiliaryDetailsV2, + pub(crate) websockets: WebSocketsV2, + pub(crate) network_requester: Option, + pub(crate) ipr: Option, + pub(crate) authenticator: Option, + pub(crate) wireguard: Option, + pub(crate) lewes_protocol: Option, } impl UnwrappedResolvedNodeDescribedInfo { pub(crate) fn into_node_description( self, - host_info: impl Into, - ) -> NymNodeData { - NymNodeData { + host_info: impl Into, + ) -> NymNodeDataV2 { + NymNodeDataV2 { host_information: host_info.into(), last_polled: OffsetDateTime::now_utc().into(), build_information: self.build_info, diff --git a/nym-api/src/node_describe_cache/refresh.rs b/nym-api/src/node_describe_cache/refresh.rs index 24fea96c44..801aa5dc29 100644 --- a/nym-api/src/node_describe_cache/refresh.rs +++ b/nym-api/src/node_describe_cache/refresh.rs @@ -3,7 +3,7 @@ use crate::node_describe_cache::query_helpers::query_for_described_data; use crate::node_describe_cache::NodeDescribeCacheError; -use nym_api_requests::models::{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, } @@ -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, expected_identity: ed25519::PublicKey, - node_type: DescribedNodeType, + node_type: DescribedNodeTypeV2, node_id: NodeId, port: Option, ) -> Self { @@ -58,7 +58,7 @@ impl RefreshData { self.node_id } - pub(crate) async fn try_refresh(self, allow_all_ips: bool) -> Option { + pub(crate) async fn try_refresh(self, allow_all_ips: bool) -> Option { match try_get_description(self, allow_all_ips).await { Ok(description) => Some(description), Err(err) => { @@ -124,7 +124,7 @@ async fn try_get_client( async fn try_get_description( data: RefreshData, allow_all_ips: bool, -) -> Result { +) -> Result { 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, diff --git a/nym-api/src/node_status_api/cache/config_score.rs b/nym-api/src/node_status_api/cache/config_score.rs index 30f24d0b40..02ae220436 100644 --- a/nym-api/src/node_status_api/cache/config_score.rs +++ b/nym-api/src/node_status_api/cache/config_score.rs @@ -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(); diff --git a/nym-api/src/nym_nodes/handlers/mod.rs b/nym-api/src/nym_nodes/handlers/mod.rs index 954349c03a..d242f328cf 100644 --- a/nym-api/src/nym_nodes/handlers/mod.rs +++ b/nym-api/src/nym_nodes/handlers/mod.rs @@ -1,495 +1,5 @@ // Copyright 2023 - Nym Technologies SA // 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 { - 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, - State(state): State, -) -> AxumResult> { - 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, - Json(request_body): Json, -) -> AxumResult> { - 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 = "application/json"), - (PaginatedResponse = "application/yaml"), - (PaginatedResponse = "application/bincode") - )) - ), - params(PaginationRequest) -)] -async fn nodes_noise( - State(state): State, - Query(pagination): Query, -) -> AxumResult>> { - // 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::>(); - - 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 = "application/json"), - (PaginatedResponse = "application/yaml"), - (PaginatedResponse = "application/bincode") - )) - ), - params(PaginationRequest) -)] -async fn get_bonded_nodes( - State(state): State, - Query(pagination): Query, -) -> FormattedResponse> { - // 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 = "application/json"), - (PaginatedResponse = "application/yaml"), - (PaginatedResponse = "application/bincode") - )) - ), - params(PaginationRequest) -)] -async fn get_described_nodes( - State(state): State, - Query(pagination): Query, -) -> AxumResult>> { - // 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, - })) -} - -#[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, - Query(output): Query, - State(state): State, -) -> AxumResult> { - 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, - Query(output): Query, - State(state): State, -) -> AxumResult> { - 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, - Query(output): Query, - State(state): State, -) -> AxumResult> { - 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, -} - -#[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, - Query(DateQuery { date, output }): Query, - State(state): State, -) -> AxumResult> { - 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, - State(state): State, - Query(pagination): Query, -) -> AxumResult> { - // 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::>(); - 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, - State(state): State, - Query(pagination): Query, -) -> AxumResult> { - // 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::>(); - 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; diff --git a/nym-api/src/nym_nodes/handlers/v1.rs b/nym-api/src/nym_nodes/handlers/v1.rs new file mode 100644 index 0000000000..36834a5fc4 --- /dev/null +++ b/nym-api/src/nym_nodes/handlers/v1.rs @@ -0,0 +1,502 @@ +// Copyright 2026 - Nym Technologies SA +// 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 { + 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, + State(state): State, +) -> AxumResult> { + 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, + Json(request_body): Json, +) -> AxumResult> { + 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 = "application/json"), + (PaginatedResponse = "application/yaml"), + (PaginatedResponse = "application/bincode") + )) + ), + params(PaginationRequest) +)] +async fn nodes_noise( + State(state): State, + Query(pagination): Query, +) -> AxumResult>> { + // 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::>(); + + 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 = "application/json"), + (PaginatedResponse = "application/yaml"), + (PaginatedResponse = "application/bincode") + )) + ), + params(PaginationRequest) +)] +async fn get_bonded_nodes( + State(state): State, + Query(pagination): Query, +) -> FormattedResponse> { + // 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 = "application/json"), + (PaginatedResponse = "application/yaml"), + (PaginatedResponse = "application/bincode") + )) + ), + params(PaginationRequest) +)] +#[deprecated(note = "use '/v2/nym-nodes/described' instead")] +async fn get_described_nodes( + State(state): State, + Query(pagination): Query, +) -> AxumResult>> { + // 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::>(); + + 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, + Query(output): Query, + State(state): State, +) -> AxumResult> { + 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, + Query(output): Query, + State(state): State, +) -> AxumResult> { + 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, + Query(output): Query, + State(state): State, +) -> AxumResult> { + 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, +} + +#[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, + Query(DateQuery { date, output }): Query, + State(state): State, +) -> AxumResult> { + 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, + State(state): State, + Query(pagination): Query, +) -> AxumResult> { + // 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::>(); + 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, + State(state): State, + Query(pagination): Query, +) -> AxumResult> { + // 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::>(); + let total = history.len(); + + Ok(output.to_response(UptimeHistoryResponse { + node_id, + history: PaginatedResponse { + pagination: Pagination { + total, + page: 0, + size: total, + }, + data: history, + }, + })) +} diff --git a/nym-api/src/nym_nodes/handlers/v2.rs b/nym-api/src/nym_nodes/handlers/v2.rs new file mode 100644 index 0000000000..ee49e575fa --- /dev/null +++ b/nym-api/src/nym_nodes/handlers/v2.rs @@ -0,0 +1,54 @@ +// Copyright 2026 - Nym Technologies SA +// 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 { + 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 = "application/json"), + (PaginatedResponse = "application/yaml"), + (PaginatedResponse = "application/bincode") + )) + ), + params(PaginationRequest) +)] +async fn get_described_nodes( + State(state): State, + Query(pagination): Query, +) -> AxumResult>> { + // 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, + })) +} diff --git a/nym-api/src/support/http/router.rs b/nym-api/src/support/http/router.rs index 56a8950968..88ca68bc8f 100644 --- a/nym-api/src/support/http/router.rs +++ b/nym-api/src/support/http/router.rs @@ -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, diff --git a/nym-api/src/unstable_routes/v2/nym_nodes/semi_skimmed/mod.rs b/nym-api/src/unstable_routes/v2/nym_nodes/semi_skimmed/mod.rs index d86003c091..555718f50d 100644 --- a/nym-api/src/unstable_routes/v2/nym_nodes/semi_skimmed/mod.rs +++ b/nym-api/src/unstable_routes/v2/nym_nodes/semi_skimmed/mod.rs @@ -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 where - NI: Iterator + 'a, + NI: Iterator + 'a, { let mut nodes = Vec::new(); for nym_node in nym_nodes_subset { diff --git a/nym-api/src/unstable_routes/v2/nym_nodes/skimmed/helpers.rs b/nym-api/src/unstable_routes/v2/nym_nodes/skimmed/helpers.rs index 56f9619803..3456c82444 100644 --- a/nym-api/src/unstable_routes/v2/nym_nodes/skimmed/helpers.rs +++ b/nym-api/src/unstable_routes/v2/nym_nodes/skimmed/helpers.rs @@ -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 where - NI: Iterator + 'a, + NI: Iterator + '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 + 'a, + NI: Iterator + 'a, { // TODO: implement it let _ = query_params.per_page; diff --git a/nym-gateway-probe/src/lib.rs b/nym-gateway-probe/src/lib.rs index 604bfb381c..3e5ab6377b 100644 --- a/nym-gateway-probe/src/lib.rs +++ b/nym-gateway-probe/src/lib.rs @@ -8,7 +8,7 @@ use std::{ }; use crate::types::Entry; -use anyhow::bail; +use anyhow::{Context, bail}; use base64::{Engine as _, engine::general_purpose}; use bytes::BytesMut; use clap::Args; @@ -62,6 +62,7 @@ mod types; use crate::bandwidth_helpers::{acquire_bandwidth, import_bandwidth}; use crate::nodes::{DirectoryNode, NymApiDirectory}; pub use mode::TestMode; +use nym_lp::peer::LpRemotePeer; use nym_node_status_client::models::AttachedTicketMaterials; pub use types::{IpPingReplies, ProbeOutcome, ProbeResult}; @@ -1108,10 +1109,16 @@ where let mut rng = rand::thread_rng(); let client_ed25519_keypair = std::sync::Arc::new(ed25519::KeyPair::new(&mut rng)); + // Step 0: Derive X25519 keys from Ed25519 for the gateways + let gateway_x25519_key = gateway_identity + .to_x25519() + .context("failed to convert entry ed25519 key to x25519")?; + let peer = LpRemotePeer::new(gateway_identity, gateway_x25519_key); + // Create LP registration client (uses Ed25519 keys directly, derives X25519 internally) let mut client = LpRegistrationClient::::new_with_default_config( client_ed25519_keypair, - gateway_identity, + peer, gateway_lp_address, gateway_ip, ); @@ -1264,17 +1271,31 @@ where let entry_lp_keypair = Arc::new(ed25519::KeyPair::new(&mut rng)); let exit_lp_keypair = Arc::new(ed25519::KeyPair::new(&mut rng)); + // Step 0: Derive X25519 keys from Ed25519 for the gateways + let entry_x25519_public = entry_gateway + .identity + .to_x25519() + .context("failed to convert entry ed25519 key to x25519")?; + + let exit_x25519_public = exit_gateway + .identity + .to_x25519() + .context("failed to convert exit ed25519 key to x25519")?; + // Generate WireGuard keypairs for VPN registration let entry_wg_keypair = x25519::KeyPair::new(&mut rng); let exit_wg_keypair = x25519::KeyPair::new(&mut rng); + let entry_peer = LpRemotePeer::new(entry_gateway.identity, entry_x25519_public); + let exit_peer = LpRemotePeer::new(exit_gateway.identity, exit_x25519_public); + // STEP 1: Establish outer LP session with entry gateway // LpRegistrationClient uses packet-per-connection model - connect() is gone, // connection is established automatically during handshake. info!("Establishing outer LP session with entry gateway..."); let mut entry_client = LpRegistrationClient::::new_with_default_config( entry_lp_keypair, - entry_gateway.identity, + entry_peer, entry_lp_address, entry_ip, ); @@ -1288,13 +1309,8 @@ where // STEP 2: Use nested session to register with exit gateway via forwarding info!("Registering with exit gateway via entry forwarding..."); - let mut nested_session = NestedLpSession::new( - exit_gateway.identity.to_bytes(), - exit_lp_address.to_string(), - exit_lp_keypair, - ed25519::PublicKey::from_bytes(&exit_gateway.identity.to_bytes()) - .map_err(|e| anyhow::anyhow!("Invalid exit gateway identity: {}", e))?, - ); + let mut nested_session = + NestedLpSession::new(exit_lp_address.to_string(), exit_lp_keypair, exit_peer); // Convert exit gateway identity to ed25519 public key for registration let exit_gateway_pubkey = ed25519::PublicKey::from_bytes(&exit_gateway.identity.to_bytes()) diff --git a/nym-gateway-probe/src/nodes.rs b/nym-gateway-probe/src/nodes.rs index 9bb68046cb..bc44e8c035 100644 --- a/nym-gateway-probe/src/nodes.rs +++ b/nym-gateway-probe/src/nodes.rs @@ -4,9 +4,9 @@ use crate::TestedNodeDetails; use anyhow::{Context, anyhow, bail}; use nym_api_requests::models::{ - AuthenticatorDetails, DeclaredRoles, DescribedNodeType, HostInformation, IpPacketRouterDetails, - LewesProtocolDetails, NetworkRequesterDetails, NymNodeData, OffsetDateTimeJsonSchemaWrapper, - WebSockets, WireguardDetails, + AuthenticatorDetailsV2, DeclaredRolesV2, DescribedNodeTypeV2, HostInformationV2, + IpPacketRouterDetailsV2, LewesProtocolDetailsV1, NetworkRequesterDetailsV2, NymNodeDataV2, + OffsetDateTimeJsonSchemaWrapper, WebSocketsV2, WireguardDetailsV2, }; use nym_authenticator_requests::AuthenticatorVersion; use nym_bin_common::build_information::BinaryBuildInformationOwned; @@ -16,7 +16,7 @@ use nym_node_requests::api::client::NymNodeApiClientExt; use nym_node_requests::api::v1::node::models::AuxiliaryDetails as NodeAuxiliaryDetails; use nym_sdk::mixnet::NodeIdentity; use nym_validator_client::client::NymApiClientExt; -use nym_validator_client::models::NymNodeDescription; +use nym_validator_client::models::NymNodeDescriptionV2; use rand::prelude::IteratorRandom; use std::collections::HashMap; use std::time::Duration; @@ -87,7 +87,7 @@ use url::Url; #[derive(Clone)] pub struct DirectoryNode { - described: NymNodeDescription, + described: NymNodeDescriptionV2, } impl DirectoryNode { @@ -153,21 +153,18 @@ impl DirectoryNode { /// # Returns /// A `DirectoryNode` containing all gateway metadata, or an error if the query fails pub async fn query_gateway_by_ip(address: String) -> anyhow::Result { - info!("Querying gateway directly at address: {}", address); + info!("Querying gateway directly at address: {address}"); // Parse the address to check if it contains a port let addresses_to_try = if address.contains(':') { // Address already has port specified, use it directly - vec![ - format!("http://{}", address), - format!("https://{}", address), - ] + vec![format!("http://{address}"), format!("https://{address}")] } else { // No port specified, try multiple ports in order of likelihood vec![ - format!("http://{}:{}", address, DEFAULT_NYM_NODE_HTTP_PORT), // Standard port 8080 - format!("https://{}", address), // HTTPS proxy (443) - format!("http://{}", address), // HTTP proxy (80) + format!("http://{address}:{DEFAULT_NYM_NODE_HTTP_PORT}"), // Standard port 8080 + format!("https://{address}"), // HTTPS proxy (443) + format!("http://{address}"), // HTTP proxy (80) ] }; @@ -175,7 +172,7 @@ pub async fn query_gateway_by_ip(address: String) -> anyhow::Result anyhow::Result = None; // Not needed for LP testing - let ip_packet_router: Option = - ipr_result.map(|ipr| IpPacketRouterDetails { + let network_requester: Option = None; // Not needed for LP testing + let ip_packet_router: Option = + ipr_result.map(|ipr| IpPacketRouterDetailsV2 { address: ipr.address, }); - let authenticator: Option = - authenticator_result.map(|auth| AuthenticatorDetails { + let authenticator: Option = + authenticator_result.map(|auth| AuthenticatorDetailsV2 { address: auth.address, }); #[allow(deprecated)] - let wireguard: Option = - wireguard_result.map(|wg| WireguardDetails { + let wireguard: Option = + wireguard_result.map(|wg| WireguardDetailsV2 { port: wg.tunnel_port, // Use tunnel_port for deprecated port field tunnel_port: wg.tunnel_port, metadata_port: wg.metadata_port, public_key: wg.public_key, }); - let lp: Option = lp_result.ok().map(Into::into); + let lp: Option = lp_result.ok().map(Into::into); // Construct NymNodeData - let node_data = NymNodeData { + let node_data = NymNodeDataV2 { last_polled: OffsetDateTimeJsonSchemaWrapper(OffsetDateTime::now_utc()), - host_information: HostInformation { + host_information: HostInformationV2 { ip_address: host_info.data.ip_address, hostname: host_info.data.hostname, keys: host_info.data.keys.into(), }, - declared_role: DeclaredRoles { + declared_role: DeclaredRolesV2 { mixnode: roles.mixnode_enabled, entry: roles.gateway_enabled, exit_nr: roles.network_requester_enabled, exit_ipr: roles.ip_packet_router_enabled, }, - auxiliary_details: aux_details, + auxiliary_details: aux_details.into(), build_information: BinaryBuildInformationOwned { binary_name: build_info.binary_name, build_timestamp: build_info.build_timestamp, @@ -287,16 +284,16 @@ pub async fn query_gateway_by_ip(address: String) -> anyhow::Result, - self_described: Option<&NymNodeDescription>, + self_described: Option<&NymNodeDescriptionV1>, ) -> anyhow::Result { let now = OffsetDateTime::now_utc().unix_timestamp(); @@ -553,33 +555,33 @@ pub struct InsertNodeScraperRecords { #[derive(Clone, Serialize, Deserialize, Debug)] pub struct NymNodeDescriptionDeHelper { pub node_id: NodeId, - pub contract_node_type: DescribedNodeType, + pub contract_node_type: DescribedNodeTypeV2, pub description: NymNodeDataDeHelper, } #[allow(deprecated)] -impl From for NymNodeDescription { +impl From for NymNodeDescriptionV2 { fn from(helper: NymNodeDescriptionDeHelper) -> Self { let current_x25519_sphinx_key = helper .description .host_information .keys .current_x25519_sphinx_key - .unwrap_or(SphinxKey { + .unwrap_or(SphinxKeyV1 { // indicate the legacy case rotation_id: u32::MAX, public_key: helper.description.host_information.keys.x25519, }); - NymNodeDescription { + NymNodeDescriptionV2 { node_id: helper.node_id, contract_node_type: helper.contract_node_type, - description: NymNodeData { + description: NymNodeDataV2 { last_polled: helper.description.last_polled, - host_information: HostInformation { + host_information: HostInformationV2 { ip_address: helper.description.host_information.ip_address, hostname: helper.description.host_information.hostname, - keys: HostKeys { + keys: HostKeysV2 { ed25519: helper.description.host_information.keys.ed25519, x25519: helper.description.host_information.keys.x25519, current_x25519_sphinx_key, @@ -609,6 +611,8 @@ impl From for NymNodeDescription { } } +// in this instance, since data is stored as json +// adding a new field with #[serde(default)] is fine #[derive(Clone, Serialize, Deserialize, Debug)] pub struct NymNodeDataDeHelper { #[serde(default)] @@ -617,31 +621,31 @@ pub struct NymNodeDataDeHelper { pub host_information: HostInformationDeHelper, #[serde(default)] - pub declared_role: DeclaredRoles, + pub declared_role: DeclaredRolesV2, #[serde(default)] - pub auxiliary_details: AuxiliaryDetails, + 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, + pub network_requester: Option, #[serde(default)] - pub ip_packet_router: Option, + pub ip_packet_router: Option, #[serde(default)] - pub authenticator: Option, + pub authenticator: Option, #[serde(default)] - pub wireguard: Option, + pub wireguard: Option, #[serde(default)] - pub lewes_protocol: Option, + pub lewes_protocol: Option, // for now we only care about their ws/wss situation, nothing more - pub mixnet_websockets: WebSockets, + pub mixnet_websockets: WebSocketsV1, } #[derive(Clone, Serialize, Deserialize, Debug)] @@ -661,11 +665,11 @@ pub struct HostKeysDeHelper { pub x25519: x25519::PublicKey, // legacy data will NOT have this information - pub current_x25519_sphinx_key: Option, + pub current_x25519_sphinx_key: Option, #[serde(default)] - pub pre_announced_x25519_sphinx_key: Option, + pub pre_announced_x25519_sphinx_key: Option, #[serde(default)] - pub x25519_versioned_noise: Option, + pub x25519_versioned_noise: Option, } diff --git a/nym-node-status-api/nym-node-status-api/src/db/queries/nym_nodes.rs b/nym-node-status-api/nym-node-status-api/src/db/queries/nym_nodes.rs index ae53e041c2..969fd087f6 100644 --- a/nym-node-status-api/nym-node-status-api/src/db/queries/nym_nodes.rs +++ b/nym-node-status-api/nym-node-status-api/src/db/queries/nym_nodes.rs @@ -1,14 +1,5 @@ -use crate::db::models::NymNodeDescriptionDeHelper; -use futures_util::TryStreamExt; -use nym_node_requests::api::v1::node::models::NodeDescription; -use nym_validator_client::{ - client::{NodeId, NymNodeDetails}, - models::NymNodeDescription, -}; -use std::collections::HashMap; -use tracing::{instrument, warn}; - use crate::db::DbConnection; +use crate::db::models::NymNodeDescriptionDeHelper; use crate::http::models::DailyStats; use crate::{ db::{ @@ -17,6 +8,12 @@ use crate::{ }, node_scraper::helpers::NodeDescriptionResponse, }; +use futures_util::TryStreamExt; +use nym_node_requests::api::v1::node::models::NodeDescription; +use nym_validator_client::client::{NodeId, NymNodeDetails}; +use nym_validator_client::models::NymNodeDescriptionV2; +use std::collections::HashMap; +use tracing::{instrument, warn}; pub(crate) async fn get_all_nym_nodes(pool: &DbPool) -> anyhow::Result> { let mut conn = pool.acquire().await?; @@ -195,7 +192,7 @@ pub(crate) async fn get_described_node_bond_info( pub(crate) async fn get_node_self_description( pool: &DbPool, -) -> anyhow::Result> { +) -> anyhow::Result> { let mut conn = pool.acquire().await?; sqlx::query!( diff --git a/nym-node-status-api/nym-node-status-api/src/db/tests.rs b/nym-node-status-api/nym-node-status-api/src/db/tests.rs index a80bdc1bdd..e1f444301c 100644 --- a/nym-node-status-api/nym-node-status-api/src/db/tests.rs +++ b/nym-node-status-api/nym-node-status-api/src/db/tests.rs @@ -128,7 +128,7 @@ mod db_tests { node_role: serde_json::json!(nym_validator_client::nym_nodes::NodeRole::Mixnode { layer: 1 }), - supported_roles: serde_json::json!(nym_validator_client::models::DeclaredRoles { + supported_roles: serde_json::json!(nym_validator_client::models::DeclaredRolesV1 { entry: false, mixnode: true, exit_nr: false, @@ -181,7 +181,7 @@ fn test_nym_node_insert_record_new() { mix_port: 1789, x25519_sphinx_pubkey: x25519_pk, role: nym_validator_client::nym_nodes::NodeRole::Mixnode { layer: 1 }, - supported_roles: nym_validator_client::models::DeclaredRoles { + supported_roles: nym_validator_client::models::DeclaredRolesV1 { entry: false, mixnode: true, exit_nr: false, @@ -208,7 +208,7 @@ fn test_nym_node_insert_record_new() { ); assert_eq!( record.supported_roles, - serde_json::json!(nym_validator_client::models::DeclaredRoles { + serde_json::json!(nym_validator_client::models::DeclaredRolesV1 { entry: false, mixnode: true, exit_nr: false, @@ -233,7 +233,7 @@ fn test_nym_node_insert_record_with_entry() { mix_port: 1789, x25519_sphinx_pubkey: x25519_pk, role: nym_validator_client::nym_nodes::NodeRole::EntryGateway, - supported_roles: nym_validator_client::models::DeclaredRoles { + supported_roles: nym_validator_client::models::DeclaredRolesV1 { entry: true, mixnode: false, exit_nr: true, @@ -425,7 +425,7 @@ fn test_nym_node_dto_with_invalid_keys() { node_role: serde_json::json!(nym_validator_client::nym_nodes::NodeRole::Mixnode { layer: 1 }), - supported_roles: serde_json::json!(nym_validator_client::models::DeclaredRoles { + supported_roles: serde_json::json!(nym_validator_client::models::DeclaredRolesV1 { entry: false, mixnode: true, exit_nr: false, @@ -463,7 +463,7 @@ fn test_nym_node_dto_with_invalid_performance() { node_role: serde_json::json!(nym_validator_client::nym_nodes::NodeRole::Mixnode { layer: 1 }), - supported_roles: serde_json::json!(nym_validator_client::models::DeclaredRoles { + supported_roles: serde_json::json!(nym_validator_client::models::DeclaredRolesV1 { entry: false, mixnode: true, exit_nr: false, diff --git a/nym-node-status-api/nym-node-status-api/src/http/models.rs b/nym-node-status-api/nym-node-status-api/src/http/models.rs index 2f0fe09183..cce5f0cf8b 100644 --- a/nym-node-status-api/nym-node-status-api/src/http/models.rs +++ b/nym-node-status-api/nym-node-status-api/src/http/models.rs @@ -6,7 +6,7 @@ use nym_mixnet_contract_common::CoinSchema; use nym_node_requests::api::v1::node::models::NodeDescription; use nym_validator_client::{ client::NodeId, - models::{AuthenticatorDetails, BinaryBuildInformationOwned, IpPacketRouterDetails}, + models::{AuthenticatorDetailsV1, BinaryBuildInformationOwned, IpPacketRouterDetailsV1}, nym_api::SkimmedNode, nym_nodes::{BasicEntryInformation, NodeRole}, }; @@ -100,8 +100,8 @@ pub struct DVpnGateway { pub identity_key: String, pub name: String, pub description: Option, - pub ip_packet_router: Option, - pub authenticator: Option, + pub ip_packet_router: Option, + pub authenticator: Option, pub location: Location, pub last_probe: Option, #[schema(value_type = Vec)] @@ -917,10 +917,10 @@ pub(crate) struct ExtendedNymNode { pub(crate) original_pledge: u128, pub(crate) bonding_address: Option, pub(crate) bonded: bool, - pub(crate) node_type: nym_validator_client::models::DescribedNodeType, + pub(crate) node_type: nym_validator_client::models::DescribedNodeTypeV1, pub(crate) ip_address: String, pub(crate) accepted_tnc: bool, - pub(crate) self_description: nym_validator_client::models::NymNodeData, + pub(crate) self_description: nym_validator_client::models::NymNodeDataV2, pub(crate) rewarding_details: Option, pub(crate) description: NodeDescription, pub(crate) geoip: Option, diff --git a/nym-node-status-api/nym-node-status-api/src/metrics_scraper/mod.rs b/nym-node-status-api/nym-node-status-api/src/metrics_scraper/mod.rs index c89fcc40c3..ab843af8e5 100644 --- a/nym-node-status-api/nym-node-status-api/src/metrics_scraper/mod.rs +++ b/nym-node-status-api/nym-node-status-api/src/metrics_scraper/mod.rs @@ -4,7 +4,7 @@ use nym_network_defaults::{DEFAULT_NYM_NODE_HTTP_PORT, NymNetworkDetails}; use nym_node_requests::api::{client::NymNodeApiClientExt, v1::metrics::models::SessionStats}; use nym_validator_client::{ client::{NodeId, NymNodeDetails}, - models::{DescribedNodeType, NymNodeDescription}, + models::{DescribedNodeTypeV1, NymNodeDescriptionV1}, }; use time::OffsetDateTime; @@ -64,6 +64,10 @@ async fn run( //SW TBC what nodes exactly need to be scraped, the skimmed node endpoint seems to return more nodes let bonded_nodes = nym_api.get_all_bonded_nym_nodes().await?; + + // for now allow usage of old endpoint for we don't need LP related data + // and the new endpoint might not be immediately deployed + #[allow(deprecated)] let all_nodes = nym_api.get_all_described_nodes().await?; //legacy node that did not upgrade the contract bond yet tracing::debug!("Fetched {} total nodes", all_nodes.len()); @@ -74,7 +78,7 @@ async fn run( all_nodes .into_iter() - .filter(|n| n.contract_node_type != DescribedNodeType::LegacyMixnode) + .filter(|n| n.contract_node_type != DescribedNodeTypeV1::LegacyMixnode) .for_each(|n| { nodes_to_scrape.entry(n.node_id).or_insert_with(|| n.into()); }); @@ -211,8 +215,8 @@ impl From for MetricsScrapingData { } } -impl From for MetricsScrapingData { - fn from(value: NymNodeDescription) -> Self { +impl From for MetricsScrapingData { + fn from(value: NymNodeDescriptionV1) -> Self { MetricsScrapingData::new( value.description.host_information.ip_address[0].to_string(), value.node_id, diff --git a/nym-node-status-api/nym-node-status-api/src/monitor/mod.rs b/nym-node-status-api/nym-node-status-api/src/monitor/mod.rs index a1fcab2b8f..75a1926a66 100644 --- a/nym-node-status-api/nym-node-status-api/src/monitor/mod.rs +++ b/nym-node-status-api/nym-node-status-api/src/monitor/mod.rs @@ -16,7 +16,7 @@ use nym_validator_client::{ }; use nym_validator_client::{ client::{NodeId, NymApiClientExt, NymNodeDetails}, - models::NymNodeDescription, + models::NymNodeDescriptionV1, }; use std::{collections::HashMap, sync::Arc}; use tokio::{sync::RwLock, time::Duration}; @@ -282,7 +282,7 @@ impl Monitor { } #[instrument(level = "info", skip_all)] - async fn location_cached(&mut self, node: &NymNodeDescription) -> Location { + async fn location_cached(&mut self, node: &NymNodeDescriptionV1) -> Location { let node_id = node.node_id; match self.geocache.get(&node_id).await { @@ -310,7 +310,7 @@ impl Monitor { &self, skimmed_nodes: Vec, bonded_node_info: &HashMap, - described_nodes: &HashMap, + described_nodes: &HashMap, ) -> Vec { skimmed_nodes .into_iter() @@ -335,7 +335,7 @@ impl Monitor { async fn prepare_gateway_data( &mut self, - described_gateways: &[&NymNodeDescription], + described_gateways: &[&NymNodeDescriptionV1], skimmed_gateways: &[SkimmedNode], bonded_nodes: &HashMap, ) -> anyhow::Result> { diff --git a/nym-node/Cargo.toml b/nym-node/Cargo.toml index c9de83d0bb..426f6f3137 100644 --- a/nym-node/Cargo.toml +++ b/nym-node/Cargo.toml @@ -60,6 +60,7 @@ nym-client-core-config-types = { workspace = true, features = [ nym-config = { workspace = true } nym-credential-verification = { workspace = true } nym-crypto = { workspace = true, features = ["asymmetric", "rand"] } +nym-kkt = { path = "../common/nym-kkt" } nym-nonexhaustive-delayqueue = { workspace = true } nym-mixnet-client = { workspace = true } nym-noise = { workspace = true } @@ -79,7 +80,7 @@ nym-validator-client = { workspace = true } nym-wireguard = { workspace = true } nym-wireguard-types = { workspace = true } nym-verloc = { workspace = true } -nym-metrics = { workspace = true} +nym-metrics = { workspace = true } nym-gateway-stats-storage = { workspace = true } nym-topology = { workspace = true } nym-http-api-client = { workspace = true } diff --git a/nym-node/nym-node-requests/src/api/mod.rs b/nym-node/nym-node-requests/src/api/mod.rs index 9c3e0a67d2..ba32570c2e 100644 --- a/nym-node/nym-node-requests/src/api/mod.rs +++ b/nym-node/nym-node-requests/src/api/mod.rs @@ -132,7 +132,7 @@ mod tests { use super::*; use crate::api::v1::node::models::{HostKeys, SphinxKey}; use nym_crypto::asymmetric::{ed25519, x25519}; - use nym_noise_keys::{NoiseVersion, VersionedNoiseKey}; + use nym_noise_keys::{NoiseVersion, VersionedNoiseKeyV1}; use rand_chacha::rand_core::SeedableRng; #[test] @@ -141,7 +141,7 @@ mod tests { let ed22519 = ed25519::KeyPair::new(&mut rng); let x25519_sphinx = x25519::KeyPair::new(&mut rng); let x25519_sphinx2 = x25519::KeyPair::new(&mut rng); - let x25519_versioned_noise = VersionedNoiseKey { + let x25519_versioned_noise = VersionedNoiseKeyV1 { supported_version: NoiseVersion::V1, x25519_pubkey: *x25519::KeyPair::new(&mut rng).public_key(), }; @@ -305,7 +305,7 @@ mod tests { public_key: *x25519_sphinx.public_key(), }, pre_announced_x25519_sphinx_key: None, - x25519_versioned_noise: Some(VersionedNoiseKey { + x25519_versioned_noise: Some(VersionedNoiseKeyV1 { supported_version: NoiseVersion::V1, x25519_pubkey: legacy_info_noise.keys.x25519_noise.unwrap(), }), @@ -384,7 +384,7 @@ mod tests { public_key: *x25519_sphinx.public_key(), }, pre_announced_x25519_sphinx_key: None, - x25519_versioned_noise: Some(VersionedNoiseKey { + x25519_versioned_noise: Some(VersionedNoiseKeyV1 { supported_version: NoiseVersion::V1, x25519_pubkey: legacy_info_noise.keys.x25519_noise.parse().unwrap(), }), 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 687b6d621c..4e668e5d75 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 @@ -3,8 +3,10 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use strum_macros::{Display, EnumIter, EnumString}; -#[derive(Serialize, Deserialize, Debug, Clone, Copy, JsonSchema)] +#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)] #[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] pub struct LewesProtocol { /// Helper field that specifies whether the LP listener(s) is enabled on this node. @@ -16,4 +18,101 @@ pub struct LewesProtocol { /// 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>>, +} + +impl LewesProtocol { + pub fn new(enabled: bool, control_port: u16, data_port: u16) -> Self { + LewesProtocol { + enabled, + control_port, + data_port, + kem_keys: Default::default(), + } + } + + pub fn with_kem_key_hashes( + mut self, + kem: LPKEM, + hashes: HashMap>, + ) -> Self { + self.kem_keys.insert(kem, hashes); + self + } +} + +// explicitly redefine available HashFunctions and KEMs so that we would not +// accidentally remove some type and thus break backwards compatibility at deserialisation level +// (the only thing that should break at that point would be conversion into proper nym-kkt types +// which would return a concrete error variant) + +#[derive( + Serialize, + Deserialize, + Debug, + Clone, + Copy, + JsonSchema, + Hash, + PartialEq, + Eq, + PartialOrd, + Display, + EnumString, +)] +#[strum(serialize_all = "lowercase")] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +pub enum LPKEM { + MlKem768, + XWing, + X25519, + McEliece, +} + +#[derive( + Serialize, + Deserialize, + Debug, + Clone, + Copy, + JsonSchema, + Hash, + PartialEq, + Eq, + PartialOrd, + Display, + EnumString, + EnumIter, +)] +#[strum(serialize_all = "lowercase")] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +pub enum LPHashFunction { + Blake3, + Shake128, + Shake256, + Sha256, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn kem_display() { + assert_eq!(LPKEM::MlKem768.to_string(), "mlkem768"); + assert_eq!(LPKEM::XWing.to_string(), "xwing"); + assert_eq!(LPKEM::X25519.to_string(), "x25519"); + assert_eq!(LPKEM::McEliece.to_string(), "mceliece"); + } + + #[test] + fn hash_function_display() { + assert_eq!(LPHashFunction::Blake3.to_string(), "blake3"); + assert_eq!(LPHashFunction::Shake128.to_string(), "shake128"); + assert_eq!(LPHashFunction::Shake256.to_string(), "shake256"); + assert_eq!(LPHashFunction::Sha256.to_string(), "sha256"); + } } diff --git a/nym-node/nym-node-requests/src/api/v1/node/models.rs b/nym-node/nym-node-requests/src/api/v1/node/models.rs index 2d0a9fe16f..4a6a750688 100644 --- a/nym-node/nym-node-requests/src/api/v1/node/models.rs +++ b/nym-node/nym-node-requests/src/api/v1/node/models.rs @@ -6,7 +6,7 @@ use nym_crypto::asymmetric::ed25519::{self, serde_helpers::bs58_ed25519_pubkey}; use nym_crypto::asymmetric::x25519::{ self, serde_helpers::bs58_x25519_pubkey, serde_helpers::option_bs58_x25519_pubkey, }; -use nym_noise_keys::VersionedNoiseKey; +use nym_noise_keys::VersionedNoiseKeyV1; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::net::IpAddr; @@ -146,7 +146,7 @@ pub struct HostKeys { /// Base58-encoded x25519 public key of this node used for the noise protocol. #[serde(default)] - pub x25519_versioned_noise: Option, + pub x25519_versioned_noise: Option, } // we need the intermediate struct to help us with the new explicit sphinx key fields @@ -193,7 +193,7 @@ struct HostKeysDeHelper { /// Base58-encoded x25519 public key of this node used for the noise protocol. #[serde(default)] - pub x25519_versioned_noise: Option, + pub x25519_versioned_noise: Option, } #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] 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 67496def85..36d8dc785d 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 @@ -7,7 +7,7 @@ use nym_node_requests::api::v1::lewes_protocol::models; pub mod root; -#[derive(Debug, Default, Clone, Copy)] +#[derive(Debug, Default, Clone)] pub struct Config { pub details: Option, } diff --git a/nym-node/src/node/http/state/mod.rs b/nym-node/src/node/http/state/mod.rs index 8ed756aa60..f1c9f2d81b 100644 --- a/nym-node/src/node/http/state/mod.rs +++ b/nym-node/src/node/http/state/mod.rs @@ -7,7 +7,7 @@ use crate::node::key_rotation::active_keys::ActiveSphinxKeys; use nym_credential_verification::UpgradeModeState; use nym_crypto::asymmetric::ed25519; use nym_node_metrics::NymNodeMetrics; -use nym_noise_keys::VersionedNoiseKey; +use nym_noise_keys::VersionedNoiseKeyV1; use nym_verloc::measurements::SharedVerlocStats; use std::net::IpAddr; use std::sync::Arc; @@ -20,7 +20,7 @@ pub mod metrics; pub(crate) struct StaticNodeInformation { pub(crate) ed25519_identity_keys: Arc, - pub(crate) x25519_versioned_noise_key: Option, + pub(crate) x25519_versioned_noise_key: Option, pub(crate) ip_addresses: Vec, pub(crate) hostname: Option, } diff --git a/nym-node/src/node/mod.rs b/nym-node/src/node/mod.rs index 42d803f46d..a46ae3e7da 100644 --- a/nym-node/src/node/mod.rs +++ b/nym-node/src/node/mod.rs @@ -42,6 +42,8 @@ use nym_bin_common::bin_info; use nym_credential_verification::UpgradeModeState; use nym_crypto::asymmetric::{ed25519, x25519}; use nym_gateway::node::{ActiveClientsStore, GatewayTasksBuilder, UpgradeModeCheckRequestSender}; +use nym_kkt::ciphersuite::{DEFAULT_HASH_LEN, HashFunction}; +use nym_kkt::key_utils::hash_encapsulation_key; use nym_mixnet_client::client::ActiveConnections; use nym_mixnet_client::forwarder::MixForwardingSender; use nym_network_requester::{ @@ -50,9 +52,10 @@ use nym_network_requester::{ }; use nym_node_metrics::NymNodeMetrics; use nym_node_metrics::events::MetricEventsSender; +use nym_node_requests::api::v1::lewes_protocol::models::{LPHashFunction, LPKEM}; use nym_node_requests::api::v1::node::models::{AnnouncePorts, NodeDescription}; use nym_noise::config::{NoiseConfig, NoiseNetworkView}; -use nym_noise_keys::VersionedNoiseKey; +use nym_noise_keys::VersionedNoiseKeyV1; use nym_sphinx_acknowledgements::AckKey; use nym_sphinx_addressing::Recipient; use nym_task::{ShutdownManager, ShutdownToken, ShutdownTracker}; @@ -62,6 +65,7 @@ use nym_verloc::{self, measurements::VerlocMeasurer}; use nym_wireguard::{WireguardGatewayData, peer_controller::PeerControlRequest}; use rand::rngs::OsRng; use rand::{CryptoRng, RngCore}; +use std::collections::HashMap; use std::net::SocketAddr; use std::ops::Deref; use std::path::Path; @@ -84,6 +88,7 @@ mod shared_network; pub struct GatewayTasksData { mnemonic: Arc>, + psq_kem_key: Arc, client_storage: nym_gateway::node::GatewayStorage, stats_storage: nym_gateway::node::PersistentStatsStorage, } @@ -105,7 +110,11 @@ impl GatewayTasksData { Ok(()) } - async fn new(config: &GatewayTasksConfig) -> Result { + async fn new( + config: &GatewayTasksConfig, + // this argument is temporary while we still derive KEM x25519 out of identity ed25519 + ed25519_identity: &ed25519::KeyPair, + ) -> Result { let client_storage = nym_gateway::node::GatewayStorage::init( &config.storage_paths.clients_storage, config.debug.message_retrieval_limit, @@ -120,6 +129,7 @@ impl GatewayTasksData { Ok(GatewayTasksData { mnemonic: Arc::new(config.storage_paths.load_mnemonic_from_file()?), + psq_kem_key: Arc::new(ed25519_identity.to_x25519()), client_storage, stats_storage, }) @@ -454,10 +464,14 @@ impl NymNode { let current_rotation_id = get_current_rotation_id(&config.mixnet.nym_api_urls, &config.mixnet.nyxd_urls).await?; + let ed25519_identity_keys = load_ed25519_identity_keypair( + &config.storage_paths.keys.ed25519_identity_storage_paths(), + )?; + let entry_gateway = + GatewayTasksData::new(&config.gateway_tasks, &ed25519_identity_keys).await?; + Ok(NymNode { - ed25519_identity_keys: Arc::new(load_ed25519_identity_keypair( - &config.storage_paths.keys.ed25519_identity_storage_paths(), - )?), + ed25519_identity_keys: Arc::new(ed25519_identity_keys), sphinx_key_manager: Some(SphinxKeyManager::try_load_or_regenerate( current_rotation_id, &config.storage_paths.keys.primary_x25519_sphinx_key_file, @@ -469,7 +483,7 @@ impl NymNode { description: load_node_description(&config.storage_paths.description)?, metrics: NymNodeMetrics::new(), verloc_stats: Default::default(), - entry_gateway: GatewayTasksData::new(&config.gateway_tasks).await?, + entry_gateway, upgrade_mode_state: UpgradeModeState::new( config.gateway_tasks.upgrade_mode.attester_public_key, ), @@ -631,6 +645,7 @@ impl NymNode { let mut gateway_tasks_builder = GatewayTasksBuilder::new( config.gateway, self.ed25519_identity_keys.clone(), + self.entry_gateway.psq_kem_key.clone(), self.entry_gateway.client_storage.clone(), mix_packet_sender, metrics_sender, @@ -760,6 +775,32 @@ impl NymNode { Ok(()) } + fn compute_kem_key_hashes(&self) -> (LPKEM, HashMap>) { + let kem = LPKEM::X25519; + let mut hashes = HashMap::new(); + + let kem_key_bytes = self.entry_gateway.psq_kem_key.public_key().as_bytes(); + + hashes.insert( + LPHashFunction::Blake3, + hash_encapsulation_key(&HashFunction::Blake3, DEFAULT_HASH_LEN, kem_key_bytes), + ); + hashes.insert( + LPHashFunction::Shake128, + hash_encapsulation_key(&HashFunction::SHAKE128, DEFAULT_HASH_LEN, kem_key_bytes), + ); + hashes.insert( + LPHashFunction::Shake256, + hash_encapsulation_key(&HashFunction::SHAKE256, DEFAULT_HASH_LEN, kem_key_bytes), + ); + hashes.insert( + LPHashFunction::Sha256, + hash_encapsulation_key(&HashFunction::SHA256, DEFAULT_HASH_LEN, kem_key_bytes), + ); + + (kem, hashes) + } + pub(crate) async fn build_http_server(&self) -> Result { let auxiliary_details = api_requests::v1::node::models::AuxiliaryDetails { location: self.config.host.location, @@ -833,11 +874,13 @@ impl NymNode { policy: None, }; - let lp_details = api_requests::v1::lewes_protocol::models::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(), - }; + let (kem, hashes) = self.compute_kem_key_hashes(); + let lp_details = api_requests::v1::lewes_protocol::models::LewesProtocol::new( + self.modes().entry, + self.config.gateway_tasks.lp.announced_control_port(), + self.config.gateway_tasks.lp.announced_data_port(), + ) + .with_kem_key_hashes(kem, hashes); let mut config = HttpServerConfig::new() .with_landing_page_assets(self.config.http.landing_page_assets_path.as_ref()) @@ -878,7 +921,7 @@ impl NymNode { let x25519_versioned_noise_key = if self.config.mixnet.debug.unsafe_disable_noise { None } else { - Some(VersionedNoiseKey { + Some(VersionedNoiseKeyV1 { supported_version: nym_noise::LATEST_NOISE_VERSION, x25519_pubkey: *self.x25519_noise_keys.public_key(), }) diff --git a/nym-registration-client/src/error.rs b/nym-registration-client/src/error.rs index 449d6d4608..932eb1bf54 100644 --- a/nym-registration-client/src/error.rs +++ b/nym-registration-client/src/error.rs @@ -93,6 +93,9 @@ pub enum RegistrationClientError { #[source] source: Box, }, + + #[error("failed to convert ed25519 pubkey into x25519 pubkey")] + X25519PubkeyConversionFailure, } impl RegistrationClientError { diff --git a/nym-registration-client/src/lib.rs b/nym-registration-client/src/lib.rs index 5843e4ecd4..796d56de74 100644 --- a/nym-registration-client/src/lib.rs +++ b/nym-registration-client/src/lib.rs @@ -29,6 +29,7 @@ pub use builder::config::{ pub use config::RegistrationMode; pub use error::RegistrationClientError; pub use lp_client::{LpConfig, LpRegistrationClient, NestedLpSession, error::LpClientError}; +use nym_lp::peer::LpRemotePeer; pub use types::{ LpRegistrationResult, MixnetRegistrationResult, RegistrationResult, WireguardRegistrationResult, }; @@ -154,35 +155,58 @@ impl RegistrationClient { } async fn register_lp(self) -> Result { - // Extract and validate LP addresses - let entry_lp_address = self.config.entry.node.lp_address.ok_or( + // Extract and validate LP data + let entry_lp_data = self.config.entry.node.lp_data.ok_or( RegistrationClientError::LpRegistrationNotPossible { node_id: self.config.entry.node.identity.to_base58_string(), }, )?; - let exit_lp_address = self.config.exit.node.lp_address.ok_or( + let exit_lp_data = self.config.exit.node.lp_data.ok_or( RegistrationClientError::LpRegistrationNotPossible { node_id: self.config.exit.node.identity.to_base58_string(), }, )?; - tracing::debug!("Entry gateway LP address: {entry_lp_address}"); - tracing::debug!("Exit gateway LP address: {exit_lp_address}"); + tracing::debug!("Entry gateway LP address: {}", entry_lp_data.address); + tracing::debug!("Exit gateway LP address: {}", exit_lp_data.address); // Generate fresh Ed25519 keypairs for LP registration // These are ephemeral and used only for the LP handshake protocol let entry_lp_keypair = Arc::new(ed25519::KeyPair::new(&mut OsRng)); let exit_lp_keypair = Arc::new(ed25519::KeyPair::new(&mut OsRng)); - // STEP 1: Establish outer session with entry gateway + // Step 1: Derive X25519 keys from Ed25519 for the gateways + let entry_x25519_public = self + .config + .entry + .node + .identity + .to_x25519() + .map_err(|_| RegistrationClientError::X25519PubkeyConversionFailure)?; + + let exit_x25519_public = self + .config + .exit + .node + .identity + .to_x25519() + .map_err(|_| RegistrationClientError::X25519PubkeyConversionFailure)?; + + let entry_peer = LpRemotePeer::new(self.config.entry.node.identity, entry_x25519_public) + .with_kem_key_digest(entry_lp_data.expected_kem_key_hash); + + let exit_peer = LpRemotePeer::new(self.config.exit.node.identity, exit_x25519_public) + .with_kem_key_digest(exit_lp_data.expected_kem_key_hash); + + // STEP 2: Establish outer session with entry gateway // This creates the LP session that will be used to forward packets to exit. // Uses packet-per-connection model: each handshake packet on new TCP connection. tracing::info!("Establishing outer session with entry gateway"); let mut entry_client = LpRegistrationClient::new_with_default_config( entry_lp_keypair.clone(), - self.config.entry.node.identity, - entry_lp_address, + entry_peer, + entry_lp_data.address, self.config.entry.node.ip_address, ); @@ -190,22 +214,18 @@ impl RegistrationClient { entry_client.perform_handshake().await.map_err(|source| { RegistrationClientError::EntryGatewayRegisterLp { gateway_id: self.config.entry.node.identity.to_base58_string(), - lp_address: entry_lp_address, + lp_address: entry_lp_data.address, source: Box::new(source), } })?; tracing::info!("Outer session with entry gateway established"); - // STEP 2: Use nested session to register with exit gateway via forwarding + // STEP 3: Use nested session to register with exit gateway via forwarding // This hides the client's IP address from the exit gateway tracing::info!("Registering with exit gateway via entry forwarding"); - let mut nested_session = NestedLpSession::new( - self.config.exit.node.identity.to_bytes(), - exit_lp_address.to_string(), - exit_lp_keypair, - self.config.exit.node.identity, - ); + let mut nested_session = + NestedLpSession::new(exit_lp_data.address.to_string(), exit_lp_keypair, exit_peer); // Perform handshake and registration with exit gateway (all via entry forwarding) let exit_gateway_data = nested_session @@ -220,13 +240,13 @@ impl RegistrationClient { .await .map_err(|source| RegistrationClientError::ExitGatewayRegisterLp { gateway_id: self.config.exit.node.identity.to_base58_string(), - lp_address: exit_lp_address, + lp_address: exit_lp_data.address, source: Box::new(source), })?; tracing::info!("Exit gateway registration completed via forwarding"); - // STEP 3: Register with entry gateway (packet-per-connection) + // STEP 4: Register with entry gateway (packet-per-connection) tracing::info!("Registering with entry gateway"); let entry_gateway_data = entry_client .register( @@ -238,7 +258,7 @@ impl RegistrationClient { .await .map_err(|source| RegistrationClientError::EntryGatewayRegisterLp { gateway_id: self.config.entry.node.identity.to_base58_string(), - lp_address: entry_lp_address, + lp_address: entry_lp_data.address, source: Box::new(source), })?; diff --git a/nym-registration-client/src/lp_client/client.rs b/nym-registration-client/src/lp_client/client.rs index 99afac0c88..7899cd223d 100644 --- a/nym-registration-client/src/lp_client/client.rs +++ b/nym-registration-client/src/lp_client/client.rs @@ -12,6 +12,7 @@ use nym_crypto::asymmetric::{ed25519, x25519}; use nym_lp::LpPacket; use nym_lp::codec::{OuterAeadKey, parse_lp_packet, serialize_lp_packet}; use nym_lp::message::ForwardPacketData; +use nym_lp::peer::{LpLocalPeer, LpRemotePeer}; use nym_lp::state_machine::{LpAction, LpInput, LpStateMachine}; use nym_lp_transport::traits::LpTransport; use nym_registration_common::{GatewayData, LpRegistrationRequest, LpRegistrationResponse}; @@ -36,11 +37,11 @@ use tokio::net::TcpStream; /// // Connection automatically closes after registration /// ``` pub struct LpRegistrationClient { - /// Client's Ed25519 identity keypair (used for PSQ authentication and X25519 derivation). - local_ed25519_keypair: Arc, + /// Encapsulates all the client keys needed for the Lewes Protocol. + lp_local_peer: LpLocalPeer, - /// Gateway's Ed25519 public key (from directory/discovery). - gateway_ed25519_public_key: ed25519::PublicKey, + /// Encapsulates all the gateway keys needed for the Lewes Protocol. + gateway_lp_peer: LpRemotePeer, /// Gateway LP listener address (host:port, e.g., "1.1.1.1:41264"). gateway_lp_address: SocketAddr, @@ -67,8 +68,8 @@ where /// Creates a new LP registration client. /// /// # Arguments - /// * `local_ed25519_keypair` - Client's Ed25519 identity keypair (for PSQ auth and X25519 derivation) - /// * `gateway_ed25519_public_key` - Gateway's Ed25519 public key (from directory/discovery) + /// * `local_ed25519_keypair` - Client's Ed25519 identity keypair + /// * `gateway_lp_peer` - Encapsulates all the gateway keys needed for the Lewes Protocol /// * `gateway_lp_address` - Gateway's LP listener socket address /// * `client_ip` - Client IP address for registration /// * `config` - Configuration for timeouts and TCP parameters (use `LpConfig::default()`) @@ -77,14 +78,16 @@ where /// This creates the client. Call `perform_handshake()` to establish the LP session. pub fn new( local_ed25519_keypair: Arc, - gateway_ed25519_public_key: ed25519::PublicKey, + gateway_lp_peer: LpRemotePeer, gateway_lp_address: SocketAddr, client_ip: IpAddr, config: LpConfig, ) -> Self { + let local_x25519_keypair = local_ed25519_keypair.to_x25519(); + let lp_local_peer = LpLocalPeer::new(local_ed25519_keypair, Arc::new(local_x25519_keypair)); Self { - local_ed25519_keypair, - gateway_ed25519_public_key, + lp_local_peer, + gateway_lp_peer, gateway_lp_address, state_machine: None, client_ip, @@ -97,7 +100,7 @@ where /// /// # Arguments /// * `local_ed25519_keypair` - Client's Ed25519 identity keypair - /// * `gateway_ed25519_public_key` - Gateway's Ed25519 public key + /// * `gateway_lp_peer` - Encapsulates all the gateway keys needed for the Lewes Protocol /// * `gateway_lp_address` - Gateway's LP listener socket address /// * `client_ip` - Client IP address for registration /// @@ -106,13 +109,13 @@ where /// For custom config, use `new()` directly. pub fn new_with_default_config( local_ed25519_keypair: Arc, - gateway_ed25519_public_key: ed25519::PublicKey, + gateway_lp_peer: LpRemotePeer, gateway_lp_address: SocketAddr, client_ip: IpAddr, ) -> Self { Self::new( local_ed25519_keypair, - gateway_ed25519_public_key, + gateway_lp_peer, gateway_lp_address, client_ip, LpConfig::default(), @@ -318,29 +321,15 @@ where // Ensure we have a TCP connection self.ensure_connected().await?; - // Step 1: Derive X25519 keys from Ed25519 for Noise protocol (internal to ClientHello) - // The Ed25519 keys are used for PSQ authentication and also converted to X25519 - let client_x25519_public = self - .local_ed25519_keypair - .public_key() - .to_x25519() - .map_err(|e| { - LpClientError::Crypto(format!("Failed to derive X25519 public key: {e}")) - })?; - - let gateway_x25519_public = self.gateway_ed25519_public_key.to_x25519().map_err(|e| { - LpClientError::Crypto(format!("Failed to derive X25519 public key: {e}")) - })?; - let timestamp = SystemTime::now() .duration_since(UNIX_EPOCH) .map_err(|_| LpClientError::Other("System time before UNIX epoch".into()))? .as_secs(); - // Step 2: Generate ClientHelloData with fresh salt and both public keys + // Step 1: Generate ClientHelloData with fresh salt and both public keys let client_hello_data = nym_lp::ClientHelloData::new_with_fresh_salt( - client_x25519_public, - *self.local_ed25519_keypair.public_key(), + *self.lp_local_peer.x25519().public_key(), + *self.lp_local_peer.ed25519().public_key(), timestamp, ); let salt = client_hello_data.salt; @@ -352,7 +341,7 @@ where receiver_index ); - // Step 3: Send ClientHello and receive Ack (persistent connection) + // Step 2: Send ClientHello and receive Ack (persistent connection) let client_hello_header = nym_lp::packet::LpHeader::new( nym_lp::BOOTSTRAP_RECEIVER_IDX, // session_id not yet established 0, // counter starts at 0 @@ -380,18 +369,17 @@ where } } - // Step 4: Create state machine as initiator with Ed25519 keys + // Step 3: Create state machine as initiator with Ed25519 keys // PSK derivation happens internally in the state machine constructor let mut state_machine = LpStateMachine::new( receiver_index, true, // is_initiator - self.local_ed25519_keypair.clone(), - &self.gateway_ed25519_public_key, - &gateway_x25519_public, + self.lp_local_peer.clone(), + self.gateway_lp_peer.clone(), &salt, )?; - // Step 5: Start handshake - get first packet to send (KKT request) + // Step 4: Start handshake - get first packet to send (KKT request) let mut pending_packet: Option = None; if let Some(action) = state_machine.process_input(LpInput::StartHandshake) { match action? { @@ -407,7 +395,7 @@ where } } - // Step 6: Handshake loop - all packets on persistent connection + // Step 5: Handshake loop - all packets on persistent connection loop { // Send pending packet if we have one if let Some(packet) = pending_packet.take() { @@ -1218,13 +1206,16 @@ mod tests { fn test_client_creation() { let mut rng = rand::thread_rng(); let keypair = Arc::new(ed25519::KeyPair::new(&mut rng)); - let gateway_key = *ed25519::KeyPair::new(&mut rng).public_key(); + let gateway_ed_keys = ed25519::KeyPair::new(&mut rng); + let gateway_x_keys = gateway_ed_keys.to_x25519(); + let gateway_peer = + LpRemotePeer::new(*gateway_ed_keys.public_key(), *gateway_x_keys.public_key()); let address = "127.0.0.1:41264".parse().unwrap(); let client_ip = "192.168.1.100".parse().unwrap(); let client = LpRegistrationClient::::new_with_default_config( keypair, - gateway_key, + gateway_peer, address, client_ip, ); diff --git a/nym-registration-client/src/lp_client/error.rs b/nym-registration-client/src/lp_client/error.rs index 2fc74e3fd1..2026e15991 100644 --- a/nym-registration-client/src/lp_client/error.rs +++ b/nym-registration-client/src/lp_client/error.rs @@ -55,10 +55,6 @@ pub enum LpClientError { #[error("Timeout waiting for {operation}")] Timeout { operation: String }, - /// Cryptographic operation failed - #[error("Cryptographic error: {0}")] - Crypto(String), - /// Another uncategorized error #[error("{0}")] Other(String), diff --git a/nym-registration-client/src/lp_client/nested_session.rs b/nym-registration-client/src/lp_client/nested_session.rs index 7fbdbbc11e..8f40f74c26 100644 --- a/nym-registration-client/src/lp_client/nested_session.rs +++ b/nym-registration-client/src/lp_client/nested_session.rs @@ -25,6 +25,7 @@ use nym_bandwidth_controller::BandwidthTicketProvider; use nym_credentials_interface::TicketType; use nym_crypto::asymmetric::{ed25519, x25519}; use nym_lp::codec::{OuterAeadKey, parse_lp_packet, serialize_lp_packet}; +use nym_lp::peer::{LpLocalPeer, LpRemotePeer}; use nym_lp::state_machine::{LpAction, LpInput, LpStateMachine}; use nym_lp::{LpMessage, LpPacket}; use nym_lp_transport::traits::LpTransport; @@ -55,17 +56,14 @@ use std::time::{SystemTime, UNIX_EPOCH}; /// let gateway_data = nested.handshake_and_register(&mut outer_client, ...).await?; /// ``` pub struct NestedLpSession { - /// Exit gateway's Ed25519 identity (32 bytes) - exit_identity: [u8; 32], - /// Exit gateway's LP address (e.g., "2.2.2.2:41264") exit_address: String, - /// Client's Ed25519 keypair (for PSQ authentication and X25519 derivation) - client_keypair: Arc, + /// Encapsulates all the client keys needed for the Lewes Protocol. + lp_local_peer: LpLocalPeer, - /// Exit gateway's Ed25519 public key - exit_public_key: ed25519::PublicKey, + /// Encapsulates all the exit gateway keys needed for the Lewes Protocol. + gateway_lp_peer: LpRemotePeer, /// LP state machine for exit gateway session (populated after handshake) state_machine: Option, @@ -75,21 +73,21 @@ impl NestedLpSession { /// Creates a new nested LP session handler. /// /// # Arguments - /// * `exit_identity` - Exit gateway's Ed25519 identity (32 bytes) /// * `exit_address` - Exit gateway's LP address (e.g., "2.2.2.2:41264") /// * `client_keypair` - Client's Ed25519 keypair - /// * `exit_public_key` - Exit gateway's Ed25519 public key + /// * `gateway_lp_peer` - Encapsulates all the gateway keys needed for the Lewes Protocol pub fn new( - exit_identity: [u8; 32], exit_address: String, client_keypair: Arc, - exit_public_key: ed25519::PublicKey, + gateway_lp_peer: LpRemotePeer, ) -> Self { + let local_x25519_keypair = client_keypair.to_x25519(); + let lp_local_peer = LpLocalPeer::new(client_keypair, Arc::new(local_x25519_keypair)); + Self { - exit_identity, exit_address, - client_keypair, - exit_public_key, + lp_local_peer, + gateway_lp_peer, state_machine: None, } } @@ -124,24 +122,15 @@ impl NestedLpSession { self.exit_address ); - // Step 1: Derive X25519 keys from Ed25519 for Noise protocol - let client_x25519_public = self.client_keypair.public_key().to_x25519().map_err(|e| { - LpClientError::Crypto(format!("Failed to derive X25519 public key: {}", e)) - })?; - - let gateway_x25519_public = self.exit_public_key.to_x25519().map_err(|e| { - LpClientError::Crypto(format!("Failed to derive X25519 public key: {e}")) - })?; - let timestamp = SystemTime::now() .duration_since(UNIX_EPOCH) .map_err(|_| LpClientError::Other("System time before UNIX epoch".into()))? .as_secs(); - // Step 2: Generate ClientHello for exit gateway + // Step 1: Generate ClientHello for exit gateway let client_hello_data = nym_lp::ClientHelloData::new_with_fresh_salt( - client_x25519_public, - *self.client_keypair.public_key(), + *self.lp_local_peer.x25519().public_key(), + *self.lp_local_peer.ed25519().public_key(), timestamp, ); let salt = client_hello_data.salt; @@ -152,7 +141,7 @@ impl NestedLpSession { client_hello_data.extract_timestamp() ); - // Step 3: Send ClientHello to exit gateway via forwarding + // Step 2: Send ClientHello to exit gateway via forwarding let client_hello_header = nym_lp::packet::LpHeader::new( nym_lp::BOOTSTRAP_RECEIVER_IDX, // Use constant for bootstrap session 0, // counter starts at 0 @@ -166,7 +155,7 @@ impl NestedLpSession { let client_hello_bytes = Self::serialize_packet(&client_hello_packet, None)?; let response_bytes = outer_client .send_forward_packet( - self.exit_identity, + self.gateway_lp_peer.ed25519().to_bytes(), self.exit_address.clone(), client_hello_bytes, ) @@ -191,17 +180,16 @@ impl NestedLpSession { } } - // Step 4: Create state machine for exit gateway handshake + // Step 3: Create state machine for exit gateway handshake let mut state_machine = LpStateMachine::new( receiver_index, true, // is_initiator - self.client_keypair.clone(), - &self.exit_public_key, - &gateway_x25519_public, + self.lp_local_peer.clone(), + self.gateway_lp_peer.clone(), &salt, )?; - // Step 5: Get initial packet from StartHandshake + // Step 4: Get initial packet from StartHandshake let mut pending_packet: Option = None; if let Some(action) = state_machine.process_input(LpInput::StartHandshake) { match action? { @@ -216,7 +204,7 @@ impl NestedLpSession { } } - // Step 6: Handshake loop - each packet on new connection via forwarding + // Step 5: Handshake loop - each packet on new connection via forwarding loop { if let Some(packet) = pending_packet.take() { tracing::trace!("Sending handshake packet to exit via forwarding"); @@ -356,7 +344,7 @@ impl NestedLpSession { let packet_bytes = Self::serialize_packet(&packet, outer_key.as_ref())?; outer_client .send_forward_packet( - self.exit_identity, + self.gateway_lp_peer.ed25519().to_bytes(), self.exit_address.clone(), packet_bytes, ) @@ -530,7 +518,7 @@ impl NestedLpSession { let packet_bytes = Self::serialize_packet(&packet, outer_key.as_ref())?; outer_client .send_forward_packet( - self.exit_identity, + self.gateway_lp_peer.ed25519().to_bytes(), self.exit_address.clone(), packet_bytes, ) @@ -743,7 +731,11 @@ impl NestedLpSession { let send_key = Self::get_send_key(state_machine); let packet_bytes = Self::serialize_packet(packet, send_key.as_ref())?; let response_bytes = outer_client - .send_forward_packet(self.exit_identity, self.exit_address.clone(), packet_bytes) + .send_forward_packet( + self.gateway_lp_peer.ed25519().to_bytes(), + self.exit_address.clone(), + packet_bytes, + ) .await?; let recv_key = Self::get_recv_key(state_machine); Self::parse_packet(&response_bytes, recv_key.as_ref()) diff --git a/nym-statistics-api/src/network_view.rs b/nym-statistics-api/src/network_view.rs index 1491a00903..d4e44d50a5 100644 --- a/nym-statistics-api/src/network_view.rs +++ b/nym-statistics-api/src/network_view.rs @@ -5,7 +5,7 @@ use anyhow::Result; use nym_task::ShutdownToken; use celes::Country; -use nym_validator_client::models::NymNodeDescription; +use nym_validator_client::models::NymNodeDescriptionV1; use std::collections::HashMap; use std::time::Duration; use std::{net::IpAddr, sync::Arc}; @@ -28,7 +28,8 @@ struct NodesQuerier { } impl NodesQuerier { - async fn current_nymnodes(&self) -> Result> { + #[allow(deprecated)] + async fn current_nymnodes(&self) -> Result> { Ok(self .client .get_all_described_nodes() diff --git a/nym-wallet/Cargo.lock b/nym-wallet/Cargo.lock index 0ebc2184e4..047a50110b 100644 --- a/nym-wallet/Cargo.lock +++ b/nym-wallet/Cargo.lock @@ -81,7 +81,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" dependencies = [ "crypto-common", - "generic-array", + "generic-array 0.14.7", ] [[package]] @@ -106,7 +106,7 @@ dependencies = [ "cipher", "ctr", "ghash", - "subtle", + "subtle 2.6.1", ] [[package]] @@ -259,7 +259,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" dependencies = [ "base64ct", - "blake2", + "blake2 0.10.6", "cpufeatures", "password-hash", ] @@ -681,7 +681,7 @@ dependencies = [ "ripemd", "secp256k1", "sha2 0.10.9", - "subtle", + "subtle 2.6.1", "zeroize", ] @@ -730,6 +730,18 @@ dependencies = [ "serde", ] +[[package]] +name = "blake2" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94cb07b0da6a73955f8fb85d24c466778e70cda767a568229b104f0264089330" +dependencies = [ + "byte-tools", + "crypto-mac", + "digest 0.8.1", + "opaque-debug 0.2.3", +] + [[package]] name = "blake2" version = "0.10.6" @@ -756,7 +768,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" dependencies = [ - "generic-array", + "generic-array 0.14.7", ] [[package]] @@ -765,7 +777,7 @@ version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ - "generic-array", + "generic-array 0.14.7", ] [[package]] @@ -799,22 +811,6 @@ dependencies = [ "piper", ] -[[package]] -name = "bls12_381" -version = "0.8.0" -source = "git+https://github.com/jstuczyn/bls12_381?branch=temp%2Fexperimental-serdect-updated#9bf520059cb28323fc51469cae86868ef4fa6fbd" -dependencies = [ - "digest 0.10.7", - "ff", - "group", - "pairing", - "rand_core 0.6.4", - "serde", - "serdect 0.3.0", - "subtle", - "zeroize", -] - [[package]] name = "bnum" version = "0.11.0" @@ -858,6 +854,12 @@ version = "3.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" +[[package]] +name = "byte-tools" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" + [[package]] name = "bytemuck" version = "1.22.0" @@ -1026,6 +1028,16 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "chacha" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddf3c081b5fba1e5615640aae998e0fbd10c24cbd897ee39ed754a77601a4862" +dependencies = [ + "byteorder", + "keystream", +] + [[package]] name = "chrono" version = "0.4.40" @@ -1423,9 +1435,9 @@ version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" dependencies = [ - "generic-array", + "generic-array 0.14.7", "rand_core 0.6.4", - "subtle", + "subtle 2.6.1", "zeroize", ] @@ -1435,11 +1447,21 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ - "generic-array", + "generic-array 0.14.7", "rand_core 0.6.4", "typenum", ] +[[package]] +name = "crypto-mac" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4434400df11d95d556bac068ddfedd482915eb18fe8bea89bc80b6e4b1c179e5" +dependencies = [ + "generic-array 0.12.4", + "subtle 1.0.0", +] + [[package]] name = "cssparser" version = "0.27.2" @@ -1505,7 +1527,7 @@ dependencies = [ "fiat-crypto", "rustc_version", "serde", - "subtle", + "subtle 2.6.1", "zeroize", ] @@ -1746,13 +1768,22 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "digest" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5" +dependencies = [ + "generic-array 0.12.4", +] + [[package]] name = "digest" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" dependencies = [ - "generic-array", + "generic-array 0.14.7", ] [[package]] @@ -1764,7 +1795,7 @@ dependencies = [ "block-buffer 0.10.4", "const-oid", "crypto-common", - "subtle", + "subtle 2.6.1", ] [[package]] @@ -1956,7 +1987,7 @@ dependencies = [ "rand_core 0.6.4", "serde", "sha2 0.10.9", - "subtle", + "subtle 2.6.1", "zeroize", ] @@ -1991,7 +2022,7 @@ dependencies = [ "crypto-bigint", "digest 0.10.7", "ff", - "generic-array", + "generic-array 0.14.7", "group", "hkdf", "pem-rfc7468", @@ -1999,7 +2030,7 @@ dependencies = [ "rand_core 0.6.4", "sec1", "serdect 0.2.0", - "subtle", + "subtle 2.6.1", "zeroize", ] @@ -2179,7 +2210,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" dependencies = [ "rand_core 0.6.4", - "subtle", + "subtle 2.6.1", ] [[package]] @@ -2526,6 +2557,15 @@ dependencies = [ "windows 0.58.0", ] +[[package]] +name = "generic-array" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffdf9f34f1447443d37393cc6c2b8313aebddcd96906caf34e54c68d8e57d7bd" +dependencies = [ + "typenum", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -2591,7 +2631,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" dependencies = [ - "opaque-debug", + "opaque-debug 0.3.1", "polyval", ] @@ -2705,7 +2745,7 @@ checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" dependencies = [ "ff", "rand_core 0.6.4", - "subtle", + "subtle 2.6.1", ] [[package]] @@ -3458,7 +3498,7 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" dependencies = [ - "generic-array", + "generic-array 0.14.7", ] [[package]] @@ -3709,6 +3749,12 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "keystream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33070833c9ee02266356de0c43f723152bd38bd96ddf52c82b3af10c9138b28" + [[package]] name = "kuchikiki" version = "0.8.2" @@ -3800,6 +3846,18 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe7db12097d22ec582439daf8618b8fdd1a7bef6270e9af3b1ebcd30893cf413" +[[package]] +name = "lioness" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ae926706ba42c425c9457121178330d75e273df2e82e28b758faf3de3a9acb9" +dependencies = [ + "arrayref", + "blake2 0.8.1", + "chacha", + "keystream", +] + [[package]] name = "litemap" version = "0.7.5" @@ -4153,9 +4211,10 @@ dependencies = [ [[package]] name = "nym-api-requests" -version = "0.1.0" +version = "1.20.1" dependencies = [ "bs58", + "celes", "cosmrs", "cosmwasm-std", "ecdsa", @@ -4179,6 +4238,8 @@ dependencies = [ "serde", "serde_json", "sha2 0.10.9", + "strum", + "strum_macros", "tendermint", "tendermint-rpc", "thiserror 2.0.12", @@ -4189,7 +4250,7 @@ dependencies = [ [[package]] name = "nym-bin-common" -version = "0.6.0" +version = "1.20.1" dependencies = [ "const-str", "log", @@ -4201,9 +4262,26 @@ dependencies = [ "vergen", ] +[[package]] +name = "nym-bls12_381-fork" +version = "0.8.0-forked" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce84633751030f960a2fd167b5270ec21da4c40d9b6400e1b56676a682fe6f3d" +dependencies = [ + "digest 0.10.7", + "ff", + "group", + "pairing", + "rand_core 0.6.4", + "serde", + "serdect 0.3.0", + "subtle 2.6.1", + "zeroize", +] + [[package]] name = "nym-coconut-dkg-common" -version = "0.1.0" +version = "1.20.1" dependencies = [ "cosmwasm-schema", "cosmwasm-std", @@ -4216,29 +4294,29 @@ dependencies = [ [[package]] name = "nym-compact-ecash" -version = "0.1.0" +version = "1.20.1" dependencies = [ "bincode", - "bls12_381", "bs58", "cfg-if", "digest 0.10.7", "ff", "group", "itertools 0.14.0", + "nym-bls12_381-fork", "nym-network-defaults", "nym-pemstore", "rand 0.8.5", "serde", "sha2 0.10.9", - "subtle", + "subtle 2.6.1", "thiserror 2.0.12", "zeroize", ] [[package]] name = "nym-config" -version = "0.1.0" +version = "1.20.1" dependencies = [ "dirs 6.0.0", "handlebars", @@ -4252,7 +4330,7 @@ dependencies = [ [[package]] name = "nym-contracts-common" -version = "0.5.0" +version = "1.20.1" dependencies = [ "bs58", "cosmwasm-schema", @@ -4266,9 +4344,9 @@ dependencies = [ [[package]] name = "nym-credentials-interface" -version = "0.1.0" +version = "1.20.1" dependencies = [ - "bls12_381", + "nym-bls12_381-fork", "nym-compact-ecash", "nym-ecash-time", "nym-network-defaults", @@ -4284,7 +4362,7 @@ dependencies = [ [[package]] name = "nym-crypto" -version = "0.4.0" +version = "1.20.1" dependencies = [ "base64 0.22.1", "bs58", @@ -4305,7 +4383,7 @@ dependencies = [ [[package]] name = "nym-ecash-contract-common" -version = "0.1.0" +version = "1.20.1" dependencies = [ "bs58", "cosmwasm-schema", @@ -4318,7 +4396,7 @@ dependencies = [ [[package]] name = "nym-ecash-signer-check-types" -version = "0.1.0" +version = "1.20.1" dependencies = [ "nym-coconut-dkg-common", "nym-crypto", @@ -4333,14 +4411,14 @@ dependencies = [ [[package]] name = "nym-ecash-time" -version = "0.1.0" +version = "1.20.1" dependencies = [ "time", ] [[package]] name = "nym-exit-policy" -version = "0.1.0" +version = "1.20.1" dependencies = [ "serde", "serde_json", @@ -4351,7 +4429,7 @@ dependencies = [ [[package]] name = "nym-group-contract-common" -version = "0.1.0" +version = "1.20.1" dependencies = [ "cosmwasm-schema", "cw-controllers", @@ -4362,7 +4440,7 @@ dependencies = [ [[package]] name = "nym-http-api-client" -version = "0.1.0" +version = "1.20.1" dependencies = [ "async-trait", "bincode", @@ -4392,7 +4470,7 @@ dependencies = [ [[package]] name = "nym-http-api-client-macro" -version = "0.1.0" +version = "1.20.1" dependencies = [ "proc-macro-crate 3.3.0", "proc-macro2", @@ -4403,7 +4481,7 @@ dependencies = [ [[package]] name = "nym-http-api-common" -version = "0.1.0" +version = "1.20.1" dependencies = [ "bincode", "serde", @@ -4413,7 +4491,7 @@ dependencies = [ [[package]] name = "nym-mixnet-contract-common" -version = "0.6.0" +version = "1.20.1" dependencies = [ "bs58", "cosmwasm-schema", @@ -4434,7 +4512,7 @@ dependencies = [ [[package]] name = "nym-multisig-contract-common" -version = "0.1.0" +version = "1.20.1" dependencies = [ "cosmwasm-schema", "cosmwasm-std", @@ -4449,7 +4527,7 @@ dependencies = [ [[package]] name = "nym-network-defaults" -version = "0.1.0" +version = "1.20.1" dependencies = [ "cargo_metadata 0.19.2", "dotenvy", @@ -4463,7 +4541,7 @@ dependencies = [ [[package]] name = "nym-node-requests" -version = "0.1.0" +version = "1.20.1" dependencies = [ "async-trait", "celes", @@ -4488,7 +4566,7 @@ dependencies = [ [[package]] name = "nym-noise-keys" -version = "0.1.0" +version = "1.20.1" dependencies = [ "nym-crypto", "schemars", @@ -4498,7 +4576,7 @@ dependencies = [ [[package]] name = "nym-pemstore" -version = "0.3.0" +version = "1.20.1" dependencies = [ "pem", "tracing", @@ -4507,7 +4585,7 @@ dependencies = [ [[package]] name = "nym-performance-contract-common" -version = "0.1.0" +version = "1.20.1" dependencies = [ "cosmwasm-schema", "cosmwasm-std", @@ -4520,7 +4598,7 @@ dependencies = [ [[package]] name = "nym-serde-helpers" -version = "0.1.0" +version = "1.20.1" dependencies = [ "base64 0.22.1", "bs58", @@ -4531,18 +4609,19 @@ dependencies = [ [[package]] name = "nym-sphinx-types" -version = "0.2.0" +version = "1.20.1" dependencies = [ + "sphinx-packet", "thiserror 2.0.12", ] [[package]] name = "nym-store-cipher" -version = "0.1.0" +version = "1.20.1" dependencies = [ "aes-gcm", "argon2", - "generic-array", + "generic-array 0.14.7", "getrandom 0.2.15", "rand 0.8.5", "serde", @@ -4553,7 +4632,7 @@ dependencies = [ [[package]] name = "nym-ticketbooks-merkle" -version = "0.1.0" +version = "1.20.1" dependencies = [ "nym-credentials-interface", "nym-serde-helpers", @@ -4567,7 +4646,7 @@ dependencies = [ [[package]] name = "nym-types" -version = "1.0.0" +version = "1.20.1" dependencies = [ "base64 0.22.1", "cosmrs", @@ -4597,7 +4676,7 @@ dependencies = [ [[package]] name = "nym-upgrade-mode-check" -version = "0.1.0" +version = "1.20.1" dependencies = [ "jwt-simple", "nym-crypto", @@ -4613,7 +4692,7 @@ dependencies = [ [[package]] name = "nym-validator-client" -version = "0.1.0" +version = "1.20.1" dependencies = [ "async-trait", "base64 0.22.1", @@ -4662,7 +4741,7 @@ dependencies = [ [[package]] name = "nym-vesting-contract-common" -version = "0.7.0" +version = "1.20.1" dependencies = [ "cosmwasm-schema", "cosmwasm-std", @@ -4708,7 +4787,7 @@ dependencies = [ [[package]] name = "nym-wireguard-types" -version = "0.1.0" +version = "1.20.1" dependencies = [ "base64 0.22.1", "nym-crypto", @@ -4960,6 +5039,12 @@ dependencies = [ "portable-atomic", ] +[[package]] +name = "opaque-debug" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c" + [[package]] name = "opaque-debug" version = "0.3.1" @@ -5157,7 +5242,7 @@ checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" dependencies = [ "base64ct", "rand_core 0.6.4", - "subtle", + "subtle 2.6.1", ] [[package]] @@ -5533,7 +5618,7 @@ checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" dependencies = [ "cfg-if", "cpufeatures", - "opaque-debug", + "opaque-debug 0.3.1", "universal-hash", ] @@ -5865,6 +5950,16 @@ dependencies = [ "getrandom 0.3.2", ] +[[package]] +name = "rand_distr" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31" +dependencies = [ + "num-traits", + "rand 0.8.5", +] + [[package]] name = "rand_hc" version = "0.2.0" @@ -6078,7 +6173,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" dependencies = [ "hmac", - "subtle", + "subtle 2.6.1", ] [[package]] @@ -6152,7 +6247,7 @@ dependencies = [ "sha2 0.10.9", "signature", "spki", - "subtle", + "subtle 2.6.1", "zeroize", ] @@ -6226,7 +6321,7 @@ dependencies = [ "ring", "rustls-pki-types", "rustls-webpki 0.103.1", - "subtle", + "subtle 2.6.1", "zeroize", ] @@ -6377,10 +6472,10 @@ checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" dependencies = [ "base16ct", "der", - "generic-array", + "generic-array 0.14.7", "pkcs8", "serdect 0.2.0", - "subtle", + "subtle 2.6.1", "zeroize", ] @@ -6672,7 +6767,7 @@ dependencies = [ "cfg-if", "cpufeatures", "digest 0.9.0", - "opaque-debug", + "opaque-debug 0.3.1", ] [[package]] @@ -6831,6 +6926,32 @@ dependencies = [ "system-deps", ] +[[package]] +name = "sphinx-packet" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c26f0c20d909fdda1c5d0ece3973127ca421984d55b000215df365e93722fc6e" +dependencies = [ + "aes", + "arrayref", + "blake2 0.8.1", + "bs58", + "byteorder", + "chacha", + "ctr", + "curve25519-dalek", + "digest 0.10.7", + "hkdf", + "hmac", + "lioness", + "rand 0.8.5", + "rand_distr", + "sha2 0.10.9", + "subtle 2.6.1", + "x25519-dalek", + "zeroize", +] + [[package]] name = "spin" version = "0.9.8" @@ -6911,6 +7032,12 @@ dependencies = [ "syn 2.0.100", ] +[[package]] +name = "subtle" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d67a5a62ba6e01cb2192ff309324cb4875d0c451d55fe2319433abe7a05a8ee" + [[package]] name = "subtle" version = "2.6.1" @@ -7499,7 +7626,7 @@ dependencies = [ "serde_repr", "sha2 0.10.9", "signature", - "subtle", + "subtle 2.6.1", "subtle-encoding", "tendermint-proto", "time", @@ -7554,7 +7681,7 @@ dependencies = [ "serde", "serde_bytes", "serde_json", - "subtle", + "subtle 2.6.1", "subtle-encoding", "tendermint", "tendermint-config", @@ -8115,7 +8242,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" dependencies = [ "crypto-common", - "subtle", + "subtle 2.6.1", ] [[package]] diff --git a/tools/nym-lp-client/src/client.rs b/tools/nym-lp-client/src/client.rs index c9d0d58bbb..fe78da54b8 100644 --- a/tools/nym-lp-client/src/client.rs +++ b/tools/nym-lp-client/src/client.rs @@ -32,6 +32,7 @@ use tracing::{debug, info, trace}; use crate::topology::{GatewayInfo, SpeedtestTopology}; use nym_ip_packet_requests::v8::request::IpPacketRequest; +use nym_lp::peer::LpRemotePeer; use nym_sphinx::forwarding::packet::MixPacket; /// Conv ID for KCP - hash of source and destination addresses @@ -118,9 +119,12 @@ impl SpeedtestClient { let client_ip = "0.0.0.0".parse()?; + let gw_peer = LpRemotePeer::new(self.gateway.identity, self.gateway.identity.to_x25519()?) + .with_kem_key_digest(self.gateway.kem_key_hash.clone()); + let mut lp_client = LpRegistrationClient::::new_with_default_config( self.identity_keypair.clone(), - self.gateway.identity, + gw_peer, self.gateway.lp_address, client_ip, ); @@ -161,9 +165,12 @@ impl SpeedtestClient { let client_ip = "0.0.0.0".parse()?; + let gw_peer = LpRemotePeer::new(self.gateway.identity, self.gateway.identity.to_x25519()?) + .with_kem_key_digest(self.gateway.kem_key_hash.clone()); + let mut lp_client = LpRegistrationClient::new_with_default_config( self.identity_keypair.clone(), - self.gateway.identity, + gw_peer, self.gateway.lp_address, client_ip, ); diff --git a/tools/nym-lp-client/src/topology.rs b/tools/nym-lp-client/src/topology.rs index 9cb44206a1..cbc5a35699 100644 --- a/tools/nym-lp-client/src/topology.rs +++ b/tools/nym-lp-client/src/topology.rs @@ -27,6 +27,8 @@ const LP_DATA_PORT: u16 = 51264; #[derive(Debug, Clone)] pub struct GatewayInfo { pub identity: ed25519::PublicKey, + pub kem_key_hash: Vec, + pub sphinx_key: nym_crypto::asymmetric::x25519::PublicKey, /// Mix host (IP:port for Sphinx mixing) pub mix_host: SocketAddr, @@ -181,41 +183,42 @@ impl SpeedtestTopology { /// Extract gateway info for LP connections from a SkimmedNode fn gateway_info_from_skimmed(node: &SkimmedNode) -> Result { - let first_ip = node - .ip_addresses - .first() - .ok_or_else(|| anyhow!("node has no IP addresses"))?; - - // LP default control port - const LP_CONTROL_PORT: u16 = 41264; - - Ok(GatewayInfo { - identity: node.ed25519_identity_pubkey, - sphinx_key: node.x25519_sphinx_pubkey, - mix_host: SocketAddr::new(*first_ip, node.mix_port), - lp_address: SocketAddr::new(*first_ip, LP_CONTROL_PORT), - lp_data_address: SocketAddr::new(*first_ip, LP_DATA_PORT), - }) + todo!("insufficient information to convert into GatewayInfo") + // let first_ip = node + // .ip_addresses + // .first() + // .ok_or_else(|| anyhow!("node has no IP addresses"))?; + // + // // LP default control port + // const LP_CONTROL_PORT: u16 = 41264; + // + // Ok(GatewayInfo { + // identity: node.ed25519_identity_pubkey, + // sphinx_key: node.x25519_sphinx_pubkey, + // mix_host: SocketAddr::new(*first_ip, node.mix_port), + // lp_address: SocketAddr::new(*first_ip, LP_CONTROL_PORT), + // lp_data_address: SocketAddr::new(*first_ip, LP_DATA_PORT), + // }) } -#[cfg(test)] -mod tests { - use super::*; - - #[tokio::test] - #[ignore = "requires network access"] - async fn test_fetch_topology() { - let nym_api = Url::parse("https://validator.nymtech.net/api").unwrap(); - let topology = SpeedtestTopology::fetch(&nym_api).await.unwrap(); - - assert!(topology.gateway_count() > 0); - println!("Found {} gateways", topology.gateway_count()); - - let mut rng = rand::thread_rng(); - let gateway = topology.random_gateway(&mut rng).unwrap(); - println!("Selected gateway: {:?}", gateway.identity); - - let route = topology.random_route_to_gateway(&mut rng, gateway).unwrap(); - println!("Route has {} hops", route.len()); - } -} +// #[cfg(test)] +// mod tests { +// use super::*; +// +// #[tokio::test] +// #[ignore = "requires network access"] +// async fn test_fetch_topology() { +// let nym_api = Url::parse("https://validator.nymtech.net/api").unwrap(); +// let topology = SpeedtestTopology::fetch(&nym_api).await.unwrap(); +// +// assert!(topology.gateway_count() > 0); +// println!("Found {} gateways", topology.gateway_count()); +// +// let mut rng = rand::thread_rng(); +// let gateway = topology.random_gateway(&mut rng).unwrap(); +// println!("Selected gateway: {:?}", gateway.identity); +// +// let route = topology.random_route_to_gateway(&mut rng, gateway).unwrap(); +// println!("Route has {} hops", route.len()); +// } +// } diff --git a/tools/ts-rs-cli/src/main.rs b/tools/ts-rs-cli/src/main.rs index 9c80766327..546d6bd316 100644 --- a/tools/ts-rs-cli/src/main.rs +++ b/tools/ts-rs-cli/src/main.rs @@ -1,7 +1,7 @@ #![allow(deprecated)] use nym_api_requests::models::{ - AnnotationResponse, DeclaredRoles, DescribedNodeType, GatewayCoreStatusResponse, + AnnotationResponse, DeclaredRolesV1, DescribedNodeTypeV1, GatewayCoreStatusResponse, HistoricalPerformanceResponse, HistoricalUptimeResponse, MixnodeCoreStatusResponse, MixnodeStatus, MixnodeStatusResponse, NodeAnnotation, NodeDatePerformanceResponse, NodePerformanceResponse, PerformanceHistoryResponse, StakeSaturationResponse, @@ -152,8 +152,8 @@ fn main() -> anyhow::Result<()> { do_export!(UptimeHistoryResponse); do_export!(HistoricalUptimeResponse); do_export!(HistoricalPerformanceResponse); - do_export!(DescribedNodeType); - do_export!(DeclaredRoles); + do_export!(DescribedNodeTypeV1); + do_export!(DeclaredRolesV1); do_export!(PaginatedResponse); do_export!(Pagination); diff --git a/wasm/mix-fetch/src/helpers.rs b/wasm/mix-fetch/src/helpers.rs index 9476d61715..27b8513e70 100644 --- a/wasm/mix-fetch/src/helpers.rs +++ b/wasm/mix-fetch/src/helpers.rs @@ -25,7 +25,7 @@ pub(crate) async fn get_network_requester( url::Url::parse(&nym_api_url.unwrap_or(NYM_API_URL.to_string()))?, None, ); - let nodes = client.get_all_described_nodes().await?; + let nodes = client.get_all_described_nodes_v2().await?; let providers: Vec<_> = nodes .iter() .filter_map(|node| {