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| {