Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 944fc27ef6 | |||
| 3853c0f0c9 | |||
| 97f79381b9 | |||
| 25eba09b92 | |||
| a8cecb1200 | |||
| 4e52e9bf77 | |||
| cf55e2fe86 | |||
| dc0835f1f3 | |||
| b5a8b9d283 | |||
| a395167139 | |||
| 6b98c168fc | |||
| 4645de3eb5 | |||
| e6dd670b16 | |||
| dc48750271 | |||
| 46c67440bb | |||
| e5cd9fd69e | |||
| 21c14c0df0 | |||
| 87c236a927 |
@@ -4,6 +4,52 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [2026.10-waterloo] (2026-05-27)
|
||||
|
||||
- Re-order default API urls for network details - Waterloo release ([#6799])
|
||||
- [bugfix] IPR v8<->v9 mismatch on Waterloo ([#6772])
|
||||
- Migrate to hickory 0.26.1 ([#6751])
|
||||
- add workflows for NM3 ([#6729])
|
||||
- credential proxy pool ([#6726])
|
||||
- chore: made sphinx version threshold assertion a compile time check ([#6718])
|
||||
- Feat/nmv3 updated performance calculation ([#6714])
|
||||
- feat: NMv3: submission of stress testing result into nym-api ([#6709])
|
||||
- feat: NMv3: Prometheus metrics for network monitor ([#6693])
|
||||
- feat: NMv3: add read-only results API to orchestrator ([#6689])
|
||||
- feat: NMv3: Eviction of stale testrun data ([#6685])
|
||||
- feat: NMv3: Wire up testrun assignment and result submission flow ([#6680])
|
||||
- feat: NMv3: Support multiple network monitor agents per host ([#6679])
|
||||
- Feat/nmv3 agent announcement ([#6673])
|
||||
- add node refresher for periodic scraping of bonded nym-node details ([#6626])
|
||||
- Feat/nmv3 orchestrator queue ([#6597])
|
||||
- feat: network monitor agent - standalone node stress-testing ([#6582])
|
||||
- [feat] propagate NM agent noise keys to nym-node routing ([#6577])
|
||||
- start mix stress testing topic branch ([#6575])
|
||||
- Feat/nmv3 agents subscription ([#6567])
|
||||
- Feat/nmv3 agents contract ([#6555])
|
||||
|
||||
[#6799]: https://github.com/nymtech/nym/pull/6799
|
||||
[#6772]: https://github.com/nymtech/nym/pull/6772
|
||||
[#6751]: https://github.com/nymtech/nym/pull/6751
|
||||
[#6729]: https://github.com/nymtech/nym/pull/6729
|
||||
[#6726]: https://github.com/nymtech/nym/pull/6726
|
||||
[#6718]: https://github.com/nymtech/nym/pull/6718
|
||||
[#6714]: https://github.com/nymtech/nym/pull/6714
|
||||
[#6709]: https://github.com/nymtech/nym/pull/6709
|
||||
[#6693]: https://github.com/nymtech/nym/pull/6693
|
||||
[#6689]: https://github.com/nymtech/nym/pull/6689
|
||||
[#6685]: https://github.com/nymtech/nym/pull/6685
|
||||
[#6680]: https://github.com/nymtech/nym/pull/6680
|
||||
[#6679]: https://github.com/nymtech/nym/pull/6679
|
||||
[#6673]: https://github.com/nymtech/nym/pull/6673
|
||||
[#6626]: https://github.com/nymtech/nym/pull/6626
|
||||
[#6597]: https://github.com/nymtech/nym/pull/6597
|
||||
[#6582]: https://github.com/nymtech/nym/pull/6582
|
||||
[#6577]: https://github.com/nymtech/nym/pull/6577
|
||||
[#6575]: https://github.com/nymtech/nym/pull/6575
|
||||
[#6567]: https://github.com/nymtech/nym/pull/6567
|
||||
[#6555]: https://github.com/nymtech/nym/pull/6555
|
||||
|
||||
## [2026.9-venaco] (2026-05-06)
|
||||
|
||||
- Fix for v9 IPR ([#6710])
|
||||
|
||||
Generated
+110
-12
@@ -5726,7 +5726,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-api"
|
||||
version = "1.1.79"
|
||||
version = "1.1.80"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -5971,7 +5971,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-cli"
|
||||
version = "1.1.76"
|
||||
version = "1.1.77"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64 0.22.1",
|
||||
@@ -6054,7 +6054,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-client"
|
||||
version = "1.1.76"
|
||||
version = "1.1.77"
|
||||
dependencies = [
|
||||
"bs58",
|
||||
"clap",
|
||||
@@ -6563,6 +6563,8 @@ dependencies = [
|
||||
"serde",
|
||||
"thiserror 2.0.12",
|
||||
"time",
|
||||
"tokio",
|
||||
"wasmtimer",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
@@ -7418,7 +7420,6 @@ version = "1.20.4"
|
||||
dependencies = [
|
||||
"cargo_metadata 0.19.2",
|
||||
"dotenvy",
|
||||
"log",
|
||||
"regex",
|
||||
"schemars 0.8.22",
|
||||
"serde",
|
||||
@@ -7463,9 +7464,103 @@ dependencies = [
|
||||
"utoipa-swagger-ui",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nym-network-monitor-agent"
|
||||
version = "1.0.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arrayref",
|
||||
"clap",
|
||||
"futures",
|
||||
"hkdf",
|
||||
"humantime",
|
||||
"lioness",
|
||||
"nym-bin-common",
|
||||
"nym-crypto",
|
||||
"nym-network-monitor-orchestrator-requests",
|
||||
"nym-noise",
|
||||
"nym-pemstore",
|
||||
"nym-sphinx-addressing",
|
||||
"nym-sphinx-framing",
|
||||
"nym-sphinx-params",
|
||||
"nym-sphinx-types",
|
||||
"nym-task",
|
||||
"nym-test-utils",
|
||||
"rand 0.8.5",
|
||||
"sha2 0.10.9",
|
||||
"time",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
"url",
|
||||
"x25519-dalek",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nym-network-monitor-orchestrator"
|
||||
version = "1.0.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum 0.7.9",
|
||||
"clap",
|
||||
"futures",
|
||||
"humantime",
|
||||
"nym-api-requests",
|
||||
"nym-bin-common",
|
||||
"nym-crypto",
|
||||
"nym-http-api-common",
|
||||
"nym-metrics",
|
||||
"nym-network-defaults",
|
||||
"nym-network-monitor-orchestrator-requests",
|
||||
"nym-node-requests",
|
||||
"nym-task",
|
||||
"nym-test-utils",
|
||||
"nym-validator-client",
|
||||
"rand 0.8.5",
|
||||
"sqlx",
|
||||
"strum 0.28.0",
|
||||
"thiserror 2.0.12",
|
||||
"time",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"url",
|
||||
"utoipa",
|
||||
"utoipa-swagger-ui",
|
||||
"utoipauto",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nym-network-monitor-orchestrator-requests"
|
||||
version = "1.20.4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"humantime-serde",
|
||||
"nym-crypto",
|
||||
"nym-http-api-client",
|
||||
"serde",
|
||||
"time",
|
||||
"tracing",
|
||||
"utoipa",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nym-network-monitors-contract-common"
|
||||
version = "1.20.4"
|
||||
dependencies = [
|
||||
"cosmwasm-schema",
|
||||
"cosmwasm-std",
|
||||
"cw-controllers",
|
||||
"schemars 0.8.22",
|
||||
"serde",
|
||||
"thiserror 2.0.12",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nym-network-requester"
|
||||
version = "1.1.77"
|
||||
version = "1.1.78"
|
||||
dependencies = [
|
||||
"addr",
|
||||
"anyhow",
|
||||
@@ -7515,7 +7610,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-node"
|
||||
version = "1.31.0"
|
||||
version = "1.32.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arc-swap",
|
||||
@@ -7584,6 +7679,7 @@ dependencies = [
|
||||
"nym-verloc",
|
||||
"nym-wireguard",
|
||||
"nym-wireguard-types",
|
||||
"nyxd-scraper-shared",
|
||||
"opentelemetry",
|
||||
"opentelemetry_sdk",
|
||||
"rand 0.8.5",
|
||||
@@ -7634,6 +7730,7 @@ dependencies = [
|
||||
"nym-exit-policy",
|
||||
"nym-http-api-client",
|
||||
"nym-kkt-ciphersuite",
|
||||
"nym-network-defaults",
|
||||
"nym-noise-keys",
|
||||
"nym-test-utils",
|
||||
"nym-upgrade-mode-check",
|
||||
@@ -7652,7 +7749,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-node-status-agent"
|
||||
version = "2.0.0"
|
||||
version = "2.0.1-rc3"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
@@ -7673,7 +7770,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-node-status-api"
|
||||
version = "4.6.1"
|
||||
version = "4.6.2-rc10"
|
||||
dependencies = [
|
||||
"ammonia",
|
||||
"anyhow",
|
||||
@@ -8067,7 +8164,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-socks5-client"
|
||||
version = "1.1.76"
|
||||
version = "1.1.77"
|
||||
dependencies = [
|
||||
"bs58",
|
||||
"clap",
|
||||
@@ -8572,6 +8669,7 @@ dependencies = [
|
||||
"nym-mixnet-contract-common",
|
||||
"nym-multisig-contract-common",
|
||||
"nym-network-defaults",
|
||||
"nym-network-monitors-contract-common",
|
||||
"nym-performance-contract-common",
|
||||
"nym-serde-helpers",
|
||||
"nym-vesting-contract-common",
|
||||
@@ -8865,7 +8963,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nymvisor"
|
||||
version = "0.1.41"
|
||||
version = "0.1.42"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
@@ -9635,9 +9733,9 @@ checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
|
||||
|
||||
[[package]]
|
||||
name = "prefix-trie"
|
||||
version = "0.8.3"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "90f561214012d3fc240a1f9c817cc4d57f5310910d066069c1b093f766bb5966"
|
||||
checksum = "4cf6e3177f0684016a5c209b00882e15f8bdd3f3bb48f0491df10cd102d0c6e7"
|
||||
dependencies = [
|
||||
"either",
|
||||
"ipnet",
|
||||
|
||||
+10
@@ -44,6 +44,7 @@ members = [
|
||||
"common/cosmwasm-smart-contracts/nym-performance-contract",
|
||||
"common/cosmwasm-smart-contracts/nym-pool-contract",
|
||||
"common/cosmwasm-smart-contracts/vesting-contract",
|
||||
"common/cosmwasm-smart-contracts/network-monitors-contract",
|
||||
"common/credential-proxy",
|
||||
"common/credential-storage",
|
||||
"common/credential-utils",
|
||||
@@ -176,6 +177,8 @@ members = [
|
||||
"integration-tests",
|
||||
"common/nym-kkt-ciphersuite",
|
||||
"common/nym-kkt-context",
|
||||
"nym-network-monitor-v3/nym-network-monitor-orchestrator",
|
||||
"nym-network-monitor-v3/nym-network-monitor-agent", "nym-network-monitor-v3/nym-network-monitor-orchestrator-requests",
|
||||
]
|
||||
|
||||
default-members = [
|
||||
@@ -192,6 +195,8 @@ default-members = [
|
||||
"service-providers/network-requester",
|
||||
"tools/nymvisor",
|
||||
"nym-registration-client",
|
||||
"nym-network-monitor-v3/nym-network-monitor-orchestrator",
|
||||
"nym-network-monitor-v3/nym-network-monitor-agent",
|
||||
"tools/internal/localnet-orchestrator"
|
||||
]
|
||||
|
||||
@@ -402,6 +407,10 @@ zeroize = "1.7.0"
|
||||
|
||||
prometheus = { version = "0.14.0" }
|
||||
|
||||
# recreating lioness
|
||||
# we don't care about particular versions - just pull whatever is used by sphinx
|
||||
lioness = "*"
|
||||
arrayref = "*"
|
||||
|
||||
# libcrux
|
||||
libcrux-kem = "0.0.7"
|
||||
@@ -507,6 +516,7 @@ nym-types = { version = "1.20.4", path = "common/types" }
|
||||
nym-upgrade-mode-check = { version = "1.20.4", path = "common/upgrade-mode-check" }
|
||||
nym-validator-client = { version = "1.20.4", path = "common/client-libs/validator-client", default-features = false }
|
||||
nym-vesting-contract-common = { version = "1.20.4", path = "common/cosmwasm-smart-contracts/vesting-contract" }
|
||||
nym-network-monitors-contract-common = { version = "1.20.4", path = "common/cosmwasm-smart-contracts/network-monitors-contract" }
|
||||
nym-verloc = { version = "1.20.4", path = "common/verloc" }
|
||||
nym-wireguard = { version = "1.20.4", path = "common/wireguard" }
|
||||
nym-wireguard-types = { version = "1.20.4", path = "common/wireguard-types" }
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "nym-client"
|
||||
description = "Implementation of the Nym Client"
|
||||
version = "1.1.76"
|
||||
version = "1.1.77"
|
||||
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>", "Jędrzej Stuczyński <andrew@nymtech.net>"]
|
||||
edition = "2021"
|
||||
license.workspace = true
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "nym-socks5-client"
|
||||
description = "A SOCKS5 localhost proxy that converts incoming messages to Sphinx and sends them to a Nym address"
|
||||
version = "1.1.76"
|
||||
version = "1.1.77"
|
||||
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>"]
|
||||
edition = "2021"
|
||||
license.workspace = true
|
||||
|
||||
@@ -240,7 +240,7 @@ mod nonwasm_sealed {
|
||||
impl GatewaySender for LocalGateway {
|
||||
async fn send_mix_packet(&mut self, packet: MixPacket) -> Result<(), ErasedGatewayError> {
|
||||
self.packet_forwarder
|
||||
.forward_packet(packet)
|
||||
.forward_client_packet_without_delay(packet)
|
||||
.map_err(erase_err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,3 +34,4 @@ client = ["tokio-util", "nym-task", "tokio/net", "tokio/rt"]
|
||||
[dev-dependencies]
|
||||
nym-crypto = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
tokio = { workspace = true, features = ["macros", "io-util", "rt", "rt-multi-thread"] }
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use dashmap::DashMap;
|
||||
use futures::StreamExt;
|
||||
use futures::{SinkExt, StreamExt};
|
||||
use nym_noise::config::NoiseConfig;
|
||||
use nym_noise::upgrade_noise_initiator;
|
||||
use nym_sphinx::forwarding::packet::MixPacket;
|
||||
@@ -14,6 +14,7 @@ use std::ops::Deref;
|
||||
use std::sync::atomic::{AtomicU32, AtomicUsize, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tokio::io::{AsyncRead, AsyncWrite};
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::sync::mpsc::error::TrySendError;
|
||||
@@ -90,13 +91,17 @@ impl Deref for ActiveConnections {
|
||||
pub struct ConnectionSender {
|
||||
channel: mpsc::Sender<FramedNymPacket>,
|
||||
current_reconnection_attempt: Arc<AtomicU32>,
|
||||
// Identifies the `ManagedConnection` task currently owning this entry; used
|
||||
// to ensure drop-time eviction only fires on the still-owning task.
|
||||
handle_token: Arc<()>,
|
||||
}
|
||||
|
||||
impl ConnectionSender {
|
||||
fn new(channel: mpsc::Sender<FramedNymPacket>) -> Self {
|
||||
fn new(channel: mpsc::Sender<FramedNymPacket>, handle_token: Arc<()>) -> Self {
|
||||
ConnectionSender {
|
||||
channel,
|
||||
current_reconnection_attempt: Arc::new(AtomicU32::new(0)),
|
||||
handle_token,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -107,6 +112,31 @@ struct ManagedConnection {
|
||||
message_receiver: ReceiverStream<FramedNymPacket>,
|
||||
connection_timeout: Duration,
|
||||
current_reconnection: Arc<AtomicU32>,
|
||||
active_connections: ActiveConnections,
|
||||
handle_token: Arc<()>,
|
||||
}
|
||||
|
||||
// Evicts the cache entry on task exit (only if still owned by this task).
|
||||
// Without this, a stale `ConnectionSender` survives after the peer disconnects
|
||||
// and the next outbound packet is silently swallowed by the dead TCP.
|
||||
struct EvictOnDrop {
|
||||
active_connections: ActiveConnections,
|
||||
address: SocketAddr,
|
||||
handle_token: Arc<()>,
|
||||
}
|
||||
|
||||
impl Drop for EvictOnDrop {
|
||||
fn drop(&mut self) {
|
||||
let address = self.address;
|
||||
let handle_token = &self.handle_token;
|
||||
self.active_connections.remove_if(&address, |_, sender| {
|
||||
Arc::ptr_eq(&sender.handle_token, handle_token)
|
||||
});
|
||||
trace!(
|
||||
peer = %address,
|
||||
"managed connection task exited; evicted owning cache entry"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
impl ManagedConnection {
|
||||
@@ -116,6 +146,8 @@ impl ManagedConnection {
|
||||
message_receiver: mpsc::Receiver<FramedNymPacket>,
|
||||
connection_timeout: Duration,
|
||||
current_reconnection: Arc<AtomicU32>,
|
||||
active_connections: ActiveConnections,
|
||||
handle_token: Arc<()>,
|
||||
) -> Self {
|
||||
ManagedConnection {
|
||||
address,
|
||||
@@ -123,72 +155,30 @@ impl ManagedConnection {
|
||||
message_receiver: ReceiverStream::new(message_receiver),
|
||||
connection_timeout,
|
||||
current_reconnection,
|
||||
active_connections,
|
||||
handle_token,
|
||||
}
|
||||
}
|
||||
|
||||
async fn run(self) {
|
||||
let address = self.address;
|
||||
let _evict_guard = EvictOnDrop {
|
||||
active_connections: self.active_connections,
|
||||
address,
|
||||
handle_token: self.handle_token,
|
||||
};
|
||||
|
||||
let reconnection_attempt = self.current_reconnection.load(Ordering::Acquire);
|
||||
let connect_start = tokio::time::Instant::now();
|
||||
let connection_fut = TcpStream::connect(address);
|
||||
|
||||
let conn = match tokio::time::timeout(self.connection_timeout, connection_fut).await {
|
||||
Ok(stream_res) => match stream_res {
|
||||
Ok(stream) => {
|
||||
let connect_ms = connect_start.elapsed().as_millis() as u64;
|
||||
debug!(
|
||||
peer = %address,
|
||||
connect_ms,
|
||||
"Managed to establish connection to {}", self.address
|
||||
);
|
||||
|
||||
let noise_start = tokio::time::Instant::now();
|
||||
let noise_stream =
|
||||
match upgrade_noise_initiator(stream, &self.noise_config).await {
|
||||
Ok(noise_stream) => noise_stream,
|
||||
Err(err) => {
|
||||
let noise_handshake_ms = noise_start.elapsed().as_millis() as u64;
|
||||
warn!(
|
||||
event = "connection.failed.noise",
|
||||
peer = %address,
|
||||
error = %err,
|
||||
connect_ms,
|
||||
noise_handshake_ms,
|
||||
reconnection_attempt,
|
||||
exit_reason = "noise_error",
|
||||
"Failed to perform Noise initiator handshake with {address}"
|
||||
);
|
||||
self.current_reconnection.fetch_add(1, Ordering::SeqCst);
|
||||
return;
|
||||
}
|
||||
};
|
||||
let noise_handshake_ms = noise_start.elapsed().as_millis() as u64;
|
||||
self.current_reconnection.store(0, Ordering::Release);
|
||||
debug!(
|
||||
peer = %address,
|
||||
connect_ms,
|
||||
noise_handshake_ms,
|
||||
"Noise initiator handshake completed for {:?}", address
|
||||
);
|
||||
Framed::new(noise_stream, NymCodec)
|
||||
}
|
||||
Err(err) => {
|
||||
let connect_ms = connect_start.elapsed().as_millis() as u64;
|
||||
warn!(
|
||||
event = "connection.failed.connect",
|
||||
peer = %address,
|
||||
error = %err,
|
||||
connect_ms,
|
||||
reconnection_attempt,
|
||||
exit_reason = "connect_error",
|
||||
"failed to establish connection to {address}"
|
||||
);
|
||||
return;
|
||||
}
|
||||
},
|
||||
// 1. attempt to establish the connection with timeout
|
||||
let maybe_stream = match tokio::time::timeout(self.connection_timeout, connection_fut).await
|
||||
{
|
||||
Ok(stream) => stream,
|
||||
Err(_) => {
|
||||
let connect_ms = connect_start.elapsed().as_millis() as u64;
|
||||
warn!(
|
||||
debug!(
|
||||
event = "connection.failed.timeout",
|
||||
peer = %address,
|
||||
timeout_ms = self.connection_timeout.as_millis() as u64,
|
||||
@@ -203,21 +193,133 @@ impl ManagedConnection {
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(err) = self.message_receiver.map(Ok).forward(conn).await {
|
||||
warn!(
|
||||
event = "connection.forward_error",
|
||||
peer = %address,
|
||||
error = %err,
|
||||
exit_reason = "forward_error",
|
||||
"Failed to forward packets to {address}: {err}"
|
||||
);
|
||||
}
|
||||
// 2. check if it actually succeeded
|
||||
let stream = match maybe_stream {
|
||||
Ok(stream) => stream,
|
||||
Err(err) => {
|
||||
let connect_ms = connect_start.elapsed().as_millis() as u64;
|
||||
debug!(
|
||||
event = "connection.failed.connect",
|
||||
peer = %address,
|
||||
error = %err,
|
||||
connect_ms,
|
||||
reconnection_attempt,
|
||||
exit_reason = "connect_error",
|
||||
"failed to establish connection to {address}"
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let connect_ms = connect_start.elapsed().as_millis() as u64;
|
||||
debug!(
|
||||
peer = %address,
|
||||
exit_reason = "sender_dropped",
|
||||
"connection manager to {address} finished"
|
||||
connect_ms,
|
||||
"Managed to establish connection to {}", self.address
|
||||
);
|
||||
|
||||
// 3. perform noise handshake (if applicable)
|
||||
let noise_start = tokio::time::Instant::now();
|
||||
let noise_stream = match upgrade_noise_initiator(stream, &self.noise_config).await {
|
||||
Ok(noise_stream) => noise_stream,
|
||||
Err(err) => {
|
||||
let noise_handshake_ms = noise_start.elapsed().as_millis() as u64;
|
||||
debug!(
|
||||
event = "connection.failed.noise",
|
||||
peer = %address,
|
||||
error = %err,
|
||||
connect_ms,
|
||||
noise_handshake_ms,
|
||||
reconnection_attempt,
|
||||
exit_reason = "noise_error",
|
||||
"Failed to perform Noise initiator handshake with {address}"
|
||||
);
|
||||
self.current_reconnection.fetch_add(1, Ordering::SeqCst);
|
||||
return;
|
||||
}
|
||||
};
|
||||
let noise_handshake_ms = noise_start.elapsed().as_millis() as u64;
|
||||
self.current_reconnection.store(0, Ordering::Release);
|
||||
debug!(
|
||||
peer = %address,
|
||||
connect_ms,
|
||||
noise_handshake_ms,
|
||||
"Noise initiator handshake completed for {:?}", address
|
||||
);
|
||||
let conn = Framed::new(noise_stream, NymCodec);
|
||||
|
||||
// 4. start handling the framed stream
|
||||
run_io_loop(conn, self.message_receiver, address).await;
|
||||
}
|
||||
}
|
||||
|
||||
// The connection is unidirectional (send-only); we read from it solely to
|
||||
// notice peer FIN/RST while idle so we can evict the cache entry before the
|
||||
// next outbound send finds it stale.
|
||||
async fn run_io_loop<T>(
|
||||
conn: Framed<T, NymCodec>,
|
||||
mut receiver: ReceiverStream<FramedNymPacket>,
|
||||
address: SocketAddr,
|
||||
) where
|
||||
T: AsyncRead + AsyncWrite + Unpin,
|
||||
{
|
||||
let (mut sink, mut stream) = conn.split();
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
msg = stream.next() => {
|
||||
match msg {
|
||||
None => {
|
||||
debug!(
|
||||
peer = %address,
|
||||
exit_reason = "peer_closed",
|
||||
"peer closed mixnet connection to {address}"
|
||||
);
|
||||
break;
|
||||
}
|
||||
Some(Err(err)) => {
|
||||
debug!(
|
||||
event = "connection.read_error",
|
||||
peer = %address,
|
||||
error = %err,
|
||||
exit_reason = "read_error",
|
||||
"read error on mixnet connection to {address}: {err}"
|
||||
);
|
||||
break;
|
||||
}
|
||||
Some(Ok(_)) => {
|
||||
trace!(
|
||||
peer = %address,
|
||||
"unexpected inbound packet on mixnet connection to {address}; discarding"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
outgoing = receiver.next() => {
|
||||
match outgoing {
|
||||
None => {
|
||||
debug!(
|
||||
peer = %address,
|
||||
exit_reason = "sender_dropped",
|
||||
"connection manager to {address} finished"
|
||||
);
|
||||
break;
|
||||
}
|
||||
Some(packet) => {
|
||||
if let Err(err) = sink.send(packet).await {
|
||||
debug!(
|
||||
event = "connection.forward_error",
|
||||
peer = %address,
|
||||
error = %err,
|
||||
exit_reason = "forward_error",
|
||||
"Failed to forward packet to {address}: {err}"
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -264,13 +366,18 @@ impl Client {
|
||||
sender.try_send(pending_packet).unwrap();
|
||||
}
|
||||
|
||||
// Ownership token for the task we're about to spawn; lets it tell
|
||||
// on exit whether the cache entry still names it.
|
||||
let handle_token = Arc::new(());
|
||||
|
||||
// if we already tried to connect to `address` before, grab the current attempt count
|
||||
let current_reconnection_attempt =
|
||||
if let Some(mut existing) = self.active_connections.get_mut(&address) {
|
||||
existing.channel = sender;
|
||||
existing.handle_token = Arc::clone(&handle_token);
|
||||
Arc::clone(&existing.current_reconnection_attempt)
|
||||
} else {
|
||||
let new_entry = ConnectionSender::new(sender);
|
||||
let new_entry = ConnectionSender::new(sender, Arc::clone(&handle_token));
|
||||
let current_attempt = Arc::clone(&new_entry.current_reconnection_attempt);
|
||||
self.active_connections.insert(address, new_entry);
|
||||
current_attempt
|
||||
@@ -285,6 +392,7 @@ impl Client {
|
||||
|
||||
let connections_count = self.connections_count.clone();
|
||||
let noise_config = self.noise_config.clone();
|
||||
let active_connections = self.active_connections.clone();
|
||||
tokio::spawn(async move {
|
||||
// before executing the manager, wait for what was specified, if anything
|
||||
if let Some(backoff) = backoff {
|
||||
@@ -299,6 +407,8 @@ impl Client {
|
||||
receiver,
|
||||
initial_connection_timeout,
|
||||
current_reconnection_attempt,
|
||||
active_connections,
|
||||
handle_token,
|
||||
)
|
||||
.run()
|
||||
.await;
|
||||
@@ -428,4 +538,102 @@ mod tests {
|
||||
client.config.maximum_reconnection_backoff
|
||||
);
|
||||
}
|
||||
|
||||
fn test_addr() -> SocketAddr {
|
||||
"127.0.0.1:1".parse().unwrap()
|
||||
}
|
||||
|
||||
fn insert_with_token(
|
||||
active: &ActiveConnections,
|
||||
addr: SocketAddr,
|
||||
token: Arc<()>,
|
||||
) -> mpsc::Receiver<FramedNymPacket> {
|
||||
let (tx, rx) = mpsc::channel(1);
|
||||
active.insert(addr, ConnectionSender::new(tx, token));
|
||||
rx
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn evict_on_drop_removes_entry_when_token_still_matches() {
|
||||
let active = ActiveConnections::default();
|
||||
let addr = test_addr();
|
||||
let token = Arc::new(());
|
||||
let _rx = insert_with_token(&active, addr, Arc::clone(&token));
|
||||
|
||||
assert!(active.get(&addr).is_some());
|
||||
|
||||
{
|
||||
let _guard = EvictOnDrop {
|
||||
active_connections: active.clone(),
|
||||
address: addr,
|
||||
handle_token: token,
|
||||
};
|
||||
}
|
||||
|
||||
assert!(
|
||||
active.get(&addr).is_none(),
|
||||
"owning task's drop should evict the entry"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn evict_on_drop_preserves_entry_replaced_by_newer_make_connection() {
|
||||
// Simulates the race: old task's run() has returned, but before its
|
||||
// drop guard fires, a concurrent `make_connection` replaced the
|
||||
// entry's channel + handle_token with a fresh task's token.
|
||||
let active = ActiveConnections::default();
|
||||
let addr = test_addr();
|
||||
let old_token = Arc::new(());
|
||||
let new_token = Arc::new(());
|
||||
let _rx_new = insert_with_token(&active, addr, Arc::clone(&new_token));
|
||||
|
||||
{
|
||||
let _guard = EvictOnDrop {
|
||||
active_connections: active.clone(),
|
||||
address: addr,
|
||||
handle_token: old_token,
|
||||
};
|
||||
}
|
||||
|
||||
assert!(
|
||||
active.get(&addr).is_some(),
|
||||
"old task's drop must not clobber the newer entry"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn io_loop_exits_when_peer_closes_idle_connection() {
|
||||
// The fix's second half: while no packets are flowing, peer FIN/RST
|
||||
// must still be observed so the cache entry can be evicted before the
|
||||
// next send finds it stale.
|
||||
let (a, b) = tokio::io::duplex(64);
|
||||
let conn = Framed::new(a, NymCodec);
|
||||
let (_tx, rx) = mpsc::channel(1);
|
||||
|
||||
let task = tokio::spawn(run_io_loop(conn, ReceiverStream::new(rx), test_addr()));
|
||||
|
||||
// Simulate peer closing both directions of the connection.
|
||||
drop(b);
|
||||
|
||||
tokio::time::timeout(Duration::from_secs(1), task)
|
||||
.await
|
||||
.expect("io_loop must notice peer close while idle")
|
||||
.expect("io_loop task must not panic");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn io_loop_exits_when_sender_dropped() {
|
||||
let (a, _b) = tokio::io::duplex(64);
|
||||
let conn = Framed::new(a, NymCodec);
|
||||
let (tx, rx) = mpsc::channel(1);
|
||||
|
||||
let task = tokio::spawn(run_io_loop(conn, ReceiverStream::new(rx), test_addr()));
|
||||
|
||||
drop(tx);
|
||||
|
||||
tokio::time::timeout(Duration::from_secs(1), task)
|
||||
.await
|
||||
.expect("io_loop must exit when the upstream sender is dropped")
|
||||
.expect("io_loop task must not panic");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,12 +21,16 @@ impl From<mpsc::UnboundedSender<PacketToForward>> for MixForwardingSender {
|
||||
}
|
||||
|
||||
impl MixForwardingSender {
|
||||
pub fn forward_packet(&self, packet: impl Into<PacketToForward>) -> Result<(), SendError> {
|
||||
pub fn forward_packet(&self, packet: PacketToForward) -> Result<(), SendError> {
|
||||
self.0
|
||||
.unbounded_send(packet.into())
|
||||
.unbounded_send(packet)
|
||||
.map_err(|err| err.into_send_error())
|
||||
}
|
||||
|
||||
pub fn forward_client_packet_without_delay(&self, packet: MixPacket) -> Result<(), SendError> {
|
||||
self.forward_packet(PacketToForward::client_packet_without_delay(packet))
|
||||
}
|
||||
|
||||
#[allow(clippy::len_without_is_empty)]
|
||||
pub fn len(&self) -> usize {
|
||||
self.0.len()
|
||||
@@ -38,35 +42,23 @@ pub type MixForwardingReceiver = mpsc::UnboundedReceiver<PacketToForward>;
|
||||
pub struct PacketToForward {
|
||||
pub packet: MixPacket,
|
||||
pub forward_delay_target: Option<Instant>,
|
||||
}
|
||||
|
||||
impl From<MixPacket> for PacketToForward {
|
||||
fn from(packet: MixPacket) -> Self {
|
||||
PacketToForward::new_no_delay(packet)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(MixPacket, Option<Instant>)> for PacketToForward {
|
||||
fn from((packet, delay_until): (MixPacket, Option<Instant>)) -> Self {
|
||||
PacketToForward::new(packet, delay_until)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(MixPacket, Instant)> for PacketToForward {
|
||||
fn from((packet, delay_until): (MixPacket, Instant)) -> Self {
|
||||
PacketToForward::new(packet, Some(delay_until))
|
||||
}
|
||||
pub network_monitor_packet: bool,
|
||||
}
|
||||
|
||||
impl PacketToForward {
|
||||
pub fn new(packet: MixPacket, forward_delay_target: Option<Instant>) -> Self {
|
||||
pub fn new(
|
||||
packet: MixPacket,
|
||||
forward_delay_target: Option<Instant>,
|
||||
network_monitor_packet: bool,
|
||||
) -> Self {
|
||||
PacketToForward {
|
||||
packet,
|
||||
forward_delay_target,
|
||||
network_monitor_packet,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_no_delay(packet: MixPacket) -> Self {
|
||||
Self::new(packet, None)
|
||||
pub fn client_packet_without_delay(packet: MixPacket) -> Self {
|
||||
Self::new(packet, None, false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ nym-ecash-contract-common = { workspace = true }
|
||||
nym-multisig-contract-common = { workspace = true }
|
||||
nym-group-contract-common = { workspace = true }
|
||||
nym-performance-contract-common = { workspace = true }
|
||||
nym-network-monitors-contract-common = { workspace = true }
|
||||
nym-serde-helpers = { workspace = true, features = ["hex", "base64"] }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
|
||||
@@ -104,6 +104,14 @@ impl TryFrom<NymNetworkDetails> for Config {
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn new(nyxd_url: Url, api_url: Url, nyxd_config: nyxd::Config) -> Self {
|
||||
Config {
|
||||
api_url,
|
||||
nyxd_url,
|
||||
nyxd_config,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_from_nym_network_details(
|
||||
details: &NymNetworkDetails,
|
||||
) -> Result<Self, ValidatorClientError> {
|
||||
@@ -114,6 +122,15 @@ impl Config {
|
||||
.map(|url| Url::parse(url))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
if let Some(nym_api_urls) = details.nym_api_urls.as_ref() {
|
||||
api_url.extend(
|
||||
nym_api_urls
|
||||
.iter()
|
||||
.map(|url| url.url.parse())
|
||||
.collect::<Result<Vec<_>, _>>()?,
|
||||
);
|
||||
}
|
||||
|
||||
if api_url.is_empty() {
|
||||
return Err(ValidatorClientError::NoAPIUrlAvailable);
|
||||
}
|
||||
|
||||
@@ -15,11 +15,15 @@ use nym_api_requests::ecash::models::{
|
||||
VerifyEcashTicketBody,
|
||||
};
|
||||
use nym_api_requests::ecash::VerificationKeyResponse;
|
||||
use nym_api_requests::models::network_monitor::{
|
||||
KnownNetworkMonitorResponse, StressTestBatchSubmission,
|
||||
};
|
||||
use nym_api_requests::models::{
|
||||
AnnotationResponse, ApiHealthResponse, BinaryBuildInformationOwned, ChainBlocksStatusResponse,
|
||||
ChainStatusResponse, KeyRotationInfoResponse, NodePerformanceResponse, NodeRefreshBody,
|
||||
NymNodeDescriptionV1, NymNodeDescriptionV2, PerformanceHistoryResponse, RewardedSetResponse,
|
||||
SignerInformationResponse,
|
||||
AnnotationResponseV1, ApiHealthResponse, BinaryBuildInformationOwned,
|
||||
ChainBlocksStatusResponse, ChainStatusResponse, KeyRotationInfoResponse,
|
||||
NodePerformanceResponse, NodeRefreshBody, NymNodeDescriptionV1, NymNodeDescriptionV2,
|
||||
PerformanceHistoryResponse, RewardedSetResponse, SignerInformationResponse,
|
||||
StressTestBatchSubmissionResponse,
|
||||
};
|
||||
use nym_api_requests::pagination::PaginatedResponse;
|
||||
use nym_http_api_client::{ApiClient, NO_PARAMS};
|
||||
@@ -976,7 +980,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
async fn get_node_annotation(
|
||||
&self,
|
||||
node_id: NodeId,
|
||||
) -> Result<AnnotationResponse, NymAPIError> {
|
||||
) -> Result<AnnotationResponseV1, NymAPIError> {
|
||||
self.get_json(
|
||||
&[
|
||||
routes::V1_API_VERSION,
|
||||
@@ -1359,6 +1363,53 @@ pub trait NymApiClientExt: ApiClient {
|
||||
|
||||
Ok(SemiSkimmedNodesWithMetadata::new(nodes, metadata))
|
||||
}
|
||||
|
||||
/// Queries the nym-api for whether a particular ed25519 identity key is currently recognised
|
||||
/// as an authorised network monitor permitted to submit stress testing results.
|
||||
///
|
||||
/// `identity_key` is expected to be the base58-encoded form of the ed25519 public key.
|
||||
#[instrument(level = "debug", skip(self))]
|
||||
async fn get_known_network_monitor(
|
||||
&self,
|
||||
identity_key: IdentityKeyRef<'_>,
|
||||
) -> Result<KnownNetworkMonitorResponse, NymAPIError> {
|
||||
self.get_json(
|
||||
&[
|
||||
routes::V3_API_VERSION,
|
||||
routes::NYM_NODES_ROUTES,
|
||||
routes::STRESS_TESTING,
|
||||
routes::STRESS_TESTING_KNOWN_MONITORS,
|
||||
identity_key,
|
||||
],
|
||||
NO_PARAMS,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Submit a signed batch of stress-testing results to nym-api on behalf of a network monitor
|
||||
/// orchestrator.
|
||||
///
|
||||
/// The caller is expected to have produced `request` via
|
||||
/// `StressTestBatchSubmissionContent::new(...)` and signed it with the orchestrator's ed25519
|
||||
/// key; nym-api will reject submissions that are stale, replayed, unauthorised, or whose
|
||||
/// signature fails to verify.
|
||||
#[instrument(level = "debug", skip(self, request))]
|
||||
async fn submit_stress_testing_results(
|
||||
&self,
|
||||
request: &StressTestBatchSubmission,
|
||||
) -> Result<StressTestBatchSubmissionResponse, NymAPIError> {
|
||||
self.post_json(
|
||||
&[
|
||||
routes::V3_API_VERSION,
|
||||
routes::NYM_NODES_ROUTES,
|
||||
routes::STRESS_TESTING,
|
||||
routes::STRESS_TESTING_BATCH_SUBMIT,
|
||||
],
|
||||
NO_PARAMS,
|
||||
request,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
// Client is already nym_http_api_client::Client (re-exported above), so just one impl needed
|
||||
|
||||
@@ -49,6 +49,9 @@ pub mod nym_nodes {
|
||||
pub const NYM_NODES_REWARDED_SET: &str = "rewarded-set";
|
||||
pub const NYM_NODES_REFRESH_DESCRIBED: &str = "refresh-described";
|
||||
pub const BY_ADDRESSES: &str = "by-addresses";
|
||||
pub const STRESS_TESTING: &str = "stress-testing";
|
||||
pub const STRESS_TESTING_KNOWN_MONITORS: &str = "known-monitors";
|
||||
pub const STRESS_TESTING_BATCH_SUBMIT: &str = "batch-submit";
|
||||
}
|
||||
|
||||
pub const STATUS_ROUTES: &str = "status";
|
||||
|
||||
@@ -13,6 +13,7 @@ pub mod ecash_query_client;
|
||||
pub mod group_query_client;
|
||||
pub mod mixnet_query_client;
|
||||
pub mod multisig_query_client;
|
||||
pub mod network_monitors_query_client;
|
||||
pub mod performance_query_client;
|
||||
pub mod vesting_query_client;
|
||||
|
||||
@@ -22,6 +23,7 @@ pub mod ecash_signing_client;
|
||||
pub mod group_signing_client;
|
||||
pub mod mixnet_signing_client;
|
||||
pub mod multisig_signing_client;
|
||||
pub mod network_monitors_signing_client;
|
||||
pub mod performance_signing_client;
|
||||
pub mod vesting_signing_client;
|
||||
|
||||
@@ -31,6 +33,9 @@ pub use ecash_query_client::{EcashQueryClient, PagedEcashQueryClient};
|
||||
pub use group_query_client::{GroupQueryClient, PagedGroupQueryClient};
|
||||
pub use mixnet_query_client::{MixnetQueryClient, PagedMixnetQueryClient};
|
||||
pub use multisig_query_client::{MultisigQueryClient, PagedMultisigQueryClient};
|
||||
pub use network_monitors_query_client::{
|
||||
NetworkMonitorsQueryClient, PagedNetworkMonitorsQueryClient,
|
||||
};
|
||||
pub use performance_query_client::{PagedPerformanceQueryClient, PerformanceQueryClient};
|
||||
pub use vesting_query_client::{PagedVestingQueryClient, VestingQueryClient};
|
||||
|
||||
@@ -40,6 +45,7 @@ pub use ecash_signing_client::EcashSigningClient;
|
||||
pub use group_signing_client::GroupSigningClient;
|
||||
pub use mixnet_signing_client::MixnetSigningClient;
|
||||
pub use multisig_signing_client::MultisigSigningClient;
|
||||
pub use network_monitors_signing_client::NetworkMonitorsSigningClient;
|
||||
pub use performance_signing_client::PerformanceSigningClient;
|
||||
pub use vesting_signing_client::VestingSigningClient;
|
||||
|
||||
@@ -49,6 +55,7 @@ pub trait NymContractsProvider {
|
||||
fn mixnet_contract_address(&self) -> Option<&AccountId>;
|
||||
fn vesting_contract_address(&self) -> Option<&AccountId>;
|
||||
fn performance_contract_address(&self) -> Option<&AccountId>;
|
||||
fn network_monitors_contract_address(&self) -> Option<&AccountId>;
|
||||
|
||||
// coconut-related
|
||||
fn ecash_contract_address(&self) -> Option<&AccountId>;
|
||||
@@ -62,6 +69,7 @@ pub struct TypedNymContracts {
|
||||
pub mixnet_contract_address: Option<AccountId>,
|
||||
pub vesting_contract_address: Option<AccountId>,
|
||||
pub performance_contract_address: Option<AccountId>,
|
||||
pub network_monitors_contract_address: Option<AccountId>,
|
||||
|
||||
pub ecash_contract_address: Option<AccountId>,
|
||||
pub group_contract_address: Option<AccountId>,
|
||||
@@ -86,6 +94,10 @@ impl TryFrom<NymContracts> for TypedNymContracts {
|
||||
.performance_contract_address
|
||||
.map(|addr| addr.parse())
|
||||
.transpose()?,
|
||||
network_monitors_contract_address: value
|
||||
.network_monitors_contract_address
|
||||
.map(|addr| addr.parse())
|
||||
.transpose()?,
|
||||
ecash_contract_address: value
|
||||
.ecash_contract_address
|
||||
.map(|addr| addr.parse())
|
||||
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::collect_paged;
|
||||
use crate::nyxd::contract_traits::NymContractsProvider;
|
||||
use crate::nyxd::error::NyxdError;
|
||||
use crate::nyxd::CosmWasmClient;
|
||||
use async_trait::async_trait;
|
||||
use nym_network_monitors_contract_common::{
|
||||
AuthorisedNetworkMonitor, AuthorisedNetworkMonitorOrchestratorsResponse,
|
||||
AuthorisedNetworkMonitorsPagedResponse, QueryMsg as NetworkMonitorsQueryMsg,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use std::net::SocketAddr;
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
pub trait NetworkMonitorsQueryClient {
|
||||
async fn query_network_monitors_contract<T>(
|
||||
&self,
|
||||
query: NetworkMonitorsQueryMsg,
|
||||
) -> Result<T, NyxdError>
|
||||
where
|
||||
for<'a> T: Deserialize<'a>;
|
||||
|
||||
async fn get_admin(&self) -> Result<cw_controllers::AdminResponse, NyxdError> {
|
||||
self.query_network_monitors_contract(NetworkMonitorsQueryMsg::Admin {})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_network_monitor_orchestrators(
|
||||
&self,
|
||||
) -> Result<AuthorisedNetworkMonitorOrchestratorsResponse, NyxdError> {
|
||||
self.query_network_monitors_contract(
|
||||
NetworkMonitorsQueryMsg::NetworkMonitorOrchestrators {},
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_network_monitor_agents_paged(
|
||||
&self,
|
||||
start_next_after: Option<SocketAddr>,
|
||||
limit: Option<u32>,
|
||||
) -> Result<AuthorisedNetworkMonitorsPagedResponse, NyxdError> {
|
||||
self.query_network_monitors_contract(NetworkMonitorsQueryMsg::NetworkMonitorAgents {
|
||||
start_next_after,
|
||||
limit,
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
pub trait PagedNetworkMonitorsQueryClient: NetworkMonitorsQueryClient {
|
||||
async fn get_all_network_monitor_agents(
|
||||
&self,
|
||||
) -> Result<Vec<AuthorisedNetworkMonitor>, NyxdError> {
|
||||
collect_paged!(self, get_network_monitor_agents_paged, authorised)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<T> PagedNetworkMonitorsQueryClient for T where T: NetworkMonitorsQueryClient {}
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
impl<C> NetworkMonitorsQueryClient for C
|
||||
where
|
||||
C: CosmWasmClient + NymContractsProvider + Send + Sync,
|
||||
{
|
||||
async fn query_network_monitors_contract<T>(
|
||||
&self,
|
||||
query: NetworkMonitorsQueryMsg,
|
||||
) -> Result<T, NyxdError>
|
||||
where
|
||||
for<'a> T: Deserialize<'a>,
|
||||
{
|
||||
let contract_address = &self
|
||||
.network_monitors_contract_address()
|
||||
.ok_or_else(|| NyxdError::unavailable_contract_address("network monitors contract"))?;
|
||||
self.query_contract_smart(contract_address, &query).await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::nyxd::contract_traits::tests::IgnoreValue;
|
||||
|
||||
// it's enough that this compiles and clippy is happy about it
|
||||
#[allow(dead_code)]
|
||||
fn all_query_variants_are_covered<C: NetworkMonitorsQueryClient + Send + Sync>(
|
||||
client: C,
|
||||
msg: NetworkMonitorsQueryMsg,
|
||||
) {
|
||||
match msg {
|
||||
NetworkMonitorsQueryMsg::Admin {} => client.get_admin().ignore(),
|
||||
NetworkMonitorsQueryMsg::NetworkMonitorOrchestrators {} => {
|
||||
client.get_network_monitor_orchestrators().ignore()
|
||||
}
|
||||
NetworkMonitorsQueryMsg::NetworkMonitorAgents { .. } => {
|
||||
client.get_network_monitor_agents_paged(None, None).ignore()
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
+205
@@ -0,0 +1,205 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::nyxd::contract_traits::NymContractsProvider;
|
||||
use crate::nyxd::cosmwasm_client::types::ExecuteResult;
|
||||
use crate::nyxd::error::NyxdError;
|
||||
use crate::nyxd::{Coin, Fee, SigningCosmWasmClient};
|
||||
use crate::signing::signer::OfflineSigner;
|
||||
use async_trait::async_trait;
|
||||
use nym_network_monitors_contract_common::ExecuteMsg as NetworkMonitorsExecuteMsg;
|
||||
use std::net::SocketAddr;
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
pub trait NetworkMonitorsSigningClient {
|
||||
async fn execute_network_monitors_contract(
|
||||
&self,
|
||||
fee: Option<Fee>,
|
||||
msg: NetworkMonitorsExecuteMsg,
|
||||
memo: String,
|
||||
funds: Vec<Coin>,
|
||||
) -> Result<ExecuteResult, NyxdError>;
|
||||
|
||||
async fn update_admin(
|
||||
&self,
|
||||
admin: String,
|
||||
fee: Option<Fee>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
let msg = NetworkMonitorsExecuteMsg::UpdateAdmin { admin };
|
||||
self.execute_network_monitors_contract(
|
||||
fee,
|
||||
msg,
|
||||
"NetworkMonitorsExecuteMsg::UpdateAdmin".into(),
|
||||
vec![],
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn authorise_network_monitor_orchestrator(
|
||||
&self,
|
||||
address: String,
|
||||
fee: Option<Fee>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
let msg = NetworkMonitorsExecuteMsg::AuthoriseNetworkMonitorOrchestrator { address };
|
||||
self.execute_network_monitors_contract(
|
||||
fee,
|
||||
msg,
|
||||
"NetworkMonitorsExecuteMsg::AuthoriseNetworkMonitorOrchestrator".into(),
|
||||
vec![],
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Announce (or rotate) the ed25519 identity key of the calling network monitor orchestrator.
|
||||
///
|
||||
/// The caller must already be an authorised orchestrator; the contract validates that
|
||||
/// `identity_key` is a well-formed base-58 encoding of a 32-byte ed25519 public key.
|
||||
async fn update_orchestrator_identity_key(
|
||||
&self,
|
||||
identity_key: String,
|
||||
fee: Option<Fee>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
let msg = NetworkMonitorsExecuteMsg::UpdateOrchestratorIdentityKey { key: identity_key };
|
||||
self.execute_network_monitors_contract(
|
||||
fee,
|
||||
msg,
|
||||
"NetworkMonitorsExecuteMsg::UpdateOrchestratorIdentityKey".into(),
|
||||
vec![],
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn revoke_network_monitor_orchestrator(
|
||||
&self,
|
||||
address: String,
|
||||
fee: Option<Fee>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
let msg = NetworkMonitorsExecuteMsg::RevokeNetworkMonitorOrchestrator { address };
|
||||
self.execute_network_monitors_contract(
|
||||
fee,
|
||||
msg,
|
||||
"NetworkMonitorsExecuteMsg::RevokeNetworkMonitorOrchestrator".into(),
|
||||
vec![],
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn authorise_network_monitor(
|
||||
&self,
|
||||
mixnet_address: SocketAddr,
|
||||
bs58_x25519_noise: String,
|
||||
noise_version: u8,
|
||||
fee: Option<Fee>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
let msg = NetworkMonitorsExecuteMsg::AuthoriseNetworkMonitor {
|
||||
mixnet_address,
|
||||
bs58_x25519_noise,
|
||||
noise_version,
|
||||
};
|
||||
self.execute_network_monitors_contract(
|
||||
fee,
|
||||
msg,
|
||||
"NetworkMonitorsExecuteMsg::AuthoriseNetworkMonitor".into(),
|
||||
vec![],
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn revoke_network_monitor(
|
||||
&self,
|
||||
address: SocketAddr,
|
||||
fee: Option<Fee>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
let msg = NetworkMonitorsExecuteMsg::RevokeNetworkMonitor { address };
|
||||
self.execute_network_monitors_contract(
|
||||
fee,
|
||||
msg,
|
||||
"NetworkMonitorsExecuteMsg::RevokeNetworkMonitor".into(),
|
||||
vec![],
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn revoke_all_network_monitors(
|
||||
&self,
|
||||
fee: Option<Fee>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
let msg = NetworkMonitorsExecuteMsg::RevokeAllNetworkMonitors;
|
||||
self.execute_network_monitors_contract(
|
||||
fee,
|
||||
msg,
|
||||
"NetworkMonitorsExecuteMsg::RevokeAllNetworkMonitors".into(),
|
||||
vec![],
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
impl<C> NetworkMonitorsSigningClient for C
|
||||
where
|
||||
C: SigningCosmWasmClient + NymContractsProvider + Sync,
|
||||
NyxdError: From<<Self as OfflineSigner>::Error>,
|
||||
{
|
||||
async fn execute_network_monitors_contract(
|
||||
&self,
|
||||
fee: Option<Fee>,
|
||||
msg: NetworkMonitorsExecuteMsg,
|
||||
memo: String,
|
||||
funds: Vec<Coin>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
let contract_address = &self
|
||||
.network_monitors_contract_address()
|
||||
.ok_or_else(|| NyxdError::unavailable_contract_address("network monitors contract"))?;
|
||||
|
||||
let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier())));
|
||||
|
||||
let signer_address = &self.signer_addresses()[0];
|
||||
self.execute(signer_address, contract_address, &msg, fee, memo, funds)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::nyxd::contract_traits::tests::IgnoreValue;
|
||||
use nym_network_monitors_contract_common::ExecuteMsg;
|
||||
|
||||
// it's enough that this compiles and clippy is happy about it
|
||||
#[allow(dead_code)]
|
||||
fn all_execute_variants_are_covered<C: NetworkMonitorsSigningClient + Send + Sync>(
|
||||
client: C,
|
||||
msg: NetworkMonitorsExecuteMsg,
|
||||
) {
|
||||
match msg {
|
||||
NetworkMonitorsExecuteMsg::UpdateAdmin { admin } => {
|
||||
client.update_admin(admin, None).ignore()
|
||||
}
|
||||
ExecuteMsg::AuthoriseNetworkMonitorOrchestrator { address } => client
|
||||
.authorise_network_monitor_orchestrator(address, None)
|
||||
.ignore(),
|
||||
ExecuteMsg::UpdateOrchestratorIdentityKey { key } => {
|
||||
client.update_orchestrator_identity_key(key, None).ignore()
|
||||
}
|
||||
ExecuteMsg::RevokeNetworkMonitorOrchestrator { address } => client
|
||||
.revoke_network_monitor_orchestrator(address, None)
|
||||
.ignore(),
|
||||
ExecuteMsg::AuthoriseNetworkMonitor {
|
||||
mixnet_address: address,
|
||||
bs58_x25519_noise,
|
||||
noise_version,
|
||||
} => client
|
||||
.authorise_network_monitor(address, bs58_x25519_noise, noise_version, None)
|
||||
.ignore(),
|
||||
ExecuteMsg::RevokeNetworkMonitor { address } => {
|
||||
client.revoke_network_monitor(address, None).ignore()
|
||||
}
|
||||
ExecuteMsg::RevokeAllNetworkMonitors => {
|
||||
client.revoke_all_network_monitors(None).ignore()
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,7 @@ pub mod logs;
|
||||
pub mod module_traits;
|
||||
pub mod types;
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct SigningClientOptions {
|
||||
gas_price: GasPrice,
|
||||
simulated_gas_multiplier: f32,
|
||||
@@ -80,6 +80,17 @@ impl<C, S> MaybeSigningClient<C, S> {
|
||||
opts,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn clone_query_client(&self) -> MaybeSigningClient<C, NoSigner>
|
||||
where
|
||||
C: Clone,
|
||||
{
|
||||
MaybeSigningClient {
|
||||
client: self.client.clone(),
|
||||
signer: Default::default(),
|
||||
opts: self.opts.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "http-client")]
|
||||
|
||||
@@ -24,6 +24,8 @@ use async_trait::async_trait;
|
||||
use cosmrs::tendermint::{abci, evidence::Evidence, Genesis};
|
||||
use cosmrs::tx::{Raw, SignDoc};
|
||||
use cosmwasm_std::Addr;
|
||||
use nym_contracts_common::build_information::CONTRACT_BUILD_INFO_STORAGE_KEY;
|
||||
use nym_contracts_common::ContractBuildInformation;
|
||||
use nym_network_defaults::{ChainDetails, NymNetworkDetails};
|
||||
use serde::{de::DeserializeOwned, Serialize};
|
||||
use std::fmt::Debug;
|
||||
@@ -40,6 +42,7 @@ pub use crate::nyxd::{
|
||||
fee::Fee,
|
||||
};
|
||||
pub use crate::rpc::TendermintRpcClient;
|
||||
pub use bip39;
|
||||
pub use coin::Coin;
|
||||
pub use cosmrs::{
|
||||
bank::MsgSend,
|
||||
@@ -70,14 +73,19 @@ pub use tendermint_rpc::{
|
||||
Paging, Request, Response, SimpleRequest,
|
||||
};
|
||||
|
||||
pub use nym_ecash_contract_common;
|
||||
pub use nym_mixnet_contract_common;
|
||||
pub use nym_multisig_contract_common;
|
||||
pub use nym_network_monitors_contract_common;
|
||||
pub use nym_performance_contract_common;
|
||||
pub use nym_vesting_contract_common;
|
||||
|
||||
#[cfg(feature = "http-client")]
|
||||
use crate::http_client;
|
||||
#[cfg(feature = "http-client")]
|
||||
use crate::{DirectSigningHttpRpcNyxdClient, QueryHttpRpcNyxdClient};
|
||||
#[cfg(feature = "http-client")]
|
||||
use cosmrs::rpc::{HttpClient, HttpClientUrl};
|
||||
use nym_contracts_common::build_information::CONTRACT_BUILD_INFO_STORAGE_KEY;
|
||||
use nym_contracts_common::ContractBuildInformation;
|
||||
|
||||
pub mod coin;
|
||||
pub mod contract_traits;
|
||||
@@ -262,6 +270,16 @@ impl<C, S> NyxdClient<C, S> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clone_query_client(&self) -> NyxdClient<C>
|
||||
where
|
||||
C: Clone,
|
||||
{
|
||||
NyxdClient {
|
||||
client: self.client.clone_query_client(),
|
||||
config: self.config.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn current_config(&self) -> &Config {
|
||||
&self.config
|
||||
}
|
||||
@@ -289,6 +307,10 @@ impl<C, S> NyxdClient<C, S> {
|
||||
pub fn set_simulated_gas_multiplier(&mut self, multiplier: f32) {
|
||||
self.config.simulated_gas_multiplier = multiplier;
|
||||
}
|
||||
|
||||
pub fn get_nym_contracts(&self) -> TypedNymContracts {
|
||||
self.config.contracts.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl<C, S> NymContractsProvider for NyxdClient<C, S> {
|
||||
@@ -303,6 +325,12 @@ impl<C, S> NymContractsProvider for NyxdClient<C, S> {
|
||||
fn performance_contract_address(&self) -> Option<&AccountId> {
|
||||
self.config.contracts.performance_contract_address.as_ref()
|
||||
}
|
||||
fn network_monitors_contract_address(&self) -> Option<&AccountId> {
|
||||
self.config
|
||||
.contracts
|
||||
.network_monitors_contract_address
|
||||
.as_ref()
|
||||
}
|
||||
|
||||
fn ecash_contract_address(&self) -> Option<&AccountId> {
|
||||
self.config.contracts.ecash_contract_address.as_ref()
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
[package]
|
||||
name = "nym-network-monitors-contract-common"
|
||||
description = "Common library for the Nym Network Monitors contract"
|
||||
authors.workspace = true
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
documentation.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
readme.workspace = true
|
||||
version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
thiserror = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
schemars = { workspace = true }
|
||||
|
||||
cosmwasm-std = { workspace = true }
|
||||
cosmwasm-schema = { workspace = true }
|
||||
cw-controllers = { workspace = true }
|
||||
|
||||
[features]
|
||||
schema = []
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -0,0 +1,8 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
pub mod storage_keys {
|
||||
pub const CONTRACT_ADMIN: &str = "contract-admin";
|
||||
pub const AUTHORISED_ORCHESTRATORS: &str = "authorised-orchestrators";
|
||||
pub const AUTHORISED_NETWORK_MONITORS: &str = "authorised-network-monitors";
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use cosmwasm_std::Addr;
|
||||
use cw_controllers::AdminError;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug, PartialEq)]
|
||||
pub enum NetworkMonitorsContractError {
|
||||
#[error("could not perform contract migration: {comment}")]
|
||||
FailedMigration { comment: String },
|
||||
|
||||
#[error(transparent)]
|
||||
Admin(#[from] AdminError),
|
||||
|
||||
#[error("unauthorised")]
|
||||
Unauthorized,
|
||||
|
||||
#[error("address {addr} is not an authorised orchestrator")]
|
||||
NotAnOrchestrator { addr: Addr },
|
||||
|
||||
#[error("Failed to recover x25519 public key from its base58 representation: {0}")]
|
||||
MalformedX25519AgentNoiseKey(String),
|
||||
|
||||
#[error("Failed to recover ed25519 public key from its base58 representation: {0}")]
|
||||
MalformedEd25519OrchestratorIdentityKey(String),
|
||||
|
||||
#[error(transparent)]
|
||||
StdErr(#[from] cosmwasm_std::StdError),
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
pub mod constants;
|
||||
pub mod error;
|
||||
pub mod msg;
|
||||
pub mod types;
|
||||
|
||||
pub use error::*;
|
||||
pub use msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg};
|
||||
pub use types::*;
|
||||
@@ -0,0 +1,78 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use cosmwasm_schema::cw_serde;
|
||||
use std::net::SocketAddr;
|
||||
|
||||
#[cfg(feature = "schema")]
|
||||
use crate::{
|
||||
AuthorisedNetworkMonitorOrchestratorsResponse, AuthorisedNetworkMonitorsPagedResponse,
|
||||
};
|
||||
|
||||
#[cw_serde]
|
||||
pub struct InstantiateMsg {
|
||||
/// Address of the initial network monitor orchestrator.
|
||||
pub orchestrator_address: String,
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
pub enum ExecuteMsg {
|
||||
/// Change the admin
|
||||
UpdateAdmin { admin: String },
|
||||
|
||||
/// Authorise new network monitor orchestrator
|
||||
AuthoriseNetworkMonitorOrchestrator { address: String },
|
||||
|
||||
/// Attempt to update the announced identity key of this orchestrator
|
||||
UpdateOrchestratorIdentityKey { key: String },
|
||||
|
||||
/// Revoke network monitor orchestrator authorisation.
|
||||
RevokeNetworkMonitorOrchestrator { address: String },
|
||||
|
||||
/// Authorise new network monitor (or renew authorisation)
|
||||
/// granting additional privileges when sending mixnet packets to Nym nodes.
|
||||
AuthoriseNetworkMonitor {
|
||||
/// Mixnet address of the agent.
|
||||
/// The underlying ip address is going to be used as ingress to the nodes,
|
||||
/// and the full socket address announces the egress and the association with the noise key
|
||||
mixnet_address: SocketAddr,
|
||||
|
||||
/// Base-58 encoded noise key of the agent.
|
||||
bs58_x25519_noise: String,
|
||||
|
||||
/// Version of the noise protocol used by the agent.
|
||||
noise_version: u8,
|
||||
},
|
||||
|
||||
/// Revoke network monitor authorisation.
|
||||
RevokeNetworkMonitor { address: SocketAddr },
|
||||
|
||||
/// Revoke all network monitor authorisations.
|
||||
RevokeAllNetworkMonitors,
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
#[cfg_attr(feature = "schema", derive(cosmwasm_schema::QueryResponses))]
|
||||
pub enum QueryMsg {
|
||||
#[cfg_attr(feature = "schema", returns(cw_controllers::AdminResponse))]
|
||||
Admin {},
|
||||
|
||||
// no need for pagination as we don't expect even a double digit of those
|
||||
#[cfg_attr(
|
||||
feature = "schema",
|
||||
returns(AuthorisedNetworkMonitorOrchestratorsResponse)
|
||||
)]
|
||||
NetworkMonitorOrchestrators {},
|
||||
|
||||
#[cfg_attr(feature = "schema", returns(AuthorisedNetworkMonitorsPagedResponse))]
|
||||
NetworkMonitorAgents {
|
||||
/// Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response.
|
||||
start_next_after: Option<SocketAddr>,
|
||||
|
||||
/// Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default.
|
||||
limit: Option<u32>,
|
||||
},
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
pub struct MigrateMsg {}
|
||||
@@ -0,0 +1,53 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use cosmwasm_schema::cw_serde;
|
||||
use cosmwasm_std::{Addr, Timestamp};
|
||||
use std::net::SocketAddr;
|
||||
|
||||
pub type OrchestratorAddress = Addr;
|
||||
|
||||
#[cw_serde]
|
||||
pub struct AuthorisedNetworkMonitorOrchestrator {
|
||||
/// The address associated with the network monitor orchestrator.
|
||||
pub address: Addr,
|
||||
|
||||
/// Base-58 encoded identity key of the orchestrator, announced by the orchestrator itself
|
||||
/// on startup.
|
||||
pub identity_key: Option<String>,
|
||||
|
||||
/// Timestamp of when the network monitor was authorised.
|
||||
pub authorised_at: Timestamp,
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
pub struct AuthorisedNetworkMonitor {
|
||||
/// Mixnet address of the agent.
|
||||
/// The underlying ip address is going to be used as ingress to the nodes,
|
||||
/// and the full socket address announces the egress and the association with the noise key
|
||||
pub mixnet_address: SocketAddr,
|
||||
|
||||
/// The address of the orchestrator that authorised the network monitor agent.
|
||||
pub authorised_by: OrchestratorAddress,
|
||||
|
||||
/// Timestamp of when the network monitor was authorised.
|
||||
pub authorised_at: Timestamp,
|
||||
|
||||
/// Base-58 encoded noise key of the agent.
|
||||
pub bs58_x25519_noise: String,
|
||||
|
||||
/// Version of the noise protocol used by the agent.
|
||||
pub noise_version: u8,
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
pub struct AuthorisedNetworkMonitorOrchestratorsResponse {
|
||||
pub authorised: Vec<AuthorisedNetworkMonitorOrchestrator>,
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
pub struct AuthorisedNetworkMonitorsPagedResponse {
|
||||
pub authorised: Vec<AuthorisedNetworkMonitor>,
|
||||
|
||||
pub start_next_after: Option<SocketAddr>,
|
||||
}
|
||||
@@ -27,6 +27,9 @@ pub struct QuorumStateChecker {
|
||||
cancellation_token: CancellationToken,
|
||||
check_interval: Duration,
|
||||
quorum_state: QuorumState,
|
||||
|
||||
/// indicates whether the last check has been a failure
|
||||
last_failed: bool,
|
||||
}
|
||||
|
||||
impl QuorumStateChecker {
|
||||
@@ -42,6 +45,7 @@ impl QuorumStateChecker {
|
||||
quorum_state: QuorumState {
|
||||
available: Arc::new(Default::default()),
|
||||
},
|
||||
last_failed: false,
|
||||
};
|
||||
|
||||
// first check MUST succeed, otherwise we shouldn't start
|
||||
@@ -57,6 +61,7 @@ impl QuorumStateChecker {
|
||||
}
|
||||
|
||||
async fn check_quorum_state(&self) -> Result<bool, CredentialProxyError> {
|
||||
info!("checking the current quorum state");
|
||||
let client_guard = self.client.query_chain().await;
|
||||
|
||||
// split the operation as we only need to hold the reference to chain client for the first part
|
||||
@@ -64,7 +69,8 @@ impl QuorumStateChecker {
|
||||
let dkg_details = dkg_details_with_client(client_guard.deref()).await?;
|
||||
drop(client_guard);
|
||||
|
||||
let res = check_known_dealers(dkg_details).await?;
|
||||
let res = check_known_dealers(dkg_details, 4).await?;
|
||||
info!("there are {} known DKG dealers", res.results.len());
|
||||
|
||||
let Some(signing_threshold) = res.threshold else {
|
||||
warn!(
|
||||
@@ -76,15 +82,36 @@ impl QuorumStateChecker {
|
||||
let mut working_issuer = 0;
|
||||
|
||||
for result in res.results {
|
||||
let dealer = &result.information;
|
||||
let info = format!("[id: {}] @ {}", dealer.node_index, dealer.announce_address);
|
||||
if result.chain_available() && result.signing_available() {
|
||||
info!("✅ {info} is fully available");
|
||||
working_issuer += 1;
|
||||
} else if !result.chain_available() && !result.signing_available() {
|
||||
warn!("❌ {info} is not available for both chain and signing");
|
||||
} else if !result.chain_available() {
|
||||
warn!("❌ {info} is not available for chain");
|
||||
} else {
|
||||
warn!("❌ {info} is not available for signing");
|
||||
}
|
||||
}
|
||||
|
||||
Ok((working_issuer as u64) >= signing_threshold)
|
||||
let available = (working_issuer as u64) >= signing_threshold;
|
||||
|
||||
if available {
|
||||
info!(
|
||||
"✅ Quorum state is available with {working_issuer} out of {signing_threshold} issuers"
|
||||
)
|
||||
} else {
|
||||
error!(
|
||||
"❌ Quorum state is not available with {working_issuer} out of {signing_threshold} issuers"
|
||||
)
|
||||
}
|
||||
|
||||
Ok(available)
|
||||
}
|
||||
|
||||
pub async fn run_forever(self) {
|
||||
pub async fn run_forever(mut self) {
|
||||
info!("starting quorum state checker");
|
||||
loop {
|
||||
tokio::select! {
|
||||
@@ -94,7 +121,23 @@ impl QuorumStateChecker {
|
||||
}
|
||||
_ = tokio::time::sleep(self.check_interval) => {
|
||||
match self.check_quorum_state().await {
|
||||
Ok(available) => self.quorum_state.available.store(available, Ordering::SeqCst),
|
||||
Ok(available) => {
|
||||
let previous = self.quorum_state.available.load(Ordering::SeqCst);
|
||||
// only update the quorum state to a failed state if we've had two consecutive failures
|
||||
if available {
|
||||
if !previous {
|
||||
info!("quorum recovered");
|
||||
}
|
||||
self.quorum_state.available.store(true, Ordering::SeqCst);
|
||||
} else if self.last_failed {
|
||||
if previous {
|
||||
warn!("quorum became unavailable after 2 consecutive failed checks");
|
||||
}
|
||||
self.quorum_state.available.store(false, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
self.last_failed = !available;
|
||||
},
|
||||
Err(err) => error!("failed to check current quorum state: {err}"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,5 +36,13 @@ nym-ecash-contract-common = { workspace = true }
|
||||
nym-network-defaults = { workspace = true }
|
||||
nym-serde-helpers = { workspace = true, features = ["date"] }
|
||||
|
||||
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.tokio]
|
||||
workspace = true
|
||||
features = ["time"]
|
||||
|
||||
[target."cfg(target_arch = \"wasm32\")".dependencies.wasmtimer]
|
||||
workspace = true
|
||||
features = ["tokio"]
|
||||
|
||||
[dev-dependencies]
|
||||
rand = { workspace = true }
|
||||
|
||||
@@ -6,6 +6,7 @@ use crate::ecash::bandwidth::serialiser::VersionedSerialise;
|
||||
use crate::ecash::bandwidth::CredentialSigningData;
|
||||
use crate::ecash::utils::cred_exp_date;
|
||||
use crate::error::Error;
|
||||
use log::{debug, warn};
|
||||
use nym_api_requests::ecash::BlindSignRequestBody;
|
||||
use nym_credentials_interface::{
|
||||
aggregate_wallets, generate_keypair_user_from_seed, issue_verify, withdrawal_request,
|
||||
@@ -17,8 +18,15 @@ use nym_ecash_contract_common::deposit::DepositId;
|
||||
use nym_ecash_time::{ecash_default_expiration_date, ecash_today, EcashTime};
|
||||
use nym_validator_client::nym_api::{EpochId, NymApiClientExt};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::time::Duration;
|
||||
use time::Date;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use tokio::time::sleep;
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use wasmtimer::tokio::sleep;
|
||||
|
||||
pub use nym_validator_client::nyxd::{Coin, Hash};
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
@@ -192,6 +200,49 @@ impl IssuanceTicketBook {
|
||||
Ok(unblinded_signature)
|
||||
}
|
||||
|
||||
// ideally this would have been generic over credential type, but we really don't need secp256k1 keys for bandwidth vouchers
|
||||
pub async fn obtain_partial_ticketbook_credential_with_retries(
|
||||
&self,
|
||||
client: &nym_http_api_client::Client,
|
||||
signer_index: u64,
|
||||
validator_vk: &VerificationKeyAuth,
|
||||
signing_data: CredentialSigningData,
|
||||
max_attempts: usize,
|
||||
) -> Result<PartialWallet, Error> {
|
||||
let Some(client_url) = client.base_urls().first() else {
|
||||
return Err(Error::CredentialShareObtainFailed);
|
||||
};
|
||||
let mut last_err = None;
|
||||
for attempt in 0..max_attempts {
|
||||
if attempt > 0 {
|
||||
sleep(Duration::from_millis(500 * attempt as u64)).await;
|
||||
}
|
||||
debug!(
|
||||
"attempt {} / {max_attempts} to obtain partial ticketbook credential from {client_url}",
|
||||
attempt + 1,
|
||||
);
|
||||
match self
|
||||
.obtain_partial_ticketbook_credential(
|
||||
client,
|
||||
signer_index,
|
||||
validator_vk,
|
||||
signing_data.clone(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(partial_wallet) => return Ok(partial_wallet),
|
||||
Err(err) => {
|
||||
warn!(
|
||||
"attempt {} / {max_attempts} to obtain partial ticketbook credential from {client_url} failed: {err}",
|
||||
attempt + 1,
|
||||
);
|
||||
last_err = Some(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(last_err.unwrap_or(Error::CredentialShareObtainFailed))
|
||||
}
|
||||
|
||||
// ideally this would have been generic over credential type, but we really don't need secp256k1 keys for bandwidth vouchers
|
||||
pub async fn obtain_partial_ticketbook_credential(
|
||||
&self,
|
||||
|
||||
@@ -137,6 +137,8 @@ pub async fn obtain_aggregate_wallet(
|
||||
ecash_api_clients: &[EcashApiClient],
|
||||
threshold: u64,
|
||||
) -> Result<WalletSignatures, Error> {
|
||||
const MAX_ATTEMPTS: usize = 2;
|
||||
|
||||
if ecash_api_clients.len() < threshold as usize {
|
||||
return Err(Error::NoValidatorsAvailable);
|
||||
}
|
||||
@@ -154,11 +156,12 @@ pub async fn obtain_aggregate_wallet(
|
||||
);
|
||||
|
||||
match voucher
|
||||
.obtain_partial_ticketbook_credential(
|
||||
.obtain_partial_ticketbook_credential_with_retries(
|
||||
&ecash_api_client.api_client,
|
||||
ecash_api_client.node_id,
|
||||
&ecash_api_client.verification_key,
|
||||
request.clone(),
|
||||
MAX_ATTEMPTS,
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -167,6 +170,11 @@ pub async fn obtain_aggregate_wallet(
|
||||
warn!("failed to obtain partial credential from API {ecash_api_client}: {err}",);
|
||||
}
|
||||
};
|
||||
|
||||
// we got sufficient number of shares
|
||||
if wallets.len() >= threshold as usize {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if wallets.len() < threshold as usize {
|
||||
return Err(Error::NotEnoughShares);
|
||||
|
||||
@@ -63,6 +63,9 @@ pub enum Error {
|
||||
|
||||
#[error("failed to create a secp256k1 signature")]
|
||||
Secp256k1SignFailure,
|
||||
|
||||
#[error("failed to obtain a valid credential share")]
|
||||
CredentialShareObtainFailed,
|
||||
}
|
||||
|
||||
impl From<NymAPIError> for Error {
|
||||
|
||||
@@ -31,3 +31,5 @@ pub use aes_gcm_siv::{Aes128GcmSiv, Aes256GcmSiv};
|
||||
pub use blake3;
|
||||
#[cfg(feature = "stream_cipher")]
|
||||
pub use ctr;
|
||||
#[cfg(feature = "hashing")]
|
||||
pub use sha2;
|
||||
|
||||
@@ -36,6 +36,7 @@ pub trait ChainResponse: Verifiable + TimestampedResponse {
|
||||
|
||||
// we rely on information provided from the api itself AS LONG AS it's not too outdated
|
||||
if self.timestamp() + stale_response_threshold < now {
|
||||
warn!("chain status response is stale");
|
||||
return false;
|
||||
}
|
||||
self.chain_synced()
|
||||
@@ -96,26 +97,27 @@ pub trait SignerResponse: Verifiable + TimestampedResponse {
|
||||
|
||||
// we rely on information provided from the api itself AS LONG AS it's not too outdated
|
||||
if self.timestamp() + stale_response_threshold < now {
|
||||
warn!("stale signer response");
|
||||
return false;
|
||||
}
|
||||
|
||||
if !self.has_signing_keys() {
|
||||
debug!("missing signing keys");
|
||||
warn!("missing signing keys");
|
||||
return false;
|
||||
}
|
||||
|
||||
if self.signer_disabled() {
|
||||
debug!("signer functionalities explicitly disabled");
|
||||
warn!("signer functionalities are explicitly disabled");
|
||||
return false;
|
||||
}
|
||||
|
||||
if !self.is_ecash_signer() {
|
||||
debug!("signer doesn't recognise it's a signer for this epoch");
|
||||
warn!("signer doesn't recognise it's a signer for this epoch");
|
||||
return false;
|
||||
}
|
||||
|
||||
if dkg_epoch_id != self.dkg_ecash_epoch_id() {
|
||||
debug!(
|
||||
warn!(
|
||||
"mismatched dkg epoch id. current: {dkg_epoch_id}, signer's: {}",
|
||||
self.dkg_ecash_epoch_id()
|
||||
);
|
||||
|
||||
@@ -11,10 +11,11 @@ use nym_crypto::asymmetric::ed25519;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::time::Duration;
|
||||
use time::OffsetDateTime;
|
||||
use tracing::warn;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
pub(crate) const CHAIN_STALL_THRESHOLD: Duration = Duration::from_secs(5 * 60);
|
||||
pub(crate) const STALE_RESPONSE_THRESHOLD: Duration = Duration::from_secs(5 * 60);
|
||||
pub(crate) const CHAIN_STALL_THRESHOLD: Duration = Duration::from_secs(10 * 60);
|
||||
pub(crate) const STALE_RESPONSE_THRESHOLD: Duration = Duration::from_secs(10 * 60);
|
||||
|
||||
// the reason for generics is not to remove duplication of code,
|
||||
// but because without them, we'd be having problems with circular dependencies,
|
||||
@@ -188,6 +189,7 @@ where
|
||||
};
|
||||
|
||||
let SignerStatus::Tested { result } = &self.status else {
|
||||
warn!("no valid chain response");
|
||||
return false;
|
||||
};
|
||||
result
|
||||
@@ -239,6 +241,7 @@ where
|
||||
};
|
||||
|
||||
let SignerStatus::Tested { result } = &self.status else {
|
||||
warn!("no valid signer response");
|
||||
return false;
|
||||
};
|
||||
result.signing_status.signing_available(
|
||||
|
||||
@@ -195,9 +195,9 @@ impl ClientUnderTest {
|
||||
pub(crate) async fn check_client(
|
||||
dealer_details: DealerDetails,
|
||||
dkg_epoch: u64,
|
||||
contract_share: Option<&ContractVKShare>,
|
||||
contract_share: Option<ContractVKShare>,
|
||||
) -> TypedSignerResult {
|
||||
let dealer_information = RawDealerInformation::new(&dealer_details, contract_share);
|
||||
let dealer_information = RawDealerInformation::new(&dealer_details, contract_share.as_ref());
|
||||
|
||||
// 7. attempt to construct client instances out of them
|
||||
let Ok(parsed_information) = dealer_information.parse() else {
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::client_check::check_client;
|
||||
use futures::stream::{FuturesUnordered, StreamExt};
|
||||
use futures::stream;
|
||||
use futures::stream::StreamExt;
|
||||
use nym_ecash_signer_check_types::status::{SignerResult, Status};
|
||||
use nym_network_defaults::NymNetworkDetails;
|
||||
use nym_validator_client::QueryHttpRpcNyxdClient;
|
||||
@@ -65,7 +66,7 @@ where
|
||||
C: DkgQueryClient + Sync,
|
||||
{
|
||||
let dkg_details = dkg_details_with_client(client).await?;
|
||||
check_known_dealers(dkg_details).await
|
||||
check_known_dealers(dkg_details, None).await
|
||||
}
|
||||
|
||||
pub async fn dkg_details_with_client<C>(client: &C) -> Result<DkgDetails, SignerCheckError>
|
||||
@@ -109,18 +110,21 @@ where
|
||||
|
||||
pub async fn check_known_dealers(
|
||||
dkg_details: DkgDetails,
|
||||
concurrency: impl Into<Option<usize>>,
|
||||
) -> Result<SignersTestResult, SignerCheckError> {
|
||||
// 6. for each dealer attempt to perform the checks
|
||||
let results = dkg_details
|
||||
.network_dealers
|
||||
.into_iter()
|
||||
.map(|d| {
|
||||
let share = dkg_details.submitted_shared.get(&d.assigned_index);
|
||||
check_client(d, dkg_details.dkg_epoch.epoch_id, share)
|
||||
})
|
||||
.collect::<FuturesUnordered<_>>()
|
||||
.collect::<Vec<_>>()
|
||||
.await;
|
||||
let epoch_id = dkg_details.dkg_epoch.epoch_id;
|
||||
let submitted = dkg_details.submitted_shared;
|
||||
let dealers = dkg_details.network_dealers.len();
|
||||
|
||||
let tasks = dkg_details.network_dealers.into_iter().map(move |d| {
|
||||
let share = submitted.get(&d.assigned_index).cloned();
|
||||
check_client(d, epoch_id, share)
|
||||
});
|
||||
|
||||
let limit = concurrency.into().filter(|&n| n > 0).unwrap_or(dealers);
|
||||
|
||||
let results = stream::iter(tasks).buffer_unordered(limit).collect().await;
|
||||
|
||||
Ok(SignersTestResult {
|
||||
threshold: dkg_details.threshold,
|
||||
|
||||
@@ -10,7 +10,9 @@ fn sanitize_fragment(segment: &str) -> &str {
|
||||
segment.trim_matches(|c: char| c.is_whitespace() || c == '/')
|
||||
}
|
||||
|
||||
/// Defines a path that can be used to make a request to an API.
|
||||
pub trait RequestPath: Debug {
|
||||
/// Sanitise the request path by removing empty segments and trimming whitespace and slashes
|
||||
fn to_sanitized_segments(&self) -> Vec<&str>;
|
||||
}
|
||||
|
||||
|
||||
@@ -20,21 +20,6 @@ pub const MAX_NON_STREAM_VERSION: u8 = v8::VERSION;
|
||||
/// mixnet sends, matching the node-side enforcement in `ip-packet-router`.
|
||||
pub const SPHINX_STREAM_VERSION_THRESHOLD: u8 = v9::VERSION;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
const _: () = {
|
||||
assert!(SPHINX_STREAM_VERSION_THRESHOLD > MAX_NON_STREAM_VERSION);
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn stream_transport_threshold_is_consistent() {
|
||||
assert_eq!(MAX_NON_STREAM_VERSION, 8);
|
||||
assert_eq!(SPHINX_STREAM_VERSION_THRESHOLD, 9);
|
||||
}
|
||||
}
|
||||
|
||||
// version 3: initial version
|
||||
// version 4: IPv6 support
|
||||
// version 5: Add severity level to info response
|
||||
|
||||
@@ -17,11 +17,10 @@ exclude = ["build.rs"]
|
||||
|
||||
[dependencies]
|
||||
dotenvy = { workspace = true, optional = true }
|
||||
log = { workspace = true, optional = true }
|
||||
schemars = { workspace = true, features = ["preserve_order"], optional = true }
|
||||
serde = { workspace = true, features = ["derive"], optional = true }
|
||||
serde_json = {workspace = true, optional = true }
|
||||
tracing = {workspace = true, optional = true }
|
||||
serde_json = { workspace = true, optional = true }
|
||||
tracing = { workspace = true, optional = true }
|
||||
url = { workspace = true, optional = true }
|
||||
utoipa = { workspace = true, optional = true }
|
||||
|
||||
@@ -30,9 +29,9 @@ utoipa = { workspace = true, optional = true }
|
||||
|
||||
[features]
|
||||
default = ["env", "network"]
|
||||
env = ["dotenvy", "log", "serde_json", "tracing"]
|
||||
env = ["dotenvy", "serde_json", "tracing"]
|
||||
network = ["schemars", "serde", "url"]
|
||||
utoipa = [ "dep:utoipa" ]
|
||||
utoipa = ["dep:utoipa"]
|
||||
|
||||
[build-dependencies]
|
||||
regex = { workspace = true }
|
||||
|
||||
@@ -27,16 +27,20 @@ fn print_env_vars_with_keys_in_file<P: AsRef<Path> + Copy>(config_env_file: P) {
|
||||
.expect("Invalid path to environment configuration file");
|
||||
for item in items {
|
||||
let (key, val) = item.expect("Invalid item in environment configuration file");
|
||||
log::debug!("{key}: {val}");
|
||||
tracing::debug!("{key}: {val}");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn env_configured() -> bool {
|
||||
std::env::var(var_names::CONFIGURED).is_ok()
|
||||
}
|
||||
|
||||
pub fn setup_env<P: AsRef<Path>>(config_env_file: Option<P>) {
|
||||
match std::env::var(var_names::CONFIGURED) {
|
||||
// if the configuration is not already set in the env vars
|
||||
Err(std::env::VarError::NotPresent) => {
|
||||
if let Some(config_env_file) = &config_env_file {
|
||||
log::debug!(
|
||||
tracing::debug!(
|
||||
"Loading environment variables from {:?}",
|
||||
config_env_file.as_ref()
|
||||
);
|
||||
@@ -47,12 +51,12 @@ pub fn setup_env<P: AsRef<Path>>(config_env_file: Option<P>) {
|
||||
// if nothing is set, the use mainnet defaults
|
||||
// if the user has not set `CONFIGURED`, then even if they set any of the env variables,
|
||||
// overwrite them
|
||||
log::debug!("Loading mainnet defaults");
|
||||
tracing::debug!("Loading mainnet defaults");
|
||||
crate::mainnet::export_to_env();
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
log::debug!("Environment variables already set. Using them");
|
||||
tracing::debug!("Environment variables already set. Using them");
|
||||
crate::mainnet::export_to_env()
|
||||
}
|
||||
_ => {
|
||||
|
||||
@@ -22,6 +22,8 @@ pub const VESTING_CONTRACT_ADDRESS: &str =
|
||||
pub const PERFORMANCE_CONTRACT_ADDRESS: &str = "";
|
||||
// /\ TODO: this has to be updated once the contract is deployed
|
||||
|
||||
pub const NETWORK_MONITORS_CONTRACT_ADDRESS: &str =
|
||||
"n1m3a2ltkjqud8mkmrpqvgllrtv2p4r6js6qwl7p8cqkzrq8jg6e2qwqgl8z";
|
||||
pub const ECASH_CONTRACT_ADDRESS: &str =
|
||||
"n1r7s6aksyc6pqardx88k3rkgfagwvj4z4zum9mmz2sfk3zm2mha0sd4dnun";
|
||||
pub const GROUP_CONTRACT_ADDRESS: &str =
|
||||
@@ -36,6 +38,10 @@ pub const REWARDING_VALIDATOR_ADDRESS: &str = "n10yyd98e2tuwu0f7ypz9dy3hhjw7v772
|
||||
pub const NYXD_URL: &str = "https://rpc.nymtech.net";
|
||||
pub const NYXD_WS: &str = "wss://rpc.nymtech.net/websocket";
|
||||
|
||||
// cluster of lite rpc nodes (not part of consensus, aggressive pruning, no archival state)
|
||||
pub const NYXD_QUERY_LITE: &str = "https://blockstream.nymtech.net";
|
||||
pub const NYXD_WS_LITE: &str = "wss://blockstream.nymtech.net/websocket";
|
||||
|
||||
pub const NYM_API: &str = "https://validator.nymtech.net/api/";
|
||||
#[cfg(feature = "network")]
|
||||
pub const NYM_APIS: &[ApiUrlConst] = &[
|
||||
@@ -43,10 +49,6 @@ pub const NYM_APIS: &[ApiUrlConst] = &[
|
||||
url: NYM_API,
|
||||
front_hosts: None,
|
||||
},
|
||||
ApiUrlConst {
|
||||
url: "https://nym-frontdoor.vercel.app/api/",
|
||||
front_hosts: Some(&["vercel.app", "vercel.com"]),
|
||||
},
|
||||
ApiUrlConst {
|
||||
url: "https://nym-frontdoor.global.ssl.fastly.net/api/",
|
||||
front_hosts: Some(&["yelp.global.ssl.fastly.net"]),
|
||||
@@ -68,7 +70,7 @@ pub const UPGRADE_MODE_ATTESTER_ED25519_BS58_PUBKEY: &str =
|
||||
pub const NYM_VPN_APIS: &[ApiUrlConst] = &[
|
||||
ApiUrlConst {
|
||||
url: NYM_VPN_API,
|
||||
front_hosts: Some(&["vercel.app", "vercel.com"]),
|
||||
front_hosts: None,
|
||||
},
|
||||
ApiUrlConst {
|
||||
url: "https://nymvpn-frontdoor.global.ssl.fastly.net/api/",
|
||||
@@ -137,6 +139,11 @@ pub fn read_parsed_var_if_not_default<T: std::str::FromStr>(
|
||||
.map(std::str::FromStr::from_str)
|
||||
}
|
||||
|
||||
#[cfg(feature = "env")]
|
||||
pub fn read_parsed_var<T: std::str::FromStr>(var: &str) -> Result<T, T::Err> {
|
||||
std::env::var(var).unwrap_or_default().parse()
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "env", feature = "network"))]
|
||||
pub fn export_to_env() {
|
||||
use crate::var_names;
|
||||
@@ -167,6 +174,14 @@ pub fn export_to_env() {
|
||||
var_names::COCONUT_DKG_CONTRACT_ADDRESS,
|
||||
COCONUT_DKG_CONTRACT_ADDRESS,
|
||||
);
|
||||
set_var_to_default(
|
||||
var_names::PERFORMANCE_CONTRACT_ADDRESS,
|
||||
PERFORMANCE_CONTRACT_ADDRESS,
|
||||
);
|
||||
set_var_to_default(
|
||||
var_names::NETWORK_MONITORS_CONTRACT_ADDRESS,
|
||||
NETWORK_MONITORS_CONTRACT_ADDRESS,
|
||||
);
|
||||
set_var_to_default(
|
||||
var_names::REWARDING_VALIDATOR_ADDRESS,
|
||||
REWARDING_VALIDATOR_ADDRESS,
|
||||
@@ -186,6 +201,8 @@ pub fn export_to_env() {
|
||||
var_names::UPGRADE_MODE_ATTESTER_ED25519_BS58_PUBKEY,
|
||||
UPGRADE_MODE_ATTESTER_ED25519_BS58_PUBKEY,
|
||||
);
|
||||
set_var_to_default(var_names::NYXD_QUERY_LITE, NYXD_QUERY_LITE);
|
||||
set_var_to_default(var_names::NYXD_WS_LITE, NYXD_WS_LITE);
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "env", feature = "network"))]
|
||||
@@ -237,4 +254,6 @@ pub fn export_to_env_if_not_set() {
|
||||
var_names::UPGRADE_MODE_ATTESTER_ED25519_BS58_PUBKEY,
|
||||
UPGRADE_MODE_ATTESTER_ED25519_BS58_PUBKEY,
|
||||
);
|
||||
set_var_conditionally_to_default(var_names::NYXD_QUERY_LITE, NYXD_QUERY_LITE);
|
||||
set_var_conditionally_to_default(var_names::NYXD_WS_LITE, NYXD_WS_LITE);
|
||||
}
|
||||
|
||||
@@ -39,6 +39,8 @@ pub struct NymContracts {
|
||||
pub vesting_contract_address: Option<String>,
|
||||
#[serde(default)]
|
||||
pub performance_contract_address: Option<String>,
|
||||
#[serde(default)]
|
||||
pub network_monitors_contract_address: Option<String>,
|
||||
pub ecash_contract_address: Option<String>,
|
||||
pub group_contract_address: Option<String>,
|
||||
pub multisig_contract_address: Option<String>,
|
||||
@@ -72,6 +74,15 @@ pub struct ApiUrl {
|
||||
pub front_hosts: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
impl From<Url> for ApiUrl {
|
||||
fn from(value: Url) -> Self {
|
||||
ApiUrl {
|
||||
url: value.to_string(),
|
||||
front_hosts: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize)]
|
||||
pub struct ApiUrlConst<'a> {
|
||||
pub url: &'a str,
|
||||
@@ -178,6 +189,10 @@ impl NymNetworkDetails {
|
||||
.with_group_contract(get_optional_env(var_names::GROUP_CONTRACT_ADDRESS))
|
||||
.with_multisig_contract(get_optional_env(var_names::MULTISIG_CONTRACT_ADDRESS))
|
||||
.with_coconut_dkg_contract(get_optional_env(var_names::COCONUT_DKG_CONTRACT_ADDRESS))
|
||||
.with_performance_contract(get_optional_env(var_names::PERFORMANCE_CONTRACT_ADDRESS))
|
||||
.with_network_monitors_contract(get_optional_env(
|
||||
var_names::NETWORK_MONITORS_CONTRACT_ADDRESS,
|
||||
))
|
||||
.with_nym_vpn_api_url(get_optional_env(var_names::NYM_VPN_API))
|
||||
.with_nym_vpn_api_urls(nym_vpn_api_urls)
|
||||
.with_nym_api_urls(nym_api_urls)
|
||||
@@ -199,6 +214,9 @@ impl NymNetworkDetails {
|
||||
performance_contract_address: parse_optional_str(
|
||||
mainnet::PERFORMANCE_CONTRACT_ADDRESS,
|
||||
),
|
||||
network_monitors_contract_address: parse_optional_str(
|
||||
mainnet::NETWORK_MONITORS_CONTRACT_ADDRESS,
|
||||
),
|
||||
ecash_contract_address: parse_optional_str(mainnet::ECASH_CONTRACT_ADDRESS),
|
||||
group_contract_address: parse_optional_str(mainnet::GROUP_CONTRACT_ADDRESS),
|
||||
multisig_contract_address: parse_optional_str(mainnet::MULTISIG_CONTRACT_ADDRESS),
|
||||
@@ -226,7 +244,7 @@ impl NymNetworkDetails {
|
||||
|
||||
fn set_optional_var(var_name: &str, value: Option<String>) {
|
||||
if let Some(value) = value {
|
||||
unsafe {set_var(var_name, value)}
|
||||
unsafe { set_var(var_name, value) }
|
||||
}
|
||||
}
|
||||
unsafe {
|
||||
@@ -364,15 +382,31 @@ impl NymNetworkDetails {
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_performance_contract<S: Into<String>>(mut self, contract: Option<S>) -> Self {
|
||||
self.contracts.performance_contract_address = contract.map(Into::into);
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_network_monitors_contract<S: Into<String>>(mut self, contract: Option<S>) -> Self {
|
||||
self.contracts.network_monitors_contract_address = contract.map(Into::into);
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_nym_vpn_api_url<S: Into<String>>(mut self, endpoint: Option<S>) -> Self {
|
||||
self.nym_vpn_api_url = endpoint.map(Into::into);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_nym_api_urls<U: Into<ApiUrl>>(&mut self, urls: Vec<U>) {
|
||||
self.nym_api_urls = Some(urls.into_iter().map(Into::into).collect());
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_nym_api_urls(mut self, urls: Vec<ApiUrl>) -> Self {
|
||||
self.nym_api_urls = Some(urls);
|
||||
pub fn with_nym_api_urls<U: Into<ApiUrl>>(mut self, urls: Vec<U>) -> Self {
|
||||
self.set_nym_api_urls(urls);
|
||||
self
|
||||
}
|
||||
|
||||
|
||||
@@ -18,11 +18,15 @@ pub const ECASH_CONTRACT_ADDRESS: &str = "ECASH_CONTRACT_ADDRESS";
|
||||
pub const GROUP_CONTRACT_ADDRESS: &str = "GROUP_CONTRACT_ADDRESS";
|
||||
pub const MULTISIG_CONTRACT_ADDRESS: &str = "MULTISIG_CONTRACT_ADDRESS";
|
||||
pub const COCONUT_DKG_CONTRACT_ADDRESS: &str = "COCONUT_DKG_CONTRACT_ADDRESS";
|
||||
pub const PERFORMANCE_CONTRACT_ADDRESS: &str = "PERFORMANCE_CONTRACT_ADDRESS";
|
||||
pub const NETWORK_MONITORS_CONTRACT_ADDRESS: &str = "NETWORK_MONITORS_CONTRACT_ADDRESS";
|
||||
pub const REWARDING_VALIDATOR_ADDRESS: &str = "REWARDING_VALIDATOR_ADDRESS";
|
||||
pub const NYXD: &str = "NYXD";
|
||||
pub const NYM_API: &str = "NYM_API";
|
||||
pub const NYM_APIS: &str = "NYM_APIS";
|
||||
pub const NYXD_WEBSOCKET: &str = "NYXD_WS";
|
||||
pub const NYXD_QUERY_LITE: &str = "NYXD_LITE";
|
||||
pub const NYXD_WS_LITE: &str = "NYXD_WS_LITE";
|
||||
pub const EXIT_POLICY_URL: &str = "EXIT_POLICY";
|
||||
pub const NYM_VPN_API: &str = "NYM_VPN_API";
|
||||
pub const NYM_VPN_APIS: &str = "NYM_VPN_APIS";
|
||||
|
||||
+385
-44
@@ -1,19 +1,19 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use arc_swap::ArcSwap;
|
||||
use nym_crypto::asymmetric::x25519;
|
||||
use snow::params::NoiseParams;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
net::{IpAddr, SocketAddr},
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use arc_swap::ArcSwap;
|
||||
use nym_crypto::asymmetric::x25519;
|
||||
use nym_noise_keys::{NoiseVersion, VersionedNoiseKeyV1};
|
||||
use snow::params::NoiseParams;
|
||||
|
||||
use strum_macros::{EnumIter, FromRepr};
|
||||
use tokio::sync::{Mutex, MutexGuard};
|
||||
|
||||
pub use nym_noise_keys::{NoiseVersion, VersionedNoiseKeyV1};
|
||||
|
||||
#[derive(Default, Debug, Clone, Copy, EnumIter, FromRepr, Eq, PartialEq)]
|
||||
#[repr(u8)]
|
||||
@@ -53,38 +53,125 @@ impl NoisePattern {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct SocketAddrToKey {
|
||||
inner: ArcSwap<HashMap<SocketAddr, VersionedNoiseKeyV1>>,
|
||||
}
|
||||
|
||||
// SW NOTE : Only for phased upgrade. To remove once we decide all nodes have to support Noise
|
||||
#[derive(Debug, Default)]
|
||||
struct IpAddrToVersion {
|
||||
inner: ArcSwap<HashMap<IpAddr, NoiseVersion>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct NoiseNetworkView {
|
||||
keys: Arc<SocketAddrToKey>,
|
||||
support: Arc<IpAddrToVersion>,
|
||||
inner: Arc<NoiseNetworkViewInner>,
|
||||
}
|
||||
|
||||
impl NoiseNetworkView {
|
||||
pub fn new_empty() -> Self {
|
||||
NoiseNetworkView {
|
||||
keys: Default::default(),
|
||||
support: Default::default(),
|
||||
/// Inner state of [`NoiseNetworkView`], shared behind an `Arc`.
|
||||
///
|
||||
/// # Concurrency model
|
||||
///
|
||||
/// Reads (on the packet-processing hot path) use `ArcSwap` and are fully lock-free.
|
||||
/// Writers must first acquire `update_lock` to serialise concurrent updates, then call
|
||||
/// `swap_view` to atomically publish the new map. The lock is intentionally *not* wrapping
|
||||
/// the map itself so that readers are never blocked.
|
||||
#[derive(Debug)]
|
||||
struct NoiseNetworkViewInner {
|
||||
update_lock: Mutex<()>,
|
||||
nodes: ArcSwap<HashMap<IpAddr, NoiseNode>>,
|
||||
}
|
||||
|
||||
/// A node in the noise network map, keyed by IP address.
|
||||
///
|
||||
/// A single IP can correspond to either one nym-node (which has a single noise key)
|
||||
/// or one-or-more network monitor agents (each with its own port and noise key).
|
||||
/// The two variants have independent lifecycles: nym-node entries come from the
|
||||
/// nym-api topology refresher, while agent entries come from blockchain events.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum NoiseNode {
|
||||
NymNode { key: VersionedNoiseKeyV1 },
|
||||
// due to the structure of network monitor agents,
|
||||
// it is possible to have multiple destinations with the same host ip address,
|
||||
// but a different noise key.
|
||||
// however, we are also guaranteed all of those are going to have a unique port.
|
||||
// note: we're not storing it in a map, since at maximum we might have maybe 20 or so
|
||||
// entries under a single ip address and linear look-up of a vec is faster than the overhead of a hashmap
|
||||
NetworkMonitorAgent { nodes: Vec<NetworkMonitorAgentNode> },
|
||||
}
|
||||
|
||||
impl NoiseNode {
|
||||
pub fn new_nym_node(key: VersionedNoiseKeyV1) -> Self {
|
||||
NoiseNode::NymNode { key }
|
||||
}
|
||||
|
||||
pub fn new_agent(socket_addr: SocketAddr, key: VersionedNoiseKeyV1) -> Self {
|
||||
NoiseNode::NetworkMonitorAgent {
|
||||
nodes: vec![NetworkMonitorAgentNode {
|
||||
port: socket_addr.port(),
|
||||
key,
|
||||
}],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn swap_view(&self, new: HashMap<SocketAddr, VersionedNoiseKeyV1>) {
|
||||
let noise_support = new
|
||||
.iter()
|
||||
.map(|(s_addr, key)| (s_addr.ip(), key.supported_version))
|
||||
.collect::<HashMap<_, _>>();
|
||||
self.keys.inner.store(Arc::new(new));
|
||||
self.support.inner.store(Arc::new(noise_support));
|
||||
pub fn is_nym_node(&self) -> bool {
|
||||
matches!(self, NoiseNode::NymNode { .. })
|
||||
}
|
||||
}
|
||||
|
||||
/// A single network monitor agent identified by its port on a shared host.
|
||||
///
|
||||
/// Multiple agents may share an IP address but are guaranteed to have unique ports.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct NetworkMonitorAgentNode {
|
||||
pub port: u16,
|
||||
pub key: VersionedNoiseKeyV1,
|
||||
}
|
||||
|
||||
impl NoiseNetworkView {
|
||||
pub fn new(nodes: HashMap<IpAddr, NoiseNode>) -> Self {
|
||||
// ensure we're always storing canonical IPs
|
||||
NoiseNetworkView {
|
||||
inner: Arc::new(NoiseNetworkViewInner {
|
||||
update_lock: Mutex::new(()),
|
||||
nodes: ArcSwap::from_pointee(
|
||||
nodes
|
||||
.into_iter()
|
||||
.map(|(k, v)| (k.to_canonical(), v))
|
||||
.collect(),
|
||||
),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_empty() -> Self {
|
||||
Self::new(Default::default())
|
||||
}
|
||||
|
||||
/// Build a noise view pre-populated with network monitor agents (used at startup).
|
||||
pub fn new_with_agents(agents: HashMap<IpAddr, Vec<NetworkMonitorAgentNode>>) -> Self {
|
||||
let mut nodes = HashMap::new();
|
||||
for (ip, agent_nodes) in agents {
|
||||
nodes.insert(ip, NoiseNode::NetworkMonitorAgent { nodes: agent_nodes });
|
||||
}
|
||||
Self::new(nodes)
|
||||
}
|
||||
|
||||
pub async fn get_update_permit(&self) -> MutexGuard<'_, ()> {
|
||||
self.inner.update_lock.lock().await
|
||||
}
|
||||
|
||||
/// Atomically replace the noise key map.
|
||||
///
|
||||
/// # Precondition
|
||||
///
|
||||
/// The caller **must** hold the permit returned by [`NoiseNetworkView::get_update_permit`].
|
||||
/// Passing the `MutexGuard` by value enforces this at the type level — the guard is dropped
|
||||
/// (releasing the lock) only after the swap completes, preventing torn writes from concurrent
|
||||
/// update calls.
|
||||
pub fn swap_view(&self, _permit: MutexGuard<'_, ()>, new: HashMap<IpAddr, NoiseNode>) {
|
||||
// defensive: ensure stored keys are always canonical so lookups (which canonicalise)
|
||||
// always match. callers should still canonicalise before assembling `new` to keep
|
||||
// collision resolution deterministic.
|
||||
let canonical = new
|
||||
.into_iter()
|
||||
.map(|(k, v)| (k.to_canonical(), v))
|
||||
.collect();
|
||||
self.inner.nodes.store(Arc::new(canonical));
|
||||
}
|
||||
|
||||
pub fn all_nodes(&self) -> HashMap<IpAddr, NoiseNode> {
|
||||
self.inner.nodes.load().as_ref().clone()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,20 +213,38 @@ impl NoiseConfig {
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn get_noise_key(&self, s_address: &SocketAddr) -> Option<VersionedNoiseKeyV1> {
|
||||
self.network.keys.inner.load().get(s_address).copied()
|
||||
/// Look up the noise key for a specific remote socket address.
|
||||
///
|
||||
/// Used on the **initiator** path where we need the responder's public key
|
||||
/// to start the handshake. For nym-nodes the port is ignored (one key per IP);
|
||||
/// for network monitor agents, the port disambiguates which agent's key to use.
|
||||
pub(crate) fn get_noise_key(&self, address: SocketAddr) -> Option<VersionedNoiseKeyV1> {
|
||||
let ip_to_check = address.ip().to_canonical();
|
||||
let nodes = self.network.inner.nodes.load();
|
||||
|
||||
// Resolve the noise key for `address` from a loaded snapshot of the node map.
|
||||
// For [`NoiseNode::NymNode`] entries the port is irrelevant — only the IP is matched.
|
||||
// For [`NoiseNode::NetworkMonitorAgent`] entries the port selects the specific agent.
|
||||
match nodes.get(&ip_to_check)? {
|
||||
NoiseNode::NymNode { key } => Some(*key),
|
||||
NoiseNode::NetworkMonitorAgent { nodes } => {
|
||||
let port = address.port();
|
||||
nodes.iter().find(|n| n.port == port).map(|n| n.key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Only for phased update
|
||||
//SW This can lead to some troubles if two nodes share the same IP and one support Noise but not the other.
|
||||
// This in only for the progressive update though and there is no workaround
|
||||
pub(crate) fn get_noise_support(&self, ip_addr: IpAddr) -> Option<NoiseVersion> {
|
||||
let plain_ip_support = self.network.support.inner.load().get(&ip_addr).copied();
|
||||
|
||||
// SW default bind address being [::]:1789, it can happen that a responder sees the ipv6-mapped address of the initiator, this check for that
|
||||
let canonical_ip = &ip_addr.to_canonical();
|
||||
let canonical_ip_support = self.network.support.inner.load().get(canonical_ip).copied();
|
||||
plain_ip_support.or(canonical_ip_support)
|
||||
/// Check whether a remote IP is known to support noise.
|
||||
/// Used on the responder path where we don't need the remote's key
|
||||
/// (the initiator sends it during the handshake).
|
||||
// note: in the case of network monitor agents, it must hold
|
||||
// that ALL agents on given host support it (or don't support it)
|
||||
pub(crate) fn supports_noise(&self, ip_addr: IpAddr) -> bool {
|
||||
self.network
|
||||
.inner
|
||||
.nodes
|
||||
.load()
|
||||
.contains_key(&ip_addr.to_canonical())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,4 +274,240 @@ mod tests {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod noise_key_lookup {
|
||||
use super::super::*;
|
||||
use nym_crypto::asymmetric::x25519;
|
||||
use nym_noise_keys::NoiseVersion;
|
||||
use nym_test_utils::helpers::deterministic_rng;
|
||||
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
fn dummy_key(seed: u8) -> VersionedNoiseKeyV1 {
|
||||
VersionedNoiseKeyV1 {
|
||||
supported_version: NoiseVersion::V1,
|
||||
x25519_pubkey: x25519::PublicKey::from([seed; 32]),
|
||||
}
|
||||
}
|
||||
|
||||
fn make_config(nodes: HashMap<IpAddr, NoiseNode>) -> NoiseConfig {
|
||||
NoiseConfig::new(
|
||||
Arc::new(x25519::KeyPair::new(&mut deterministic_rng())),
|
||||
NoiseNetworkView::new(nodes),
|
||||
Duration::from_secs(5),
|
||||
)
|
||||
}
|
||||
|
||||
// -- get_noise_key tests --
|
||||
|
||||
#[test]
|
||||
fn nym_node_key_returned_regardless_of_port() {
|
||||
let ip = IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1));
|
||||
let key = dummy_key(1);
|
||||
let config = make_config(HashMap::from([(ip, NoiseNode::new_nym_node(key))]));
|
||||
|
||||
// any port should resolve to the same key
|
||||
assert_eq!(config.get_noise_key(SocketAddr::new(ip, 1000)), Some(key));
|
||||
assert_eq!(config.get_noise_key(SocketAddr::new(ip, 9999)), Some(key));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn agent_key_resolved_by_port() {
|
||||
let ip = IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1));
|
||||
let key_a = dummy_key(1);
|
||||
let key_b = dummy_key(2);
|
||||
|
||||
let node = NoiseNode::NetworkMonitorAgent {
|
||||
nodes: vec![
|
||||
NetworkMonitorAgentNode {
|
||||
port: 1000,
|
||||
key: key_a,
|
||||
},
|
||||
NetworkMonitorAgentNode {
|
||||
port: 2000,
|
||||
key: key_b,
|
||||
},
|
||||
],
|
||||
};
|
||||
let config = make_config(HashMap::from([(ip, node)]));
|
||||
|
||||
assert_eq!(config.get_noise_key(SocketAddr::new(ip, 1000)), Some(key_a));
|
||||
assert_eq!(config.get_noise_key(SocketAddr::new(ip, 2000)), Some(key_b));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn agent_unknown_port_returns_none() {
|
||||
let ip = IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1));
|
||||
let node = NoiseNode::NetworkMonitorAgent {
|
||||
nodes: vec![NetworkMonitorAgentNode {
|
||||
port: 1000,
|
||||
key: dummy_key(1),
|
||||
}],
|
||||
};
|
||||
let config = make_config(HashMap::from([(ip, node)]));
|
||||
|
||||
assert!(config.get_noise_key(SocketAddr::new(ip, 9999)).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn completely_unknown_address_returns_none() {
|
||||
let config = make_config(HashMap::new());
|
||||
|
||||
assert!(config
|
||||
.get_noise_key(SocketAddr::new(IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4)), 80))
|
||||
.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn canonical_ipv6_fallback_for_nym_node() {
|
||||
// register under the plain IPv4 address
|
||||
let v4 = IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4));
|
||||
let key = dummy_key(1);
|
||||
let config = make_config(HashMap::from([(v4, NoiseNode::new_nym_node(key))]));
|
||||
|
||||
// query with the IPv4-mapped IPv6 form (::ffff:1.2.3.4)
|
||||
let v6_mapped = IpAddr::V6(Ipv4Addr::new(1, 2, 3, 4).to_ipv6_mapped());
|
||||
assert_eq!(
|
||||
config.get_noise_key(SocketAddr::new(v6_mapped, 1789)),
|
||||
Some(key)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn canonical_ipv6_fallback_for_agent() {
|
||||
let v4 = IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4));
|
||||
let key = dummy_key(1);
|
||||
let node = NoiseNode::NetworkMonitorAgent {
|
||||
nodes: vec![NetworkMonitorAgentNode { port: 1000, key }],
|
||||
};
|
||||
let config = make_config(HashMap::from([(v4, node)]));
|
||||
|
||||
let v6_mapped = IpAddr::V6(Ipv4Addr::new(1, 2, 3, 4).to_ipv6_mapped());
|
||||
assert_eq!(
|
||||
config.get_noise_key(SocketAddr::new(v6_mapped, 1000)),
|
||||
Some(key)
|
||||
);
|
||||
// wrong port still returns None even with the fallback
|
||||
assert!(config
|
||||
.get_noise_key(SocketAddr::new(v6_mapped, 9999))
|
||||
.is_none());
|
||||
}
|
||||
|
||||
// -- supports_noise tests --
|
||||
|
||||
#[test]
|
||||
fn supports_noise_true_for_nym_node() {
|
||||
let ip = IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1));
|
||||
let config = make_config(HashMap::from([(ip, NoiseNode::new_nym_node(dummy_key(1)))]));
|
||||
|
||||
assert!(config.supports_noise(ip));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn supports_noise_true_for_agent_ip() {
|
||||
let ip = IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1));
|
||||
let node = NoiseNode::NetworkMonitorAgent {
|
||||
nodes: vec![NetworkMonitorAgentNode {
|
||||
port: 1000,
|
||||
key: dummy_key(1),
|
||||
}],
|
||||
};
|
||||
let config = make_config(HashMap::from([(ip, node)]));
|
||||
|
||||
assert!(config.supports_noise(ip));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn supports_noise_false_for_unknown_ip() {
|
||||
let config = make_config(HashMap::new());
|
||||
|
||||
assert!(!config.supports_noise(IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn supports_noise_canonical_ipv6_fallback() {
|
||||
let v4 = IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4));
|
||||
let config = make_config(HashMap::from([(v4, NoiseNode::new_nym_node(dummy_key(1)))]));
|
||||
|
||||
let v6_mapped = IpAddr::V6(Ipv4Addr::new(1, 2, 3, 4).to_ipv6_mapped());
|
||||
assert!(config.supports_noise(v6_mapped));
|
||||
}
|
||||
|
||||
// -- new_with_agents test --
|
||||
|
||||
#[test]
|
||||
fn new_with_agents_builds_correct_view() {
|
||||
let ip = IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1));
|
||||
let key_a = dummy_key(1);
|
||||
let key_b = dummy_key(2);
|
||||
|
||||
let agents = HashMap::from([(
|
||||
ip,
|
||||
vec![
|
||||
NetworkMonitorAgentNode {
|
||||
port: 1000,
|
||||
key: key_a,
|
||||
},
|
||||
NetworkMonitorAgentNode {
|
||||
port: 2000,
|
||||
key: key_b,
|
||||
},
|
||||
],
|
||||
)]);
|
||||
|
||||
let config = NoiseConfig::new(
|
||||
Arc::new(x25519::KeyPair::new(&mut deterministic_rng())),
|
||||
NoiseNetworkView::new_with_agents(agents),
|
||||
Duration::from_secs(5),
|
||||
);
|
||||
|
||||
assert_eq!(config.get_noise_key(SocketAddr::new(ip, 1000)), Some(key_a));
|
||||
assert_eq!(config.get_noise_key(SocketAddr::new(ip, 2000)), Some(key_b));
|
||||
assert!(config.supports_noise(ip));
|
||||
}
|
||||
|
||||
// -- swap_view canonicalisation test --
|
||||
|
||||
// Regression: an agent registered via blockchain events flows through `swap_view` (called
|
||||
// from `NetworkMonitorAgentsModule::new_agent` and from the periodic network refresher).
|
||||
// If a non-canonical (IPv4-mapped IPv6) key reaches `swap_view`, lookups via
|
||||
// `supports_noise` (which canonicalises) used to miss, producing the
|
||||
// "can't speak Noise yet, falling back to TCP" warning despite the agent being correctly
|
||||
// authorised in the routing filter.
|
||||
#[tokio::test]
|
||||
async fn swap_view_canonicalises_non_canonical_keys() {
|
||||
let view = NoiseNetworkView::new_empty();
|
||||
let v4 = IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4));
|
||||
let v6_mapped = IpAddr::V6(Ipv4Addr::new(1, 2, 3, 4).to_ipv6_mapped());
|
||||
|
||||
let mut nodes = HashMap::new();
|
||||
// intentionally insert under the IPv4-mapped form — what a buggy caller might do
|
||||
nodes.insert(
|
||||
v6_mapped,
|
||||
NoiseNode::NetworkMonitorAgent {
|
||||
nodes: vec![NetworkMonitorAgentNode {
|
||||
port: 1000,
|
||||
key: dummy_key(1),
|
||||
}],
|
||||
},
|
||||
);
|
||||
|
||||
let permit = view.get_update_permit().await;
|
||||
view.swap_view(permit, nodes);
|
||||
|
||||
let config = NoiseConfig::new(
|
||||
Arc::new(x25519::KeyPair::new(&mut deterministic_rng())),
|
||||
view,
|
||||
Duration::from_secs(5),
|
||||
);
|
||||
|
||||
// lookup via either form must succeed
|
||||
assert!(config.supports_noise(v4));
|
||||
assert!(config.supports_noise(v6_mapped));
|
||||
assert!(config
|
||||
.get_noise_key(SocketAddr::new(v6_mapped, 1000))
|
||||
.is_some());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,12 @@ pub enum Connection<C> {
|
||||
Noise(#[pin] Box<NoiseStream<C>>),
|
||||
}
|
||||
|
||||
impl<C> Connection<C> {
|
||||
pub fn is_noise(&self) -> bool {
|
||||
matches!(self, Connection::Noise(_))
|
||||
}
|
||||
}
|
||||
|
||||
impl<C> AsyncRead for Connection<C>
|
||||
where
|
||||
C: AsyncRead + AsyncWrite + Unpin,
|
||||
|
||||
@@ -66,7 +66,7 @@ pub async fn upgrade_noise_initiator(
|
||||
Error::Prereq(Prerequisite::RemotePublicKey)
|
||||
})?;
|
||||
|
||||
let Some(key) = config.get_noise_key(&responder_addr) else {
|
||||
let Some(key) = config.get_noise_key(responder_addr) else {
|
||||
warn!("{responder_addr} can't speak Noise yet, falling back to TCP");
|
||||
return Ok(Connection::Raw(conn));
|
||||
};
|
||||
@@ -106,7 +106,7 @@ pub async fn upgrade_noise_responder(
|
||||
};
|
||||
|
||||
// if responder doesn't announce noise support, we fallback to tcp
|
||||
if config.get_noise_support(initiator_addr.ip()).is_none() {
|
||||
if !config.supports_noise(initiator_addr.ip()) {
|
||||
warn!("{initiator_addr} can't speak Noise yet, falling back to TCP",);
|
||||
return Ok(Connection::Raw(conn));
|
||||
};
|
||||
|
||||
@@ -110,6 +110,12 @@ pub enum PacketProcessingError {
|
||||
PacketReplay,
|
||||
}
|
||||
|
||||
impl PacketProcessingError {
|
||||
pub fn is_replay(&self) -> bool {
|
||||
matches!(self, PacketProcessingError::PacketReplay)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PartialyUnwrappedPacketWithKeyRotation {
|
||||
pub packet: PartiallyUnwrappedPacket,
|
||||
pub used_key_rotation: u32,
|
||||
|
||||
@@ -29,7 +29,7 @@ pub use sphinx_packet::{
|
||||
packet::builder::DEFAULT_PAYLOAD_SIZE,
|
||||
payload::{
|
||||
PAYLOAD_OVERHEAD_SIZE, Payload,
|
||||
key::{PayloadKey, PayloadKeySeed},
|
||||
key::{PayloadKey, PayloadKeySeed, derive_payload_key},
|
||||
},
|
||||
route::{Destination, DestinationAddressBytes, Node, NodeAddressBytes, SURBIdentifier},
|
||||
surb::{SURB, SURBMaterial},
|
||||
|
||||
@@ -7,8 +7,9 @@ use nyxd_scraper_shared::NyxdScraper;
|
||||
pub use nyxd_scraper_shared::constants;
|
||||
pub use nyxd_scraper_shared::error::ScraperError;
|
||||
pub use nyxd_scraper_shared::{
|
||||
BlockModule, MsgModule, NyxdScraperTransaction, ParsedTransactionResponse, PruningOptions,
|
||||
PruningStrategy, StartingBlockOpts, TxModule,
|
||||
BlockModule, DecodedMessage, MsgModule, NyxdScraperTransaction, ParsedTransactionDetails,
|
||||
ParsedTransactionResponse, PruningOptions, PruningStrategy, StartingBlockOpts, TxModule,
|
||||
parse_msg,
|
||||
};
|
||||
pub use storage::models;
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ use std::str::FromStr;
|
||||
// replicate behaviour of `CosmosMessageAddressesParser` from juno
|
||||
pub(crate) fn parse_addresses_from_events(tx: &ParsedTransactionResponse) -> Vec<String> {
|
||||
let mut addresses: Vec<String> = Vec::new();
|
||||
for event in &tx.tx_result.events {
|
||||
for event in &tx.tx_details.tx_result.events {
|
||||
for attribute in &event.attributes {
|
||||
let Ok(value) = attribute.value_str() else {
|
||||
continue;
|
||||
|
||||
@@ -147,6 +147,7 @@ impl PostgresStorageTransaction {
|
||||
for chain_tx in txs {
|
||||
// bdjuno style, base64 encode them
|
||||
let signatures = chain_tx
|
||||
.tx_details
|
||||
.tx
|
||||
.signatures
|
||||
.iter()
|
||||
@@ -154,12 +155,14 @@ impl PostgresStorageTransaction {
|
||||
.collect();
|
||||
|
||||
let messages = chain_tx
|
||||
.parsed_messages
|
||||
.decoded_messages
|
||||
.values()
|
||||
.map(|msg| &msg.decoded_content)
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let signer_infos = chain_tx
|
||||
.tx_details
|
||||
.tx
|
||||
.auth_info
|
||||
.signer_infos
|
||||
@@ -167,28 +170,28 @@ impl PostgresStorageTransaction {
|
||||
.map(|info| proto::cosmos::tx::v1beta1::SignerInfo::from(info.clone()))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let hash = chain_tx.hash.to_string();
|
||||
let height = chain_tx.height.into();
|
||||
let index = chain_tx.index as i32;
|
||||
let hash = chain_tx.tx_details.hash.to_string();
|
||||
let height = chain_tx.tx_details.height().into();
|
||||
let index = chain_tx.tx_details.index as i32;
|
||||
|
||||
let log = serde_json::to_value(chain_tx.tx_result.log.clone())
|
||||
let log = serde_json::to_value(chain_tx.tx_details.tx_result.log.clone())
|
||||
.inspect_err(|e| error!(hash, height, index, "Failed to parse logs: {e}"))
|
||||
.unwrap_or_default();
|
||||
let events = &chain_tx.tx_result.events;
|
||||
let events = &chain_tx.tx_details.tx_result.events;
|
||||
|
||||
insert_transaction(
|
||||
hash,
|
||||
height,
|
||||
index,
|
||||
chain_tx.tx_result.code.is_ok(),
|
||||
chain_tx.tx_details.tx_result.code.is_ok(),
|
||||
serde_json::Value::Array(messages),
|
||||
chain_tx.tx.body.memo.clone(),
|
||||
chain_tx.tx_details.tx.body.memo.clone(),
|
||||
signatures,
|
||||
serde_json::to_value(signer_infos)?,
|
||||
serde_json::to_value(&chain_tx.tx.auth_info.fee)?,
|
||||
chain_tx.tx_result.gas_wanted,
|
||||
chain_tx.tx_result.gas_used,
|
||||
chain_tx.tx_result.log.clone(),
|
||||
serde_json::to_value(&chain_tx.tx_details.tx.auth_info.fee)?,
|
||||
chain_tx.tx_details.tx_result.gas_wanted,
|
||||
chain_tx.tx_details.tx_result.gas_used,
|
||||
chain_tx.tx_details.tx_result.log.clone(),
|
||||
json!(log),
|
||||
json!(events),
|
||||
self.inner.as_mut(),
|
||||
@@ -207,17 +210,20 @@ impl PostgresStorageTransaction {
|
||||
|
||||
for chain_tx in txs {
|
||||
let involved_addresses = parse_addresses_from_events(chain_tx);
|
||||
for (index, msg) in chain_tx.tx.body.messages.iter().enumerate() {
|
||||
let parsed_message = chain_tx.parsed_messages.get(&index);
|
||||
for (index, msg) in chain_tx.tx_details.tx.body.messages.iter().enumerate() {
|
||||
let parsed_message = chain_tx
|
||||
.decoded_messages
|
||||
.get(&index)
|
||||
.map(|msg| &msg.decoded_content);
|
||||
let value = serde_json::to_value(parsed_message)?;
|
||||
|
||||
insert_message(
|
||||
chain_tx.hash.to_string(),
|
||||
chain_tx.tx_details.hash.to_string(),
|
||||
index as i64,
|
||||
msg.type_url.clone(),
|
||||
value,
|
||||
involved_addresses.clone(),
|
||||
chain_tx.height.into(),
|
||||
chain_tx.tx_details.height().into(),
|
||||
self.inner.as_mut(),
|
||||
)
|
||||
.await?
|
||||
|
||||
@@ -33,9 +33,9 @@ impl TxModule for FancyTxModule {
|
||||
async fn handle_tx(&mut self, tx: &ParsedTransactionResponse) -> Result<(), ScraperError> {
|
||||
println!(
|
||||
"✨ got new tx for height {}: {} ({} msgs)",
|
||||
tx.block.header.height,
|
||||
tx.hash,
|
||||
tx.parsed_messages.len()
|
||||
tx.tx_details.height(),
|
||||
tx.tx_details.hash,
|
||||
tx.tx_details.tx.body.messages.len()
|
||||
);
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -281,7 +281,7 @@ where
|
||||
&mut self,
|
||||
block: BlockToProcess,
|
||||
) -> Result<(), ScraperError> {
|
||||
info!("processing block at height {}", block.height);
|
||||
debug!("processing block at height {}", block.height);
|
||||
|
||||
let full_info = self
|
||||
.rpc_client
|
||||
@@ -291,8 +291,13 @@ where
|
||||
if let Some(tx_info) = &full_info.transactions {
|
||||
debug!("this block has {} transaction(s)", tx_info.len());
|
||||
for tx in tx_info {
|
||||
debug!("{} has {} message(s)", tx.hash, tx.tx.body.messages.len());
|
||||
for (index, msg) in tx.tx.body.messages.iter().enumerate() {
|
||||
let details = &tx.tx_details;
|
||||
debug!(
|
||||
"{} has {} message(s)",
|
||||
details.hash,
|
||||
details.tx.body.messages.len()
|
||||
);
|
||||
for (index, msg) in details.tx.body.messages.iter().enumerate() {
|
||||
debug!("{index}: {:?}", msg.type_url)
|
||||
}
|
||||
}
|
||||
@@ -315,11 +320,24 @@ where
|
||||
for tx_module in &mut self.tx_modules {
|
||||
tx_module.handle_tx(block_tx).await?;
|
||||
}
|
||||
let tx_details = &block_tx.tx_details;
|
||||
|
||||
// the ones concerned with individual messages
|
||||
for (index, msg) in block_tx.tx.body.messages.iter().enumerate() {
|
||||
for (index, msg) in tx_details.tx.body.messages.iter().enumerate() {
|
||||
let Some(decoded) = block_tx.decoded_messages.get(&index) else {
|
||||
warn!(
|
||||
"height: {} tx: {} tx_index: {}, msg_index: {index}: message failed to get decoded",
|
||||
tx_details.height(),
|
||||
tx_details.hash,
|
||||
tx_details.index,
|
||||
);
|
||||
continue;
|
||||
};
|
||||
for msg_module in &mut self.msg_modules {
|
||||
if msg.type_url == msg_module.type_url() {
|
||||
msg_module.handle_msg(index, msg, block_tx).await?
|
||||
msg_module
|
||||
.handle_msg(index, msg, decoded, tx_details)
|
||||
.await?
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,9 +7,16 @@ use tendermint::{Block, Hash, abci, block, tx};
|
||||
use tendermint_rpc::endpoint::{block as block_endpoint, block_results, validators};
|
||||
use tendermint_rpc::event::{Event, EventData};
|
||||
|
||||
// just get all everything out of tx::Response, but parse raw `tx` bytes
|
||||
/// Message decoded from the raw transaction and converted into json.
|
||||
/// Note that it might have gone through additional processing as set by the `MessageRegistry`
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ParsedTransactionResponse {
|
||||
pub struct DecodedMessage {
|
||||
pub type_url: String,
|
||||
pub decoded_content: serde_json::Value,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ParsedTransactionDetails {
|
||||
/// The hash of the transaction.
|
||||
///
|
||||
/// Deserialized from a hex-encoded string (there is a discrepancy between
|
||||
@@ -17,8 +24,6 @@ pub struct ParsedTransactionResponse {
|
||||
/// the Tendermint RPC).
|
||||
pub hash: Hash,
|
||||
|
||||
pub height: block::Height,
|
||||
|
||||
pub index: u32,
|
||||
|
||||
pub tx_result: abci::types::ExecTxResult,
|
||||
@@ -27,13 +32,23 @@ pub struct ParsedTransactionResponse {
|
||||
|
||||
pub proof: Option<tx::Proof>,
|
||||
|
||||
pub parsed_messages: BTreeMap<usize, serde_json::Value>,
|
||||
|
||||
pub parsed_message_urls: BTreeMap<usize, String>,
|
||||
|
||||
pub block: Block,
|
||||
}
|
||||
|
||||
impl ParsedTransactionDetails {
|
||||
pub fn height(&self) -> block::Height {
|
||||
self.block.header.height
|
||||
}
|
||||
}
|
||||
|
||||
// just get all everything out of tx::Response, but parse raw `tx` bytes
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ParsedTransactionResponse {
|
||||
pub tx_details: ParsedTransactionDetails,
|
||||
|
||||
pub decoded_messages: BTreeMap<usize, DecodedMessage>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct FullBlockInformation {
|
||||
/// Basic block information, including its signers.
|
||||
|
||||
@@ -82,10 +82,8 @@ pub enum ScraperError {
|
||||
source: cosmrs::ErrorReport,
|
||||
},
|
||||
|
||||
#[error("could not parse msg in tx {hash} at index {index} into {type_url}: {source}")]
|
||||
#[error("could not parse msg of type {type_url}: {source}")]
|
||||
MsgParseFailure {
|
||||
hash: Hash,
|
||||
index: usize,
|
||||
type_url: String,
|
||||
#[source]
|
||||
source: cosmrs::ErrorReport,
|
||||
|
||||
@@ -47,7 +47,7 @@ pub fn validator_consensus_address(id: account::Id) -> Result<AccountId, Malform
|
||||
}
|
||||
|
||||
pub fn tx_gas_sum(txs: &[ParsedTransactionResponse]) -> i64 {
|
||||
txs.iter().map(|tx| tx.tx_result.gas_used).sum()
|
||||
txs.iter().map(|tx| tx.tx_details.tx_result.gas_used).sum()
|
||||
}
|
||||
|
||||
pub fn validator_info(
|
||||
|
||||
@@ -15,12 +15,14 @@ pub(crate) mod subscriber;
|
||||
pub mod watcher;
|
||||
|
||||
pub use block_processor::pruning::{PruningOptions, PruningStrategy};
|
||||
pub use block_processor::types::ParsedTransactionResponse;
|
||||
pub use block_processor::types::{
|
||||
DecodedMessage, ParsedTransactionDetails, ParsedTransactionResponse,
|
||||
};
|
||||
pub use cosmos_module::{
|
||||
CosmosModule,
|
||||
message_registry::{MessageRegistry, default_message_registry},
|
||||
};
|
||||
pub use cosmrs::Any;
|
||||
pub use modules::{BlockModule, MsgModule, TxModule};
|
||||
pub use modules::{BlockModule, MsgModule, TxModule, parse_msg};
|
||||
pub use scraper::{Config, NyxdScraper, StartingBlockOpts};
|
||||
pub use storage::{NyxdScraperStorage, NyxdScraperTransaction};
|
||||
|
||||
@@ -6,5 +6,5 @@ mod msg_module;
|
||||
mod tx_module;
|
||||
|
||||
pub use block_module::BlockModule;
|
||||
pub use msg_module::MsgModule;
|
||||
pub use msg_module::{MsgModule, parse_msg};
|
||||
pub use tx_module::TxModule;
|
||||
|
||||
@@ -1,11 +1,47 @@
|
||||
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::block_processor::types::ParsedTransactionResponse;
|
||||
use crate::block_processor::types::{DecodedMessage, ParsedTransactionDetails};
|
||||
use crate::error::ScraperError;
|
||||
use async_trait::async_trait;
|
||||
use cosmrs::Any;
|
||||
use cosmrs::tx::Msg;
|
||||
|
||||
/// Parse a protobuf `Any` message into a strongly typed Cosmos message.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// let execute_msg: MsgExecuteContract = parse_msg(msg)?;
|
||||
/// ```
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns `ScraperError::MsgParseFailure` if:
|
||||
/// - The type URL doesn't match the expected type
|
||||
/// - The protobuf bytes are malformed
|
||||
/// - The message schema is incompatible with this version of the code
|
||||
pub fn parse_msg<T: Msg>(msg: &Any) -> Result<T, ScraperError> {
|
||||
T::from_any(msg).map_err(|source| ScraperError::MsgParseFailure {
|
||||
type_url: msg.type_url.clone(),
|
||||
source,
|
||||
})
|
||||
}
|
||||
|
||||
/// Trait for modules that process specific message types from blockchain transactions.
|
||||
///
|
||||
/// # Parameters
|
||||
///
|
||||
/// - `index`: Position of this message within the transaction (0-based)
|
||||
/// - `msg`: Raw protobuf message (use `parse_msg()` to decode)
|
||||
/// - `decoded_msg`: Pre-decoded JSON representation (may be None for unsupported types)
|
||||
/// - `tx`: Transaction details including block height, hash, and execution result
|
||||
///
|
||||
/// # Error Handling
|
||||
///
|
||||
/// - Return `Err` for critical failures that should stop block processing
|
||||
/// - Return `Ok(())` for non-critical errors (e.g., unexpected contract schema)
|
||||
/// - Log warnings for debugging without propagating errors
|
||||
#[async_trait]
|
||||
pub trait MsgModule {
|
||||
fn type_url(&self) -> String;
|
||||
@@ -14,6 +50,7 @@ pub trait MsgModule {
|
||||
&mut self,
|
||||
index: usize,
|
||||
msg: &Any,
|
||||
tx: &ParsedTransactionResponse,
|
||||
decoded_msg: &DecodedMessage,
|
||||
tx: &ParsedTransactionDetails,
|
||||
) -> Result<(), ScraperError>;
|
||||
}
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::block_processor::types::{
|
||||
BlockToProcess, FullBlockInformation, ParsedTransactionResponse,
|
||||
BlockToProcess, DecodedMessage, FullBlockInformation, ParsedTransactionResponse,
|
||||
};
|
||||
use crate::error::ScraperError;
|
||||
use crate::helpers::tx_hash;
|
||||
use crate::{Any, MessageRegistry, default_message_registry};
|
||||
use crate::{Any, MessageRegistry, ParsedTransactionDetails, default_message_registry};
|
||||
use futures::StreamExt;
|
||||
use futures::future::join3;
|
||||
use std::collections::BTreeMap;
|
||||
@@ -77,8 +77,7 @@ impl RpcClient {
|
||||
) -> Result<Vec<ParsedTransactionResponse>, ScraperError> {
|
||||
let mut transactions = Vec::with_capacity(raw_transactions.len());
|
||||
for raw_tx in raw_transactions {
|
||||
let mut parsed_messages = BTreeMap::new();
|
||||
let mut parsed_message_urls = BTreeMap::new();
|
||||
let mut decoded_messages = BTreeMap::new();
|
||||
let tx = cosmrs::Tx::from_bytes(&raw_tx.tx).map_err(|source| {
|
||||
ScraperError::TxParseFailure {
|
||||
hash: raw_tx.hash,
|
||||
@@ -87,22 +86,27 @@ impl RpcClient {
|
||||
})?;
|
||||
|
||||
for (index, msg) in tx.body.messages.iter().enumerate() {
|
||||
if let Some(value) = self.decode_or_skip(msg) {
|
||||
parsed_messages.insert(index, value);
|
||||
parsed_message_urls.insert(index, msg.type_url.clone());
|
||||
if let Some(decoded_content) = self.decode_or_skip(msg) {
|
||||
decoded_messages.insert(
|
||||
index,
|
||||
DecodedMessage {
|
||||
type_url: msg.type_url.clone(),
|
||||
decoded_content,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
transactions.push(ParsedTransactionResponse {
|
||||
hash: raw_tx.hash,
|
||||
height: raw_tx.height,
|
||||
index: raw_tx.index,
|
||||
tx_result: raw_tx.tx_result,
|
||||
tx,
|
||||
proof: raw_tx.proof,
|
||||
parsed_messages,
|
||||
parsed_message_urls,
|
||||
block: block.clone(),
|
||||
tx_details: ParsedTransactionDetails {
|
||||
hash: raw_tx.hash,
|
||||
index: raw_tx.index,
|
||||
tx_result: raw_tx.tx_result,
|
||||
tx,
|
||||
proof: raw_tx.proof,
|
||||
block: block.clone(),
|
||||
},
|
||||
decoded_messages,
|
||||
})
|
||||
}
|
||||
Ok(transactions)
|
||||
|
||||
@@ -14,15 +14,16 @@ use tracing::info;
|
||||
use url::Url;
|
||||
|
||||
pub struct WatcherConfig {
|
||||
/// Url to the websocket endpoint of a validator, for example `wss://rpc.nymtech.net/websocket`
|
||||
/// Url to the websocket endpoint of a validator, for example, `wss://rpc.nymtech.net/websocket`
|
||||
pub websocket_url: Url,
|
||||
|
||||
/// Url to the rpc endpoint of a validator, for example `https://rpc.nymtech.net/`
|
||||
/// Url to the rpc endpoint of a validator, for example, `https://rpc.nymtech.net/`
|
||||
pub rpc_url: Url,
|
||||
}
|
||||
|
||||
pub struct NyxdWatcherBuilder {
|
||||
config: WatcherConfig,
|
||||
custom_shutdown: CancellationToken,
|
||||
|
||||
block_modules: Vec<Box<dyn BlockModule + Send>>,
|
||||
tx_modules: Vec<Box<dyn TxModule + Send>>,
|
||||
@@ -33,12 +34,19 @@ impl NyxdWatcherBuilder {
|
||||
pub fn new(config: WatcherConfig) -> Self {
|
||||
NyxdWatcherBuilder {
|
||||
config,
|
||||
custom_shutdown: CancellationToken::new(),
|
||||
block_modules: vec![],
|
||||
tx_modules: vec![],
|
||||
msg_modules: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_custom_shutdown(mut self, token: CancellationToken) -> Self {
|
||||
self.custom_shutdown = token;
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_block_module<M: BlockModule + Send + 'static>(mut self, module: M) -> Self {
|
||||
self.block_modules.push(Box::new(module));
|
||||
|
||||
@@ -7,8 +7,9 @@ use nyxd_scraper_shared::NyxdScraper;
|
||||
pub use nyxd_scraper_shared::constants;
|
||||
pub use nyxd_scraper_shared::error::ScraperError;
|
||||
pub use nyxd_scraper_shared::{
|
||||
BlockModule, MsgModule, NyxdScraperTransaction, ParsedTransactionResponse, PruningOptions,
|
||||
PruningStrategy, StartingBlockOpts, TxModule,
|
||||
BlockModule, DecodedMessage, MsgModule, NyxdScraperTransaction, ParsedTransactionDetails,
|
||||
ParsedTransactionResponse, PruningOptions, PruningStrategy, StartingBlockOpts, TxModule,
|
||||
parse_msg,
|
||||
};
|
||||
pub use storage::models;
|
||||
|
||||
|
||||
@@ -132,15 +132,15 @@ impl SqliteStorageTransaction {
|
||||
|
||||
for chain_tx in txs {
|
||||
insert_transaction(
|
||||
chain_tx.hash.to_string(),
|
||||
chain_tx.height.into(),
|
||||
chain_tx.index as i64,
|
||||
chain_tx.tx_result.code.is_ok(),
|
||||
chain_tx.tx.body.messages.len() as i64,
|
||||
chain_tx.tx.body.memo.clone(),
|
||||
chain_tx.tx_result.gas_wanted,
|
||||
chain_tx.tx_result.gas_used,
|
||||
chain_tx.tx_result.log.clone(),
|
||||
chain_tx.tx_details.hash.to_string(),
|
||||
chain_tx.tx_details.height().into(),
|
||||
chain_tx.tx_details.index as i64,
|
||||
chain_tx.tx_details.tx_result.code.is_ok(),
|
||||
chain_tx.tx_details.tx.body.messages.len() as i64,
|
||||
chain_tx.tx_details.tx.body.memo.clone(),
|
||||
chain_tx.tx_details.tx_result.gas_wanted,
|
||||
chain_tx.tx_details.tx_result.gas_used,
|
||||
chain_tx.tx_details.tx_result.log.clone(),
|
||||
self.0.as_mut(),
|
||||
)
|
||||
.await?;
|
||||
@@ -156,12 +156,12 @@ impl SqliteStorageTransaction {
|
||||
debug!("persisting messages");
|
||||
|
||||
for chain_tx in txs {
|
||||
for (index, msg) in chain_tx.tx.body.messages.iter().enumerate() {
|
||||
for (index, msg) in chain_tx.tx_details.tx.body.messages.iter().enumerate() {
|
||||
insert_message(
|
||||
chain_tx.hash.to_string(),
|
||||
chain_tx.tx_details.hash.to_string(),
|
||||
index as i64,
|
||||
msg.type_url.clone(),
|
||||
chain_tx.height.into(),
|
||||
chain_tx.tx_details.height().into(),
|
||||
self.0.as_mut(),
|
||||
)
|
||||
.await?
|
||||
|
||||
Generated
+28
@@ -1180,6 +1180,22 @@ dependencies = [
|
||||
"rand_chacha",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "network-monitors"
|
||||
version = "1.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bs58",
|
||||
"cosmwasm-schema",
|
||||
"cosmwasm-std",
|
||||
"cw-controllers",
|
||||
"cw-storage-plus",
|
||||
"cw2",
|
||||
"nym-contracts-common",
|
||||
"nym-contracts-common-testing",
|
||||
"nym-network-monitors-contract-common",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-bigint"
|
||||
version = "0.4.6"
|
||||
@@ -1456,6 +1472,18 @@ dependencies = [
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nym-network-monitors-contract-common"
|
||||
version = "1.20.4"
|
||||
dependencies = [
|
||||
"cosmwasm-schema",
|
||||
"cosmwasm-std",
|
||||
"cw-controllers",
|
||||
"schemars",
|
||||
"serde",
|
||||
"thiserror 2.0.12",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nym-pemstore"
|
||||
version = "1.20.4"
|
||||
|
||||
@@ -10,6 +10,7 @@ members = [
|
||||
"multisig/cw4-group",
|
||||
"vesting",
|
||||
"performance",
|
||||
"network-monitors",
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
@@ -19,6 +20,8 @@ homepage = "https://nymtech.net"
|
||||
documentation = "https://nymtech.net"
|
||||
edition = "2021"
|
||||
license = "Apache-2.0"
|
||||
rust-version = "1.86.0"
|
||||
readme = "../README.md"
|
||||
|
||||
[profile.release]
|
||||
opt-level = 3
|
||||
@@ -67,6 +70,7 @@ nym-ecash-contract-common = "1.20.4"
|
||||
nym-group-contract-common = "1.20.4"
|
||||
nym-mixnet-contract-common = "1.20.4"
|
||||
nym-multisig-contract-common = "1.20.4"
|
||||
nym-network-monitors-contract-common = "1.20.4"
|
||||
nym-network-defaults = { version = "1.20.4", default-features = false }
|
||||
nym-performance-contract-common = "1.20.4"
|
||||
nym-pool-contract-common = "1.20.4"
|
||||
@@ -94,8 +98,6 @@ unimplemented = "deny"
|
||||
unreachable = "deny"
|
||||
|
||||
# For local development, import via path instead of crates.io, e.g.
|
||||
# [patch.crates-io]
|
||||
# nym-coconut-dkg-common = { path = "../common/cosmwasm-smart-contracts/coconut-dkg" }
|
||||
|
||||
[patch.crates-io]
|
||||
nym-network-monitors-contract-common = { path = "../common/cosmwasm-smart-contracts/network-monitors-contract" }
|
||||
nym-ecash-contract-common = { path = "../common/cosmwasm-smart-contracts/ecash-contract" }
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
[alias]
|
||||
wasm = "build --release --lib --target wasm32-unknown-unknown"
|
||||
unit-test = "test --lib"
|
||||
schema = "run --bin schema --features=schema-gen"
|
||||
@@ -0,0 +1,43 @@
|
||||
[package]
|
||||
name = "network-monitors"
|
||||
description = "CosmWasm smart contract storing information on Nym network monitors"
|
||||
version = "1.0.0"
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
documentation.workspace = true
|
||||
rust-version.workspace = true
|
||||
readme.workspace = true
|
||||
publish = false
|
||||
|
||||
[[bin]]
|
||||
name = "schema"
|
||||
required-features = ["schema-gen"]
|
||||
|
||||
[lib]
|
||||
name = "network_monitors_contract"
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
bs58 = { workspace = true }
|
||||
cosmwasm-std = { workspace = true }
|
||||
cw2 = { workspace = true }
|
||||
cw-storage-plus = { workspace = true }
|
||||
cw-controllers = { workspace = true }
|
||||
|
||||
cosmwasm-schema = { workspace = true, optional = true }
|
||||
|
||||
nym-network-monitors-contract-common = { workspace = true }
|
||||
nym-contracts-common = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow = { workspace = true }
|
||||
nym-contracts-common-testing = { workspace = true }
|
||||
|
||||
[features]
|
||||
schema-gen = ["nym-network-monitors-contract-common/schema", "cosmwasm-schema"]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -0,0 +1,5 @@
|
||||
wasm:
|
||||
RUSTFLAGS='-C link-arg=-s' cargo build --release --target wasm32-unknown-unknown
|
||||
|
||||
generate-schema:
|
||||
cargo schema
|
||||
@@ -0,0 +1,368 @@
|
||||
{
|
||||
"contract_name": "network-monitors",
|
||||
"contract_version": "0.1.0",
|
||||
"idl_version": "1.0.0",
|
||||
"instantiate": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "InstantiateMsg",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"orchestrator_address"
|
||||
],
|
||||
"properties": {
|
||||
"orchestrator_address": {
|
||||
"description": "Address of the initial network monitor orchestrator.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"execute": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "ExecuteMsg",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Change the admin",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"update_admin"
|
||||
],
|
||||
"properties": {
|
||||
"update_admin": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"admin"
|
||||
],
|
||||
"properties": {
|
||||
"admin": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"description": "Authorise new network monitor orchestrator",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"authorise_network_monitor_orchestrator"
|
||||
],
|
||||
"properties": {
|
||||
"authorise_network_monitor_orchestrator": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"address"
|
||||
],
|
||||
"properties": {
|
||||
"address": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"description": "Revoke network monitor orchestrator authorisation.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"revoke_network_monitor_orchestrator"
|
||||
],
|
||||
"properties": {
|
||||
"revoke_network_monitor_orchestrator": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"address"
|
||||
],
|
||||
"properties": {
|
||||
"address": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"description": "Authorise new network monitor (or renew authorisation) granting additional privileges when sending mixnet packets to Nym nodes.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"authorise_network_monitor"
|
||||
],
|
||||
"properties": {
|
||||
"authorise_network_monitor": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"address"
|
||||
],
|
||||
"properties": {
|
||||
"address": {
|
||||
"type": "string",
|
||||
"format": "ip"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"description": "Revoke network monitor authorisation.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"revoke_network_monitor"
|
||||
],
|
||||
"properties": {
|
||||
"revoke_network_monitor": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"address"
|
||||
],
|
||||
"properties": {
|
||||
"address": {
|
||||
"type": "string",
|
||||
"format": "ip"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"description": "Revoke all network monitor authorisations.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"revoke_all_network_monitors"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"query": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "QueryMsg",
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"admin"
|
||||
],
|
||||
"properties": {
|
||||
"admin": {
|
||||
"type": "object",
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"network_monitor_orchestrators"
|
||||
],
|
||||
"properties": {
|
||||
"network_monitor_orchestrators": {
|
||||
"type": "object",
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"network_monitor_agents"
|
||||
],
|
||||
"properties": {
|
||||
"network_monitor_agents": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"limit": {
|
||||
"description": "Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default.",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"start_next_after": {
|
||||
"description": "Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
],
|
||||
"format": "ip"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"migrate": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "MigrateMsg",
|
||||
"type": "object",
|
||||
"additionalProperties": false
|
||||
},
|
||||
"sudo": null,
|
||||
"responses": {
|
||||
"admin": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "AdminResponse",
|
||||
"description": "Returned from Admin.query_admin()",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"admin": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"network_monitor_agents": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "AuthorisedNetworkMonitorsPagedResponse",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"authorised"
|
||||
],
|
||||
"properties": {
|
||||
"authorised": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/AuthorisedNetworkMonitor"
|
||||
}
|
||||
},
|
||||
"start_next_after": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
],
|
||||
"format": "ip"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"definitions": {
|
||||
"Addr": {
|
||||
"description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.",
|
||||
"type": "string"
|
||||
},
|
||||
"AuthorisedNetworkMonitor": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"address",
|
||||
"authorised_at",
|
||||
"authorised_by"
|
||||
],
|
||||
"properties": {
|
||||
"address": {
|
||||
"description": "The Ip address associated with the network monitor agent.",
|
||||
"type": "string",
|
||||
"format": "ip"
|
||||
},
|
||||
"authorised_at": {
|
||||
"description": "Timestamp of when the network monitor was authorised.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Timestamp"
|
||||
}
|
||||
]
|
||||
},
|
||||
"authorised_by": {
|
||||
"description": "The address of the orchestrator that authorised the network monitor agent.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Addr"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"Timestamp": {
|
||||
"description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Uint64"
|
||||
}
|
||||
]
|
||||
},
|
||||
"Uint64": {
|
||||
"description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"network_monitor_orchestrators": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "AuthorisedNetworkMonitorOrchestratorsResponse",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"authorised"
|
||||
],
|
||||
"properties": {
|
||||
"authorised": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/AuthorisedNetworkMonitorOrchestrator"
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"definitions": {
|
||||
"Addr": {
|
||||
"description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.",
|
||||
"type": "string"
|
||||
},
|
||||
"AuthorisedNetworkMonitorOrchestrator": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"address",
|
||||
"authorised_at"
|
||||
],
|
||||
"properties": {
|
||||
"address": {
|
||||
"description": "The address associated with the network monitor orchestrator.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Addr"
|
||||
}
|
||||
]
|
||||
},
|
||||
"authorised_at": {
|
||||
"description": "Timestamp of when the network monitor was authorised.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Timestamp"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"Timestamp": {
|
||||
"description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Uint64"
|
||||
}
|
||||
]
|
||||
},
|
||||
"Uint64": {
|
||||
"description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "ExecuteMsg",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "Change the admin",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"update_admin"
|
||||
],
|
||||
"properties": {
|
||||
"update_admin": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"admin"
|
||||
],
|
||||
"properties": {
|
||||
"admin": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"description": "Authorise new network monitor orchestrator",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"authorise_network_monitor_orchestrator"
|
||||
],
|
||||
"properties": {
|
||||
"authorise_network_monitor_orchestrator": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"address"
|
||||
],
|
||||
"properties": {
|
||||
"address": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"description": "Revoke network monitor orchestrator authorisation.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"revoke_network_monitor_orchestrator"
|
||||
],
|
||||
"properties": {
|
||||
"revoke_network_monitor_orchestrator": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"address"
|
||||
],
|
||||
"properties": {
|
||||
"address": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"description": "Authorise new network monitor (or renew authorisation) granting additional privileges when sending mixnet packets to Nym nodes.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"authorise_network_monitor"
|
||||
],
|
||||
"properties": {
|
||||
"authorise_network_monitor": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"address"
|
||||
],
|
||||
"properties": {
|
||||
"address": {
|
||||
"type": "string",
|
||||
"format": "ip"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"description": "Revoke network monitor authorisation.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"revoke_network_monitor"
|
||||
],
|
||||
"properties": {
|
||||
"revoke_network_monitor": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"address"
|
||||
],
|
||||
"properties": {
|
||||
"address": {
|
||||
"type": "string",
|
||||
"format": "ip"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"description": "Revoke all network monitor authorisations.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"revoke_all_network_monitors"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "InstantiateMsg",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"orchestrator_address"
|
||||
],
|
||||
"properties": {
|
||||
"orchestrator_address": {
|
||||
"description": "Address of the initial network monitor orchestrator.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "MigrateMsg",
|
||||
"type": "object",
|
||||
"additionalProperties": false
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "QueryMsg",
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"admin"
|
||||
],
|
||||
"properties": {
|
||||
"admin": {
|
||||
"type": "object",
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"network_monitor_orchestrators"
|
||||
],
|
||||
"properties": {
|
||||
"network_monitor_orchestrators": {
|
||||
"type": "object",
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"network_monitor_agents"
|
||||
],
|
||||
"properties": {
|
||||
"network_monitor_agents": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"limit": {
|
||||
"description": "Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default.",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"start_next_after": {
|
||||
"description": "Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
],
|
||||
"format": "ip"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "AdminResponse",
|
||||
"description": "Returned from Admin.query_admin()",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"admin": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "AuthorisedNetworkMonitorsPagedResponse",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"authorised"
|
||||
],
|
||||
"properties": {
|
||||
"authorised": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/AuthorisedNetworkMonitor"
|
||||
}
|
||||
},
|
||||
"start_next_after": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
],
|
||||
"format": "ip"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"definitions": {
|
||||
"Addr": {
|
||||
"description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.",
|
||||
"type": "string"
|
||||
},
|
||||
"AuthorisedNetworkMonitor": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"address",
|
||||
"authorised_at",
|
||||
"authorised_by"
|
||||
],
|
||||
"properties": {
|
||||
"address": {
|
||||
"description": "The Ip address associated with the network monitor agent.",
|
||||
"type": "string",
|
||||
"format": "ip"
|
||||
},
|
||||
"authorised_at": {
|
||||
"description": "Timestamp of when the network monitor was authorised.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Timestamp"
|
||||
}
|
||||
]
|
||||
},
|
||||
"authorised_by": {
|
||||
"description": "The address of the orchestrator that authorised the network monitor agent.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Addr"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"Timestamp": {
|
||||
"description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Uint64"
|
||||
}
|
||||
]
|
||||
},
|
||||
"Uint64": {
|
||||
"description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "AuthorisedNetworkMonitorOrchestratorsResponse",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"authorised"
|
||||
],
|
||||
"properties": {
|
||||
"authorised": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/AuthorisedNetworkMonitorOrchestrator"
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"definitions": {
|
||||
"Addr": {
|
||||
"description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.",
|
||||
"type": "string"
|
||||
},
|
||||
"AuthorisedNetworkMonitorOrchestrator": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"address",
|
||||
"authorised_at"
|
||||
],
|
||||
"properties": {
|
||||
"address": {
|
||||
"description": "The address associated with the network monitor orchestrator.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Addr"
|
||||
}
|
||||
]
|
||||
},
|
||||
"authorised_at": {
|
||||
"description": "Timestamp of when the network monitor was authorised.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Timestamp"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"Timestamp": {
|
||||
"description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Uint64"
|
||||
}
|
||||
]
|
||||
},
|
||||
"Uint64": {
|
||||
"description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use cosmwasm_schema::write_api;
|
||||
use nym_network_monitors_contract_common::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg};
|
||||
|
||||
fn main() {
|
||||
write_api! {
|
||||
instantiate: InstantiateMsg,
|
||||
query: QueryMsg,
|
||||
execute: ExecuteMsg,
|
||||
migrate: MigrateMsg,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::queries::{
|
||||
query_admin, query_network_monitor_agents, query_network_monitor_orchestrators,
|
||||
};
|
||||
use crate::storage::NETWORK_MONITORS_CONTRACT_STORAGE;
|
||||
use crate::transactions::{
|
||||
try_authorise_network_monitor, try_authorise_network_monitor_orchestrator,
|
||||
try_revoke_all_network_monitors, try_revoke_network_monitor,
|
||||
try_revoke_network_monitor_orchestrator, try_update_contract_admin,
|
||||
try_update_orchestrator_identity_key,
|
||||
};
|
||||
use cosmwasm_std::{
|
||||
entry_point, to_json_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response,
|
||||
};
|
||||
use nym_contracts_common::set_build_information;
|
||||
use nym_network_monitors_contract_common::{
|
||||
ExecuteMsg, InstantiateMsg, MigrateMsg, NetworkMonitorsContractError, QueryMsg,
|
||||
};
|
||||
|
||||
const CONTRACT_NAME: &str = "crate:nym-network-monitors-contract";
|
||||
const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
#[entry_point]
|
||||
pub fn instantiate(
|
||||
deps: DepsMut,
|
||||
env: Env,
|
||||
info: MessageInfo,
|
||||
msg: InstantiateMsg,
|
||||
) -> Result<Response, NetworkMonitorsContractError> {
|
||||
cw2::set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
|
||||
set_build_information!(deps.storage)?;
|
||||
|
||||
let orchestrator = deps.api.addr_validate(&msg.orchestrator_address)?;
|
||||
NETWORK_MONITORS_CONTRACT_STORAGE.initialise(deps, env, info.sender, orchestrator)?;
|
||||
|
||||
Ok(Response::default())
|
||||
}
|
||||
|
||||
#[entry_point]
|
||||
pub fn execute(
|
||||
deps: DepsMut,
|
||||
env: Env,
|
||||
info: MessageInfo,
|
||||
msg: ExecuteMsg,
|
||||
) -> Result<Response, NetworkMonitorsContractError> {
|
||||
match msg {
|
||||
ExecuteMsg::UpdateAdmin { admin } => try_update_contract_admin(deps, info, admin),
|
||||
ExecuteMsg::AuthoriseNetworkMonitorOrchestrator { address } => {
|
||||
try_authorise_network_monitor_orchestrator(deps, env, info, address)
|
||||
}
|
||||
ExecuteMsg::UpdateOrchestratorIdentityKey { key } => {
|
||||
try_update_orchestrator_identity_key(deps, info, key)
|
||||
}
|
||||
ExecuteMsg::RevokeNetworkMonitorOrchestrator { address } => {
|
||||
try_revoke_network_monitor_orchestrator(deps, info, address)
|
||||
}
|
||||
ExecuteMsg::AuthoriseNetworkMonitor {
|
||||
mixnet_address: address,
|
||||
bs58_x25519_noise,
|
||||
noise_version,
|
||||
} => try_authorise_network_monitor(
|
||||
deps,
|
||||
env,
|
||||
info,
|
||||
address,
|
||||
bs58_x25519_noise,
|
||||
noise_version,
|
||||
),
|
||||
ExecuteMsg::RevokeNetworkMonitor { address } => {
|
||||
try_revoke_network_monitor(deps, info, address)
|
||||
}
|
||||
ExecuteMsg::RevokeAllNetworkMonitors => try_revoke_all_network_monitors(deps, info),
|
||||
}
|
||||
}
|
||||
|
||||
#[entry_point]
|
||||
pub fn query(deps: Deps, _: Env, msg: QueryMsg) -> Result<Binary, NetworkMonitorsContractError> {
|
||||
match msg {
|
||||
QueryMsg::Admin {} => Ok(to_json_binary(&query_admin(deps)?)?),
|
||||
QueryMsg::NetworkMonitorOrchestrators {} => {
|
||||
Ok(to_json_binary(&query_network_monitor_orchestrators(deps)?)?)
|
||||
}
|
||||
QueryMsg::NetworkMonitorAgents {
|
||||
start_next_after,
|
||||
limit,
|
||||
} => Ok(to_json_binary(&query_network_monitor_agents(
|
||||
deps,
|
||||
start_next_after,
|
||||
limit,
|
||||
)?)?),
|
||||
}
|
||||
}
|
||||
|
||||
#[entry_point]
|
||||
pub fn migrate(
|
||||
deps: DepsMut,
|
||||
_env: Env,
|
||||
_msg: MigrateMsg,
|
||||
) -> Result<Response, NetworkMonitorsContractError> {
|
||||
set_build_information!(deps.storage)?;
|
||||
cw2::ensure_from_older_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
|
||||
|
||||
Ok(Default::default())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[cfg(test)]
|
||||
mod contract_instantiation {
|
||||
use super::*;
|
||||
use cosmwasm_std::testing::{message_info, mock_dependencies, mock_env};
|
||||
use cosmwasm_std::Addr;
|
||||
|
||||
#[test]
|
||||
fn sets_contract_admin_to_the_message_sender() -> anyhow::Result<()> {
|
||||
let mut deps = mock_dependencies();
|
||||
let env = mock_env();
|
||||
let init_msg = InstantiateMsg {
|
||||
orchestrator_address: deps.api.addr_make("foo").to_string(),
|
||||
};
|
||||
|
||||
let some_sender = deps.api.addr_make("some_sender");
|
||||
instantiate(
|
||||
deps.as_mut(),
|
||||
env,
|
||||
message_info(&some_sender, &[]),
|
||||
init_msg,
|
||||
)?;
|
||||
|
||||
NETWORK_MONITORS_CONTRACT_STORAGE
|
||||
.contract_admin
|
||||
.assert_admin(deps.as_ref(), &some_sender)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sets_the_initial_orchestrator() -> anyhow::Result<()> {
|
||||
let mut deps = mock_dependencies();
|
||||
let env = mock_env();
|
||||
let admin = deps.api.addr_make("some_sender");
|
||||
|
||||
let bad_addr = "foo".to_string();
|
||||
let good_addr = deps.api.addr_make("foo").to_string();
|
||||
|
||||
let bad_init_msg = InstantiateMsg {
|
||||
orchestrator_address: bad_addr.clone(),
|
||||
};
|
||||
|
||||
let good_init_msg = InstantiateMsg {
|
||||
orchestrator_address: good_addr.clone(),
|
||||
};
|
||||
|
||||
let res = instantiate(
|
||||
deps.as_mut(),
|
||||
env.clone(),
|
||||
message_info(&admin, &[]),
|
||||
bad_init_msg,
|
||||
);
|
||||
assert!(res.is_err());
|
||||
|
||||
let is_orchestrator = NETWORK_MONITORS_CONTRACT_STORAGE
|
||||
.is_orchestrator(deps.as_ref(), &Addr::unchecked(&good_addr))?;
|
||||
assert!(!is_orchestrator);
|
||||
|
||||
instantiate(deps.as_mut(), env, message_info(&admin, &[]), good_init_msg)?;
|
||||
|
||||
let is_orchestrator = NETWORK_MONITORS_CONTRACT_STORAGE
|
||||
.is_orchestrator(deps.as_ref(), &Addr::unchecked(&good_addr))?;
|
||||
assert!(is_orchestrator);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
pub mod contract;
|
||||
pub mod queries;
|
||||
pub mod queued_migrations;
|
||||
pub mod storage;
|
||||
pub mod transactions;
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod testing;
|
||||
@@ -0,0 +1,345 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::storage::{retrieval_limits, AgentStorageKey, NETWORK_MONITORS_CONTRACT_STORAGE};
|
||||
use cosmwasm_std::{Deps, StdResult};
|
||||
use cw_controllers::AdminResponse;
|
||||
use cw_storage_plus::Bound;
|
||||
use nym_network_monitors_contract_common::{
|
||||
AuthorisedNetworkMonitorOrchestratorsResponse, AuthorisedNetworkMonitorsPagedResponse,
|
||||
NetworkMonitorsContractError,
|
||||
};
|
||||
use std::net::SocketAddr;
|
||||
|
||||
pub fn query_admin(deps: Deps) -> Result<AdminResponse, NetworkMonitorsContractError> {
|
||||
NETWORK_MONITORS_CONTRACT_STORAGE
|
||||
.contract_admin
|
||||
.query_admin(deps)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
// no need for pagination as we don't expect even a double digit of those
|
||||
pub fn query_network_monitor_orchestrators(
|
||||
deps: Deps,
|
||||
) -> Result<AuthorisedNetworkMonitorOrchestratorsResponse, NetworkMonitorsContractError> {
|
||||
let authorised = NETWORK_MONITORS_CONTRACT_STORAGE
|
||||
.authorised_orchestrators
|
||||
.range(deps.storage, None, None, cosmwasm_std::Order::Ascending)
|
||||
.map(|record| record.map(|(_, details)| details))
|
||||
.collect::<StdResult<Vec<_>>>()?;
|
||||
|
||||
Ok(AuthorisedNetworkMonitorOrchestratorsResponse { authorised })
|
||||
}
|
||||
|
||||
pub fn query_network_monitor_agents(
|
||||
deps: Deps,
|
||||
start_after: Option<SocketAddr>,
|
||||
limit: Option<u32>,
|
||||
) -> Result<AuthorisedNetworkMonitorsPagedResponse, NetworkMonitorsContractError> {
|
||||
let limit = limit
|
||||
.unwrap_or(retrieval_limits::AGENTS_DEFAULT_LIMIT)
|
||||
.min(retrieval_limits::AGENTS_MAX_LIMIT) as usize;
|
||||
|
||||
let start = start_after.map(|addr| Bound::exclusive(AgentStorageKey::from(addr)));
|
||||
|
||||
let authorised = NETWORK_MONITORS_CONTRACT_STORAGE
|
||||
.authorised_agents
|
||||
.range(deps.storage, start, None, cosmwasm_std::Order::Ascending)
|
||||
.take(limit)
|
||||
.map(|record| record.map(|(_, details)| details))
|
||||
.collect::<StdResult<Vec<_>>>()?;
|
||||
|
||||
let start_next_after = authorised.last().map(|last| last.mixnet_address);
|
||||
|
||||
Ok(AuthorisedNetworkMonitorsPagedResponse {
|
||||
authorised,
|
||||
start_next_after,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[cfg(test)]
|
||||
mod admin_query {
|
||||
use crate::queries::query_admin;
|
||||
use crate::testing::init_contract_tester;
|
||||
use nym_contracts_common_testing::{AdminExt, ChainOpts, ContractOpts, RandExt};
|
||||
use nym_network_monitors_contract_common::ExecuteMsg;
|
||||
|
||||
#[test]
|
||||
fn returns_current_admin() -> anyhow::Result<()> {
|
||||
let mut test = init_contract_tester();
|
||||
|
||||
let initial_admin = test.admin_unchecked();
|
||||
|
||||
// initial
|
||||
let res = query_admin(test.deps())?;
|
||||
assert_eq!(res.admin, Some(initial_admin.to_string()));
|
||||
|
||||
let new_admin = test.generate_account();
|
||||
|
||||
// sanity check
|
||||
assert_ne!(initial_admin, new_admin);
|
||||
|
||||
// after update
|
||||
test.execute_msg(
|
||||
initial_admin.clone(),
|
||||
&ExecuteMsg::UpdateAdmin {
|
||||
admin: new_admin.to_string(),
|
||||
},
|
||||
)?;
|
||||
|
||||
let updated_admin = query_admin(test.deps())?;
|
||||
assert_eq!(updated_admin.admin, Some(new_admin.to_string()));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod network_monitor_orchestrators_query {
|
||||
use super::*;
|
||||
use crate::testing::{init_contract_tester, NetworkMonitorsContractTesterExt};
|
||||
use nym_contracts_common_testing::{AdminExt, ContractOpts};
|
||||
use nym_network_monitors_contract_common::ExecuteMsg;
|
||||
|
||||
#[test]
|
||||
fn returns_empty_list_when_there_are_no_extra_orchestrators() -> anyhow::Result<()> {
|
||||
// make sure to start with an empty state
|
||||
let mut test = init_contract_tester();
|
||||
test.remove_all_orchestrators();
|
||||
|
||||
let res = query_network_monitor_orchestrators(test.deps())?;
|
||||
|
||||
assert!(res.authorised.is_empty());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn returns_all_authorised_orchestrators() -> anyhow::Result<()> {
|
||||
// make sure to start with an empty state
|
||||
let mut test = init_contract_tester();
|
||||
test.remove_all_orchestrators();
|
||||
|
||||
let orchestrator1 = test.add_orchestrator()?;
|
||||
let orchestrator2 = test.add_orchestrator()?;
|
||||
let orchestrator3 = test.add_orchestrator()?;
|
||||
|
||||
let res = query_network_monitor_orchestrators(test.deps())?;
|
||||
|
||||
assert_eq!(res.authorised.len(), 3);
|
||||
assert!(res.authorised.iter().any(|o| o.address == orchestrator1));
|
||||
assert!(res.authorised.iter().any(|o| o.address == orchestrator2));
|
||||
assert!(res.authorised.iter().any(|o| o.address == orchestrator3));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn does_not_return_revoked_orchestrators() -> anyhow::Result<()> {
|
||||
// make sure to start with an empty state
|
||||
let mut test = init_contract_tester();
|
||||
test.remove_all_orchestrators();
|
||||
|
||||
let orchestrator1 = test.add_orchestrator()?;
|
||||
let orchestrator2 = test.add_orchestrator()?;
|
||||
|
||||
test.execute_raw(
|
||||
test.admin_unchecked(),
|
||||
ExecuteMsg::RevokeNetworkMonitorOrchestrator {
|
||||
address: orchestrator1.to_string(),
|
||||
},
|
||||
)?;
|
||||
|
||||
let res = query_network_monitor_orchestrators(test.deps())?;
|
||||
|
||||
assert!(!res.authorised.iter().any(|o| o.address == orchestrator1));
|
||||
assert!(res.authorised.iter().any(|o| o.address == orchestrator2));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn returns_entries_in_ascending_order() -> anyhow::Result<()> {
|
||||
// make sure to start with an empty state
|
||||
let mut test = init_contract_tester();
|
||||
test.remove_all_orchestrators();
|
||||
|
||||
test.add_orchestrator()?;
|
||||
test.add_orchestrator()?;
|
||||
test.add_orchestrator()?;
|
||||
|
||||
let res = query_network_monitor_orchestrators(test.deps())?;
|
||||
|
||||
assert!(res
|
||||
.authorised
|
||||
.windows(2)
|
||||
.all(|window| window[0].address <= window[1].address));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod network_monitor_agents_query {
|
||||
use super::*;
|
||||
use crate::testing::{
|
||||
init_contract_tester, storage_socket_comp, NetworkMonitorsContract,
|
||||
NetworkMonitorsContractTesterExt,
|
||||
};
|
||||
use nym_contracts_common_testing::{ContractOpts, ContractTester};
|
||||
use std::net::SocketAddr;
|
||||
|
||||
fn storage_sorted_addresses(
|
||||
test: &mut ContractTester<NetworkMonitorsContract>,
|
||||
n: usize,
|
||||
) -> Vec<SocketAddr> {
|
||||
let mut ips = Vec::new();
|
||||
for _ in 0..n {
|
||||
ips.push(test.random_socket());
|
||||
}
|
||||
|
||||
ips.sort_by(|a, b| storage_socket_comp(*a, *b));
|
||||
ips
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn returns_empty_response_when_no_agents_are_authorised() -> anyhow::Result<()> {
|
||||
let test = init_contract_tester();
|
||||
|
||||
let res = query_network_monitor_agents(test.deps(), None, None)?;
|
||||
|
||||
assert!(res.authorised.is_empty());
|
||||
assert_eq!(res.start_next_after, None);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn returns_all_authorised_agents_below_default_limit() -> anyhow::Result<()> {
|
||||
let mut test = init_contract_tester();
|
||||
let agents = storage_sorted_addresses(&mut test, 5);
|
||||
|
||||
for agent in &agents {
|
||||
test.add_dummy_agent(*agent)
|
||||
}
|
||||
|
||||
let res = query_network_monitor_agents(test.deps(), None, None)?;
|
||||
|
||||
assert_eq!(res.authorised.len(), agents.len());
|
||||
assert_eq!(res.start_next_after, agents.last().copied());
|
||||
|
||||
for agent in &agents {
|
||||
assert!(res.authorised.iter().any(|a| a.mixnet_address == *agent));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn respects_explicit_limit() -> anyhow::Result<()> {
|
||||
let mut test = init_contract_tester();
|
||||
let agents = storage_sorted_addresses(&mut test, 5);
|
||||
|
||||
for agent in &agents {
|
||||
test.add_dummy_agent(*agent)
|
||||
}
|
||||
|
||||
let res = query_network_monitor_agents(test.deps(), None, Some(2))?;
|
||||
|
||||
assert_eq!(res.authorised.len(), 2);
|
||||
assert_eq!(res.authorised[0].mixnet_address, agents[0]);
|
||||
assert_eq!(res.authorised[1].mixnet_address, agents[1]);
|
||||
assert_eq!(res.start_next_after, Some(agents[1]));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn respects_start_after_for_pagination() -> anyhow::Result<()> {
|
||||
let mut test = init_contract_tester();
|
||||
let agents = storage_sorted_addresses(&mut test, 5);
|
||||
|
||||
for agent in &agents {
|
||||
test.add_dummy_agent(*agent)
|
||||
}
|
||||
|
||||
let res = query_network_monitor_agents(test.deps(), Some(agents[1]), Some(2))?;
|
||||
|
||||
assert_eq!(res.authorised.len(), 2);
|
||||
assert_eq!(res.authorised[0].mixnet_address, agents[2]);
|
||||
assert_eq!(res.authorised[1].mixnet_address, agents[3]);
|
||||
assert_eq!(res.start_next_after, Some(agents[3]));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn caps_limit_at_maximum() -> anyhow::Result<()> {
|
||||
let mut test = init_contract_tester();
|
||||
let total = retrieval_limits::AGENTS_MAX_LIMIT as usize + 20;
|
||||
let agents = storage_sorted_addresses(&mut test, total);
|
||||
|
||||
for agent in &agents {
|
||||
test.add_dummy_agent(*agent)
|
||||
}
|
||||
|
||||
let res = query_network_monitor_agents(
|
||||
test.deps(),
|
||||
None,
|
||||
Some(retrieval_limits::AGENTS_MAX_LIMIT + 1),
|
||||
)?;
|
||||
|
||||
assert_eq!(
|
||||
res.authorised.len(),
|
||||
retrieval_limits::AGENTS_MAX_LIMIT as usize
|
||||
);
|
||||
assert_eq!(
|
||||
res.start_next_after,
|
||||
Some(agents[retrieval_limits::AGENTS_MAX_LIMIT as usize - 1])
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn start_next_after_is_none_for_empty_page() -> anyhow::Result<()> {
|
||||
let mut test = init_contract_tester();
|
||||
let agents = storage_sorted_addresses(&mut test, 3);
|
||||
|
||||
for agent in &agents {
|
||||
test.add_dummy_agent(*agent)
|
||||
}
|
||||
|
||||
let res = query_network_monitor_agents(test.deps(), Some(agents[2]), Some(10))?;
|
||||
|
||||
assert!(res.authorised.is_empty());
|
||||
assert_eq!(res.start_next_after, None);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn returns_entries_in_ascending_order() -> anyhow::Result<()> {
|
||||
let mut test = init_contract_tester();
|
||||
let agents = storage_sorted_addresses(&mut test, 6);
|
||||
|
||||
for agent in &agents {
|
||||
test.add_dummy_agent(*agent)
|
||||
}
|
||||
|
||||
let res = query_network_monitor_agents(test.deps(), None, None)?;
|
||||
|
||||
assert!(res.authorised.windows(2).all(|window| storage_socket_comp(
|
||||
window[0].mixnet_address,
|
||||
window[1].mixnet_address
|
||||
)
|
||||
.is_le()));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,188 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::contract::{execute, instantiate, migrate, query};
|
||||
use cosmwasm_std::{Addr, Order};
|
||||
use nym_contracts_common_testing::{
|
||||
mock_dependencies, AdminExt, ChainOpts, CommonStorageKeys, ContractFn, ContractOpts,
|
||||
ContractTester, DenomExt, PermissionedFn, QueryFn, RandExt, Rng, RngCore, TestableNymContract,
|
||||
};
|
||||
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
|
||||
|
||||
use crate::storage::NetworkMonitorsStorage;
|
||||
use nym_network_monitors_contract_common::constants::storage_keys;
|
||||
use nym_network_monitors_contract_common::{
|
||||
ExecuteMsg, InstantiateMsg, MigrateMsg, NetworkMonitorsContractError, QueryMsg,
|
||||
};
|
||||
|
||||
pub struct NetworkMonitorsContract;
|
||||
|
||||
impl TestableNymContract for NetworkMonitorsContract {
|
||||
const NAME: &'static str = "nym-network-monitors-contract";
|
||||
type InitMsg = InstantiateMsg;
|
||||
type ExecuteMsg = ExecuteMsg;
|
||||
type QueryMsg = QueryMsg;
|
||||
type MigrateMsg = MigrateMsg;
|
||||
type ContractError = NetworkMonitorsContractError;
|
||||
|
||||
fn instantiate() -> ContractFn<Self::InitMsg, Self::ContractError> {
|
||||
instantiate
|
||||
}
|
||||
|
||||
fn execute() -> ContractFn<Self::ExecuteMsg, Self::ContractError> {
|
||||
execute
|
||||
}
|
||||
|
||||
fn query() -> QueryFn<Self::QueryMsg, Self::ContractError> {
|
||||
query
|
||||
}
|
||||
|
||||
fn migrate() -> PermissionedFn<Self::MigrateMsg, Self::ContractError> {
|
||||
migrate
|
||||
}
|
||||
|
||||
fn base_init_msg() -> Self::InitMsg {
|
||||
let deps = mock_dependencies();
|
||||
InstantiateMsg {
|
||||
orchestrator_address: deps.api.addr_make("initial-dummy-orchestrator").to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init_contract_tester() -> ContractTester<NetworkMonitorsContract> {
|
||||
NetworkMonitorsContract::init()
|
||||
.with_common_storage_key(CommonStorageKeys::Admin, storage_keys::CONTRACT_ADMIN)
|
||||
}
|
||||
|
||||
pub trait NetworkMonitorsContractTesterExt:
|
||||
ContractOpts<
|
||||
ExecuteMsg = ExecuteMsg,
|
||||
QueryMsg = QueryMsg,
|
||||
ContractError = NetworkMonitorsContractError,
|
||||
> + ChainOpts
|
||||
+ AdminExt
|
||||
+ DenomExt
|
||||
+ RandExt
|
||||
{
|
||||
fn add_orchestrator(&mut self) -> Result<Addr, NetworkMonitorsContractError> {
|
||||
let admin = self.admin_unchecked();
|
||||
let addr = self.generate_account();
|
||||
self.execute_raw(
|
||||
admin,
|
||||
ExecuteMsg::AuthoriseNetworkMonitorOrchestrator {
|
||||
address: addr.to_string(),
|
||||
},
|
||||
)?;
|
||||
Ok(addr)
|
||||
}
|
||||
|
||||
fn remove_all_orchestrators(&mut self) {
|
||||
let orchestrators = self.all_orchestrators();
|
||||
for orchestrator in orchestrators {
|
||||
self.execute_raw(
|
||||
self.admin_unchecked(),
|
||||
ExecuteMsg::RevokeNetworkMonitorOrchestrator {
|
||||
address: orchestrator.to_string(),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
fn add_dummy_agent(&mut self, agent: SocketAddr) {
|
||||
let orchestrators = self.all_orchestrators();
|
||||
let orchestrator = match orchestrators.first() {
|
||||
Some(orchestrator) => orchestrator.clone(),
|
||||
None => self.add_orchestrator().unwrap().clone(),
|
||||
};
|
||||
|
||||
self.execute_raw(
|
||||
orchestrator,
|
||||
ExecuteMsg::AuthoriseNetworkMonitor {
|
||||
mixnet_address: agent,
|
||||
bs58_x25519_noise: "11111111111111111111111111111111".to_string(),
|
||||
noise_version: 1,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn random_ipv4(&mut self) -> IpAddr {
|
||||
let rng = self.raw_rng();
|
||||
IpAddr::V4(Ipv4Addr::new(rng.gen(), rng.gen(), rng.gen(), rng.gen()))
|
||||
}
|
||||
|
||||
fn random_ipv6(&mut self) -> IpAddr {
|
||||
let rng = self.raw_rng();
|
||||
IpAddr::V6(Ipv6Addr::new(
|
||||
rng.gen(),
|
||||
rng.gen(),
|
||||
rng.gen(),
|
||||
rng.gen(),
|
||||
rng.gen(),
|
||||
rng.gen(),
|
||||
rng.gen(),
|
||||
rng.gen(),
|
||||
))
|
||||
}
|
||||
|
||||
fn random_ip(&mut self) -> IpAddr {
|
||||
let rng = self.raw_rng();
|
||||
|
||||
// toss a coin, if even => ipv4, if odd => ipv6
|
||||
if rng.next_u32() % 2 == 0 {
|
||||
self.random_ipv4()
|
||||
} else {
|
||||
self.random_ipv6()
|
||||
}
|
||||
}
|
||||
|
||||
fn random_socket_ipv4(&mut self) -> SocketAddr {
|
||||
let port = self.raw_rng().gen();
|
||||
SocketAddr::new(self.random_ipv4(), port)
|
||||
}
|
||||
|
||||
fn random_socket_ipv6(&mut self) -> SocketAddr {
|
||||
let port = self.raw_rng().gen();
|
||||
SocketAddr::new(self.random_ipv6(), port)
|
||||
}
|
||||
|
||||
fn random_socket(&mut self) -> SocketAddr {
|
||||
let port = self.raw_rng().gen();
|
||||
SocketAddr::new(self.random_ip(), port)
|
||||
}
|
||||
|
||||
fn all_agents(&self) -> Vec<SocketAddr> {
|
||||
NetworkMonitorsStorage::new()
|
||||
.authorised_agents
|
||||
.range(self.storage(), None, None, Order::Ascending)
|
||||
.map(|record| record.unwrap().1.mixnet_address)
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn all_orchestrators(&self) -> Vec<Addr> {
|
||||
NetworkMonitorsStorage::new()
|
||||
.authorised_orchestrators
|
||||
.range(self.storage(), None, None, Order::Ascending)
|
||||
.map(|record| record.unwrap().0)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl NetworkMonitorsContractTesterExt for ContractTester<NetworkMonitorsContract> {}
|
||||
|
||||
/// Compare SocketAddrs in the same order as the storage key encoding.
|
||||
///
|
||||
/// Storage keys are: `[0, ip_len] [ip_octets...] [port_be_bytes]`
|
||||
/// This means IPv4 (len=4) always sorts before IPv6 (len=16),
|
||||
/// within the same type keys sort by IP octets then by port.
|
||||
pub(crate) fn storage_socket_comp(a: SocketAddr, b: SocketAddr) -> std::cmp::Ordering {
|
||||
let ip_ord = match (a.ip(), b.ip()) {
|
||||
(IpAddr::V4(a), IpAddr::V4(b)) => a.octets().cmp(&b.octets()),
|
||||
(IpAddr::V6(a), IpAddr::V6(b)) => a.octets().cmp(&b.octets()),
|
||||
// length prefix [0, 4] < [0, 16] so all IPv4 sorts before all IPv6
|
||||
(IpAddr::V4(_), IpAddr::V6(_)) => std::cmp::Ordering::Less,
|
||||
(IpAddr::V6(_), IpAddr::V4(_)) => std::cmp::Ordering::Greater,
|
||||
};
|
||||
ip_ord.then(a.port().cmp(&b.port()))
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -17,6 +17,7 @@ GROUP_CONTRACT_ADDRESS=n1ewmwz97xm0h8rdk8sw7h9mwn866qkx9hl9zlmagqfkhuzvwk5hhq844
|
||||
MULTISIG_CONTRACT_ADDRESS=n1tz0setr8vkh9udp8xyxgpqc89ns27k4d0jx2h942hr0ax63yjhmqz6xct8
|
||||
COCONUT_DKG_CONTRACT_ADDRESS=n1v3n2ly2dp3a9ng3ff6rh26yfkn0pc5hed7w2shc5u9ca5c865utqj5elvh
|
||||
ECASH_CONTRACT_ADDRESS=n1v3vydvs2ued84yv3khqwtgldmgwn0elljsdh08dr5s2j9x4rc5fs9jlwz9
|
||||
NETWORK_MONITORS_CONTRACT_ADDRESS=n1x5krtvyqklj360x38v62ze42g8s8trfsfqzlv8c9296chcpvqadssqnem5
|
||||
|
||||
STATISTICS_SERVICE_DOMAIN_ADDRESS="http://0.0.0.0"
|
||||
NYXD=https://rpc.sandbox.nymtech.net
|
||||
|
||||
@@ -241,7 +241,7 @@ impl<R, S> AuthenticatedHandler<R, S> {
|
||||
.inner
|
||||
.shared_state
|
||||
.outbound_mix_sender
|
||||
.forward_packet(mix_packet)
|
||||
.forward_client_packet_without_delay(mix_packet)
|
||||
{
|
||||
error!("We failed to forward requested mix packet - {err}. Presumably our mix forwarder has crashed. We cannot continue.");
|
||||
process::exit(1);
|
||||
|
||||
@@ -77,6 +77,8 @@ pub struct LocalAuthenticatorOpts {
|
||||
pub struct GatewayTasksBuilder {
|
||||
config: Config,
|
||||
|
||||
network: NymNetworkDetails,
|
||||
|
||||
network_requester_opts: Option<LocalNetworkRequesterOpts>,
|
||||
|
||||
ip_packet_router_opts: Option<LocalIpPacketRouterOpts>,
|
||||
@@ -120,6 +122,7 @@ impl GatewayTasksBuilder {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn new(
|
||||
config: Config,
|
||||
network: NymNetworkDetails,
|
||||
identity: Arc<ed25519::KeyPair>,
|
||||
storage: GatewayStorage,
|
||||
mix_packet_sender: MixForwardingSender,
|
||||
@@ -133,6 +136,7 @@ impl GatewayTasksBuilder {
|
||||
) -> GatewayTasksBuilder {
|
||||
GatewayTasksBuilder {
|
||||
config,
|
||||
network,
|
||||
network_requester_opts: None,
|
||||
ip_packet_router_opts: None,
|
||||
authenticator_opts: None,
|
||||
@@ -184,8 +188,7 @@ impl GatewayTasksBuilder {
|
||||
.choose(&mut thread_rng())
|
||||
.ok_or(GatewayError::NoNyxdAvailable)?;
|
||||
|
||||
let network_details = NymNetworkDetails::new_from_env();
|
||||
let client_config = nyxd::Config::try_from_nym_network_details(&network_details)?;
|
||||
let client_config = nyxd::Config::try_from_nym_network_details(&self.network)?;
|
||||
|
||||
let nyxd_client = DirectSigningHttpRpcNyxdClient::connect_with_mnemonic(
|
||||
client_config,
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
|
||||
[package]
|
||||
name = "nym-api"
|
||||
version = "1.1.79"
|
||||
version = "1.1.80"
|
||||
authors.workspace = true
|
||||
edition = "2021"
|
||||
license = "GPL-3.0"
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
|
||||
CREATE TABLE nym_node_stress_testing_result
|
||||
(
|
||||
-- Orchestrator-local testrun id that produced this result. Paired with `submitter_pubkey`
|
||||
-- it uniquely identifies a measurement and lets us dedupe retried submissions (the
|
||||
-- orchestrator uses at-least-once delivery and may re-POST the same row after a crash
|
||||
-- between a successful POST and its watermark update).
|
||||
testrun_id INTEGER NOT NULL,
|
||||
|
||||
-- Base58-encoded ed25519 identity key of the submitting orchestrator. Part of the primary
|
||||
-- key so distinct orchestrators can coincidentally share a `testrun_id` without colliding.
|
||||
submitter_pubkey TEXT NOT NULL,
|
||||
|
||||
-- unfortunately, due to legacy reasons we have separate tables for mixnodes and gateways
|
||||
-- so that we can't put a reference constraint here
|
||||
node_id INTEGER NOT NULL,
|
||||
|
||||
result REAL NOT NULL,
|
||||
|
||||
was_reachable BOOLEAN NOT NULL,
|
||||
|
||||
test_timestamp TIMESTAMP WITHOUT TIME ZONE NOT NULL,
|
||||
|
||||
PRIMARY KEY (testrun_id, submitter_pubkey)
|
||||
);
|
||||
@@ -2,6 +2,8 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::pagination::PaginatedResponse;
|
||||
use nym_crypto::asymmetric::ed25519;
|
||||
use nym_crypto::asymmetric::ed25519::serde_helpers::bs58_ed25519_pubkey;
|
||||
use nym_mixnet_contract_common::NodeId;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -72,3 +74,111 @@ pub struct GatewayCoreStatusResponse {
|
||||
pub identity: String,
|
||||
pub count: i64,
|
||||
}
|
||||
|
||||
pub use v3::*;
|
||||
|
||||
/// Request/response types for the v3 network-monitor flow, in which an orchestrator submits
|
||||
/// stress testing results to nym-api via signed batches.
|
||||
pub mod v3 {
|
||||
use super::*;
|
||||
use crate::signable::SignedMessage;
|
||||
use std::time::Duration;
|
||||
use time::OffsetDateTime;
|
||||
|
||||
/// Signed envelope posted by a network monitor orchestrator to
|
||||
/// `POST /v3/nym-nodes/stress-testing/batch-submit`.
|
||||
///
|
||||
/// The signature is checked against the `signer` field of the inner
|
||||
/// [`StressTestBatchSubmissionContent`], which must also match one of the orchestrators
|
||||
/// registered in the network-monitors contract.
|
||||
pub type StressTestBatchSubmission = SignedMessage<StressTestBatchSubmissionContent>;
|
||||
|
||||
/// Confirmation returned to an orchestrator after a successful submission.
|
||||
/// Currently empty — exists to give the response an explicit type rather than
|
||||
/// relying on `Json(())`.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct StressTestBatchSubmissionResponse {}
|
||||
|
||||
/// Single stress-test measurement for one node, produced by a network monitor orchestrator.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct StressTestResult {
|
||||
/// Orchestrator-local id of the test run that produced this result. Combined with the
|
||||
/// batch's `signer` it uniquely identifies the measurement, allowing nym-api to dedupe
|
||||
/// retried submissions on the at-least-once delivery path.
|
||||
pub testrun_id: i64,
|
||||
|
||||
/// Contract-assigned id of the node that was tested.
|
||||
pub node_id: NodeId,
|
||||
|
||||
/// Whether the tested node was acting as a mixnode during the measurement.
|
||||
///
|
||||
/// Included explicitly (rather than inferred from on-chain role) so the API can reject or
|
||||
/// route entries that don't match the expected role without re-querying the contract.
|
||||
pub is_mixnode: bool,
|
||||
|
||||
#[schema(value_type = String)]
|
||||
#[serde(with = "time::serde::rfc3339")]
|
||||
pub test_timestamp: OffsetDateTime,
|
||||
|
||||
/// Measured performance score in the `[0.0, 1.0]` range.
|
||||
pub test_performance: f64,
|
||||
|
||||
/// Whether the node responded at all during testing.
|
||||
///
|
||||
/// Recorded alongside `test_performance` so that a genuine 0.0 score (node responded but
|
||||
/// dropped everything) can be distinguished from the node being offline entirely.
|
||||
pub was_reachable: bool,
|
||||
}
|
||||
|
||||
/// Body of a stress-test batch submission, signed by a network monitor orchestrator.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct StressTestBatchSubmissionContent {
|
||||
/// ed25519 identity key of the submitting orchestrator. Must match an entry in the
|
||||
/// network-monitors contract for the batch to be accepted.
|
||||
#[schema(value_type = String)]
|
||||
#[serde(with = "ed25519::bs58_ed25519_pubkey")]
|
||||
pub signer: ed25519::PublicKey,
|
||||
|
||||
/// Time at which this batch was produced. Also used as a monotonic nonce for replay
|
||||
/// protection: the API rejects submissions whose timestamp is not strictly greater than
|
||||
/// the orchestrator's previous accepted submission.
|
||||
#[schema(value_type = String)]
|
||||
#[serde(with = "time::serde::rfc3339")]
|
||||
pub timestamp: OffsetDateTime,
|
||||
|
||||
pub results: Vec<StressTestResult>,
|
||||
}
|
||||
|
||||
impl StressTestBatchSubmissionContent {
|
||||
/// Build a batch submission body stamped with the current UTC time.
|
||||
pub fn new(signer: ed25519::PublicKey, results: Vec<StressTestResult>) -> Self {
|
||||
StressTestBatchSubmissionContent {
|
||||
signer,
|
||||
timestamp: OffsetDateTime::now_utc(),
|
||||
results,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether this submission is older than `max_age` relative to the current UTC time.
|
||||
///
|
||||
/// Used server-side to reject submissions that have been sitting around too long, even if
|
||||
/// they are otherwise well-formed and correctly signed.
|
||||
pub fn is_stale(&self, max_age: Duration) -> bool {
|
||||
self.timestamp + max_age < OffsetDateTime::now_utc()
|
||||
}
|
||||
}
|
||||
|
||||
/// Response body for `GET /v3/nym-nodes/stress-testing/known-monitors/{identity_key}`,
|
||||
/// used by orchestrators to check whether this nym-api currently recognises their key.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct KnownNetworkMonitorResponse {
|
||||
/// The ed25519 identity key that was queried (base58-encoded on the wire).
|
||||
#[serde(with = "bs58_ed25519_pubkey")]
|
||||
#[schema(value_type = String)]
|
||||
pub identity_key: ed25519::PublicKey,
|
||||
|
||||
/// Whether the queried identity key is currently recognised by this nym-api
|
||||
/// as an authorised network monitor permitted to submit stress testing results.
|
||||
pub authorised: bool,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::helpers::PlaceholderJsonSchemaImpl;
|
||||
use crate::models::DisplayRole;
|
||||
use crate::pagination::PaginatedResponse;
|
||||
use cosmwasm_std::Decimal;
|
||||
use nym_contracts_common::{IdentityKey, NaiveFloat};
|
||||
@@ -15,7 +16,6 @@ use std::time::Duration;
|
||||
use time::{Date, OffsetDateTime};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use crate::models::DisplayRole;
|
||||
pub use config_score::*;
|
||||
|
||||
pub type StakeSaturation = Decimal;
|
||||
@@ -406,17 +406,17 @@ pub struct NodePerformance {
|
||||
feature = "generate-ts",
|
||||
ts(
|
||||
export,
|
||||
export_to = "ts-packages/types/src/types/rust/NodeAnnotation.ts"
|
||||
export_to = "ts-packages/types/src/types/rust/NodeAnnotationV1.ts"
|
||||
)
|
||||
)]
|
||||
pub struct NodeAnnotation {
|
||||
pub struct NodeAnnotationV1 {
|
||||
#[cfg_attr(feature = "generate-ts", ts(type = "string"))]
|
||||
// legacy
|
||||
#[schema(value_type = String)]
|
||||
pub last_24h_performance: Performance,
|
||||
pub current_role: Option<DisplayRole>,
|
||||
|
||||
pub detailed_performance: DetailedNodePerformance,
|
||||
pub detailed_performance: DetailedNodePerformanceV1,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema, ToSchema)]
|
||||
@@ -425,11 +425,47 @@ pub struct NodeAnnotation {
|
||||
feature = "generate-ts",
|
||||
ts(
|
||||
export,
|
||||
export_to = "ts-packages/types/src/types/rust/DetailedNodePerformance.ts"
|
||||
export_to = "ts-packages/types/src/types/rust/NodeAnnotationV2.ts"
|
||||
)
|
||||
)]
|
||||
pub struct NodeAnnotationV2 {
|
||||
pub current_role: Option<DisplayRole>,
|
||||
|
||||
pub detailed_performance: DetailedNodePerformanceV2,
|
||||
}
|
||||
|
||||
impl From<NodeAnnotationV2> for NodeAnnotationV1 {
|
||||
fn from(value: NodeAnnotationV2) -> Self {
|
||||
// map it from 0-1 range into 0-100
|
||||
let scaled_performance =
|
||||
value.detailed_performance.performance_score.clamp(0.0, 1.0) * 100.;
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let legacy_performance =
|
||||
Performance::from_percentage_value(scaled_performance as u64).unwrap();
|
||||
|
||||
NodeAnnotationV1 {
|
||||
last_24h_performance: legacy_performance,
|
||||
current_role: value.current_role,
|
||||
detailed_performance: DetailedNodePerformanceV1 {
|
||||
performance_score: value.detailed_performance.performance_score,
|
||||
routing_score: value.detailed_performance.routing_score,
|
||||
config_score: value.detailed_performance.config_score,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema, ToSchema)]
|
||||
#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))]
|
||||
#[cfg_attr(
|
||||
feature = "generate-ts",
|
||||
ts(
|
||||
export,
|
||||
export_to = "ts-packages/types/src/types/rust/DetailedNodePerformanceV1.ts"
|
||||
)
|
||||
)]
|
||||
#[non_exhaustive]
|
||||
pub struct DetailedNodePerformance {
|
||||
pub struct DetailedNodePerformanceV1 {
|
||||
/// routing_score * config_score
|
||||
pub performance_score: f64,
|
||||
|
||||
@@ -437,12 +473,12 @@ pub struct DetailedNodePerformance {
|
||||
pub config_score: ConfigScore,
|
||||
}
|
||||
|
||||
impl DetailedNodePerformance {
|
||||
impl DetailedNodePerformanceV1 {
|
||||
pub fn new(
|
||||
performance_score: f64,
|
||||
routing_score: RoutingScore,
|
||||
config_score: ConfigScore,
|
||||
) -> DetailedNodePerformance {
|
||||
) -> DetailedNodePerformanceV1 {
|
||||
Self {
|
||||
performance_score,
|
||||
routing_score,
|
||||
@@ -455,6 +491,47 @@ impl DetailedNodePerformance {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema, ToSchema)]
|
||||
#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))]
|
||||
#[cfg_attr(
|
||||
feature = "generate-ts",
|
||||
ts(
|
||||
export,
|
||||
export_to = "ts-packages/types/src/types/rust/DetailedNodePerformanceV2.ts"
|
||||
)
|
||||
)]
|
||||
#[non_exhaustive]
|
||||
pub struct DetailedNodePerformanceV2 {
|
||||
/// routing_score * config_score
|
||||
/// or
|
||||
/// routing_score * config_score * stress_testing_score, if enabled
|
||||
pub performance_score: f64,
|
||||
|
||||
pub routing_score: RoutingScore,
|
||||
pub config_score: ConfigScore,
|
||||
pub stress_testing_score: StressTestingScore,
|
||||
}
|
||||
|
||||
impl DetailedNodePerformanceV2 {
|
||||
pub fn new(
|
||||
performance_score: f64,
|
||||
routing_score: RoutingScore,
|
||||
config_score: ConfigScore,
|
||||
stress_testing_score: StressTestingScore,
|
||||
) -> DetailedNodePerformanceV2 {
|
||||
Self {
|
||||
performance_score,
|
||||
routing_score,
|
||||
config_score,
|
||||
stress_testing_score,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_rewarding_performance(&self) -> Performance {
|
||||
Performance::naive_try_from_f64(self.performance_score).unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema, ToSchema)]
|
||||
#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))]
|
||||
#[cfg_attr(
|
||||
@@ -481,6 +558,32 @@ impl RoutingScore {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema, ToSchema)]
|
||||
#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))]
|
||||
#[cfg_attr(
|
||||
feature = "generate-ts",
|
||||
ts(
|
||||
export,
|
||||
export_to = "ts-packages/types/src/types/rust/StressTestingScore.ts"
|
||||
)
|
||||
)]
|
||||
pub struct StressTestingScore {
|
||||
pub score: f64,
|
||||
/// Distinguishes a genuine zero score (node was tested and scored 0) from
|
||||
/// "node was unreachable" (no successful sample was collected). Consumers may use
|
||||
/// this to decide whether to penalise the node or treat the score as missing.
|
||||
pub was_reachable: bool,
|
||||
}
|
||||
|
||||
impl StressTestingScore {
|
||||
pub fn unreachable() -> Self {
|
||||
StressTestingScore {
|
||||
score: 0.0,
|
||||
was_reachable: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema, ToSchema)]
|
||||
#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))]
|
||||
#[cfg_attr(
|
||||
@@ -541,13 +644,28 @@ impl ConfigScore {
|
||||
feature = "generate-ts",
|
||||
ts(
|
||||
export,
|
||||
export_to = "ts-packages/types/src/types/rust/AnnotationResponse.ts"
|
||||
export_to = "ts-packages/types/src/types/rust/AnnotationResponseV1.ts"
|
||||
)
|
||||
)]
|
||||
pub struct AnnotationResponse {
|
||||
pub struct AnnotationResponseV1 {
|
||||
#[schema(value_type = u32)]
|
||||
pub node_id: NodeId,
|
||||
pub annotation: Option<NodeAnnotation>,
|
||||
pub annotation: Option<NodeAnnotationV1>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, ToSchema)]
|
||||
#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))]
|
||||
#[cfg_attr(
|
||||
feature = "generate-ts",
|
||||
ts(
|
||||
export,
|
||||
export_to = "ts-packages/types/src/types/rust/AnnotationResponseV2.ts"
|
||||
)
|
||||
)]
|
||||
pub struct AnnotationResponseV2 {
|
||||
#[schema(value_type = u32)]
|
||||
pub node_id: NodeId,
|
||||
pub annotation: Option<NodeAnnotationV2>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, ToSchema)]
|
||||
|
||||
@@ -55,7 +55,8 @@ impl<T> SignedMessage<T> {
|
||||
pub(crate) mod sealed {
|
||||
use crate::ecash::models::*;
|
||||
use crate::models::{
|
||||
ChainBlocksStatusResponseBody, DetailedSignersStatusResponseBody, SignersStatusResponseBody,
|
||||
v3, ChainBlocksStatusResponseBody, DetailedSignersStatusResponseBody,
|
||||
SignersStatusResponseBody,
|
||||
};
|
||||
|
||||
pub trait Sealed {}
|
||||
@@ -72,4 +73,7 @@ pub(crate) mod sealed {
|
||||
impl Sealed for ChainBlocksStatusResponseBody {}
|
||||
impl Sealed for SignersStatusResponseBody {}
|
||||
impl Sealed for DetailedSignersStatusResponseBody {}
|
||||
|
||||
// v3 stress testing
|
||||
impl Sealed for v3::StressTestBatchSubmissionContent {}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ use crate::support::http::state::chain_status::ChainStatusCache;
|
||||
use crate::support::http::state::contract_details::ContractDetailsCache;
|
||||
use crate::support::http::state::force_refresh::ForcedRefresh;
|
||||
use crate::support::http::state::mixnet_contract_cache::MixnetContractCacheState;
|
||||
use crate::support::http::state::network_monitors::{LastNMSubmissions, NetworkMonitorsCache};
|
||||
use crate::support::http::state::node_annotations_cache::NodeAnnotationsCache;
|
||||
use crate::support::http::state::AppState;
|
||||
use crate::support::nyxd::Client;
|
||||
@@ -1301,7 +1302,9 @@ impl TestFixture {
|
||||
ecash_signers_cache: Default::default(),
|
||||
address_info_cache: AddressInfoCache::new(Duration::from_secs(42), 1000),
|
||||
forced_refresh: ForcedRefresh::new(true),
|
||||
network_monitor_submissions: LastNMSubmissions::new(),
|
||||
mixnet_contract_cache,
|
||||
network_monitors_cache: NetworkMonitorsCache::new(Duration::from_secs(42)),
|
||||
node_annotations_cache,
|
||||
storage,
|
||||
described_nodes_cache: SharedCache::<DescribedNodes>::new(),
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
use crate::epoch_operations::EpochAdvancer;
|
||||
use crate::support::caching::cache::UninitialisedCache;
|
||||
use cosmwasm_std::{Decimal, Fraction};
|
||||
use nym_api_requests::models::NodeAnnotation;
|
||||
use nym_api_requests::models::NodeAnnotationV2;
|
||||
use nym_mixnet_contract_common::helpers::IntoBaseDecimal;
|
||||
use nym_mixnet_contract_common::reward_params::{NodeRewardingParameters, Performance, WorkFactor};
|
||||
use nym_mixnet_contract_common::{
|
||||
@@ -211,7 +211,7 @@ fn determine_per_node_work(
|
||||
impl EpochAdvancer {
|
||||
fn load_performance(
|
||||
status_cache: &Result<
|
||||
RwLockReadGuard<'_, HashMap<NodeId, NodeAnnotation>>,
|
||||
RwLockReadGuard<'_, HashMap<NodeId, NodeAnnotationV2>>,
|
||||
UninitialisedCache,
|
||||
>,
|
||||
node_id: NodeId,
|
||||
|
||||
@@ -8,8 +8,7 @@ use crate::node_describe_cache::cache::DescribedNodes;
|
||||
use crate::node_describe_cache::NodeDescriptionTopologyExt;
|
||||
use crate::node_status_api::NodeStatusCache;
|
||||
use crate::support::caching::cache::SharedCache;
|
||||
use nym_api_requests::models::{NodeAnnotation, NymNodeDescriptionV2};
|
||||
use nym_contracts_common::NaiveFloat;
|
||||
use nym_api_requests::models::{NodeAnnotationV2, NymNodeDescriptionV2};
|
||||
use nym_crypto::asymmetric::{ed25519, x25519};
|
||||
use nym_mixnet_contract_common::{LegacyMixLayer, NodeId};
|
||||
use nym_node_tester_utils::node::{NodeType, TestableNode};
|
||||
@@ -205,7 +204,7 @@ impl PacketPreparer {
|
||||
&self,
|
||||
rng: &mut R,
|
||||
current_rotation_id: u32,
|
||||
node_statuses: &HashMap<NodeId, NodeAnnotation>,
|
||||
node_statuses: &HashMap<NodeId, NodeAnnotationV2>,
|
||||
mixing_nym_nodes: impl Iterator<Item = &'a NymNodeDescriptionV2> + 'a,
|
||||
) -> HashMap<LegacyMixLayer, Vec<(RoutingNode, f64)>> {
|
||||
let mut layered_mixes = HashMap::new();
|
||||
@@ -219,7 +218,7 @@ impl PacketPreparer {
|
||||
// if the node is not present, default to 0.5
|
||||
let weight = node_statuses
|
||||
.get(&mixing_nym_node.node_id)
|
||||
.map(|node| node.last_24h_performance.naive_to_f64())
|
||||
.map(|node| node.detailed_performance.performance_score)
|
||||
.unwrap_or(0.5);
|
||||
let layer = self.random_legacy_layer(rng);
|
||||
let layer_mixes = layered_mixes.entry(layer).or_insert_with(Vec::new);
|
||||
@@ -245,7 +244,7 @@ impl PacketPreparer {
|
||||
fn to_legacy_gateway_nodes<'a>(
|
||||
&self,
|
||||
current_rotation_id: u32,
|
||||
node_statuses: &HashMap<NodeId, NodeAnnotation>,
|
||||
node_statuses: &HashMap<NodeId, NodeAnnotationV2>,
|
||||
gateway_capable_nym_nodes: impl Iterator<Item = &'a NymNodeDescriptionV2> + 'a,
|
||||
) -> Vec<(RoutingNode, f64)> {
|
||||
let mut gateways = Vec::new();
|
||||
@@ -259,7 +258,7 @@ impl PacketPreparer {
|
||||
// if the node is not present, default to 0.5
|
||||
let weight = node_statuses
|
||||
.get(&gateway_capable_node.node_id)
|
||||
.map(|node| node.last_24h_performance.naive_to_f64())
|
||||
.map(|node| node.detailed_performance.performance_score)
|
||||
.unwrap_or(0.5);
|
||||
gateways.push((parsed_node, weight))
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
use crate::support::caching::cache::UninitialisedCache;
|
||||
use nym_api_requests::models::{NymNodeDescriptionV1, NymNodeDescriptionV2};
|
||||
use nym_config::defaults::DEFAULT_NYM_NODE_HTTP_PORT;
|
||||
use nym_mixnet_contract_common::NodeId;
|
||||
use nym_node_requests::api::client::NymNodeApiClientError;
|
||||
use nym_topology::node::RoutingNodeError;
|
||||
@@ -23,18 +22,11 @@ pub enum NodeDescribeCacheError {
|
||||
source: UninitialisedCache,
|
||||
},
|
||||
|
||||
#[error("node {node_id} has provided malformed host information ({host}: {source}")]
|
||||
MalformedHost {
|
||||
host: String,
|
||||
#[error(transparent)]
|
||||
ClientRetrievalFailure(#[from] nym_node_requests::error::Error),
|
||||
|
||||
node_id: NodeId,
|
||||
|
||||
#[source]
|
||||
source: Box<NymNodeApiClientError>,
|
||||
},
|
||||
|
||||
#[error("node {node_id} with host '{host}' doesn't seem to expose its declared http port nor any of the standard API ports, i.e.: 80, 443 or {}", DEFAULT_NYM_NODE_HTTP_PORT)]
|
||||
NoHttpPortsAvailable { host: String, node_id: NodeId },
|
||||
#[error("failed to retrieve host information of node {node_id} - this is most likely a bug")]
|
||||
NoHostInformationAvailable { node_id: NodeId, host: String },
|
||||
|
||||
#[error("failed to query node {node_id}: {source}")]
|
||||
ApiFailure {
|
||||
@@ -44,17 +36,6 @@ pub enum NodeDescribeCacheError {
|
||||
source: Box<NymNodeApiClientError>,
|
||||
},
|
||||
|
||||
// TODO: perhaps include more details here like whether key/signature/payload was malformed
|
||||
#[error("could not verify signed host information for node {node_id}")]
|
||||
MissignedHostInformation { node_id: NodeId },
|
||||
|
||||
#[error("identity of node {node_id} does not match. expected {expected} but got {got}")]
|
||||
MismatchedIdentity {
|
||||
node_id: NodeId,
|
||||
expected: String,
|
||||
got: String,
|
||||
},
|
||||
|
||||
#[error("node {node_id} is announcing an illegal ip address")]
|
||||
IllegalIpAddress { node_id: NodeId },
|
||||
}
|
||||
|
||||
@@ -5,13 +5,10 @@ use crate::node_describe_cache::query_helpers::query_for_described_data;
|
||||
use crate::node_describe_cache::NodeDescribeCacheError;
|
||||
use nym_api_requests::models::{DescribedNodeTypeV2, NymNodeDescriptionV2};
|
||||
use nym_bin_common::bin_info;
|
||||
use nym_config::defaults::DEFAULT_NYM_NODE_HTTP_PORT;
|
||||
use nym_crypto::asymmetric::ed25519;
|
||||
use nym_mixnet_contract_common::{NodeId, NymNodeDetails};
|
||||
use nym_node_requests::api::client::NymNodeApiClientExt;
|
||||
use nym_validator_client::UserAgent;
|
||||
use std::time::Duration;
|
||||
use tracing::debug;
|
||||
use nym_node_requests::api::helpers::NymNodeApiClientRetriever;
|
||||
use tracing::{debug, error};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct RefreshData {
|
||||
@@ -69,93 +66,39 @@ impl RefreshData {
|
||||
}
|
||||
}
|
||||
|
||||
async fn try_get_client(
|
||||
host: &str,
|
||||
node_id: NodeId,
|
||||
custom_port: Option<u16>,
|
||||
) -> Result<nym_node_requests::api::Client, NodeDescribeCacheError> {
|
||||
// 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://{host}:{DEFAULT_NYM_NODE_HTTP_PORT}"), // 'standard' nym-node
|
||||
format!("https://{host}"), // node behind https proxy (443)
|
||||
format!("http://{host}"), // node behind http proxy (80)
|
||||
];
|
||||
|
||||
// note: I removed 'standard' legacy mixnode port because it should now be automatically pulled via
|
||||
// the 'custom_port' since it should have been present in the contract.
|
||||
|
||||
if let Some(port) = custom_port {
|
||||
addresses_to_try.insert(0, format!("http://{host}:{port}"));
|
||||
}
|
||||
|
||||
for address in addresses_to_try {
|
||||
// if provided host was malformed, no point in continuing
|
||||
let client = match nym_node_requests::api::Client::builder(address).and_then(|b| {
|
||||
b.with_timeout(Duration::from_secs(5))
|
||||
.no_hickory_dns()
|
||||
.with_user_agent(UserAgent::from(bin_info!()))
|
||||
.build()
|
||||
}) {
|
||||
Ok(client) => client,
|
||||
Err(err) => {
|
||||
return Err(NodeDescribeCacheError::MalformedHost {
|
||||
host: host.to_string(),
|
||||
node_id,
|
||||
source: Box::new(err),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if let Ok(health) = client.get_health().await {
|
||||
if health.status.is_up() {
|
||||
return Ok(client);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(NodeDescribeCacheError::NoHttpPortsAvailable {
|
||||
host: host.to_string(),
|
||||
node_id,
|
||||
})
|
||||
}
|
||||
|
||||
async fn try_get_description(
|
||||
data: RefreshData,
|
||||
allow_all_ips: bool,
|
||||
) -> Result<NymNodeDescriptionV2, NodeDescribeCacheError> {
|
||||
let client = try_get_client(&data.host, data.node_id, data.port).await?;
|
||||
let client = NymNodeApiClientRetriever::new(bin_info!())
|
||||
.with_expected_identity(Some(data.expected_identity.to_base58_string()))
|
||||
.with_verify_host_information()
|
||||
.with_custom_port(data.port)
|
||||
.get_client(&data.host, data.node_id)
|
||||
.await?;
|
||||
|
||||
let map_query_err = |err| NodeDescribeCacheError::ApiFailure {
|
||||
node_id: data.node_id,
|
||||
source: Box::new(err),
|
||||
let host_info = match client.host_information {
|
||||
Some(host_info) => host_info,
|
||||
// this branch should be impossible unless unexpected code changes occurred
|
||||
None => {
|
||||
error!(
|
||||
"failed to retrieve host information of node {} - this is most likely a bug",
|
||||
data.node_id
|
||||
);
|
||||
return Err(NodeDescribeCacheError::NoHostInformationAvailable {
|
||||
node_id: data.node_id,
|
||||
host: data.host.to_string(),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
let host_info = client.get_host_information().await.map_err(map_query_err)?;
|
||||
|
||||
// check if the identity key matches the information provided during bonding
|
||||
if data.expected_identity != host_info.keys.ed25519_identity {
|
||||
return Err(NodeDescribeCacheError::MismatchedIdentity {
|
||||
node_id: data.node_id,
|
||||
expected: data.expected_identity.to_base58_string(),
|
||||
got: host_info.keys.ed25519_identity.to_base58_string(),
|
||||
});
|
||||
}
|
||||
|
||||
if !host_info.verify_host_information() {
|
||||
return Err(NodeDescribeCacheError::MissignedHostInformation {
|
||||
node_id: data.node_id,
|
||||
});
|
||||
}
|
||||
|
||||
if !allow_all_ips && !host_info.data.check_ips() {
|
||||
return Err(NodeDescribeCacheError::IllegalIpAddress {
|
||||
node_id: data.node_id,
|
||||
});
|
||||
}
|
||||
|
||||
let node_info = query_for_described_data(&client, data.node_id).await?;
|
||||
let node_info = query_for_described_data(&client.client, data.node_id).await?;
|
||||
let description = node_info.into_node_description(host_info.data);
|
||||
|
||||
Ok(NymNodeDescriptionV2 {
|
||||
|
||||
@@ -68,7 +68,7 @@ impl ContractPerformanceProvider {
|
||||
|
||||
pub(crate) async fn node_routing_scores(
|
||||
&self,
|
||||
node_ids: Vec<NodeId>,
|
||||
node_ids: &[NodeId],
|
||||
epoch_id: EpochId,
|
||||
) -> Result<NodesRoutingScores, PerformanceRetrievalFailure> {
|
||||
let Some(first) = node_ids.first() else {
|
||||
@@ -84,7 +84,7 @@ impl ContractPerformanceProvider {
|
||||
})?;
|
||||
|
||||
let mut scores = HashMap::new();
|
||||
for node_id in node_ids {
|
||||
for &node_id in node_ids {
|
||||
let score = self.node_routing_score_with_fallback(&contract_cache, node_id, epoch_id);
|
||||
scores.insert(node_id, score);
|
||||
}
|
||||
|
||||
@@ -2,55 +2,66 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::mixnet_contract_cache::cache::MixnetContractCache;
|
||||
use crate::node_performance::provider::PerformanceRetrievalFailure;
|
||||
use crate::node_performance::provider::{NodesStressTestingScores, PerformanceRetrievalFailure};
|
||||
use crate::support::caching::cache::UninitialisedCache;
|
||||
use crate::support::storage::NymApiStorage;
|
||||
use nym_api_requests::models::RoutingScore;
|
||||
use nym_api_requests::models::{RoutingScore, StressTestingScore};
|
||||
use nym_mixnet_contract_common::{EpochId, NodeId};
|
||||
use std::collections::HashMap;
|
||||
use std::time::Duration;
|
||||
use time::OffsetDateTime;
|
||||
|
||||
pub(crate) struct LegacyStoragePerformanceProvider {
|
||||
/// Specifies the duration of the rolling average used for stress testing score.
|
||||
stress_testing_data_period: Duration,
|
||||
|
||||
storage: NymApiStorage,
|
||||
mixnet_contract_cache: MixnetContractCache,
|
||||
}
|
||||
|
||||
impl LegacyStoragePerformanceProvider {
|
||||
pub(crate) fn new(storage: NymApiStorage, mixnet_contract_cache: MixnetContractCache) -> Self {
|
||||
pub(crate) fn new(
|
||||
storage: NymApiStorage,
|
||||
mixnet_contract_cache: MixnetContractCache,
|
||||
stress_testing_data_period: Duration,
|
||||
) -> Self {
|
||||
LegacyStoragePerformanceProvider {
|
||||
stress_testing_data_period,
|
||||
storage,
|
||||
mixnet_contract_cache,
|
||||
}
|
||||
}
|
||||
|
||||
async fn map_epoch_id_to_end_unix_timestamp(
|
||||
async fn map_epoch_id_to_end_timestamp(
|
||||
&self,
|
||||
epoch_id: EpochId,
|
||||
) -> Result<i64, UninitialisedCache> {
|
||||
) -> Result<OffsetDateTime, UninitialisedCache> {
|
||||
let interval_details = self.mixnet_contract_cache.current_interval().await?;
|
||||
let duration = interval_details.epoch_length();
|
||||
let current_end = interval_details.current_epoch_end();
|
||||
let current_id = interval_details.current_epoch_absolute_id();
|
||||
|
||||
if current_id == epoch_id {
|
||||
return Ok(current_end.unix_timestamp());
|
||||
return Ok(current_end);
|
||||
}
|
||||
|
||||
if current_id < epoch_id {
|
||||
let diff = epoch_id - current_id;
|
||||
let end = current_end + diff * duration;
|
||||
return Ok(end.unix_timestamp());
|
||||
return Ok(end);
|
||||
}
|
||||
|
||||
// epoch_id > current_id
|
||||
let diff = current_id - epoch_id;
|
||||
let end = current_end - diff * duration;
|
||||
Ok(end.unix_timestamp())
|
||||
Ok(end)
|
||||
}
|
||||
|
||||
pub(crate) async fn epoch_id_unix_timestamp(
|
||||
pub(crate) async fn epoch_id_timestamp(
|
||||
&self,
|
||||
epoch_id: EpochId,
|
||||
) -> Result<i64, PerformanceRetrievalFailure> {
|
||||
self.map_epoch_id_to_end_unix_timestamp(epoch_id)
|
||||
) -> Result<OffsetDateTime, PerformanceRetrievalFailure> {
|
||||
self.map_epoch_id_to_end_timestamp(epoch_id)
|
||||
.await
|
||||
.map_err(|_| {
|
||||
PerformanceRetrievalFailure::new(
|
||||
@@ -66,7 +77,7 @@ impl LegacyStoragePerformanceProvider {
|
||||
node_id: NodeId,
|
||||
epoch_id: EpochId,
|
||||
) -> Result<RoutingScore, PerformanceRetrievalFailure> {
|
||||
let end_ts = self.epoch_id_unix_timestamp(epoch_id).await?;
|
||||
let end_ts = self.epoch_id_timestamp(epoch_id).await?.unix_timestamp();
|
||||
self.get_node_routing_score_with_unix_timestamp(node_id, epoch_id, end_ts)
|
||||
.await
|
||||
}
|
||||
@@ -88,4 +99,56 @@ impl LegacyStoragePerformanceProvider {
|
||||
let score = reliability / 100.;
|
||||
Ok(RoutingScore::new(score as f64))
|
||||
}
|
||||
|
||||
pub(crate) async fn node_stress_testing_score(
|
||||
&self,
|
||||
node_id: NodeId,
|
||||
epoch_id: EpochId,
|
||||
) -> Result<StressTestingScore, PerformanceRetrievalFailure> {
|
||||
let end_ts = self.epoch_id_timestamp(epoch_id).await?;
|
||||
let start_ts = end_ts - self.stress_testing_data_period;
|
||||
|
||||
self.node_stress_testing_score_in_range(node_id, epoch_id, start_ts, end_ts)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn node_stress_testing_score_in_range(
|
||||
&self,
|
||||
node_id: NodeId,
|
||||
epoch_id: EpochId,
|
||||
start_ts: OffsetDateTime,
|
||||
end_ts: OffsetDateTime,
|
||||
) -> Result<StressTestingScore, PerformanceRetrievalFailure> {
|
||||
let result = self
|
||||
.storage
|
||||
.get_average_node_stress_test_score(node_id, start_ts, end_ts)
|
||||
.await
|
||||
.map_err(|err| PerformanceRetrievalFailure::new(node_id, epoch_id, err.to_string()))?;
|
||||
|
||||
match result {
|
||||
None => Ok(StressTestingScore::unreachable()),
|
||||
Some(result) => Ok(result.into()),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn get_node_stress_testing_scores(
|
||||
&self,
|
||||
node_ids: &[NodeId],
|
||||
epoch_id: EpochId,
|
||||
) -> Result<NodesStressTestingScores, PerformanceRetrievalFailure> {
|
||||
let mut scores = HashMap::new();
|
||||
|
||||
let end_ts = self.epoch_id_timestamp(epoch_id).await?;
|
||||
let start_ts = end_ts - self.stress_testing_data_period;
|
||||
|
||||
for &node_id in node_ids {
|
||||
scores.insert(
|
||||
node_id,
|
||||
self.node_stress_testing_score_in_range(node_id, epoch_id, start_ts, end_ts)
|
||||
.await,
|
||||
);
|
||||
}
|
||||
|
||||
Ok(NodesStressTestingScores { inner: scores })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,11 +4,11 @@
|
||||
use crate::node_performance::provider::contract_provider::ContractPerformanceProvider;
|
||||
use async_trait::async_trait;
|
||||
use legacy_storage_provider::LegacyStoragePerformanceProvider;
|
||||
use nym_api_requests::models::RoutingScore;
|
||||
use nym_api_requests::models::{RoutingScore, StressTestingScore};
|
||||
use nym_mixnet_contract_common::{EpochId, NodeId};
|
||||
use std::collections::HashMap;
|
||||
use thiserror::Error;
|
||||
use tracing::debug;
|
||||
use tracing::{debug, error};
|
||||
|
||||
pub(crate) mod contract_provider;
|
||||
pub(crate) mod legacy_storage_provider;
|
||||
@@ -31,6 +31,41 @@ impl PerformanceRetrievalFailure {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct NodesStressTestingScores {
|
||||
inner: HashMap<NodeId, Result<StressTestingScore, PerformanceRetrievalFailure>>,
|
||||
}
|
||||
|
||||
impl NodesStressTestingScores {
|
||||
pub(crate) fn get_or_log(&self, node_id: NodeId) -> StressTestingScore {
|
||||
match self.inner.get(&node_id) {
|
||||
Some(Ok(score)) => *score,
|
||||
Some(Err(err)) => {
|
||||
debug!("{err}");
|
||||
StressTestingScore::unreachable()
|
||||
}
|
||||
None => StressTestingScore::unreachable(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Number of nodes for which the orchestrator has produced at least one reachable sample
|
||||
/// in the configured window. Used by the refresher to gate whether stress-testing data is
|
||||
/// applied at all: if the orchestrator is down or has not yet submitted anything, this
|
||||
/// returns 0 and the refresher falls back to routing × config score only.
|
||||
///
|
||||
/// Note: nodes that were tested but found unreachable (`was_reachable=false`) intentionally
|
||||
/// do **not** count here. Counting them would let a single recently-rebooted orchestrator
|
||||
/// pass the threshold while every node it touched still scored 0.
|
||||
pub(crate) fn available_count(&self) -> usize {
|
||||
self.inner
|
||||
.iter()
|
||||
.filter(|(_, v)| match v {
|
||||
Ok(score) => score.was_reachable,
|
||||
Err(_) => false,
|
||||
})
|
||||
.count()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct NodesRoutingScores {
|
||||
inner: HashMap<NodeId, Result<RoutingScore, PerformanceRetrievalFailure>>,
|
||||
}
|
||||
@@ -57,24 +92,39 @@ impl NodesRoutingScores {
|
||||
pub(crate) trait NodePerformanceProvider {
|
||||
/// Obtain a performance/routing score of a particular node for given epoch
|
||||
#[allow(unused)]
|
||||
async fn get_node_score(
|
||||
async fn get_node_routing_score(
|
||||
&self,
|
||||
node_id: NodeId,
|
||||
epoch_id: EpochId,
|
||||
) -> Result<RoutingScore, PerformanceRetrievalFailure>;
|
||||
|
||||
/// An optimisation for obtaining node scores of multiple nodes at once
|
||||
async fn get_batch_node_scores(
|
||||
async fn get_batch_node_routing_scores(
|
||||
&self,
|
||||
node_ids: Vec<NodeId>,
|
||||
node_ids: &[NodeId],
|
||||
epoch_id: EpochId,
|
||||
) -> Result<NodesRoutingScores, PerformanceRetrievalFailure>;
|
||||
|
||||
/// Obtain a stress-testing score of a particular node for given epoch
|
||||
#[allow(unused)]
|
||||
async fn get_node_stress_testing_score(
|
||||
&self,
|
||||
node_id: NodeId,
|
||||
epoch_id: EpochId,
|
||||
) -> Result<StressTestingScore, PerformanceRetrievalFailure>;
|
||||
|
||||
/// An optimisation for obtaining node scores of multiple nodes at once
|
||||
async fn get_batch_node_stress_testing_scores(
|
||||
&self,
|
||||
node_ids: &[NodeId],
|
||||
epoch_id: EpochId,
|
||||
) -> Result<NodesStressTestingScores, PerformanceRetrievalFailure>;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl NodePerformanceProvider for ContractPerformanceProvider {
|
||||
#[allow(unused)]
|
||||
async fn get_node_score(
|
||||
async fn get_node_routing_score(
|
||||
&self,
|
||||
node_id: NodeId,
|
||||
epoch_id: EpochId,
|
||||
@@ -82,19 +132,45 @@ impl NodePerformanceProvider for ContractPerformanceProvider {
|
||||
self.node_routing_score(node_id, epoch_id).await
|
||||
}
|
||||
|
||||
async fn get_batch_node_scores(
|
||||
async fn get_batch_node_routing_scores(
|
||||
&self,
|
||||
node_ids: Vec<NodeId>,
|
||||
node_ids: &[NodeId],
|
||||
epoch_id: EpochId,
|
||||
) -> Result<NodesRoutingScores, PerformanceRetrievalFailure> {
|
||||
self.node_routing_scores(node_ids, epoch_id).await
|
||||
}
|
||||
|
||||
async fn get_node_stress_testing_score(
|
||||
&self,
|
||||
node_id: NodeId,
|
||||
epoch_id: EpochId,
|
||||
) -> Result<StressTestingScore, PerformanceRetrievalFailure> {
|
||||
error!("stress testing data not available in contract data");
|
||||
Err(PerformanceRetrievalFailure {
|
||||
node_id,
|
||||
epoch_id,
|
||||
error: "stress testing data not available in contract data".to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
async fn get_batch_node_stress_testing_scores(
|
||||
&self,
|
||||
_: &[NodeId],
|
||||
epoch_id: EpochId,
|
||||
) -> Result<NodesStressTestingScores, PerformanceRetrievalFailure> {
|
||||
error!("stress testing data not available in contract data");
|
||||
Err(PerformanceRetrievalFailure {
|
||||
node_id: 0,
|
||||
epoch_id,
|
||||
error: "stress testing data not available in contract data".to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl NodePerformanceProvider for LegacyStoragePerformanceProvider {
|
||||
#[allow(unused)]
|
||||
async fn get_node_score(
|
||||
async fn get_node_routing_score(
|
||||
&self,
|
||||
node_id: NodeId,
|
||||
epoch_id: EpochId,
|
||||
@@ -102,15 +178,15 @@ impl NodePerformanceProvider for LegacyStoragePerformanceProvider {
|
||||
self.node_routing_score(node_id, epoch_id).await
|
||||
}
|
||||
|
||||
async fn get_batch_node_scores(
|
||||
async fn get_batch_node_routing_scores(
|
||||
&self,
|
||||
node_ids: Vec<NodeId>,
|
||||
node_ids: &[NodeId],
|
||||
epoch_id: EpochId,
|
||||
) -> Result<NodesRoutingScores, PerformanceRetrievalFailure> {
|
||||
let mut scores = HashMap::new();
|
||||
|
||||
let epoch_timestamp = self.epoch_id_unix_timestamp(epoch_id).await?;
|
||||
for node_id in node_ids {
|
||||
let epoch_timestamp = self.epoch_id_timestamp(epoch_id).await?.unix_timestamp();
|
||||
for &node_id in node_ids {
|
||||
scores.insert(
|
||||
node_id,
|
||||
self.get_node_routing_score_with_unix_timestamp(node_id, epoch_id, epoch_timestamp)
|
||||
@@ -120,4 +196,22 @@ impl NodePerformanceProvider for LegacyStoragePerformanceProvider {
|
||||
|
||||
Ok(NodesRoutingScores { inner: scores })
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
async fn get_node_stress_testing_score(
|
||||
&self,
|
||||
node_id: NodeId,
|
||||
epoch_id: EpochId,
|
||||
) -> Result<StressTestingScore, PerformanceRetrievalFailure> {
|
||||
self.node_stress_testing_score(node_id, epoch_id).await
|
||||
}
|
||||
|
||||
async fn get_batch_node_stress_testing_scores(
|
||||
&self,
|
||||
node_ids: &[NodeId],
|
||||
epoch_id: EpochId,
|
||||
) -> Result<NodesStressTestingScores, PerformanceRetrievalFailure> {
|
||||
self.get_node_stress_testing_scores(node_ids, epoch_id)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
+4
-4
@@ -1,7 +1,7 @@
|
||||
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use nym_api_requests::models::NodeAnnotation;
|
||||
use nym_api_requests::models::NodeAnnotationV2;
|
||||
use nym_mixnet_contract_common::NodeId;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
@@ -10,11 +10,11 @@ use std::collections::HashMap;
|
||||
#[allow(deprecated)]
|
||||
pub(crate) struct NodeStatusCacheData {
|
||||
/// Basic annotation for nym-nodes
|
||||
pub(crate) node_annotations: HashMap<NodeId, NodeAnnotation>,
|
||||
pub(crate) node_annotations: HashMap<NodeId, NodeAnnotationV2>,
|
||||
}
|
||||
|
||||
impl From<HashMap<NodeId, NodeAnnotation>> for NodeStatusCacheData {
|
||||
fn from(node_annotations: HashMap<NodeId, NodeAnnotation>) -> Self {
|
||||
impl From<HashMap<NodeId, NodeAnnotationV2>> for NodeStatusCacheData {
|
||||
fn from(node_annotations: HashMap<NodeId, NodeAnnotationV2>) -> Self {
|
||||
NodeStatusCacheData { node_annotations }
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user