Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a66b2ae006 | |||
| 48045f79d7 | |||
| 4188c9fce4 | |||
| 7c104173b0 | |||
| 32fc3c1e70 | |||
| cd61e7c903 | |||
| cc4dcffc42 | |||
| 162b74293e | |||
| 7e2e122725 | |||
| cc28575176 | |||
| c6675bb207 | |||
| cb1731494f | |||
| bfa52f7634 | |||
| 690a18953a | |||
| 8f950eece4 | |||
| 1ee8a5a19b | |||
| ddf7c3b860 | |||
| eabad283fa | |||
| 6cf8d79988 | |||
| d21eff3b7f |
Generated
+52
@@ -4888,6 +4888,7 @@ dependencies = [
|
||||
"nym-mixnet-contract-common",
|
||||
"nym-network-defaults",
|
||||
"nym-node-requests",
|
||||
"nym-noise-keys",
|
||||
"nym-serde-helpers",
|
||||
"nym-ticketbooks-merkle",
|
||||
"rand_chacha 0.3.1",
|
||||
@@ -5999,8 +6000,11 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"dashmap",
|
||||
"futures",
|
||||
"nym-crypto",
|
||||
"nym-noise",
|
||||
"nym-sphinx",
|
||||
"nym-task",
|
||||
"rand 0.8.5",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"tokio-util",
|
||||
@@ -6213,6 +6217,8 @@ dependencies = [
|
||||
"nym-network-requester",
|
||||
"nym-node-metrics",
|
||||
"nym-node-requests",
|
||||
"nym-noise",
|
||||
"nym-noise-keys",
|
||||
"nym-nonexhaustive-delayqueue",
|
||||
"nym-pemstore",
|
||||
"nym-sphinx-acknowledgements",
|
||||
@@ -6276,6 +6282,7 @@ dependencies = [
|
||||
"nym-crypto",
|
||||
"nym-exit-policy",
|
||||
"nym-http-api-client",
|
||||
"nym-noise-keys",
|
||||
"nym-wireguard-types",
|
||||
"rand_chacha 0.3.1",
|
||||
"schemars",
|
||||
@@ -6411,6 +6418,35 @@ dependencies = [
|
||||
"wasmtimer",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nym-noise"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"arc-swap",
|
||||
"bytes",
|
||||
"futures",
|
||||
"nym-crypto",
|
||||
"nym-noise-keys",
|
||||
"pin-project",
|
||||
"sha2 0.10.9",
|
||||
"snow",
|
||||
"strum 0.26.3",
|
||||
"thiserror 2.0.12",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nym-noise-keys"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"nym-crypto",
|
||||
"schemars",
|
||||
"serde",
|
||||
"utoipa",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nym-nonexhaustive-delayqueue"
|
||||
version = "0.1.0"
|
||||
@@ -9276,6 +9312,22 @@ dependencies = [
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "snow"
|
||||
version = "0.9.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "850948bee068e713b8ab860fe1adc4d109676ab4c3b621fd8147f06b261f2f85"
|
||||
dependencies = [
|
||||
"aes-gcm",
|
||||
"blake2 0.10.6",
|
||||
"chacha20poly1305",
|
||||
"curve25519-dalek",
|
||||
"rand_core 0.6.4",
|
||||
"rustc_version 0.4.1",
|
||||
"sha2 0.10.9",
|
||||
"subtle 2.6.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.5.9"
|
||||
|
||||
+7
-2
@@ -33,7 +33,8 @@ members = [
|
||||
"common/commands",
|
||||
"common/config",
|
||||
"common/cosmwasm-smart-contracts/coconut-dkg",
|
||||
"common/cosmwasm-smart-contracts/contracts-common", "common/cosmwasm-smart-contracts/easy_addr",
|
||||
"common/cosmwasm-smart-contracts/contracts-common",
|
||||
"common/cosmwasm-smart-contracts/easy_addr",
|
||||
"common/cosmwasm-smart-contracts/ecash-contract",
|
||||
"common/cosmwasm-smart-contracts/group-contract",
|
||||
"common/cosmwasm-smart-contracts/mixnet-contract",
|
||||
@@ -64,6 +65,8 @@ members = [
|
||||
"common/nym-id",
|
||||
"common/nym-metrics",
|
||||
"common/nym_offline_compact_ecash",
|
||||
"common/nymnoise",
|
||||
"common/nymnoise/keys",
|
||||
"common/nymsphinx",
|
||||
"common/nymsphinx/acknowledgements",
|
||||
"common/nymsphinx/addressing",
|
||||
@@ -131,7 +134,8 @@ members = [
|
||||
"tools/internal/testnet-manager",
|
||||
"tools/internal/testnet-manager",
|
||||
"tools/internal/testnet-manager/dkg-bypass-contract",
|
||||
"tools/internal/testnet-manager/dkg-bypass-contract", "tools/internal/validator-status-check",
|
||||
"tools/internal/testnet-manager/dkg-bypass-contract",
|
||||
"tools/internal/validator-status-check",
|
||||
"tools/nym-cli",
|
||||
"tools/nym-id-cli",
|
||||
"tools/nym-nr-query",
|
||||
@@ -305,6 +309,7 @@ serde_with = "3.9.0"
|
||||
serde_yaml = "0.9.25"
|
||||
sha2 = "0.10.9"
|
||||
si-scale = "0.2.3"
|
||||
snow = "0.9.6"
|
||||
sphinx-packet = "=0.6.0"
|
||||
sqlx = "0.7.4"
|
||||
strum = "0.26"
|
||||
|
||||
@@ -44,7 +44,6 @@ nym-sphinx = { path = "../nymsphinx" }
|
||||
nym-statistics-common = { path = "../statistics" }
|
||||
nym-pemstore = { path = "../pemstore" }
|
||||
nym-topology = { path = "../topology", features = ["persistence"] }
|
||||
nym-mixnet-client = { path = "../client-libs/mixnet-client", default-features = false }
|
||||
nym-validator-client = { path = "../client-libs/validator-client", default-features = false }
|
||||
nym-task = { path = "../task" }
|
||||
nym-credentials-interface = { path = "../credentials-interface" }
|
||||
@@ -57,6 +56,9 @@ nym-client-core-surb-storage = { path = "./surb-storage" }
|
||||
nym-client-core-gateways-storage = { path = "./gateways-storage" }
|
||||
nym-ecash-time = { path = "../ecash-time" }
|
||||
|
||||
[target."cfg(not(target_arch = \"wasm32\"))".dependencies]
|
||||
nym-mixnet-client = { path = "../client-libs/mixnet-client", default-features = false }
|
||||
|
||||
### For serving prometheus metrics
|
||||
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.hyper]
|
||||
workspace = true
|
||||
|
||||
@@ -16,9 +16,14 @@ tokio-util = { workspace = true, features = ["codec"], optional = true }
|
||||
tokio-stream = { workspace = true }
|
||||
|
||||
# internal
|
||||
nym-noise = { path = "../../nymnoise" }
|
||||
nym-sphinx = { path = "../../nymsphinx" }
|
||||
nym-task = { path = "../../task", optional = true }
|
||||
|
||||
[features]
|
||||
default = ["client"]
|
||||
client = ["tokio-util", "nym-task", "tokio/net", "tokio/rt"]
|
||||
client = ["tokio-util", "nym-task", "tokio/net", "tokio/rt"]
|
||||
|
||||
[dev-dependencies]
|
||||
nym-crypto = { path = "../../crypto" }
|
||||
rand = { workspace = true }
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
|
||||
use dashmap::DashMap;
|
||||
use futures::StreamExt;
|
||||
use nym_noise::config::NoiseConfig;
|
||||
use nym_noise::upgrade_noise_initiator;
|
||||
use nym_sphinx::addressing::nodes::NymNodeRoutingAddress;
|
||||
use nym_sphinx::framing::codec::NymCodec;
|
||||
use nym_sphinx::framing::packet::FramedNymPacket;
|
||||
@@ -59,6 +61,7 @@ pub trait SendWithoutResponse {
|
||||
|
||||
pub struct Client {
|
||||
active_connections: ActiveConnections,
|
||||
noise_config: NoiseConfig,
|
||||
connections_count: Arc<AtomicUsize>,
|
||||
config: Config,
|
||||
}
|
||||
@@ -104,6 +107,7 @@ impl ConnectionSender {
|
||||
|
||||
struct ManagedConnection {
|
||||
address: SocketAddr,
|
||||
noise_config: NoiseConfig,
|
||||
message_receiver: ReceiverStream<FramedNymPacket>,
|
||||
connection_timeout: Duration,
|
||||
current_reconnection: Arc<AtomicU32>,
|
||||
@@ -112,12 +116,14 @@ struct ManagedConnection {
|
||||
impl ManagedConnection {
|
||||
fn new(
|
||||
address: SocketAddr,
|
||||
noise_config: NoiseConfig,
|
||||
message_receiver: mpsc::Receiver<FramedNymPacket>,
|
||||
connection_timeout: Duration,
|
||||
current_reconnection: Arc<AtomicU32>,
|
||||
) -> Self {
|
||||
ManagedConnection {
|
||||
address,
|
||||
noise_config,
|
||||
message_receiver: ReceiverStream::new(message_receiver),
|
||||
connection_timeout,
|
||||
current_reconnection,
|
||||
@@ -132,9 +138,21 @@ impl ManagedConnection {
|
||||
Ok(stream_res) => match stream_res {
|
||||
Ok(stream) => {
|
||||
debug!("Managed to establish connection to {}", self.address);
|
||||
// if we managed to connect, reset the reconnection count (whatever it might have been)
|
||||
|
||||
let noise_stream =
|
||||
match upgrade_noise_initiator(stream, &self.noise_config).await {
|
||||
Ok(noise_stream) => noise_stream,
|
||||
Err(err) => {
|
||||
error!("Failed to perform Noise handshake with {address} - {err}");
|
||||
// we failed to finish the noise handshake - increase reconnection attempt
|
||||
self.current_reconnection.fetch_add(1, Ordering::SeqCst);
|
||||
return;
|
||||
}
|
||||
};
|
||||
// if we managed to connect AND do the noise handshake, reset the reconnection count (whatever it might have been)
|
||||
self.current_reconnection.store(0, Ordering::Release);
|
||||
Framed::new(stream, NymCodec)
|
||||
debug!("Noise initiator handshake completed for {:?}", address);
|
||||
Framed::new(noise_stream, NymCodec)
|
||||
}
|
||||
Err(err) => {
|
||||
debug!("failed to establish connection to {address} (err: {err})",);
|
||||
@@ -167,9 +185,14 @@ impl ManagedConnection {
|
||||
}
|
||||
|
||||
impl Client {
|
||||
pub fn new(config: Config, connections_count: Arc<AtomicUsize>) -> Client {
|
||||
pub fn new(
|
||||
config: Config,
|
||||
noise_config: NoiseConfig,
|
||||
connections_count: Arc<AtomicUsize>,
|
||||
) -> Client {
|
||||
Client {
|
||||
active_connections: Default::default(),
|
||||
noise_config,
|
||||
connections_count,
|
||||
config,
|
||||
}
|
||||
@@ -224,6 +247,7 @@ impl Client {
|
||||
let initial_connection_timeout = self.config.initial_connection_timeout;
|
||||
|
||||
let connections_count = self.connections_count.clone();
|
||||
let noise_config = self.noise_config.clone();
|
||||
tokio::spawn(async move {
|
||||
// before executing the manager, wait for what was specified, if anything
|
||||
if let Some(backoff) = backoff {
|
||||
@@ -234,6 +258,7 @@ impl Client {
|
||||
connections_count.fetch_add(1, Ordering::SeqCst);
|
||||
ManagedConnection::new(
|
||||
address.into(),
|
||||
noise_config,
|
||||
receiver,
|
||||
initial_connection_timeout,
|
||||
current_reconnection_attempt,
|
||||
@@ -302,8 +327,12 @@ impl SendWithoutResponse for Client {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use nym_crypto::asymmetric::x25519;
|
||||
use nym_noise::config::NoiseNetworkView;
|
||||
use rand::rngs::OsRng;
|
||||
|
||||
fn dummy_client() -> Client {
|
||||
let mut rng = OsRng; //for test only, so we don't care if rng source isn't crypto grade
|
||||
Client::new(
|
||||
Config {
|
||||
initial_reconnection_backoff: Duration::from_millis(10_000),
|
||||
@@ -311,6 +340,11 @@ mod tests {
|
||||
initial_connection_timeout: Duration::from_millis(1_500),
|
||||
maximum_connection_buffer_size: 128,
|
||||
},
|
||||
NoiseConfig::new(
|
||||
Arc::new(x25519::KeyPair::new(&mut rng)),
|
||||
NoiseNetworkView::new_empty(),
|
||||
Duration::from_millis(1_500),
|
||||
),
|
||||
Default::default(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ use nym_api_requests::models::{
|
||||
NymNodeDescription, RewardEstimationResponse, StakeSaturationResponse,
|
||||
};
|
||||
use nym_api_requests::models::{LegacyDescribedGateway, MixNodeBondAnnotated};
|
||||
use nym_api_requests::nym_nodes::{NodesByAddressesResponse, SkimmedNode};
|
||||
use nym_api_requests::nym_nodes::{NodesByAddressesResponse, SemiSkimmedNode, SkimmedNode};
|
||||
use nym_coconut_dkg_common::types::EpochId;
|
||||
use nym_http_api_client::UserAgent;
|
||||
use nym_mixnet_contract_common::EpochRewardedSet;
|
||||
@@ -524,6 +524,31 @@ impl NymApiClient {
|
||||
Ok(nodes)
|
||||
}
|
||||
|
||||
/// retrieve expanded information for all bonded nodes on the network
|
||||
pub async fn get_all_expanded_nodes(
|
||||
&self,
|
||||
) -> Result<Vec<SemiSkimmedNode>, 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 nodes = Vec::new();
|
||||
|
||||
loop {
|
||||
let mut res = self
|
||||
.nym_api
|
||||
.get_expanded_nodes(false, Some(page), None)
|
||||
.await?;
|
||||
|
||||
nodes.append(&mut res.nodes.data);
|
||||
if nodes.len() < res.nodes.pagination.total {
|
||||
page += 1
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(nodes)
|
||||
}
|
||||
|
||||
pub async fn health(&self) -> Result<ApiHealthResponse, ValidatorClientError> {
|
||||
Ok(self.nym_api.health().await?)
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ use nym_api_requests::models::{
|
||||
};
|
||||
use nym_api_requests::nym_nodes::{
|
||||
NodesByAddressesRequestBody, NodesByAddressesResponse, PaginatedCachedNodesResponse,
|
||||
SemiSkimmedNode,
|
||||
};
|
||||
use nym_api_requests::pagination::PaginatedResponse;
|
||||
pub use nym_api_requests::{
|
||||
@@ -474,6 +475,39 @@ pub trait NymApiClientExt: ApiClient {
|
||||
.await
|
||||
}
|
||||
|
||||
#[instrument(level = "debug", skip(self))]
|
||||
async fn get_expanded_nodes(
|
||||
&self,
|
||||
no_legacy: bool,
|
||||
page: Option<u32>,
|
||||
per_page: Option<u32>,
|
||||
) -> Result<PaginatedCachedNodesResponse<SemiSkimmedNode>, NymAPIError> {
|
||||
let mut params = Vec::new();
|
||||
|
||||
if no_legacy {
|
||||
params.push(("no_legacy", "true".to_string()))
|
||||
}
|
||||
|
||||
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::API_VERSION,
|
||||
"unstable",
|
||||
routes::NYM_NODES_ROUTES,
|
||||
"semi-skimmed",
|
||||
],
|
||||
¶ms,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[deprecated]
|
||||
#[instrument(level = "debug", skip(self))]
|
||||
async fn get_active_mixnodes(&self) -> Result<Vec<MixNodeDetails>, NymAPIError> {
|
||||
|
||||
@@ -256,6 +256,10 @@ impl PrivateKey {
|
||||
self.0.to_bytes()
|
||||
}
|
||||
|
||||
pub fn as_bytes(&self) -> &[u8; PRIVATE_KEY_SIZE] {
|
||||
self.0.as_bytes()
|
||||
}
|
||||
|
||||
pub fn from_bytes(b: &[u8]) -> Result<Self, KeyRecoveryError> {
|
||||
if b.len() != PRIVATE_KEY_SIZE {
|
||||
return Err(KeyRecoveryError::InvalidSizePrivateKey {
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
[package]
|
||||
name = "nym-noise"
|
||||
version = "0.1.0"
|
||||
authors = ["Simon Wicky <simon@nymtech.net>"]
|
||||
edition = "2021"
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
arc-swap = { workspace = true }
|
||||
bytes = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
pin-project = { workspace = true }
|
||||
sha2 = { workspace = true }
|
||||
snow = { workspace = true }
|
||||
strum = { workspace = true, features = ["derive"] }
|
||||
thiserror = { workspace = true }
|
||||
tokio = { workspace = true, features = ["net", "io-util", "time"] }
|
||||
tokio-util = { workspace = true, features = ["codec"] }
|
||||
|
||||
# internal
|
||||
nym-crypto = { path = "../crypto" }
|
||||
nym-noise-keys = { path = "keys" }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -0,0 +1,17 @@
|
||||
[package]
|
||||
name = "nym-noise-keys"
|
||||
version = "0.1.0"
|
||||
authors = ["Simon Wicky <simon@nymtech.net>"]
|
||||
edition = "2021"
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
schemars = { workspace = true, features = ["preserve_order"] }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
utoipa = { workspace = true }
|
||||
|
||||
# internal
|
||||
nym-crypto = { path = "../../crypto", features = ["asymmetric", "serde"] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -0,0 +1,40 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use nym_crypto::asymmetric::x25519;
|
||||
use nym_crypto::asymmetric::x25519::serde_helpers::bs58_x25519_pubkey;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(from = "u8", into = "u8")]
|
||||
pub enum NoiseVersion {
|
||||
V1 = 1,
|
||||
Unknown, //Implies a newer version we don't know
|
||||
}
|
||||
|
||||
impl From<u8> for NoiseVersion {
|
||||
fn from(value: u8) -> Self {
|
||||
match value {
|
||||
1 => NoiseVersion::V1,
|
||||
_ => NoiseVersion::Unknown,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<NoiseVersion> for u8 {
|
||||
fn from(version: NoiseVersion) -> Self {
|
||||
version as u8
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, utoipa::ToSchema)]
|
||||
pub struct VersionedNoiseKey {
|
||||
#[schemars(with = "u8")]
|
||||
#[schema(value_type = u8)]
|
||||
pub version: NoiseVersion,
|
||||
|
||||
#[schemars(with = "String")]
|
||||
#[serde(with = "bs58_x25519_pubkey")]
|
||||
#[schema(value_type = String)]
|
||||
pub x25519_pubkey: x25519::PublicKey,
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
net::{IpAddr, SocketAddr},
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use arc_swap::ArcSwap;
|
||||
use nym_crypto::asymmetric::x25519;
|
||||
use nym_noise_keys::{NoiseVersion, VersionedNoiseKey};
|
||||
use snow::params::NoiseParams;
|
||||
|
||||
use strum::EnumIter;
|
||||
|
||||
#[derive(Default, Debug, Clone, Copy, EnumIter)]
|
||||
pub enum NoisePattern {
|
||||
#[default]
|
||||
XKpsk3,
|
||||
IKpsk2,
|
||||
}
|
||||
|
||||
impl NoisePattern {
|
||||
pub(crate) fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
Self::XKpsk3 => "Noise_XKpsk3_25519_AESGCM_SHA256",
|
||||
Self::IKpsk2 => "Noise_IKpsk2_25519_ChaChaPoly_BLAKE2s", //Wireguard handshake (not exactly though)
|
||||
}
|
||||
}
|
||||
|
||||
// SAFETY: we have tests to ensure that hardcoded pattern are correct
|
||||
#[allow(clippy::unwrap_used)]
|
||||
pub(crate) fn psk_position(&self) -> u8 {
|
||||
//automatic parsing, works for correct pattern, more convenient
|
||||
match self.as_str().find("psk") {
|
||||
Some(n) => {
|
||||
let psk_index = n + 3;
|
||||
let psk_char = self.as_str().chars().nth(psk_index).unwrap();
|
||||
psk_char.to_string().parse().unwrap()
|
||||
}
|
||||
None => 0,
|
||||
}
|
||||
}
|
||||
|
||||
// SAFETY : we have tests to ensure that hardcoded pattern are correct
|
||||
#[allow(clippy::unwrap_used)]
|
||||
pub(crate) fn as_noise_params(&self) -> NoiseParams {
|
||||
self.as_str().parse().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct SocketAddrToKey {
|
||||
inner: ArcSwap<HashMap<SocketAddr, VersionedNoiseKey>>,
|
||||
}
|
||||
|
||||
// SW NOTE : Only for phased upgrade. To remove once we decide all nodes have to support Noise
|
||||
#[derive(Debug, Default)]
|
||||
struct IpAddrToVersion {
|
||||
inner: ArcSwap<HashMap<IpAddr, NoiseVersion>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct NoiseNetworkView {
|
||||
keys: Arc<SocketAddrToKey>,
|
||||
support: Arc<IpAddrToVersion>,
|
||||
}
|
||||
|
||||
impl NoiseNetworkView {
|
||||
pub fn new_empty() -> Self {
|
||||
NoiseNetworkView {
|
||||
keys: Default::default(),
|
||||
support: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn swap_view(&self, new: HashMap<SocketAddr, VersionedNoiseKey>) {
|
||||
let noise_support = new
|
||||
.iter()
|
||||
.map(|(s_addr, key)| (s_addr.ip(), key.version))
|
||||
.collect::<HashMap<_, _>>();
|
||||
self.keys.inner.store(Arc::new(new));
|
||||
self.support.inner.store(Arc::new(noise_support));
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct NoiseConfig {
|
||||
network: NoiseNetworkView,
|
||||
|
||||
pub(crate) local_key: Arc<x25519::KeyPair>,
|
||||
pub(crate) pattern: NoisePattern,
|
||||
pub(crate) timeout: Duration,
|
||||
|
||||
pub(crate) unsafe_disabled: bool, // allows for nodes to not attempt to do a noise handshake, VERY UNSAFE, FOR DEBUG PURPOSE ONLY
|
||||
}
|
||||
|
||||
impl NoiseConfig {
|
||||
pub fn new(
|
||||
noise_key: Arc<x25519::KeyPair>,
|
||||
network: NoiseNetworkView,
|
||||
timeout: Duration,
|
||||
) -> Self {
|
||||
NoiseConfig {
|
||||
network,
|
||||
local_key: noise_key,
|
||||
pattern: Default::default(),
|
||||
timeout,
|
||||
unsafe_disabled: false,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_noise_pattern(mut self, pattern: NoisePattern) -> Self {
|
||||
self.pattern = pattern;
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_unsafe_disabled(mut self, disabled: bool) -> Self {
|
||||
self.unsafe_disabled = disabled;
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn get_noise_key(&self, s_address: &SocketAddr) -> Option<VersionedNoiseKey> {
|
||||
self.network.keys.inner.load().get(s_address).copied()
|
||||
}
|
||||
|
||||
// Only for phased update
|
||||
//SW This can lead to some troubles if two nodes shares the same IP and one support Noise but not the other. This in only for the progressive update though and there is no workaround
|
||||
pub(crate) fn get_noise_support(&self, ip_addr: IpAddr) -> Option<NoiseVersion> {
|
||||
let plain_ip_support = self.network.support.inner.load().get(&ip_addr).copied();
|
||||
|
||||
// SW default bind address being [::]:1789, it can happen that a responder sees the ipv6-mapped address of the initiator, this check for that
|
||||
let canonical_ip = &ip_addr.to_canonical();
|
||||
let canonical_ip_support = self.network.support.inner.load().get(canonical_ip).copied();
|
||||
plain_ip_support.or(canonical_ip_support)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use snow::params::NoiseParams;
|
||||
|
||||
use super::NoisePattern;
|
||||
use std::str::FromStr;
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
// The goal of these is to make sure every NoisePatterns are correct and unwrap can be used on them
|
||||
|
||||
#[test]
|
||||
fn noise_patterns_are_valid() {
|
||||
for pattern in NoisePattern::iter() {
|
||||
assert!(NoiseParams::from_str(pattern.as_str()).is_ok())
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn noise_patterns_psk_position_is_valid() {
|
||||
for pattern in NoisePattern::iter() {
|
||||
match pattern {
|
||||
NoisePattern::XKpsk3 => assert_eq!(pattern.psk_position(), 3),
|
||||
NoisePattern::IKpsk2 => assert_eq!(pattern.psk_position(), 2),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use std::io;
|
||||
|
||||
use pin_project::pin_project;
|
||||
use tokio::{
|
||||
io::{AsyncRead, AsyncWrite},
|
||||
net::TcpStream,
|
||||
};
|
||||
|
||||
use crate::stream::NoiseStream;
|
||||
|
||||
//SW once plain TCP support is dropped, this whole enum can be dropped, and we can only propagate NoiseStream
|
||||
#[pin_project(project = ConnectionProj)]
|
||||
pub enum Connection {
|
||||
Tcp(#[pin] TcpStream),
|
||||
Noise(#[pin] NoiseStream),
|
||||
}
|
||||
|
||||
impl Connection {
|
||||
pub fn peer_addr(&self) -> Result<std::net::SocketAddr, io::Error> {
|
||||
match self {
|
||||
Self::Noise(stream) => stream.peer_addr(),
|
||||
Self::Tcp(stream) => stream.peer_addr(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncRead for Connection {
|
||||
fn poll_read(
|
||||
self: std::pin::Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
buf: &mut tokio::io::ReadBuf<'_>,
|
||||
) -> std::task::Poll<io::Result<()>> {
|
||||
match self.project() {
|
||||
ConnectionProj::Noise(stream) => stream.poll_read(cx, buf),
|
||||
ConnectionProj::Tcp(stream) => stream.poll_read(cx, buf),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncWrite for Connection {
|
||||
fn poll_write(
|
||||
self: std::pin::Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
buf: &[u8],
|
||||
) -> std::task::Poll<Result<usize, io::Error>> {
|
||||
match self.project() {
|
||||
ConnectionProj::Noise(stream) => stream.poll_write(cx, buf),
|
||||
ConnectionProj::Tcp(stream) => stream.poll_write(cx, buf),
|
||||
}
|
||||
}
|
||||
fn poll_flush(
|
||||
self: std::pin::Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
) -> std::task::Poll<Result<(), io::Error>> {
|
||||
match self.project() {
|
||||
ConnectionProj::Noise(stream) => stream.poll_flush(cx),
|
||||
ConnectionProj::Tcp(stream) => stream.poll_flush(cx),
|
||||
}
|
||||
}
|
||||
|
||||
fn poll_shutdown(
|
||||
self: std::pin::Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
) -> std::task::Poll<Result<(), io::Error>> {
|
||||
match self.project() {
|
||||
ConnectionProj::Noise(stream) => stream.poll_shutdown(cx),
|
||||
ConnectionProj::Tcp(stream) => stream.poll_shutdown(cx),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use snow::Error;
|
||||
use std::io;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum NoiseError {
|
||||
#[error("encountered a Noise decryption error")]
|
||||
DecryptionError,
|
||||
|
||||
#[error("encountered a Noise Protocol error: {0}")]
|
||||
ProtocolError(Error),
|
||||
|
||||
#[error("encountered an IO error: {0}")]
|
||||
IoError(#[from] io::Error),
|
||||
|
||||
#[error("Incorrect state")]
|
||||
IncorrectStateError,
|
||||
|
||||
#[error("Handshake did not complete")]
|
||||
HandshakeError,
|
||||
|
||||
#[error("Unknown noise version")]
|
||||
UnknownVersion,
|
||||
|
||||
#[error("Handshake timeout")]
|
||||
HandshakeTimeout(#[from] tokio::time::error::Elapsed),
|
||||
}
|
||||
|
||||
impl From<Error> for NoiseError {
|
||||
fn from(err: Error) -> Self {
|
||||
match err {
|
||||
Error::Decrypt => NoiseError::DecryptionError,
|
||||
err => NoiseError::ProtocolError(err),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::config::NoiseConfig;
|
||||
use crate::connection::Connection;
|
||||
use crate::error::NoiseError;
|
||||
use crate::stream::NoiseStream;
|
||||
use nym_crypto::asymmetric::x25519;
|
||||
use nym_noise_keys::NoiseVersion;
|
||||
use sha2::{Digest, Sha256};
|
||||
use snow::{error::Prerequisite, Error};
|
||||
use tokio::net::TcpStream;
|
||||
use tracing::*;
|
||||
|
||||
pub mod config;
|
||||
pub mod connection;
|
||||
pub mod error;
|
||||
pub mod stream;
|
||||
|
||||
const NOISE_PSK_PREFIX: &[u8] = b"NYMTECH_NOISE_dQw4w9WgXcQ";
|
||||
|
||||
pub const LATEST_NOISE_VERSION: NoiseVersion = NoiseVersion::V1;
|
||||
|
||||
fn generate_psk_v1(responder_pub_key: &x25519::PublicKey) -> [u8; 32] {
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(NOISE_PSK_PREFIX);
|
||||
hasher.update(responder_pub_key.to_bytes());
|
||||
hasher.finalize().into()
|
||||
}
|
||||
|
||||
async fn upgrade_noise_initiator_v1(
|
||||
conn: TcpStream,
|
||||
config: &NoiseConfig,
|
||||
remote_pub_key: &x25519::PublicKey,
|
||||
) -> Result<Connection, NoiseError> {
|
||||
trace!("Perform Noise Handshake, initiator side");
|
||||
|
||||
let secret_hash = generate_psk_v1(remote_pub_key);
|
||||
let noise_stream = NoiseStream::new_initiator(conn, config, remote_pub_key, &secret_hash)?;
|
||||
|
||||
Ok(Connection::Noise(
|
||||
tokio::time::timeout(config.timeout, noise_stream.perform_handshake()).await??,
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn upgrade_noise_initiator(
|
||||
conn: TcpStream,
|
||||
config: &NoiseConfig,
|
||||
) -> Result<Connection, NoiseError> {
|
||||
if config.unsafe_disabled {
|
||||
warn!("Noise is disabled in the config. Not attempting any handshake");
|
||||
return Ok(Connection::Tcp(conn));
|
||||
}
|
||||
|
||||
//Get init material
|
||||
let responder_addr = conn.peer_addr().map_err(|err| {
|
||||
error!("Unable to extract peer address from connection - {err}");
|
||||
Error::Prereq(Prerequisite::RemotePublicKey)
|
||||
})?;
|
||||
|
||||
match config.get_noise_key(&responder_addr) {
|
||||
Some(key) => match key.version {
|
||||
NoiseVersion::V1 => upgrade_noise_initiator_v1(conn, config, &key.x25519_pubkey).await,
|
||||
// We're talking to a more recent node, but we can't adapt. Let's try to do our best and if it fails, it fails.
|
||||
// If that node sees we're older, it will try to adapt too.
|
||||
NoiseVersion::Unknown => {
|
||||
warn!("{responder_addr} is announcing an unknown version of Noise, we will still attempt our latest known version");
|
||||
upgrade_noise_initiator_v1(conn, config, &key.x25519_pubkey)
|
||||
.await
|
||||
.or(Err(NoiseError::UnknownVersion))
|
||||
}
|
||||
},
|
||||
None => {
|
||||
warn!("{responder_addr} can't speak Noise yet, falling back to TCP");
|
||||
Ok(Connection::Tcp(conn))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn upgrade_noise_responder_v1(
|
||||
conn: TcpStream,
|
||||
config: &NoiseConfig,
|
||||
) -> Result<Connection, NoiseError> {
|
||||
trace!("Perform Noise Handshake, responder side");
|
||||
|
||||
let secret_hash = generate_psk_v1(config.local_key.public_key());
|
||||
let noise_stream = NoiseStream::new_responder(conn, config, &secret_hash)?;
|
||||
|
||||
Ok(Connection::Noise(
|
||||
tokio::time::timeout(config.timeout, noise_stream.perform_handshake()).await??,
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn upgrade_noise_responder(
|
||||
conn: TcpStream,
|
||||
config: &NoiseConfig,
|
||||
) -> Result<Connection, NoiseError> {
|
||||
if config.unsafe_disabled {
|
||||
warn!("Noise is disabled in the config. Not attempting any handshake");
|
||||
return Ok(Connection::Tcp(conn));
|
||||
}
|
||||
|
||||
//Get init material
|
||||
let initiator_addr = match conn.peer_addr() {
|
||||
Ok(addr) => addr,
|
||||
Err(err) => {
|
||||
error!("Unable to extract peer address from connection - {err}");
|
||||
return Err(Error::Prereq(Prerequisite::RemotePublicKey).into());
|
||||
}
|
||||
};
|
||||
|
||||
// Port is random and we just need the support info
|
||||
match config.get_noise_support(initiator_addr.ip()) {
|
||||
None => {
|
||||
warn!("{initiator_addr} can't speak Noise yet, falling back to TCP",);
|
||||
Ok(Connection::Tcp(conn))
|
||||
}
|
||||
// responder's info on version is shaky, so ideally, initiator has to adapt.
|
||||
// if we are newer, it won't ba able to, so let's try to meet him on his ground.
|
||||
Some(LATEST_NOISE_VERSION) | Some(NoiseVersion::Unknown) => {
|
||||
// Node is announcing the same version as us, great or
|
||||
// Node is announcing a newer version than us, it should adapt to us though
|
||||
upgrade_noise_responder_v1(conn, config).await
|
||||
} //SW sample of code to allow backwards compatibility when we introduce new versions
|
||||
// Some(IntermediateNoiseVersion) => {
|
||||
// Node is announcing an older version, let's try to adapt
|
||||
// upgrade_noise_responder_Vwhatever
|
||||
// }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::{config::NoiseConfig, error::NoiseError};
|
||||
use bytes::BytesMut;
|
||||
use futures::{Sink, SinkExt, Stream, StreamExt};
|
||||
use nym_crypto::asymmetric::x25519;
|
||||
use pin_project::pin_project;
|
||||
use snow::{Builder, HandshakeState, TransportState};
|
||||
use std::io;
|
||||
use std::pin::Pin;
|
||||
use std::task::Poll;
|
||||
use std::{cmp::min, task::ready};
|
||||
use tokio::{
|
||||
io::{AsyncRead, AsyncWrite, ReadBuf},
|
||||
net::TcpStream,
|
||||
};
|
||||
use tokio_util::codec::{Framed, LengthDelimitedCodec};
|
||||
|
||||
const TAGLEN: usize = 16;
|
||||
const HANDSHAKE_MAX_LEN: usize = 1024; // using this constant to limit the handshake's buffer size
|
||||
|
||||
pub(crate) type Psk = [u8; 32];
|
||||
|
||||
/// Wrapper around a TcpStream
|
||||
#[pin_project]
|
||||
pub struct NoiseStream {
|
||||
#[pin]
|
||||
inner_stream: Framed<TcpStream, LengthDelimitedCodec>,
|
||||
handshake: Option<HandshakeState>,
|
||||
noise: Option<TransportState>,
|
||||
dec_buffer: BytesMut,
|
||||
}
|
||||
|
||||
impl NoiseStream {
|
||||
pub(crate) fn new_initiator(
|
||||
inner_stream: TcpStream,
|
||||
config: &NoiseConfig,
|
||||
remote_pub_key: &x25519::PublicKey,
|
||||
psk: &Psk,
|
||||
) -> Result<NoiseStream, NoiseError> {
|
||||
let handshake = Builder::new(config.pattern.as_noise_params())
|
||||
.local_private_key(config.local_key.private_key().as_bytes())
|
||||
.remote_public_key(&remote_pub_key.to_bytes())
|
||||
.psk(config.pattern.psk_position(), psk)
|
||||
.build_initiator()?;
|
||||
Ok(NoiseStream::new_inner(inner_stream, handshake))
|
||||
}
|
||||
|
||||
pub(crate) fn new_responder(
|
||||
inner_stream: TcpStream,
|
||||
config: &NoiseConfig,
|
||||
psk: &Psk,
|
||||
) -> Result<NoiseStream, NoiseError> {
|
||||
let handshake = Builder::new(config.pattern.as_noise_params())
|
||||
.local_private_key(config.local_key.private_key().as_bytes())
|
||||
.psk(config.pattern.psk_position(), psk)
|
||||
.build_responder()?;
|
||||
Ok(NoiseStream::new_inner(inner_stream, handshake))
|
||||
}
|
||||
|
||||
fn new_inner(inner_stream: TcpStream, handshake: HandshakeState) -> NoiseStream {
|
||||
NoiseStream {
|
||||
inner_stream: LengthDelimitedCodec::builder()
|
||||
.length_field_type::<u16>()
|
||||
.new_framed(inner_stream),
|
||||
handshake: Some(handshake),
|
||||
noise: None,
|
||||
dec_buffer: BytesMut::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn perform_handshake(mut self) -> Result<Self, NoiseError> {
|
||||
//Check if we are in the correct state
|
||||
let Some(mut handshake) = self.handshake.take() else {
|
||||
return Err(NoiseError::IncorrectStateError);
|
||||
};
|
||||
|
||||
while !handshake.is_handshake_finished() {
|
||||
if handshake.is_my_turn() {
|
||||
self.send_handshake_msg(&mut handshake).await?;
|
||||
} else {
|
||||
self.recv_handshake_msg(&mut handshake).await?;
|
||||
}
|
||||
}
|
||||
|
||||
self.noise = Some(handshake.into_transport_mode()?);
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
async fn send_handshake_msg(
|
||||
&mut self,
|
||||
handshake: &mut HandshakeState,
|
||||
) -> Result<(), NoiseError> {
|
||||
let mut buf = BytesMut::zeroed(HANDSHAKE_MAX_LEN); // we're in the handshake, we can afford a smaller buffer
|
||||
let len = handshake.write_message(&[], &mut buf)?;
|
||||
buf.truncate(len);
|
||||
self.inner_stream.send(buf.into()).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn recv_handshake_msg(
|
||||
&mut self,
|
||||
handshake: &mut HandshakeState,
|
||||
) -> Result<(), NoiseError> {
|
||||
match self.inner_stream.next().await {
|
||||
Some(Ok(msg)) => {
|
||||
let mut buf = BytesMut::zeroed(HANDSHAKE_MAX_LEN); // we're in the handshake, we can afford a smaller buffer
|
||||
handshake.read_message(&msg, &mut buf)?;
|
||||
Ok(())
|
||||
}
|
||||
Some(Err(err)) => Err(NoiseError::IoError(err)),
|
||||
None => Err(NoiseError::HandshakeError),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn peer_addr(&self) -> Result<std::net::SocketAddr, io::Error> {
|
||||
self.inner_stream.get_ref().peer_addr()
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncRead for NoiseStream {
|
||||
fn poll_read(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
buf: &mut ReadBuf<'_>,
|
||||
) -> Poll<std::io::Result<()>> {
|
||||
let projected_self = self.project();
|
||||
|
||||
let pending = match projected_self.inner_stream.poll_next(cx) {
|
||||
Poll::Pending => {
|
||||
//no new data, a return value of Poll::Pending means the waking is already scheduled
|
||||
//Nothing new to decrypt, only check if we can return something from dec_storage, happens after
|
||||
true
|
||||
}
|
||||
|
||||
Poll::Ready(Some(Ok(noise_msg))) => {
|
||||
// We have a new noise msg
|
||||
let mut dec_msg = BytesMut::zeroed(noise_msg.len() - TAGLEN);
|
||||
let len = match projected_self.noise {
|
||||
Some(transport_state) => {
|
||||
match transport_state.read_message(&noise_msg, &mut dec_msg) {
|
||||
Ok(len) => len,
|
||||
Err(_) => return Poll::Ready(Err(io::ErrorKind::InvalidInput.into())),
|
||||
}
|
||||
}
|
||||
None => return Poll::Ready(Err(io::ErrorKind::Other.into())),
|
||||
};
|
||||
projected_self.dec_buffer.extend(&dec_msg[..len]);
|
||||
false
|
||||
}
|
||||
|
||||
Poll::Ready(Some(Err(err))) => return Poll::Ready(Err(err)),
|
||||
|
||||
Poll::Ready(None) => {
|
||||
//Stream is done, we might still have data in the buffer though, happens afterwards
|
||||
false
|
||||
}
|
||||
};
|
||||
|
||||
// Checking if there is something to return from the buffer
|
||||
let read_len = min(buf.remaining(), projected_self.dec_buffer.len());
|
||||
if read_len > 0 {
|
||||
buf.put_slice(&projected_self.dec_buffer.split_to(read_len));
|
||||
return Poll::Ready(Ok(()));
|
||||
}
|
||||
|
||||
// buf.remaining == 0 or nothing in the buffer, we must return the value we had from the inner_stream
|
||||
if pending {
|
||||
//If we end up here, it means the previous poll_next was pending as well, hence waking is already scheduled
|
||||
Poll::Pending
|
||||
} else {
|
||||
Poll::Ready(Ok(()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncWrite for NoiseStream {
|
||||
fn poll_write(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
buf: &[u8],
|
||||
) -> Poll<Result<usize, std::io::Error>> {
|
||||
let mut projected_self = self.project();
|
||||
|
||||
// returns on Poll::Pending and Poll:Ready(Err)
|
||||
ready!(projected_self.inner_stream.as_mut().poll_ready(cx))?;
|
||||
|
||||
// Ready to send, encrypting message
|
||||
let mut noise_buf = BytesMut::zeroed(buf.len() + TAGLEN);
|
||||
|
||||
let Ok(len) = (match projected_self.noise {
|
||||
Some(transport_state) => transport_state.write_message(buf, &mut noise_buf),
|
||||
None => return Poll::Ready(Err(io::ErrorKind::Other.into())),
|
||||
}) else {
|
||||
return Poll::Ready(Err(io::ErrorKind::InvalidInput.into()));
|
||||
};
|
||||
noise_buf.truncate(len);
|
||||
|
||||
// Tokio uses the same `start_send ` in their SinkWriter implementation. https://docs.rs/tokio-util/latest/src/tokio_util/io/sink_writer.rs.html#104
|
||||
match projected_self
|
||||
.inner_stream
|
||||
.as_mut()
|
||||
.start_send(noise_buf.into())
|
||||
{
|
||||
Ok(()) => Poll::Ready(Ok(buf.len())),
|
||||
Err(e) => Poll::Ready(Err(e)),
|
||||
}
|
||||
}
|
||||
|
||||
fn poll_flush(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
) -> Poll<Result<(), std::io::Error>> {
|
||||
self.project().inner_stream.poll_flush(cx)
|
||||
}
|
||||
|
||||
fn poll_shutdown(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
) -> Poll<Result<(), std::io::Error>> {
|
||||
self.project().inner_stream.poll_close(cx)
|
||||
}
|
||||
}
|
||||
@@ -37,6 +37,7 @@ nym-compact-ecash = { path = "../../common/nym_offline_compact_ecash" }
|
||||
nym-contracts-common = { path = "../../common/cosmwasm-smart-contracts/contracts-common", features = ["naive_float"] }
|
||||
nym-mixnet-contract-common = { path = "../../common/cosmwasm-smart-contracts/mixnet-contract", features = ["utoipa"] }
|
||||
nym-node-requests = { path = "../../nym-node/nym-node-requests", default-features = false, features = ["openapi"] }
|
||||
nym-noise-keys = { path = "../../common/nymnoise/keys"}
|
||||
nym-network-defaults = { path = "../../common/network-defaults" }
|
||||
nym-ticketbooks-merkle = { path = "../../common/ticketbooks-merkle" }
|
||||
|
||||
|
||||
@@ -8,15 +8,13 @@ use crate::helpers::PlaceholderJsonSchemaImpl;
|
||||
use crate::legacy::{
|
||||
LegacyGatewayBondWithId, LegacyMixNodeBondWithLayer, LegacyMixNodeDetailsWithLayer,
|
||||
};
|
||||
use crate::nym_nodes::SemiSkimmedNode;
|
||||
use crate::nym_nodes::{BasicEntryInformation, NodeRole, SkimmedNode};
|
||||
use crate::pagination::PaginatedResponse;
|
||||
use cosmwasm_std::{Addr, Coin, Decimal, Uint128};
|
||||
use nym_contracts_common::NaiveFloat;
|
||||
use nym_crypto::asymmetric::ed25519::{self, serde_helpers::bs58_ed25519_pubkey};
|
||||
use nym_crypto::asymmetric::x25519::{
|
||||
self,
|
||||
serde_helpers::{bs58_x25519_pubkey, option_bs58_x25519_pubkey},
|
||||
};
|
||||
use nym_crypto::asymmetric::x25519::{self, serde_helpers::bs58_x25519_pubkey};
|
||||
use nym_mixnet_contract_common::nym_node::Role;
|
||||
use nym_mixnet_contract_common::reward_params::{Performance, RewardingParams};
|
||||
use nym_mixnet_contract_common::rewarding::RewardEstimate;
|
||||
@@ -28,6 +26,7 @@ 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::node::models::{AuxiliaryDetails, NodeRoles};
|
||||
use nym_noise_keys::VersionedNoiseKey;
|
||||
use schemars::gen::SchemaGenerator;
|
||||
use schemars::schema::{InstanceType, Schema, SchemaObject};
|
||||
use schemars::JsonSchema;
|
||||
@@ -465,6 +464,17 @@ impl MixNodeBondAnnotated {
|
||||
performance: self.node_performance.last_24h,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn try_to_semi_skimmed_node(
|
||||
&self,
|
||||
role: NodeRole,
|
||||
) -> Result<SemiSkimmedNode, MalformedNodeBond> {
|
||||
let skimmed_node = self.try_to_skimmed_node(role)?;
|
||||
Ok(SemiSkimmedNode {
|
||||
basic: skimmed_node,
|
||||
x25519_noise_versioned_key: None, // legacy node won't ever support Noise
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, ToSchema)]
|
||||
@@ -530,6 +540,17 @@ impl GatewayBondAnnotated {
|
||||
performance: self.node_performance.last_24h,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn try_to_semi_skimmed_node(
|
||||
&self,
|
||||
role: NodeRole,
|
||||
) -> Result<SemiSkimmedNode, MalformedNodeBond> {
|
||||
let skimmed_node = self.try_to_skimmed_node(role)?;
|
||||
Ok(SemiSkimmedNode {
|
||||
basic: skimmed_node,
|
||||
x25519_noise_versioned_key: None, // legacy node won't ever support Noise
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, ToSchema)]
|
||||
@@ -866,10 +887,7 @@ pub struct HostKeys {
|
||||
pub x25519: x25519::PublicKey,
|
||||
|
||||
#[serde(default)]
|
||||
#[serde(with = "option_bs58_x25519_pubkey")]
|
||||
#[schemars(with = "Option<String>")]
|
||||
#[schema(value_type = String)]
|
||||
pub x25519_noise: Option<x25519::PublicKey>,
|
||||
pub x25519_noise: Option<VersionedNoiseKey>,
|
||||
}
|
||||
|
||||
impl From<nym_node_requests::api::v1::node::models::HostKeys> for HostKeys {
|
||||
@@ -1022,6 +1040,19 @@ impl NymNodeDescription {
|
||||
performance,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_semi_skimmed_node(
|
||||
&self,
|
||||
role: NodeRole,
|
||||
performance: Performance,
|
||||
) -> SemiSkimmedNode {
|
||||
let skimmed_node = self.to_skimmed_node(role, performance);
|
||||
|
||||
SemiSkimmedNode {
|
||||
basic: skimmed_node,
|
||||
x25519_noise_versioned_key: self.description.host_information.keys.x25519_noise,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
|
||||
@@ -1307,10 +1338,7 @@ pub struct NetworkMonitorRunDetailsResponse {
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
|
||||
pub struct NoiseDetails {
|
||||
#[schemars(with = "String")]
|
||||
#[serde(with = "bs58_x25519_pubkey")]
|
||||
#[schema(value_type = String)]
|
||||
pub x25119_pubkey: x25519::PublicKey,
|
||||
pub key: VersionedNoiseKey,
|
||||
|
||||
pub mixnet_port: u16,
|
||||
|
||||
|
||||
@@ -9,6 +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::{Interval, NodeId};
|
||||
use nym_noise_keys::VersionedNoiseKey;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::net::IpAddr;
|
||||
@@ -202,7 +203,8 @@ impl SkimmedNode {
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
|
||||
pub struct SemiSkimmedNode {
|
||||
pub basic: SkimmedNode,
|
||||
pub x25519_noise_pubkey: String,
|
||||
|
||||
pub x25519_noise_versioned_key: Option<VersionedNoiseKey>,
|
||||
// pub location:
|
||||
}
|
||||
|
||||
|
||||
@@ -174,7 +174,7 @@ async fn nodes_noise(
|
||||
.map(|noise_key| (noise_key, n))
|
||||
})
|
||||
.map(|(noise_key, node)| NoiseDetails {
|
||||
x25119_pubkey: noise_key,
|
||||
key: noise_key,
|
||||
mixnet_port: node.description.mix_port(),
|
||||
ip_addresses: node.description.host_information.ip_address.clone(),
|
||||
})
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
use nym_api_requests::models::{
|
||||
GatewayBondAnnotated, MalformedNodeBond, MixNodeBondAnnotated, OffsetDateTimeJsonSchemaWrapper,
|
||||
};
|
||||
use nym_api_requests::nym_nodes::{NodeRole, SkimmedNode};
|
||||
use nym_api_requests::nym_nodes::{NodeRole, SemiSkimmedNode, SkimmedNode};
|
||||
use nym_mixnet_contract_common::reward_params::Performance;
|
||||
use time::OffsetDateTime;
|
||||
|
||||
@@ -14,6 +14,11 @@ pub(crate) trait LegacyAnnotation {
|
||||
fn identity(&self) -> &str;
|
||||
|
||||
fn try_to_skimmed_node(&self, role: NodeRole) -> Result<SkimmedNode, MalformedNodeBond>;
|
||||
|
||||
fn try_to_semi_skimmed_node(
|
||||
&self,
|
||||
role: NodeRole,
|
||||
) -> Result<SemiSkimmedNode, MalformedNodeBond>;
|
||||
}
|
||||
|
||||
impl LegacyAnnotation for MixNodeBondAnnotated {
|
||||
@@ -28,6 +33,13 @@ impl LegacyAnnotation for MixNodeBondAnnotated {
|
||||
fn try_to_skimmed_node(&self, role: NodeRole) -> Result<SkimmedNode, MalformedNodeBond> {
|
||||
self.try_to_skimmed_node(role)
|
||||
}
|
||||
|
||||
fn try_to_semi_skimmed_node(
|
||||
&self,
|
||||
role: NodeRole,
|
||||
) -> Result<SemiSkimmedNode, MalformedNodeBond> {
|
||||
self.try_to_semi_skimmed_node(role)
|
||||
}
|
||||
}
|
||||
|
||||
impl LegacyAnnotation for GatewayBondAnnotated {
|
||||
@@ -42,6 +54,13 @@ impl LegacyAnnotation for GatewayBondAnnotated {
|
||||
fn try_to_skimmed_node(&self, role: NodeRole) -> Result<SkimmedNode, MalformedNodeBond> {
|
||||
self.try_to_skimmed_node(role)
|
||||
}
|
||||
|
||||
fn try_to_semi_skimmed_node(
|
||||
&self,
|
||||
role: NodeRole,
|
||||
) -> Result<SemiSkimmedNode, MalformedNodeBond> {
|
||||
self.try_to_semi_skimmed_node(role)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn refreshed_at(
|
||||
|
||||
@@ -1,13 +1,96 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::node_status_api::models::{AxumErrorResponse, AxumResult};
|
||||
use crate::node_describe_cache::DescribedNodes;
|
||||
use crate::node_status_api::models::AxumResult;
|
||||
use crate::nym_nodes::handlers::unstable::helpers::{refreshed_at, LegacyAnnotation};
|
||||
use crate::nym_nodes::handlers::unstable::NodesParamsWithRole;
|
||||
use crate::support::http::state::AppState;
|
||||
use axum::extract::{Query, State};
|
||||
use nym_api_requests::nym_nodes::{CachedNodesResponse, SemiSkimmedNode};
|
||||
use nym_api_requests::models::{
|
||||
NodeAnnotation, NymNodeDescription, OffsetDateTimeJsonSchemaWrapper,
|
||||
};
|
||||
use nym_api_requests::nym_nodes::{NodeRole, PaginatedCachedNodesResponse, SemiSkimmedNode};
|
||||
use nym_api_requests::pagination::PaginatedResponse;
|
||||
use nym_http_api_common::FormattedResponse;
|
||||
use nym_mixnet_contract_common::NodeId;
|
||||
use nym_topology::CachedEpochRewardedSet;
|
||||
use std::collections::HashMap;
|
||||
use tracing::trace;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
pub type PaginatedSemiSkimmedNodes =
|
||||
AxumResult<FormattedResponse<PaginatedCachedNodesResponse<SemiSkimmedNode>>>;
|
||||
|
||||
//SW TODO : this is copied from skimmed nodes, surely we can do better than that
|
||||
fn build_nym_nodes_response<'a, NI>(
|
||||
rewarded_set: &CachedEpochRewardedSet,
|
||||
nym_nodes_subset: NI,
|
||||
annotations: &HashMap<NodeId, NodeAnnotation>,
|
||||
active_only: bool,
|
||||
) -> Vec<SemiSkimmedNode>
|
||||
where
|
||||
NI: Iterator<Item = &'a NymNodeDescription> + 'a,
|
||||
{
|
||||
let mut nodes = Vec::new();
|
||||
for nym_node in nym_nodes_subset {
|
||||
let node_id = nym_node.node_id;
|
||||
|
||||
let role: NodeRole = rewarded_set.role(node_id).into();
|
||||
|
||||
// if the role is inactive, see if our filter allows it
|
||||
if active_only && role.is_inactive() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// honestly, not sure under what exact circumstances this value could be missing,
|
||||
// but in that case just use 0 performance
|
||||
let annotation = annotations.get(&node_id).copied().unwrap_or_default();
|
||||
|
||||
nodes.push(nym_node.to_semi_skimmed_node(role, annotation.last_24h_performance));
|
||||
}
|
||||
nodes
|
||||
}
|
||||
|
||||
//SW TODO : this is copied from skimmed nodes, surely we can do better than that
|
||||
/// Given all relevant caches, add appropriate legacy nodes to the part of the response
|
||||
fn add_legacy<LN>(
|
||||
nodes: &mut Vec<SemiSkimmedNode>,
|
||||
rewarded_set: &CachedEpochRewardedSet,
|
||||
describe_cache: &DescribedNodes,
|
||||
annotated_legacy_nodes: &HashMap<NodeId, LN>,
|
||||
) where
|
||||
LN: LegacyAnnotation,
|
||||
{
|
||||
for (node_id, legacy) in annotated_legacy_nodes.iter() {
|
||||
let role: NodeRole = rewarded_set.role(*node_id).into();
|
||||
|
||||
// if we have self-described info, prefer it over contract data
|
||||
if let Some(described) = describe_cache.get_node(node_id) {
|
||||
nodes.push(described.to_semi_skimmed_node(role, legacy.performance()))
|
||||
} else {
|
||||
match legacy.try_to_semi_skimmed_node(role) {
|
||||
Ok(node) => nodes.push(node),
|
||||
Err(err) => {
|
||||
let id = legacy.identity();
|
||||
trace!("node {id} is malformed: {err}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)] // not dead, used in OpenAPI docs
|
||||
#[derive(ToSchema)]
|
||||
#[schema(title = "PaginatedCachedNodesExpandedResponseSchema")]
|
||||
pub struct PaginatedCachedNodesExpandedResponseSchema {
|
||||
pub refreshed_at: OffsetDateTimeJsonSchemaWrapper,
|
||||
#[schema(value_type = SemiSkimmedNode)]
|
||||
pub nodes: PaginatedResponse<SemiSkimmedNode>,
|
||||
}
|
||||
|
||||
/// Return all Nym Nodes and optionally legacy mixnodes/gateways (if `no-legacy` flag is not used)
|
||||
/// that are currently bonded.
|
||||
#[utoipa::path(
|
||||
tag = "Unstable Nym Nodes",
|
||||
get,
|
||||
@@ -15,13 +98,43 @@ use nym_http_api_common::FormattedResponse;
|
||||
path = "",
|
||||
context_path = "/v1/unstable/nym-nodes/semi-skimmed",
|
||||
responses(
|
||||
// (status = 200, body = CachedNodesResponse<SemiSkimmedNode>)
|
||||
(status = 501)
|
||||
(status = 200, content(
|
||||
(PaginatedCachedNodesExpandedResponseSchema = "application/json"),
|
||||
(PaginatedCachedNodesExpandedResponseSchema = "application/yaml"),
|
||||
(PaginatedCachedNodesExpandedResponseSchema = "application/bincode")
|
||||
))
|
||||
)
|
||||
)]
|
||||
pub(super) async fn nodes_expanded(
|
||||
_state: State<AppState>,
|
||||
_query_params: Query<NodesParamsWithRole>,
|
||||
) -> AxumResult<FormattedResponse<CachedNodesResponse<SemiSkimmedNode>>> {
|
||||
Err(AxumErrorResponse::not_implemented())
|
||||
state: State<AppState>,
|
||||
query_params: Query<NodesParamsWithRole>,
|
||||
) -> PaginatedSemiSkimmedNodes {
|
||||
// 1. grab all relevant described nym-nodes
|
||||
let rewarded_set = state.rewarded_set().await?;
|
||||
|
||||
let describe_cache = state.describe_nodes_cache_data().await?;
|
||||
let all_nym_nodes = describe_cache.all_nym_nodes();
|
||||
let annotations = state.node_annotations().await?;
|
||||
let legacy_mixnodes = state.legacy_mixnode_annotations().await?;
|
||||
let legacy_gateways = state.legacy_gateways_annotations().await?;
|
||||
|
||||
let mut nodes = build_nym_nodes_response(&rewarded_set, all_nym_nodes, &annotations, false);
|
||||
|
||||
// add legacy gateways to the response
|
||||
add_legacy(&mut nodes, &rewarded_set, &describe_cache, &legacy_gateways);
|
||||
|
||||
// add legacy mixnodes to the response
|
||||
add_legacy(&mut nodes, &rewarded_set, &describe_cache, &legacy_mixnodes);
|
||||
|
||||
// min of all caches
|
||||
let refreshed_at = refreshed_at([
|
||||
rewarded_set.timestamp(),
|
||||
annotations.timestamp(),
|
||||
describe_cache.timestamp(),
|
||||
legacy_mixnodes.timestamp(),
|
||||
legacy_gateways.timestamp(),
|
||||
]);
|
||||
|
||||
let output = query_params.output.unwrap_or_default();
|
||||
Ok(output.to_response(PaginatedCachedNodesResponse::new_full(refreshed_at, nodes)))
|
||||
}
|
||||
|
||||
@@ -55,6 +55,8 @@ nym-config = { path = "../common/config" }
|
||||
nym-crypto = { path = "../common/crypto", features = ["asymmetric", "rand"] }
|
||||
nym-nonexhaustive-delayqueue = { path = "../common/nonexhaustive-delayqueue" }
|
||||
nym-mixnet-client = { path = "../common/client-libs/mixnet-client" }
|
||||
nym-noise = { path = "../common/nymnoise" }
|
||||
nym-noise-keys = { path = "../common/nymnoise/keys" }
|
||||
nym-pemstore = { path = "../common/pemstore" }
|
||||
nym-sphinx-acknowledgements = { path = "../common/nymsphinx/acknowledgements" }
|
||||
nym-sphinx-addressing = { path = "../common/nymsphinx/addressing" }
|
||||
|
||||
@@ -26,6 +26,7 @@ nym-crypto = { path = "../../common/crypto", features = [
|
||||
"serde",
|
||||
] }
|
||||
nym-exit-policy = { path = "../../common/exit-policy" }
|
||||
nym-noise-keys = { path = "../../common/nymnoise/keys" }
|
||||
nym-wireguard-types = { path = "../../common/wireguard-types", default-features = false }
|
||||
|
||||
# feature-specific dependencies:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// Copyright 2023-2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::api::v1::node::models::{LegacyHostInformation, LegacyHostInformationV2};
|
||||
@@ -8,7 +8,6 @@ use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::ops::Deref;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
#[cfg(feature = "client")]
|
||||
pub mod client;
|
||||
@@ -20,7 +19,7 @@ pub use client::Client;
|
||||
// create the type alias manually if openapi is not enabled
|
||||
pub type SignedHostInformation = SignedData<crate::api::v1::node::models::HostInformation>;
|
||||
|
||||
#[derive(ToSchema)]
|
||||
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
|
||||
pub struct SignedDataHostInfo {
|
||||
// #[serde(flatten)]
|
||||
pub data: crate::api::v1::node::models::HostInformation,
|
||||
@@ -40,6 +39,7 @@ impl<T> SignedData<T> {
|
||||
T: Serialize,
|
||||
{
|
||||
let plaintext = serde_json::to_string(&data)?;
|
||||
|
||||
let signature = key.sign(plaintext).to_base58_string();
|
||||
Ok(SignedData { data, signature })
|
||||
}
|
||||
@@ -66,6 +66,8 @@ impl SignedHostInformation {
|
||||
return true;
|
||||
}
|
||||
|
||||
// TODO: @JS: to remove downgrade support in future release(s)
|
||||
|
||||
// attempt to verify legacy signatures
|
||||
let legacy_v2 = SignedData {
|
||||
data: LegacyHostInformationV2::from(self.data.clone()),
|
||||
@@ -105,8 +107,11 @@ impl Display for ErrorResponse {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use super::*;
|
||||
use crate::api::v1::node::models::HostInformation;
|
||||
use nym_crypto::asymmetric::{ed25519, x25519};
|
||||
use nym_noise_keys::{NoiseVersion, VersionedNoiseKey};
|
||||
use rand_chacha::rand_core::SeedableRng;
|
||||
|
||||
#[test]
|
||||
@@ -114,7 +119,10 @@ mod tests {
|
||||
let mut rng = rand_chacha::ChaCha20Rng::from_seed([0u8; 32]);
|
||||
let ed22519 = ed25519::KeyPair::new(&mut rng);
|
||||
let x25519_sphinx = x25519::KeyPair::new(&mut rng);
|
||||
let x25519_noise = x25519::KeyPair::new(&mut rng);
|
||||
let x25519_noise = VersionedNoiseKey {
|
||||
version: NoiseVersion::V1,
|
||||
x25519_pubkey: *x25519::KeyPair::new(&mut rng).public_key(),
|
||||
};
|
||||
|
||||
let host_info = crate::api::v1::node::models::HostInformation {
|
||||
ip_address: vec!["1.1.1.1".parse().unwrap()],
|
||||
@@ -136,7 +144,7 @@ mod tests {
|
||||
keys: crate::api::v1::node::models::HostKeys {
|
||||
ed25519_identity: *ed22519.public_key(),
|
||||
x25519_sphinx: *x25519_sphinx.public_key(),
|
||||
x25519_noise: Some(*x25519_noise.public_key()),
|
||||
x25519_noise: Some(x25519_noise),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -189,7 +197,10 @@ mod tests {
|
||||
keys: crate::api::v1::node::models::HostKeys {
|
||||
ed25519_identity: legacy_info_noise.keys.ed25519_identity.parse().unwrap(),
|
||||
x25519_sphinx: legacy_info_noise.keys.x25519_sphinx.parse().unwrap(),
|
||||
x25519_noise: Some(legacy_info_noise.keys.x25519_noise.parse().unwrap()),
|
||||
x25519_noise: Some(VersionedNoiseKey {
|
||||
version: NoiseVersion::V1,
|
||||
x25519_pubkey: legacy_info_noise.keys.x25519_noise.parse().unwrap(),
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -216,6 +227,48 @@ mod tests {
|
||||
assert!(!current_struct_no_noise.verify(ed22519.public_key()));
|
||||
assert!(current_struct_no_noise.verify_host_information());
|
||||
|
||||
assert!(!current_struct_noise.verify(ed22519.public_key()));
|
||||
assert!(current_struct_noise.verify_host_information())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dummy_current_signed_host_verification() {
|
||||
let mut rng = rand_chacha::ChaCha20Rng::from_seed([0u8; 32]);
|
||||
let ed22519 = ed25519::KeyPair::new(&mut rng);
|
||||
let x25519_sphinx = x25519::KeyPair::new(&mut rng);
|
||||
let x25519_noise = x25519::KeyPair::new(&mut rng);
|
||||
|
||||
let host_info_no_noise = HostInformation {
|
||||
ip_address: vec!["1.1.1.1".parse().unwrap()],
|
||||
hostname: Some("foomp.com".to_string()),
|
||||
keys: crate::api::v1::node::models::HostKeys {
|
||||
ed25519_identity: *ed22519.public_key(),
|
||||
x25519_sphinx: *x25519_sphinx.public_key(),
|
||||
x25519_noise: None,
|
||||
},
|
||||
};
|
||||
|
||||
let host_info_noise = crate::api::v1::node::models::HostInformation {
|
||||
ip_address: vec!["1.1.1.1".parse().unwrap()],
|
||||
hostname: Some("foomp.com".to_string()),
|
||||
keys: crate::api::v1::node::models::HostKeys {
|
||||
ed25519_identity: *ed22519.public_key(),
|
||||
x25519_sphinx: *x25519_sphinx.public_key(),
|
||||
x25519_noise: Some(VersionedNoiseKey {
|
||||
version: NoiseVersion::V1,
|
||||
x25519_pubkey: *x25519_noise.public_key(),
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
// signature on legacy data
|
||||
let current_struct_no_noise =
|
||||
SignedData::new(host_info_no_noise, ed22519.private_key()).unwrap();
|
||||
let current_struct_noise = SignedData::new(host_info_noise, ed22519.private_key()).unwrap();
|
||||
|
||||
assert!(current_struct_no_noise.verify(ed22519.public_key()));
|
||||
assert!(current_struct_no_noise.verify_host_information());
|
||||
|
||||
// if noise key is present, the signature is actually valid
|
||||
assert!(current_struct_noise.verify(ed22519.public_key()));
|
||||
assert!(current_struct_noise.verify_host_information())
|
||||
|
||||
@@ -3,10 +3,8 @@
|
||||
|
||||
use celes::Country;
|
||||
use nym_crypto::asymmetric::ed25519::{self, serde_helpers::bs58_ed25519_pubkey};
|
||||
use nym_crypto::asymmetric::x25519::{
|
||||
self,
|
||||
serde_helpers::{bs58_x25519_pubkey, option_bs58_x25519_pubkey},
|
||||
};
|
||||
use nym_crypto::asymmetric::x25519::{self, serde_helpers::bs58_x25519_pubkey};
|
||||
use nym_noise_keys::VersionedNoiseKey;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::net::IpAddr;
|
||||
@@ -124,10 +122,7 @@ pub struct HostKeys {
|
||||
|
||||
/// Base58-encoded x25519 public key of this node used for the noise protocol.
|
||||
#[serde(default)]
|
||||
#[serde(with = "option_bs58_x25519_pubkey")]
|
||||
#[schemars(with = "Option<String>")]
|
||||
#[cfg_attr(feature = "openapi", schema(value_type = Option<String>))]
|
||||
pub x25519_noise: Option<x25519::PublicKey>,
|
||||
pub x25519_noise: Option<VersionedNoiseKey>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
@@ -150,7 +145,7 @@ impl From<HostKeys> for LegacyHostKeysV2 {
|
||||
x25519_sphinx: value.x25519_sphinx.to_base58_string(),
|
||||
x25519_noise: value
|
||||
.x25519_noise
|
||||
.map(|k| k.to_base58_string())
|
||||
.map(|k| k.x25519_pubkey.to_base58_string())
|
||||
.unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -742,8 +742,7 @@ impl Default for MixnetDebug {
|
||||
packet_forwarding_maximum_backoff: Self::DEFAULT_PACKET_FORWARDING_MAXIMUM_BACKOFF,
|
||||
initial_connection_timeout: Self::DEFAULT_INITIAL_CONNECTION_TIMEOUT,
|
||||
maximum_connection_buffer_size: Self::DEFAULT_MAXIMUM_CONNECTION_BUFFER_SIZE,
|
||||
// to be changed by @SW once the implementation is there
|
||||
unsafe_disable_noise: true,
|
||||
unsafe_disable_noise: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,13 +7,14 @@ use crate::node::http::api::api_requests;
|
||||
use crate::node::http::error::NymNodeHttpError;
|
||||
use nym_crypto::asymmetric::{ed25519, x25519};
|
||||
use nym_node_requests::api::SignedHostInformation;
|
||||
use nym_noise_keys::VersionedNoiseKey;
|
||||
|
||||
pub mod system_info;
|
||||
|
||||
pub(crate) fn sign_host_details(
|
||||
config: &Config,
|
||||
x22519_sphinx: &x25519::PublicKey,
|
||||
x25519_noise: &x25519::PublicKey,
|
||||
x25519_noise: &VersionedNoiseKey,
|
||||
ed22519_identity: &ed25519::KeyPair,
|
||||
) -> Result<SignedHostInformation, NymNodeError> {
|
||||
let x25519_noise = if config.mixnet.debug.unsafe_disable_noise {
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
|
||||
use crate::node::mixnet::shared::SharedData;
|
||||
use futures::StreamExt;
|
||||
use nym_noise::connection::Connection;
|
||||
use nym_noise::upgrade_noise_responder;
|
||||
use nym_sphinx_forwarding::packet::MixPacket;
|
||||
use nym_sphinx_framing::codec::NymCodec;
|
||||
use nym_sphinx_framing::packet::FramedNymPacket;
|
||||
@@ -61,7 +63,6 @@ impl PendingReplayCheckPackets {
|
||||
|
||||
pub(crate) struct ConnectionHandler {
|
||||
shared: SharedData,
|
||||
mixnet_connection: Framed<TcpStream, NymCodec>,
|
||||
remote_address: SocketAddr,
|
||||
|
||||
// packets pending for replay detection
|
||||
@@ -78,11 +79,7 @@ impl Drop for ConnectionHandler {
|
||||
}
|
||||
|
||||
impl ConnectionHandler {
|
||||
pub(crate) fn new(
|
||||
shared: &SharedData,
|
||||
tcp_stream: TcpStream,
|
||||
remote_address: SocketAddr,
|
||||
) -> Self {
|
||||
pub(crate) fn new(shared: &SharedData, remote_address: SocketAddr) -> Self {
|
||||
let shutdown = shared.shutdown.child_token(remote_address.to_string());
|
||||
shared.metrics.network.new_active_ingress_mixnet_client();
|
||||
|
||||
@@ -93,11 +90,11 @@ impl ConnectionHandler {
|
||||
replay_protection_filter: shared.replay_protection_filter.clone(),
|
||||
mixnet_forwarder: shared.mixnet_forwarder.clone(),
|
||||
final_hop: shared.final_hop.clone(),
|
||||
noise_config: shared.noise_config.clone(),
|
||||
metrics: shared.metrics.clone(),
|
||||
shutdown,
|
||||
},
|
||||
remote_address,
|
||||
mixnet_connection: Framed::new(tcp_stream, NymCodec),
|
||||
pending_packets: PendingReplayCheckPackets::new(),
|
||||
}
|
||||
}
|
||||
@@ -365,7 +362,29 @@ impl ConnectionHandler {
|
||||
remote = %self.remote_address
|
||||
)
|
||||
)]
|
||||
pub(crate) async fn handle_stream(&mut self) {
|
||||
pub(crate) async fn handle_connection(&mut self, socket: TcpStream) {
|
||||
let noise_stream = match upgrade_noise_responder(socket, &self.shared.noise_config).await {
|
||||
Ok(noise_stream) => noise_stream,
|
||||
Err(err) => {
|
||||
error!(
|
||||
"Failed to perform Noise handshake with {:?} - {err}",
|
||||
self.remote_address
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
debug!(
|
||||
"Noise responder handshake completed for {:?}",
|
||||
self.remote_address
|
||||
);
|
||||
self.handle_stream(Framed::new(noise_stream, NymCodec))
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn handle_stream(
|
||||
&mut self,
|
||||
mut mixnet_connection: Framed<Connection, NymCodec>,
|
||||
) {
|
||||
loop {
|
||||
tokio::select! {
|
||||
biased;
|
||||
@@ -373,7 +392,7 @@ impl ConnectionHandler {
|
||||
trace!("connection handler: received shutdown");
|
||||
break
|
||||
}
|
||||
maybe_framed_nym_packet = self.mixnet_connection.next() => {
|
||||
maybe_framed_nym_packet = mixnet_connection.next() => {
|
||||
match maybe_framed_nym_packet {
|
||||
Some(Ok(packet)) => self.handle_received_nym_packet(packet).await,
|
||||
Some(Err(err)) => {
|
||||
|
||||
@@ -10,6 +10,7 @@ use nym_gateway::node::GatewayStorageError;
|
||||
use nym_mixnet_client::forwarder::{MixForwardingSender, PacketToForward};
|
||||
use nym_node_metrics::mixnet::PacketKind;
|
||||
use nym_node_metrics::NymNodeMetrics;
|
||||
use nym_noise::config::NoiseConfig;
|
||||
use nym_sphinx_forwarding::packet::MixPacket;
|
||||
use nym_sphinx_framing::processing::{
|
||||
MixPacketVersion, MixProcessingResult, MixProcessingResultData, PacketProcessingError,
|
||||
@@ -75,6 +76,9 @@ pub(crate) struct SharedData {
|
||||
// data specific to the final hop (gateway) processing
|
||||
pub(super) final_hop: SharedFinalHopData,
|
||||
|
||||
// for establishing a Noise connection
|
||||
pub(super) noise_config: NoiseConfig,
|
||||
|
||||
pub(super) metrics: NymNodeMetrics,
|
||||
pub(super) shutdown: ShutdownToken,
|
||||
}
|
||||
@@ -87,12 +91,14 @@ fn convert_to_metrics_version(processed: MixPacketVersion) -> PacketKind {
|
||||
}
|
||||
|
||||
impl SharedData {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) fn new(
|
||||
processing_config: ProcessingConfig,
|
||||
x25519_keys: Arc<x25519::KeyPair>,
|
||||
replay_protection_filter: ReplayProtectionBloomfilter,
|
||||
mixnet_forwarder: MixForwardingSender,
|
||||
final_hop: SharedFinalHopData,
|
||||
noise_config: NoiseConfig,
|
||||
metrics: NymNodeMetrics,
|
||||
shutdown: ShutdownToken,
|
||||
) -> Self {
|
||||
@@ -102,6 +108,7 @@ impl SharedData {
|
||||
replay_protection_filter,
|
||||
mixnet_forwarder,
|
||||
final_hop,
|
||||
noise_config,
|
||||
metrics,
|
||||
shutdown,
|
||||
}
|
||||
@@ -164,8 +171,9 @@ impl SharedData {
|
||||
match accepted {
|
||||
Ok((socket, remote_addr)) => {
|
||||
debug!("accepted incoming mixnet connection from: {remote_addr}");
|
||||
let mut handler = ConnectionHandler::new(self, socket, remote_addr);
|
||||
let join_handle = tokio::spawn(async move { handler.handle_stream().await });
|
||||
let mut handler = ConnectionHandler::new(self, remote_addr);
|
||||
let join_handle =
|
||||
tokio::spawn(async move { handler.handle_connection(socket).await });
|
||||
self.log_connected_clients();
|
||||
Some(join_handle)
|
||||
}
|
||||
|
||||
@@ -44,6 +44,8 @@ use nym_network_requester::{
|
||||
use nym_node_metrics::events::MetricEventsSender;
|
||||
use nym_node_metrics::NymNodeMetrics;
|
||||
use nym_node_requests::api::v1::node::models::{AnnouncePorts, NodeDescription};
|
||||
use nym_noise::config::{NoiseConfig, NoiseNetworkView};
|
||||
use nym_noise_keys::VersionedNoiseKey;
|
||||
use nym_sphinx_acknowledgements::AckKey;
|
||||
use nym_sphinx_addressing::Recipient;
|
||||
use nym_task::{ShutdownManager, ShutdownToken, TaskClient};
|
||||
@@ -700,7 +702,10 @@ impl NymNode {
|
||||
let host_details = sign_host_details(
|
||||
&self.config,
|
||||
self.x25519_sphinx_keys.public_key(),
|
||||
self.x25519_noise_keys.public_key(),
|
||||
&VersionedNoiseKey {
|
||||
version: nym_noise::LATEST_NOISE_VERSION,
|
||||
x25519_pubkey: *self.x25519_noise_keys.public_key(),
|
||||
},
|
||||
&self.ed25519_identity_keys,
|
||||
)?;
|
||||
|
||||
@@ -1009,6 +1014,7 @@ impl NymNode {
|
||||
&self,
|
||||
active_clients_store: &ActiveClientsStore,
|
||||
routing_filter: F,
|
||||
noise_config: NoiseConfig,
|
||||
shutdown: ShutdownToken,
|
||||
) -> Result<(MixForwardingSender, ActiveConnections), NymNodeError>
|
||||
where
|
||||
@@ -1032,6 +1038,7 @@ impl NymNode {
|
||||
);
|
||||
let mixnet_client = nym_mixnet_client::Client::new(
|
||||
mixnet_client_config,
|
||||
noise_config.clone(),
|
||||
self.metrics
|
||||
.network
|
||||
.active_egress_mixnet_connections_counter(),
|
||||
@@ -1059,6 +1066,7 @@ impl NymNode {
|
||||
replay_protection_bloomfilter,
|
||||
mix_packet_sender.clone(),
|
||||
final_hop_data,
|
||||
noise_config,
|
||||
self.metrics.clone(),
|
||||
shutdown,
|
||||
);
|
||||
@@ -1068,9 +1076,17 @@ impl NymNode {
|
||||
}
|
||||
|
||||
pub(crate) async fn run_minimal_mixnet_processing(self) -> Result<(), NymNodeError> {
|
||||
let noise_config = nym_noise::config::NoiseConfig::new(
|
||||
self.x25519_noise_keys.clone(),
|
||||
NoiseNetworkView::new_empty(),
|
||||
self.config.mixnet.debug.initial_connection_timeout,
|
||||
)
|
||||
.with_unsafe_disabled(true);
|
||||
|
||||
self.start_mixnet_listener(
|
||||
&ActiveClientsStore::new(),
|
||||
OpenFilter,
|
||||
noise_config,
|
||||
self.shutdown_manager.clone_token("mixnet-traffic"),
|
||||
)
|
||||
.await?;
|
||||
@@ -1110,10 +1126,18 @@ impl NymNode {
|
||||
let network_refresher = self.build_network_refresher().await?;
|
||||
let active_clients_store = ActiveClientsStore::new();
|
||||
|
||||
let noise_config = nym_noise::config::NoiseConfig::new(
|
||||
self.x25519_noise_keys.clone(),
|
||||
network_refresher.noise_view(),
|
||||
self.config.mixnet.debug.initial_connection_timeout,
|
||||
)
|
||||
.with_unsafe_disabled(self.config.mixnet.debug.unsafe_disable_noise);
|
||||
|
||||
let (mix_packet_sender, active_egress_mixnet_connections) = self
|
||||
.start_mixnet_listener(
|
||||
&active_clients_store,
|
||||
network_refresher.routing_filter(),
|
||||
noise_config,
|
||||
self.shutdown_manager.clone_token("mixnet-traffic"),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -6,14 +6,15 @@ use crate::node::routing_filter::network_filter::NetworkRoutingFilter;
|
||||
use async_trait::async_trait;
|
||||
use nym_gateway::node::UserAgent;
|
||||
use nym_node_metrics::prometheus_wrapper::{PrometheusMetric, PROMETHEUS_METRICS};
|
||||
use nym_noise::config::NoiseNetworkView;
|
||||
use nym_task::ShutdownToken;
|
||||
use nym_topology::node::RoutingNode;
|
||||
use nym_topology::{EpochRewardedSet, NymTopology, Role, TopologyProvider};
|
||||
use nym_validator_client::nym_api::NymApiClientExt;
|
||||
use nym_validator_client::nym_nodes::{NodesByAddressesResponse, SkimmedNode};
|
||||
use nym_validator_client::nym_nodes::{NodesByAddressesResponse, SemiSkimmedNode};
|
||||
use nym_validator_client::{NymApiClient, ValidatorClientError};
|
||||
use std::collections::HashSet;
|
||||
use std::net::IpAddr;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::net::{IpAddr, SocketAddr};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tokio::sync::RwLock;
|
||||
@@ -53,10 +54,10 @@ impl NodesQuerier {
|
||||
res
|
||||
}
|
||||
|
||||
async fn current_nymnodes(&mut self) -> Result<Vec<SkimmedNode>, ValidatorClientError> {
|
||||
async fn current_nymnodes(&mut self) -> Result<Vec<SemiSkimmedNode>, ValidatorClientError> {
|
||||
let res = self
|
||||
.client
|
||||
.get_all_basic_nodes()
|
||||
.get_all_expanded_nodes()
|
||||
.await
|
||||
.inspect_err(|err| error!("failed to get network nodes: {err}"));
|
||||
|
||||
@@ -112,13 +113,19 @@ impl TopologyProvider for CachedTopologyProvider {
|
||||
let self_node = self.gateway_node.identity_key;
|
||||
|
||||
let mut topology = NymTopology::new_empty(network_guard.rewarded_set.clone())
|
||||
.with_additional_nodes(network_guard.network_nodes.iter().filter(|node| {
|
||||
if node.supported_roles.mixnode {
|
||||
node.performance.round_to_integer() >= self.min_mix_performance
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}));
|
||||
.with_additional_nodes(
|
||||
network_guard
|
||||
.network_nodes
|
||||
.iter()
|
||||
.map(|node| &node.basic)
|
||||
.filter(|node| {
|
||||
if node.supported_roles.mixnode {
|
||||
node.performance.round_to_integer() >= self.min_mix_performance
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
if !topology.has_node_details(self.gateway_node.node_id) {
|
||||
debug!("{self_node} didn't exist in topology. inserting it.",);
|
||||
@@ -148,7 +155,7 @@ impl CachedNetwork {
|
||||
|
||||
struct CachedNetworkInner {
|
||||
rewarded_set: EpochRewardedSet,
|
||||
network_nodes: Vec<SkimmedNode>,
|
||||
network_nodes: Vec<SemiSkimmedNode>,
|
||||
}
|
||||
|
||||
pub struct NetworkRefresher {
|
||||
@@ -159,6 +166,7 @@ pub struct NetworkRefresher {
|
||||
|
||||
network: CachedNetwork,
|
||||
routing_filter: NetworkRoutingFilter,
|
||||
noise_view: NoiseNetworkView,
|
||||
}
|
||||
|
||||
impl NetworkRefresher {
|
||||
@@ -186,6 +194,7 @@ impl NetworkRefresher {
|
||||
shutdown_token,
|
||||
network: CachedNetwork::new_empty(),
|
||||
routing_filter: NetworkRoutingFilter::new_empty(testnet),
|
||||
noise_view: NoiseNetworkView::new_empty(),
|
||||
};
|
||||
|
||||
this.obtain_initial_network().await?;
|
||||
@@ -240,7 +249,7 @@ impl NetworkRefresher {
|
||||
// collect all known/allowed nodes information
|
||||
let known_nodes = nodes
|
||||
.iter()
|
||||
.flat_map(|n| n.ip_addresses.iter())
|
||||
.flat_map(|n| n.basic.ip_addresses.iter())
|
||||
.copied()
|
||||
.collect::<HashSet<_>>();
|
||||
|
||||
@@ -263,6 +272,22 @@ impl NetworkRefresher {
|
||||
self.routing_filter.resolved.swap_denied(current_denied);
|
||||
self.routing_filter.pending.clear().await;
|
||||
|
||||
//update noise Noise Nodes
|
||||
let noise_nodes = nodes
|
||||
.iter()
|
||||
.filter(|n| n.x25519_noise_versioned_key.is_some())
|
||||
.flat_map(|n| {
|
||||
n.basic.ip_addresses.iter().map(|ip_addr| {
|
||||
(
|
||||
SocketAddr::new(*ip_addr, n.basic.mix_port),
|
||||
#[allow(clippy::unwrap_used)]
|
||||
n.x25519_noise_versioned_key.unwrap(), // SAFETY : we filtered out nodes where this option can be None
|
||||
)
|
||||
})
|
||||
})
|
||||
.collect::<HashMap<_, _>>();
|
||||
self.noise_view.swap_view(noise_nodes);
|
||||
|
||||
let mut network_guard = self.network.inner.write().await;
|
||||
network_guard.network_nodes = nodes;
|
||||
network_guard.rewarded_set = rewarded_set;
|
||||
@@ -296,6 +321,10 @@ impl NetworkRefresher {
|
||||
self.network.clone()
|
||||
}
|
||||
|
||||
pub(crate) fn noise_view(&self) -> NoiseNetworkView {
|
||||
self.noise_view.clone()
|
||||
}
|
||||
|
||||
pub(crate) async fn run(&mut self) {
|
||||
let mut full_refresh_interval = interval(self.full_refresh_interval);
|
||||
full_refresh_interval.reset();
|
||||
|
||||
Generated
+12
@@ -4017,6 +4017,7 @@ dependencies = [
|
||||
"nym-mixnet-contract-common",
|
||||
"nym-network-defaults",
|
||||
"nym-node-requests",
|
||||
"nym-noise-keys",
|
||||
"nym-serde-helpers",
|
||||
"nym-ticketbooks-merkle",
|
||||
"schemars",
|
||||
@@ -4277,6 +4278,7 @@ dependencies = [
|
||||
"nym-crypto",
|
||||
"nym-exit-policy",
|
||||
"nym-http-api-client",
|
||||
"nym-noise-keys",
|
||||
"nym-wireguard-types",
|
||||
"schemars",
|
||||
"serde",
|
||||
@@ -4287,6 +4289,16 @@ dependencies = [
|
||||
"utoipa",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nym-noise-keys"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"nym-crypto",
|
||||
"schemars",
|
||||
"serde",
|
||||
"utoipa",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nym-pemstore"
|
||||
version = "0.3.0"
|
||||
|
||||
Reference in New Issue
Block a user