Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ab1fc724ae | |||
| 775250b0ee | |||
| e975051566 | |||
| 8207a9c38a | |||
| bbdf0f158a | |||
| b276a8bc61 | |||
| f31e6f2470 | |||
| ea08345025 | |||
| 3e5537b753 | |||
| 23f86b6c1e | |||
| 8ba58ba11e | |||
| be16fddc75 | |||
| a7d6cba11d | |||
| a67ff33054 | |||
| 61badfdcfe | |||
| 19dfbeb2b4 | |||
| 9f13616c24 |
+3
-3
@@ -415,9 +415,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/undici": {
|
||||
"version": "5.28.5",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-5.28.5.tgz",
|
||||
"integrity": "sha512-zICwjrDrcrUE0pyyJc1I2QzBkLM8FINsgOrt6WjA+BgajVq9Nxu2PbFFXUrAggLfDXlZGZBVZYw7WNV5KiBiBA==",
|
||||
"version": "5.29.0",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-5.29.0.tgz",
|
||||
"integrity": "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@fastify/busboy": "^2.0.0"
|
||||
|
||||
Generated
+1270
-506
File diff suppressed because it is too large
Load Diff
+6
-3
@@ -66,6 +66,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",
|
||||
@@ -203,7 +205,7 @@ bloomfilter = "3.0.1"
|
||||
bs58 = "0.5.1"
|
||||
bytecodec = "0.4.15"
|
||||
bytes = "1.10.1"
|
||||
cargo_metadata = "0.18.1"
|
||||
cargo_metadata = "0.19.2"
|
||||
celes = "2.6.0"
|
||||
cfg-if = "1.0.0"
|
||||
chacha20 = "0.9.0"
|
||||
@@ -310,6 +312,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.8.6"
|
||||
strum = "0.26"
|
||||
@@ -319,10 +322,10 @@ syn = "1"
|
||||
sysinfo = "0.33.0"
|
||||
tap = "1.0.1"
|
||||
tar = "0.4.44"
|
||||
tempfile = "3.19"
|
||||
tempfile = "3.20"
|
||||
thiserror = "2.0"
|
||||
time = "0.3.41"
|
||||
tokio = "1.44"
|
||||
tokio = "1.45"
|
||||
tokio-postgres = "0.7"
|
||||
tokio-stream = "0.1.17"
|
||||
tokio-test = "0.4.4"
|
||||
|
||||
@@ -2048,10 +2048,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/http-proxy-middleware": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.4.tgz",
|
||||
"integrity": "sha512-m/4FxX17SUvz4lJ5WPXOHDUuCwIqXLfLHs1s0uZ3oYjhoXlx9csYxaOa0ElDEJ+h8Q4iJ1s+lTMbiCa4EXIJqg==",
|
||||
"version": "2.0.9",
|
||||
"resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz",
|
||||
"integrity": "sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/http-proxy": "^1.17.8",
|
||||
"http-proxy": "^1.18.1",
|
||||
@@ -6095,9 +6096,9 @@
|
||||
}
|
||||
},
|
||||
"http-proxy-middleware": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.4.tgz",
|
||||
"integrity": "sha512-m/4FxX17SUvz4lJ5WPXOHDUuCwIqXLfLHs1s0uZ3oYjhoXlx9csYxaOa0ElDEJ+h8Q4iJ1s+lTMbiCa4EXIJqg==",
|
||||
"version": "2.0.9",
|
||||
"resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz",
|
||||
"integrity": "sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/http-proxy": "^1.17.8",
|
||||
|
||||
@@ -108,7 +108,7 @@ impl GatewayClient {
|
||||
#[cfg(feature = "verify")]
|
||||
pub fn verify(&self, gateway_key: &PrivateKey, nonce: u64) -> Result<(), Error> {
|
||||
// use gateways key as a ref to an x25519_dalek key
|
||||
let dh = (gateway_key.as_ref()).diffie_hellman(&self.pub_key);
|
||||
let dh = gateway_key.inner().diffie_hellman(&self.pub_key);
|
||||
|
||||
// TODO: change that to use our nym_crypto::hmac module instead
|
||||
#[allow(clippy::expect_used)]
|
||||
|
||||
@@ -117,7 +117,7 @@ impl GatewayClient {
|
||||
#[cfg(feature = "verify")]
|
||||
pub fn verify(&self, gateway_key: &PrivateKey, nonce: u64) -> Result<(), Error> {
|
||||
// use gateways key as a ref to an x25519_dalek key
|
||||
let dh = (gateway_key.as_ref()).diffie_hellman(&self.pub_key);
|
||||
let dh = gateway_key.inner().diffie_hellman(&self.pub_key);
|
||||
|
||||
// TODO: change that to use our nym_crypto::hmac module instead
|
||||
#[allow(clippy::expect_used)]
|
||||
|
||||
@@ -117,7 +117,7 @@ impl GatewayClient {
|
||||
#[cfg(feature = "verify")]
|
||||
pub fn verify(&self, gateway_key: &PrivateKey, nonce: u64) -> Result<(), Error> {
|
||||
// use gateways key as a ref to an x25519_dalek key
|
||||
let dh = (gateway_key.as_ref()).diffie_hellman(&self.pub_key);
|
||||
let dh = gateway_key.inner().diffie_hellman(&self.pub_key);
|
||||
|
||||
// TODO: change that to use our nym_crypto::hmac module instead
|
||||
#[allow(clippy::expect_used)]
|
||||
|
||||
@@ -169,7 +169,7 @@ impl GatewayClient {
|
||||
#[cfg(feature = "verify")]
|
||||
pub fn verify(&self, gateway_key: &PrivateKey, nonce: u64) -> Result<(), Error> {
|
||||
// use gateways key as a ref to an x25519_dalek key
|
||||
let dh = (gateway_key.as_ref()).diffie_hellman(&self.pub_key);
|
||||
let dh = gateway_key.inner().diffie_hellman(&self.pub_key);
|
||||
|
||||
// TODO: change that to use our nym_crypto::hmac module instead
|
||||
#[allow(clippy::expect_used)]
|
||||
|
||||
@@ -169,7 +169,7 @@ impl GatewayClient {
|
||||
#[cfg(feature = "verify")]
|
||||
pub fn verify(&self, gateway_key: &PrivateKey, nonce: u64) -> Result<(), Error> {
|
||||
// use gateways key as a ref to an x25519_dalek key
|
||||
let dh = (gateway_key.as_ref()).diffie_hellman(&self.pub_key);
|
||||
let dh = gateway_key.inner().diffie_hellman(&self.pub_key);
|
||||
|
||||
// TODO: change that to use our nym_crypto::hmac module instead
|
||||
#[allow(clippy::expect_used)]
|
||||
|
||||
@@ -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::forwarding::packet::MixPacket;
|
||||
use nym_sphinx::framing::codec::NymCodec;
|
||||
use nym_sphinx::framing::packet::FramedNymPacket;
|
||||
@@ -52,6 +54,7 @@ pub trait SendWithoutResponse {
|
||||
|
||||
pub struct Client {
|
||||
active_connections: ActiveConnections,
|
||||
noise_config: NoiseConfig,
|
||||
connections_count: Arc<AtomicUsize>,
|
||||
config: Config,
|
||||
}
|
||||
@@ -97,6 +100,7 @@ impl ConnectionSender {
|
||||
|
||||
struct ManagedConnection {
|
||||
address: SocketAddr,
|
||||
noise_config: NoiseConfig,
|
||||
message_receiver: ReceiverStream<FramedNymPacket>,
|
||||
connection_timeout: Duration,
|
||||
current_reconnection: Arc<AtomicU32>,
|
||||
@@ -105,12 +109,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,
|
||||
@@ -125,9 +131,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})",);
|
||||
@@ -160,9 +178,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,
|
||||
}
|
||||
@@ -217,6 +240,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 {
|
||||
@@ -227,6 +251,7 @@ impl Client {
|
||||
connections_count.fetch_add(1, Ordering::SeqCst);
|
||||
ManagedConnection::new(
|
||||
address,
|
||||
noise_config,
|
||||
receiver,
|
||||
initial_connection_timeout,
|
||||
current_reconnection_attempt,
|
||||
@@ -291,8 +316,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),
|
||||
@@ -300,6 +329,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(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ use nym_api_requests::models::{
|
||||
};
|
||||
use nym_api_requests::models::{LegacyDescribedGateway, MixNodeBondAnnotated};
|
||||
use nym_api_requests::nym_nodes::{
|
||||
NodesByAddressesResponse, SkimmedNode, SkimmedNodesWithMetadata,
|
||||
NodesByAddressesResponse, SemiSkimmedNodesWithMetadata, SkimmedNode, SkimmedNodesWithMetadata,
|
||||
};
|
||||
use nym_coconut_dkg_common::types::EpochId;
|
||||
use nym_http_api_client::UserAgent;
|
||||
@@ -530,6 +530,43 @@ impl NymApiClient {
|
||||
collect_paged_skimmed_v2!(self, get_basic_nodes_v2)
|
||||
}
|
||||
|
||||
/// retrieve expanded information for all bonded nodes on the network
|
||||
pub async fn get_all_expanded_nodes(
|
||||
&self,
|
||||
) -> Result<SemiSkimmedNodesWithMetadata, ValidatorClientError> {
|
||||
// Unroll the first iteration to get the metadata
|
||||
let mut page = 0;
|
||||
|
||||
let res = self
|
||||
.nym_api
|
||||
.get_expanded_nodes(false, Some(page), None)
|
||||
.await?;
|
||||
let mut nodes = res.nodes.data;
|
||||
let metadata = res.metadata;
|
||||
|
||||
if res.nodes.pagination.total == nodes.len() {
|
||||
return Ok(SemiSkimmedNodesWithMetadata::new(nodes, metadata));
|
||||
}
|
||||
|
||||
page += 1;
|
||||
|
||||
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(SemiSkimmedNodesWithMetadata::new(nodes, metadata))
|
||||
}
|
||||
|
||||
pub async fn health(&self) -> Result<ApiHealthResponse, ValidatorClientError> {
|
||||
Ok(self.nym_api.health().await?)
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ pub use nym_api_requests::{
|
||||
MixnodeStatusResponse, MixnodeUptimeHistoryResponse, RewardEstimationResponse,
|
||||
StakeSaturationResponse, UptimeResponse,
|
||||
},
|
||||
nym_nodes::{CachedNodesResponse, SkimmedNode},
|
||||
nym_nodes::{CachedNodesResponse, SemiSkimmedNode, SkimmedNode},
|
||||
NymNetworkDetailsResponse,
|
||||
};
|
||||
use nym_contracts_common::IdentityKey;
|
||||
@@ -643,6 +643,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<PaginatedCachedNodesResponseV2<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::V2_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> {
|
||||
|
||||
@@ -218,6 +218,12 @@ impl From<PublicKey> for x25519_dalek::PublicKey {
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<[u8]> for PublicKey {
|
||||
fn as_ref(&self) -> &[u8] {
|
||||
self.0.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Zeroize, ZeroizeOnDrop)]
|
||||
pub struct PrivateKey(x25519_dalek::StaticSecret);
|
||||
|
||||
@@ -248,6 +254,10 @@ impl PrivateKey {
|
||||
PrivateKey(x25519_secret)
|
||||
}
|
||||
|
||||
pub fn inner(&self) -> &x25519_dalek::StaticSecret {
|
||||
&self.0
|
||||
}
|
||||
|
||||
pub fn public_key(&self) -> PublicKey {
|
||||
self.into()
|
||||
}
|
||||
@@ -256,6 +266,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 {
|
||||
@@ -335,6 +349,12 @@ impl AsRef<x25519_dalek::StaticSecret> for PrivateKey {
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<[u8]> for PrivateKey {
|
||||
fn as_ref(&self) -> &[u8] {
|
||||
self.0.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
[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" }
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow = { workspace = true }
|
||||
tokio = { workspace = true, features = ["full"] }
|
||||
rand_chacha = { workspace = true }
|
||||
nym-crypto = { path = "../crypto", features = ["rand"] }
|
||||
|
||||
|
||||
[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,43 @@
|
||||
// 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,
|
||||
Unknown(u8), //Implies a newer version we don't know
|
||||
}
|
||||
|
||||
impl From<u8> for NoiseVersion {
|
||||
fn from(value: u8) -> Self {
|
||||
match value {
|
||||
1 => NoiseVersion::V1,
|
||||
other => NoiseVersion::Unknown(other),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<NoiseVersion> for u8 {
|
||||
fn from(version: NoiseVersion) -> Self {
|
||||
match version {
|
||||
NoiseVersion::V1 => 1,
|
||||
NoiseVersion::Unknown(other) => other,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, utoipa::ToSchema)]
|
||||
pub struct VersionedNoiseKey {
|
||||
#[schemars(with = "u8")]
|
||||
#[schema(value_type = u8)]
|
||||
pub supported_version: NoiseVersion,
|
||||
|
||||
#[schemars(with = "String")]
|
||||
#[serde(with = "bs58_x25519_pubkey")]
|
||||
#[schema(value_type = String)]
|
||||
pub x25519_pubkey: x25519::PublicKey,
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
// 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, FromRepr};
|
||||
|
||||
#[derive(Default, Debug, Clone, Copy, EnumIter, FromRepr, Eq, PartialEq)]
|
||||
#[repr(u8)]
|
||||
#[non_exhaustive]
|
||||
pub enum NoisePattern {
|
||||
#[default]
|
||||
XKpsk3 = 1,
|
||||
IKpsk2 = 2,
|
||||
}
|
||||
|
||||
impl NoisePattern {
|
||||
pub(crate) const 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.supported_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,67 @@
|
||||
// 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};
|
||||
|
||||
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<C> {
|
||||
Raw(#[pin] C),
|
||||
Noise(#[pin] Box<NoiseStream<C>>),
|
||||
}
|
||||
|
||||
impl<C> AsyncRead for Connection<C>
|
||||
where
|
||||
C: AsyncRead + AsyncWrite + Unpin,
|
||||
{
|
||||
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::Raw(stream) => stream.poll_read(cx, buf),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<C> AsyncWrite for Connection<C>
|
||||
where
|
||||
C: AsyncWrite + AsyncRead + Unpin,
|
||||
{
|
||||
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::Raw(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::Raw(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::Raw(stream) => stream.poll_shutdown(cx),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use nym_noise_keys::NoiseVersion;
|
||||
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 (encoded value: {encoded})")]
|
||||
UnknownVersion { encoded: u8 },
|
||||
|
||||
#[error("unknown noise pattern (encoded value: {encoded})")]
|
||||
UnknownPattern { encoded: u8 },
|
||||
|
||||
#[error("unknown noise message type (encoded value: {encoded})")]
|
||||
UnknownMessageType { encoded: u8 },
|
||||
|
||||
#[error("failed to generate psk for requested version {noise_version}")]
|
||||
PskGenerationFailure { noise_version: u8 },
|
||||
|
||||
#[error("noise initiator attempted to use version v{noise_version} of the protocol - we don't know how to handle it")]
|
||||
UnknownVersionHandshake { noise_version: u8 },
|
||||
|
||||
#[error("noise initiator attempted to use an unexpected noise pattern. we're configured for {configured} while it requested {received}")]
|
||||
UnexpectedNoisePattern {
|
||||
configured: &'static str,
|
||||
received: &'static str,
|
||||
},
|
||||
|
||||
#[error("handshake version has unexpectedly changed. initial was {initial:?} and received {received:?}")]
|
||||
UnexpectedHandshakeVersion {
|
||||
initial: NoiseVersion,
|
||||
received: NoiseVersion,
|
||||
},
|
||||
|
||||
#[error("data packet version has unexpectedly changed. initial was {initial:?} and received {received:?}")]
|
||||
UnexpectedDataVersion {
|
||||
initial: NoiseVersion,
|
||||
received: NoiseVersion,
|
||||
},
|
||||
|
||||
#[error("received a non-handshake message during noise handshake")]
|
||||
NonHandshakeMessageReceived,
|
||||
|
||||
#[error("received a non-data message post noise handshake")]
|
||||
NonDataMessageReceived,
|
||||
|
||||
#[error("handshake message exceeded maximum size (got {size} bytes)")]
|
||||
HandshakeTooBig { size: usize },
|
||||
|
||||
#[error("noise message exceeded maximum size (got {size} bytes)")]
|
||||
DataTooBig { size: usize },
|
||||
|
||||
#[error("Handshake timeout")]
|
||||
HandshakeTimeout(#[from] tokio::time::error::Elapsed),
|
||||
}
|
||||
|
||||
impl NoiseError {
|
||||
pub(crate) fn naive_to_io_error(self) -> std::io::Error {
|
||||
match self {
|
||||
NoiseError::IoError(err) => err,
|
||||
other => std::io::Error::other(other),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Error> for NoiseError {
|
||||
fn from(err: Error) -> Self {
|
||||
match err {
|
||||
Error::Decrypt => NoiseError::DecryptionError,
|
||||
err => NoiseError::ProtocolError(err),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use nym_noise_keys::NoiseVersion;
|
||||
use snow::error::Prerequisite;
|
||||
use snow::Error;
|
||||
use tokio::net::TcpStream;
|
||||
use tracing::{error, warn};
|
||||
|
||||
pub mod config;
|
||||
pub mod connection;
|
||||
pub mod error;
|
||||
pub mod stream;
|
||||
|
||||
use crate::config::NoiseConfig;
|
||||
use crate::connection::Connection;
|
||||
use crate::error::NoiseError;
|
||||
use crate::stream::NoiseStreamBuilder;
|
||||
|
||||
const NOISE_PSK_PREFIX: &[u8] = b"NYMTECH_NOISE_dQw4w9WgXcQ";
|
||||
|
||||
pub const LATEST_NOISE_VERSION: NoiseVersion = NoiseVersion::V1;
|
||||
|
||||
// TODO: this should be behind some trait because presumably, depending on the version,
|
||||
// other arguments would be needed
|
||||
mod psk_gen {
|
||||
use crate::error::NoiseError;
|
||||
use crate::stream::Psk;
|
||||
use crate::NOISE_PSK_PREFIX;
|
||||
use nym_crypto::asymmetric::x25519;
|
||||
use nym_noise_keys::NoiseVersion;
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
pub(crate) fn generate_psk(
|
||||
responder_pub_key: x25519::PublicKey,
|
||||
version: NoiseVersion,
|
||||
) -> Result<Psk, NoiseError> {
|
||||
match version {
|
||||
NoiseVersion::V1 => Ok(generate_psk_v1(responder_pub_key)),
|
||||
NoiseVersion::Unknown(noise_version) => {
|
||||
Err(NoiseError::PskGenerationFailure { noise_version })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn upgrade_noise_initiator(
|
||||
conn: TcpStream,
|
||||
config: &NoiseConfig,
|
||||
) -> Result<Connection<TcpStream>, NoiseError> {
|
||||
if config.unsafe_disabled {
|
||||
warn!("Noise is disabled in the config. Not attempting any handshake");
|
||||
return Ok(Connection::Raw(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)
|
||||
})?;
|
||||
|
||||
let Some(key) = config.get_noise_key(&responder_addr) else {
|
||||
warn!("{responder_addr} can't speak Noise yet, falling back to TCP");
|
||||
return Ok(Connection::Raw(conn));
|
||||
};
|
||||
|
||||
let handshake_version = match key.supported_version {
|
||||
NoiseVersion::V1 => NoiseVersion::V1,
|
||||
|
||||
// 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(version) => {
|
||||
warn!("{responder_addr} is announcing an v{version} version of Noise that we don't know how to parse, we will attempt to downgrade to our current highest supported version");
|
||||
LATEST_NOISE_VERSION
|
||||
}
|
||||
};
|
||||
|
||||
NoiseStreamBuilder::new(conn)
|
||||
.perform_initiator_handshake(config, handshake_version, key.x25519_pubkey)
|
||||
.await
|
||||
.map(|stream| Connection::Noise(Box::new(stream)))
|
||||
}
|
||||
pub async fn upgrade_noise_responder(
|
||||
conn: TcpStream,
|
||||
config: &NoiseConfig,
|
||||
) -> Result<Connection<TcpStream>, NoiseError> {
|
||||
if config.unsafe_disabled {
|
||||
warn!("Noise is disabled in the config. Not attempting any handshake");
|
||||
return Ok(Connection::Raw(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());
|
||||
}
|
||||
};
|
||||
|
||||
// if responder doesn't announce noise support, we fallback to tcp
|
||||
if config.get_noise_support(initiator_addr.ip()).is_none() {
|
||||
warn!("{initiator_addr} can't speak Noise yet, falling back to TCP",);
|
||||
return Ok(Connection::Raw(conn));
|
||||
};
|
||||
|
||||
NoiseStreamBuilder::new(conn)
|
||||
.perform_responder_handshake(config)
|
||||
.await
|
||||
.map(|stream| Connection::Noise(Box::new(stream)))
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::error::NoiseError;
|
||||
use crate::stream::framing::{NymNoiseFrame, NymNoiseHeader};
|
||||
use bytes::{BufMut, BytesMut};
|
||||
use tokio_util::codec::{Decoder, Encoder};
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum DecodeState {
|
||||
Header,
|
||||
Payload(NymNoiseHeader),
|
||||
}
|
||||
|
||||
pub struct NymNoiseCodec {
|
||||
state: DecodeState,
|
||||
}
|
||||
|
||||
impl NymNoiseCodec {
|
||||
pub fn new() -> Self {
|
||||
NymNoiseCodec {
|
||||
state: DecodeState::Header,
|
||||
}
|
||||
}
|
||||
|
||||
fn decode_header(&self, src: &mut BytesMut) -> Result<Option<NymNoiseHeader>, NoiseError> {
|
||||
if src.len() < NymNoiseHeader::SIZE {
|
||||
// Not enough data
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// note: successful call to 'decode' advances the buffer by NymNoiseHeader::SIZE
|
||||
let Some(header) = NymNoiseHeader::decode(src)? else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
Ok(Some(header))
|
||||
}
|
||||
|
||||
fn decode_data(&self, n: usize, src: &mut BytesMut) -> Option<BytesMut> {
|
||||
// At this point, the buffer has already had the required capacity
|
||||
// reserved. All there is to do is read.
|
||||
if src.len() < n {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(src.split_to(n))
|
||||
}
|
||||
}
|
||||
|
||||
impl Decoder for NymNoiseCodec {
|
||||
type Item = NymNoiseFrame;
|
||||
type Error = NoiseError;
|
||||
|
||||
fn decode(&mut self, src: &mut BytesMut) -> Result<Option<Self::Item>, Self::Error> {
|
||||
let header = match self.state {
|
||||
DecodeState::Header => match self.decode_header(src)? {
|
||||
None => return Ok(None),
|
||||
Some(header) => {
|
||||
self.state = DecodeState::Payload(header);
|
||||
header
|
||||
}
|
||||
},
|
||||
DecodeState::Payload(header) => header,
|
||||
};
|
||||
|
||||
let Some(data) = self.decode_data(header.data_len as usize, src) else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
// Update the decode state
|
||||
self.state = DecodeState::Header;
|
||||
|
||||
// make sure the buffer has enough space to read the next header
|
||||
src.reserve(NymNoiseHeader::SIZE);
|
||||
|
||||
Ok(Some(NymNoiseFrame {
|
||||
header,
|
||||
data: data.freeze(),
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
impl Encoder<NymNoiseFrame> for NymNoiseCodec {
|
||||
type Error = NoiseError;
|
||||
|
||||
fn encode(&mut self, frame: NymNoiseFrame, dst: &mut BytesMut) -> Result<(), Self::Error> {
|
||||
frame.header.encode(dst);
|
||||
dst.put_slice(frame.data.as_ref());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::config::NoisePattern;
|
||||
use crate::error::NoiseError;
|
||||
use bytes::{Buf, BufMut, Bytes, BytesMut};
|
||||
use nym_noise_keys::NoiseVersion;
|
||||
use strum::FromRepr;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct NymNoiseFrame {
|
||||
pub header: NymNoiseHeader,
|
||||
pub data: Bytes,
|
||||
}
|
||||
|
||||
impl NymNoiseFrame {
|
||||
pub fn new_handshake_frame(
|
||||
data: Bytes,
|
||||
version: NoiseVersion,
|
||||
pattern: NoisePattern,
|
||||
) -> Result<Self, NoiseError> {
|
||||
if data.len() > u16::MAX as usize {
|
||||
return Err(NoiseError::HandshakeTooBig { size: data.len() });
|
||||
}
|
||||
|
||||
Ok(NymNoiseFrame {
|
||||
header: NymNoiseHeader {
|
||||
version,
|
||||
noise_pattern: pattern,
|
||||
message_type: NymNoiseMessageType::Handshake,
|
||||
data_len: data.len() as u16,
|
||||
},
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn new_data_frame(
|
||||
data: Bytes,
|
||||
version: NoiseVersion,
|
||||
pattern: NoisePattern,
|
||||
) -> Result<Self, NoiseError> {
|
||||
if data.len() > u16::MAX as usize {
|
||||
return Err(NoiseError::HandshakeTooBig { size: data.len() });
|
||||
}
|
||||
|
||||
Ok(NymNoiseFrame {
|
||||
header: NymNoiseHeader {
|
||||
version,
|
||||
noise_pattern: pattern,
|
||||
message_type: NymNoiseMessageType::Data,
|
||||
data_len: data.len() as u16,
|
||||
},
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn version(&self) -> NoiseVersion {
|
||||
self.header.version
|
||||
}
|
||||
|
||||
pub fn is_handshake_message(&self) -> bool {
|
||||
self.header.is_handshake_message()
|
||||
}
|
||||
|
||||
pub fn is_data_message(&self) -> bool {
|
||||
self.header.is_data_message()
|
||||
}
|
||||
|
||||
pub fn noise_pattern(&self) -> NoisePattern {
|
||||
self.header.noise_pattern
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, FromRepr)]
|
||||
#[repr(u8)]
|
||||
#[non_exhaustive]
|
||||
pub enum NymNoiseMessageType {
|
||||
Handshake = 0,
|
||||
Data = 1,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct NymNoiseHeader {
|
||||
pub version: NoiseVersion,
|
||||
pub noise_pattern: NoisePattern,
|
||||
pub message_type: NymNoiseMessageType,
|
||||
pub data_len: u16,
|
||||
}
|
||||
|
||||
impl NymNoiseHeader {
|
||||
pub(crate) const SIZE: usize = 8;
|
||||
|
||||
pub fn is_handshake_message(&self) -> bool {
|
||||
matches!(self.message_type, NymNoiseMessageType::Handshake)
|
||||
}
|
||||
|
||||
pub fn is_data_message(&self) -> bool {
|
||||
matches!(self.message_type, NymNoiseMessageType::Data)
|
||||
}
|
||||
|
||||
// 0 1 2 3 4 5 6 7 8
|
||||
// +-+-+-+-+-+-+-+-+
|
||||
// |V|P|T|Len| Res.|
|
||||
// +-+-+-+-+-+-+-+-+
|
||||
pub(crate) fn encode(&self, dst: &mut BytesMut) {
|
||||
dst.reserve(Self::SIZE);
|
||||
|
||||
// byte 0
|
||||
dst.put_u8(self.version.into());
|
||||
|
||||
// byte 1
|
||||
dst.put_u8(self.noise_pattern as u8);
|
||||
|
||||
// byte 2
|
||||
dst.put_u8(self.message_type as u8);
|
||||
|
||||
// byte 3-4
|
||||
dst.put_u16(self.data_len);
|
||||
|
||||
// byte 5-7 (RESERVED):
|
||||
dst.extend_from_slice(&[0u8; 3])
|
||||
}
|
||||
|
||||
pub(crate) fn decode(src: &mut BytesMut) -> Result<Option<Self>, NoiseError> {
|
||||
if src.len() < Self::SIZE {
|
||||
// can't do anything if we don't have enough bytes - but reserve enough for the next call
|
||||
src.reserve(Self::SIZE);
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let version = src.get_u8();
|
||||
let pattern = src.get_u8();
|
||||
let message_type = src.get_u8();
|
||||
let data_len = src.get_u16();
|
||||
|
||||
// reserved
|
||||
src.advance(3);
|
||||
|
||||
let version = NoiseVersion::from(version);
|
||||
|
||||
// here, based on versions, we could do vary the further parsing
|
||||
// match version {
|
||||
// NoiseVersion::V1 => {}
|
||||
// NoiseVersion::Unknown(_) => {}
|
||||
// }
|
||||
|
||||
let noise_pattern = NoisePattern::from_repr(pattern)
|
||||
.ok_or(NoiseError::UnknownPattern { encoded: pattern })?;
|
||||
let message_type =
|
||||
NymNoiseMessageType::from_repr(message_type).ok_or(NoiseError::UnknownMessageType {
|
||||
encoded: message_type,
|
||||
})?;
|
||||
|
||||
Ok(Some(NymNoiseHeader {
|
||||
version,
|
||||
noise_pattern,
|
||||
message_type,
|
||||
data_len,
|
||||
}))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,583 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::config::{NoiseConfig, NoisePattern};
|
||||
use crate::error::NoiseError;
|
||||
use crate::psk_gen::generate_psk;
|
||||
use crate::stream::codec::NymNoiseCodec;
|
||||
use crate::stream::framing::NymNoiseFrame;
|
||||
use bytes::{Bytes, BytesMut};
|
||||
use futures::{Sink, SinkExt, Stream, StreamExt};
|
||||
use nym_crypto::asymmetric::x25519;
|
||||
use nym_noise_keys::NoiseVersion;
|
||||
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};
|
||||
use tokio_util::codec::Framed;
|
||||
|
||||
mod codec;
|
||||
mod framing;
|
||||
|
||||
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];
|
||||
|
||||
pub(crate) struct NoiseStreamBuilder<C> {
|
||||
inner_stream: Framed<C, NymNoiseCodec>,
|
||||
}
|
||||
|
||||
impl<C> NoiseStreamBuilder<C> {
|
||||
pub(crate) fn new(inner_stream: C) -> Self
|
||||
where
|
||||
C: AsyncRead + AsyncWrite,
|
||||
{
|
||||
NoiseStreamBuilder {
|
||||
inner_stream: Framed::new(inner_stream, NymNoiseCodec::new()),
|
||||
}
|
||||
}
|
||||
|
||||
async fn perform_initiator_handshake_inner(
|
||||
self,
|
||||
pattern: NoisePattern,
|
||||
local_private_key: impl AsRef<[u8]>,
|
||||
remote_pub_key: impl AsRef<[u8]>,
|
||||
psk: Psk,
|
||||
version: NoiseVersion,
|
||||
) -> Result<NoiseStream<C>, NoiseError>
|
||||
where
|
||||
C: AsyncRead + AsyncWrite + Unpin,
|
||||
{
|
||||
let handshake = Builder::new(pattern.as_noise_params())
|
||||
.local_private_key(local_private_key.as_ref())
|
||||
.remote_public_key(remote_pub_key.as_ref())
|
||||
.psk(pattern.psk_position(), &psk)
|
||||
.build_initiator()?;
|
||||
|
||||
self.perform_handshake(handshake, version, pattern).await
|
||||
}
|
||||
|
||||
pub(crate) async fn perform_initiator_handshake(
|
||||
self,
|
||||
config: &NoiseConfig,
|
||||
version: NoiseVersion,
|
||||
remote_pub_key: x25519::PublicKey,
|
||||
) -> Result<NoiseStream<C>, NoiseError>
|
||||
where
|
||||
C: AsyncRead + AsyncWrite + Unpin,
|
||||
{
|
||||
let psk = generate_psk(remote_pub_key, version)?;
|
||||
|
||||
let timeout = config.timeout;
|
||||
tokio::time::timeout(
|
||||
timeout,
|
||||
self.perform_initiator_handshake_inner(
|
||||
config.pattern,
|
||||
config.local_key.private_key(),
|
||||
remote_pub_key,
|
||||
psk,
|
||||
version,
|
||||
),
|
||||
)
|
||||
.await?
|
||||
}
|
||||
|
||||
async fn perform_responder_handshake_inner(
|
||||
mut self,
|
||||
noise_pattern: NoisePattern,
|
||||
local_private_key: impl AsRef<[u8]>,
|
||||
local_pub_key: x25519::PublicKey,
|
||||
) -> Result<NoiseStream<C>, NoiseError>
|
||||
where
|
||||
C: AsyncRead + AsyncWrite + Unpin,
|
||||
{
|
||||
// 1. we read the first message from the initiator to establish noise version and pattern
|
||||
// and determine if we can continue with the handshake
|
||||
let initial_frame = self
|
||||
.inner_stream
|
||||
.next()
|
||||
.await
|
||||
.ok_or(NoiseError::IoError(io::ErrorKind::BrokenPipe.into()))??;
|
||||
|
||||
if !initial_frame.is_handshake_message() {
|
||||
return Err(NoiseError::NonHandshakeMessageReceived);
|
||||
}
|
||||
|
||||
let pattern = initial_frame.noise_pattern();
|
||||
|
||||
// I can imagine we should be able to handle multiple patterns here, but I guess there's a reason a value is set in the config
|
||||
// but refactoring this shouldn't be too difficult
|
||||
if pattern != noise_pattern {
|
||||
return Err(NoiseError::UnexpectedNoisePattern {
|
||||
configured: noise_pattern.as_str(),
|
||||
received: pattern.as_str(),
|
||||
});
|
||||
}
|
||||
|
||||
// 2. generate psk and handshake state
|
||||
let psk = generate_psk(local_pub_key, initial_frame.header.version)?;
|
||||
|
||||
let mut handshake = Builder::new(pattern.as_noise_params())
|
||||
.local_private_key(local_private_key.as_ref())
|
||||
.psk(pattern.psk_position(), &psk)
|
||||
.build_responder()?;
|
||||
|
||||
// update handshake state with initial frame
|
||||
let mut buf = BytesMut::zeroed(HANDSHAKE_MAX_LEN);
|
||||
handshake.read_message(&initial_frame.data, &mut buf)?;
|
||||
|
||||
// 3. run handshake to completion
|
||||
self.perform_handshake(handshake, initial_frame.version(), pattern)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn perform_responder_handshake(
|
||||
self,
|
||||
config: &NoiseConfig,
|
||||
) -> Result<NoiseStream<C>, NoiseError>
|
||||
where
|
||||
C: AsyncRead + AsyncWrite + Unpin,
|
||||
{
|
||||
let timeout = config.timeout;
|
||||
tokio::time::timeout(
|
||||
timeout,
|
||||
self.perform_responder_handshake_inner(
|
||||
config.pattern,
|
||||
config.local_key.private_key(),
|
||||
*config.local_key.public_key(),
|
||||
),
|
||||
)
|
||||
.await?
|
||||
}
|
||||
|
||||
async fn send_handshake_msg(
|
||||
&mut self,
|
||||
handshake: &mut HandshakeState,
|
||||
version: NoiseVersion,
|
||||
pattern: NoisePattern,
|
||||
) -> Result<(), NoiseError>
|
||||
where
|
||||
C: AsyncRead + AsyncWrite + Unpin,
|
||||
{
|
||||
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);
|
||||
|
||||
let frame = NymNoiseFrame::new_handshake_frame(buf.freeze(), version, pattern)?;
|
||||
self.inner_stream.send(frame).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn recv_handshake_msg(
|
||||
&mut self,
|
||||
handshake: &mut HandshakeState,
|
||||
version: NoiseVersion,
|
||||
pattern: NoisePattern,
|
||||
) -> Result<(), NoiseError>
|
||||
where
|
||||
C: AsyncRead + AsyncWrite + Unpin,
|
||||
{
|
||||
match self.inner_stream.next().await {
|
||||
Some(Ok(frame)) => {
|
||||
// validate the frame
|
||||
if !frame.is_handshake_message() {
|
||||
return Err(NoiseError::NonHandshakeMessageReceived);
|
||||
}
|
||||
if frame.version() != version {
|
||||
return Err(NoiseError::UnexpectedHandshakeVersion {
|
||||
initial: version,
|
||||
received: frame.version(),
|
||||
});
|
||||
}
|
||||
if frame.noise_pattern() != pattern {
|
||||
return Err(NoiseError::UnexpectedNoisePattern {
|
||||
configured: pattern.as_str(),
|
||||
received: frame.noise_pattern().as_str(),
|
||||
});
|
||||
}
|
||||
|
||||
let mut buf = BytesMut::zeroed(HANDSHAKE_MAX_LEN); // we're in the handshake, we can afford a smaller buffer
|
||||
handshake.read_message(&frame.data, &mut buf)?;
|
||||
Ok(())
|
||||
}
|
||||
Some(Err(err)) => Err(err),
|
||||
None => Err(NoiseError::HandshakeError),
|
||||
}
|
||||
}
|
||||
|
||||
async fn perform_handshake(
|
||||
mut self,
|
||||
mut handshake_state: HandshakeState,
|
||||
version: NoiseVersion,
|
||||
pattern: NoisePattern,
|
||||
) -> Result<NoiseStream<C>, NoiseError>
|
||||
where
|
||||
C: AsyncRead + AsyncWrite + Unpin,
|
||||
{
|
||||
while !handshake_state.is_handshake_finished() {
|
||||
if handshake_state.is_my_turn() {
|
||||
self.send_handshake_msg(&mut handshake_state, version, pattern)
|
||||
.await?;
|
||||
} else {
|
||||
self.recv_handshake_msg(&mut handshake_state, version, pattern)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
let transport = handshake_state.into_transport_mode()?;
|
||||
Ok(NoiseStream {
|
||||
inner_stream: self.inner_stream,
|
||||
negotiated_pattern: pattern,
|
||||
negotiated_version: version,
|
||||
transport,
|
||||
dec_buffer: Default::default(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Wrapper around a TcpStream
|
||||
pub struct NoiseStream<C> {
|
||||
inner_stream: Framed<C, NymNoiseCodec>,
|
||||
|
||||
negotiated_pattern: NoisePattern,
|
||||
negotiated_version: NoiseVersion,
|
||||
|
||||
transport: TransportState,
|
||||
dec_buffer: BytesMut,
|
||||
}
|
||||
|
||||
impl<C> NoiseStream<C> {
|
||||
fn validate_data_frame(&self, frame: NymNoiseFrame) -> Result<Bytes, NoiseError> {
|
||||
if !frame.is_data_message() {
|
||||
return Err(NoiseError::NonDataMessageReceived);
|
||||
}
|
||||
// validate the frame
|
||||
if !frame.is_data_message() {
|
||||
return Err(NoiseError::NonDataMessageReceived);
|
||||
}
|
||||
if frame.version() != self.negotiated_version {
|
||||
return Err(NoiseError::UnexpectedDataVersion {
|
||||
initial: self.negotiated_version,
|
||||
received: frame.version(),
|
||||
});
|
||||
}
|
||||
if frame.noise_pattern() != self.negotiated_pattern {
|
||||
return Err(NoiseError::UnexpectedNoisePattern {
|
||||
configured: self.negotiated_pattern.as_str(),
|
||||
received: frame.noise_pattern().as_str(),
|
||||
});
|
||||
};
|
||||
|
||||
Ok(frame.data)
|
||||
}
|
||||
|
||||
fn poll_data_frame(
|
||||
&mut self,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
) -> Poll<Option<io::Result<Bytes>>>
|
||||
where
|
||||
C: AsyncRead + AsyncWrite + Unpin,
|
||||
{
|
||||
match ready!(Pin::new(&mut self.inner_stream).poll_next(cx)) {
|
||||
None => Poll::Ready(None),
|
||||
Some(Err(err)) => Poll::Ready(Some(Err(err.naive_to_io_error()))),
|
||||
Some(Ok(frame)) => match self.validate_data_frame(frame) {
|
||||
Err(err) => Poll::Ready(Some(Err(err.naive_to_io_error()))),
|
||||
Ok(data) => Poll::Ready(Some(Ok(data))),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<C> AsyncRead for NoiseStream<C>
|
||||
where
|
||||
C: AsyncRead + AsyncWrite + Unpin,
|
||||
{
|
||||
fn poll_read(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
buf: &mut ReadBuf<'_>,
|
||||
) -> Poll<std::io::Result<()>> {
|
||||
let pending = match self.poll_data_frame(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 self.transport.read_message(&noise_msg, &mut dec_msg) {
|
||||
Ok(len) => len,
|
||||
Err(_) => return Poll::Ready(Err(io::ErrorKind::InvalidInput.into())),
|
||||
};
|
||||
|
||||
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 afterward
|
||||
false
|
||||
}
|
||||
};
|
||||
|
||||
// Checking if there is something to return from the buffer
|
||||
let read_len = min(buf.remaining(), self.dec_buffer.len());
|
||||
if read_len > 0 {
|
||||
buf.put_slice(&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<C> AsyncWrite for NoiseStream<C>
|
||||
where
|
||||
C: AsyncWrite + Unpin,
|
||||
{
|
||||
fn poll_write(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
buf: &[u8],
|
||||
) -> Poll<Result<usize, std::io::Error>> {
|
||||
// returns on Poll::Pending and Poll:Ready(Err)
|
||||
ready!(Pin::new(&mut self.inner_stream).poll_ready(cx))
|
||||
.map_err(|err| err.naive_to_io_error())?;
|
||||
|
||||
// we can send at most u16::MAX bytes in a frame, but we also have to include the tag when encoding
|
||||
let msg_len = min(u16::MAX as usize - TAGLEN, buf.len());
|
||||
|
||||
// Ready to send, encrypting message
|
||||
let mut noise_buf = BytesMut::zeroed(msg_len + TAGLEN);
|
||||
|
||||
let Ok(len) = self
|
||||
.transport
|
||||
.write_message(&buf[..msg_len], &mut noise_buf)
|
||||
else {
|
||||
return Poll::Ready(Err(io::ErrorKind::InvalidInput.into()));
|
||||
};
|
||||
noise_buf.truncate(len);
|
||||
|
||||
let frame = NymNoiseFrame::new_data_frame(
|
||||
noise_buf.freeze(),
|
||||
self.negotiated_version,
|
||||
self.negotiated_pattern,
|
||||
)
|
||||
.map_err(|err| err.naive_to_io_error())?;
|
||||
|
||||
// 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 Pin::new(&mut self.inner_stream).start_send(frame) {
|
||||
Ok(()) => Poll::Ready(Ok(msg_len)),
|
||||
Err(e) => Poll::Ready(Err(e.naive_to_io_error())),
|
||||
}
|
||||
}
|
||||
|
||||
fn poll_flush(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
) -> Poll<Result<(), std::io::Error>> {
|
||||
Pin::new(&mut self.inner_stream)
|
||||
.poll_flush(cx)
|
||||
.map_err(|err| err.naive_to_io_error())
|
||||
}
|
||||
|
||||
fn poll_shutdown(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
) -> Poll<Result<(), std::io::Error>> {
|
||||
Pin::new(&mut self.inner_stream)
|
||||
.poll_close(cx)
|
||||
.map_err(|err| err.naive_to_io_error())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use nym_crypto::asymmetric::x25519;
|
||||
use rand_chacha::rand_core::SeedableRng;
|
||||
use std::io::Error;
|
||||
use std::mem;
|
||||
use std::sync::Arc;
|
||||
use std::task::{Context, Waker};
|
||||
use std::time::Duration;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::join;
|
||||
use tokio::sync::Mutex;
|
||||
use tokio::time::timeout;
|
||||
|
||||
fn mock_streams() -> (MockStream, MockStream) {
|
||||
let ch1 = Arc::new(Mutex::new(Default::default()));
|
||||
let ch2 = Arc::new(Mutex::new(Default::default()));
|
||||
|
||||
(
|
||||
MockStream {
|
||||
inner: MockStreamInner {
|
||||
tx: ch1.clone(),
|
||||
rx: ch2.clone(),
|
||||
},
|
||||
},
|
||||
MockStream {
|
||||
inner: MockStreamInner { tx: ch2, rx: ch1 },
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
struct MockStream {
|
||||
inner: MockStreamInner,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl MockStream {
|
||||
fn unchecked_tx_data(&self) -> Vec<u8> {
|
||||
self.inner.tx.try_lock().unwrap().data.clone()
|
||||
}
|
||||
|
||||
fn unchecked_rx_data(&self) -> Vec<u8> {
|
||||
self.inner.rx.try_lock().unwrap().data.clone()
|
||||
}
|
||||
}
|
||||
|
||||
struct MockStreamInner {
|
||||
tx: Arc<Mutex<DataWrapper>>,
|
||||
rx: Arc<Mutex<DataWrapper>>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct DataWrapper {
|
||||
data: Vec<u8>,
|
||||
waker: Option<Waker>,
|
||||
}
|
||||
|
||||
impl AsyncRead for MockStream {
|
||||
fn poll_read(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &mut ReadBuf<'_>,
|
||||
) -> Poll<io::Result<()>> {
|
||||
let mut inner = self.inner.rx.try_lock().unwrap();
|
||||
let data = mem::take(&mut inner.data);
|
||||
if data.is_empty() {
|
||||
inner.waker = Some(cx.waker().clone());
|
||||
return Poll::Pending;
|
||||
}
|
||||
|
||||
if let Some(waker) = inner.waker.take() {
|
||||
waker.wake();
|
||||
}
|
||||
|
||||
buf.put_slice(&data);
|
||||
Poll::Ready(Ok(()))
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncWrite for MockStream {
|
||||
fn poll_write(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &[u8],
|
||||
) -> Poll<Result<usize, Error>> {
|
||||
let mut inner = self.inner.tx.try_lock().unwrap();
|
||||
let len = buf.len();
|
||||
|
||||
if !inner.data.is_empty() {
|
||||
assert!(inner.waker.is_none());
|
||||
inner.waker = Some(cx.waker().clone());
|
||||
return Poll::Pending;
|
||||
}
|
||||
|
||||
inner.data.extend_from_slice(buf);
|
||||
if let Some(waker) = inner.waker.take() {
|
||||
waker.wake();
|
||||
}
|
||||
Poll::Ready(Ok(len))
|
||||
}
|
||||
|
||||
fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Result<(), Error>> {
|
||||
Poll::Ready(Ok(()))
|
||||
}
|
||||
|
||||
fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Result<(), Error>> {
|
||||
Poll::Ready(Ok(()))
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn noise_handshake() -> anyhow::Result<()> {
|
||||
let dummy_seed = [42u8; 32];
|
||||
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
|
||||
|
||||
let initiator_keys = Arc::new(x25519::KeyPair::new(&mut rng));
|
||||
let responder_keys = Arc::new(x25519::KeyPair::new(&mut rng));
|
||||
|
||||
let (initiator_stream, responder_stream) = mock_streams();
|
||||
|
||||
let psk = generate_psk(*responder_keys.public_key(), NoiseVersion::V1)?;
|
||||
let pattern = NoisePattern::default();
|
||||
|
||||
let stream_initiator = NoiseStreamBuilder::new(initiator_stream)
|
||||
.perform_initiator_handshake_inner(
|
||||
pattern,
|
||||
initiator_keys.private_key().to_bytes(),
|
||||
responder_keys.public_key().to_bytes(),
|
||||
psk,
|
||||
NoiseVersion::V1,
|
||||
);
|
||||
|
||||
let stream_responder = NoiseStreamBuilder::new(responder_stream)
|
||||
.perform_responder_handshake_inner(
|
||||
pattern,
|
||||
responder_keys.private_key().to_bytes(),
|
||||
*responder_keys.public_key(),
|
||||
);
|
||||
|
||||
let initiator_fut =
|
||||
tokio::spawn(
|
||||
async move { timeout(Duration::from_millis(200), stream_initiator).await },
|
||||
);
|
||||
let responder_fut =
|
||||
tokio::spawn(
|
||||
async move { timeout(Duration::from_millis(200), stream_responder).await },
|
||||
);
|
||||
|
||||
let (initiator, responder) = join!(initiator_fut, responder_fut);
|
||||
|
||||
let mut initiator = initiator???;
|
||||
let mut responder = responder???;
|
||||
|
||||
let msg = b"hello there";
|
||||
// if noise was successful we should be able to write a proper message across
|
||||
timeout(Duration::from_millis(200), initiator.write_all(msg)).await??;
|
||||
|
||||
initiator.inner_stream.flush().await?;
|
||||
|
||||
let inner_buf = initiator.inner_stream.get_ref().unchecked_tx_data();
|
||||
|
||||
let mut buf = [0u8; 11];
|
||||
timeout(Duration::from_millis(200), responder.read(&mut buf)).await??;
|
||||
|
||||
assert_eq!(&buf[..], msg);
|
||||
|
||||
// the inner content is different from the actual msg since it was encrypted
|
||||
assert_ne!(inner_buf, buf);
|
||||
assert_ne!(inner_buf.len(), msg.len());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
Generated
+16
-2
@@ -266,6 +266,20 @@ dependencies = [
|
||||
"thiserror 1.0.64",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cargo_metadata"
|
||||
version = "0.19.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba"
|
||||
dependencies = [
|
||||
"camino",
|
||||
"cargo-platform",
|
||||
"semver",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.12",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.0"
|
||||
@@ -1249,7 +1263,7 @@ dependencies = [
|
||||
name = "nym-network-defaults"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"cargo_metadata",
|
||||
"cargo_metadata 0.19.2",
|
||||
"regex",
|
||||
]
|
||||
|
||||
@@ -2019,7 +2033,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e27d6bdd219887a9eadd19e1c34f32e47fa332301184935c6d9bca26f3cca525"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"cargo_metadata",
|
||||
"cargo_metadata 0.18.1",
|
||||
"cfg-if",
|
||||
"regex",
|
||||
"rustc_version",
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
#!/bin/sh
|
||||
sqlite3 -init settings.sql /Users/jedrzej/workspace/nym/target/debug/build/nym-api-84610644ac15a598/out/nym-api-example.sqlite
|
||||
@@ -38,6 +38,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;
|
||||
@@ -471,6 +470,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)]
|
||||
@@ -536,6 +546,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)]
|
||||
@@ -878,10 +899,7 @@ pub struct HostKeys {
|
||||
pub pre_announced_x25519_sphinx_key: Option<SphinxKey>,
|
||||
|
||||
#[serde(default)]
|
||||
#[serde(with = "option_bs58_x25519_pubkey")]
|
||||
#[schemars(with = "Option<String>")]
|
||||
#[schema(value_type = String)]
|
||||
pub x25519_noise: Option<x25519::PublicKey>,
|
||||
pub x25519_versioned_noise: Option<VersionedNoiseKey>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
|
||||
@@ -910,7 +928,7 @@ impl From<nym_node_requests::api::v1::node::models::HostKeys> for HostKeys {
|
||||
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_noise: value.x25519_noise,
|
||||
x25519_versioned_noise: value.x25519_versioned_noise,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1087,6 +1105,24 @@ impl NymNodeDescription {
|
||||
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)]
|
||||
@@ -1372,10 +1408,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::{EpochId, Interval, NodeId};
|
||||
use nym_noise_keys::VersionedNoiseKey;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::net::IpAddr;
|
||||
@@ -27,6 +28,18 @@ impl SkimmedNodesWithMetadata {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, utoipa::ToSchema)]
|
||||
pub struct SemiSkimmedNodesWithMetadata {
|
||||
pub nodes: Vec<SemiSkimmedNode>,
|
||||
pub metadata: NodesResponseMetadata,
|
||||
}
|
||||
|
||||
impl SemiSkimmedNodesWithMetadata {
|
||||
pub fn new(nodes: Vec<SemiSkimmedNode>, metadata: NodesResponseMetadata) -> Self {
|
||||
SemiSkimmedNodesWithMetadata { nodes, metadata }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Clone, Copy, Debug, Serialize, Deserialize, schemars::JsonSchema, utoipa::ToSchema, PartialEq,
|
||||
)]
|
||||
@@ -260,7 +273,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:
|
||||
}
|
||||
|
||||
|
||||
@@ -160,11 +160,11 @@ async fn nodes_noise(
|
||||
n.description
|
||||
.host_information
|
||||
.keys
|
||||
.x25519_noise
|
||||
.x25519_versioned_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(
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::support::http::state::AppState;
|
||||
use crate::unstable_routes::v2::nym_nodes::semi_skimmed::nodes_expanded;
|
||||
use crate::unstable_routes::v2::nym_nodes::skimmed::{
|
||||
entry_gateways_basic_all, exit_gateways_basic_all, mixnodes_basic_active, mixnodes_basic_all,
|
||||
nodes_basic_all,
|
||||
@@ -11,6 +12,7 @@ use axum::Router;
|
||||
use tower_http::compression::CompressionLayer;
|
||||
|
||||
pub(crate) mod helpers;
|
||||
pub(crate) mod semi_skimmed;
|
||||
pub(crate) mod skimmed;
|
||||
|
||||
#[allow(deprecated)]
|
||||
@@ -29,5 +31,9 @@ pub(crate) fn routes() -> Router<AppState> {
|
||||
.route("/entry-gateways", get(entry_gateways_basic_all))
|
||||
.route("/exit-gateways", get(exit_gateways_basic_all)),
|
||||
)
|
||||
.nest(
|
||||
"/semi-skimmed",
|
||||
Router::new().route("/", get(nodes_expanded)),
|
||||
)
|
||||
.layer(CompressionLayer::new())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::node_describe_cache::cache::DescribedNodes;
|
||||
use crate::node_status_api::models::AxumResult;
|
||||
use crate::support::http::state::AppState;
|
||||
use crate::unstable_routes::helpers::{refreshed_at, LegacyAnnotation};
|
||||
use crate::unstable_routes::v2::nym_nodes::helpers::NodesParamsWithRole;
|
||||
use axum::extract::{Query, State};
|
||||
use nym_api_requests::models::{
|
||||
NodeAnnotation, NymNodeDescription, OffsetDateTimeJsonSchemaWrapper,
|
||||
};
|
||||
use nym_api_requests::nym_nodes::{NodeRole, PaginatedCachedNodesResponseV2, 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<PaginatedCachedNodesResponseV2<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>,
|
||||
current_key_rotation: u32,
|
||||
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(
|
||||
current_key_rotation,
|
||||
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>,
|
||||
current_key_rotation: u32,
|
||||
) 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(
|
||||
current_key_rotation,
|
||||
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,
|
||||
params(NodesParamsWithRole),
|
||||
path = "",
|
||||
context_path = "/v2/unstable/nym-nodes/semi-skimmed",
|
||||
responses(
|
||||
(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>,
|
||||
) -> 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 contract_cache = state.nym_contract_cache();
|
||||
let current_key_rotation = contract_cache.current_key_rotation_id().await?;
|
||||
let interval = contract_cache.current_interval().await?;
|
||||
|
||||
let mut nodes = build_nym_nodes_response(
|
||||
&rewarded_set,
|
||||
all_nym_nodes,
|
||||
&annotations,
|
||||
current_key_rotation,
|
||||
false,
|
||||
);
|
||||
|
||||
// add legacy gateways to the response
|
||||
add_legacy(
|
||||
&mut nodes,
|
||||
&rewarded_set,
|
||||
&describe_cache,
|
||||
&legacy_gateways,
|
||||
current_key_rotation,
|
||||
);
|
||||
|
||||
// add legacy mixnodes to the response
|
||||
add_legacy(
|
||||
&mut nodes,
|
||||
&rewarded_set,
|
||||
&describe_cache,
|
||||
&legacy_mixnodes,
|
||||
current_key_rotation,
|
||||
);
|
||||
|
||||
// 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(PaginatedCachedNodesResponseV2::new_full(
|
||||
interval.current_epoch_absolute_id(),
|
||||
current_key_rotation,
|
||||
refreshed_at,
|
||||
nodes,
|
||||
)))
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
set -eu
|
||||
export ENVIRONMENT=${ENVIRONMENT:-"mainnet"}
|
||||
|
||||
probe_git_ref="nym-vpn-core-v1.4.0"
|
||||
probe_git_ref="nym-vpn-core-v1.10.0"
|
||||
|
||||
crate_root=$(dirname $(realpath "$0"))
|
||||
monorepo_root=$(realpath "${crate_root}/../..")
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
[package]
|
||||
name = "nym-node-status-api"
|
||||
version = "2.3.3"
|
||||
version = "2.4.1-rc.1"
|
||||
authors.workspace = true
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
@@ -23,23 +23,39 @@ envy = { workspace = true }
|
||||
futures-util = { workspace = true }
|
||||
itertools = { workspace = true }
|
||||
moka = { workspace = true, features = ["future"] }
|
||||
nym-contracts-common = { path = "../../common/cosmwasm-smart-contracts/contracts-common" }
|
||||
nym-mixnet-contract-common = { path = "../../common/cosmwasm-smart-contracts/mixnet-contract", features = ["utoipa"] }
|
||||
nym-bin-common = { path = "../../common/bin-common", features = ["models"] }
|
||||
nym-node-status-client = { path = "../nym-node-status-client" }
|
||||
nym-crypto = { path = "../../common/crypto", features = ["asymmetric", "serde"] }
|
||||
nym-http-api-client = { path = "../../common/http-api-client" }
|
||||
nym-http-api-common = { path = "../../common/http-api-common", features = ["middleware"]}
|
||||
nym-network-defaults = { path = "../../common/network-defaults" }
|
||||
nym-serde-helpers = { path = "../../common/serde-helpers" }
|
||||
nym-statistics-common = { path = "../../common/statistics" }
|
||||
nym-validator-client = { path = "../../common/client-libs/validator-client" }
|
||||
nym-task = { path = "../../common/task" }
|
||||
nym-node-requests = { path = "../../nym-node/nym-node-requests", features = ["openapi"] }
|
||||
# TODO dz had to switch to cheddar versions because develop is ahead of current
|
||||
# Nym API: revert after Cheddar is out
|
||||
nym-contracts-common = { git = "https://github.com/nymtech/nym.git", branch = "release/2025.11-cheddar" }
|
||||
nym-mixnet-contract-common = { git = "https://github.com/nymtech/nym.git", branch = "release/2025.11-cheddar" }
|
||||
nym-bin-common = { git = "https://github.com/nymtech/nym.git", branch = "release/2025.11-cheddar" }
|
||||
nym-node-status-client = { git = "https://github.com/nymtech/nym.git", branch = "release/2025.11-cheddar" }
|
||||
nym-crypto = { git = "https://github.com/nymtech/nym.git", branch = "release/2025.11-cheddar" }
|
||||
nym-http-api-client = { git = "https://github.com/nymtech/nym.git", branch = "release/2025.11-cheddar" }
|
||||
nym-http-api-common = { git = "https://github.com/nymtech/nym.git", branch = "release/2025.11-cheddar", features = ["middleware"]}
|
||||
nym-network-defaults = { git = "https://github.com/nymtech/nym.git", branch = "release/2025.11-cheddar" }
|
||||
nym-serde-helpers = { git = "https://github.com/nymtech/nym.git", branch = "release/2025.11-cheddar" }
|
||||
nym-statistics-common = { git = "https://github.com/nymtech/nym.git", branch = "release/2025.11-cheddar" }
|
||||
# nym-contracts-common = { path = "../../common/cosmwasm-smart-contracts/contracts-common" }
|
||||
# nym-mixnet-contract-common = { path = "../../common/cosmwasm-smart-contracts/mixnet-contract", features = ["utoipa"] }
|
||||
# nym-bin-common = { path = "../../common/bin-common", features = ["models"] }
|
||||
# nym-node-status-client = { path = "../nym-node-status-client" }
|
||||
# nym-crypto = { path = "../../common/crypto", features = ["asymmetric", "serde"] }
|
||||
# nym-http-api-client = { path = "../../common/http-api-client" }
|
||||
# nym-http-api-common = { path = "../../common/http-api-common", features = ["middleware"]}
|
||||
# nym-network-defaults = { path = "../../common/network-defaults" }
|
||||
# nym-serde-helpers = { path = "../../common/serde-helpers" }
|
||||
# nym-statistics-common = { path = "../../common/statistics" }
|
||||
# nym-validator-client = { path = "../../common/client-libs/validator-client" }
|
||||
nym-validator-client = { git = "https://github.com/nymtech/nym.git", branch = "release/2025.11-cheddar" }
|
||||
# nym-task = { path = "../../common/task" }
|
||||
# nym-node-requests = { path = "../../nym-node/nym-node-requests", features = ["openapi"] }
|
||||
nym-task = { git = "https://github.com/nymtech/nym.git", branch = "release/2025.11-cheddar" }
|
||||
nym-node-requests = { git = "https://github.com/nymtech/nym.git", branch = "release/2025.11-cheddar" }
|
||||
rand = { workspace = true }
|
||||
rand_chacha = { workspace = true }
|
||||
regex = { workspace = true }
|
||||
reqwest = { workspace = true }
|
||||
semver = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
serde_json_path = { workspace = true }
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::{
|
||||
http::{self, models::SummaryHistory},
|
||||
utils::{decimal_to_i64, unix_timestamp_to_utc_rfc3339, NumericalCheckedCast},
|
||||
@@ -14,6 +12,7 @@ use nym_validator_client::{
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::FromRow;
|
||||
use std::str::FromStr;
|
||||
use strum_macros::{EnumString, FromRepr};
|
||||
use time::{Date, OffsetDateTime};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
@@ -111,7 +111,8 @@ pub(crate) async fn get_all_gateways(pool: &DbPool) -> anyhow::Result<Vec<Gatewa
|
||||
error!("Conversion from DTO failed: {e}. Invalidly stored data?");
|
||||
e
|
||||
})?;
|
||||
tracing::trace!("Fetched {} gateways from DB", items.len());
|
||||
// TODO dz revert to trace
|
||||
tracing::info!("Fetched {} gateways from DB", items.len());
|
||||
Ok(items)
|
||||
}
|
||||
|
||||
|
||||
@@ -36,6 +36,8 @@ pub(crate) async fn get_all_nym_nodes(pool: &DbPool) -> anyhow::Result<Vec<NymNo
|
||||
bond_info as "bond_info: serde_json::Value"
|
||||
FROM
|
||||
nym_nodes
|
||||
ORDER BY
|
||||
node_id
|
||||
"#,
|
||||
)
|
||||
.fetch(&mut *conn)
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
use axum::{
|
||||
extract::{Path, Query, State},
|
||||
Json, Router,
|
||||
};
|
||||
use semver::Version;
|
||||
use serde::Deserialize;
|
||||
use std::sync::LazyLock;
|
||||
use tracing::instrument;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::http::models::DVpnGateway;
|
||||
use crate::http::{
|
||||
error::{HttpError, HttpResult},
|
||||
state::AppState,
|
||||
};
|
||||
|
||||
static MIN_SUPPORTED_VERSION: LazyLock<Version> = LazyLock::new(|| Version::new(1, 6, 2));
|
||||
|
||||
pub(crate) fn routes() -> Router<AppState> {
|
||||
Router::new()
|
||||
.route("/", axum::routing::get(dvpn_gateways))
|
||||
.route(
|
||||
"/country/:two_letter_country_code",
|
||||
axum::routing::get(gateways_by_country),
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Deserialize, IntoParams)]
|
||||
#[into_params(parameter_in = Query)]
|
||||
struct MinNodeVersionQuery {
|
||||
min_node_version: Option<String>,
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
tag = "dVPN Directory Cache",
|
||||
get,
|
||||
params(
|
||||
MinNodeVersionQuery
|
||||
),
|
||||
path = "/directory/gateways",
|
||||
summary = "Gets available entry and exit gateways from the Nym network directory",
|
||||
context_path = "/dvpn/v1",
|
||||
responses(
|
||||
(status = 200, body = Vec<DVpnGateway>)
|
||||
)
|
||||
)]
|
||||
#[instrument(level = tracing::Level::INFO, skip(state))]
|
||||
async fn dvpn_gateways(
|
||||
Query(MinNodeVersionQuery { min_node_version }): Query<MinNodeVersionQuery>,
|
||||
state: State<AppState>,
|
||||
) -> HttpResult<Json<Vec<DVpnGateway>>> {
|
||||
let min_node_version = match min_node_version {
|
||||
Some(min_version) => semver::Version::parse(&min_version)
|
||||
.map_err(|_| HttpError::invalid_input("Min version must be valid semver"))?,
|
||||
None => MIN_SUPPORTED_VERSION.clone(),
|
||||
};
|
||||
|
||||
Ok(Json(
|
||||
state
|
||||
.cache()
|
||||
.get_dvpn_gateway_list(state.db_pool(), &min_node_version)
|
||||
.await,
|
||||
))
|
||||
}
|
||||
|
||||
#[allow(dead_code)] // clippy doesn't detect usage in utoipa macros
|
||||
#[derive(Deserialize, IntoParams)]
|
||||
#[into_params(parameter_in = Path)]
|
||||
struct TwoLetterCountryCodeParam {
|
||||
#[param(min_length = 2, max_length = 2)]
|
||||
two_letter_country_code: String,
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
tag = "dVPN Directory Cache",
|
||||
get,
|
||||
params(
|
||||
TwoLetterCountryCodeParam
|
||||
),
|
||||
path = "/directory/gateways/country/{two_letter_country_code}",
|
||||
summary = "Gets available gateways from the Nym network directory by country",
|
||||
context_path = "/dvpn/v1",
|
||||
responses(
|
||||
(status = 200, body = Vec<DVpnGateway>)
|
||||
)
|
||||
)]
|
||||
#[instrument(level = tracing::Level::INFO, skip(state), fields(two_letter_country_code = two_letter_country_code))]
|
||||
async fn gateways_by_country(
|
||||
Path(TwoLetterCountryCodeParam {
|
||||
two_letter_country_code,
|
||||
}): Path<TwoLetterCountryCodeParam>,
|
||||
state: State<AppState>,
|
||||
) -> HttpResult<Json<Vec<DVpnGateway>>> {
|
||||
let country_filter = two_letter_country_code.to_lowercase();
|
||||
match two_letter_country_code.len() {
|
||||
2 => Ok(Json(
|
||||
state
|
||||
.cache()
|
||||
.get_dvpn_gateway_list(state.db_pool(), &MIN_SUPPORTED_VERSION)
|
||||
.await
|
||||
.into_iter()
|
||||
.filter(|gw| {
|
||||
gw.location.two_letter_iso_country_code.to_lowercase() == country_filter
|
||||
})
|
||||
.collect(),
|
||||
)),
|
||||
_ => Err(HttpError::invalid_input(
|
||||
"Only two letter country code is allowed",
|
||||
)),
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ use utoipa_swagger_ui::SwaggerUi;
|
||||
|
||||
use crate::http::{server::HttpServer, state::AppState};
|
||||
|
||||
pub(crate) mod dvpn;
|
||||
pub(crate) mod gateways;
|
||||
pub(crate) mod metrics;
|
||||
pub(crate) mod mixnodes;
|
||||
@@ -44,6 +45,10 @@ impl RouterBuilder {
|
||||
"/explorer/v3",
|
||||
Router::new().nest("/nym-nodes", nym_nodes::routes()),
|
||||
)
|
||||
.nest(
|
||||
"/dvpn/v1",
|
||||
Router::new().nest("/directory/gateways", dvpn::routes()),
|
||||
)
|
||||
.nest(
|
||||
"/internal",
|
||||
Router::new().nest("/testruns", testruns::routes()),
|
||||
|
||||
@@ -1,12 +1,22 @@
|
||||
use cosmwasm_std::{Addr, Coin, Decimal};
|
||||
use nym_mixnet_contract_common::CoinSchema;
|
||||
use nym_node_requests::api::v1::node::models::NodeDescription;
|
||||
use nym_validator_client::client::NodeId;
|
||||
use nym_validator_client::{
|
||||
client::NodeId,
|
||||
models::{
|
||||
AuthenticatorDetails, BinaryBuildInformationOwned, IpPacketRouterDetails, NymNodeData,
|
||||
},
|
||||
nym_api::SkimmedNode,
|
||||
nym_nodes::{BasicEntryInformation, NodeRole},
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing::{error, instrument};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
pub(crate) use nym_node_status_client::models::TestrunAssignment;
|
||||
|
||||
use crate::monitor::ExplorerPrettyBond;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
pub struct Gateway {
|
||||
pub gateway_identity_key: String,
|
||||
@@ -23,6 +33,192 @@ pub struct Gateway {
|
||||
pub config_score: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)]
|
||||
pub struct BuildInformation {
|
||||
pub build_version: String,
|
||||
pub commit_branch: String,
|
||||
pub commit_sha: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)]
|
||||
pub struct Location {
|
||||
pub two_letter_iso_country_code: String,
|
||||
pub latitude: f64,
|
||||
pub longitude: f64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)]
|
||||
pub struct DVpnGateway {
|
||||
pub identity_key: String,
|
||||
pub name: String,
|
||||
pub ip_packet_router: Option<IpPacketRouterDetails>,
|
||||
pub authenticator: Option<AuthenticatorDetails>,
|
||||
pub location: Location,
|
||||
pub last_probe: Option<DirectoryGwProbe>,
|
||||
pub ip_addresses: Vec<String>,
|
||||
pub mix_port: u16,
|
||||
pub role: NodeRole,
|
||||
pub entry: Option<BasicEntryInformation>,
|
||||
// The performance data here originates from the nym-api, and is effectively mixnet performance
|
||||
// at the time of writing this
|
||||
pub performance: String,
|
||||
pub build_information: BinaryBuildInformationOwned,
|
||||
}
|
||||
|
||||
/// based on
|
||||
/// https://github.com/nymtech/nym-vpn-client/blob/nym-vpn-core-v1.10.0/nym-vpn-core/crates/nym-gateway-probe/src/types.rs
|
||||
/// TODO: long term types should be moved into this repo because nym-vpn-client
|
||||
/// could pull it as a dependency and we'd have a single source of truth
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)]
|
||||
pub struct LastProbeResult {
|
||||
node: String,
|
||||
used_entry: String,
|
||||
outcome: ProbeOutcome,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)]
|
||||
pub struct DirectoryGwProbe {
|
||||
last_updated_utc: String,
|
||||
outcome: ProbeOutcome,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)]
|
||||
pub struct ProbeOutcome {
|
||||
as_entry: directory_gw_probe_outcome::Entry,
|
||||
as_exit: Option<directory_gw_probe_outcome::Exit>,
|
||||
wg: Option<wg_outcome_versions::ProbeOutcomeV1>,
|
||||
}
|
||||
|
||||
pub mod directory_gw_probe_outcome {
|
||||
use super::*;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(untagged)]
|
||||
#[allow(clippy::enum_variant_names)]
|
||||
pub enum Entry {
|
||||
Tested(EntryTestResult),
|
||||
NotTested,
|
||||
EntryFailure,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
pub struct EntryTestResult {
|
||||
pub can_connect: bool,
|
||||
pub can_route: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)]
|
||||
pub struct Exit {
|
||||
can_connect: bool,
|
||||
can_route_ip_v4: bool,
|
||||
can_route_ip_external_v4: bool,
|
||||
can_route_ip_v6: bool,
|
||||
can_route_ip_external_v6: bool,
|
||||
}
|
||||
}
|
||||
|
||||
pub mod wg_outcome_versions {
|
||||
use super::*;
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)]
|
||||
pub struct ProbeOutcomeV1 {
|
||||
pub can_register: bool,
|
||||
|
||||
pub can_handshake_v4: bool,
|
||||
pub can_resolve_dns_v4: bool,
|
||||
pub ping_hosts_performance_v4: f32,
|
||||
pub ping_ips_performance_v4: f32,
|
||||
|
||||
pub can_handshake_v6: bool,
|
||||
pub can_resolve_dns_v6: bool,
|
||||
pub ping_hosts_performance_v6: f32,
|
||||
pub ping_ips_performance_v6: f32,
|
||||
|
||||
pub download_duration_sec_v4: u64,
|
||||
pub downloaded_file_v4: String,
|
||||
pub download_error_v4: String,
|
||||
|
||||
pub download_duration_sec_v6: u64,
|
||||
pub downloaded_file_v6: String,
|
||||
pub download_error_v6: String,
|
||||
}
|
||||
}
|
||||
|
||||
impl DVpnGateway {
|
||||
#[instrument(level = tracing::Level::INFO, name = "dvpn_gw_new", skip_all, fields(gateway_key = gateway.gateway_identity_key, node_id = skimmed_node.node_id))]
|
||||
pub(crate) fn new(gateway: Gateway, skimmed_node: &SkimmedNode) -> anyhow::Result<Self> {
|
||||
let location = gateway
|
||||
.explorer_pretty_bond
|
||||
.ok_or_else(|| anyhow::anyhow!("Missing explorer_pretty_bond"))
|
||||
.and_then(|value| {
|
||||
serde_json::from_value::<ExplorerPrettyBond>(value).map_err(From::from)
|
||||
})
|
||||
.map(|bond| bond.location)?;
|
||||
|
||||
let self_described = gateway
|
||||
.self_described
|
||||
.ok_or_else(|| anyhow::anyhow!("Missing self_described"))
|
||||
.and_then(|value| serde_json::from_value::<NymNodeData>(value).map_err(From::from))?;
|
||||
|
||||
let last_probe_result = match gateway.last_probe_result {
|
||||
Some(value) => {
|
||||
let parsed =
|
||||
serde_json::from_value::<LastProbeResult>(value).inspect_err(|err| {
|
||||
error!("Failed to deserialize probe result: {err}");
|
||||
})?;
|
||||
Some(parsed)
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
identity_key: gateway.gateway_identity_key,
|
||||
name: gateway.description.moniker,
|
||||
ip_packet_router: self_described.ip_packet_router,
|
||||
authenticator: self_described.authenticator,
|
||||
location: Location {
|
||||
latitude: location.location.latitude,
|
||||
longitude: location.location.longitude,
|
||||
two_letter_iso_country_code: location.two_letter_iso_country_code,
|
||||
},
|
||||
last_probe: last_probe_result.map(|res| DirectoryGwProbe {
|
||||
last_updated_utc: gateway.last_testrun_utc.unwrap_or_default(),
|
||||
outcome: res.outcome,
|
||||
}),
|
||||
ip_addresses: skimmed_node
|
||||
.ip_addresses
|
||||
.iter()
|
||||
.map(|ip| ip.to_string())
|
||||
.collect(),
|
||||
mix_port: skimmed_node.mix_port,
|
||||
role: skimmed_node.role.clone(),
|
||||
entry: skimmed_node.entry.clone(),
|
||||
performance: to_percent(gateway.performance),
|
||||
build_information: self_described.build_information,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn to_percent(performance: u8) -> String {
|
||||
let fraction = performance as f32 / 100.0;
|
||||
format!("{:.2}", fraction)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn to_percent_should_work() {
|
||||
let starting = [0u8, 33, 50, 99, 100];
|
||||
let expected = ["0.00", "0.33", "0.50", "0.99", "1.00"];
|
||||
|
||||
for (starting, expected) in starting.into_iter().zip(expected) {
|
||||
assert_eq!(expected, to_percent(starting));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)]
|
||||
pub struct GatewaySkinny {
|
||||
pub gateway_identity_key: String,
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
use cosmwasm_std::Decimal;
|
||||
use itertools::Itertools;
|
||||
use moka::{future::Cache, Entry};
|
||||
use nym_contracts_common::NaiveFloat;
|
||||
use nym_crypto::asymmetric::ed25519::PublicKey;
|
||||
use nym_mixnet_contract_common::NodeId;
|
||||
use nym_validator_client::nym_api::SkimmedNode;
|
||||
use semver::Version;
|
||||
use std::{collections::HashMap, sync::Arc, time::Duration};
|
||||
use tokio::sync::RwLock;
|
||||
use tracing::instrument;
|
||||
use tracing::{error, instrument, warn};
|
||||
|
||||
use crate::{
|
||||
db::{queries, DbPool},
|
||||
http::models::{DailyStats, ExtendedNymNode, Gateway, Mixnode, NodeGeoData, SummaryHistory},
|
||||
http::models::{
|
||||
DVpnGateway, DailyStats, ExtendedNymNode, Gateway, Mixnode, NodeGeoData, SummaryHistory,
|
||||
},
|
||||
monitor::{DelegationsCache, NodeGeoCache},
|
||||
};
|
||||
|
||||
@@ -88,6 +92,8 @@ const MIXNODE_STATS_HISTORY_DAYS: usize = 30;
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct HttpCache {
|
||||
gateways: Cache<String, Arc<RwLock<Vec<Gateway>>>>,
|
||||
// each min_version has their own cached list
|
||||
dvpn_gateways: Cache<Version, Arc<RwLock<Vec<DVpnGateway>>>>,
|
||||
mixnodes: Cache<String, Arc<RwLock<Vec<Mixnode>>>>,
|
||||
nym_nodes: Cache<String, Arc<RwLock<Vec<ExtendedNymNode>>>>,
|
||||
mixstats: Cache<String, Arc<RwLock<Vec<DailyStats>>>>,
|
||||
@@ -102,6 +108,10 @@ impl HttpCache {
|
||||
.max_capacity(2)
|
||||
.time_to_live(Duration::from_secs(ttl_seconds))
|
||||
.build(),
|
||||
dvpn_gateways: Cache::builder()
|
||||
.max_capacity(6)
|
||||
.time_to_live(Duration::from_secs(ttl_seconds))
|
||||
.build(),
|
||||
mixnodes: Cache::builder()
|
||||
.max_capacity(2)
|
||||
.time_to_live(Duration::from_secs(ttl_seconds))
|
||||
@@ -158,10 +168,11 @@ impl HttpCache {
|
||||
let gateways = crate::db::queries::get_all_gateways(db)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
self.upsert_gateway_list(gateways.clone()).await;
|
||||
|
||||
if gateways.is_empty() {
|
||||
tracing::warn!("Database contains 0 gateways");
|
||||
tracing::warn!("Database: gateway list is empty");
|
||||
} else {
|
||||
self.upsert_gateway_list(gateways.clone()).await;
|
||||
}
|
||||
|
||||
gateways
|
||||
@@ -169,6 +180,132 @@ impl HttpCache {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn upsert_dvpn_gateway_list(
|
||||
&self,
|
||||
new_gateway_list: Vec<DVpnGateway>,
|
||||
min_node_version: &Version,
|
||||
) -> Entry<Version, Arc<RwLock<Vec<DVpnGateway>>>> {
|
||||
self.dvpn_gateways
|
||||
.entry_by_ref(min_node_version)
|
||||
.and_upsert_with(|maybe_entry| async {
|
||||
if let Some(entry) = maybe_entry {
|
||||
let v = entry.into_value();
|
||||
let mut guard = v.write().await;
|
||||
*guard = new_gateway_list;
|
||||
v.clone()
|
||||
} else {
|
||||
Arc::new(RwLock::new(new_gateway_list))
|
||||
}
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_dvpn_gateway_list(
|
||||
&self,
|
||||
db: &DbPool,
|
||||
min_node_version: &Version,
|
||||
) -> Vec<DVpnGateway> {
|
||||
match self.dvpn_gateways.get(min_node_version).await {
|
||||
Some(guard) => {
|
||||
tracing::trace!("Fetching from cache...");
|
||||
let read_lock = guard.read().await;
|
||||
read_lock.clone()
|
||||
}
|
||||
None => {
|
||||
tracing::trace!("No gateways (dVPN) in cache, refreshing from DB...");
|
||||
|
||||
let gateways = self.get_gateway_list(db).await;
|
||||
|
||||
let started_with = gateways.len();
|
||||
let Ok(skimmed_nodes) = crate::db::queries::get_described_bonded_nym_nodes(db)
|
||||
.await
|
||||
.map(|records| {
|
||||
records
|
||||
.into_iter()
|
||||
.filter_map(|dto| SkimmedNode::try_from(dto).ok())
|
||||
.map(|skimmed_node| {
|
||||
(
|
||||
skimmed_node.ed25519_identity_pubkey.to_base58_string(),
|
||||
skimmed_node,
|
||||
)
|
||||
})
|
||||
.collect::<HashMap<_, _>>()
|
||||
})
|
||||
.inspect_err(|err| {
|
||||
// this would fail only in case of internal error: log and return gracefully
|
||||
error!("Failed to get nym_nodes from DB: {err}");
|
||||
})
|
||||
else {
|
||||
return Vec::new();
|
||||
};
|
||||
|
||||
let res_gws = gateways
|
||||
.into_iter()
|
||||
.filter(|gw| gw.bonded)
|
||||
.filter_map(|gw| match skimmed_nodes.get(&gw.gateway_identity_key) {
|
||||
Some(skimmed_node) => Some((gw, skimmed_node)),
|
||||
None => {
|
||||
warn!(
|
||||
"No SkimmedNode data found for GW, identity_key={}",
|
||||
gw.gateway_identity_key
|
||||
);
|
||||
None
|
||||
}
|
||||
})
|
||||
.filter_map(
|
||||
|(gw, skimmed_node)| match DVpnGateway::new(gw, skimmed_node) {
|
||||
Ok(gw) => Some(gw),
|
||||
Err(err) => {
|
||||
error!(
|
||||
"Failed to convert GW node_id={} to dVPN form: {}",
|
||||
skimmed_node.node_id, err
|
||||
);
|
||||
None
|
||||
}
|
||||
},
|
||||
)
|
||||
.filter(|gw| {
|
||||
let gw_version = &gw.build_information.build_version;
|
||||
if let Ok(gw_version) = Version::parse(gw_version) {
|
||||
&gw_version >= min_node_version
|
||||
} else {
|
||||
warn!("Failed to parse GW version {}", gw_version);
|
||||
false
|
||||
}
|
||||
})
|
||||
.filter(|gw| {
|
||||
// gateways must have a country
|
||||
if gw.location.two_letter_iso_country_code.len() == 2 {
|
||||
true
|
||||
} else {
|
||||
warn!(
|
||||
"Invalid country code: {}",
|
||||
gw.location.two_letter_iso_country_code
|
||||
);
|
||||
false
|
||||
}
|
||||
})
|
||||
// sort by country, then by identity key
|
||||
.sorted_by_key(|item| {
|
||||
(
|
||||
item.location.two_letter_iso_country_code.clone(),
|
||||
item.identity_key.clone(),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if res_gws.is_empty() && started_with > 0 {
|
||||
tracing::warn!("Started with {}, got 0 gateways", started_with);
|
||||
} else {
|
||||
self.upsert_dvpn_gateway_list(res_gws.clone(), min_node_version)
|
||||
.await;
|
||||
}
|
||||
|
||||
res_gws
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn upsert_mixnode_list(
|
||||
&self,
|
||||
new_mixnode_list: Vec<Mixnode>,
|
||||
@@ -201,10 +338,11 @@ impl HttpCache {
|
||||
let mixnodes = crate::db::queries::get_all_mixnodes(db)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
self.upsert_mixnode_list(mixnodes.clone()).await;
|
||||
|
||||
if mixnodes.is_empty() {
|
||||
tracing::warn!("Database contains 0 mixnodes");
|
||||
} else {
|
||||
self.upsert_mixnode_list(mixnodes.clone()).await;
|
||||
}
|
||||
|
||||
mixnodes
|
||||
@@ -245,14 +383,13 @@ impl HttpCache {
|
||||
None => {
|
||||
tracing::trace!("No nym nodes in cache, refreshing cache from DB...");
|
||||
|
||||
let nym_nodes = aggregate_node_info_from_db(db, node_geocache)
|
||||
.await
|
||||
.inspect(|nym_nodes| {
|
||||
if nym_nodes.is_empty() {
|
||||
tracing::warn!("Database contains 0 nym nodes");
|
||||
}
|
||||
})?;
|
||||
self.upsert_nym_node_list(nym_nodes.clone()).await;
|
||||
let nym_nodes = aggregate_node_info_from_db(db, node_geocache).await?;
|
||||
|
||||
if nym_nodes.is_empty() {
|
||||
tracing::warn!("Database contains 0 nym nodes");
|
||||
} else {
|
||||
self.upsert_nym_node_list(nym_nodes.clone()).await;
|
||||
}
|
||||
|
||||
Ok(nym_nodes)
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ impl IpInfoClient {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub(crate) struct ExplorerPrettyBond {
|
||||
pub(crate) identity_key: String,
|
||||
pub(crate) owner: Addr,
|
||||
@@ -71,7 +71,7 @@ pub(crate) struct ExplorerPrettyBond {
|
||||
pub(crate) location: Location,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize)]
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub(crate) struct Location {
|
||||
pub(crate) two_letter_iso_country_code: String,
|
||||
#[serde(flatten)]
|
||||
|
||||
@@ -7,7 +7,6 @@ use crate::db::models::{
|
||||
NYMNODES_DESCRIBED_COUNT, NYMNODE_COUNT,
|
||||
};
|
||||
use crate::db::{queries, DbPool};
|
||||
use crate::monitor::geodata::{ExplorerPrettyBond, Location};
|
||||
use crate::utils::now_utc;
|
||||
use crate::utils::{decimal_to_i64, LogError, NumericalCheckedCast};
|
||||
use anyhow::anyhow;
|
||||
@@ -30,7 +29,7 @@ use std::{
|
||||
use tokio::{sync::RwLock, time::Duration};
|
||||
use tracing::instrument;
|
||||
|
||||
pub(crate) use geodata::IpInfoClient;
|
||||
pub(crate) use geodata::{ExplorerPrettyBond, IpInfoClient, Location};
|
||||
pub(crate) use node_delegations::DelegationsCache;
|
||||
|
||||
mod geodata;
|
||||
@@ -106,11 +105,10 @@ impl Monitor {
|
||||
.clone()
|
||||
.expect("rust sdk mainnet default missing api_url");
|
||||
|
||||
let nym_api =
|
||||
nym_http_api_client::ClientBuilder::new_with_urls(vec![default_api_url.into()])
|
||||
.no_hickory_dns()
|
||||
.with_timeout(self.nym_api_client_timeout)
|
||||
.build::<&str>()?;
|
||||
let nym_api = nym_http_api_client::ClientBuilder::new_with_url(default_api_url)
|
||||
.no_hickory_dns()
|
||||
.with_timeout(self.nym_api_client_timeout)
|
||||
.build::<&str>()?;
|
||||
|
||||
let api_client = NymApiClient::from(nym_api);
|
||||
|
||||
@@ -134,6 +132,8 @@ impl Monitor {
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
tracing::info!("🟣 🚪 gateway nodes: {}", gateways.len());
|
||||
|
||||
let bonded_nym_nodes = api_client
|
||||
.get_all_bonded_nym_nodes()
|
||||
.await?
|
||||
@@ -150,7 +150,8 @@ impl Monitor {
|
||||
.await
|
||||
.log_error("get_all_basic_nodes")?;
|
||||
|
||||
tracing::info!("🟣 get_all_basic_nodes: {}", nym_nodes.len());
|
||||
let nym_node_count = nym_nodes.len();
|
||||
tracing::info!("🟣 get_all_basic_nodes: {}", nym_node_count);
|
||||
|
||||
let nym_node_records =
|
||||
self.prepare_nym_node_data(nym_nodes.clone(), &bonded_nym_nodes, &described_nodes);
|
||||
@@ -251,7 +252,7 @@ impl Monitor {
|
||||
//
|
||||
|
||||
let nodes_summary = vec![
|
||||
(NYMNODE_COUNT, nym_nodes.len()),
|
||||
(NYMNODE_COUNT, nym_node_count),
|
||||
(ASSIGNED_MIXING_COUNT, assigned_mixing_count),
|
||||
(MIXNODES_LEGACY_COUNT, count_legacy_mixnodes),
|
||||
(NYMNODES_DESCRIBED_COUNT, described_nodes.len()),
|
||||
@@ -267,7 +268,7 @@ impl Monitor {
|
||||
let last_updated = now_utc();
|
||||
let last_updated_utc = last_updated.unix_timestamp().to_string();
|
||||
let network_summary = NetworkSummary {
|
||||
total_nodes: nym_nodes.len().cast_checked()?,
|
||||
total_nodes: nym_node_count.cast_checked()?,
|
||||
mixnodes: mixnode::MixnodeSummary {
|
||||
bonded: mixnode::MixingNodesSummary {
|
||||
count: assigned_mixing_count.cast_checked()?,
|
||||
@@ -457,11 +458,7 @@ impl Monitor {
|
||||
async fn check_ipinfo_bandwidth(&self) {
|
||||
match self.ipinfo.check_remaining_bandwidth().await {
|
||||
Ok(bandwidth) => {
|
||||
tracing::info!(
|
||||
"ipinfo monthly bandwidth: {}/{} spent",
|
||||
bandwidth.month,
|
||||
bandwidth.limit
|
||||
);
|
||||
tracing::info!("ipinfo monthly bandwidth: {} spent", bandwidth.month);
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::debug!("Couldn't check ipinfo bandwidth: {}", err);
|
||||
|
||||
@@ -57,7 +57,7 @@ async fn run(
|
||||
.clone()
|
||||
.expect("rust sdk mainnet default missing api_url");
|
||||
|
||||
let nym_api = nym_http_api_client::ClientBuilder::new_with_urls(vec![default_api_url.into()])
|
||||
let nym_api = nym_http_api_client::ClientBuilder::new_with_url(default_api_url)
|
||||
.no_hickory_dns()
|
||||
.with_timeout(nym_api_client_timeout)
|
||||
.build::<&str>()?;
|
||||
|
||||
@@ -24,7 +24,7 @@ pub(crate) async fn spawn(pool: DbPool, refresh_interval: Duration) {
|
||||
});
|
||||
}
|
||||
|
||||
#[instrument(level = "debug", name = "testrun_queue", skip_all)]
|
||||
#[instrument(level = "info", name = "testrun_queue", skip_all)]
|
||||
async fn run(pool: &DbPool) -> anyhow::Result<()> {
|
||||
tracing::info!("Spawning testruns...");
|
||||
if pool.is_closed() {
|
||||
|
||||
@@ -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::{
|
||||
@@ -41,6 +41,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 })
|
||||
}
|
||||
@@ -67,6 +68,17 @@ impl SignedHostInformation {
|
||||
return true;
|
||||
}
|
||||
|
||||
// TODO: @JS: to remove downgrade support in future release(s)
|
||||
|
||||
let legacy_v3 = SignedData {
|
||||
data: LegacyHostInformationV3::from(self.data.clone()),
|
||||
signature: self.signature.clone(),
|
||||
};
|
||||
|
||||
if legacy_v3.verify(&self.keys.ed25519_identity) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// attempt to verify legacy signatures
|
||||
let legacy_v3 = SignedData {
|
||||
data: LegacyHostInformationV3::from(self.data.clone()),
|
||||
@@ -116,9 +128,11 @@ impl Display for ErrorResponse {
|
||||
#[allow(deprecated)]
|
||||
#[cfg(test)]
|
||||
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 rand_chacha::rand_core::SeedableRng;
|
||||
|
||||
#[test]
|
||||
@@ -127,7 +141,10 @@ 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_noise = x25519::KeyPair::new(&mut rng);
|
||||
let x25519_versioned_noise = VersionedNoiseKey {
|
||||
supported_version: NoiseVersion::V1,
|
||||
x25519_pubkey: *x25519::KeyPair::new(&mut rng).public_key(),
|
||||
};
|
||||
|
||||
let current_rotation_id = 1234;
|
||||
|
||||
@@ -142,8 +159,8 @@ mod tests {
|
||||
rotation_id: current_rotation_id,
|
||||
public_key: *x25519_sphinx.public_key(),
|
||||
},
|
||||
x25519_noise: None,
|
||||
pre_announced_x25519_sphinx_key: None,
|
||||
x25519_versioned_noise: None,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -162,7 +179,7 @@ mod tests {
|
||||
public_key: *x25519_sphinx.public_key(),
|
||||
},
|
||||
pre_announced_x25519_sphinx_key: None,
|
||||
x25519_noise: Some(*x25519_noise.public_key()),
|
||||
x25519_versioned_noise: Some(x25519_versioned_noise),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -186,7 +203,7 @@ mod tests {
|
||||
rotation_id: current_rotation_id + 1,
|
||||
public_key: *x25519_sphinx2.public_key(),
|
||||
}),
|
||||
x25519_noise: None,
|
||||
x25519_versioned_noise: None,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -208,7 +225,7 @@ mod tests {
|
||||
rotation_id: current_rotation_id + 1,
|
||||
public_key: *x25519_sphinx2.public_key(),
|
||||
}),
|
||||
x25519_noise: Some(*x25519_noise.public_key()),
|
||||
x25519_versioned_noise: Some(x25519_versioned_noise),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -225,13 +242,13 @@ mod tests {
|
||||
let x25519_sphinx = x25519::KeyPair::new(&mut rng);
|
||||
let x25519_noise = x25519::KeyPair::new(&mut rng);
|
||||
|
||||
let legacy_info = crate::api::v1::node::models::LegacyHostInformationV3 {
|
||||
let legacy_info_no_noise = crate::api::v1::node::models::LegacyHostInformationV3 {
|
||||
ip_address: vec!["1.1.1.1".parse().unwrap()],
|
||||
hostname: Some("foomp.com".to_string()),
|
||||
keys: crate::api::v1::node::models::LegacyHostKeysV3 {
|
||||
ed25519_identity: *ed22519.public_key(),
|
||||
x25519_sphinx: *x25519_sphinx.public_key(),
|
||||
x25519_noise: Some(*x25519_noise.public_key()),
|
||||
x25519_noise: None,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -247,12 +264,12 @@ mod tests {
|
||||
public_key: *x25519_sphinx.public_key(),
|
||||
},
|
||||
pre_announced_x25519_sphinx_key: None,
|
||||
x25519_noise: Some(*x25519_noise.public_key()),
|
||||
x25519_versioned_noise: None,
|
||||
},
|
||||
};
|
||||
|
||||
// signature on legacy data
|
||||
let signature = SignedData::new(legacy_info, ed22519.private_key())
|
||||
let signature = SignedData::new(legacy_info_no_noise, ed22519.private_key())
|
||||
.unwrap()
|
||||
.signature;
|
||||
|
||||
@@ -263,7 +280,53 @@ mod tests {
|
||||
};
|
||||
|
||||
assert!(!current_struct.verify(ed22519.public_key()));
|
||||
assert!(current_struct.verify_host_information())
|
||||
assert!(current_struct.verify_host_information());
|
||||
|
||||
// //technically this variant should never happen
|
||||
let legacy_info_noise = crate::api::v1::node::models::LegacyHostInformationV3 {
|
||||
ip_address: vec!["1.1.1.1".parse().unwrap()],
|
||||
hostname: Some("foomp.com".to_string()),
|
||||
keys: crate::api::v1::node::models::LegacyHostKeysV3 {
|
||||
ed25519_identity: *ed22519.public_key(),
|
||||
x25519_sphinx: *x25519_sphinx.public_key(),
|
||||
x25519_noise: Some(*x25519_noise.public_key()),
|
||||
},
|
||||
};
|
||||
|
||||
// note the usage of u32::max rotation id (as that's what the legacy data would be deserialised into)
|
||||
let current_struct_noise = crate::api::v1::node::models::HostInformation {
|
||||
ip_address: vec!["1.1.1.1".parse().unwrap()],
|
||||
hostname: Some("foomp.com".to_string()),
|
||||
keys: HostKeys {
|
||||
ed25519_identity: *ed22519.public_key(),
|
||||
x25519_sphinx: *x25519_sphinx.public_key(),
|
||||
primary_x25519_sphinx_key: SphinxKey {
|
||||
rotation_id: u32::MAX,
|
||||
public_key: *x25519_sphinx.public_key(),
|
||||
},
|
||||
pre_announced_x25519_sphinx_key: None,
|
||||
x25519_versioned_noise: Some(VersionedNoiseKey {
|
||||
supported_version: NoiseVersion::V1,
|
||||
x25519_pubkey: legacy_info_noise.keys.x25519_noise.unwrap(),
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
// signature on legacy data
|
||||
|
||||
let signature_noise = SignedData::new(legacy_info_noise, ed22519.private_key())
|
||||
.unwrap()
|
||||
.signature;
|
||||
|
||||
// signed blob with the 'current' structure
|
||||
|
||||
let current_struct_noise = SignedData {
|
||||
data: current_struct_noise,
|
||||
signature: signature_noise,
|
||||
};
|
||||
|
||||
assert!(!current_struct_noise.verify(ed22519.public_key()));
|
||||
assert!(current_struct_noise.verify_host_information())
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -305,7 +368,7 @@ mod tests {
|
||||
public_key: *x25519_sphinx.public_key(),
|
||||
},
|
||||
pre_announced_x25519_sphinx_key: None,
|
||||
x25519_noise: None,
|
||||
x25519_versioned_noise: None,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -321,7 +384,10 @@ mod tests {
|
||||
public_key: *x25519_sphinx.public_key(),
|
||||
},
|
||||
pre_announced_x25519_sphinx_key: None,
|
||||
x25519_noise: Some(legacy_info_noise.keys.x25519_noise.parse().unwrap()),
|
||||
x25519_versioned_noise: Some(VersionedNoiseKey {
|
||||
supported_version: NoiseVersion::V1,
|
||||
x25519_pubkey: legacy_info_noise.keys.x25519_noise.parse().unwrap(),
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -379,7 +445,7 @@ mod tests {
|
||||
public_key: *x25519_sphinx.public_key(),
|
||||
},
|
||||
pre_announced_x25519_sphinx_key: None,
|
||||
x25519_noise: None,
|
||||
x25519_versioned_noise: None,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
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},
|
||||
self, serde_helpers::bs58_x25519_pubkey, serde_helpers::option_bs58_x25519_pubkey,
|
||||
};
|
||||
use nym_noise_keys::VersionedNoiseKey;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::net::IpAddr;
|
||||
@@ -145,9 +145,8 @@ pub struct HostKeys {
|
||||
pub pre_announced_x25519_sphinx_key: Option<SphinxKey>,
|
||||
|
||||
/// Base58-encoded x25519 public key of this node used for the noise protocol.
|
||||
#[schemars(with = "Option<String>")]
|
||||
#[cfg_attr(feature = "openapi", schema(value_type = Option<String>))]
|
||||
pub x25519_noise: Option<x25519::PublicKey>,
|
||||
#[serde(default)]
|
||||
pub x25519_versioned_noise: Option<VersionedNoiseKey>,
|
||||
}
|
||||
|
||||
// we need the intermediate struct to help us with the new explicit sphinx key fields
|
||||
@@ -167,7 +166,7 @@ impl From<HostKeysDeHelper> for HostKeys {
|
||||
x25519_sphinx: value.x25519_sphinx,
|
||||
primary_x25519_sphinx_key,
|
||||
pre_announced_x25519_sphinx_key: value.pre_announced_x25519_sphinx_key,
|
||||
x25519_noise: value.x25519_noise,
|
||||
x25519_versioned_noise: value.x25519_versioned_noise,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -194,8 +193,7 @@ struct HostKeysDeHelper {
|
||||
|
||||
/// Base58-encoded x25519 public key of this node used for the noise protocol.
|
||||
#[serde(default)]
|
||||
#[serde(with = "option_bs58_x25519_pubkey")]
|
||||
pub x25519_noise: Option<x25519::PublicKey>,
|
||||
pub x25519_versioned_noise: Option<VersionedNoiseKey>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
@@ -224,18 +222,14 @@ impl SphinxKey {
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct LegacyHostKeysV3 {
|
||||
/// Base58-encoded ed25519 public key of this node. Currently, it corresponds to either mixnode's or gateway's identity.
|
||||
#[serde(alias = "ed25519")]
|
||||
#[serde(with = "bs58_ed25519_pubkey")]
|
||||
pub ed25519_identity: ed25519::PublicKey,
|
||||
|
||||
/// Base58-encoded x25519 public key of this node used for sphinx/outfox packet creation.
|
||||
/// Currently, it corresponds to either mixnode's or gateway's key.
|
||||
#[serde(alias = "x25519")]
|
||||
#[serde(with = "bs58_x25519_pubkey")]
|
||||
pub x25519_sphinx: x25519::PublicKey,
|
||||
|
||||
/// Base58-encoded x25519 public key of this node used for the noise protocol.
|
||||
#[serde(default)]
|
||||
#[serde(with = "option_bs58_x25519_pubkey")]
|
||||
pub x25519_noise: Option<x25519::PublicKey>,
|
||||
@@ -259,7 +253,7 @@ impl From<HostKeys> for LegacyHostKeysV3 {
|
||||
LegacyHostKeysV3 {
|
||||
ed25519_identity: value.ed25519_identity,
|
||||
x25519_sphinx: value.primary_x25519_sphinx_key.public_key,
|
||||
x25519_noise: value.x25519_noise,
|
||||
x25519_noise: value.x25519_versioned_noise.map(|k| k.x25519_pubkey),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -765,8 +765,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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ pub(crate) async fn host_information(
|
||||
rotation_id: primary_key.rotation_id(),
|
||||
public_key: primary_pubkey,
|
||||
},
|
||||
x25519_noise: state.static_information.x25519_noise_key,
|
||||
x25519_versioned_noise: state.static_information.x25519_versioned_noise_key,
|
||||
pre_announced_x25519_sphinx_key: pre_announced,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -4,8 +4,9 @@
|
||||
use crate::node::http::state::load::CachedNodeLoad;
|
||||
use crate::node::http::state::metrics::MetricsAppState;
|
||||
use crate::node::key_rotation::active_keys::ActiveSphinxKeys;
|
||||
use nym_crypto::asymmetric::{ed25519, x25519};
|
||||
use nym_crypto::asymmetric::ed25519;
|
||||
use nym_node_metrics::NymNodeMetrics;
|
||||
use nym_noise_keys::VersionedNoiseKey;
|
||||
use nym_verloc::measurements::SharedVerlocStats;
|
||||
use std::net::IpAddr;
|
||||
use std::sync::Arc;
|
||||
@@ -17,7 +18,7 @@ pub mod metrics;
|
||||
|
||||
pub(crate) struct StaticNodeInformation {
|
||||
pub(crate) ed25519_identity_keys: Arc<ed25519::KeyPair>,
|
||||
pub(crate) x25519_noise_key: Option<x25519::PublicKey>,
|
||||
pub(crate) x25519_versioned_noise_key: Option<VersionedNoiseKey>,
|
||||
pub(crate) ip_addresses: Vec<IpAddr>,
|
||||
pub(crate) hostname: Option<String>,
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -72,7 +74,6 @@ impl PendingReplayCheckPackets {
|
||||
|
||||
pub(crate) struct ConnectionHandler {
|
||||
shared: SharedData,
|
||||
mixnet_connection: Framed<TcpStream, NymCodec>,
|
||||
remote_address: SocketAddr,
|
||||
|
||||
// packets pending for replay detection
|
||||
@@ -89,11 +90,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();
|
||||
|
||||
@@ -104,11 +101,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(),
|
||||
}
|
||||
}
|
||||
@@ -466,7 +463,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<TcpStream>, NymCodec>,
|
||||
) {
|
||||
loop {
|
||||
tokio::select! {
|
||||
biased;
|
||||
@@ -474,7 +493,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,
|
||||
@@ -74,6 +75,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,
|
||||
}
|
||||
@@ -86,12 +90,14 @@ fn convert_to_metrics_version(processed: MixPacketVersion) -> PacketKind {
|
||||
}
|
||||
|
||||
impl SharedData {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) fn new(
|
||||
processing_config: ProcessingConfig,
|
||||
sphinx_keys: ActiveSphinxKeys,
|
||||
replay_protection_filter: ReplayProtectionBloomfilters,
|
||||
mixnet_forwarder: MixForwardingSender,
|
||||
final_hop: SharedFinalHopData,
|
||||
noise_config: NoiseConfig,
|
||||
metrics: NymNodeMetrics,
|
||||
shutdown: ShutdownToken,
|
||||
) -> Self {
|
||||
@@ -101,6 +107,7 @@ impl SharedData {
|
||||
replay_protection_filter,
|
||||
mixnet_forwarder,
|
||||
final_hop,
|
||||
noise_config,
|
||||
metrics,
|
||||
shutdown,
|
||||
}
|
||||
@@ -163,8 +170,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)
|
||||
}
|
||||
|
||||
@@ -50,6 +50,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};
|
||||
@@ -800,16 +802,19 @@ impl NymNode {
|
||||
config.api.v1_config.node.roles.ip_packet_router_enabled = true;
|
||||
}
|
||||
|
||||
let x25519_noise_key = if self.config.mixnet.debug.unsafe_disable_noise {
|
||||
let x25519_versioned_noise_key = if self.config.mixnet.debug.unsafe_disable_noise {
|
||||
None
|
||||
} else {
|
||||
Some(*self.x25519_noise_keys.public_key())
|
||||
Some(VersionedNoiseKey {
|
||||
supported_version: nym_noise::LATEST_NOISE_VERSION,
|
||||
x25519_pubkey: *self.x25519_noise_keys.public_key(),
|
||||
})
|
||||
};
|
||||
|
||||
let app_state = AppState::new(
|
||||
StaticNodeInformation {
|
||||
ed25519_identity_keys: self.ed25519_identity_keys.clone(),
|
||||
x25519_noise_key,
|
||||
x25519_versioned_noise_key,
|
||||
ip_addresses: self.config.host.public_ips.clone(),
|
||||
hostname: self.config.host.hostname.clone(),
|
||||
},
|
||||
@@ -1031,6 +1036,7 @@ impl NymNode {
|
||||
active_clients_store: &ActiveClientsStore,
|
||||
replay_protection_bloomfilter: ReplayProtectionBloomfilters,
|
||||
routing_filter: F,
|
||||
noise_config: NoiseConfig,
|
||||
shutdown: ShutdownToken,
|
||||
) -> Result<(MixForwardingSender, ActiveConnections), NymNodeError>
|
||||
where
|
||||
@@ -1054,6 +1060,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(),
|
||||
@@ -1080,6 +1087,7 @@ impl NymNode {
|
||||
replay_protection_bloomfilter,
|
||||
mix_packet_sender.clone(),
|
||||
final_hop_data,
|
||||
noise_config,
|
||||
self.metrics.clone(),
|
||||
shutdown,
|
||||
);
|
||||
@@ -1089,10 +1097,18 @@ 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(),
|
||||
ReplayProtectionBloomfilters::new_disabled(),
|
||||
OpenFilter,
|
||||
noise_config,
|
||||
self.shutdown_manager.clone_token("mixnet-traffic"),
|
||||
)
|
||||
.await?;
|
||||
@@ -1137,11 +1153,19 @@ impl NymNode {
|
||||
|
||||
let bloomfilters_manager = self.setup_replay_detection().await?;
|
||||
|
||||
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,
|
||||
bloomfilters_manager.bloomfilters(),
|
||||
network_refresher.routing_filter(),
|
||||
noise_config,
|
||||
self.shutdown_manager.clone_token("mixnet-traffic"),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -8,6 +8,7 @@ use async_trait::async_trait;
|
||||
use nym_crypto::asymmetric::ed25519;
|
||||
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::{
|
||||
@@ -16,10 +17,10 @@ use nym_topology::{
|
||||
};
|
||||
use nym_validator_client::nym_api::NymApiClientExt;
|
||||
use nym_validator_client::nym_nodes::{
|
||||
NodesByAddressesResponse, SkimmedNode, SkimmedNodesWithMetadata,
|
||||
NodesByAddressesResponse, SemiSkimmedNode, SemiSkimmedNodesWithMetadata,
|
||||
};
|
||||
use nym_validator_client::{NymApiClient, ValidatorClientError};
|
||||
use std::collections::HashSet;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::net::{IpAddr, SocketAddr};
|
||||
use std::ops::Deref;
|
||||
use std::sync::Arc;
|
||||
@@ -63,10 +64,12 @@ impl NodesQuerier {
|
||||
res
|
||||
}
|
||||
|
||||
async fn current_nymnodes(&mut self) -> Result<SkimmedNodesWithMetadata, ValidatorClientError> {
|
||||
async fn current_nymnodes(
|
||||
&mut self,
|
||||
) -> Result<SemiSkimmedNodesWithMetadata, ValidatorClientError> {
|
||||
let res = self
|
||||
.client
|
||||
.get_all_basic_nodes_with_metadata()
|
||||
.get_all_expanded_nodes()
|
||||
.await
|
||||
.inspect_err(|err| error!("failed to get network nodes: {err}"));
|
||||
|
||||
@@ -150,13 +153,19 @@ impl TopologyProvider for CachedTopologyProvider {
|
||||
network_guard.rewarded_set.clone(),
|
||||
Vec::new(),
|
||||
)
|
||||
.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(self.gateway_node.identity_key) {
|
||||
debug!("{self_node} didn't exist in topology. inserting it.",);
|
||||
@@ -188,7 +197,7 @@ impl CachedNetwork {
|
||||
struct CachedNetworkInner {
|
||||
rewarded_set: EpochRewardedSet,
|
||||
topology_metadata: NymTopologyMetadata,
|
||||
network_nodes: Vec<SkimmedNode>,
|
||||
network_nodes: Vec<SemiSkimmedNode>,
|
||||
}
|
||||
|
||||
pub struct NetworkRefresher {
|
||||
@@ -199,6 +208,7 @@ pub struct NetworkRefresher {
|
||||
|
||||
network: CachedNetwork,
|
||||
routing_filter: NetworkRoutingFilter,
|
||||
noise_view: NoiseNetworkView,
|
||||
}
|
||||
|
||||
impl NetworkRefresher {
|
||||
@@ -226,6 +236,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?;
|
||||
@@ -282,7 +293,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<_>>();
|
||||
|
||||
@@ -305,6 +316,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.topology_metadata =
|
||||
NymTopologyMetadata::new(metadata.rotation_id, metadata.absolute_epoch_id);
|
||||
@@ -340,6 +367,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();
|
||||
|
||||
@@ -177,7 +177,7 @@ impl ThroughputTestingClient {
|
||||
// by tagging the packet
|
||||
let shared_secret = private
|
||||
.as_ref()
|
||||
.as_ref()
|
||||
.inner()
|
||||
.diffie_hellman(&header.shared_secret);
|
||||
let payload_key = rederive_lioness_payload_key(shared_secret.as_bytes());
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
[package]
|
||||
name = "nym-statistics-api"
|
||||
version = "0.1.0"
|
||||
version = "0.1.1"
|
||||
authors.workspace = true
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
@@ -39,7 +39,6 @@ utoipauto.workspace = true
|
||||
|
||||
#internal
|
||||
nym-bin-common = { path = "../common/bin-common" }
|
||||
nym-http-api-client = { path = "../common/http-api-client" }
|
||||
nym-http-api-common = { path = "../common/http-api-common", features = [
|
||||
"middleware",
|
||||
] }
|
||||
@@ -47,7 +46,9 @@ nym-statistics-common = { path = "../common/statistics", features = [
|
||||
"openapi",
|
||||
] }
|
||||
nym-task = { path = "../common/task" }
|
||||
nym-validator-client = { path = "../common/client-libs/validator-client" }
|
||||
|
||||
nym-http-api-client = { git = "https://github.com/nymtech/nym", branch = "release/2025.11-cheddar" }
|
||||
nym-validator-client = { git = "https://github.com/nymtech/nym", branch = "release/2025.11-cheddar" } # current develop is incompatible with current deployed API
|
||||
|
||||
[build-dependencies]
|
||||
anyhow = { workspace = true }
|
||||
|
||||
@@ -33,7 +33,7 @@ async fn main() -> anyhow::Result<()> {
|
||||
args.nym_api_url,
|
||||
shutdown_manager.child_token("network-refresher"),
|
||||
)
|
||||
.await?;
|
||||
.await;
|
||||
|
||||
let http_server =
|
||||
http::server::build_http_api(storage, network_refresher.network_view(), args.http_port)
|
||||
|
||||
@@ -71,15 +71,15 @@ impl NetworkRefresher {
|
||||
pub(crate) async fn initialise_new(
|
||||
maybe_nym_api_url: Option<Url>,
|
||||
shutdown_token: ShutdownToken,
|
||||
) -> Result<Self> {
|
||||
) -> Self {
|
||||
let node_querier = match maybe_nym_api_url {
|
||||
Some(url) => Some(NodesQuerier {
|
||||
client: nym_http_api_client::Client::builder::<_, anyhow::Error>(url)?
|
||||
.no_hickory_dns()
|
||||
.with_user_agent("node-statistics-api")
|
||||
.build::<anyhow::Error>()?
|
||||
.into(),
|
||||
}),
|
||||
Some(url) => match Self::build_http_api_client(url) {
|
||||
Ok(client) => Some(NodesQuerier { client }),
|
||||
Err(e) => {
|
||||
warn!("Failed to build Nym API client, no network view will be availabe : {e}");
|
||||
None
|
||||
}
|
||||
},
|
||||
None => {
|
||||
warn!("No Nym API specified, network view is unavailable");
|
||||
None
|
||||
@@ -93,8 +93,20 @@ impl NetworkRefresher {
|
||||
network: NetworkView::new_empty(),
|
||||
};
|
||||
|
||||
this.refresh_network_nodes().await?;
|
||||
Ok(this)
|
||||
if let Err(e) = this.refresh_network_nodes().await {
|
||||
warn!("Failed to fetch initial network nodes : {e}");
|
||||
}
|
||||
this
|
||||
}
|
||||
|
||||
fn build_http_api_client(url: Url) -> Result<NymApiClient> {
|
||||
Ok(
|
||||
nym_http_api_client::Client::builder::<_, anyhow::Error>(url)?
|
||||
.no_hickory_dns()
|
||||
.with_user_agent("node-statistics-api")
|
||||
.build::<anyhow::Error>()?
|
||||
.into(),
|
||||
)
|
||||
}
|
||||
|
||||
async fn refresh_network_nodes(&mut self) -> Result<()> {
|
||||
|
||||
Generated
+13
-1
@@ -4032,6 +4032,7 @@ dependencies = [
|
||||
"nym-mixnet-contract-common",
|
||||
"nym-network-defaults",
|
||||
"nym-node-requests",
|
||||
"nym-noise-keys",
|
||||
"nym-serde-helpers",
|
||||
"nym-ticketbooks-merkle",
|
||||
"schemars",
|
||||
@@ -4272,7 +4273,7 @@ dependencies = [
|
||||
name = "nym-network-defaults"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"cargo_metadata 0.18.1",
|
||||
"cargo_metadata 0.19.2",
|
||||
"dotenvy",
|
||||
"log",
|
||||
"regex",
|
||||
@@ -4294,6 +4295,7 @@ dependencies = [
|
||||
"nym-crypto",
|
||||
"nym-exit-policy",
|
||||
"nym-http-api-client",
|
||||
"nym-noise-keys",
|
||||
"nym-wireguard-types",
|
||||
"schemars",
|
||||
"serde",
|
||||
@@ -4304,6 +4306,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