Files
nym/nym-node/nym-node-requests/src/api/helpers.rs
T
Jędrzej Stuczyński 46c67440bb Mixnode stress testing (#6575)
* Squashing the mix stress testing branch (#6575)

reduced chain watcher per block log severity

update network monitors contract semver to 1.0.0

fix build issues

fix mixnet client dropping initial packet on egress reconnection

adjusted logs for network monitor agent

changed default testing interval to 2h

refresh NM contract information

explicit return type for batch submission

for mixnet listener task to get scheduled before beginning connectivity test

make sure to always use canonical ip for network monitor noise keys

feat: NMv3: make agents decide egress port (#6746)

add config v12->v13 config migration for nym nodes

fix formatting in wallet types

simplified client config creation

remove other swagger redirect

removed swagger redirect on /swagger/ route

log version info on startup

add workflows, contract address, and dockerfile

bugfix: use correct endpoints when setting up orchestrator (#6733)

clippy

adjust DEFAULT_MIN_STRESS_TESTED_NODES ratio

expose route with new performance metrics

fixes and additional docs

use stress testing scores

stub for usage of stress testing scores

stub traits

added new fields to nym-api config controlling usage of stress test data

guard against duplicate packets

prevent usage of chain_authorisation_check_max_attempts with value of 0

make sure duplicate results cant be inserted into the db

submit test results from orchestrator on an interval

docs and fixes

nym-api side of handling result submission

stubs for submitting results

NM orchestrator verifying nym-api result submission permissions

NM orchestrator to update announced key on startup

allow NM orchestrator to announce its identity key to the contract

stubs within nym-api for accepting NMv3 results

added additional metrics

docs

bugfixes + making sure to only assign mixnode testruns

fixed node refresher to only retrieve mixnodes and add additional metrics

topology metrics

defined basic prometheus metrics

authorised endpoint for returning prometheus data

create initial stub for prometheus metrics

post rebasing fixes

adjusted routes

missing implementation for storage getters

a lot of new stubs and db accessors

stubs for results endpoints

update utoipa tags for agent rountes

shared auth between metrics and results

moved stale results eviction into the interval.tick branch

refactor and comments

create background process to evict stale data

include sphinx packet delay as part of the stats

fix mock construction

add median to the calculated latency distribution

remove unused imports

cleanup

performing testrun and submitting the results

assigning testruns to requesting agents

basic stub for http server for the NMv3 orchestrator

chore: rename existing 'NetworkMonitorAgent' to 'NodeStressTester'

make sure to use canonical ips within the noise config

fixed contract tests

cargo fmt

additional comments and unit tests

contract and nym-node support of NM agents being run on the same host

basic unit tests

refactoring

make agents retrieve mix port assignment from the orchestrator

provide sensible defaults to CLI arguments

stub the initial structure for the agent

chore: remove redundant import

missed tick behaviour

removed redundant mutex

removed redundant try_get_client

reuse existing constant for default nymnode port

add node refresher for periodic scraping of bonded nym-node details

- NodeRefresher periodically queries the mixnet contract for all bonded
  nodes and probes each node's HTTP API for host information, sphinx keys,
  noise keys, and key rotation IDs
- Extract NymNodeApiClientRetriever into nym-node-requests with port
  probing, identity verification, and host information signature checking
- Add clone_query_client on NyxdClient so the refresher can hold its own
  query client without locking the signing client
- Batch upsert for nym_node rows (single transaction instead of per-row)
- Reuse the new helpers in nym-api's node_describe_cache

ensure assignment of testrun begins an IMMEDIATE tx

construction of the orchestrator struct

initial set of cli args

make sure to not assign testable nodes too often

very initial database structure and cli

fixed construction of RoutableNetworkMonitors

remove redundant constructor for NoiseNode

forbid 0-nonsense config values

add type safety for test route construction

moved lioness and arrayref to workspace deps

fixed dockerfile build

always use canonical addresses in RoutableNetworkMonitors

fixed old contract formatting issues

removed redundant into() call

network monitor agent fixes

additional logs

config unit tests

more docs

standalone stress testing invocation

further refactoring and changes

refactor testing loop and return valid test result upon completion

initial sending/receiving test loop

generating reusable sphinx headers

additional structure for receiving ingress packets

initial scaffolding for NMv3 agent

added validation of x25519 noise key

removed unstable call to 'is_multiple_of'

remove calls to from_octets as they're unavailable in pre 1.91

additional docs/comments

propagating noise information about NM for mixnet routing

pass full socket address of the agent into the contract storage

feat: store noise keys alongside ip addresses within the contract

removed redundant comment

ensure NM packets can only go to NM

PR review comments

added additional docs

allow NM to replay packets + fix replay prometheus metrics

propagate information about nm agent to connection handler

updated nym-node config migration

feat: introduced nym-node websocket subscription for keeping updated list of NM agents

allow admin to also revoke monitor agents

remove agents upon orchestrator removal

fixed schema generation and regenerated the contract schema

removed rustc restriction on contracts-common

added client methods for interacting with the contract

added unit tests for contract methods

implemented logic of the network monitors contract

create initial structure for network monitors contract

start mix stress testing topic branch

* make nym-node default to the new blockstream rpc/ws node cluster

* reduced mixnet-client log severity

* set network monitors contract address for mainnet
2026-05-22 09:43:20 +01:00

256 lines
10 KiB
Rust

// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
#[cfg(feature = "client")]
pub use client_helpers::*;
#[cfg(feature = "client")]
mod client_helpers {
use crate::api::SignedHostInformation;
use crate::api::client::NymNodeApiClientExt;
use nym_http_api_client::UserAgent;
use nym_network_defaults::DEFAULT_NYM_NODE_HTTP_PORT;
use std::time::Duration;
/// Builder-style helper for obtaining a validated [`crate::api::Client`] for a nym-node.
///
/// On top of the basic port-probing performed by [`try_get_valid_nym_node_api_client`],
/// this struct optionally:
/// - verifies that the node's self-reported ed25519 identity matches an expected value
/// (e.g. the identity committed on-chain during bonding), and
/// - checks the cryptographic signature on the node's host information.
///
/// Both checks require an extra HTTP round-trip to the node's `/host-information` endpoint
/// and are skipped when neither option is enabled.
#[derive(Debug)]
pub struct NymNodeApiClientRetriever {
/// Expected (base58-encoded) ed25519 identity of the node.
/// used to check against data retrieved from the host information
expected_identity: Option<String>,
/// Custom port to use when attempting to query the node.
custom_port: Option<u16>,
/// User agent to use when attempting to query the node.
user_agent: UserAgent,
/// Specify whether the signature on the host information should be verified.
verify_host_information: bool,
}
impl NymNodeApiClientRetriever {
/// Creates a new retriever with the given user agent.
/// All optional checks (identity verification, host information signature)
/// are disabled by default — use the builder methods to enable them.
pub fn new(user_agent: impl Into<UserAgent>) -> Self {
Self {
expected_identity: None,
custom_port: None,
user_agent: user_agent.into(),
verify_host_information: false,
}
}
/// If set, the node's self-reported ed25519 identity (from its `/host-information`
/// endpoint) will be compared against this value. A mismatch produces
/// [`crate::error::Error::MismatchedIdentity`].
#[must_use]
pub fn with_expected_identity(mut self, expected_identity: Option<String>) -> Self {
self.expected_identity = expected_identity;
self
}
/// Prepend `http://<host>:<port>` to the list of addresses probed during
/// [`get_client`](Self::get_client), so it is tried before the standard ports.
#[must_use]
pub fn with_custom_port(mut self, port: Option<u16>) -> Self {
self.custom_port = port;
self
}
/// Enable cryptographic verification of the node's host information signature.
/// When enabled, [`get_client`](Self::get_client) will return
/// [`crate::error::Error::MissignedHostInformation`] if the signature is invalid.
#[must_use]
pub fn with_verify_host_information(mut self) -> Self {
self.verify_host_information = true;
self
}
/// Probe the node's HTTP API, perform any configured verification, and return the
/// client together with the [`SignedHostInformation`] if it was fetched.
///
/// The host information is only retrieved when identity verification or signature
/// checking is enabled. When neither is active, the returned
/// [`ApiClientWithHostInformation::host_information`] will be `None`.
pub async fn get_client(
self,
base_host: &str,
node_id: u32,
) -> Result<ApiClientWithHostInformation, crate::error::Error> {
let base_client = try_get_valid_nym_node_api_client(
base_host,
node_id,
self.custom_port,
self.user_agent,
)
.await?;
// no need to retrieve host information if we don't have to perform any verification
if !self.verify_host_information && self.expected_identity.is_none() {
return Ok(base_client.into());
}
let host_info = retrieve_validated_host_information(
&base_client,
node_id,
&self.expected_identity,
self.verify_host_information,
)
.await?;
Ok(ApiClientWithHostInformation::from(base_client).with_host_information(host_info))
}
}
/// Fetch a node's [`SignedHostInformation`] and optionally validate it.
///
/// This is the standalone equivalent of the checks performed inside
/// [`NymNodeApiClientRetriever::get_client`], useful when the caller already
/// holds a [`crate::api::Client`] and only needs the host information.
///
/// When `expected_ed25519_identity` is `Some`, the node's self-reported identity
/// is compared against it — a mismatch produces [`crate::error::Error::MismatchedIdentity`].
/// When `verify_host_information` is `true`, the cryptographic signature on the
/// host information is checked — an invalid signature produces
/// [`crate::error::Error::MissignedHostInformation`].
pub async fn retrieve_validated_host_information(
client: &crate::api::Client,
node_id: u32,
expected_ed25519_identity: &Option<String>,
verify_host_information: bool,
) -> Result<SignedHostInformation, crate::error::Error> {
let host_info = match client.get_host_information().await {
Ok(info) => info,
Err(err) => {
return Err(crate::error::Error::QueryFailure {
host: client.current_url().to_string(),
node_id,
source: Box::new(err),
});
}
};
if let Some(expected_identity) = expected_ed25519_identity {
// check if the identity key matches the information provided during bonding
if expected_identity.as_str() != host_info.keys.ed25519_identity.to_base58_string() {
return Err(crate::error::Error::MismatchedIdentity {
node_id,
expected: expected_identity.clone(),
got: host_info.keys.ed25519_identity.to_base58_string(),
});
}
}
// check if the host information has been signed with the node's key
if verify_host_information && !host_info.verify_host_information() {
return Err(crate::error::Error::MissignedHostInformation { node_id });
}
Ok(host_info)
}
/// A nym-node API client bundled with the node's [`SignedHostInformation`],
/// if it was retrieved during the connection/verification phase.
///
/// This avoids a redundant second call to the `/host-information` endpoint
/// when the caller also needs the host information after obtaining the client.
pub struct ApiClientWithHostInformation {
pub client: crate::api::Client,
pub host_information: Option<SignedHostInformation>,
}
impl ApiClientWithHostInformation {
fn with_host_information(self, host_information: SignedHostInformation) -> Self {
Self {
host_information: Some(host_information),
..self
}
}
}
impl From<crate::api::Client> for ApiClientWithHostInformation {
fn from(client: crate::api::Client) -> Self {
Self {
client,
host_information: None,
}
}
}
/// Probe a nym-node's HTTP API and return a connected [`crate::api::Client`].
///
/// `base_host` is a hostname (e.g. `nymtech.net`) or IP address (e.g. `127.0.0.1`).
/// The function tries the following addresses in order, returning the first one whose
/// `/health` endpoint reports an "up" status:
///
/// 1. `http://<host>:<custom_port>` (only when `custom_port` is `Some`)
/// 2. `http://<host>:8080` — the standard nym-node API port
/// 3. `https://<host>` — node behind an HTTPS reverse proxy (port 443)
/// 4. `http://<host>` — node behind an HTTP reverse proxy (port 80)
///
/// This function is intended for infrastructure binaries (nym-api, network monitor, etc.),
/// not regular clients, which is why hickory DNS is explicitly disabled.
pub async fn try_get_valid_nym_node_api_client(
base_host: &str,
node_id: u32,
custom_port: Option<u16>,
user_agent: impl Into<UserAgent>,
) -> Result<crate::api::Client, crate::error::Error> {
// first try the standard port in case the operator didn't put the node behind the proxy,
// then default https (443)
// finally default http (80)
let mut addresses_to_try = vec![
format!("http://{base_host}:{DEFAULT_NYM_NODE_HTTP_PORT}"), // 'standard' nym-node
format!("https://{base_host}"), // node behind https proxy (443)
format!("http://{base_host}"), // node behind http proxy (80)
];
// if a custom port was provided, try to connect to it first
if let Some(port) = custom_port {
addresses_to_try.insert(0, format!("http://{base_host}:{port}"));
}
let user_agent = user_agent.into();
for address in addresses_to_try {
// if provided base_host was malformed, there's no point in continuing
let client = match crate::api::Client::builder(address).and_then(|b| {
b.with_timeout(Duration::from_secs(5))
.no_hickory_dns()
.with_user_agent(user_agent.clone())
.build()
}) {
Ok(client) => client,
Err(err) => {
return Err(crate::error::Error::MalformedHost {
host: base_host.to_string(),
node_id,
source: Box::new(err),
});
}
};
if let Ok(health) = client.get_health().await
&& health.status.is_up()
{
return Ok(client);
}
}
Err(crate::error::Error::NoHttpPortsAvailable {
host: base_host.to_string(),
node_id,
})
}
}