Compare commits

...

22 Commits

Author SHA1 Message Date
benedetta davico 0d420fb0a5 remove explorer-api in workflow 2025-06-10 11:01:24 +02:00
benedettadavico fce195fdba update changelog 2025-06-10 10:28:47 +02:00
Bogdan-Ștefan Neacşu 3a528d3b89 No autoremoval of peers (#5831)
* No autoremoval

* Remove startup_timestamp
2025-06-06 12:48:34 +03:00
benedettadavico e9bb9792ab bump binaries 2025-06-04 14:42:04 +02:00
Bogdan-Ștefan Neacşu 88d4a9b111 Set cached storage counters to 0 (#5812)
* Set cached storage counters to 0

* u64 to i64 log possible error

* Check addition too
2025-06-04 12:11:46 +03:00
dynco-nym 4c67f01efb Make address cache configurable (#5784)
* Make address cache configurable

* TestFixture
2025-05-28 10:41:12 +02:00
Simon Wicky b69c2e1e94 Nym Statistics API (#5800)
* move stats types from vpn-client to here

* base stats api

* change storage schema

* add link to nymAPI for whitelisting

* remove outdated comment

* more comments update

* example of chrono vs time

* Add build.rs
- exports DATABASE_URL so cargo check works
- exports SQLX_OFFLINE for CI
- added pg_up.sh which spawns PG container
  - required for cargo sqlx prepare

* fixes time vs chrono issue and cleaner build with docker

* add correct swagger types, with feature locking where relevant

* apply dynco suggestions

---------

Co-authored-by: dynco-nym <173912580+dynco-nym@users.noreply.github.com>
2025-05-28 10:23:11 +02:00
benedetta davico d27e3b49db Merge pull request #5806 from nymtech/release/2025.10-brie 2025-05-28 09:38:36 +02:00
benedetta davico ac12455f97 add comment 2025-05-27 16:35:51 +02:00
Jędrzej Stuczyński 0b92a59f1a hack: temporarily use next.config.js instead of next.config.ts (#5805) 2025-05-27 11:41:51 +01:00
Jędrzej Stuczyński 474eff67fa chore: adjusted wallet storybook mocks to fix the build (#5804) 2025-05-27 11:38:13 +01:00
benedetta davico 1c6db86259 Merge pull request #5803 from nymtech/benny/change-rust-version
change rust version to fix ci
2025-05-27 12:11:13 +02:00
Jędrzej Stuczyński 4a1ce8154a chore: resolve 1.87 clippy warnings (#5802)
* Clippy in wallet & sdk

* Clippy in wallet

* Pin rust to 1.86 in builder

* apply changes from b7da75a18c

* missing nym-node features

* Box all the things

* additional boxes in the wallet

* post rebasing clippy

---------

Co-authored-by: dynco-nym <173912580+dynco-nym@users.noreply.github.com>
2025-05-27 11:08:36 +01:00
benedetta davico e126c1f7f1 Update publish-nym-binaries.yml 2025-05-27 11:45:53 +02:00
benedetta davico 31772019cd Update ci-contracts.yml 2025-05-27 11:44:01 +02:00
Bogdan-Ștefan Neacşu aca98ab04f Track wireguard credential retries (#5783)
* Add a cache for the credentials seen before on top-up

* Verify seen credentials on top ups

* Add warning log for timestamp subtraction

* Add unit test
2025-05-27 12:35:44 +03:00
Jędrzej Stuczyński f925c6caf0 QoL: RequestPath trait for http-api-client (#5788)
* qol: RequestPath trait for http-api-client

* additional test case

* applied the change to other trait methods
2025-05-27 10:30:13 +01:00
benedettadavico 5369e5eab9 update changelog 2025-05-27 10:03:22 +02:00
Andrej Mihajlov 2e634c59a7 Merge pull request #5801 from nymtech/am/backport-pr-5779 2025-05-26 21:03:29 +02:00
jmwample d7383d74f3 more relaxed usage of reqwest accept-encoding 2025-05-26 17:54:19 +02:00
dynco-nym e98d60d7ce Add node_bonded field to delegations (#5759)
* Add node_bonded field to delegations
- clarifies whether the delegation is to a bonded or unbonded node
- include delegations to unbonded nodes in the returned list

* PR feedback
2025-05-19 15:18:41 +02:00
benedettadavico f47650d6c8 bump binary versions 2025-05-16 13:03:37 +02:00
95 changed files with 1948 additions and 395 deletions
+1
View File
@@ -13,6 +13,7 @@ on:
- 'nym-network-monitor/**'
- 'nym-node/**'
- 'nym-node-status-api/**'
- 'nym-statistics-api/**'
- 'nym-outfox/**'
- 'nym-validator-rewarder/**'
- 'nyx-chain-watcher/**'
+3 -1
View File
@@ -20,6 +20,7 @@ jobs:
runs-on: ubuntu-22.04
env:
CARGO_TERM_COLOR: always
RUSTUP_PERMIT_COPY_RENAME: 1
steps:
- uses: actions/checkout@v4
@@ -27,7 +28,8 @@ jobs:
uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
# pinned due to issues building contracts
toolchain: 1.86.0
target: wasm32-unknown-unknown
override: true
components: rustfmt, clippy
+1 -3
View File
@@ -52,7 +52,7 @@ jobs:
- name: Install Rust stable
uses: actions-rs/toolchain@v1
with:
toolchain: stable
toolchain: 1.86.0
override: true
- name: Build all binaries
@@ -66,7 +66,6 @@ jobs:
with:
name: my-artifact
path: |
target/release/explorer-api
target/release/nym-client
target/release/nym-socks5-client
target/release/nym-api
@@ -82,7 +81,6 @@ jobs:
if: github.event_name == 'release'
with:
files: |
target/release/explorer-api
target/release/nym-client
target/release/nym-socks5-client
target/release/nym-api
+76
View File
@@ -4,6 +4,82 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
## [Unreleased]
## [2025.11-cheddar] (2025-06-10)
- No autoremoval of peers ([#5831])
- Set cached storage counters to 0 ([#5812])
- hack: temporarily use next.config.js instead of next.config.ts ([#5805])
- chore: resolve 1.87 clippy warnings ([#5802])
- Nym Statistics API ([#5800])
- QoL: RequestPath trait for http-api-client ([#5788])
- Fix contains ticketbook function that always returned true ([#5787])
- swap a decode into a fromrow to please future postgres feature ([#5785])
- Make address cache configurable ([#5784])
- Track wireguard credential retries ([#5783])
[#5831]: https://github.com/nymtech/nym/pull/5831
[#5812]: https://github.com/nymtech/nym/pull/5812
[#5805]: https://github.com/nymtech/nym/pull/5805
[#5802]: https://github.com/nymtech/nym/pull/5802
[#5800]: https://github.com/nymtech/nym/pull/5800
[#5788]: https://github.com/nymtech/nym/pull/5788
[#5787]: https://github.com/nymtech/nym/pull/5787
[#5785]: https://github.com/nymtech/nym/pull/5785
[#5784]: https://github.com/nymtech/nym/pull/5784
[#5783]: https://github.com/nymtech/nym/pull/5783
## [2025.10-brie] (2025-05-27)
- Backport PR 5779 ([#5801])
- Expanded Accept Encoding for `reqwest` ([#5779])
- Teach HttpClientError how to report its status code and timeout ([#5770])
- Skip refreshing the topology on startup as we already have an initial set ([#5768])
- Fetch the topology from the nym-api concurrently ([#5767])
- feat: use bincode by default in NymApiClient + remove feature-lock ([#5761])
- Instrument create_request ([#5760])
- Add node_bonded field to delegations ([#5759])
- build(deps): bump mikefarah/yq from 4.45.1 to 4.45.4 ([#5758])
- Raw route submissions ([#5756])
- feat: expires header for `/active` nym-api responses ([#5755])
- Decrease default average packet delay to 15 ms ([#5754])
- build(deps): bump the patch-updates group across 1 directory with 12 updates ([#5753])
- Remove pretty_env_logger and switch remaining crates to use tracing ([#5749])
- Update pretty_env_logger to latest to not depend on unmaintained crate atty ([#5748])
- Upgrade prometheus crate to fix security warning ([#5747])
- Downgrade deranged crate to 0.4.0 ([#5746])
- feat: nym-api bincode + yaml support ([#5745])
- fix parallel feature in ecash crate with send + sync ([#5744])
- Remove old test directory - Update validator docker ([#5743])
- [Feature] `RememberMe` is the new don't `ForgetMe` ([#5742])
- build(deps): bump ammonia from 4.0.0 to 4.1.0 ([#5739])
- build(deps): bump base-x from 3.0.9 to 3.0.11 in /testnet-faucet ([#5737])
- build(deps): bump http-proxy-middleware from 2.0.8 to 2.0.9 ([#5730])
[#5801]: https://github.com/nymtech/nym/pull/5801
[#5779]: https://github.com/nymtech/nym/pull/5779
[#5770]: https://github.com/nymtech/nym/pull/5770
[#5768]: https://github.com/nymtech/nym/pull/5768
[#5767]: https://github.com/nymtech/nym/pull/5767
[#5761]: https://github.com/nymtech/nym/pull/5761
[#5760]: https://github.com/nymtech/nym/pull/5760
[#5759]: https://github.com/nymtech/nym/pull/5759
[#5758]: https://github.com/nymtech/nym/pull/5758
[#5756]: https://github.com/nymtech/nym/pull/5756
[#5755]: https://github.com/nymtech/nym/pull/5755
[#5754]: https://github.com/nymtech/nym/pull/5754
[#5753]: https://github.com/nymtech/nym/pull/5753
[#5749]: https://github.com/nymtech/nym/pull/5749
[#5748]: https://github.com/nymtech/nym/pull/5748
[#5747]: https://github.com/nymtech/nym/pull/5747
[#5746]: https://github.com/nymtech/nym/pull/5746
[#5745]: https://github.com/nymtech/nym/pull/5745
[#5744]: https://github.com/nymtech/nym/pull/5744
[#5743]: https://github.com/nymtech/nym/pull/5743
[#5742]: https://github.com/nymtech/nym/pull/5742
[#5739]: https://github.com/nymtech/nym/pull/5739
[#5737]: https://github.com/nymtech/nym/pull/5737
[#5730]: https://github.com/nymtech/nym/pull/5730
## [2025.9-appenzeller] (2025-05-13)
- build(deps): bump clap from 4.5.36 to 4.5.37 in the patch-updates group ([#5722])
Generated
+45 -7
View File
@@ -4480,6 +4480,12 @@ dependencies = [
"tracing",
]
[[package]]
name = "mock_instant"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e1d4c44418358edcac6e1d9ce59cea7fb38052429c7704033f1196f0c179e6a"
[[package]]
name = "moka"
version = "0.12.10"
@@ -4782,7 +4788,7 @@ checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3"
[[package]]
name = "nym-api"
version = "1.1.57"
version = "1.1.60"
dependencies = [
"anyhow",
"async-trait",
@@ -4927,6 +4933,7 @@ dependencies = [
"futures",
"ipnetwork",
"log",
"mock_instant",
"nym-authenticator-requests",
"nym-bin-common",
"nym-client-core",
@@ -5036,7 +5043,7 @@ dependencies = [
[[package]]
name = "nym-cli"
version = "1.1.55"
version = "1.1.57"
dependencies = [
"anyhow",
"base64 0.22.1",
@@ -5118,7 +5125,7 @@ dependencies = [
[[package]]
name = "nym-client"
version = "1.1.55"
version = "1.1.57"
dependencies = [
"bs58",
"clap",
@@ -6122,7 +6129,7 @@ dependencies = [
[[package]]
name = "nym-network-requester"
version = "1.1.56"
version = "1.1.58"
dependencies = [
"addr",
"anyhow",
@@ -6172,7 +6179,7 @@ dependencies = [
[[package]]
name = "nym-node"
version = "1.11.0"
version = "1.13.0"
dependencies = [
"anyhow",
"arc-swap",
@@ -6568,7 +6575,7 @@ dependencies = [
[[package]]
name = "nym-socks5-client"
version = "1.1.55"
version = "1.1.57"
dependencies = [
"bs58",
"clap",
@@ -6824,6 +6831,36 @@ dependencies = [
"thiserror 2.0.12",
]
[[package]]
name = "nym-statistics-api"
version = "0.1.0"
dependencies = [
"anyhow",
"axum 0.7.9",
"axum-extra",
"celes",
"clap",
"nym-bin-common",
"nym-http-api-client",
"nym-http-api-common",
"nym-statistics-common",
"nym-task",
"nym-validator-client",
"serde",
"serde_json",
"sqlx",
"time",
"tokio",
"tokio-util",
"tower-http",
"tracing",
"tracing-subscriber",
"url",
"utoipa",
"utoipa-swagger-ui",
"utoipauto",
]
[[package]]
name = "nym-statistics-common"
version = "0.1.0"
@@ -6844,6 +6881,7 @@ dependencies = [
"thiserror 2.0.12",
"time",
"tokio",
"utoipa",
"wasmtimer",
]
@@ -7173,7 +7211,7 @@ dependencies = [
[[package]]
name = "nymvisor"
version = "0.1.20"
version = "0.1.22"
dependencies = [
"anyhow",
"bytes",
+2
View File
@@ -112,6 +112,7 @@ members = [
"nym-node/nym-node-metrics",
"nym-node/nym-node-requests",
"nym-outfox",
"nym-statistics-api",
"nym-validator-rewarder",
"nyx-chain-watcher",
"sdk/ffi/cpp",
@@ -152,6 +153,7 @@ default-members = [
"nym-node",
"nym-node-status-api/nym-node-status-agent",
"nym-node-status-api/nym-node-status-api",
"nym-statistics-api",
"nym-validator-rewarder",
"nyx-chain-watcher",
"service-providers/authenticator",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "nym-client"
version = "1.1.55"
version = "1.1.57"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>", "Jędrzej Stuczyński <andrew@nymtech.net>"]
description = "Implementation of the Nym Client"
edition = "2021"
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "nym-socks5-client"
version = "1.1.55"
version = "1.1.57"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>"]
description = "A SOCKS5 localhost proxy that converts incoming messages to Sphinx and sends them to a Nym address"
edition = "2021"
@@ -456,7 +456,7 @@ where
log::error!("Could not authenticate and start up the gateway connection - {err}");
ClientCoreError::GatewayClientError {
gateway_id: details.gateway_id.to_base58_string(),
source: err,
source: Box::new(err),
}
};
+10 -5
View File
@@ -18,7 +18,7 @@ pub enum ClientCoreError {
#[error("gateway client error ({gateway_id}): {source}")]
GatewayClientError {
gateway_id: String,
source: GatewayClientError,
source: Box<GatewayClientError>,
},
#[error("custom gateway client error: {source}")]
@@ -88,10 +88,7 @@ pub enum ClientCoreError {
},
#[error("failed to establish connection to gateway: {source}")]
GatewayConnectionFailure {
#[from]
source: tungstenite::Error,
},
GatewayConnectionFailure { source: Box<tungstenite::Error> },
#[cfg(target_arch = "wasm32")]
#[error("failed to establish gateway connection (wasm)")]
@@ -227,6 +224,14 @@ pub enum ClientCoreError {
HkdfDerivationError {},
}
impl From<tungstenite::Error> for ClientCoreError {
fn from(err: tungstenite::Error) -> ClientCoreError {
ClientCoreError::GatewayConnectionFailure {
source: Box::new(err),
}
}
}
/// Set of messages that the client can send to listeners via the task manager
#[derive(Debug)]
pub enum ClientCoreStatusMessage {
+2 -2
View File
@@ -329,7 +329,7 @@ pub(super) async fn register_with_gateway(
log::warn!("Failed to establish connection with gateway!");
ClientCoreError::GatewayClientError {
gateway_id: gateway_id.to_base58_string(),
source: err,
source: Box::new(err),
}
})?;
let auth_response = gateway_client
@@ -339,7 +339,7 @@ pub(super) async fn register_with_gateway(
log::warn!("Failed to register with the gateway {gateway_id}: {err}");
ClientCoreError::GatewayClientError {
gateway_id: gateway_id.to_base58_string(),
source: err,
source: Box::new(err),
}
})?;
+1 -1
View File
@@ -232,7 +232,7 @@ where
} => {
log::debug!("GatewaySetup::ReuseConnection");
Ok(reuse_gateway_connection(
authenticated_ephemeral_client,
*authenticated_ephemeral_client,
*gateway_details,
managed_keys,
))
+2 -2
View File
@@ -218,7 +218,7 @@ pub enum GatewaySetup {
ReuseConnection {
/// The authenticated ephemeral client that was created during `init`
authenticated_ephemeral_client: InitGatewayClient,
authenticated_ephemeral_client: Box<InitGatewayClient>,
// Details of this pre-initialised client (i.e. gateway and keys)
gateway_details: Box<GatewayRegistration>,
@@ -261,7 +261,7 @@ impl GatewaySetup {
pub fn try_reuse_connection(init_res: InitialisationResult) -> Result<Self, ClientCoreError> {
if let Some(authenticated_ephemeral_client) = init_res.authenticated_ephemeral_client {
Ok(GatewaySetup::ReuseConnection {
authenticated_ephemeral_client,
authenticated_ephemeral_client: Box::new(authenticated_ephemeral_client),
gateway_details: Box::new(init_res.gateway_registration),
client_keys: init_res.client_keys,
})
@@ -56,7 +56,7 @@ pub(crate) async fn connect_async(
}
.map_err(|err| GatewayClientError::NetworkConnectionFailed {
address: endpoint.to_owned(),
source: err.into(),
source: Box::new(tungstenite::Error::from(err)),
})?;
#[cfg(unix)]
@@ -72,7 +72,7 @@ pub(crate) async fn connect_async(
Err(err) => {
stream = Err(GatewayClientError::NetworkConnectionFailed {
address: endpoint.to_owned(),
source: err.into(),
source: Box::new(tungstenite::Error::from(err)),
});
continue;
}
@@ -83,6 +83,6 @@ pub(crate) async fn connect_async(
.await
.map_err(|error| GatewayClientError::NetworkConnectionFailed {
address: endpoint.to_owned(),
source: error,
source: Box::new(error),
})
}
+12 -3
View File
@@ -25,7 +25,7 @@ pub enum GatewayClientError {
RequestError(#[from] GatewayRequestsError),
#[error("There was a network error: {0}")]
NetworkError(#[from] WsError),
NetworkError(Box<WsError>),
#[error("failed to upgrade our shared key - the gateway sent malformed response")]
FatalKeyUpgradeFailure,
@@ -41,7 +41,10 @@ pub enum GatewayClientError {
NetworkErrorWasm(#[from] JsError),
#[error("connection failed: {address}: {source}")]
NetworkConnectionFailed { address: String, source: WsError },
NetworkConnectionFailed {
address: String,
source: Box<WsError>,
},
#[error("no socket address for endpoint: {address}")]
NoEndpointForConnection { address: String },
@@ -127,10 +130,16 @@ pub enum GatewayClientError {
ShutdownInProgress,
}
impl From<WsError> for GatewayClientError {
fn from(error: WsError) -> Self {
GatewayClientError::NetworkError(Box::new(error))
}
}
impl GatewayClientError {
pub fn is_closed_connection(&self) -> bool {
match self {
GatewayClientError::NetworkError(ws_err) => match ws_err {
GatewayClientError::NetworkError(ws_err) => match ws_err.as_ref() {
WsError::AlreadyClosed | WsError::ConnectionClosed => true,
WsError::Io(io_err) => matches!(
io_err.kind(),
+2 -2
View File
@@ -28,7 +28,7 @@ pub(crate) fn cleanup_socket_message(
msg: Option<Result<Message, WsError>>,
) -> Result<Message, GatewayClientError> {
match msg {
Some(msg) => msg.map_err(GatewayClientError::NetworkError),
Some(msg) => msg.map_err(GatewayClientError::from),
None => Err(GatewayClientError::ConnectionAbruptlyClosed),
}
}
@@ -39,7 +39,7 @@ pub(crate) fn cleanup_socket_messages(
match msgs {
Some(msgs) => msgs
.into_iter()
.map(|msg| msg.map_err(GatewayClientError::NetworkError))
.map(|msg| msg.map_err(GatewayClientError::from))
.collect(),
None => Err(GatewayClientError::ConnectionAbruptlyClosed),
}
+1 -2
View File
@@ -48,8 +48,7 @@ pub mod nym_config {
log::trace!("Loading from file: {:#?}", filepath.as_ref().to_owned());
let config_contents = fs::read_to_string(filepath)?;
toml::from_str(&config_contents)
.map_err(|toml_err| io::Error::new(io::ErrorKind::Other, toml_err))
toml::from_str(&config_contents).map_err(io::Error::other)
}
}
}
+1 -2
View File
@@ -151,8 +151,7 @@ where
let content = fs::read_to_string(path)?;
// TODO: should we be preserving original error type instead?
deserialize_config_from_toml_str(&content)
.map_err(|toml_err| io::Error::new(io::ErrorKind::Other, toml_err))
deserialize_config_from_toml_str(&content).map_err(io::Error::other)
}
//
+68 -48
View File
@@ -138,7 +138,12 @@
pub use reqwest::{IntoUrl, StatusCode};
use crate::path::RequestPath;
use async_trait::async_trait;
use bytes::Bytes;
use http::header::CONTENT_TYPE;
use http::HeaderMap;
use mime::Mime;
use reqwest::header::HeaderValue;
use reqwest::{RequestBuilder, Response};
use serde::de::DeserializeOwned;
@@ -149,10 +154,6 @@ use thiserror::Error;
use tracing::{debug, instrument, warn};
use url::Url;
use bytes::Bytes;
use http::header::CONTENT_TYPE;
use http::HeaderMap;
use mime::Mime;
#[cfg(not(target_arch = "wasm32"))]
use std::net::SocketAddr;
#[cfg(not(target_arch = "wasm32"))]
@@ -163,6 +164,8 @@ pub use user_agent::UserAgent;
#[cfg(not(target_arch = "wasm32"))]
mod dns;
mod path;
#[cfg(not(target_arch = "wasm32"))]
pub use dns::{HickoryDnsError, HickoryDnsResolver};
@@ -454,14 +457,15 @@ impl Client {
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait ApiClientCore {
/// Create an HTTP request using the host configured in this client.
fn create_request<B, K, V>(
fn create_request<P, B, K, V>(
&self,
method: reqwest::Method,
path: PathSegments<'_>,
path: P,
params: Params<'_, K, V>,
json_body: Option<&B>,
) -> RequestBuilder
where
P: RequestPath,
B: Serialize + ?Sized,
K: AsRef<str>,
V: AsRef<str>;
@@ -512,7 +516,7 @@ pub trait ApiClientCore {
};
let params: Vec<(String, String)> = standin_url.query_pairs().into_owned().collect();
self.create_request(method, &path, &params, json_body)
self.create_request(method, path.as_slice(), &params, json_body)
}
/// Send a created HTTP request.
@@ -525,14 +529,15 @@ pub trait ApiClientCore {
E: Display;
/// Create and send a created HTTP request.
async fn send_request<B, K, V, E>(
async fn send_request<P, B, K, V, E>(
&self,
method: reqwest::Method,
path: PathSegments<'_>,
path: P,
params: Params<'_, K, V>,
json_body: Option<&B>,
) -> Result<Response, HttpClientError<E>>
where
P: RequestPath + Send + Sync,
B: Serialize + ?Sized + Sync,
K: AsRef<str> + Sync,
V: AsRef<str> + Sync,
@@ -547,14 +552,15 @@ pub trait ApiClientCore {
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl ApiClientCore for Client {
#[instrument(level = "debug", skip_all, fields(path=?path))]
fn create_request<B, K, V>(
fn create_request<P, B, K, V>(
&self,
method: reqwest::Method,
path: PathSegments<'_>,
path: P,
params: Params<'_, K, V>,
json_body: Option<&B>,
) -> RequestBuilder
where
P: RequestPath,
B: Serialize + ?Sized,
K: AsRef<str>,
V: AsRef<str>,
@@ -597,12 +603,9 @@ impl ApiClientCore for Client {
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait ApiClient: ApiClientCore {
/// Create an HTTP GET Request with the provided path and parameters
fn create_get_request<K, V>(
&self,
path: PathSegments<'_>,
params: Params<'_, K, V>,
) -> RequestBuilder
fn create_get_request<P, K, V>(&self, path: P, params: Params<'_, K, V>) -> RequestBuilder
where
P: RequestPath,
K: AsRef<str>,
V: AsRef<str>,
{
@@ -610,13 +613,14 @@ pub trait ApiClient: ApiClientCore {
}
/// Create an HTTP POST Request with the provided path, parameters, and json body
fn create_post_request<B, K, V>(
fn create_post_request<P, B, K, V>(
&self,
path: PathSegments<'_>,
path: P,
params: Params<'_, K, V>,
json_body: &B,
) -> RequestBuilder
where
P: RequestPath,
B: Serialize + ?Sized,
K: AsRef<str>,
V: AsRef<str>,
@@ -625,12 +629,9 @@ pub trait ApiClient: ApiClientCore {
}
/// Create an HTTP DELETE Request with the provided path and parameters
fn create_delete_request<K, V>(
&self,
path: PathSegments<'_>,
params: Params<'_, K, V>,
) -> RequestBuilder
fn create_delete_request<P, K, V>(&self, path: P, params: Params<'_, K, V>) -> RequestBuilder
where
P: RequestPath,
K: AsRef<str>,
V: AsRef<str>,
{
@@ -638,13 +639,14 @@ pub trait ApiClient: ApiClientCore {
}
/// Create an HTTP PATCH Request with the provided path, parameters, and json body
fn create_patch_request<B, K, V>(
fn create_patch_request<P, B, K, V>(
&self,
path: PathSegments<'_>,
path: P,
params: Params<'_, K, V>,
json_body: &B,
) -> RequestBuilder
where
P: RequestPath,
B: Serialize + ?Sized,
K: AsRef<str>,
V: AsRef<str>,
@@ -654,12 +656,13 @@ pub trait ApiClient: ApiClientCore {
/// Create and send an HTTP GET Request with the provided path and parameters
#[instrument(level = "debug", skip_all, fields(path=?path))]
async fn send_get_request<K, V, E>(
async fn send_get_request<P, K, V, E>(
&self,
path: PathSegments<'_>,
path: P,
params: Params<'_, K, V>,
) -> Result<Response, HttpClientError<E>>
where
P: RequestPath + Send + Sync,
K: AsRef<str> + Sync,
V: AsRef<str> + Sync,
E: Display,
@@ -669,13 +672,14 @@ pub trait ApiClient: ApiClientCore {
}
/// Create and send an HTTP POST Request with the provided path, parameters, and json data
async fn send_post_request<B, K, V, E>(
async fn send_post_request<P, B, K, V, E>(
&self,
path: PathSegments<'_>,
path: P,
params: Params<'_, K, V>,
json_body: &B,
) -> Result<Response, HttpClientError<E>>
where
P: RequestPath + Send + Sync,
B: Serialize + ?Sized + Sync,
K: AsRef<str> + Sync,
V: AsRef<str> + Sync,
@@ -686,12 +690,13 @@ pub trait ApiClient: ApiClientCore {
}
/// Create and send an HTTP DELETE Request with the provided path and parameters
async fn send_delete_request<K, V, E>(
async fn send_delete_request<P, K, V, E>(
&self,
path: PathSegments<'_>,
path: P,
params: Params<'_, K, V>,
) -> Result<Response, HttpClientError<E>>
where
P: RequestPath + Send + Sync,
K: AsRef<str> + Sync,
V: AsRef<str> + Sync,
E: Display,
@@ -701,13 +706,14 @@ pub trait ApiClient: ApiClientCore {
}
/// Create and send an HTTP PATCH Request with the provided path, parameters, and json data
async fn send_patch_request<B, K, V, E>(
async fn send_patch_request<P, B, K, V, E>(
&self,
path: PathSegments<'_>,
path: P,
params: Params<'_, K, V>,
json_body: &B,
) -> Result<Response, HttpClientError<E>>
where
P: RequestPath + Send + Sync,
B: Serialize + ?Sized + Sync,
K: AsRef<str> + Sync,
V: AsRef<str> + Sync,
@@ -722,12 +728,13 @@ pub trait ApiClient: ApiClientCore {
/// into the provided type `T`.
#[instrument(level = "debug", skip_all, fields(path=?path))]
// TODO: deprecate in favour of get_response that works based on mime type in the response
async fn get_json<T, K, V, E>(
async fn get_json<P, T, K, V, E>(
&self,
path: PathSegments<'_>,
path: P,
params: Params<'_, K, V>,
) -> Result<T, HttpClientError<E>>
where
P: RequestPath + Send + Sync,
for<'a> T: Deserialize<'a>,
K: AsRef<str> + Sync,
V: AsRef<str> + Sync,
@@ -739,12 +746,13 @@ pub trait ApiClient: ApiClientCore {
/// 'get' data from the segment-defined path, e.g. `["api", "v1", "mixnodes"]`, with tuple
/// defined key-value parameters, e.g. `[("since", "12345")]`. Attempt to parse the response
/// into the provided type `T` based on the content type header
async fn get_response<T, K, V, E>(
async fn get_response<P, T, K, V, E>(
&self,
path: PathSegments<'_>,
path: P,
params: Params<'_, K, V>,
) -> Result<T, HttpClientError<E>>
where
P: RequestPath + Send + Sync,
for<'a> T: Deserialize<'a>,
K: AsRef<str> + Sync,
V: AsRef<str> + Sync,
@@ -759,13 +767,14 @@ pub trait ApiClient: ApiClientCore {
/// 'post' json data to the segment-defined path, e.g. `["api", "v1", "mixnodes"]`, with tuple
/// defined key-value parameters, e.g. `[("since", "12345")]`. Attempt to parse the response
/// into the provided type `T`.
async fn post_json<B, T, K, V, E>(
async fn post_json<P, B, T, K, V, E>(
&self,
path: PathSegments<'_>,
path: P,
params: Params<'_, K, V>,
json_body: &B,
) -> Result<T, HttpClientError<E>>
where
P: RequestPath + Send + Sync,
B: Serialize + ?Sized + Sync,
for<'a> T: Deserialize<'a>,
K: AsRef<str> + Sync,
@@ -781,12 +790,13 @@ pub trait ApiClient: ApiClientCore {
/// 'delete' json data from the segment-defined path, e.g. `["api", "v1", "mixnodes"]`, with
/// tuple defined key-value parameters, e.g. `[("since", "12345")]`. Attempt to parse the
/// response into the provided type `T`.
async fn delete_json<T, K, V, E>(
async fn delete_json<P, T, K, V, E>(
&self,
path: PathSegments<'_>,
path: P,
params: Params<'_, K, V>,
) -> Result<T, HttpClientError<E>>
where
P: RequestPath + Send + Sync,
for<'a> T: Deserialize<'a>,
K: AsRef<str> + Sync,
V: AsRef<str> + Sync,
@@ -801,13 +811,14 @@ pub trait ApiClient: ApiClientCore {
/// 'patch' json data at the segment-defined path, e.g. `["api", "v1", "mixnodes"]`, with tuple
/// defined key-value parameters, e.g. `[("since", "12345")]`. Attempt to parse the response
/// into the provided type `T`.
async fn patch_json<B, T, K, V, E>(
async fn patch_json<P, B, T, K, V, E>(
&self,
path: PathSegments<'_>,
path: P,
params: Params<'_, K, V>,
json_body: &B,
) -> Result<T, HttpClientError<E>>
where
P: RequestPath + Send + Sync,
B: Serialize + ?Sized + Sync,
for<'a> T: Deserialize<'a>,
K: AsRef<str> + Sync,
@@ -890,7 +901,7 @@ impl<C> ApiClient for C where C: ApiClientCore + Sync {}
/// utility function that should solve the double slash problem in API urls forever.
fn sanitize_url<K: AsRef<str>, V: AsRef<str>>(
base: &Url,
segments: PathSegments<'_>,
request_path: impl RequestPath,
params: Params<'_, K, V>,
) -> Url {
let mut url = base.clone();
@@ -900,10 +911,7 @@ fn sanitize_url<K: AsRef<str>, V: AsRef<str>>(
path_segments.pop_if_empty();
for segment in segments {
let segment = segment.strip_prefix('/').unwrap_or(segment);
let segment = segment.strip_suffix('/').unwrap_or(segment);
for segment in request_path.to_sanitized_segments() {
path_segments.push(segment);
}
@@ -1048,6 +1056,18 @@ mod tests {
fn sanitizing_urls() {
let base_url: Url = "http://foomp.com".parse().unwrap();
// works with a full string
assert_eq!(
"http://foomp.com/foo/bar",
sanitize_url(&base_url, "/foo//bar/", NO_PARAMS).as_str()
);
// (and leading slash doesn't matter)
assert_eq!(
"http://foomp.com/foo/bar",
sanitize_url(&base_url, "foo//bar/", NO_PARAMS).as_str()
);
// works with 1 segment
assert_eq!(
"http://foomp.com/foo",
+59
View File
@@ -0,0 +1,59 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use std::fmt::Debug;
/// Collection of URL Path Segments
pub type PathSegments<'a> = &'a [&'a str];
fn sanitize_fragment(segment: &str) -> &str {
segment.trim_matches(|c: char| c.is_whitespace() || c == '/')
}
pub trait RequestPath: Debug {
fn to_sanitized_segments(&self) -> Vec<&str>;
}
macro_rules! impl_stringified_sanitized_segments {
($frag_iter:expr) => {{
let mut path_segments = Vec::new();
for segment in $frag_iter {
if !segment.is_empty() {
path_segments.push(sanitize_fragment(segment));
}
}
path_segments
}};
}
impl RequestPath for PathSegments<'_> {
fn to_sanitized_segments(&self) -> Vec<&str> {
impl_stringified_sanitized_segments!(self.iter())
}
}
impl<const N: usize> RequestPath for &[&str; N] {
fn to_sanitized_segments(&self) -> Vec<&str> {
impl_stringified_sanitized_segments!(self.iter())
}
}
impl RequestPath for &str {
fn to_sanitized_segments(&self) -> Vec<&str> {
impl_stringified_sanitized_segments!(self.split('/'))
}
}
impl RequestPath for String {
fn to_sanitized_segments(&self) -> Vec<&str> {
impl_stringified_sanitized_segments!(self.split('/'))
}
}
impl RequestPath for &String {
fn to_sanitized_segments(&self) -> Vec<&str> {
impl_stringified_sanitized_segments!(self.split('/'))
}
}
+8 -8
View File
@@ -26,54 +26,54 @@ pub enum ScraperError {
WebSocketConnectionFailure {
url: String,
#[source]
source: tendermint_rpc::Error,
source: Box<tendermint_rpc::Error>,
},
#[error("failed to establish rpc connection to {url}: {source}")]
HttpConnectionFailure {
url: String,
#[source]
source: tendermint_rpc::Error,
source: Box<tendermint_rpc::Error>,
},
#[error("failed to create chain subscription: {source}")]
ChainSubscriptionFailure {
#[source]
source: tendermint_rpc::Error,
source: Box<tendermint_rpc::Error>,
},
#[error("could not obtain basic block information at height: {height}: {source}")]
BlockQueryFailure {
height: u32,
#[source]
source: tendermint_rpc::Error,
source: Box<tendermint_rpc::Error>,
},
#[error("could not obtain block results information at height: {height}: {source}")]
BlockResultsQueryFailure {
height: u32,
#[source]
source: tendermint_rpc::Error,
source: Box<tendermint_rpc::Error>,
},
#[error("could not obtain validators information at height: {height}: {source}")]
ValidatorsQueryFailure {
height: u32,
#[source]
source: tendermint_rpc::Error,
source: Box<tendermint_rpc::Error>,
},
#[error("could not obtain tx results for tx: {hash}: {source}")]
TxResultsQueryFailure {
hash: Hash,
#[source]
source: tendermint_rpc::Error,
source: Box<tendermint_rpc::Error>,
},
#[error("could not obtain current abci info: {source}")]
AbciInfoQueryFailure {
#[source]
source: tendermint_rpc::Error,
source: Box<tendermint_rpc::Error>,
},
#[error("could not parse tx {hash}: {source}")]
+30 -18
View File
@@ -29,7 +29,7 @@ impl RpcClient {
let http_client = HttpClient::new(url.as_str()).map_err(|source| {
ScraperError::HttpConnectionFailure {
url: url.to_string(),
source,
source: Box::new(source),
}
})?;
@@ -90,7 +90,10 @@ impl RpcClient {
self.inner
.block(height)
.await
.map_err(|source| ScraperError::BlockQueryFailure { height, source })
.map_err(|source| ScraperError::BlockQueryFailure {
height,
source: Box::new(source),
})
}
#[instrument(skip(self), err(Display))]
@@ -100,31 +103,37 @@ impl RpcClient {
) -> Result<block_results::Response, ScraperError> {
debug!("getting block results");
self.inner
.block_results(height)
.await
.map_err(|source| ScraperError::BlockResultsQueryFailure { height, source })
self.inner.block_results(height).await.map_err(|source| {
ScraperError::BlockResultsQueryFailure {
height,
source: Box::new(source),
}
})
}
pub(crate) async fn current_block_height(&self) -> Result<u64, ScraperError> {
debug!("getting current block height");
let info = self
.inner
.abci_info()
.await
.map_err(|source| ScraperError::AbciInfoQueryFailure { source })?;
let info =
self.inner
.abci_info()
.await
.map_err(|source| ScraperError::AbciInfoQueryFailure {
source: Box::new(source),
})?;
Ok(info.last_block_height.value())
}
pub(crate) async fn earliest_available_block_height(&self) -> Result<u64, ScraperError> {
debug!("getting earliest available block height");
let status = self
.inner
.status()
.await
.map_err(|source| ScraperError::AbciInfoQueryFailure { source })?;
let status =
self.inner
.status()
.await
.map_err(|source| ScraperError::AbciInfoQueryFailure {
source: Box::new(source),
})?;
Ok(status.sync_info.earliest_block_height.value())
}
@@ -167,7 +176,7 @@ impl RpcClient {
.await
.map_err(|source| ScraperError::TxResultsQueryFailure {
hash: tx_hash,
source,
source: Box::new(source),
})
}
@@ -181,6 +190,9 @@ impl RpcClient {
self.inner
.validators(height, Paging::All)
.await
.map_err(|source| ScraperError::ValidatorsQueryFailure { height, source })
.map_err(|source| ScraperError::ValidatorsQueryFailure {
height,
source: Box::new(source),
})
}
}
@@ -42,7 +42,7 @@ impl ChainSubscriber {
let websocket_url = websocket_endpoint.as_str().try_into().map_err(|source| {
ScraperError::WebSocketConnectionFailure {
url: websocket_endpoint.to_string(),
source,
source: Box::new(source),
}
})?;
@@ -52,7 +52,7 @@ impl ChainSubscriber {
.await
.map_err(|source| ScraperError::WebSocketConnectionFailure {
url: websocket_endpoint.to_string(),
source,
source: Box::new(source),
})?;
Ok(ChainSubscriber {
@@ -83,7 +83,7 @@ impl ChainSubscriber {
.await
.map_err(|source| ScraperError::WebSocketConnectionFailure {
url: self.websocket_endpoint.to_string(),
source,
source: Box::new(source),
})?;
self.websocket_client = client;
self.websocket_driver = Some(driver);
@@ -121,7 +121,9 @@ impl ChainSubscriber {
.websocket_client
.subscribe(EventType::NewBlock.into())
.await
.map_err(|source| ScraperError::ChainSubscriptionFailure { source })?;
.map_err(|source| ScraperError::ChainSubscriptionFailure {
source: Box::new(source),
})?;
let mut failures = 0;
+6 -9
View File
@@ -54,14 +54,11 @@ where
let key_pem = read_pem_file(path)?;
if T::pem_type() != key_pem.tag {
return Err(io::Error::new(
io::ErrorKind::Other,
format!(
"unexpected key pem tag. Got '{}', expected: '{}'",
key_pem.tag,
T::pem_type()
),
));
return Err(io::Error::other(format!(
"unexpected key pem tag. Got '{}', expected: '{}'",
key_pem.tag,
T::pem_type()
)));
}
let key = match T::from_bytes(&key_pem.contents) {
@@ -84,7 +81,7 @@ fn read_pem_file<P: AsRef<Path>>(filepath: P) -> io::Result<Pem> {
let mut pem_bytes = File::open(filepath)?;
let mut buf = Vec::new();
pem_bytes.read_to_end(&mut buf)?;
pem::parse(&buf).map_err(|e| io::Error::new(io::ErrorKind::Other, e))
pem::parse(&buf).map_err(io::Error::other)
}
fn write_pem_file<P: AsRef<Path>>(filepath: P, data: Vec<u8>, tag: &str) -> io::Result<()> {
+6
View File
@@ -28,5 +28,11 @@ nym-credentials-interface = { path = "../credentials-interface" }
nym-metrics = { path = "../nym-metrics" }
nym-task = { path = "../task" }
utoipa = { workspace = true, optional = true }
[target."cfg(target_arch = \"wasm32\")".dependencies.wasmtimer]
workspace = true
[features]
default = []
openapi = ["dep:utoipa"]
+1 -1
View File
@@ -1,7 +1,7 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::report::{ClientStatsReport, OsInformation};
use crate::report::client::{ClientStatsReport, OsInformation};
use nym_task::TaskClient;
use time::{OffsetDateTime, Time};
+5
View File
@@ -26,11 +26,16 @@ pub mod report;
pub mod types;
const CLIENT_ID_PREFIX: &str = "client_stats_id";
const VPN_CLIENT_ID_PREFIX: &str = "vpnclient_stats_id";
pub fn generate_client_stats_id(id_key: ed25519::PublicKey) -> String {
generate_stats_id(CLIENT_ID_PREFIX, id_key.to_base58_string())
}
pub fn generate_vpn_client_stats_id<M: AsRef<[u8]>>(seed: M) -> String {
generate_stats_id(VPN_CLIENT_ID_PREFIX, seed)
}
fn generate_stats_id<M: AsRef<[u8]>>(prefix: &str, id_seed: M) -> String {
let mut hasher = sha2::Sha256::new();
hasher.update(prefix);
@@ -6,7 +6,7 @@ use crate::clients::{
nym_api_statistics::NymApiStats, packet_statistics::PacketStatistics,
};
use super::error::StatsError;
use crate::error::StatsError;
use serde::{Deserialize, Serialize};
use sysinfo::System;
+5
View File
@@ -0,0 +1,5 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
pub mod client;
pub mod vpn_client;
@@ -0,0 +1,51 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use serde::{Deserialize, Serialize};
const KIND: &str = "vpn_client_stats_report";
const VERSION: &str = "v1";
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VpnClientStatsReport {
pub kind: String,
pub api_version: String,
pub stats_id: String,
pub static_information: StaticInformationReport,
//SW called it basic so we can swap it easily down the line for more data
pub basic_usage: Option<UsageReport>,
}
impl VpnClientStatsReport {
pub fn new(stats_id: String, static_information: StaticInformationReport) -> Self {
VpnClientStatsReport {
kind: KIND.into(),
api_version: VERSION.into(),
stats_id,
static_information,
basic_usage: None,
}
}
#[must_use]
pub fn with_usage_report(mut self, usage_report: UsageReport) -> Self {
self.basic_usage = Some(usage_report);
self
}
}
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StaticInformationReport {
pub os_type: String,
pub os_version: Option<String>,
pub os_arch: String,
pub app_version: String,
}
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UsageReport {
pub connection_time_ms: Option<i32>,
pub two_hop: bool,
}
+2 -4
View File
@@ -25,10 +25,8 @@ fn map_ws_error(err: WebSocketError) -> WsError {
// TODO: are we preserving correct semantics?
WebSocketError::ConnectionError => WsError::ConnectionClosed,
WebSocketError::ConnectionClose(_event) => WsError::ConnectionClosed,
WebSocketError::MessageSendError(err) => {
WsError::Io(io::Error::new(io::ErrorKind::Other, err.to_string()))
}
_ => WsError::Io(io::Error::new(io::ErrorKind::Other, "new websocket error")),
WebSocketError::MessageSendError(err) => WsError::Io(io::Error::other(err.to_string())),
_ => WsError::Io(io::Error::other("new websocket error")),
}
}
+46 -40
View File
@@ -13,12 +13,10 @@ use nym_gateway_storage::models::WireguardPeer;
use nym_task::TaskClient;
use nym_wireguard_types::DEFAULT_PEER_TIMEOUT_CHECK;
use std::sync::Arc;
use std::time::{Duration, SystemTime};
use tokio::sync::{mpsc, RwLock};
use tokio_stream::{wrappers::IntervalStream, StreamExt};
pub(crate) type SharedBandwidthStorageManager = Arc<RwLock<BandwidthStorageManager>>;
const AUTO_REMOVE_AFTER: Duration = Duration::from_secs(60 * 60); // 1 hour
pub struct PeerHandle {
public_key: Key,
@@ -28,7 +26,6 @@ pub struct PeerHandle {
request_tx: mpsc::Sender<PeerControlRequest>,
timeout_check_interval: IntervalStream,
task_client: TaskClient,
startup_timestamp: SystemTime,
}
impl PeerHandle {
@@ -53,7 +50,6 @@ impl PeerHandle {
request_tx,
timeout_check_interval,
task_client,
startup_timestamp: SystemTime::now(),
}
}
@@ -73,26 +69,49 @@ impl PeerHandle {
Ok(success)
}
async fn active_peer(
&mut self,
storage_peer: &WireguardPeer,
kernel_peer: &Peer,
) -> Result<bool, Error> {
if let Some(bandwidth_manager) = &self.bandwidth_storage_manager {
if kernel_peer.last_handshake.is_none()
&& SystemTime::now().duration_since(self.startup_timestamp)? >= AUTO_REMOVE_AFTER
{
let success = self.remove_peer().await?;
self.peer_storage_manager.remove_peer();
tracing::debug!(
"Peer {} has not been active for more then {} seconds, removing it",
kernel_peer.public_key.to_string(),
AUTO_REMOVE_AFTER.as_secs()
fn compute_spent_bandwidth(kernel_peer: &Peer, storage_peer: &WireguardPeer) -> Option<u64> {
let storage_peer_rx_bytes = u64::try_from(storage_peer.rx_bytes)
.inspect_err(|e| tracing::error!("Storage rx bytes could not be converted: {e}"))
.ok()?;
let storage_peer_tx_bytes = u64::try_from(storage_peer.tx_bytes)
.inspect_err(|e| tracing::error!("Storage tx bytes could not be converted: {e}"))
.ok()?;
let kernel_total = kernel_peer
.rx_bytes
.checked_add(kernel_peer.tx_bytes)
.or_else(|| {
tracing::error!(
"Overflow on kernel adding bytes: {} + {}",
kernel_peer.rx_bytes,
kernel_peer.tx_bytes
);
return Ok(!success);
}
let spent_bandwidth = (kernel_peer.rx_bytes + kernel_peer.tx_bytes)
.checked_sub(storage_peer.rx_bytes as u64 + storage_peer.tx_bytes as u64)
None
})?;
let storage_total = storage_peer_rx_bytes
.checked_add(storage_peer_tx_bytes)
.or_else(|| {
tracing::error!("Overflow on storage adding bytes: {storage_peer_rx_bytes} + {storage_peer_tx_bytes}");
None
})?;
kernel_total.checked_sub(storage_total).or_else(|| {
tracing::error!("Overflow on spent bandwidth subtraction: kernel - storage = {kernel_total} - {storage_total}");
None
})
}
async fn active_peer(&mut self, kernel_peer: &Peer) -> Result<bool, Error> {
let Some(storage_peer) = self.peer_storage_manager.get_peer() else {
log::debug!(
"Peer {:?} not in storage anymore, shutting down handle",
self.public_key
);
return Ok(false);
};
if let Some(bandwidth_manager) = &self.bandwidth_storage_manager {
let spent_bandwidth = Self::compute_spent_bandwidth(kernel_peer, &storage_peer)
.unwrap_or_else(|| {
// if gateway restarted, the kernel values restart from 0
// and we should restart from 0 in storage as well
@@ -100,8 +119,10 @@ impl PeerHandle {
self.peer_storage_manager.peer_information.as_mut()
{
peer_information.force_sync = true;
peer_information.peer.rx_bytes = kernel_peer.rx_bytes;
peer_information.peer.tx_bytes = kernel_peer.tx_bytes;
}
kernel_peer.rx_bytes + kernel_peer.tx_bytes
0
})
.try_into()
.map_err(|_| Error::InconsistentConsumedBytes)?;
@@ -124,14 +145,6 @@ impl PeerHandle {
}
}
} else {
if SystemTime::now().duration_since(self.startup_timestamp)? >= AUTO_REMOVE_AFTER {
log::debug!(
"Peer {} has been present for 30 days, removing it",
self.public_key
);
let success = self.remove_peer().await?;
return Ok(!success);
}
let spent_bandwidth = kernel_peer.rx_bytes + kernel_peer.tx_bytes;
if spent_bandwidth >= BANDWIDTH_CAP_PER_DAY {
log::debug!(
@@ -158,14 +171,7 @@ impl PeerHandle {
// the host information hasn't beed updated yet
return Ok(true);
};
let Some(storage_peer) = self.peer_storage_manager.get_peer() else {
log::debug!(
"Peer {:?} not in storage anymore, shutting down handle",
self.public_key
);
return Ok(false);
};
if !self.active_peer(&storage_peer, &kernel_peer).await? {
if !self.active_peer(&kernel_peer).await? {
log::debug!(
"Peer {:?} is not active anymore, shutting down handle",
self.public_key
+24
View File
@@ -0,0 +1,24 @@
// @ts-check
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
basePath: "/explorer",
assetPrefix: "/explorer",
trailingSlash: false,
async redirects() {
return [
// Change the basePath to /explorer
{
source: "/",
destination: "/explorer",
basePath: false,
permanent: true,
},
];
},
};
module.exports = nextConfig
-23
View File
@@ -1,23 +0,0 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
reactStrictMode: true,
basePath: "/explorer",
assetPrefix: "/explorer",
trailingSlash: false,
async redirects() {
return [
// Change the basePath to /explorer
{
source: "/",
destination: "/explorer",
basePath: false,
permanent: true,
},
];
},
};
export default nextConfig;
+1 -1
View File
@@ -50,7 +50,7 @@ nym-api-requests = { path = "../nym-api/nym-api-requests" }
nym-credentials = { path = "../common/credentials" }
nym-credentials-interface = { path = "../common/credentials-interface" }
nym-credential-verification = { path = "../common/credential-verification" }
nym-crypto = { path = "../common/crypto" }
nym-crypto = { path = "../common/crypto", features = ["sphinx"] }
nym-gateway-storage = { path = "../common/gateway-storage" }
nym-gateway-stats-storage = { path = "../common/gateway-stats-storage" }
nym-gateway-requests = { path = "../common/gateway-requests" }
+9 -4
View File
@@ -69,10 +69,7 @@ pub enum GatewayError {
},
#[error("there was an issue with the local authenticator: {source}")]
AuthenticatorFailure {
#[from]
source: AuthenticatorError,
},
AuthenticatorFailure { source: Box<AuthenticatorError> },
#[error("failed to startup local {typ}")]
ServiceProviderStartupFailure { typ: &'static str },
@@ -138,3 +135,11 @@ impl From<ClientCoreError> for GatewayError {
}
}
}
impl From<AuthenticatorError> for GatewayError {
fn from(error: AuthenticatorError) -> Self {
GatewayError::AuthenticatorFailure {
source: Box::new(error),
}
}
}
@@ -22,7 +22,7 @@ enum ActiveClient {
Remote(RemoteClientData),
/// Handle to a locally (inside the same process) running client.
Embedded(LocalEmbeddedClientHandle),
Embedded(Box<LocalEmbeddedClientHandle>),
}
impl ActiveClient {
@@ -163,7 +163,7 @@ impl ActiveClientsStore {
/// Inserts a handle to the embedded client
pub fn insert_embedded(&self, local_client_handle: LocalEmbeddedClientHandle) {
let key = local_client_handle.client_destination();
let entry = ActiveClient::Embedded(local_client_handle);
let entry = ActiveClient::Embedded(Box::new(local_client_handle));
if self.inner.insert(key, entry).is_some() {
// this is literally impossible since we're starting the local embedded client before
// even spawning the websocket listener task
@@ -83,7 +83,7 @@ pub(crate) enum InitialAuthenticationError {
UnexpectedMessageType { typ: String },
#[error("Experienced connection error: {0}")]
ConnectionError(#[from] WsError),
ConnectionError(Box<WsError>),
#[error("Attempted to negotiate connection with client using incompatible protocol version. Ours is {current} and the client reports {client:?}")]
IncompatibleProtocol { client: Option<u8>, current: u8 },
@@ -91,7 +91,7 @@ pub(crate) enum InitialAuthenticationError {
#[error("failed to send authentication response: {source}")]
ResponseSendFailure {
#[source]
source: WsError,
source: Box<WsError>,
},
#[error("possibly received a sphinx packet without prior authentication. Request is going to be ignored")]
@@ -103,7 +103,7 @@ pub(crate) enum InitialAuthenticationError {
#[error("failed to obtain message from websocket stream: {source}")]
FailedToReadMessage {
#[source]
source: WsError,
source: Box<WsError>,
},
#[error("timed out while waiting for initial data")]
@@ -113,6 +113,12 @@ pub(crate) enum InitialAuthenticationError {
EmptyClientDetails,
}
impl From<WsError> for InitialAuthenticationError {
fn from(error: WsError) -> Self {
InitialAuthenticationError::ConnectionError(Box::new(error))
}
}
pub(crate) struct FreshHandler<R, S> {
rng: R,
pub(crate) shared_state: CommonHandlerState,
@@ -163,7 +169,7 @@ impl<R, S> FreshHandler<R, S> {
SocketStream::RawTcp(conn) => {
// TODO: perhaps in the future, rather than panic here (and uncleanly shut tcp stream)
// return a result with an error?
let ws_stream = tokio_tungstenite::accept_async(conn).await?;
let ws_stream = Box::new(tokio_tungstenite::accept_async(conn).await?);
SocketStream::UpgradedWebSocket(ws_stream)
}
other => other,
@@ -342,7 +348,7 @@ impl<R, S> FreshHandler<R, S> {
// push them to the client
if let Err(err) = self.push_packets_to_client(shared_keys, messages).await {
warn!("We failed to send stored messages to fresh client - {err}",);
return Err(InitialAuthenticationError::ConnectionError(err));
return Err(InitialAuthenticationError::ConnectionError(Box::new(err)));
} else {
// if it was successful - remove them from the store
self.shared_state.storage.remove_messages(ids).await?;
@@ -880,7 +886,9 @@ impl<R, S> FreshHandler<R, S> {
.await
{
debug!("failed to send authentication response: {source}");
return Err(InitialAuthenticationError::ResponseSendFailure { source });
return Err(InitialAuthenticationError::ResponseSendFailure {
source: Box::new(source),
});
}
let Some(client_details) = auth_result.client_details else {
@@ -963,7 +971,9 @@ impl<R, S> FreshHandler<R, S> {
Ok(Some(Ok(msg))) => msg,
Ok(Some(Err(source))) => {
debug!("failed to obtain message from websocket stream! stopping connection handler: {source}");
return Err(InitialAuthenticationError::FailedToReadMessage { source });
return Err(InitialAuthenticationError::FailedToReadMessage {
source: Box::new(source),
});
}
Ok(None) => return Err(InitialAuthenticationError::ClosedConnection),
Err(_timeout) => return Err(InitialAuthenticationError::Timeout),
@@ -31,7 +31,7 @@ const INITIAL_MESSAGE_TIMEOUT: Duration = Duration::from_millis(10_000);
pub(crate) enum SocketStream<S> {
RawTcp(S),
UpgradedWebSocket(WebSocketStream<S>),
UpgradedWebSocket(Box<WebSocketStream<S>>),
Invalid,
}
+1 -1
View File
@@ -4,7 +4,7 @@
[package]
name = "nym-api"
license = "GPL-3.0"
version = "1.1.57"
version = "1.1.60"
authors.workspace = true
edition = "2021"
rust-version.workspace = true
+1 -1
View File
@@ -16,7 +16,7 @@ pub enum DkgError {
StatePersistenceFailure {
path: PathBuf,
#[source]
source: EcashError,
source: Box<EcashError>,
},
#[error("failed to query for the current DKG epoch state: {source}")]
+1 -1
View File
@@ -73,7 +73,7 @@ impl<R: RngCore + CryptoRng + Clone> DkgController<R> {
persistent_state.save_to_file(save_path).map_err(|source| {
DkgError::StatePersistenceFailure {
path: save_path.to_path_buf(),
source,
source: Box::new(source),
}
})
}
+1 -1
View File
@@ -1275,7 +1275,7 @@ impl TestFixture {
AppState {
nyxd_client,
chain_status_cache: ChainStatusCache::new(Duration::from_secs(42)),
address_info_cache: AddressInfoCache::new(),
address_info_cache: AddressInfoCache::new(Duration::from_secs(42), 1000),
forced_refresh: ForcedRefresh::new(true),
nym_contract_cache: NymContractCache::new(),
node_status_cache: NodeStatusCache::new(),
+13 -1
View File
@@ -102,6 +102,15 @@ pub(crate) struct Args {
#[clap(long)]
pub(crate) bind_address: Option<SocketAddr>,
/// account/address cache TTL: should be lower than epoch length (1 hour)
/// because, at worst, data will be stale for <epoch_length> + <cache_ttl> seconds
#[clap(long, env = "ADDRESS_CACHE_REFRESH_INTERVAL_S")]
pub(crate) address_cache_ttl_seconds: Option<u64>,
/// number of addresses that are cached on account/address endpoint
#[clap(long, env = "ADDRESS_CACHE_CAPACITY")]
pub(crate) address_cache_capacity: Option<u64>,
#[clap(hide = true, long, default_value_t = false)]
pub(crate) allow_illegal_ips: bool,
}
@@ -194,7 +203,10 @@ async fn start_nym_api_tasks_axum(config: &Config) -> anyhow::Result<ShutdownHan
let router = router.with_state(AppState {
nyxd_client: nyxd_client.clone(),
chain_status_cache: ChainStatusCache::new(DEFAULT_CHAIN_STATUS_CACHE_TTL),
address_info_cache: AddressInfoCache::new(),
address_info_cache: AddressInfoCache::new(
config.address_cache.time_to_live,
config.address_cache.capacity,
),
forced_refresh: ForcedRefresh::new(
config.topology_cacher.debug.node_describe_allow_illegal_ips,
),
+28
View File
@@ -54,6 +54,9 @@ const DEFAULT_TOPOLOGY_CACHE_INTERVAL: Duration = Duration::from_secs(30);
const DEFAULT_NODE_STATUS_CACHE_INTERVAL: Duration = Duration::from_secs(120);
const DEFAULT_CIRCULATING_SUPPLY_CACHE_INTERVAL: Duration = Duration::from_secs(3600);
pub(crate) const DEFAULT_ADDRESS_CACHE_TTL: Duration = Duration::from_secs(60 * 15);
pub(crate) const DEFAULT_ADDRESS_CACHE_CAPACITY: u64 = 1000;
pub(crate) const DEFAULT_NODE_DESCRIBE_CACHE_INTERVAL: Duration = Duration::from_secs(4500);
pub(crate) const DEFAULT_NODE_DESCRIBE_BATCH_SIZE: usize = 50;
@@ -111,6 +114,9 @@ pub struct Config {
#[serde(alias = "coconut_signer")]
pub ecash_signer: EcashSigner,
#[serde(skip)]
pub address_cache: AddressCacheConfig,
}
impl NymConfigTemplate for Config {
@@ -130,6 +136,7 @@ impl Config {
circulating_supply_cacher: Default::default(),
rewarding: Default::default(),
ecash_signer: EcashSigner::new_default(id.as_ref()),
address_cache: Default::default(),
}
}
@@ -179,6 +186,12 @@ impl Config {
if args.allow_illegal_ips {
self.topology_cacher.debug.node_describe_allow_illegal_ips = true
}
if let Some(address_cache_ttl) = args.address_cache_ttl {
self.address_cache.time_to_live = address_cache_ttl;
}
if let Some(address_cache_capacity) = args.address_cache_capacity {
self.address_cache.capacity = address_cache_capacity;
}
self
}
@@ -292,6 +305,21 @@ impl Base {
}
}
#[derive(Debug, PartialEq, Eq)]
pub struct AddressCacheConfig {
pub time_to_live: Duration,
pub capacity: u64,
}
impl Default for AddressCacheConfig {
fn default() -> Self {
Self {
time_to_live: DEFAULT_ADDRESS_CACHE_TTL,
capacity: DEFAULT_ADDRESS_CACHE_CAPACITY,
}
}
}
// this got separated into 2 structs so that we could have a sane `default` implementation for the latter
#[derive(Debug, Deserialize, PartialEq, Eq, Serialize)]
pub struct NetworkMonitor {
+9 -1
View File
@@ -2,7 +2,7 @@
// SPDX-License-Identifier: GPL-3.0-only
use crate::support::cli::{init, run};
use std::net::SocketAddr;
use std::{net::SocketAddr, time::Duration};
// Configuration that can be overridden.
pub(crate) struct OverrideConfig {
@@ -32,6 +32,9 @@ pub(crate) struct OverrideConfig {
/// default: `127.0.0.1:8080` in `debug` builds and `0.0.0.0:8080` in `release`
pub(crate) bind_address: Option<SocketAddr>,
pub(crate) address_cache_ttl: Option<Duration>,
pub(crate) address_cache_capacity: Option<u64>,
pub(crate) allow_illegal_ips: bool,
}
@@ -46,6 +49,9 @@ impl From<init::Args> for OverrideConfig {
announce_address: args.announce_address,
monitor_credentials_mode: Some(args.monitor_credentials_mode),
bind_address: args.bind_address,
// irrelevant for --init command because we set the value in --run
address_cache_ttl: None,
address_cache_capacity: None,
allow_illegal_ips: args.allow_illegal_ips,
}
}
@@ -62,6 +68,8 @@ impl From<run::Args> for OverrideConfig {
announce_address: args.announce_address,
monitor_credentials_mode: args.monitor_credentials_mode,
bind_address: args.bind_address,
address_cache_ttl: args.address_cache_ttl_seconds.map(Duration::from_secs),
address_cache_capacity: args.address_cache_capacity,
allow_illegal_ips: args.allow_illegal_ips,
}
}
+7 -13
View File
@@ -17,17 +17,11 @@ pub(crate) struct AddressInfoCache {
}
impl AddressInfoCache {
pub(crate) fn new() -> Self {
// epoch duration = 1 hour
// cache TTL is slightly lower than that to avoid too stale data in case
// cache was refreshed JUST BEFORE epoch transition
let cache_ttl = Duration::from_secs(60 * 30);
let max_capacity = 1000;
pub(crate) fn new(cache_ttl: Duration, capacity: u64) -> Self {
AddressInfoCache {
inner: Cache::builder()
.time_to_live(cache_ttl)
.max_capacity(max_capacity)
.max_capacity(capacity)
.build(),
}
}
@@ -108,13 +102,13 @@ impl AddressInfoCache {
address: account_id.to_string(),
balance: balance.into(),
delegations: delegation_data
.delegations()
.into_iter()
.map(|d| NyxAccountDelegationDetails {
delegated: d.amount,
height: d.height,
node_id: d.node_id,
proxy: d.proxy,
delegated: d.details().amount.clone(),
height: d.details().height,
node_id: d.details().node_id,
proxy: d.details().proxy.clone(),
node_bonded: d.is_node_bonded(),
})
.collect(),
accumulated_rewards,
@@ -8,10 +8,9 @@ use crate::{
};
use cosmwasm_std::{Coin, Decimal};
use nym_mixnet_contract_common::NodeRewarding;
use nym_topology::NodeId;
use nym_validator_client::nyxd::AccountId;
use std::collections::{HashMap, HashSet};
use tracing::warn;
use tracing::error;
pub(crate) struct AddressDataCollector {
nyxd_client: crate::nyxd::Client,
@@ -56,18 +55,18 @@ impl AddressDataCollector {
Ok(balance)
}
pub(crate) async fn get_delegations(&mut self) -> AxumResult<AddressDelegationInfo> {
pub(crate) async fn get_delegations(&mut self) -> AxumResult<Vec<AddressDelegationInfo>> {
let og_delegations = self
.nyxd_client
.get_all_delegator_delegations(&self.account_id)
.await?;
.await?
.into_iter()
.map(|delegation| (delegation.node_id, delegation))
.collect::<HashMap<_, _>>();
let delegated_to_nodes = og_delegations
.iter()
.map(|d| d.node_id)
.collect::<HashSet<_>>();
let mut node_delegation_info = Vec::new();
let nym_nodes = self
let delegated_to_nodes_bonded = self
.nym_contract_cache
.all_cached_nym_nodes()
.await
@@ -85,61 +84,75 @@ impl AddressDataCollector {
// add to totals
self.total_value += pending_operator_reward;
}
if delegated_to_nodes.contains(&node_details.node_id()) {
Some((
node_details.node_id(),
// avoid cloning node data which we don't need
(
node_details.rewarding_details.clone(),
node_details.is_unbonding(),
),
))
if let Some(delegation) = og_delegations.get(&node_details.node_id()) {
node_delegation_info.push(AddressDelegationInfo {
details: delegation.clone(),
node_reward_info: NodeBondStatus::Bonded {
rewarding_info: node_details.rewarding_details.to_owned(),
unbonding: node_details.is_unbonding(),
},
});
Some(node_details.node_id())
} else {
None
}
})
.collect::<HashMap<_, _>>();
.collect::<HashSet<_>>();
Ok(AddressDelegationInfo {
delegations: og_delegations,
delegated_to_nodes: nym_nodes,
})
for (node_id, delegation) in og_delegations {
if !delegated_to_nodes_bonded.contains(&node_id) {
node_delegation_info.push(AddressDelegationInfo {
details: delegation.clone(),
node_reward_info: NodeBondStatus::UnBonded,
});
}
}
Ok(node_delegation_info)
}
pub(crate) async fn calculate_rewards(
&mut self,
delegation_data: &AddressDelegationInfo,
delegation_data: &Vec<AddressDelegationInfo>,
) -> AxumResult<Vec<NyxAccountDelegationRewardDetails>> {
let mut accumulated_rewards = Vec::new();
for delegation in delegation_data.delegations.iter() {
let node_id = &delegation.node_id;
for delegation in delegation_data {
let node_id = delegation.details.node_id;
if let Some((rewarding_details, is_unbonding)) =
delegation_data.delegated_to_nodes.get(node_id)
{
match rewarding_details.determine_delegation_reward(delegation) {
Ok(delegation_reward) => {
let reward = NyxAccountDelegationRewardDetails {
node_id: delegation.node_id,
rewards: decimal_to_coin(delegation_reward, &self.base_denom),
amount_staked: delegation.amount.clone(),
node_still_fully_bonded: !is_unbonding,
};
// 4. sum the rewards and delegations
self.total_delegations += delegation.amount.amount.u128();
self.total_value += delegation.amount.amount.u128();
self.total_value += reward.rewards.amount.u128();
self.claimable_rewards += reward.rewards.amount.u128();
match &delegation.node_reward_info {
NodeBondStatus::Bonded {
rewarding_info,
unbonding,
} => {
match rewarding_info.determine_delegation_reward(&delegation.details) {
Ok(delegation_reward) => {
let reward = NyxAccountDelegationRewardDetails {
node_id,
rewards: decimal_to_coin(delegation_reward, &self.base_denom),
amount_staked: delegation.details.amount.clone(),
node_still_fully_bonded: !unbonding,
};
// 4. sum the rewards and delegations
self.total_delegations += delegation.details.amount.amount.u128();
self.total_value += delegation.details.amount.amount.u128();
self.total_value += reward.rewards.amount.u128();
self.claimable_rewards += reward.rewards.amount.u128();
accumulated_rewards.push(reward);
}
Err(err) => {
warn!(
"Couldn't determine delegations for {} on node {}: {}",
&self.account_id, node_id, err
)
accumulated_rewards.push(reward);
}
Err(err) => {
error!(
"Couldn't determine delegations for {} on node {}: {}",
&self.account_id, node_id, err
)
}
}
}
NodeBondStatus::UnBonded => {
// directory cache doesn't store node details required to
// calculate rewarding for unbonded nodes
}
}
}
@@ -171,17 +184,27 @@ impl AddressDataCollector {
}
pub(crate) struct AddressDelegationInfo {
delegations: Vec<nym_mixnet_contract_common::Delegation>,
delegated_to_nodes: HashMap<NodeId, RewardAndBondInfo>,
details: nym_mixnet_contract_common::Delegation,
node_reward_info: NodeBondStatus,
}
impl AddressDelegationInfo {
pub(crate) fn delegations(self) -> Vec<nym_mixnet_contract_common::Delegation> {
self.delegations
pub(crate) fn details(&self) -> &nym_mixnet_contract_common::Delegation {
&self.details
}
pub(crate) fn is_node_bonded(&self) -> bool {
matches!(self.node_reward_info, NodeBondStatus::Bonded { .. })
}
}
type RewardAndBondInfo = (NodeRewarding, bool);
pub(crate) enum NodeBondStatus {
Bonded {
rewarding_info: NodeRewarding,
unbonding: bool,
},
UnBonded,
}
fn decimal_to_coin(decimal: Decimal, denom: impl Into<String>) -> Coin {
Coin::new(decimal.to_uint_floor(), denom)
@@ -189,6 +212,7 @@ fn decimal_to_coin(decimal: Decimal, denom: impl Into<String>) -> Coin {
#[cfg(test)]
mod test {
use super::*;
#[tokio::test]
+4
View File
@@ -21,6 +21,7 @@ pub struct NyxAccountDelegationDetails {
pub height: u64,
#[schema(value_type = Option<String>)]
pub proxy: Option<Addr>,
pub node_bonded: bool,
}
#[derive(Clone, Debug, Serialize, Deserialize, utoipa::ToSchema, utoipa::ToResponse)]
@@ -41,6 +42,9 @@ pub struct NyxAccountDetails {
#[schema(value_type = CoinSchema)]
pub total_value: Coin,
pub delegations: Vec<NyxAccountDelegationDetails>,
/// Shows rewards from delegations to **currently** bonded nodes.
/// Rewards from nodes that user delegated to, but were later unbonded,
/// are claimable, but not shown here.
pub accumulated_rewards: Vec<NyxAccountDelegationRewardDetails>,
#[schema(value_type = String)]
pub total_delegations: Coin,
+2 -2
View File
@@ -3,7 +3,7 @@
[package]
name = "nym-node"
version = "1.11.0"
version = "1.13.0"
authors.workspace = true
repository.workspace = true
homepage.workspace = true
@@ -84,7 +84,7 @@ tower-http = { workspace = true, features = ["fs"] }
utoipa = { workspace = true, features = ["axum_extras", "time"] }
utoipa-swagger-ui = { workspace = true, features = ["axum"] }
nym-http-api-common = { path = "../common/http-api-common", features = ["utoipa"] }
nym-http-api-common = { path = "../common/http-api-common", features = ["utoipa", "output", "middleware"] }
nym-node-requests = { path = "nym-node-requests", default-features = false, features = ["openapi"] }
nym-node-metrics = { path = "nym-node-metrics" }
+21 -3
View File
@@ -158,13 +158,13 @@ pub enum NymNodeError {
BloomfilterIoFailure { source: io::Error, path: PathBuf },
#[error(transparent)]
GatewayFailure(#[from] nym_gateway::GatewayError),
GatewayFailure(Box<nym_gateway::GatewayError>),
#[error(transparent)]
GatewayTasksStartupFailure(Box<dyn std::error::Error + Send + Sync>),
#[error(transparent)]
EntryGatewayFailure(#[from] EntryGatewayError),
EntryGatewayFailure(Box<EntryGatewayError>),
#[error(transparent)]
ServiceProvidersFailure(#[from] ServiceProvidersError),
@@ -177,6 +177,18 @@ pub enum NymNodeError {
FailedUpgrade,
}
impl From<EntryGatewayError> for NymNodeError {
fn from(error: EntryGatewayError) -> Self {
NymNodeError::EntryGatewayFailure(Box::new(error))
}
}
impl From<nym_gateway::GatewayError> for NymNodeError {
fn from(error: nym_gateway::GatewayError) -> Self {
NymNodeError::GatewayFailure(Box::new(error))
}
}
impl NymNodeError {
pub fn config_validation_failure<S: Into<String>>(error: S) -> Self {
NymNodeError::ConfigValidationFailure {
@@ -219,7 +231,13 @@ pub enum EntryGatewayError {
},
#[error("entry gateway failure: {0}")]
External(#[from] nym_gateway::GatewayError),
External(Box<nym_gateway::GatewayError>),
}
impl From<nym_gateway::GatewayError> for EntryGatewayError {
fn from(error: nym_gateway::GatewayError) -> Self {
EntryGatewayError::External(Box::new(error))
}
}
#[derive(Debug, Error)]
@@ -0,0 +1,21 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO active_device (\n day,\n device_id,\n os_type,\n os_version,\n architecture,\n app_version,\n user_agent,\n from_mixnet)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8)\n ON CONFLICT (device_id, day) DO NOTHING",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Date",
"Text",
"Text",
"Text",
"Text",
"Text",
"Text",
"Bool"
]
},
"nullable": []
},
"hash": "13bf07e42c49ea365e816eb94e4e4f607989ee95f68a0fcd95bc4a53f4e79cbb"
}
@@ -0,0 +1,19 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO connection_stats (\n received_at,\n connection_time_ms,\n two_hop,\n source_ip,\n country_code,\n from_mixnet) VALUES ($1::timestamptz, $2, $3, $4, $5, $6)",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Timestamptz",
"Int4",
"Bool",
"Text",
"Text",
"Bool"
]
},
"nullable": []
},
"hash": "dce9f3dae7ae0dc5953d1f69e843bea9553fb05a2f656cfff6598f12a21c99ba"
}
+55
View File
@@ -0,0 +1,55 @@
# Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
# SPDX-License-Identifier: Apache-2.0
[package]
name = "nym-statistics-api"
version = "0.1.0"
authors.workspace = true
repository.workspace = true
homepage.workspace = true
documentation.workspace = true
edition.workspace = true
license.workspace = true
rust-version.workspace = true
[dependencies]
anyhow.workspace = true
axum = { workspace = true, features = ["tokio", "macros"] }
axum-extra = { workspace = true, features = ["typed-header"] }
celes.workspace = true
clap = { workspace = true, features = ["cargo", "derive", "env", "string"] }
serde = { workspace = true, features = ["derive"] }
serde_json.workspace = true
sqlx = { workspace = true, features = [
"runtime-tokio-rustls",
"postgres",
"time",
] }
time.workspace = true
tokio = { workspace = true, features = ["rt-multi-thread"] }
tokio-util.workspace = true
tracing.workspace = true
tracing-subscriber = { workspace = true, features = ["env-filter"] }
tower-http = { workspace = true, features = ["cors", "trace"] }
url.workspace = true
utoipa = { workspace = true, features = ["axum_extras", "time"] }
utoipa-swagger-ui = { workspace = true, features = ["axum"] }
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",
] }
nym-statistics-common = { path = "../common/statistics", features = [
"openapi",
] }
nym-task = { path = "../common/task" }
nym-validator-client = { path = "../common/client-libs/validator-client" }
[build-dependencies]
anyhow = { workspace = true }
tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }
sqlx = { workspace = true, features = ["runtime-tokio-rustls", "postgres"] }
+28
View File
@@ -0,0 +1,28 @@
# Nym-statistics-api
A simple API to collect and store statistics sent by nym-vpn-client.
## Build instructions
The statistics API is backed by a PostgreSQL database so you'll need a PostgreSQL server running if you want to add migrations or add/modify SQL queries. I recommend https://postgresapp.com on MacOS, very easy to use. If you're on another OS, it's up to you.
Assuming your database is running at `postgresql://user:password@host:port/database_name` you'll likely need to run the following :
```bash
DATABASE_URL="postgresql://user:password@host:port/database_name"
# if you don't have an existing datase
sqlx database create --database-url $DATABASE_URL
sqlx migrate run --database-url $DATABASE_URL
# reset it if you messed with migrations while developping
sqlx database reset --database-url $DATABASE_URL
# or just run new migrations
sqlx migrate run --database-url $DATABASE_URL
# then prepare queries for offline build mode
cargo sqlx prepare --database-url $DATABASE_URL
```
This should allow `cargo build` without having any postgreSQL server running.
Be sure to add the `.sqlx` directory to version control
@@ -0,0 +1,24 @@
CREATE TABLE active_device (
day DATE NOT NULL,
device_id TEXT NOT NULL,
os_type TEXT,
os_version TEXT,
architecture TEXT,
app_version TEXT,
user_agent TEXT,
from_mixnet BOOLEAN,
PRIMARY KEY (device_id, day)
);
CREATE TABLE connection_stats (
received_at TIMESTAMP WITH TIME ZONE NOT NULL,
connection_time_ms INTEGER,
two_hop BOOLEAN,
source_ip TEXT,
country_code TEXT,
from_mixnet BOOLEAN
);
CREATE INDEX idx_active_device_day ON active_device (day);
+30
View File
@@ -0,0 +1,30 @@
#!/bin/bash
set -e
export PGUSER="nym"
export PGPASSWORD="password1"
export PGPORT="5432"
export DB_NAME="nym_statistics_api"
export DATABASE_URL="postgres://${PGUSER}:${PGPASSWORD}@localhost:${PGPORT}/${DB_NAME}"
cat <<EOF > .env
SQLX_OFFLINE=true
POSTGRES_USER=$PGUSER
POSTGRES_PASSWORD=$PGPASSWORD
PGPORT=$PGPORT
DB_NAME=$DB_NAME
DATABASE_URL=$DATABASE_URL
EOF
docker run --rm -it \
--name ${DB_NAME} \
-e POSTGRES_USER=${PGUSER} \
-e POSTGRES_PASSWORD=${PGPASSWORD} \
-e POSTGRES_DB=${DB_NAME} \
-p ${PGPORT}:${PGPORT} \
postgres
# sqlx migrate run
# cargo sqlx prepare
+38
View File
@@ -0,0 +1,38 @@
use clap::Parser;
use nym_bin_common::bin_info;
use std::sync::OnceLock;
use url::Url;
// Helper for passing LONG_VERSION to clap
fn pretty_build_info_static() -> &'static str {
static PRETTY_BUILD_INFORMATION: OnceLock<String> = OnceLock::new();
PRETTY_BUILD_INFORMATION.get_or_init(|| bin_info!().pretty_print())
}
#[derive(Clone, Debug, Parser)]
#[clap(author = "Nymtech", version, long_version = pretty_build_info_static(), about)]
pub(crate) struct Cli {
/// URL for the NYM API to get a network view from
#[clap(long, env = "NYM_API_URL")]
pub(crate) nym_api_url: Option<Url>,
/// HTTP port on which to run statistics api.
#[clap(long, default_value_t = 8000, env = "NYM_STATISTICS_API_HTTP_PORT")]
pub(crate) http_port: u16,
/// Connection url for the database.
#[clap(long, env = "DATABASE_URL")]
pub(crate) database_url: String,
/// Username for the database.
#[clap(long, env = "POSTGRES_USER")]
pub(crate) username: String,
/// Password for the database.
#[clap(long, env = "POSTGRES_PASSWORD")]
pub(crate) password: String,
/// PgSQL port for the database.
#[clap(long, default_value_t = 5432, env = "PGPORT")]
pub(crate) pg_port: u16,
}
+74
View File
@@ -0,0 +1,74 @@
use anyhow::anyhow;
use axum::{response::Redirect, Router};
use nym_http_api_common::middleware::logging::log_request_info;
use tokio::net::ToSocketAddrs;
use tower_http::cors::CorsLayer;
use utoipa::OpenApi;
use utoipa_swagger_ui::SwaggerUi;
use crate::http::{server::HttpServer, state::AppState};
pub(crate) mod stats;
pub(crate) struct RouterBuilder {
unfinished_router: Router<AppState>,
}
impl RouterBuilder {
pub(crate) fn with_default_routes() -> Self {
let router = Router::new()
.merge(
SwaggerUi::new("/swagger")
.url("/api-docs/openapi.json", super::api_docs::ApiDoc::openapi()),
)
.route(
"/",
axum::routing::get(|| async { Redirect::permanent("/swagger") }),
)
.nest("/v1", Router::new().nest("/stats", stats::routes()));
Self {
unfinished_router: router,
}
}
pub(crate) fn with_state(self, state: AppState) -> RouterWithState {
RouterWithState {
router: self.finalize_routes().with_state(state),
}
}
fn finalize_routes(self) -> Router<AppState> {
// layers added later wrap earlier layers
self.unfinished_router
// CORS layer needs to wrap other API layers
.layer(setup_cors())
// logger should be outermost layer
.layer(axum::middleware::from_fn(log_request_info))
}
}
pub(crate) struct RouterWithState {
router: Router,
}
impl RouterWithState {
pub(crate) async fn build_server<A: ToSocketAddrs>(
self,
bind_address: A,
) -> anyhow::Result<HttpServer> {
tokio::net::TcpListener::bind(bind_address)
.await
.map(|listener| HttpServer::new(self.router, listener))
.map_err(|err| anyhow!("Couldn't bind to address due to {}", err))
}
}
fn setup_cors() -> CorsLayer {
use axum::http::Method;
CorsLayer::new()
.allow_origin(tower_http::cors::Any)
.allow_methods([Method::POST, Method::GET, Method::PATCH, Method::OPTIONS])
.allow_headers(tower_http::cors::Any)
.allow_credentials(false)
}
+63
View File
@@ -0,0 +1,63 @@
use std::net::SocketAddr;
use axum::{
extract::{ConnectInfo, State},
Json, Router,
};
use axum_extra::{headers::UserAgent, TypedHeader};
use nym_statistics_common::report::vpn_client::VpnClientStatsReport;
use tracing::debug;
use crate::{
http::{
error::{HttpError, HttpResult},
state::AppState,
},
storage::models::{ConnectionInfoDto, DailyActiveDeviceDto},
};
pub(crate) fn routes() -> Router<AppState> {
Router::new().route("/report", axum::routing::post(submit_stats_report))
}
#[utoipa::path(
post,
request_body = VpnClientStatsReport,
tag = "Stats",
path = "/report",
context_path = "/v1/stats",
responses(
(status = 200)
)
)]
#[tracing::instrument(level = "info", skip_all)]
async fn submit_stats_report(
State(mut state): State<AppState>,
ConnectInfo(addr): ConnectInfo<SocketAddr>,
TypedHeader(user_agent): TypedHeader<UserAgent>,
Json(report): Json<VpnClientStatsReport>,
) -> HttpResult<Json<()>> {
let now = time::OffsetDateTime::now_utc();
let gateway_record = state.network_view().get_country_by_ip(&addr.ip()).await;
let from_mixnet = gateway_record.is_some();
let maybe_location = gateway_record.unwrap_or_default();
if from_mixnet {
debug!("Received a report from the network");
} else {
debug!("Received a report from outside of the network");
}
let active_device = DailyActiveDeviceDto::new(now, &report, user_agent, from_mixnet);
let maybe_connection_info =
ConnectionInfoDto::maybe_new(now, &report, addr, maybe_location, from_mixnet);
state
.storage()
.store_vpn_client_report(active_device, maybe_connection_info)
.await
.map_err(HttpError::internal_with_logging)?;
Ok(Json(()))
}
+10
View File
@@ -0,0 +1,10 @@
use utoipa::OpenApi;
use utoipauto::utoipauto;
// manually import external structs which are behind feature flags because they
// can't be automatically discovered
// https://github.com/ProbablyClem/utoipauto/issues/13#issuecomment-1974911829
#[utoipauto(paths = "./nym-statistics-api/src")]
#[derive(OpenApi)]
#[openapi(info(title = "Nym Statistics API"), tags(), components(schemas()))]
pub(super) struct ApiDoc;
+28
View File
@@ -0,0 +1,28 @@
use std::fmt::Display;
pub(crate) type HttpResult<T> = Result<T, HttpError>;
pub(crate) struct HttpError {
message: String,
status: axum::http::StatusCode,
}
impl HttpError {
pub(crate) fn internal_with_logging(msg: impl Display) -> Self {
tracing::error!("{}", msg.to_string());
Self::internal()
}
pub(crate) fn internal() -> Self {
Self {
message: serde_json::json!({"message": "Internal server error"}).to_string(),
status: axum::http::StatusCode::INTERNAL_SERVER_ERROR,
}
}
}
impl axum::response::IntoResponse for HttpError {
fn into_response(self) -> axum::response::Response {
(self.status, self.message).into_response()
}
}
+5
View File
@@ -0,0 +1,5 @@
pub(crate) mod api;
pub(crate) mod api_docs;
pub(crate) mod error;
pub(crate) mod server;
pub(crate) mod state;
+48
View File
@@ -0,0 +1,48 @@
use axum::Router;
use core::net::SocketAddr;
use nym_task::ShutdownToken;
use tokio::net::TcpListener;
use crate::{
http::{api::RouterBuilder, state::AppState},
network_view::NetworkView,
storage::StatisticsStorage,
};
pub(crate) async fn build_http_api(
storage: StatisticsStorage,
cached_network: NetworkView,
http_port: u16,
) -> anyhow::Result<HttpServer> {
let router_builder = RouterBuilder::with_default_routes();
let state = AppState::new(storage, cached_network).await;
let router = router_builder.with_state(state);
let bind_addr = format!("0.0.0.0:{}", http_port);
tracing::info!("Binding server to {bind_addr}");
router.build_server(bind_addr).await
}
pub(crate) struct HttpServer {
router: Router,
listener: TcpListener,
}
impl HttpServer {
pub(crate) fn new(router: Router, listener: TcpListener) -> Self {
Self { router, listener }
}
pub(crate) async fn run(self, shutdown_token: ShutdownToken) -> std::io::Result<()> {
// into_make_service_with_connect_info allows us to see client ip address
axum::serve(
self.listener,
self.router
.into_make_service_with_connect_info::<SocketAddr>(),
)
.with_graceful_shutdown(async move { shutdown_token.cancelled().await })
.await
}
}
+24
View File
@@ -0,0 +1,24 @@
use crate::{network_view::NetworkView, storage::StatisticsStorage};
#[derive(Debug, Clone)]
pub(crate) struct AppState {
storage_manager: StatisticsStorage,
network_view: NetworkView,
}
impl AppState {
pub(crate) async fn new(storage_manager: StatisticsStorage, network_view: NetworkView) -> Self {
Self {
storage_manager,
network_view,
}
}
pub(crate) fn storage(&mut self) -> &mut StatisticsStorage {
&mut self.storage_manager
}
pub(crate) fn network_view(&self) -> &NetworkView {
&self.network_view
}
}
+44
View File
@@ -0,0 +1,44 @@
use tracing::level_filters::LevelFilter;
use tracing_subscriber::{filter::Directive, EnvFilter};
pub(crate) fn setup_tracing_logger() -> anyhow::Result<()> {
fn directive_checked(directive: impl Into<String>) -> anyhow::Result<Directive> {
directive.into().parse().map_err(From::from)
}
let log_builder = tracing_subscriber::fmt()
// Use a more compact, abbreviated log format
.compact()
// Display source code file paths
.with_file(true)
// Display source code line numbers
.with_line_number(true)
.with_thread_ids(true)
// Don't display the event's target (module path)
.with_target(false);
let mut filter = EnvFilter::builder()
// if RUST_LOG isn't set, set default level
.with_default_directive(LevelFilter::DEBUG.into())
.from_env_lossy();
// these crates are more granularly filtered
let warn_crates = [
"rustls",
"sqlx",
"tower_http",
"axum",
"reqwest",
"hyper_util",
];
for crate_name in warn_crates {
filter = filter.add_directive(directive_checked(format!("{}=warn", crate_name))?);
}
let log_level_hint = filter.max_level_hint();
log_builder.with_env_filter(filter).init();
tracing::info!("Log level: {:?}", log_level_hint);
Ok(())
}
+53
View File
@@ -0,0 +1,53 @@
use clap::Parser;
use network_view::NetworkRefresher;
use nym_task::ShutdownManager;
mod cli;
mod http;
mod logging;
mod network_view;
mod storage;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
logging::setup_tracing_logger()?;
let args = cli::Cli::parse();
let connection_url = args.database_url.clone();
tracing::debug!("Using config:\n{:#?}", args);
let storage = storage::StatisticsStorage::init(
connection_url,
args.username,
args.password,
args.pg_port,
)
.await?;
tracing::info!("Connection to database successful");
let shutdown_manager = ShutdownManager::new("nym-statistics-api");
let network_refresher = NetworkRefresher::initialise_new(
args.nym_api_url,
shutdown_manager.child_token("network-refresher"),
)
.await?;
let http_server =
http::server::build_http_api(storage, network_refresher.network_view(), args.http_port)
.await
.expect("Failed to build http server");
let server_shutdown = shutdown_manager.clone_token("http-api-server");
// Starting tasks
shutdown_manager.spawn(async move { http_server.run(server_shutdown).await });
network_refresher.start();
tracing::info!("Started HTTP server on port {}", args.http_port);
shutdown_manager.close();
shutdown_manager.wait_for_shutdown_signal().await;
Ok(())
}
+152
View File
@@ -0,0 +1,152 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use anyhow::Result;
use nym_task::ShutdownToken;
use celes::Country;
use nym_validator_client::models::NymNodeDescription;
use nym_validator_client::NymApiClient;
use std::collections::HashMap;
use std::time::Duration;
use std::{net::IpAddr, sync::Arc};
use tokio::sync::RwLock;
use tokio::time::interval;
use url::Url;
use tracing::{error, info, trace, warn};
const NETWORK_CACHE_TTL: Duration = Duration::from_secs(600);
type IpToCountryMap = HashMap<IpAddr, Option<Country>>;
// SW this should use a proper NS API client once it exists
struct NodesQuerier {
client: NymApiClient,
}
impl NodesQuerier {
async fn current_nymnodes(&self) -> Result<Vec<NymNodeDescription>> {
Ok(self
.client
.get_all_described_nodes()
.await
.inspect_err(|err| error!("failed to get network nodes: {err}"))?)
}
}
#[derive(Clone, Debug)]
pub(crate) struct NetworkView {
inner: Arc<RwLock<NetworkViewInner>>,
}
impl NetworkView {
fn new_empty() -> Self {
NetworkView {
inner: Arc::new(RwLock::new(NetworkViewInner {
network_nodes: HashMap::new(),
})),
}
}
pub(crate) async fn get_country_by_ip(&self, ip_addr: &IpAddr) -> Option<Option<Country>> {
self.inner.read().await.network_nodes.get(ip_addr).copied()
}
}
#[derive(Debug)]
struct NetworkViewInner {
network_nodes: IpToCountryMap,
}
pub struct NetworkRefresher {
querier: Option<NodesQuerier>,
full_refresh_interval: Duration,
shutdown_token: ShutdownToken,
network: NetworkView,
}
impl NetworkRefresher {
pub(crate) async fn initialise_new(
maybe_nym_api_url: Option<Url>,
shutdown_token: ShutdownToken,
) -> Result<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(),
}),
None => {
warn!("No Nym API specified, network view is unavailable");
None
}
};
let mut this = NetworkRefresher {
querier: node_querier,
full_refresh_interval: NETWORK_CACHE_TTL,
shutdown_token,
network: NetworkView::new_empty(),
};
this.refresh_network_nodes().await?;
Ok(this)
}
async fn refresh_network_nodes(&mut self) -> Result<()> {
if let Some(querier) = &self.querier {
let nodes = querier.current_nymnodes().await?;
// collect all known/allowed nodes information
let known_nodes = nodes
.iter()
.flat_map(|n| {
n.description
.host_information
.ip_address
.clone()
.into_iter()
.zip(std::iter::repeat(n.description.auxiliary_details.location))
})
.collect::<HashMap<_, _>>();
let mut network_guard = self.network.inner.write().await;
network_guard.network_nodes = known_nodes;
}
Ok(())
}
pub(crate) fn network_view(&self) -> NetworkView {
self.network.clone()
}
pub(crate) async fn run(&mut self) {
info!("NetworkRefresher started successfully");
let mut full_refresh_interval = interval(self.full_refresh_interval);
full_refresh_interval.reset();
while !self.shutdown_token.is_cancelled() {
tokio::select! {
biased;
_ = self.shutdown_token.cancelled() => {
trace!("NetworkRefresher: Received shutdown");
}
_ = full_refresh_interval.tick() => {
if self.refresh_network_nodes().await.is_err() {
warn!("Failed to refresh network nodes, we're gonna keep the same set");
}
}
}
}
trace!("NetworkRefresher: Exiting");
}
pub(crate) fn start(mut self) {
tokio::spawn(async move { self.run().await });
}
}
+102
View File
@@ -0,0 +1,102 @@
use anyhow::{anyhow, Result};
use models::{ConnectionInfoDto, DailyActiveDeviceDto};
use sqlx::{migrate::Migrator, postgres::PgConnectOptions};
use std::str::FromStr;
pub(crate) mod models;
pub(crate) type DbPool = sqlx::PgPool;
static MIGRATOR: Migrator = sqlx::migrate!("./migrations");
#[derive(Debug, Clone)]
pub(crate) struct StatisticsStorage {
connection_pool: DbPool,
}
impl StatisticsStorage {
pub async fn init(
connection_url: String,
user: String,
password: String,
port: u16,
) -> Result<Self> {
let connect_options = PgConnectOptions::from_str(&connection_url)?
.port(port)
.username(&user)
.password(&password)
.application_name(nym_bin_common::bin_info!().binary_name);
let pool = sqlx::PgPool::connect_with(connect_options)
.await
.map_err(|err| anyhow!("Failed to connect to {}: {}", &connection_url, err))?;
MIGRATOR.run(&pool).await?;
Ok(StatisticsStorage {
connection_pool: pool,
})
}
pub(crate) async fn store_vpn_client_report(
&mut self,
active_device: DailyActiveDeviceDto,
connection_info: Option<ConnectionInfoDto>,
) -> Result<()> {
self.store_device(active_device).await?;
if let Some(connection_info) = connection_info {
self.store_connection_stats(connection_info).await?;
}
Ok(())
}
// Interestingly enough, because gateway-storage is using the `chrono` feature of sqlx and in 0.7.4 it takes priority over the `time` one, we cannot use the query! macro here.
// Due to features unification, the binary will not compile when built from the workspace root because it will expect `chrono` types.
// As a consequence, there is no compile time verification of these queries.
async fn store_device(&self, active_device: DailyActiveDeviceDto) -> Result<()> {
sqlx::query!(
r#"INSERT INTO active_device (
day,
device_id,
os_type,
os_version,
architecture,
app_version,
user_agent,
from_mixnet)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
ON CONFLICT (device_id, day) DO NOTHING"#,
active_device.day as time::Date,
active_device.stats_id,
active_device.os_type,
active_device.os_version,
active_device.os_arch,
active_device.app_version,
active_device.user_agent,
active_device.from_mixnet
)
.execute(&self.connection_pool)
.await?;
Ok(())
}
async fn store_connection_stats(&self, connection_info: ConnectionInfoDto) -> Result<()> {
sqlx::query!(
r#"INSERT INTO connection_stats (
received_at,
connection_time_ms,
two_hop,
source_ip,
country_code,
from_mixnet) VALUES ($1::timestamptz, $2, $3, $4, $5, $6)"#,
connection_info.received_at as time::OffsetDateTime,
connection_info.connection_time_ms,
connection_info.two_hop,
connection_info.received_from,
connection_info.country_code,
connection_info.from_mixnet
)
.execute(&self.connection_pool)
.await?;
Ok(())
}
}
+69
View File
@@ -0,0 +1,69 @@
use std::net::SocketAddr;
use axum_extra::headers::UserAgent;
use celes::Country;
use nym_statistics_common::report::vpn_client::VpnClientStatsReport;
use time::{Date, OffsetDateTime};
pub type StatsId = String;
#[derive(Debug, Clone, sqlx::FromRow)]
pub(crate) struct DailyActiveDeviceDto {
pub(crate) day: Date,
pub(crate) stats_id: StatsId,
pub(crate) os_type: String,
pub(crate) os_version: Option<String>,
pub(crate) os_arch: String,
pub(crate) app_version: String,
pub(crate) user_agent: String,
pub(crate) from_mixnet: bool,
}
impl DailyActiveDeviceDto {
pub(crate) fn new(
received_at: OffsetDateTime,
stats_report: &VpnClientStatsReport,
user_agent: UserAgent,
from_mixnet: bool,
) -> Self {
Self {
day: received_at.date(),
stats_id: stats_report.stats_id.clone(),
os_type: stats_report.static_information.os_type.clone(),
os_version: stats_report.static_information.os_version.clone(),
os_arch: stats_report.static_information.os_arch.clone(),
app_version: stats_report.static_information.app_version.clone(),
user_agent: user_agent.to_string(),
from_mixnet,
}
}
}
#[derive(Debug, Clone, sqlx::FromRow)]
pub(crate) struct ConnectionInfoDto {
pub(crate) received_at: OffsetDateTime,
pub(crate) received_from: String,
pub(crate) connection_time_ms: Option<i32>,
pub(crate) two_hop: bool,
pub(crate) country_code: Option<String>,
pub(crate) from_mixnet: bool,
}
impl ConnectionInfoDto {
pub(crate) fn maybe_new(
received_at: OffsetDateTime,
stats_report: &VpnClientStatsReport,
received_from: SocketAddr,
maybe_country: Option<Country>,
from_mixnet: bool,
) -> Option<Self> {
stats_report.basic_usage.as_ref().map(|usage_report| Self {
received_at,
received_from: received_from.ip().to_string(),
connection_time_ms: usage_report.connection_time_ms,
two_hop: usage_report.two_hop,
country_code: maybe_country.map(|c| c.alpha2.into()),
from_mixnet,
})
}
}
@@ -0,0 +1,8 @@
/**
* This is a mock for Tauri's API package (@tauri-apps/api/app), to prevent stories from being excluded, because they either use
* or import dependencies that use Tauri.
*/
module.exports = {
invoke: () => undefined,
}
@@ -0,0 +1,4 @@
/**
* This is a mock for Tauri's API package (@tauri-apps/api/app), to prevent stories from being excluded, because they either use
* or import dependencies that use Tauri.
*/
+43
View File
@@ -415,11 +415,14 @@ version = "0.4.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59a194f9d963d8099596278594b3107448656ba73831c9d8c783e613ce86da64"
dependencies = [
"brotli",
"flate2",
"futures-core",
"memchr",
"pin-project-lite",
"tokio",
"zstd",
"zstd-safe",
]
[[package]]
@@ -940,6 +943,8 @@ version = "1.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fcb57c740ae1daf453ae85f16e37396f672b039e00d9d866e07ddb24e328e3a"
dependencies = [
"jobserver",
"libc",
"shlex",
]
@@ -3529,6 +3534,16 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
[[package]]
name = "jobserver"
version = "0.1.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a"
dependencies = [
"getrandom 0.3.2",
"libc",
]
[[package]]
name = "jpeg-decoder"
version = "0.3.1"
@@ -9270,6 +9285,34 @@ dependencies = [
"memchr",
]
[[package]]
name = "zstd"
version = "0.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a"
dependencies = [
"zstd-safe",
]
[[package]]
name = "zstd-safe"
version = "7.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d"
dependencies = [
"zstd-sys",
]
[[package]]
name = "zstd-sys"
version = "2.0.15+zstd.1.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb81183ddd97d0c74cedf1d50d85c8d08c1b8b68ee863bdee9e706eedba1a237"
dependencies = [
"cc",
"pkg-config",
]
[[package]]
name = "zvariant"
version = "5.4.0"
+4 -6
View File
@@ -142,7 +142,7 @@ impl Config {
let location = Self::config_file_path(None);
match toml::to_string_pretty(&global)
.map_err(|toml_err| io::Error::new(io::ErrorKind::Other, toml_err))
.map_err(io::Error::other)
.map(|toml| fs::write(location.clone(), toml))
{
Ok(_) => log::debug!("Writing to: {:#?}", location),
@@ -162,7 +162,7 @@ impl Config {
let location = Self::config_file_path(Some(network));
match toml::to_string_pretty(config)
.map_err(|toml_err| io::Error::new(io::ErrorKind::Other, toml_err))
.map_err(io::Error::other)
.map(|toml| fs::write(location.clone(), toml))
{
Ok(_) => log::debug!("Writing to: {:#?}", location),
@@ -367,10 +367,8 @@ fn load_from_file<T>(file: PathBuf) -> Result<T, io::Error>
where
T: DeserializeOwned,
{
fs::read_to_string(file).and_then(|contents| {
toml::from_str::<T>(&contents)
.map_err(|toml_err| io::Error::new(io::ErrorKind::Other, toml_err))
})
fs::read_to_string(file)
.and_then(|contents| toml::from_str::<T>(&contents).map_err(io::Error::other))
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
+4 -4
View File
@@ -32,7 +32,7 @@ pub enum BackendError {
NyxdError {
pretty_error: String,
#[source]
source: NyxdError,
source: Box<NyxdError>,
},
#[error(transparent)]
CosmwasmStd {
@@ -192,18 +192,18 @@ impl From<NyxdError> for BackendError {
if let Some(pretty_log) = pretty_log {
Self::NyxdError {
pretty_error: pretty_log.to_string(),
source,
source: Box::new(source),
}
} else {
Self::NyxdError {
pretty_error: source.to_string(),
source,
source: Box::new(source),
}
}
}
nyxd_error => Self::NyxdError {
pretty_error: nyxd_error.to_string(),
source: nyxd_error,
source: Box::new(nyxd_error),
},
}
}
@@ -68,7 +68,7 @@ impl Substream {
}
fn check_closed(mut self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Result<(), IoError> {
let closed_err = IoError::new(ErrorKind::Other, "stream closed");
let closed_err = IoError::other("stream closed");
// close_rx will return an error if the channel is closed (ie. sender was dropped),
// or if it's empty
@@ -172,12 +172,7 @@ impl AsyncWrite for Substream {
),
}),
})
.map_err(|e| {
IoError::new(
ErrorKind::Other,
format!("poll_write outbound_tx error: {}", e),
)
})?;
.map_err(|e| IoError::other(format!("poll_write outbound_tx error: {}", e)))?;
Poll::Ready(Ok(buf.len()))
}
@@ -195,7 +190,7 @@ impl AsyncWrite for Substream {
let mut closed = self.closed.lock();
if *closed {
return Poll::Ready(Err(IoError::new(ErrorKind::Other, "stream closed")));
return Poll::Ready(Err(IoError::other("stream closed")));
}
*closed = true;
@@ -210,12 +205,7 @@ impl AsyncWrite for Substream {
message: SubstreamMessage::new_close(self.substream_id.clone()),
}),
})
.map_err(|e| {
IoError::new(
ErrorKind::Other,
format!("poll_close outbound_rx error: {}", e),
)
})?;
.map_err(|e| IoError::other(format!("poll_close outbound_rx error: {}", e)))?;
Poll::Ready(Ok(()))
}
+8 -10
View File
@@ -212,19 +212,18 @@ where
cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<std::result::Result<usize, std::io::Error>> {
ready!(self.tx.poll_ready_unpin(cx)).map_err(|_| {
std::io::Error::new(std::io::ErrorKind::Other, "failed to send packet to mixnet")
})?;
ready!(self.tx.poll_ready_unpin(cx))
.map_err(|_| std::io::Error::other("failed to send packet to mixnet"))?;
let input_message = self
.message_translator
.to_input_message(buf)
.map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err))?;
.map_err(std::io::Error::other)?;
// Pass it to the mixnet sender
self.tx.start_send_unpin(input_message).map_err(|_| {
std::io::Error::new(std::io::ErrorKind::Other, "failed to send packet to mixnet")
})?;
self.tx
.start_send_unpin(input_message)
.map_err(|_| std::io::Error::other("failed to send packet to mixnet"))?;
Poll::Ready(Ok(buf.len()))
}
@@ -233,9 +232,8 @@ where
mut self: Pin<&mut Self>,
cx: &mut Context,
) -> Poll<std::result::Result<(), std::io::Error>> {
ready!(self.tx.poll_flush_unpin(cx)).map_err(|_| {
std::io::Error::new(std::io::ErrorKind::Other, "failed to send packet to mixnet")
})?;
ready!(self.tx.poll_flush_unpin(cx))
.map_err(|_| std::io::Error::other("failed to send packet to mixnet"))?;
Poll::Ready(Ok(()))
}
@@ -51,3 +51,6 @@ nym-task = { path = "../../common/task" }
nym-types = { path = "../../common/types" }
nym-wireguard = { path = "../../common/wireguard" }
nym-wireguard-types = { path = "../../common/wireguard-types" }
[dev-dependencies]
mock_instant = "0.5.3"
@@ -132,7 +132,9 @@ pub(crate) async fn execute(args: &Request) -> Result<(), AuthenticatorError> {
None,
))
.await
.map_err(|source| AuthenticatorError::FailedToSendPacketToMixnet { source })?;
.map_err(|source| AuthenticatorError::FailedToSendPacketToMixnet {
source: Box::new(source),
})?;
log::info!("Sent request, sleeping 60 seconds or until killed");
sleep(Duration::from_secs(60)).await;
+3 -3
View File
@@ -24,7 +24,7 @@ pub enum AuthenticatorError {
ShortPacket,
#[error("failed to connect to mixnet: {source}")]
FailedToConnectToMixnet { source: nym_sdk::Error },
FailedToConnectToMixnet { source: Box<nym_sdk::Error> },
#[error("failed to deserialize tagged packet: {source}")]
FailedToDeserializeTaggedPacket { source: bincode::Error },
@@ -33,13 +33,13 @@ pub enum AuthenticatorError {
FailedToLoadConfig(String),
#[error("failed to send packet to mixnet: {source}")]
FailedToSendPacketToMixnet { source: nym_sdk::Error },
FailedToSendPacketToMixnet { source: Box<nym_sdk::Error> },
#[error("failed to serialize response packet: {source}")]
FailedToSerializeResponsePacket { source: Box<bincode::ErrorKind> },
#[error("failed to setup mixnet client: {source}")]
FailedToSetupMixnetClient { source: nym_sdk::Error },
FailedToSetupMixnetClient { source: Box<nym_sdk::Error> },
#[error("{0}")]
GatewayStorageError(#[from] nym_gateway_storage::error::GatewayStorageError),
@@ -10,3 +10,4 @@ pub mod error;
pub mod mixnet_client;
pub mod mixnet_listener;
mod peer_manager;
mod seen_credential_cache;
@@ -26,7 +26,9 @@ pub async fn create_mixnet_client(
let mut client_builder =
nym_sdk::mixnet::MixnetClientBuilder::new_with_default_storage(storage_paths)
.await
.map_err(|err| AuthenticatorError::FailedToSetupMixnetClient { source: err })?
.map_err(|err| AuthenticatorError::FailedToSetupMixnetClient {
source: Box::new(err),
})?
.network_details(NymNetworkDetails::new_from_env())
.debug_config(debug_config)
.custom_shutdown(shutdown)
@@ -41,12 +43,16 @@ pub async fn create_mixnet_client(
client_builder = client_builder.custom_topology_provider(topology_provider);
}
let mixnet_client = client_builder
.build()
.map_err(|err| AuthenticatorError::FailedToSetupMixnetClient { source: err })?;
let mixnet_client =
client_builder
.build()
.map_err(|err| AuthenticatorError::FailedToSetupMixnetClient {
source: Box::new(err),
})?;
mixnet_client
.connect_to_mixnet()
.await
.map_err(|err| AuthenticatorError::FailedToConnectToMixnet { source: err })
mixnet_client.connect_to_mixnet().await.map_err(|err| {
AuthenticatorError::FailedToConnectToMixnet {
source: Box::new(err),
}
})
}
@@ -7,7 +7,7 @@ use std::{
time::{Duration, SystemTime},
};
use crate::{config::Config, error::*};
use crate::{config::Config, error::*, seen_credential_cache::SeenCredentialCache};
use crate::{error::AuthenticatorError, peer_manager::PeerManager};
use defguard_wireguard_rs::net::IpAddrMask;
use defguard_wireguard_rs::{host::Peer, key::Key};
@@ -77,6 +77,8 @@ pub(crate) struct MixnetListener {
pub(crate) ecash_verifier: Option<Arc<EcashManager>>,
pub(crate) timeout_check_interval: IntervalStream,
pub(crate) seen_credential_cache: SeenCredentialCache,
}
impl MixnetListener {
@@ -98,6 +100,7 @@ impl MixnetListener {
peer_manager: PeerManager::new(wireguard_gateway_data),
ecash_verifier,
timeout_check_interval,
seen_credential_cache: SeenCredentialCache::new(),
}
}
@@ -728,19 +731,28 @@ impl MixnetListener {
"bandwidth entry should have just been created".to_string(),
))?;
let client_bandwidth = ClientBandwidth::new(bandwidth.into());
let mut verifier = CredentialVerifier::new(
CredentialSpendingRequest::new(msg.credential()),
ecash_verifier.clone(),
BandwidthStorageManager::new(
ecash_verifier.storage().clone(),
client_bandwidth,
client_id,
BandwidthFlushingBehaviourConfig::default(),
true,
),
);
let available_bandwidth = verifier.verify().await?;
let available_bandwidth = if self.received_retry(&*msg) {
// don't process the credential and just return the current bandwidth
bandwidth.available
} else {
let client_bandwidth = ClientBandwidth::new(bandwidth.into());
let credential = msg.credential();
let mut verifier = CredentialVerifier::new(
CredentialSpendingRequest::new(credential.clone()),
ecash_verifier.clone(),
BandwidthStorageManager::new(
ecash_verifier.storage().clone(),
client_bandwidth,
client_id,
BandwidthFlushingBehaviourConfig::default(),
true,
),
);
let available_bandwidth = verifier.verify().await?;
self.seen_credential_cache
.insert_credential(credential, msg.pub_key());
available_bandwidth
};
let bytes = match AuthenticatorVersion::from(protocol) {
AuthenticatorVersion::V5 => v5::response::AuthenticatorResponse::new_topup_bandwidth(
@@ -777,6 +789,18 @@ impl MixnetListener {
Ok((bytes, reply_to))
}
fn received_retry(&self, msg: &(dyn TopUpMessage + Send + Sync + 'static)) -> bool {
if let Some(peer_pub_key) = self
.seen_credential_cache
.get_peer_pub_key(&msg.credential())
{
// check if the same peer sent the same credential twice, probably because of a retry
peer_pub_key == msg.pub_key()
} else {
false
}
}
async fn on_reconstructed_message(
&mut self,
reconstructed: ReconstructedMessage,
@@ -836,10 +860,11 @@ impl MixnetListener {
sender_tag: Option<AnonymousSenderTag>,
) -> Result<()> {
let input_message = create_input_message(recipient, sender_tag, response)?;
self.mixnet_client
.send(input_message)
.await
.map_err(|err| AuthenticatorError::FailedToSendPacketToMixnet { source: err })
self.mixnet_client.send(input_message).await.map_err(|err| {
AuthenticatorError::FailedToSendPacketToMixnet {
source: Box::new(err),
}
})
}
pub(crate) async fn run(mut self) -> Result<()> {
@@ -855,6 +880,7 @@ impl MixnetListener {
if let Err(e) = self.remove_stale_registrations().await {
log::error!("Could not clear stale registrations. The registration process might get jammed soon - {:?}", e);
}
self.seen_credential_cache.remove_stale();
}
msg = self.mixnet_client.next() => {
if let Some(msg) = msg {
@@ -0,0 +1,188 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
#[cfg(test)]
use mock_instant::thread_local::SystemTime;
#[cfg(not(test))]
use std::time::SystemTime;
use std::{collections::HashMap, time::Duration};
use nym_credentials_interface::CredentialSpendingData;
use nym_wireguard_types::PeerPublicKey;
const SEEN_CREDENTIAL_CACHE_TIME: Duration = Duration::from_secs(60 * 60); // 1 hour
#[derive(Eq, Hash, PartialEq)]
struct TimestampedPeerPubKey {
peer_pub_key: PeerPublicKey,
timestamp: SystemTime,
}
pub(crate) struct SeenCredentialCache {
cached_credentials: HashMap<String, TimestampedPeerPubKey>,
}
impl SeenCredentialCache {
pub(crate) fn new() -> Self {
SeenCredentialCache {
cached_credentials: HashMap::new(),
}
}
pub(crate) fn insert_credential(
&mut self,
credential: CredentialSpendingData,
peer_pub_key: PeerPublicKey,
) {
let value = TimestampedPeerPubKey {
peer_pub_key,
timestamp: SystemTime::now(),
};
self.cached_credentials
.insert(credential.serial_number_b58(), value);
}
pub(crate) fn get_peer_pub_key(
&self,
credential: &CredentialSpendingData,
) -> Option<PeerPublicKey> {
self.cached_credentials
.get(&credential.serial_number_b58())
.map(|value| value.peer_pub_key)
}
pub(crate) fn remove_stale(&mut self) {
let now = SystemTime::now();
self.cached_credentials.retain(|_, value| {
let Ok(cache_time) = now.duration_since(value.timestamp) else {
log::warn!("Got decreasing consecutive system timestamps");
return false;
};
cache_time < SEEN_CREDENTIAL_CACHE_TIME
});
}
}
#[cfg(test)]
mod test {
use mock_instant::thread_local::MockClock;
use std::str::FromStr;
use super::*;
const CREDENTIAL_BYTES: [u8; 1245] = [
0, 0, 4, 133, 96, 179, 223, 185, 136, 23, 213, 166, 59, 203, 66, 69, 209, 181, 227, 254,
16, 102, 98, 237, 59, 119, 170, 111, 31, 194, 51, 59, 120, 17, 115, 229, 79, 91, 11, 139,
154, 2, 212, 23, 68, 70, 167, 3, 240, 54, 224, 171, 221, 1, 69, 48, 60, 118, 119, 249, 123,
35, 172, 227, 131, 96, 232, 209, 187, 123, 4, 197, 102, 90, 96, 45, 125, 135, 140, 99, 1,
151, 17, 131, 143, 157, 97, 107, 139, 232, 212, 87, 14, 115, 253, 255, 166, 167, 186, 43,
90, 96, 173, 105, 120, 40, 10, 163, 250, 224, 214, 200, 178, 4, 160, 16, 130, 59, 76, 193,
39, 240, 3, 101, 141, 209, 183, 226, 186, 207, 56, 210, 187, 7, 164, 240, 164, 205, 37, 81,
184, 214, 193, 195, 90, 205, 238, 225, 195, 104, 12, 123, 203, 57, 233, 243, 215, 145, 195,
196, 57, 38, 125, 172, 18, 47, 63, 165, 110, 219, 180, 40, 58, 116, 92, 254, 160, 98, 48,
92, 254, 232, 107, 184, 80, 234, 60, 160, 235, 249, 76, 41, 38, 165, 28, 40, 136, 74, 48,
166, 50, 245, 23, 201, 140, 101, 79, 93, 235, 128, 186, 146, 126, 180, 134, 43, 13, 186,
19, 195, 48, 168, 201, 29, 216, 95, 176, 198, 132, 188, 64, 39, 212, 150, 32, 52, 53, 38,
228, 199, 122, 226, 217, 75, 40, 191, 151, 48, 164, 242, 177, 79, 14, 122, 105, 151, 85,
88, 199, 162, 17, 96, 103, 83, 178, 128, 9, 24, 30, 74, 108, 241, 85, 240, 166, 97, 241,
85, 199, 11, 198, 226, 234, 70, 107, 145, 28, 208, 114, 51, 12, 234, 108, 101, 202, 112,
48, 185, 22, 159, 67, 109, 49, 27, 149, 90, 109, 32, 226, 112, 7, 201, 208, 209, 104, 31,
97, 134, 204, 145, 27, 181, 206, 181, 106, 32, 110, 136, 115, 249, 201, 111, 5, 245, 203,
71, 121, 169, 126, 151, 178, 236, 59, 221, 195, 48, 135, 115, 6, 50, 227, 74, 97, 107, 107,
213, 90, 2, 203, 154, 138, 47, 128, 52, 134, 128, 224, 51, 65, 240, 90, 8, 55, 175, 180,
178, 204, 206, 168, 110, 51, 57, 189, 169, 48, 169, 136, 121, 99, 51, 170, 178, 214, 74, 1,
96, 151, 167, 25, 173, 180, 171, 155, 10, 55, 142, 234, 190, 113, 90, 79, 80, 244, 71, 166,
30, 235, 113, 150, 133, 1, 218, 17, 109, 111, 223, 24, 216, 177, 41, 2, 204, 65, 221, 212,
207, 236, 144, 6, 65, 224, 55, 42, 1, 1, 161, 134, 118, 127, 111, 220, 110, 127, 240, 71,
223, 129, 12, 93, 20, 220, 60, 56, 71, 146, 184, 95, 132, 69, 28, 56, 53, 192, 213, 22,
119, 230, 152, 225, 182, 188, 163, 219, 37, 175, 247, 73, 14, 247, 38, 72, 243, 1, 48, 131,
59, 8, 13, 96, 143, 185, 127, 241, 161, 217, 24, 149, 193, 40, 16, 30, 202, 151, 28, 119,
240, 153, 101, 156, 61, 193, 72, 245, 199, 181, 12, 231, 65, 166, 67, 142, 121, 207, 202,
58, 197, 113, 188, 248, 42, 124, 105, 48, 161, 241, 55, 209, 36, 194, 27, 63, 233, 144,
189, 85, 117, 234, 9, 139, 46, 31, 206, 114, 95, 131, 29, 240, 13, 81, 142, 140, 133, 33,
30, 41, 141, 37, 80, 217, 95, 221, 76, 115, 86, 201, 165, 51, 252, 9, 28, 209, 1, 48, 150,
74, 248, 212, 187, 222, 66, 210, 3, 200, 19, 217, 171, 184, 42, 148, 53, 150, 57, 50, 6,
227, 227, 62, 49, 42, 148, 148, 157, 82, 191, 58, 24, 34, 56, 98, 120, 89, 105, 176, 85,
15, 253, 241, 41, 153, 195, 136, 1, 48, 142, 126, 213, 101, 223, 79, 133, 230, 105, 38,
161, 149, 2, 21, 136, 150, 42, 72, 218, 85, 146, 63, 223, 58, 108, 186, 183, 248, 62, 20,
47, 34, 113, 160, 177, 204, 181, 16, 24, 212, 224, 35, 84, 51, 168, 56, 136, 11, 1, 48,
135, 242, 62, 149, 230, 178, 32, 224, 119, 26, 234, 163, 237, 224, 114, 95, 112, 140, 170,
150, 96, 125, 136, 221, 180, 78, 18, 11, 12, 184, 2, 198, 217, 119, 43, 69, 4, 172, 109,
55, 183, 40, 131, 172, 161, 88, 183, 101, 1, 48, 173, 216, 22, 73, 42, 255, 211, 93, 249,
87, 159, 115, 61, 91, 55, 130, 17, 216, 60, 34, 122, 55, 8, 244, 244, 153, 151, 57, 5, 144,
178, 55, 249, 64, 211, 168, 34, 148, 56, 89, 92, 203, 70, 124, 219, 152, 253, 165, 0, 32,
203, 116, 63, 7, 240, 222, 82, 86, 11, 149, 167, 72, 224, 55, 190, 66, 201, 65, 168, 184,
96, 47, 194, 241, 168, 124, 7, 74, 214, 250, 37, 76, 32, 218, 69, 122, 103, 215, 145, 169,
24, 212, 229, 168, 106, 10, 144, 31, 13, 25, 178, 242, 250, 106, 159, 40, 48, 163, 165, 61,
130, 57, 146, 4, 73, 32, 254, 233, 125, 135, 212, 29, 111, 4, 177, 114, 15, 210, 170, 82,
108, 110, 62, 166, 81, 209, 106, 176, 156, 14, 133, 242, 60, 127, 120, 242, 28, 97, 0, 1,
32, 103, 93, 109, 89, 240, 91, 1, 84, 150, 50, 206, 157, 203, 49, 220, 120, 234, 175, 234,
150, 126, 225, 94, 163, 164, 199, 138, 114, 62, 99, 106, 112, 1, 32, 171, 40, 220, 82, 241,
203, 76, 146, 111, 139, 182, 179, 237, 182, 115, 75, 128, 201, 107, 43, 214, 0, 135, 217,
160, 68, 150, 232, 144, 114, 237, 98, 32, 30, 134, 232, 59, 93, 163, 253, 244, 13, 202, 52,
147, 168, 83, 121, 123, 95, 21, 210, 209, 225, 223, 143, 49, 10, 205, 238, 1, 22, 83, 81,
70, 1, 32, 26, 76, 6, 234, 160, 50, 139, 102, 161, 232, 155, 106, 130, 171, 226, 210, 233,
178, 85, 247, 71, 123, 55, 53, 46, 67, 148, 137, 156, 207, 208, 107, 1, 32, 102, 31, 4, 98,
110, 156, 144, 61, 229, 140, 198, 84, 196, 238, 128, 35, 131, 182, 137, 125, 241, 95, 69,
131, 170, 27, 2, 144, 75, 72, 242, 102, 3, 32, 121, 80, 45, 173, 56, 65, 218, 27, 40, 251,
197, 32, 169, 104, 123, 110, 90, 78, 153, 166, 38, 9, 129, 228, 99, 8, 1, 116, 142, 233,
162, 69, 32, 216, 169, 159, 116, 95, 12, 63, 176, 195, 6, 183, 123, 135, 75, 61, 112, 106,
83, 235, 176, 41, 27, 248, 48, 71, 165, 170, 12, 92, 103, 103, 81, 32, 58, 74, 75, 145,
192, 94, 153, 69, 80, 128, 241, 3, 16, 117, 192, 86, 161, 103, 44, 174, 211, 196, 182, 124,
55, 11, 107, 142, 49, 88, 6, 41, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,
6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,
6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 0, 37, 139, 240, 0, 0,
0, 0, 0, 0, 0, 1,
];
const PUB_KEY: &str = "yvNUDpT5l7W/xDhiu6HkqTHDQwbs/B3J5UrLmORl1EQ=";
#[test]
fn insertion() {
let mut cache = SeenCredentialCache::new();
let credential = CredentialSpendingData::try_from_bytes(&CREDENTIAL_BYTES).unwrap();
let peer_pub_key = PeerPublicKey::from_str(PUB_KEY).unwrap();
assert!(cache.get_peer_pub_key(&credential).is_none());
cache.insert_credential(credential.clone(), peer_pub_key);
let cached_peer_pub_key = cache.get_peer_pub_key(&credential).unwrap();
assert_eq!(cached_peer_pub_key, peer_pub_key);
}
#[test]
fn staleness() {
let mut cache = SeenCredentialCache::new();
let credential = CredentialSpendingData::try_from_bytes(&CREDENTIAL_BYTES).unwrap();
let peer_pub_key = PeerPublicKey::from_str(PUB_KEY).unwrap();
cache.insert_credential(credential.clone(), peer_pub_key);
cache.remove_stale();
assert!(cache.get_peer_pub_key(&credential).is_some());
MockClock::advance_system_time(SEEN_CREDENTIAL_CACHE_TIME * 2);
cache.remove_stale();
assert!(cache.get_peer_pub_key(&credential).is_none());
}
#[test]
fn invalid_time() {
assert!(MockClock::is_thread_local());
assert!(SystemTime::now().is_thread_local());
let mut cache = SeenCredentialCache::new();
let credential = CredentialSpendingData::try_from_bytes(&CREDENTIAL_BYTES).unwrap();
let peer_pub_key = PeerPublicKey::from_str(PUB_KEY).unwrap();
// set some value for time
MockClock::set_system_time(Duration::from_secs(10));
cache.insert_credential(credential.clone(), peer_pub_key);
// then set the time in the past
MockClock::set_system_time(Duration::ZERO);
cache.remove_stale();
// invalid time should remove the credential, just in case
assert!(cache.get_peer_pub_key(&credential).is_none());
}
}
@@ -133,12 +133,16 @@ impl ConnectedClientHandler {
let input_packet = self
.input_message_creator
.to_input_message(&bundled_packets)
.map_err(|source| IpPacketRouterError::FailedToSendPacketToMixnet { source })?;
.map_err(|source| IpPacketRouterError::FailedToSendPacketToMixnet {
source: Box::new(source),
})?;
self.mixnet_client_sender
.send(input_packet)
.await
.map_err(|source| IpPacketRouterError::FailedToSendPacketToMixnet { source })
.map_err(|source| IpPacketRouterError::FailedToSendPacketToMixnet {
source: Box::new(source),
})
}
async fn run(mut self) -> Result<()> {
@@ -29,10 +29,10 @@ pub enum IpPacketRouterError {
ConfigValidationFailure,
#[error("failed to setup mixnet client: {source}")]
FailedToSetupMixnetClient { source: nym_sdk::Error },
FailedToSetupMixnetClient { source: Box<nym_sdk::Error> },
#[error("failed to connect to mixnet: {source}")]
FailedToConnectToMixnet { source: nym_sdk::Error },
FailedToConnectToMixnet { source: Box<nym_sdk::Error> },
#[error("the entity wrapping the ip packet router has disconnected")]
DisconnectedParent,
@@ -59,7 +59,7 @@ pub enum IpPacketRouterError {
FailedToWritePacketToTun,
#[error("failed to send packet to mixnet: {source}")]
FailedToSendPacketToMixnet { source: nym_sdk::Error },
FailedToSendPacketToMixnet { source: Box<nym_sdk::Error> },
#[error("failed to encode mixnet message: {source}")]
FailedToEncodeMixnetMessage {
@@ -27,7 +27,9 @@ pub(crate) async fn create_mixnet_client(
let mut client_builder =
nym_sdk::mixnet::MixnetClientBuilder::new_with_default_storage(storage_paths)
.await
.map_err(|err| IpPacketRouterError::FailedToSetupMixnetClient { source: err })?
.map_err(|err| IpPacketRouterError::FailedToSetupMixnetClient {
source: Box::new(err),
})?
.network_details(NymNetworkDetails::new_from_env())
.debug_config(debug_config)
.custom_shutdown(shutdown)
@@ -43,12 +45,16 @@ pub(crate) async fn create_mixnet_client(
client_builder = client_builder.custom_topology_provider(topology_provider);
}
let mixnet_client = client_builder
.build()
.map_err(|err| IpPacketRouterError::FailedToSetupMixnetClient { source: err })?;
let mixnet_client =
client_builder
.build()
.map_err(|err| IpPacketRouterError::FailedToSetupMixnetClient {
source: Box::new(err),
})?;
mixnet_client
.connect_to_mixnet()
.await
.map_err(|err| IpPacketRouterError::FailedToConnectToMixnet { source: err })
mixnet_client.connect_to_mixnet().await.map_err(|err| {
IpPacketRouterError::FailedToConnectToMixnet {
source: Box::new(err),
}
})
}
@@ -435,10 +435,11 @@ impl MixnetListener {
let input_message =
crate::util::create_message::create_input_message(&send_to, response_bytes);
self.mixnet_client
.send(input_message)
.await
.map_err(|err| IpPacketRouterError::FailedToSendPacketToMixnet { source: err })
self.mixnet_client.send(input_message).await.map_err(|err| {
IpPacketRouterError::FailedToSendPacketToMixnet {
source: Box::new(err),
}
})
}
// A single incoming request can trigger multiple responses, such as when data requests contain
@@ -4,7 +4,7 @@
[package]
name = "nym-network-requester"
license = "GPL-3.0"
version = "1.1.56"
version = "1.1.58"
authors.workspace = true
edition.workspace = true
rust-version = "1.70"
@@ -576,7 +576,9 @@ async fn create_mixnet_client(
let mut client_builder =
nym_sdk::mixnet::MixnetClientBuilder::new_with_default_storage(storage_paths)
.await
.map_err(|err| NetworkRequesterError::FailedToSetupMixnetClient { source: err })?
.map_err(|err| NetworkRequesterError::FailedToSetupMixnetClient {
source: Box::new(err),
})?
.network_details(NymNetworkDetails::new_from_env())
.debug_config(debug_config)
.custom_shutdown(shutdown)
@@ -591,12 +593,16 @@ async fn create_mixnet_client(
client_builder = client_builder.custom_topology_provider(topology_provider);
}
let mixnet_client = client_builder
.build()
.map_err(|err| NetworkRequesterError::FailedToSetupMixnetClient { source: err })?;
let mixnet_client =
client_builder
.build()
.map_err(|err| NetworkRequesterError::FailedToSetupMixnetClient {
source: Box::new(err),
})?;
mixnet_client
.connect_to_mixnet()
.await
.map_err(|err| NetworkRequesterError::FailedToConnectToMixnet { source: err })
mixnet_client.connect_to_mixnet().await.map_err(|err| {
NetworkRequesterError::FailedToConnectToMixnet {
source: Box::new(err),
}
})
}
@@ -30,10 +30,10 @@ pub enum NetworkRequesterError {
ConfigValidationFailure,
#[error("failed to setup mixnet client: {source}")]
FailedToSetupMixnetClient { source: nym_sdk::Error },
FailedToSetupMixnetClient { source: Box<nym_sdk::Error> },
#[error("failed to connect to mixnet: {source}")]
FailedToConnectToMixnet { source: nym_sdk::Error },
FailedToConnectToMixnet { source: Box<nym_sdk::Error> },
#[error("the entity wrapping the network requester has disconnected")]
DisconnectedParent,
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "nym-cli"
version = "1.1.55"
version = "1.1.57"
authors.workspace = true
edition = "2021"
license.workspace = true
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "nymvisor"
version = "0.1.20"
version = "0.1.22"
authors.workspace = true
repository.workspace = true
homepage.workspace = true
+4 -16
View File
@@ -150,10 +150,7 @@ impl UpgradePlan {
pub(crate) fn try_load<P: AsRef<Path>>(path: P) -> Result<Self, NymvisorError> {
let path = path.as_ref();
let mut upgrade_plan: UpgradePlan = fs::File::open(path)
.and_then(|file| {
serde_json::from_reader(file)
.map_err(|serde_json_err| io::Error::new(io::ErrorKind::Other, serde_json_err))
})
.and_then(|file| serde_json::from_reader(file).map_err(io::Error::other))
.map_err(|source| NymvisorError::UpgradePlanLoadFailure {
path: path.to_path_buf(),
source,
@@ -274,10 +271,7 @@ impl UpgradeInfo {
pub(crate) fn try_load<P: AsRef<Path>>(path: P) -> Result<Self, NymvisorError> {
let path = path.as_ref();
fs::File::open(path)
.and_then(|file| {
serde_json::from_reader(file)
.map_err(|serde_json_err| io::Error::new(io::ErrorKind::Other, serde_json_err))
})
.and_then(|file| serde_json::from_reader(file).map_err(io::Error::other))
.map_err(|source| NymvisorError::UpgradeInfoLoadFailure {
path: path.to_path_buf(),
source,
@@ -426,10 +420,7 @@ impl UpgradeHistory {
pub(crate) fn try_load<P: AsRef<Path>>(path: P) -> Result<Self, NymvisorError> {
let path = path.as_ref();
let mut history: UpgradeHistory = fs::File::open(path)
.and_then(|file| {
serde_json::from_reader(file)
.map_err(|serde_json_err| io::Error::new(io::ErrorKind::Other, serde_json_err))
})
.and_then(|file| serde_json::from_reader(file).map_err(io::Error::other))
.map_err(|source| NymvisorError::UpgradeHistoryLoadFailure {
path: path.to_path_buf(),
source,
@@ -481,10 +472,7 @@ impl CurrentVersionInfo {
pub(crate) fn try_load<P: AsRef<Path>>(path: P) -> Result<Self, NymvisorError> {
let path = path.as_ref();
fs::File::open(path)
.and_then(|file| {
serde_json::from_reader(file)
.map_err(|serde_json_err| io::Error::new(io::ErrorKind::Other, serde_json_err))
})
.and_then(|file| serde_json::from_reader(file).map_err(io::Error::other))
.map_err(|source| NymvisorError::CurrentVersionInfoLoadFailure {
path: path.to_path_buf(),
source,