Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 520b880b1b | |||
| b96dc11558 | |||
| 7b076ba212 | |||
| 39504b1d5f | |||
| d30bbb3f2a | |||
| 9d4690d5ad | |||
| 6cfa88c0b9 | |||
| 181e1f7526 | |||
| 69c54674cf | |||
| d15c47dbde | |||
| 7529cde148 | |||
| 9ca0f32c47 |
Generated
+4
-4
@@ -11482,7 +11482,7 @@ dependencies = [
|
||||
"nym-sdk",
|
||||
"reqwest 0.13.4",
|
||||
"rustls 0.23.40",
|
||||
"smoltcp 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"smoltcp",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
"tokio-rustls 0.26.4",
|
||||
@@ -12072,16 +12072,16 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sysinfo"
|
||||
version = "0.38.4"
|
||||
version = "0.37.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92ab6a2f8bfe508deb3c6406578252e491d299cbbf3bc0529ecc3313aee4a52f"
|
||||
checksum = "16607d5caffd1c07ce073528f9ed972d88db15dd44023fa57142963be3feb11f"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"memchr",
|
||||
"ntapi",
|
||||
"objc2-core-foundation",
|
||||
"objc2-io-kit",
|
||||
"windows 0.62.2",
|
||||
"windows 0.61.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
+1
-1
@@ -369,7 +369,7 @@ strum = "0.28.0"
|
||||
strum_macros = "0.28.0"
|
||||
subtle-encoding = "0.5"
|
||||
syn = "2"
|
||||
sysinfo = "0.38.4"
|
||||
sysinfo = "0.37.0"
|
||||
tap = "1.0.1"
|
||||
tar = "0.4.45"
|
||||
test-with = { version = "0.15.4", default-features = false }
|
||||
|
||||
@@ -36,4 +36,4 @@ client = ["tokio-util", "nym-task", "nym-metrics", "tokio/net", "tokio/rt"]
|
||||
[dev-dependencies]
|
||||
nym-crypto = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
tokio = { workspace = true, features = ["macros", "io-util", "rt", "rt-multi-thread", "test-util"] }
|
||||
tokio = { workspace = true, features = ["macros", "io-util", "rt", "rt-multi-thread"] }
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// Copyright 2021-2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::metrics::{MixnetMetric, Traced};
|
||||
use crate::trace::{TraceStage, Traced};
|
||||
use dashmap::DashMap;
|
||||
use futures::{Sink, SinkExt, StreamExt};
|
||||
use futures::{SinkExt, StreamExt};
|
||||
use nym_noise::config::NoiseConfig;
|
||||
use nym_noise::upgrade_noise_initiator;
|
||||
use nym_sphinx::forwarding::packet::MixPacket;
|
||||
@@ -11,7 +11,7 @@ use nym_sphinx::framing::codec::NymCodec;
|
||||
use nym_sphinx::framing::packet::FramedNymPacket;
|
||||
use std::io;
|
||||
use std::net::SocketAddr;
|
||||
use std::ops::{ControlFlow, Deref};
|
||||
use std::ops::Deref;
|
||||
use std::sync::atomic::{AtomicU32, AtomicUsize, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
@@ -19,7 +19,7 @@ use tokio::io::{AsyncRead, AsyncWrite};
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::sync::mpsc::error::TrySendError;
|
||||
use tokio::time::{sleep, Instant};
|
||||
use tokio::time::sleep;
|
||||
use tokio_stream::wrappers::ReceiverStream;
|
||||
use tokio_util::codec::Framed;
|
||||
use tracing::*;
|
||||
@@ -31,13 +31,6 @@ pub struct Config {
|
||||
pub initial_connection_timeout: Duration,
|
||||
pub maximum_connection_buffer_size: usize,
|
||||
pub use_legacy_packet_encoding: bool,
|
||||
/// Close an egress connection after this long with no packets sent (0 disables). The cache
|
||||
/// entry is evicted on close and the next packet to that peer transparently reconnects.
|
||||
pub connection_idle_timeout: Duration,
|
||||
/// Max time a single batch flush may block on the peer socket before we give up on it
|
||||
/// (0 disables). One timeout is treated as transient congestion - the batch is abandoned but
|
||||
/// the connection is retained (no re-handshake); only a few *consecutive* timeouts tear it down.
|
||||
pub connection_write_timeout: Duration,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
@@ -47,8 +40,6 @@ impl Config {
|
||||
initial_connection_timeout: Duration,
|
||||
maximum_connection_buffer_size: usize,
|
||||
use_legacy_packet_encoding: bool,
|
||||
connection_idle_timeout: Duration,
|
||||
connection_write_timeout: Duration,
|
||||
) -> Self {
|
||||
Config {
|
||||
initial_reconnection_backoff,
|
||||
@@ -56,8 +47,6 @@ impl Config {
|
||||
initial_connection_timeout,
|
||||
maximum_connection_buffer_size,
|
||||
use_legacy_packet_encoding,
|
||||
connection_idle_timeout,
|
||||
connection_write_timeout,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -125,8 +114,6 @@ struct ManagedConnection {
|
||||
noise_config: NoiseConfig,
|
||||
message_receiver: ReceiverStream<Traced<FramedNymPacket>>,
|
||||
connection_timeout: Duration,
|
||||
idle_timeout: Duration,
|
||||
write_timeout: Duration,
|
||||
current_reconnection: Arc<AtomicU32>,
|
||||
active_connections: ActiveConnections,
|
||||
handle_token: Arc<()>,
|
||||
@@ -156,14 +143,11 @@ impl Drop for EvictOnDrop {
|
||||
}
|
||||
|
||||
impl ManagedConnection {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn new(
|
||||
address: SocketAddr,
|
||||
noise_config: NoiseConfig,
|
||||
message_receiver: mpsc::Receiver<Traced<FramedNymPacket>>,
|
||||
connection_timeout: Duration,
|
||||
idle_timeout: Duration,
|
||||
write_timeout: Duration,
|
||||
current_reconnection: Arc<AtomicU32>,
|
||||
active_connections: ActiveConnections,
|
||||
handle_token: Arc<()>,
|
||||
@@ -173,8 +157,6 @@ impl ManagedConnection {
|
||||
noise_config,
|
||||
message_receiver: ReceiverStream::new(message_receiver),
|
||||
connection_timeout,
|
||||
idle_timeout,
|
||||
write_timeout,
|
||||
current_reconnection,
|
||||
active_connections,
|
||||
handle_token,
|
||||
@@ -183,8 +165,6 @@ impl ManagedConnection {
|
||||
|
||||
async fn run(self) {
|
||||
let address = self.address;
|
||||
let idle_timeout = self.idle_timeout;
|
||||
let write_timeout = self.write_timeout;
|
||||
let _evict_guard = EvictOnDrop {
|
||||
active_connections: self.active_connections,
|
||||
address,
|
||||
@@ -279,14 +259,7 @@ impl ManagedConnection {
|
||||
conn.set_backpressure_boundary(OUTBOUND_WRITE_BUFFER);
|
||||
|
||||
// 4. start handling the framed stream
|
||||
run_io_loop(
|
||||
conn,
|
||||
self.message_receiver,
|
||||
address,
|
||||
idle_timeout,
|
||||
write_timeout,
|
||||
)
|
||||
.await;
|
||||
run_io_loop(conn, self.message_receiver, address).await;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -301,124 +274,6 @@ const OUTBOUND_FLUSH_BATCH: usize = 1024;
|
||||
/// a flush is usually a single frame.
|
||||
const OUTBOUND_WRITE_BUFFER: usize = 32 * 1024;
|
||||
|
||||
/// Drive the read half solely to notice peer FIN/RST (the connection is send-only). Returns
|
||||
/// `Break` when the peer closed the connection or the read errored, `Continue` otherwise.
|
||||
fn handle_peer_read<P, E: std::fmt::Display>(
|
||||
msg: Option<Result<P, E>>,
|
||||
address: SocketAddr,
|
||||
) -> ControlFlow<()> {
|
||||
match msg {
|
||||
None => {
|
||||
debug!(
|
||||
peer = %address,
|
||||
exit_reason = "peer_closed",
|
||||
"peer closed mixnet connection to {address}"
|
||||
);
|
||||
ControlFlow::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}"
|
||||
);
|
||||
ControlFlow::Break(())
|
||||
}
|
||||
Some(Ok(_)) => {
|
||||
trace!(
|
||||
peer = %address,
|
||||
"unexpected inbound packet on mixnet connection to {address}; discarding"
|
||||
);
|
||||
ControlFlow::Continue(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Number of consecutive flush timeouts to the same peer we tolerate before dropping the
|
||||
/// connection. A single timeout is transient congestion (batch abandoned, connection retained to
|
||||
/// avoid a re-handshake); this many in a row means the peer is persistently unable to keep up, so
|
||||
/// we tear the connection down (it reconnects on the next packet).
|
||||
const MAX_CONSECUTIVE_WRITE_TIMEOUTS: u32 = 3;
|
||||
|
||||
/// Outcome of attempting to flush one batch to the peer.
|
||||
enum BatchOutcome {
|
||||
/// the batch was flushed to the socket
|
||||
Sent,
|
||||
/// the flush exceeded the write timeout (peer congested): the un-fed tail of the batch is
|
||||
/// dropped, but the already-encoded frames stay buffered for a later flush and the connection
|
||||
/// is left intact - the noise transport stays nonce-consistent across the cancelled flush, so
|
||||
/// resuming the write is sound
|
||||
WriteTimedOut,
|
||||
/// the sink errored: the connection is dead
|
||||
Failed,
|
||||
}
|
||||
|
||||
/// Feed a ready batch into the sink and flush it once (far fewer syscalls than per-packet), then
|
||||
/// stamp the egress latency stages: `EgressQueue` before each feed, then `SocketWrite` + the
|
||||
/// end-to-end total once the batch has hit the wire. The flush is bounded by `write_timeout`
|
||||
/// (0 disables) so a congested peer can't block this connection's egress queue into the
|
||||
/// multi-second range. The caller decides what a timeout means (see [`MAX_CONSECUTIVE_WRITE_TIMEOUTS`]).
|
||||
async fn forward_batch<S>(
|
||||
sink: &mut S,
|
||||
batch: Vec<Traced<FramedNymPacket>>,
|
||||
address: SocketAddr,
|
||||
write_timeout: Duration,
|
||||
) -> BatchOutcome
|
||||
where
|
||||
S: Sink<FramedNymPacket> + Unpin,
|
||||
S::Error: std::fmt::Display,
|
||||
{
|
||||
let mut traces = Vec::with_capacity(batch.len());
|
||||
let write = async {
|
||||
for mut traced in batch {
|
||||
// time spent waiting in this connection's egress buffer
|
||||
traced.record(MixnetMetric::EgressQueue);
|
||||
sink.feed(traced.inner).await?;
|
||||
traces.push(traced.trace);
|
||||
}
|
||||
sink.flush().await
|
||||
};
|
||||
|
||||
// bound how long we block on a slow/congested peer socket. On timeout the `write` future is
|
||||
// cancelled, which is safe: every already-encoded frame is buffered (nonce-consistent), so a
|
||||
// later flush resumes the byte stream in order.
|
||||
let write_result = if write_timeout.is_zero() {
|
||||
Ok(write.await)
|
||||
} else {
|
||||
tokio::time::timeout(write_timeout, write).await
|
||||
};
|
||||
|
||||
// socket-write time + end-to-end total for whatever was fed (on a timeout, those frames are
|
||||
// buffered and will hit the wire on a subsequent flush)
|
||||
for mut trace in traces {
|
||||
trace.record(MixnetMetric::SocketWrite);
|
||||
trace.record_total();
|
||||
}
|
||||
|
||||
match write_result {
|
||||
Ok(Ok(())) => BatchOutcome::Sent,
|
||||
Ok(Err(err)) => {
|
||||
debug!(
|
||||
event = "connection.forward_error",
|
||||
peer = %address,
|
||||
error = %err,
|
||||
exit_reason = "forward_error",
|
||||
"failed to forward packet batch to {address}: {err}"
|
||||
);
|
||||
BatchOutcome::Failed
|
||||
}
|
||||
Err(_elapsed) => BatchOutcome::WriteTimedOut,
|
||||
}
|
||||
}
|
||||
|
||||
/// Instant at which a connection idle since `last_activity` should be closed, or `None` if idle
|
||||
/// reaping is disabled (`timeout` is zero).
|
||||
fn idle_deadline(last_activity: Instant, timeout: Duration) -> Option<Instant> {
|
||||
(!timeout.is_zero()).then(|| last_activity + timeout)
|
||||
}
|
||||
|
||||
// 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.
|
||||
@@ -426,8 +281,6 @@ async fn run_io_loop<T>(
|
||||
conn: Framed<T, NymCodec>,
|
||||
receiver: ReceiverStream<Traced<FramedNymPacket>>,
|
||||
address: SocketAddr,
|
||||
idle_timeout: Duration,
|
||||
write_timeout: Duration,
|
||||
) where
|
||||
T: AsyncRead + AsyncWrite + Unpin,
|
||||
{
|
||||
@@ -437,73 +290,78 @@ async fn run_io_loop<T>(
|
||||
// which otherwise caps egress throughput and backs up the per-connection queue under load
|
||||
let mut receiver = receiver.ready_chunks(OUTBOUND_FLUSH_BATCH);
|
||||
|
||||
// reset by every batch we send; drives the idle-connection reaping below
|
||||
let mut last_send = tokio::time::Instant::now();
|
||||
// consecutive flush timeouts; a run of them (a persistently congested peer) drops the connection
|
||||
let mut consecutive_write_timeouts = 0u32;
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
msg = stream.next() => {
|
||||
if handle_peer_read(msg, address).is_break() {
|
||||
break;
|
||||
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() => {
|
||||
let Some(batch) = outgoing else {
|
||||
debug!(
|
||||
peer = %address,
|
||||
exit_reason = "sender_dropped",
|
||||
"connection manager to {address} finished"
|
||||
);
|
||||
break;
|
||||
};
|
||||
match forward_batch(&mut sink, batch, address, write_timeout).await {
|
||||
BatchOutcome::Sent => {
|
||||
consecutive_write_timeouts = 0;
|
||||
last_send = Instant::now();
|
||||
}
|
||||
BatchOutcome::WriteTimedOut => {
|
||||
consecutive_write_timeouts += 1;
|
||||
warn!(
|
||||
event = "connection.write_congested",
|
||||
match outgoing {
|
||||
None => {
|
||||
debug!(
|
||||
peer = %address,
|
||||
write_ms = write_timeout.as_millis() as u64,
|
||||
attempt = consecutive_write_timeouts,
|
||||
max_attempts = MAX_CONSECUTIVE_WRITE_TIMEOUTS,
|
||||
"egress flush to {address} timed out (peer congested); abandoned batch, retaining connection"
|
||||
exit_reason = "sender_dropped",
|
||||
"connection manager to {address} finished"
|
||||
);
|
||||
if consecutive_write_timeouts >= MAX_CONSECUTIVE_WRITE_TIMEOUTS {
|
||||
break;
|
||||
}
|
||||
Some(batch) => {
|
||||
// feed the whole ready batch, then flush once
|
||||
let mut traces = Vec::with_capacity(batch.len());
|
||||
let res = async {
|
||||
for mut traced in batch {
|
||||
// time spent waiting in this connection's egress buffer
|
||||
traced.record(TraceStage::EgressQueue);
|
||||
sink.feed(traced.inner).await?;
|
||||
traces.push(traced.trace);
|
||||
}
|
||||
sink.flush().await
|
||||
}
|
||||
.await;
|
||||
|
||||
// after the batch hit the wire: socket-write time and end-to-end total
|
||||
for mut trace in traces {
|
||||
trace.record(TraceStage::SocketWrite);
|
||||
trace.record_total();
|
||||
}
|
||||
if let Err(err) = res {
|
||||
debug!(
|
||||
event = "connection.forward_error",
|
||||
peer = %address,
|
||||
exit_reason = "write_timeout",
|
||||
"egress connection to {address} congested for {MAX_CONSECUTIVE_WRITE_TIMEOUTS} consecutive flushes; dropping it"
|
||||
error = %err,
|
||||
exit_reason = "forward_error",
|
||||
"failed to forward packet batch to {address}: {err}"
|
||||
);
|
||||
break;
|
||||
}
|
||||
// keep the connection: a single congestion spike shouldn't cost a
|
||||
// re-handshake. `last_send` is deliberately not bumped, so a peer that goes
|
||||
// congested-then-silent still idle-reaps on schedule.
|
||||
}
|
||||
BatchOutcome::Failed => break,
|
||||
}
|
||||
}
|
||||
// close the connection (freeing the task/socket) if we haven't sent anything for too
|
||||
// long; EvictOnDrop then clears the cache entry and the next packet reconnects
|
||||
_ = async {
|
||||
match idle_deadline(last_send, idle_timeout) {
|
||||
Some(d) => tokio::time::sleep_until(d).await,
|
||||
None => std::future::pending::<()>().await,
|
||||
}
|
||||
} => {
|
||||
debug!(
|
||||
peer = %address,
|
||||
exit_reason = "idle_timeout",
|
||||
idle_secs = idle_timeout.as_secs(),
|
||||
"closing idle egress mixnet connection to {address}"
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -572,10 +430,8 @@ impl Client {
|
||||
let reconnection_attempt = current_reconnection_attempt.load(Ordering::Acquire);
|
||||
let backoff = self.determine_backoff(reconnection_attempt);
|
||||
|
||||
// copy the values before moving into another task
|
||||
// copy the value before moving into another task
|
||||
let initial_connection_timeout = self.config.initial_connection_timeout;
|
||||
let connection_idle_timeout = self.config.connection_idle_timeout;
|
||||
let connection_write_timeout = self.config.connection_write_timeout;
|
||||
|
||||
let connections_count = self.connections_count.clone();
|
||||
let noise_config = self.noise_config.clone();
|
||||
@@ -593,8 +449,6 @@ impl Client {
|
||||
noise_config,
|
||||
receiver,
|
||||
initial_connection_timeout,
|
||||
connection_idle_timeout,
|
||||
connection_write_timeout,
|
||||
current_reconnection_attempt,
|
||||
active_connections,
|
||||
handle_token,
|
||||
@@ -611,9 +465,6 @@ impl SendWithoutResponse for Client {
|
||||
let address = packet.inner.next_hop_address();
|
||||
trace!("Sending packet to {address}");
|
||||
|
||||
// capture the sample state before the trace is moved into `queued`
|
||||
let sampled = packet.trace.is_sampled();
|
||||
|
||||
// TODO: optimisation for the future: rather than constantly using legacy encoding,
|
||||
// use the mix packet type / flags to pick encoding per packet
|
||||
let legacy = self.config.use_legacy_packet_encoding;
|
||||
@@ -638,11 +489,6 @@ impl SendWithoutResponse for Client {
|
||||
let channel_available = sender.channel.capacity();
|
||||
let channel_used = channel_capacity - channel_available;
|
||||
|
||||
// record how full this peer's egress buffer was (sampled packets only, to bound cost)
|
||||
if sampled {
|
||||
crate::metrics::observe_egress_buffer_fill(channel_used, channel_capacity);
|
||||
}
|
||||
|
||||
let sending_res = sender.channel.try_send(queued);
|
||||
drop(sender);
|
||||
|
||||
@@ -698,8 +544,6 @@ mod tests {
|
||||
initial_connection_timeout: Duration::from_millis(1_500),
|
||||
maximum_connection_buffer_size: 128,
|
||||
use_legacy_packet_encoding: false,
|
||||
connection_idle_timeout: Duration::from_secs(300),
|
||||
connection_write_timeout: Duration::from_millis(500),
|
||||
},
|
||||
NoiseConfig::new(
|
||||
Arc::new(x25519::KeyPair::new(&mut rng)),
|
||||
@@ -809,14 +653,7 @@ mod tests {
|
||||
let conn = Framed::new(a, NymCodec);
|
||||
let (_tx, rx) = mpsc::channel(1);
|
||||
|
||||
// idle reaping disabled so only the peer-close path is exercised
|
||||
let task = tokio::spawn(run_io_loop(
|
||||
conn,
|
||||
ReceiverStream::new(rx),
|
||||
test_addr(),
|
||||
Duration::ZERO,
|
||||
Duration::ZERO,
|
||||
));
|
||||
let task = tokio::spawn(run_io_loop(conn, ReceiverStream::new(rx), test_addr()));
|
||||
|
||||
// Simulate peer closing both directions of the connection.
|
||||
drop(b);
|
||||
@@ -833,13 +670,7 @@ mod tests {
|
||||
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(),
|
||||
Duration::ZERO,
|
||||
Duration::ZERO,
|
||||
));
|
||||
let task = tokio::spawn(run_io_loop(conn, ReceiverStream::new(rx), test_addr()));
|
||||
|
||||
drop(tx);
|
||||
|
||||
@@ -848,32 +679,4 @@ mod tests {
|
||||
.expect("io_loop must exit when the upstream sender is dropped")
|
||||
.expect("io_loop task must not panic");
|
||||
}
|
||||
|
||||
#[tokio::test(start_paused = true)]
|
||||
async fn io_loop_closes_idle_connection() {
|
||||
// With no packets sent and the peer still connected, the idle timeout must eventually
|
||||
// close the connection so the task/socket don't linger forever. The paused clock is
|
||||
// virtual - it auto-advances to the next timer, so this completes instantly despite the
|
||||
// durations below (no real waiting).
|
||||
let (a, _b) = tokio::io::duplex(64);
|
||||
let conn = Framed::new(a, NymCodec);
|
||||
// keep the sender alive so the sender-dropped path can't fire instead
|
||||
let (_tx, rx) = mpsc::channel(1);
|
||||
|
||||
let idle_timeout = Duration::from_millis(50);
|
||||
let task = tokio::spawn(run_io_loop(
|
||||
conn,
|
||||
ReceiverStream::new(rx),
|
||||
test_addr(),
|
||||
idle_timeout,
|
||||
Duration::ZERO,
|
||||
));
|
||||
|
||||
// auto-advance fires the nearest timer (the 50ms idle deadline, sooner than this 500ms
|
||||
// guard) once the task is otherwise idle, reaping the connection
|
||||
tokio::time::timeout(Duration::from_millis(500), task)
|
||||
.await
|
||||
.expect("io_loop must close the connection after the idle timeout")
|
||||
.expect("io_loop task must not panic");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::metrics::PacketTrace;
|
||||
use crate::trace::PacketTrace;
|
||||
use futures::channel::mpsc;
|
||||
use futures::channel::mpsc::SendError;
|
||||
use nym_sphinx::forwarding::packet::MixPacket;
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
#[cfg(feature = "client")]
|
||||
pub mod client;
|
||||
pub mod forwarder;
|
||||
pub mod metrics;
|
||||
pub mod trace;
|
||||
|
||||
#[cfg(feature = "client")]
|
||||
pub use client::{Client, Config, SendWithoutResponse};
|
||||
|
||||
+39
-125
@@ -4,33 +4,18 @@
|
||||
use strum::{AsRefStr, EnumIter, EnumProperty, IntoEnumIterator};
|
||||
use tokio::time::Instant;
|
||||
|
||||
/// Histogram buckets (seconds) for per-stage and total packet latency: exponential, ~100us .. ~6.5s.
|
||||
/// Shared by every latency stage so the waterfall is directly comparable; the top finite bucket is
|
||||
/// intentionally high so a rare multi-second processing spike is measured with magnitude rather than
|
||||
/// being clipped into the `+Inf` overflow.
|
||||
const STAGE_LATENCY_BUCKETS: [f64; 17] = [
|
||||
/// Histogram buckets (seconds) for per-stage and total packet latency: exponential,
|
||||
/// ~100us .. ~1.6s. Shared by every stage so the waterfall is directly comparable.
|
||||
const STAGE_LATENCY_BUCKETS: [f64; 14] = [
|
||||
0.0001, 0.0002, 0.0004, 0.0008, 0.0016, 0.0032, 0.0064, 0.0128, 0.0256, 0.0512, 0.1024, 0.2048,
|
||||
0.4096, 0.8192, 1.6384, 3.2768, 6.5536,
|
||||
0.4096, 0.8192,
|
||||
];
|
||||
|
||||
/// Count buckets (1 .. MAX_DRAIN_BATCH) for the forwarder drain-batch-size histogram.
|
||||
const DRAIN_BATCH_BUCKETS: [f64; 9] = [1.0, 2.0, 4.0, 8.0, 16.0, 32.0, 64.0, 128.0, 256.0];
|
||||
|
||||
/// Fill-ratio buckets (used/capacity) for the per-connection egress buffer. A ratio near 1.0 means
|
||||
/// the buffer is close to full and packets to that peer are about to be dropped.
|
||||
const EGRESS_FILL_BUCKETS: [f64; 9] = [0.05, 0.1, 0.25, 0.5, 0.75, 0.9, 0.95, 0.99, 1.0];
|
||||
|
||||
/// Every histogram this crate emits, defined in one place. `AsRefStr` (`#[strum(to_string=...)]`)
|
||||
/// gives the prometheus metric name - the bare `mixnet_packet_*` family, with no per-crate prefix
|
||||
/// since this is a shared library writing straight to the process-global registry. The `help` prop
|
||||
/// gives the description and [`MixnetMetric::buckets`] gives the bucket layout.
|
||||
///
|
||||
/// Register the whole family at boot with [`register_all`]. Latency-stage variants are observed via
|
||||
/// the [`PacketTrace`] stopwatch; the auxiliary variants via the `observe_*` helpers. (Passing an
|
||||
/// auxiliary variant to `PacketTrace::record` is meaningless but harmless.)
|
||||
/// A stage in the packet-forwarding pipeline, in order. Each maps to its own latency histogram
|
||||
/// (`AsRefStr` = metric name, `help` prop = description); `Total` is the end-to-end
|
||||
/// receive -> socket-write time. Defined here so call sites just name the stage.
|
||||
#[derive(Clone, Copy, EnumIter, AsRefStr, EnumProperty)]
|
||||
pub enum MixnetMetric {
|
||||
// ----- latency stages: the per-packet waterfall, recorded via `PacketTrace` -----
|
||||
pub enum TraceStage {
|
||||
/// receive -> sphinx unwrap (partial: shared secret + header MAC)
|
||||
#[strum(to_string = "mixnet_packet_stage_unwrap_seconds")]
|
||||
#[strum(props(help = "Seconds spent unwrapping a received sphinx packet"))]
|
||||
@@ -72,87 +57,31 @@ pub enum MixnetMetric {
|
||||
#[strum(to_string = "mixnet_packet_total_latency_seconds")]
|
||||
#[strum(props(help = "Total in-node latency of a forwarded packet, receive to socket write"))]
|
||||
Total,
|
||||
|
||||
// ----- auxiliary histograms: observed directly, not part of the latency waterfall -----
|
||||
/// number of packets the forwarder drained from the ingress channel per wakeup
|
||||
#[strum(to_string = "mixnet_packet_forwarder_drain_batch_size")]
|
||||
#[strum(props(
|
||||
help = "Number of ingress packets the forwarder drained per select! wakeup (batch size)"
|
||||
))]
|
||||
ForwarderDrainBatchSize,
|
||||
/// number of expired packets the forwarder drained from the delay queue per wakeup
|
||||
#[strum(to_string = "mixnet_packet_forwarder_delay_drain_batch_size")]
|
||||
#[strum(props(
|
||||
help = "Number of expired delay-queue packets the forwarder drained per select! wakeup (batch size)"
|
||||
))]
|
||||
ForwarderDelayDrainBatchSize,
|
||||
/// per-connection egress buffer occupancy (used/capacity) at send time
|
||||
#[strum(to_string = "mixnet_packet_egress_buffer_fill_ratio")]
|
||||
#[strum(props(
|
||||
help = "Per-connection egress buffer fill ratio (used/capacity) sampled at packet send time"
|
||||
))]
|
||||
EgressBufferFillRatio,
|
||||
}
|
||||
|
||||
impl MixnetMetric {
|
||||
/// Histogram bucket layout for this metric.
|
||||
fn buckets(&self) -> &'static [f64] {
|
||||
match self {
|
||||
MixnetMetric::ForwarderDrainBatchSize | MixnetMetric::ForwarderDelayDrainBatchSize => {
|
||||
&DRAIN_BATCH_BUCKETS
|
||||
}
|
||||
MixnetMetric::EgressBufferFillRatio => &EGRESS_FILL_BUCKETS,
|
||||
// every latency stage shares the seconds buckets
|
||||
_ => &STAGE_LATENCY_BUCKETS,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Pre-register every histogram (at zero) into the global metrics registry so the whole
|
||||
/// `mixnet_packet_*` family is present on the prometheus endpoint from boot, before anything has
|
||||
/// been observed. Idempotent.
|
||||
pub fn register_all() {
|
||||
/// Pre-register every stage histogram (at zero) into the global metrics registry so the whole
|
||||
/// `mixnet_packet_*` family is present on the prometheus endpoint from boot, before any sampled
|
||||
/// packet has been observed. Idempotent.
|
||||
pub fn register_stage_metrics() {
|
||||
let registry = nym_metrics::metrics_registry();
|
||||
for metric in MixnetMetric::iter() {
|
||||
for stage in TraceStage::iter() {
|
||||
registry.register_histogram(
|
||||
metric.as_ref(),
|
||||
metric.get_str("help"),
|
||||
Some(metric.buckets()),
|
||||
stage.as_ref(),
|
||||
stage.get_str("help"),
|
||||
Some(STAGE_LATENCY_BUCKETS.as_slice()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Observe a value into a metric's histogram in the process-global registry.
|
||||
fn observe(metric: MixnetMetric, value: f64) {
|
||||
/// Observe a stage latency into the process-global metrics registry. Explicit metric name (no
|
||||
/// per-crate prefix) so every stage lands in one uniform `mixnet_packet_*` family regardless of
|
||||
/// which crate records it.
|
||||
fn observe(stage: TraceStage, secs: f64) {
|
||||
nym_metrics::metrics_registry().maybe_register_and_add_to_histogram(
|
||||
metric.as_ref(),
|
||||
value,
|
||||
Some(metric.buckets()),
|
||||
metric.get_str("help"),
|
||||
);
|
||||
}
|
||||
|
||||
/// Observe how many ingress-channel packets the forwarder drained in a single wakeup.
|
||||
pub fn observe_drain_batch_size(batch_size: usize) {
|
||||
observe(MixnetMetric::ForwarderDrainBatchSize, batch_size as f64);
|
||||
}
|
||||
|
||||
/// Observe how many expired delay-queue packets the forwarder drained in a single wakeup.
|
||||
pub fn observe_delay_drain_batch_size(batch_size: usize) {
|
||||
observe(
|
||||
MixnetMetric::ForwarderDelayDrainBatchSize,
|
||||
batch_size as f64,
|
||||
);
|
||||
}
|
||||
|
||||
/// Observe how full a per-connection egress buffer was when a packet was queued for it.
|
||||
pub fn observe_egress_buffer_fill(used: usize, capacity: usize) {
|
||||
if capacity == 0 {
|
||||
return;
|
||||
}
|
||||
observe(
|
||||
MixnetMetric::EgressBufferFillRatio,
|
||||
used as f64 / capacity as f64,
|
||||
stage.as_ref(),
|
||||
secs,
|
||||
Some(STAGE_LATENCY_BUCKETS.as_slice()),
|
||||
stage.get_str("help"),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -182,11 +111,6 @@ impl PacketTrace {
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether this packet is being traced (sampled).
|
||||
pub fn is_sampled(&self) -> bool {
|
||||
matches!(self, PacketTrace::On { .. })
|
||||
}
|
||||
|
||||
/// Seconds spent in the stage just completed, advancing the cursor to now.
|
||||
/// Returns `None` for unsampled packets.
|
||||
fn lap(&mut self) -> Option<f64> {
|
||||
@@ -213,24 +137,24 @@ impl PacketTrace {
|
||||
|
||||
/// Close out the stage just completed: lap the timer and, only if the packet is sampled,
|
||||
/// observe `stage`'s latency histogram.
|
||||
pub fn record(&mut self, stage: MixnetMetric) {
|
||||
pub fn record(&mut self, stage: TraceStage) {
|
||||
if let Some(secs) = self.lap() {
|
||||
observe(stage, secs);
|
||||
}
|
||||
}
|
||||
|
||||
/// Observe the end-to-end [`MixnetMetric::Total`] latency (since receive) if sampled. Unlike
|
||||
/// Observe the end-to-end [`TraceStage::Total`] latency (since receive) if sampled. Unlike
|
||||
/// [`PacketTrace::record`] this does not lap, so it can be called at the very end.
|
||||
pub fn record_total(&self) {
|
||||
if let Some(secs) = self.total() {
|
||||
observe(MixnetMetric::Total, secs);
|
||||
observe(TraceStage::Total, secs);
|
||||
}
|
||||
}
|
||||
|
||||
/// Observe an explicit `secs` value for `stage` if the packet is sampled, without lapping the
|
||||
/// stage cursor. For diagnostics that don't fit the sequential waterfall (e.g. delay-queue
|
||||
/// overrun, measured against the target deadline rather than the previous stage).
|
||||
pub fn record_value(&self, stage: MixnetMetric, secs: f64) {
|
||||
pub fn record_value(&self, stage: TraceStage, secs: f64) {
|
||||
if matches!(self, PacketTrace::On { .. }) {
|
||||
observe(stage, secs);
|
||||
}
|
||||
@@ -259,12 +183,12 @@ impl<T> Traced<T> {
|
||||
}
|
||||
|
||||
/// Record the stage just completed for the carried trace (see [`PacketTrace::record`]).
|
||||
pub fn record(&mut self, stage: MixnetMetric) {
|
||||
pub fn record(&mut self, stage: TraceStage) {
|
||||
self.trace.record(stage)
|
||||
}
|
||||
|
||||
/// Observe an explicit value for the carried trace (see [`PacketTrace::record_value`]).
|
||||
pub fn record_value(&self, stage: MixnetMetric, secs: f64) {
|
||||
pub fn record_value(&self, stage: TraceStage, secs: f64) {
|
||||
self.trace.record_value(stage, secs)
|
||||
}
|
||||
}
|
||||
@@ -274,38 +198,28 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
// guards that AsRefStr honours `#[strum(to_string = ...)]` (rather than falling back to the
|
||||
// variant name), that every metric is in the `mixnet_packet_*` family, and carries a help
|
||||
// string, and that each metric resolves to a bucket layout.
|
||||
// variant name) and that every stage carries a help string.
|
||||
#[test]
|
||||
fn every_metric_has_a_mixnet_packet_name_help_and_buckets() {
|
||||
for metric in MixnetMetric::iter() {
|
||||
fn every_stage_has_a_mixnet_packet_name_and_help() {
|
||||
for stage in TraceStage::iter() {
|
||||
assert!(
|
||||
metric.as_ref().starts_with("mixnet_packet_"),
|
||||
stage.as_ref().starts_with("mixnet_packet_"),
|
||||
"unexpected metric name: {}",
|
||||
metric.as_ref()
|
||||
stage.as_ref()
|
||||
);
|
||||
assert!(
|
||||
metric.get_str("help").is_some(),
|
||||
stage.get_str("help").is_some(),
|
||||
"missing help for {}",
|
||||
metric.as_ref()
|
||||
);
|
||||
assert!(
|
||||
!metric.buckets().is_empty(),
|
||||
"missing buckets for {}",
|
||||
metric.as_ref()
|
||||
stage.as_ref()
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
MixnetMetric::Unwrap.as_ref(),
|
||||
TraceStage::Unwrap.as_ref(),
|
||||
"mixnet_packet_stage_unwrap_seconds"
|
||||
);
|
||||
assert_eq!(
|
||||
MixnetMetric::Total.as_ref(),
|
||||
TraceStage::Total.as_ref(),
|
||||
"mixnet_packet_total_latency_seconds"
|
||||
);
|
||||
assert_eq!(
|
||||
MixnetMetric::ForwarderDrainBatchSize.as_ref(),
|
||||
"mixnet_packet_forwarder_drain_batch_size"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,7 @@ use nym_api_requests::models::network_monitor::{
|
||||
};
|
||||
use nym_api_requests::models::node_families::NodeFamily;
|
||||
use nym_api_requests::models::{
|
||||
AnnotationResponseV1, AnnotationResponseV2, ApiHealthResponse, BinaryBuildInformationOwned,
|
||||
AnnotationResponseV1, ApiHealthResponse, BinaryBuildInformationOwned,
|
||||
ChainBlocksStatusResponse, ChainStatusResponse, KeyRotationInfoResponse,
|
||||
NodePerformanceResponse, NodeRefreshBody, NymNodeDescriptionV1, NymNodeDescriptionV2,
|
||||
PerformanceHistoryResponse, RewardedSetResponse, SignerInformationResponse,
|
||||
@@ -1033,22 +1033,6 @@ pub trait NymApiClientExt: ApiClient {
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_node_annotation_v2(
|
||||
&self,
|
||||
node_id: NodeId,
|
||||
) -> Result<AnnotationResponseV2, NymAPIError> {
|
||||
self.get_json(
|
||||
&[
|
||||
routes::V2_API_VERSION,
|
||||
routes::NYM_NODES_ROUTES,
|
||||
routes::NYM_NODES_ANNOTATION,
|
||||
&node_id.to_string(),
|
||||
],
|
||||
NO_PARAMS,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[deprecated]
|
||||
async fn get_mixnode_avg_uptime(&self, mix_id: NodeId) -> Result<UptimeResponse, NymAPIError> {
|
||||
self.get_json(
|
||||
|
||||
+2
-8
@@ -7,12 +7,12 @@ use crate::nyxd::error::NyxdError;
|
||||
use crate::nyxd::CosmWasmClient;
|
||||
use async_trait::async_trait;
|
||||
use cosmrs::AccountId;
|
||||
use nym_mixnet_contract_common::NodeId;
|
||||
use serde::Deserialize;
|
||||
|
||||
use nym_mixnet_contract_common::NodeId;
|
||||
pub use nym_node_families_contract_common::{
|
||||
msg::QueryMsg as NodeFamiliesQueryMsg, AllFamilyMembersPagedResponse,
|
||||
AllPastFamilyInvitationsPagedResponse, Config, FamiliesPagedResponse, FamilyMemberRecord,
|
||||
AllPastFamilyInvitationsPagedResponse, FamiliesPagedResponse, FamilyMemberRecord,
|
||||
FamilyMembersPagedResponse, GlobalPastFamilyInvitationCursor, NodeFamily,
|
||||
NodeFamilyByNameResponse, NodeFamilyByOwnerResponse, NodeFamilyId,
|
||||
NodeFamilyMembershipResponse, NodeFamilyResponse, PastFamilyInvitation,
|
||||
@@ -35,11 +35,6 @@ pub trait NodeFamiliesQueryClient {
|
||||
where
|
||||
for<'a> T: Deserialize<'a>;
|
||||
|
||||
async fn get_config(&self) -> Result<Config, NyxdError> {
|
||||
self.query_node_families_contract(NodeFamiliesQueryMsg::GetConfig {})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_family_by_id(
|
||||
&self,
|
||||
family_id: NodeFamilyId,
|
||||
@@ -365,7 +360,6 @@ mod tests {
|
||||
msg: NodeFamiliesQueryMsg,
|
||||
) {
|
||||
match msg {
|
||||
NodeFamiliesQueryMsg::GetConfig {} => client.get_config().ignore(),
|
||||
NodeFamiliesQueryMsg::GetFamilyById { family_id } => {
|
||||
client.get_family_by_id(family_id).ignore()
|
||||
}
|
||||
|
||||
@@ -96,10 +96,6 @@ pub enum ExecuteMsg {
|
||||
#[cw_serde]
|
||||
#[cfg_attr(feature = "schema", derive(cosmwasm_schema::QueryResponses))]
|
||||
pub enum QueryMsg {
|
||||
/// Retrieve current contract configuration values
|
||||
#[cfg_attr(feature = "schema", returns(Config))]
|
||||
GetConfig {},
|
||||
|
||||
/// Look up a single family by its id.
|
||||
#[cfg_attr(feature = "schema", returns(NodeFamilyResponse))]
|
||||
GetFamilyById { family_id: NodeFamilyId },
|
||||
|
||||
@@ -73,27 +73,6 @@ impl<T> NonExhaustiveDelayQueue<T> {
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.inner.is_empty()
|
||||
}
|
||||
|
||||
/// Pop the next *already-expired* item without awaiting, or `None` if nothing is ready right
|
||||
/// now (the queue is empty, or its earliest item has not reached its deadline yet). Lets a
|
||||
/// caller drain a burst of simultaneously-expired items in a tight loop without yielding.
|
||||
///
|
||||
/// It polls the inner queue with a **no-op waker**, so a not-yet-due (`None`) result registers
|
||||
/// no real wakeup. This is therefore sound ONLY when the caller subsequently polls the
|
||||
/// [`Stream`] impl (`.next().await`) before parking the task - that re-arms the timer against
|
||||
/// the task's real waker, superseding the no-op one. The intended use is "drain the extra ready
|
||||
/// items right after `.next()` yielded one, in a loop that returns to `.next().await`". Calling
|
||||
/// it as the last thing before suspending would drop the wakeup (same caveat as
|
||||
/// `futures::FutureExt::now_or_never`).
|
||||
pub fn try_next_expired(&mut self) -> Option<Expired<T>> {
|
||||
let mut cx = Context::from_waker(Waker::noop());
|
||||
match Pin::new(&mut self.inner).poll_expired(&mut cx) {
|
||||
// a ready-expired item, or `None` because the queue is empty
|
||||
Poll::Ready(maybe_item) => maybe_item,
|
||||
// queue is non-empty but nothing is due yet
|
||||
Poll::Pending => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Default for NonExhaustiveDelayQueue<T> {
|
||||
|
||||
@@ -10,7 +10,6 @@ use crate::{Any, MessageRegistry, ParsedTransactionDetails, default_message_regi
|
||||
use futures::StreamExt;
|
||||
use futures::future::join3;
|
||||
use std::collections::BTreeMap;
|
||||
use std::future::Future;
|
||||
use std::sync::Arc;
|
||||
use tendermint::{Block, Hash};
|
||||
use tendermint_rpc::endpoint::{block, block_results, tx, validators};
|
||||
@@ -19,38 +18,6 @@ use tokio::sync::Mutex;
|
||||
use tracing::{debug, instrument, warn};
|
||||
use url::Url;
|
||||
|
||||
const MAX_QUERY_ATTEMPTS: usize = 3;
|
||||
|
||||
/// Runs `op` up to `max_attempts` times (at least once), returning the first success or, on full
|
||||
/// exhaustion, the last error encountered.
|
||||
async fn query_with_retries<F, Fut, T>(mut max_attempts: usize, op: F) -> Result<T, ScraperError>
|
||||
where
|
||||
F: Fn() -> Fut,
|
||||
Fut: Future<Output = Result<T, ScraperError>>,
|
||||
{
|
||||
if max_attempts == 0 {
|
||||
max_attempts = 1;
|
||||
}
|
||||
|
||||
let mut last_err = None;
|
||||
|
||||
for i in 0..max_attempts {
|
||||
match op().await {
|
||||
Ok(result) => return Ok(result),
|
||||
Err(err) => {
|
||||
debug!("query failed, retrying {}/{max_attempts} - {err}", i + 1);
|
||||
last_err = Some(err);
|
||||
}
|
||||
}
|
||||
|
||||
tokio::time::sleep(std::time::Duration::from_millis(300 * (i as u64 + 1))).await;
|
||||
}
|
||||
|
||||
// SAFETY: max_attempts >= 1, so we only reach here after at least one recorded failure
|
||||
#[allow(clippy::unwrap_used)]
|
||||
Err(last_err.unwrap())
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct RetrievalConfig {
|
||||
pub get_validators: bool,
|
||||
@@ -206,24 +173,13 @@ impl RpcClient {
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument(skip(self), err(Display))]
|
||||
async fn get_block_results_with_retries(
|
||||
&self,
|
||||
height: u32,
|
||||
max_attempts: usize,
|
||||
) -> Result<block_results::Response, ScraperError> {
|
||||
query_with_retries(max_attempts, || self.get_block_results(height)).await
|
||||
}
|
||||
|
||||
async fn maybe_get_block_results(
|
||||
&self,
|
||||
height: u32,
|
||||
retrieve: bool,
|
||||
) -> Result<Option<block_results::Response>, ScraperError> {
|
||||
if retrieve {
|
||||
self.get_block_results_with_retries(height, MAX_QUERY_ATTEMPTS)
|
||||
.await
|
||||
.map(Some)
|
||||
self.get_block_results(height).await.map(Some)
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
@@ -263,6 +219,8 @@ impl RpcClient {
|
||||
|
||||
// "Data is just a wrapper for a list of transactions, where transactions are arbitrary byte arrays"
|
||||
// source: https://github.com/tendermint/spec/blob/d46cd7f573a2c6a2399fcab2cde981330aa63f37/spec/core/data_structures.md#data
|
||||
//
|
||||
// I hate that zip as much as you, dear reader, but for some reason the compiler didn't let me remove the `move`
|
||||
futures::stream::iter(
|
||||
raw.iter()
|
||||
.map(tx_hash)
|
||||
@@ -270,14 +228,12 @@ impl RpcClient {
|
||||
.zip(std::iter::repeat(ordered_results.clone())),
|
||||
)
|
||||
.for_each_concurrent(4, |((id, tx_hash), ordered_results)| async move {
|
||||
let res = self
|
||||
.get_transaction_result_with_retries(tx_hash, MAX_QUERY_ATTEMPTS)
|
||||
.await;
|
||||
let res = self.get_transaction_result(tx_hash).await;
|
||||
ordered_results.lock().await.insert(id, res);
|
||||
})
|
||||
.await;
|
||||
|
||||
// safety: the futures have completed so we MUST have the only arc reference
|
||||
// safety the futures have completed so we MUST have the only arc reference
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let inner = Arc::into_inner(ordered_results).unwrap().into_inner();
|
||||
|
||||
@@ -310,15 +266,6 @@ impl RpcClient {
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument(skip(self, tx_hash), fields(tx_hash = %tx_hash), err(Display))]
|
||||
async fn get_transaction_result_with_retries(
|
||||
&self,
|
||||
tx_hash: Hash,
|
||||
max_attempts: usize,
|
||||
) -> Result<tx::Response, ScraperError> {
|
||||
query_with_retries(max_attempts, || self.get_transaction_result(tx_hash)).await
|
||||
}
|
||||
|
||||
#[instrument(skip(self))]
|
||||
pub async fn get_validators_details(
|
||||
&self,
|
||||
@@ -335,24 +282,13 @@ impl RpcClient {
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument(skip(self), err(Display))]
|
||||
async fn get_validators_details_with_retries(
|
||||
&self,
|
||||
height: u32,
|
||||
max_attempts: usize,
|
||||
) -> Result<validators::Response, ScraperError> {
|
||||
query_with_retries(max_attempts, || self.get_validators_details(height)).await
|
||||
}
|
||||
|
||||
async fn maybe_get_validators_details(
|
||||
&self,
|
||||
height: u32,
|
||||
retrieve: bool,
|
||||
) -> Result<Option<validators::Response>, ScraperError> {
|
||||
if retrieve {
|
||||
self.get_validators_details_with_retries(height, MAX_QUERY_ATTEMPTS)
|
||||
.await
|
||||
.map(Some)
|
||||
self.get_validators_details(height).await.map(Some)
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ use tracing::{debug, trace, warn};
|
||||
pub use crate::node::{EntryDetails, RoutingNode, SupportedRoles};
|
||||
pub use error::NymTopologyError;
|
||||
pub use nym_mixnet_contract_common::nym_node::Role;
|
||||
pub use nym_mixnet_contract_common::{EpochRewardedSet, NodeId, RewardedSet};
|
||||
pub use nym_mixnet_contract_common::{EpochRewardedSet, NodeId};
|
||||
pub use rewarded_set::CachedEpochRewardedSet;
|
||||
|
||||
pub mod error;
|
||||
|
||||
@@ -8,7 +8,6 @@ use nym_mixnet_contract_common::NodeId;
|
||||
use nym_sphinx_addressing::nodes::NymNodeRoutingAddress;
|
||||
use nym_sphinx_types::Node as SphinxNode;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::Debug;
|
||||
use std::net::{IpAddr, SocketAddr};
|
||||
use thiserror::Error;
|
||||
|
||||
@@ -46,7 +45,7 @@ impl From<DeclaredRolesV1> for SupportedRoles {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct RoutingNode {
|
||||
pub node_id: NodeId,
|
||||
|
||||
@@ -59,19 +58,6 @@ pub struct RoutingNode {
|
||||
pub supported_roles: SupportedRoles,
|
||||
}
|
||||
|
||||
impl Debug for RoutingNode {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("RoutingNode")
|
||||
.field("node_id", &self.node_id)
|
||||
.field("mix_host", &self.mix_host)
|
||||
.field("entry", &self.entry)
|
||||
.field("identity_key", &self.identity_key.to_base58_string())
|
||||
.field("sphinx_key", &self.sphinx_key.to_base58_string())
|
||||
.field("supported_roles", &self.supported_roles)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl RoutingNode {
|
||||
pub fn ws_entry_address_tls(&self) -> Option<String> {
|
||||
let entry = self.entry.as_ref()?;
|
||||
|
||||
Generated
+2
-1
@@ -1037,7 +1037,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "node-families"
|
||||
version = "0.1.1"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"cosmwasm-schema",
|
||||
@@ -1052,6 +1052,7 @@ dependencies = [
|
||||
"nym-mixnet-contract",
|
||||
"nym-mixnet-contract-common",
|
||||
"nym-node-families-contract-common",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -87,7 +87,7 @@ cw3-flex-multisig = { version = "2.0.0", path = "multisig/cw3-flex-multisig" }
|
||||
cw4-group = { version = "2.0.0", path = "multisig/cw4-group" }
|
||||
nym-mixnet-contract = { version = "1.5.1", path = "mixnet" }
|
||||
nym-vesting-contract = { version = "1.4.1", path = "vesting" }
|
||||
node-families = { version = "0.1.1", path = "node-families" }
|
||||
node-families = { version = "0.1.0", path = "node-families" }
|
||||
|
||||
[workspace.lints.clippy]
|
||||
unwrap_used = "deny"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "node-families"
|
||||
description = "Nym Node Families contract"
|
||||
version = "0.1.1"
|
||||
version = "0.1.0"
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
@@ -25,6 +25,7 @@ cosmwasm-std = { workspace = true }
|
||||
cw2 = { workspace = true }
|
||||
cw-storage-plus = { workspace = true }
|
||||
cw-controllers = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
cosmwasm-schema = { workspace = true, optional = true }
|
||||
cw-utils = { workspace = true }
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
use crate::queries::{
|
||||
query_all_family_members_paged, query_all_past_invitations_paged,
|
||||
query_all_pending_invitations_paged, query_config, query_families_paged, query_family_by_id,
|
||||
query_all_pending_invitations_paged, query_families_paged, query_family_by_id,
|
||||
query_family_by_name, query_family_by_owner, query_family_members_paged,
|
||||
query_family_membership, query_past_invitations_for_family_paged,
|
||||
query_past_invitations_for_node_paged, query_past_members_for_family_paged,
|
||||
@@ -100,7 +100,6 @@ pub fn execute(
|
||||
#[entry_point]
|
||||
pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> Result<Binary, NodeFamiliesContractError> {
|
||||
match msg {
|
||||
QueryMsg::GetConfig {} => Ok(to_json_binary(&query_config(deps)?)?),
|
||||
QueryMsg::GetFamilyById { family_id } => {
|
||||
Ok(to_json_binary(&query_family_by_id(deps, family_id)?)?)
|
||||
}
|
||||
|
||||
@@ -7,23 +7,18 @@ use cosmwasm_std::{Deps, Env, Order, StdResult};
|
||||
use cw_storage_plus::Bound;
|
||||
use nym_mixnet_contract_common::NodeId;
|
||||
use nym_node_families_contract_common::{
|
||||
AllFamilyMembersPagedResponse, AllPastFamilyInvitationsPagedResponse, Config,
|
||||
FamiliesPagedResponse, FamilyMemberRecord, FamilyMembersPagedResponse,
|
||||
GlobalPastFamilyInvitationCursor, NodeFamiliesContractError, NodeFamilyByNameResponse,
|
||||
NodeFamilyByOwnerResponse, NodeFamilyId, NodeFamilyMembershipResponse, NodeFamilyResponse,
|
||||
PastFamilyInvitationCursor, PastFamilyInvitationForNodeCursor,
|
||||
PastFamilyInvitationsForNodePagedResponse, PastFamilyInvitationsPagedResponse,
|
||||
PastFamilyMemberCursor, PastFamilyMemberForNodeCursor, PastFamilyMembersForNodePagedResponse,
|
||||
PastFamilyMembersPagedResponse, PendingFamilyInvitationDetails,
|
||||
PendingFamilyInvitationResponse, PendingFamilyInvitationsPagedResponse,
|
||||
PendingInvitationsForNodePagedResponse, PendingInvitationsPagedResponse,
|
||||
AllFamilyMembersPagedResponse, AllPastFamilyInvitationsPagedResponse, FamiliesPagedResponse,
|
||||
FamilyMemberRecord, FamilyMembersPagedResponse, GlobalPastFamilyInvitationCursor,
|
||||
NodeFamiliesContractError, NodeFamilyByNameResponse, NodeFamilyByOwnerResponse, NodeFamilyId,
|
||||
NodeFamilyMembershipResponse, NodeFamilyResponse, PastFamilyInvitationCursor,
|
||||
PastFamilyInvitationForNodeCursor, PastFamilyInvitationsForNodePagedResponse,
|
||||
PastFamilyInvitationsPagedResponse, PastFamilyMemberCursor, PastFamilyMemberForNodeCursor,
|
||||
PastFamilyMembersForNodePagedResponse, PastFamilyMembersPagedResponse,
|
||||
PendingFamilyInvitationDetails, PendingFamilyInvitationResponse,
|
||||
PendingFamilyInvitationsPagedResponse, PendingInvitationsForNodePagedResponse,
|
||||
PendingInvitationsPagedResponse,
|
||||
};
|
||||
|
||||
/// Retrieve current contract configuration values
|
||||
pub fn query_config(deps: Deps) -> Result<Config, NodeFamiliesContractError> {
|
||||
Ok(NodeFamiliesStorage::new().config.load(deps.storage)?)
|
||||
}
|
||||
|
||||
/// Resolve a single family by its id. Returns `family: None` if no family
|
||||
/// with that id exists.
|
||||
pub fn query_family_by_id(
|
||||
|
||||
@@ -0,0 +1,244 @@
|
||||
// ENS-over-the-mixnet demo, ported from wasm/ens-demo. Resolve <name>.eth to an
|
||||
// address + contenthash, then fetch the IPFS site, every byte through mixFetch.
|
||||
// The tunnel lifecycle + options live in <MixTunnelSetup>; this component owns
|
||||
// the ENS flow and receives a `mixFetch` when the tunnel is ready.
|
||||
|
||||
import React, { useRef, useState } from 'react';
|
||||
import type { JsonRpcProvider } from 'ethers';
|
||||
import { MixTunnelSetup, type MixFetchFn } from '../shared/mixTunnel';
|
||||
import { Button, LogPanel, useLogs, box, row, input, sub, legend } from '../shared/ui';
|
||||
import { buildProvider, callMixFetch, decompressBody, expandGatewayUrl, formatSize, htmlFingerprint, renderFingerprint } from './lib';
|
||||
|
||||
const NAME_PRESETS = ['vitalik.eth', 'ens.eth', 'gregskril.eth', 'raffy.eth', 'luc.eth'];
|
||||
const RPC_PRESETS = ['https://ethereum-rpc.publicnode.com', 'https://rpc.ankr.com/eth', 'https://eth.public-rpc.com'];
|
||||
const GATEWAY_PRESETS = ['https://{cid}.ipfs.dweb.link/', 'https://dweb.link/ipfs/{cid}/'];
|
||||
|
||||
const IP_ECHO_URL = 'https://ipinfo.io/ip';
|
||||
const IP_SHAPE_RE = /^[\d.:a-f]{3,45}$/i;
|
||||
|
||||
const preStyle: React.CSSProperties = {
|
||||
maxHeight: 240,
|
||||
overflowY: 'auto',
|
||||
fontSize: 12.5,
|
||||
whiteSpace: 'pre-wrap',
|
||||
overflowWrap: 'anywhere',
|
||||
background: 'rgba(127,127,127,0.06)',
|
||||
border: '1px solid rgba(127,127,127,0.2)',
|
||||
borderRadius: 6,
|
||||
padding: '0.5rem',
|
||||
margin: '0.5rem 0 0',
|
||||
};
|
||||
|
||||
export function EnsDemo() {
|
||||
const { log, lines } = useLogs();
|
||||
const [mixFetch, setMixFetch] = useState<MixFetchFn | null>(null);
|
||||
const providerRef = useRef<JsonRpcProvider | null>(null);
|
||||
|
||||
const [ensName, setEnsName] = useState('vitalik.eth');
|
||||
const [ensRpc, setEnsRpc] = useState(RPC_PRESETS[0]);
|
||||
const [gateway, setGateway] = useState(GATEWAY_PRESETS[0]);
|
||||
const [customCid, setCustomCid] = useState('');
|
||||
const [lastCid, setLastCid] = useState<string | null>(null);
|
||||
const [preview, setPreview] = useState<string | null>(null);
|
||||
const [verifyLink, setVerifyLink] = useState<string | null>(null);
|
||||
const [busy, setBusy] = useState(false);
|
||||
|
||||
const connected = mixFetch != null;
|
||||
const ensLog = (msg: string, colour?: 'green' | 'red' | 'orange' | 'gray') => log('ens', msg, colour);
|
||||
|
||||
function ensureProvider(): JsonRpcProvider {
|
||||
if (providerRef.current) return providerRef.current;
|
||||
const rpc = ensRpc.trim();
|
||||
if (!rpc) throw new Error('RPC URL is required');
|
||||
ensLog(`building JsonRpcProvider({ rpc: ${rpc}, transport: mixFetch })`);
|
||||
providerRef.current = buildProvider(rpc, mixFetch!, ensLog);
|
||||
return providerRef.current;
|
||||
}
|
||||
|
||||
function onReady(fn: MixFetchFn) {
|
||||
setMixFetch(() => fn);
|
||||
}
|
||||
function onDisconnect() {
|
||||
setMixFetch(null);
|
||||
providerRef.current = null;
|
||||
setLastCid(null);
|
||||
}
|
||||
|
||||
async function resolveAddress() {
|
||||
const name = ensName.trim();
|
||||
if (!name) return ensLog('Name is required', 'red');
|
||||
let provider: JsonRpcProvider;
|
||||
try {
|
||||
provider = ensureProvider();
|
||||
} catch (e) {
|
||||
return ensLog(`${e}`, 'red');
|
||||
}
|
||||
ensLog(`step 1/3: resolving ${name} via ENS Registry + Resolver`);
|
||||
const t0 = performance.now();
|
||||
try {
|
||||
const addr = await provider.resolveName(name);
|
||||
const ms = (performance.now() - t0).toFixed(0);
|
||||
if (addr) ensLog(`${name} -> ${addr} (${ms} ms total)`, 'green');
|
||||
else ensLog(`${name} has no addr record (${ms} ms)`, 'orange');
|
||||
} catch (e: any) {
|
||||
ensLog(`resolveName failed: ${e.shortMessage || e.message || e}`, 'red');
|
||||
}
|
||||
}
|
||||
|
||||
async function getContenthash() {
|
||||
const name = ensName.trim();
|
||||
if (!name) return ensLog('Name is required', 'red');
|
||||
let provider: JsonRpcProvider;
|
||||
try {
|
||||
provider = ensureProvider();
|
||||
} catch (e) {
|
||||
return ensLog(`${e}`, 'red');
|
||||
}
|
||||
ensLog(`step 2/3: reading contenthash record from ${name}'s resolver`);
|
||||
const t0 = performance.now();
|
||||
try {
|
||||
const resolver = await provider.getResolver(name);
|
||||
if (!resolver) return ensLog(`${name} has no resolver`, 'orange');
|
||||
const content = await resolver.getContentHash();
|
||||
const ms = (performance.now() - t0).toFixed(0);
|
||||
if (!content) return ensLog(`${name} has no contenthash (${ms} ms)`, 'orange');
|
||||
ensLog(`contenthash: ${content} (${ms} ms total)`, 'green');
|
||||
const ipfsMatch = content.match(/^ipfs:\/\/(.+)$/);
|
||||
if (ipfsMatch) {
|
||||
setLastCid(ipfsMatch[1]);
|
||||
ensLog(`decoded CID: ${ipfsMatch[1]}`);
|
||||
} else {
|
||||
ensLog('non-IPFS scheme in contenthash; nothing to fetch', 'orange');
|
||||
}
|
||||
} catch (e: any) {
|
||||
ensLog(`contenthash lookup failed: ${e.shortMessage || e.message || e}`, 'red');
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchIpfsCid(cid: string, label: string) {
|
||||
if (!cid) return ensLog(`${label}: CID is required`, 'red');
|
||||
const gw = gateway.trim();
|
||||
if (!gw) return ensLog('IPFS gateway is required', 'red');
|
||||
const url = expandGatewayUrl(gw, cid);
|
||||
ensLog(`${label} GET ${url}`);
|
||||
const t0 = performance.now();
|
||||
try {
|
||||
const raw = await callMixFetch(mixFetch!, url, {});
|
||||
const buf = await decompressBody(raw.body, raw.headers);
|
||||
const ms = (performance.now() - t0).toFixed(0);
|
||||
const ctype = raw.headers['content-type'] || '';
|
||||
const wireSize = raw.body ? raw.body.byteLength : 0;
|
||||
const wireNote = wireSize !== buf.byteLength ? ` (${formatSize(wireSize)} wire, decompressed)` : '';
|
||||
ensLog(`${raw.status} ${raw.statusText}: ${formatSize(buf.byteLength)} ${ctype}${wireNote} in ${ms} ms`, 'green');
|
||||
|
||||
const text = new TextDecoder('utf-8', { fatal: false }).decode(buf);
|
||||
const looksLikeHtml = ctype.includes('html') || /<html\b|<!doctype html/i.test(text.slice(0, 1000));
|
||||
if (looksLikeHtml) {
|
||||
const fp = htmlFingerprint(text);
|
||||
if (fp.title) ensLog(`page title: "${fp.title}"`, 'green');
|
||||
setPreview(renderFingerprint(fp, buf.byteLength));
|
||||
} else if (ctype.includes('json')) {
|
||||
try {
|
||||
setPreview(JSON.stringify(JSON.parse(text), null, 2));
|
||||
} catch {
|
||||
setPreview(text);
|
||||
}
|
||||
} else if (ctype.includes('text/')) {
|
||||
setPreview(text);
|
||||
} else {
|
||||
setPreview(`[binary content, ${formatSize(buf.byteLength)}, ${ctype || 'unknown type'}]`);
|
||||
}
|
||||
setVerifyLink(`https://ipfs.io/ipfs/${cid}/`);
|
||||
ensLog('Visual content check (open the link below in another tab), not CID-hash verification.', 'gray');
|
||||
} catch (e: any) {
|
||||
ensLog(`${label} fetch failed: ${e.message || e}`, 'red');
|
||||
ensLog("If this is a 403/429 or connection error, the exit IP may be rate-limited. Tick 'Use random IPR' and reload.", 'orange');
|
||||
}
|
||||
}
|
||||
|
||||
async function verifyIp() {
|
||||
if (!mixFetch) return;
|
||||
setBusy(true);
|
||||
ensLog('comparing direct-clearnet IP vs Nym-exit IP...');
|
||||
let directIp: string;
|
||||
try {
|
||||
const text = (await (await fetch(IP_ECHO_URL)).text()).trim();
|
||||
directIp = IP_SHAPE_RE.test(text) ? text : `(unexpected: ${text})`;
|
||||
} catch (e: any) {
|
||||
directIp = `error: ${e.message || e}`;
|
||||
}
|
||||
ensLog(` your real IP (direct fetch, no Nym): ${directIp}`, 'orange');
|
||||
let nymIp: string;
|
||||
try {
|
||||
const raw = await callMixFetch(mixFetch, IP_ECHO_URL, {});
|
||||
const text = new TextDecoder().decode(raw.body).trim();
|
||||
nymIp = IP_SHAPE_RE.test(text) ? text : `(unexpected: ${text})`;
|
||||
} catch (e: any) {
|
||||
nymIp = `error: ${e.message || e}`;
|
||||
}
|
||||
ensLog(` what the upstream sees via mixFetch -> Nym: ${nymIp}`, 'green');
|
||||
if (!nymIp.startsWith('error') && !directIp.startsWith('error') && nymIp !== directIp) {
|
||||
ensLog('IPs differ. The RPC and gateway see the Nym exit, not you. Every ENS step uses the same path.', 'green');
|
||||
} else if (nymIp.startsWith('error') || directIp.startsWith('error')) {
|
||||
ensLog('Could not complete the comparison. Try again, or reconnect with a different IPR.', 'red');
|
||||
} else {
|
||||
ensLog('IPs match. The mixnet route may not be active, or the IP service is behind a shared CDN. Try again.', 'red');
|
||||
}
|
||||
setBusy(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ margin: '1.5rem 0' }}>
|
||||
<MixTunnelSetup onReady={onReady} onDisconnect={onDisconnect} clientIdPrefix="ens-demo" />
|
||||
|
||||
<div style={box}>
|
||||
<div style={legend}>ENS lookup</div>
|
||||
<div style={row}>
|
||||
<Button onClick={verifyIp} disabled={!connected || busy}>Verify IP routing</Button>
|
||||
<span style={sub}>Confirms traffic exits through Nym. The comparison makes one direct (clearnet) call to ipinfo.io, so you will see a single ipinfo.io row in the Network tab.</span>
|
||||
</div>
|
||||
|
||||
<div style={row}>
|
||||
<label style={sub}>Name</label>
|
||||
<select style={input} value={ensName} onChange={(e) => setEnsName(e.target.value)} disabled={!connected}>
|
||||
{NAME_PRESETS.map((n) => <option key={n} value={n}>{n}</option>)}
|
||||
</select>
|
||||
<input style={input} value={ensName} onChange={(e) => setEnsName(e.target.value)} disabled={!connected} />
|
||||
</div>
|
||||
<div style={row}>
|
||||
<label style={sub}>RPC</label>
|
||||
<select style={input} value={RPC_PRESETS.includes(ensRpc) ? ensRpc : ''} onChange={(e) => { setEnsRpc(e.target.value); providerRef.current = null; }} disabled={!connected}>
|
||||
{RPC_PRESETS.map((u) => <option key={u} value={u}>{u}</option>)}
|
||||
</select>
|
||||
<input style={input} value={ensRpc} onChange={(e) => { setEnsRpc(e.target.value); providerRef.current = null; }} disabled={!connected} />
|
||||
</div>
|
||||
<div style={row}>
|
||||
<label style={sub}>IPFS gateway</label>
|
||||
<select style={input} value={GATEWAY_PRESETS.includes(gateway) ? gateway : ''} onChange={(e) => setGateway(e.target.value)} disabled={!connected}>
|
||||
{GATEWAY_PRESETS.map((g) => <option key={g} value={g}>{g}</option>)}
|
||||
</select>
|
||||
<input style={input} value={gateway} onChange={(e) => setGateway(e.target.value)} disabled={!connected} />
|
||||
</div>
|
||||
|
||||
<div style={row}>
|
||||
<Button onClick={resolveAddress} disabled={!connected}>1. Resolve address</Button>
|
||||
<Button onClick={getContenthash} disabled={!connected}>2. Get contenthash</Button>
|
||||
<Button onClick={() => lastCid && fetchIpfsCid(lastCid, 'step 3/3:')} disabled={!connected || !lastCid}>3. Fetch from IPFS</Button>
|
||||
</div>
|
||||
<div style={row}>
|
||||
<label style={sub}>Or fetch any CID</label>
|
||||
<input style={input} value={customCid} onChange={(e) => setCustomCid(e.target.value)} placeholder="bafybe... or Qm..." disabled={!connected} />
|
||||
<Button onClick={() => fetchIpfsCid(customCid.trim(), 'custom')} disabled={!connected || !customCid.trim()}>Fetch</Button>
|
||||
</div>
|
||||
|
||||
<LogPanel lines={lines('ens')} placeholder="Connect the tunnel, then run a lookup." />
|
||||
{verifyLink && (
|
||||
<div style={{ ...sub, marginTop: '0.4rem' }}>
|
||||
verify visually in another tab: <a href={verifyLink} target="_blank" rel="noopener noreferrer">{verifyLink}</a>
|
||||
</div>
|
||||
)}
|
||||
{preview != null && <pre style={preStyle}>{preview}</pre>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
// ENS demo logic, ported from wasm/ens-demo/index.js and decoupled from the DOM.
|
||||
// The interesting bit is buildProvider(): ethers v6 lets us swap its HTTP
|
||||
// transport by assigning FetchRequest.getUrlFunc, so every JSON-RPC call routes
|
||||
// through mixFetch. To ethers, mixFetch looks like any fetch; to the mixnet,
|
||||
// ethers looks like any caller.
|
||||
|
||||
import { JsonRpcProvider, FetchRequest } from 'ethers';
|
||||
import type { MixFetchFn } from '../shared/mixTunnel';
|
||||
import type { Colour } from '../shared/ui';
|
||||
|
||||
export type LogFn = (msg: string, colour?: Colour) => void;
|
||||
export type LogLinkFn = (prefix: string, url: string) => void;
|
||||
|
||||
export function formatSize(bytes: number): string {
|
||||
if (bytes >= 1048576) return (bytes / 1048576).toFixed(1) + ' MB';
|
||||
if (bytes >= 1024) return (bytes / 1024).toFixed(1) + ' KB';
|
||||
return bytes + ' B';
|
||||
}
|
||||
|
||||
// base58btc decoder (Bitcoin/IPFS variant; alphabet excludes 0/O/I/l), used to
|
||||
// take CIDv0 strings apart into their raw multihash bytes.
|
||||
const BASE58_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
|
||||
const BASE58_MAP: Record<string, number> = (() => {
|
||||
const m: Record<string, number> = Object.create(null);
|
||||
for (let i = 0; i < BASE58_ALPHABET.length; i++) m[BASE58_ALPHABET[i]] = i;
|
||||
return m;
|
||||
})();
|
||||
|
||||
function base58Decode(str: string): Uint8Array {
|
||||
let zeros = 0;
|
||||
while (zeros < str.length && str[zeros] === '1') zeros++;
|
||||
let value = 0n;
|
||||
for (const c of str) {
|
||||
if (!(c in BASE58_MAP)) throw new Error(`invalid base58 char: ${c}`);
|
||||
value = value * 58n + BigInt(BASE58_MAP[c]);
|
||||
}
|
||||
const bytes: number[] = [];
|
||||
while (value > 0n) {
|
||||
bytes.unshift(Number(value & 0xffn));
|
||||
value >>= 8n;
|
||||
}
|
||||
for (let i = 0; i < zeros; i++) bytes.unshift(0);
|
||||
return new Uint8Array(bytes);
|
||||
}
|
||||
|
||||
// RFC 4648 base32, lowercase, no padding (the multibase 'b' alphabet CIDv1 uses).
|
||||
const BASE32_ALPHABET = 'abcdefghijklmnopqrstuvwxyz234567';
|
||||
|
||||
function base32Encode(bytes: Uint8Array): string {
|
||||
let bits = 0;
|
||||
let value = 0;
|
||||
let output = '';
|
||||
for (const byte of bytes) {
|
||||
value = (value << 8) | byte;
|
||||
bits += 8;
|
||||
while (bits >= 5) {
|
||||
output += BASE32_ALPHABET[(value >>> (bits - 5)) & 0x1f];
|
||||
bits -= 5;
|
||||
}
|
||||
}
|
||||
if (bits > 0) output += BASE32_ALPHABET[(value << (5 - bits)) & 0x1f];
|
||||
return output;
|
||||
}
|
||||
|
||||
// Convert CIDv0 ("Qm...") to CIDv1 ("bafybe..."). Pure re-encoding: same content,
|
||||
// same sha2-256 digest, different envelope. Needed for IPFS subdomain gateways,
|
||||
// whose DNS labels are case-insensitive (base58btc is case-sensitive; base32
|
||||
// lowercase survives the round trip). Pass through unchanged if not a canonical
|
||||
// CIDv0.
|
||||
export function cidV0ToV1(cid: string): string {
|
||||
if (!cid.startsWith('Qm') || cid.length !== 46) return cid;
|
||||
let decoded: Uint8Array;
|
||||
try {
|
||||
decoded = base58Decode(cid);
|
||||
} catch {
|
||||
return cid;
|
||||
}
|
||||
if (decoded.length !== 34 || decoded[0] !== 0x12 || decoded[1] !== 0x20) return cid;
|
||||
const v1 = new Uint8Array(36);
|
||||
v1[0] = 0x01; // CID version 1
|
||||
v1[1] = 0x70; // codec: dag-pb (preserves CIDv0's implicit codec)
|
||||
v1.set(decoded, 2); // copy multihash verbatim
|
||||
return 'b' + base32Encode(v1);
|
||||
}
|
||||
|
||||
// Expand a gateway template against a CID. {cid} placeholder supports path form
|
||||
// (https://gw/ipfs/{cid}/) and subdomain form (https://{cid}.ipfs.gw/); the
|
||||
// latter forces CIDv0 -> CIDv1. Legacy no-placeholder form is path-only.
|
||||
export function expandGatewayUrl(gateway: string, cid: string): string {
|
||||
if (gateway.includes('{cid}')) {
|
||||
const isSubdomain = /\{cid\}\.ipfs\./.test(gateway) || /\{cid\}\.ipns\./.test(gateway);
|
||||
const finalCid = isSubdomain ? cidV0ToV1(cid) : cid;
|
||||
return gateway.replace('{cid}', finalCid);
|
||||
}
|
||||
return `${gateway}${cid}/`;
|
||||
}
|
||||
|
||||
export interface FlatResponse {
|
||||
status: number;
|
||||
statusText: string;
|
||||
headers: Record<string, string>;
|
||||
body: Uint8Array;
|
||||
}
|
||||
|
||||
// Normalise headers to a lowercase-keyed plain object so downstream code only
|
||||
// ever sees one shape (mix-fetch v2 returns a real Response; smolmix's older
|
||||
// shape returned [k,v] tuples).
|
||||
export function headersToObj(headers: unknown): Record<string, string> {
|
||||
const out: Record<string, string> = {};
|
||||
if (!headers) return out;
|
||||
if (headers instanceof Headers || headers instanceof Map) {
|
||||
for (const [k, v] of (headers as Headers).entries()) out[k.toLowerCase()] = v;
|
||||
return out;
|
||||
}
|
||||
if (Array.isArray(headers)) {
|
||||
for (const [k, v] of headers) out[String(k).toLowerCase()] = v;
|
||||
return out;
|
||||
}
|
||||
if (typeof headers === 'object') {
|
||||
for (const [k, v] of Object.entries(headers as Record<string, string>)) out[k.toLowerCase()] = v;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// Single boundary into the mixnet: flatten the Response to {status, headers
|
||||
// (lowercased), body (bytes)} once, because downstream does its own
|
||||
// decompression / TextDecoder work on the raw wire payload.
|
||||
export async function callMixFetch(mixFetch: MixFetchFn, url: string, init?: RequestInit): Promise<FlatResponse> {
|
||||
const res = await mixFetch(url, init || {});
|
||||
const body = new Uint8Array(await res.arrayBuffer());
|
||||
return { status: res.status, statusText: res.statusText, headers: headersToObj(res.headers), body };
|
||||
}
|
||||
|
||||
// Decompress a body if the server set Content-Encoding. Native fetch does this
|
||||
// transparently; smolmix returns raw wire bytes. Brotli (br) is not in
|
||||
// DecompressionStream; pass it through.
|
||||
export async function decompressBody(body: Uint8Array, headers: Record<string, string>): Promise<Uint8Array> {
|
||||
if (!body || body.byteLength === 0) return body;
|
||||
const enc = (headers['content-encoding'] || '').toLowerCase().trim();
|
||||
if (!enc || enc === 'identity') return body;
|
||||
|
||||
let format: 'gzip' | 'deflate' | 'deflate-raw' | null = null;
|
||||
if (enc === 'gzip' || enc === 'x-gzip') format = 'gzip';
|
||||
else if (enc === 'deflate') format = 'deflate';
|
||||
else if (enc === 'deflate-raw') format = 'deflate-raw';
|
||||
if (!format) return body;
|
||||
|
||||
// body is always a plain ArrayBuffer-backed Uint8Array at runtime; the cast
|
||||
// sidesteps the TS 5.7 generic-typed-array vs BlobPart (ArrayBuffer) mismatch.
|
||||
const stream = new Blob([body as BlobPart]).stream().pipeThrough(new DecompressionStream(format));
|
||||
const buf = await new Response(stream).arrayBuffer();
|
||||
return new Uint8Array(buf);
|
||||
}
|
||||
|
||||
export function stripContentEncoding(headers: Record<string, string>): Record<string, string> {
|
||||
const out = { ...headers };
|
||||
delete out['content-encoding'];
|
||||
return out;
|
||||
}
|
||||
|
||||
export interface Fingerprint {
|
||||
title: string | null;
|
||||
h1: string | null;
|
||||
description: string | null;
|
||||
ogTitle: string | null;
|
||||
counts: { links: number; images: number; scripts: number; stylesheets: number; headings: number };
|
||||
bodyTextLen: number;
|
||||
}
|
||||
|
||||
// Extract a human-meaningful fingerprint from HTML for visual comparison against
|
||||
// the same page in another tab. DOMParser runs no scripts and fetches nothing.
|
||||
export function htmlFingerprint(text: string): Fingerprint {
|
||||
const doc = new DOMParser().parseFromString(text, 'text/html');
|
||||
const get = (sel: string, attr?: string): string | null => {
|
||||
const el = doc.querySelector(sel);
|
||||
if (!el) return null;
|
||||
return attr ? el.getAttribute(attr) : el.textContent?.trim() ?? null;
|
||||
};
|
||||
return {
|
||||
title: get('title'),
|
||||
h1: get('h1'),
|
||||
description: get('meta[name="description"]', 'content') || get('meta[property="og:description"]', 'content'),
|
||||
ogTitle: get('meta[property="og:title"]', 'content'),
|
||||
counts: {
|
||||
links: doc.querySelectorAll('a').length,
|
||||
images: doc.querySelectorAll('img').length,
|
||||
scripts: doc.querySelectorAll('script').length,
|
||||
stylesheets: doc.querySelectorAll('link[rel="stylesheet"], style').length,
|
||||
headings: doc.querySelectorAll('h1, h2, h3, h4, h5, h6').length,
|
||||
},
|
||||
bodyTextLen: doc.body ? doc.body.textContent!.replace(/\s+/g, ' ').trim().length : 0,
|
||||
};
|
||||
}
|
||||
|
||||
export function renderFingerprint(fp: Fingerprint, totalBytes: number): string {
|
||||
const orNone = (s: string | null) => s || '(none)';
|
||||
return [
|
||||
'page fingerprint:',
|
||||
'',
|
||||
` title: ${orNone(fp.title)}`,
|
||||
` H1: ${orNone(fp.h1)}`,
|
||||
` description: ${orNone(fp.description)}`,
|
||||
` og:title: ${orNone(fp.ogTitle)}`,
|
||||
'',
|
||||
` ${fp.counts.links} links, ${fp.counts.images} images, ${fp.counts.scripts} scripts, ${fp.counts.stylesheets} stylesheets, ${fp.counts.headings} headings`,
|
||||
` body text: ${fp.bodyTextLen.toLocaleString()} chars after stripping tags`,
|
||||
` HTML size: ${formatSize(totalBytes)} raw response body`,
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
// Bridge ethers' FetchRequest to mixFetch. Every JSON-RPC call routes through
|
||||
// the mixnet; we decompress (smolmix doesn't) and log per-call timing/selector.
|
||||
export function buildProvider(rpcUrl: string, mixFetch: MixFetchFn, ensLog: LogFn): JsonRpcProvider {
|
||||
const base = new FetchRequest(rpcUrl);
|
||||
|
||||
base.getUrlFunc = async (req: FetchRequest) => {
|
||||
const t0 = performance.now();
|
||||
const raw = await callMixFetch(mixFetch, req.url, {
|
||||
method: req.method,
|
||||
headers: req.headers,
|
||||
body: (req.body ?? undefined) as BodyInit | undefined,
|
||||
});
|
||||
const ms = (performance.now() - t0).toFixed(0);
|
||||
|
||||
const body = await decompressBody(raw.body, raw.headers);
|
||||
const headers = stripContentEncoding(raw.headers);
|
||||
|
||||
ensLog(` RPC ${req.method} ${req.url} -> ${raw.status} ${raw.statusText}`);
|
||||
ensLog(` ${raw.body.byteLength}B wire, ${body.byteLength}B decoded, ${ms} ms`);
|
||||
|
||||
if (req.body) {
|
||||
try {
|
||||
const parsed = JSON.parse(new TextDecoder().decode(req.body));
|
||||
const data: string = parsed.params?.[0]?.data || '';
|
||||
const selector = data.slice(0, 10);
|
||||
ensLog(` -> method=${parsed.method} selector=${selector} args=0x${data.slice(10)}`);
|
||||
} catch {
|
||||
/* not all RPC bodies are eth_call with calldata */
|
||||
}
|
||||
}
|
||||
if (body.byteLength) {
|
||||
try {
|
||||
ensLog(` <- ${new TextDecoder().decode(body)}`);
|
||||
} catch {
|
||||
/* binary body */
|
||||
}
|
||||
}
|
||||
|
||||
return { statusCode: raw.status, statusMessage: raw.statusText, headers, body };
|
||||
};
|
||||
|
||||
// staticNetwork skips the eth_chainId discovery probe: one round trip saved.
|
||||
return new JsonRpcProvider(base, 'mainnet', { staticNetwork: true });
|
||||
}
|
||||
@@ -0,0 +1,323 @@
|
||||
// Railgun-over-the-mixnet demo, ported from wasm/railgun-demo. Two privacy
|
||||
// layers: Nym hides the network (RPC via mixFetch), Railgun hides the
|
||||
// application layer (shielded notes). Sepolia testnet only.
|
||||
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { HDNodeWallet, JsonRpcProvider, formatEther } from 'ethers';
|
||||
import { MixTunnelSetup, type MixFetchFn } from '../shared/mixTunnel';
|
||||
import { Button, LogPanel, useLogs, box, row, input, sub, legend } from '../shared/ui';
|
||||
import { buildProvider, callMixFetch, installGlobalMixFetchRouting, withRetry } from '../shared/mixfetch';
|
||||
import {
|
||||
DEFAULT_MNEMONIC,
|
||||
SEPOLIA_CHAIN_ID,
|
||||
STORAGE_KEY,
|
||||
createRailgunWalletFromMnemonic,
|
||||
derivePublicAddress,
|
||||
ensureRailgunEngine,
|
||||
shieldEth,
|
||||
type RailgunWalletInfo,
|
||||
} from './lib';
|
||||
|
||||
const RPC_PRESETS = ['https://ethereum-sepolia-rpc.publicnode.com', 'https://rpc.sepolia.org'];
|
||||
// Fixed shield amount: a single small value so the shared, faucet-funded testnet
|
||||
// wallet can't be drained by an arbitrary amount.
|
||||
const SHIELD_AMOUNT = '0.01';
|
||||
const IP_ECHO_URL = 'https://ipinfo.io/ip';
|
||||
const IP_SHAPE_RE = /^[\d.:a-f]{3,45}$/i;
|
||||
|
||||
export function RailgunDemo() {
|
||||
const { log, lines } = useLogs();
|
||||
const dlog = (msg: string, colour?: 'green' | 'red' | 'orange' | 'gray') => log('railgun', msg, colour);
|
||||
|
||||
const [mixFetch, setMixFetch] = useState<MixFetchFn | null>(null);
|
||||
const [mnemonic, setMnemonic] = useState('');
|
||||
const [publicAddr, setPublicAddr] = useState('(not generated)');
|
||||
const [railgunWallet, setRailgunWallet] = useState<RailgunWalletInfo | null>(null);
|
||||
const [rpc, setRpc] = useState(RPC_PRESETS[0]);
|
||||
const [balance, setBalance] = useState('');
|
||||
const [txHash, setTxHash] = useState<string | null>(null);
|
||||
const [storageStatus, setStorageStatus] = useState('');
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [importPhrase, setImportPhrase] = useState('');
|
||||
|
||||
const publicWalletRef = useRef<HDNodeWallet | null>(null);
|
||||
const providerRef = useRef<JsonRpcProvider | null>(null);
|
||||
|
||||
const connected = mixFetch != null;
|
||||
const hasWallet = publicAddr !== '(not generated)';
|
||||
|
||||
function updateStorageStatus() {
|
||||
let stored: string | null = null;
|
||||
try {
|
||||
stored = localStorage.getItem(STORAGE_KEY);
|
||||
} catch {
|
||||
/* localStorage disabled */
|
||||
}
|
||||
setStorageStatus(stored ? 'wallet saved in browser storage (auto-loaded on reload)' : 'no wallet saved; generate or import to persist one');
|
||||
}
|
||||
|
||||
function ensureProvider(): JsonRpcProvider {
|
||||
if (providerRef.current) return providerRef.current;
|
||||
if (!mixFetch) throw new Error('connect the mixnet tunnel first');
|
||||
const url = rpc.trim();
|
||||
if (!url) throw new Error('Sepolia RPC URL is required');
|
||||
dlog(`building JsonRpcProvider({ rpc: ${url}, transport: mixFetch })`);
|
||||
providerRef.current = buildProvider(url, mixFetch, SEPOLIA_CHAIN_ID);
|
||||
return providerRef.current;
|
||||
}
|
||||
|
||||
async function deriveRailgun(phrase: string) {
|
||||
dlog('initialising Railgun engine + deriving shielded address...');
|
||||
try {
|
||||
await ensureRailgunEngine(rpc.trim(), dlog);
|
||||
const result = await createRailgunWalletFromMnemonic(phrase.trim());
|
||||
setRailgunWallet(result);
|
||||
dlog(`Railgun address derived: ${result.railgunAddress}`, 'green');
|
||||
} catch (e: any) {
|
||||
dlog(`Railgun derivation failed: ${e.message || e}`, 'red');
|
||||
}
|
||||
}
|
||||
|
||||
function loadWallet(phrase: string) {
|
||||
let wallet: HDNodeWallet;
|
||||
try {
|
||||
wallet = derivePublicAddress(phrase);
|
||||
} catch (e: any) {
|
||||
dlog(`invalid mnemonic: ${e.message || e}`, 'red');
|
||||
return;
|
||||
}
|
||||
publicWalletRef.current = wallet;
|
||||
setPublicAddr(wallet.address);
|
||||
setRailgunWallet(null);
|
||||
setMnemonic(phrase.trim());
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, phrase.trim());
|
||||
} catch {
|
||||
/* localStorage disabled */
|
||||
}
|
||||
updateStorageStatus();
|
||||
dlog(`public address derived: ${wallet.address}`, 'green');
|
||||
if (mixFetch) void deriveRailgun(phrase);
|
||||
else dlog('connect the mixnet tunnel to derive the Railgun address', 'orange');
|
||||
}
|
||||
|
||||
// Auto-load on mount: stored mnemonic, else the funded testnet fallback.
|
||||
useEffect(() => {
|
||||
let stored: string | null = null;
|
||||
try {
|
||||
stored = localStorage.getItem(STORAGE_KEY);
|
||||
} catch {
|
||||
/* localStorage disabled */
|
||||
}
|
||||
const phrase = stored || DEFAULT_MNEMONIC;
|
||||
setMnemonic(phrase);
|
||||
updateStorageStatus();
|
||||
try {
|
||||
const wallet = derivePublicAddress(phrase);
|
||||
publicWalletRef.current = wallet;
|
||||
setPublicAddr(wallet.address);
|
||||
dlog(`auto-loaded wallet: ${wallet.address}`, 'green');
|
||||
dlog('public side ready. The Railgun address derives once the tunnel is up.');
|
||||
} catch (e: any) {
|
||||
dlog(`auto-load failed: ${e.message || e}`, 'red');
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
function onReady(fn: MixFetchFn) {
|
||||
setMixFetch(() => fn);
|
||||
// Route every ethers HTTP call (incl. Railgun's internal providers) through Nym.
|
||||
installGlobalMixFetchRouting(fn);
|
||||
if (publicWalletRef.current && !railgunWallet) void deriveRailgun(mnemonic);
|
||||
}
|
||||
function onDisconnect() {
|
||||
setMixFetch(null);
|
||||
providerRef.current = null;
|
||||
}
|
||||
|
||||
function generateWallet() {
|
||||
dlog('generating fresh BIP-39 mnemonic...');
|
||||
const wallet = HDNodeWallet.createRandom();
|
||||
loadWallet(wallet.mnemonic!.phrase);
|
||||
}
|
||||
function importWallet() {
|
||||
loadWallet(importPhrase);
|
||||
setImportPhrase('');
|
||||
}
|
||||
function clearWallet() {
|
||||
try {
|
||||
localStorage.removeItem(STORAGE_KEY);
|
||||
} catch {
|
||||
/* localStorage disabled */
|
||||
}
|
||||
publicWalletRef.current = null;
|
||||
setPublicAddr('(not generated)');
|
||||
setRailgunWallet(null);
|
||||
setMnemonic('');
|
||||
updateStorageStatus();
|
||||
dlog('cleared stored wallet; reload to load the funded fallback');
|
||||
}
|
||||
|
||||
async function checkBalance() {
|
||||
if (!publicWalletRef.current) return dlog('generate or import a wallet first', 'red');
|
||||
let provider: JsonRpcProvider;
|
||||
try {
|
||||
provider = ensureProvider();
|
||||
} catch (e: any) {
|
||||
return dlog(`${e.message || e}`, 'red');
|
||||
}
|
||||
setBusy(true);
|
||||
dlog(`eth_getBalance(${publicWalletRef.current.address}) via mixFetch...`);
|
||||
try {
|
||||
const wei = await withRetry(() => provider.getBalance(publicWalletRef.current!.address), 'eth_getBalance', { log: dlog });
|
||||
const eth = formatEther(wei);
|
||||
setBalance(`${eth} ETH (Sepolia)`);
|
||||
dlog(`balance: ${eth} ETH`, 'green');
|
||||
} catch (e: any) {
|
||||
dlog(`balance lookup failed: ${e.shortMessage || e.message || e}`, 'red');
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function shield() {
|
||||
if (!railgunWallet) return dlog('Railgun wallet not derived; connect tunnel + generate wallet first', 'red');
|
||||
if (!publicWalletRef.current) return dlog('public wallet missing', 'red');
|
||||
let provider: JsonRpcProvider;
|
||||
try {
|
||||
provider = ensureProvider();
|
||||
} catch (e: any) {
|
||||
return dlog(`${e.message || e}`, 'red');
|
||||
}
|
||||
setBusy(true);
|
||||
setTxHash(null);
|
||||
try {
|
||||
await shieldEth({
|
||||
publicWallet: publicWalletRef.current,
|
||||
railgunWallet,
|
||||
provider,
|
||||
amountStr: SHIELD_AMOUNT,
|
||||
log: dlog,
|
||||
onTxHash: setTxHash,
|
||||
});
|
||||
} catch (e: any) {
|
||||
dlog(`shield failed: ${e.shortMessage || e.message || e}`, 'red');
|
||||
// Tear down the provider so its background pollers stop after a failure.
|
||||
try {
|
||||
providerRef.current?.destroy();
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
providerRef.current = null;
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function verifyIp() {
|
||||
if (!mixFetch) return dlog('connect the mixnet tunnel first', 'red');
|
||||
setBusy(true);
|
||||
dlog('comparing direct-clearnet IP vs Nym-exit IP...');
|
||||
let directIp: string;
|
||||
try {
|
||||
directIp = (await (await fetch(IP_ECHO_URL)).text()).trim();
|
||||
if (!IP_SHAPE_RE.test(directIp)) directIp = `(unexpected: ${directIp})`;
|
||||
} catch (e: any) {
|
||||
directIp = `error: ${e.message || e}`;
|
||||
}
|
||||
dlog(` your real IP (direct fetch, no Nym): ${directIp}`, 'orange');
|
||||
let nymIp: string;
|
||||
try {
|
||||
const raw = await callMixFetch(mixFetch, IP_ECHO_URL, {});
|
||||
nymIp = new TextDecoder().decode(raw.body).trim();
|
||||
if (!IP_SHAPE_RE.test(nymIp)) nymIp = `(unexpected: ${nymIp})`;
|
||||
} catch (e: any) {
|
||||
nymIp = `error: ${e.message || e}`;
|
||||
}
|
||||
dlog(` what the upstream sees via mixFetch -> Nym: ${nymIp}`, 'green');
|
||||
if (!nymIp.startsWith('error') && !directIp.startsWith('error') && nymIp !== directIp) {
|
||||
dlog('IPs differ. Every Shield broadcast uses this same Nym-exit path.', 'green');
|
||||
} else {
|
||||
dlog('Could not confirm a different exit IP. Try again, or reconnect with a different IPR.', 'red');
|
||||
}
|
||||
setBusy(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ margin: '1.5rem 0' }}>
|
||||
<div style={{ ...box, borderColor: 'var(--colorWarn, #d97706)' }}>
|
||||
<strong>Sepolia testnet only.</strong>{' '}
|
||||
<span style={sub}>
|
||||
The wallet holds only test ETH from public faucets and the mnemonic is stored in plain
|
||||
browser storage. Never paste a mainnet mnemonic into this demo.
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<MixTunnelSetup onReady={onReady} onDisconnect={onDisconnect} clientIdPrefix="railgun-demo" />
|
||||
|
||||
<div style={box}>
|
||||
<div style={legend}>Wallet</div>
|
||||
<div style={sub}>
|
||||
A testnet wallet is auto-loaded from browser storage. Its mnemonic is not shown here:
|
||||
it holds only Sepolia test ETH, so please don't be cheeky and try to pull the funded
|
||||
testnet key out. Import your own below if you'd rather.
|
||||
</div>
|
||||
<div style={row}>
|
||||
<input
|
||||
type="password"
|
||||
autoComplete="off"
|
||||
style={input}
|
||||
value={importPhrase}
|
||||
onChange={(e) => setImportPhrase(e.target.value)}
|
||||
placeholder="import your own 12-word mnemonic (optional)"
|
||||
/>
|
||||
<Button onClick={importWallet} disabled={!importPhrase.trim()}>Import</Button>
|
||||
<Button onClick={generateWallet}>Generate</Button>
|
||||
<Button onClick={clearWallet}>Clear</Button>
|
||||
</div>
|
||||
<div style={sub}>public address: <code>{publicAddr}</code></div>
|
||||
<div style={sub}>Railgun address: <code>{railgunWallet ? railgunWallet.railgunAddress : connected ? '(deriving...)' : '(connect tunnel to derive)'}</code></div>
|
||||
<div style={sub}>{storageStatus}</div>
|
||||
</div>
|
||||
|
||||
<div style={box}>
|
||||
<div style={legend}>Public Sepolia state</div>
|
||||
<div style={row}>
|
||||
<label style={sub}>RPC</label>
|
||||
<select style={input} value={RPC_PRESETS.includes(rpc) ? rpc : ''} disabled={connected || busy} onChange={(e) => { setRpc(e.target.value); providerRef.current = null; }}>
|
||||
{RPC_PRESETS.map((u) => <option key={u} value={u}>{u}</option>)}
|
||||
</select>
|
||||
<input style={input} value={rpc} disabled={connected || busy} onChange={(e) => { setRpc(e.target.value); providerRef.current = null; }} />
|
||||
</div>
|
||||
<div style={row}>
|
||||
<Button onClick={checkBalance} disabled={!connected || !hasWallet || busy}>Check balance</Button>
|
||||
<Button onClick={verifyIp} disabled={!connected || busy}>Verify IP routing</Button>
|
||||
<span style={sub}>{balance}</span>
|
||||
</div>
|
||||
<div style={sub}>Verify IP makes one direct (clearnet) call to ipinfo.io for the comparison, so you will see a single ipinfo.io row in the Network tab.</div>
|
||||
</div>
|
||||
|
||||
<div style={box}>
|
||||
<div style={legend}>Shield ETH into a private note</div>
|
||||
<div style={row}>
|
||||
<Button onClick={shield} disabled={!connected || !railgunWallet || busy}>Shield {SHIELD_AMOUNT} ETH</Button>
|
||||
<span style={sub}>Fixed at {SHIELD_AMOUNT} ETH so the shared testnet wallet isn't drained.</span>
|
||||
</div>
|
||||
{txHash && (
|
||||
<div style={{ marginTop: '0.5rem' }}>
|
||||
<a
|
||||
href={`https://sepolia.etherscan.io/tx/${txHash}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={{ color: '#3b82f6', textDecoration: 'underline', fontWeight: 600 }}
|
||||
>
|
||||
View transaction on Etherscan
|
||||
</a>{' '}
|
||||
<code style={{ ...sub, opacity: 0.7 }}>{txHash.slice(0, 10)}...{txHash.slice(-8)}</code>
|
||||
</div>
|
||||
)}
|
||||
<LogPanel lines={lines('railgun')} placeholder="Connect the tunnel, then derive the wallet and shield." />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
// Railgun-over-the-mixnet logic, ported from wasm/railgun-demo/index.js.
|
||||
// Two privacy layers: Nym hides the network (every RPC via mixFetch), Railgun
|
||||
// hides the application layer (shielded notes break the on-chain graph).
|
||||
//
|
||||
// The @railgun-community SDK is imported dynamically (no bundled types here, so
|
||||
// it is typed `any`) and the engine is a process-global singleton, so the
|
||||
// started/loaded flags live at module scope, which is the faithful model.
|
||||
|
||||
import { HDNodeWallet, Mnemonic, JsonRpcProvider, keccak256, parseEther } from 'ethers';
|
||||
import { withRetry } from '../shared/mixfetch';
|
||||
|
||||
export const SEPOLIA_CHAIN_ID = 11155111;
|
||||
export const SEPOLIA_NETWORK_NAME = 'Ethereum_Sepolia';
|
||||
export const SEPOLIA_WETH = '0xfFf9976782d46CC05630D1f6eBAb18b2324d6B14';
|
||||
export const TXID_VERSION_V2 = 'V2_PoseidonMerkle';
|
||||
export const STORAGE_KEY = 'railgun-demo-mnemonic';
|
||||
export const DEFAULT_MNEMONIC = 'inherit joy bubble reveal fit skin repair involve spoil cube robot angry';
|
||||
const ENCRYPTION_KEY = '0101010101010101010101010101010101010101010101010101010101010101';
|
||||
|
||||
export type RailgunLog = (msg: string, colour?: 'green' | 'red' | 'orange' | 'gray') => void;
|
||||
export interface RailgunWalletInfo { id: string; railgunAddress: string; }
|
||||
|
||||
let engineStarted = false;
|
||||
let providerLoaded = false;
|
||||
|
||||
// Pure-client public address derivation. No network, no engine; works before
|
||||
// the tunnel is up so the page can show the funding target immediately.
|
||||
export function derivePublicAddress(phrase: string): HDNodeWallet {
|
||||
const mnemonic = Mnemonic.fromPhrase(phrase.trim());
|
||||
return HDNodeWallet.fromMnemonic(mnemonic, "m/44'/60'/0'/0/0");
|
||||
}
|
||||
|
||||
async function ensureEngineStarted(): Promise<void> {
|
||||
if (engineStarted) return;
|
||||
const railgun: any = await import('@railgun-community/wallet');
|
||||
const { MemoryLevel }: any = await import('memory-level');
|
||||
const db = new MemoryLevel();
|
||||
// Read-only artifact store: Shield does not need proving artifacts.
|
||||
const artifactStore = new railgun.ArtifactStore(
|
||||
async () => null,
|
||||
async () => {},
|
||||
async () => false,
|
||||
);
|
||||
await railgun.startRailgunEngine(
|
||||
'railgundemo', // wallet source id; alphanumeric only
|
||||
db,
|
||||
false, // shouldDebug
|
||||
artifactStore,
|
||||
false, // useNativeArtifacts (Node-only)
|
||||
false, // skipMerkletreeScans
|
||||
undefined, // poiNodeURLs (undefined keeps POI uninstantiated)
|
||||
undefined, // customPOILists
|
||||
false, // verboseScanLogging
|
||||
);
|
||||
// Sepolia is POI-gated but the public aggregator is dead and POI is not what
|
||||
// this demo proves: clear the network's poi field so the engine treats it as
|
||||
// a pre-POI deployment. Production would point at a real POI URL instead.
|
||||
const { NETWORK_CONFIG }: any = await import('@railgun-community/shared-models');
|
||||
NETWORK_CONFIG[SEPOLIA_NETWORK_NAME].poi = undefined;
|
||||
// Disable GraphQL quick-sync (its subgraph map lacks Sepolia, so it would
|
||||
// spam-XHR /undefined). Falls back to direct eth_getLogs scanning.
|
||||
const engine = railgun.getEngine();
|
||||
engine.quickSyncEvents = async () => ({ commitmentEvents: [], unshieldEvents: [], nullifierEvents: [] });
|
||||
engine.quickSyncRailgunTransactionsV2 = async () => [];
|
||||
engineStarted = true;
|
||||
}
|
||||
|
||||
async function loadProviderOnce(rpc: string): Promise<void> {
|
||||
if (providerLoaded) return;
|
||||
const railgun: any = await import('@railgun-community/wallet');
|
||||
// One provider, weight 2 (the validator requires totalWeight >= 2; a single
|
||||
// HTTPS endpoint avoids competing TCP handshakes during cold start).
|
||||
const fallbackConfig = { chainId: SEPOLIA_CHAIN_ID, providers: [{ provider: rpc, priority: 1, weight: 2 }] };
|
||||
await railgun.loadProvider(fallbackConfig, SEPOLIA_NETWORK_NAME, 10000);
|
||||
providerLoaded = true;
|
||||
}
|
||||
|
||||
export async function ensureRailgunEngine(rpc: string, log: RailgunLog): Promise<void> {
|
||||
if (engineStarted && providerLoaded) return;
|
||||
log('initialising Railgun engine (one-time)...');
|
||||
await ensureEngineStarted();
|
||||
// loadProvider hits the network via mixFetch; cold-start can exceed Railgun's
|
||||
// 60s timeout, so retry (the second attempt finds the pool warm).
|
||||
await withRetry(() => loadProviderOnce(rpc), 'loadProvider', { log });
|
||||
log('Railgun engine ready', 'green');
|
||||
}
|
||||
|
||||
export async function createRailgunWalletFromMnemonic(phrase: string): Promise<RailgunWalletInfo> {
|
||||
const railgun: any = await import('@railgun-community/wallet');
|
||||
const creationBlockNumbers = { [SEPOLIA_NETWORK_NAME]: 10_900_000 };
|
||||
return await railgun.createRailgunWallet(ENCRYPTION_KEY, phrase, creationBlockNumbers);
|
||||
}
|
||||
|
||||
// Shield ETH into a shielded note. The headline action: a 4-step flow that
|
||||
// signs a shield key, estimates gas, populates the tx, then signs + broadcasts
|
||||
// (idempotently) through the mixFetch-routed provider.
|
||||
export async function shieldEth(opts: {
|
||||
publicWallet: HDNodeWallet;
|
||||
railgunWallet: RailgunWalletInfo;
|
||||
provider: JsonRpcProvider;
|
||||
amountStr: string;
|
||||
log: RailgunLog;
|
||||
onTxHash: (hash: string) => void;
|
||||
}): Promise<void> {
|
||||
const { publicWallet, railgunWallet, provider, amountStr, log, onTxHash } = opts;
|
||||
const railgun: any = await import('@railgun-community/wallet');
|
||||
|
||||
let amountWei: bigint;
|
||||
try {
|
||||
amountWei = parseEther(amountStr);
|
||||
} catch {
|
||||
log(`invalid amount: "${amountStr}"`, 'red');
|
||||
return;
|
||||
}
|
||||
if (amountWei <= 0n) {
|
||||
log('amount must be > 0', 'red');
|
||||
return;
|
||||
}
|
||||
|
||||
log(`shielding ${amountStr} ETH -> ${railgunWallet.railgunAddress}`);
|
||||
|
||||
// Step 1: shieldPrivateKey = keccak256 of a signature over a deterministic
|
||||
// message. Signing proves consent and binds the key to the public wallet.
|
||||
log('step 1/4: signing shield-key derivation message...');
|
||||
const msg = railgun.getShieldPrivateKeySignatureMessage();
|
||||
const sigHex = await publicWallet.signMessage(msg);
|
||||
const shieldPrivateKey = keccak256(sigHex);
|
||||
const wrappedERC20Amount = { tokenAddress: SEPOLIA_WETH, amount: amountWei };
|
||||
|
||||
// Step 2: gas estimate (needs the funder's address to simulate the call).
|
||||
log('step 2/4: estimating gas via mixFetch...');
|
||||
// withRetry can't infer T from the `any`-typed SDK call, so it falls back to
|
||||
// unknown; annotate the result to read its fields.
|
||||
const gasEstResp: any = await withRetry(
|
||||
() =>
|
||||
railgun.gasEstimateForShieldBaseToken(
|
||||
TXID_VERSION_V2,
|
||||
SEPOLIA_NETWORK_NAME,
|
||||
railgunWallet.railgunAddress,
|
||||
shieldPrivateKey,
|
||||
wrappedERC20Amount,
|
||||
publicWallet.address,
|
||||
),
|
||||
'gasEstimateForShieldBaseToken',
|
||||
{ log },
|
||||
);
|
||||
log(` gas estimate: ${gasEstResp.gasEstimate.toString()} units`);
|
||||
|
||||
// EIP-1559 gas details, padded 50% so a transient spike during the mixnet
|
||||
// round trip does not strand the tx.
|
||||
const feeData = await provider.getFeeData();
|
||||
const maxFeePerGas = ((feeData.maxFeePerGas ?? feeData.gasPrice ?? 30_000_000_000n) * 3n) / 2n;
|
||||
const maxPriorityFeePerGas = ((feeData.maxPriorityFeePerGas ?? 2_000_000_000n) * 3n) / 2n;
|
||||
const gasDetails = { evmGasType: 2, gasEstimate: gasEstResp.gasEstimate, maxFeePerGas, maxPriorityFeePerGas };
|
||||
|
||||
// Step 3: populate the actual transaction.
|
||||
log('step 3/4: populating shield transaction...');
|
||||
const populateResp = await railgun.populateShieldBaseToken(
|
||||
TXID_VERSION_V2,
|
||||
SEPOLIA_NETWORK_NAME,
|
||||
railgunWallet.railgunAddress,
|
||||
shieldPrivateKey,
|
||||
wrappedERC20Amount,
|
||||
gasDetails,
|
||||
);
|
||||
const tx = populateResp.transaction;
|
||||
|
||||
// Step 4: sign then broadcast separately, so the tx hash is fixed before any
|
||||
// broadcast attempt and a dropped response can be retried idempotently.
|
||||
log('step 4/4: signing + broadcasting via mixFetch -> Nym...');
|
||||
const signer = publicWallet.connect(provider);
|
||||
const populated = await signer.populateTransaction(tx);
|
||||
const signedHex = await signer.signTransaction(populated);
|
||||
const txHash = keccak256(signedHex);
|
||||
log(` signed tx hash: ${txHash}`);
|
||||
log(` -> To: ${populated.to} (Railgun Sepolia proxy contract)`);
|
||||
log(` -> calldata selector: ${(populated.data || '').slice(0, 10)} (Railgun shield function; Etherscan decodes this)`);
|
||||
log(` -> full calldata: ${populated.data || ''}`);
|
||||
onTxHash(txHash);
|
||||
|
||||
let sentTx: any;
|
||||
for (let attempt = 1; attempt <= 3; attempt++) {
|
||||
try {
|
||||
sentTx = await provider.broadcastTransaction(signedHex);
|
||||
log(` broadcast OK (attempt ${attempt})`, 'green');
|
||||
break;
|
||||
} catch (e: any) {
|
||||
const m = e.shortMessage || e.message || String(e);
|
||||
try {
|
||||
const existing = await provider.getTransaction(txHash);
|
||||
if (existing) {
|
||||
log(' broadcast response failed but tx is on chain, partial success', 'green');
|
||||
sentTx = existing;
|
||||
break;
|
||||
}
|
||||
} catch {
|
||||
/* getTransaction failed too; treat as not on chain, retry */
|
||||
}
|
||||
if (attempt < 3) {
|
||||
log(` broadcast attempt ${attempt}/3 failed (${m}), retrying in 10s...`, 'orange');
|
||||
await new Promise((r) => setTimeout(r, 10_000));
|
||||
} else {
|
||||
log(' broadcast failed after 3 attempts. The mixnet route to the RPC is degraded. Disconnect, tick "Use random IPR", reconnect, then Shield again.', 'red');
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log('waiting for receipt (1 confirmation)...');
|
||||
const receipt = await sentTx.wait(1);
|
||||
if (receipt && receipt.status === 1) {
|
||||
log(`shielded. Block ${receipt.blockNumber}, gas used ${receipt.gasUsed}`, 'green');
|
||||
log("verify on Etherscan: the To field is Railgun's Sepolia proxy, the method decodes to a shield, and the logs hold an encrypted Shield commitment.", 'green');
|
||||
} else {
|
||||
log('tx mined but reverted', 'red');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
// Shared mixnet glossary for the demo pages, with links to the relevant docs.
|
||||
// Raw <a> inside a React component does not pick up Nextra's MDX link styling,
|
||||
// so the links are styled explicitly via the L helper.
|
||||
import React from 'react';
|
||||
|
||||
function L({ href, children }: { href: string; children: React.ReactNode }) {
|
||||
return (
|
||||
<a href={href} style={{ color: '#3b82f6', textDecoration: 'underline' }}>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
export function MixnetGlossary() {
|
||||
return (
|
||||
<ul>
|
||||
<li>
|
||||
<strong>Mixnet.</strong> An overlay network that routes your traffic through several relays,
|
||||
mixed in with everyone else's, so no single point can link sender to receiver. See{' '}
|
||||
<L href="/network/mixnet-mode">mixnet mode</L>.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Entry gateway.</strong> Your first hop into the mixnet. The browser holds one
|
||||
WebSocket to it, and all tunnelled traffic travels over that single connection as opaque
|
||||
frames. See <L href="/network/infrastructure/nym-nodes">Nym nodes</L>.
|
||||
</li>
|
||||
<li>
|
||||
<strong>IPR (IP Packet Router), the exit.</strong> Where traffic leaves the mixnet for the
|
||||
public internet. The destination sees the IPR's IP, not yours. See{' '}
|
||||
<L href="/network/infrastructure/exit-services#ip-packet-router">exit services</L>.
|
||||
</li>
|
||||
<li>
|
||||
<strong>SURB (single-use reply block).</strong> A prepaid, single-use return envelope. The
|
||||
exit replies through it without ever learning your address. See{' '}
|
||||
<L href="/network/mixnet-mode/anonymous-replies">anonymous replies</L>.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Cover traffic / Poisson timing.</strong> Decoy packets sent on randomised timing, so
|
||||
your real traffic blends into a steady stream. See{' '}
|
||||
<L href="/network/mixnet-mode/cover-traffic">cover traffic</L>.
|
||||
</li>
|
||||
<li>
|
||||
<strong>mixFetch.</strong> A <code>fetch()</code>-shaped function from{' '}
|
||||
<L href="/developers/mix-fetch"><code>@nymproject/mix-fetch</code></L>. It runs the mixnet
|
||||
client (smolmix) in a Web Worker, so each request goes through the mixnet rather than the
|
||||
browser's network stack.
|
||||
</li>
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// Shared "Watch the Network tab" callout, used on the playground and the demo
|
||||
// pages. Generic wording so it reads correctly wherever a single mixnet tunnel
|
||||
// carries the page's traffic.
|
||||
import React from 'react';
|
||||
import { Callout } from 'nextra/components';
|
||||
|
||||
export function NetworkTabCallout() {
|
||||
return (
|
||||
<Callout type="info">
|
||||
<strong>Watch the Network tab.</strong> Open DevTools → Network before you connect. Once the
|
||||
tunnel reports ready, every operation you run here adds <strong>no new request</strong> to that
|
||||
tab: it is multiplexed inside the single WebSocket to the entry gateway. Only the clearnet
|
||||
comparison buttons add rows. (Setup also fetches the network topology over HTTPS and refreshes
|
||||
it periodically, so those nym-api calls and the gateway WebSocket are the only clearnet requests
|
||||
you will see.) Your real traffic never leaves the browser as an identifiable, per-destination
|
||||
request.
|
||||
</Callout>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
// Shared mixnet-tunnel setup panel for the in-docs demos.
|
||||
//
|
||||
// Owns the connection lifecycle (setup / disconnect / state) and the options
|
||||
// surface (IPR pin, SURBs, DNS, timeouts, ...), and hands the parent demo a
|
||||
// `mixFetch` function once the tunnel is `ready`. Modelled on the playground's
|
||||
// inline setup section; the demos differ only in what they do with `mixFetch`.
|
||||
//
|
||||
// The package import is dynamic so the multi-MB wasm chunk loads only when the
|
||||
// visitor clicks Connect, not on page render. Everything here is client-only;
|
||||
// render the demo page with `next/dynamic` + `ssr: false`.
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Button, LogPanel, StatusText, useLogs, box, row, input, num, sub, legend, type Status } from './ui';
|
||||
|
||||
export type MixFetchFn = (url: string, init?: RequestInit) => Promise<Response>;
|
||||
|
||||
interface MixFetchModule {
|
||||
setupMixTunnel: (opts?: Record<string, unknown>) => Promise<void>;
|
||||
disconnectMixTunnel: () => Promise<void>;
|
||||
getTunnelState: () => Promise<{ state: string; reason?: string }>;
|
||||
mixFetch: MixFetchFn;
|
||||
}
|
||||
|
||||
// Lazy-load the published mix-fetch facade. The literal specifier keeps webpack
|
||||
// code-splitting the wasm into an async chunk.
|
||||
async function loadMixFetch(): Promise<MixFetchModule> {
|
||||
// @ts-ignore -- @nymproject/mix-fetch resolves at runtime; lazy wasm chunk
|
||||
const m = await import('@nymproject/mix-fetch');
|
||||
return m as unknown as MixFetchModule;
|
||||
}
|
||||
|
||||
const clampSurbs = (n: number, min: number) => Math.min(50, Math.max(min, n));
|
||||
|
||||
// Default IPR exit for the docs demos. Pinned so a demo connects to a known
|
||||
// exit by default; users can switch to auto-discovery with 'Use random IPR'.
|
||||
const DEFAULT_IPR =
|
||||
'6B6iuWX4bQP4GVA4Yq7XmZencaaGw6BaPY6xJWYSwsbF.6g6LRx1fgU2Q2A4ZPKonYHtfBARh1GPMe1LtXk6vpRR8@q2A2cbooyC16YJzvdYaSMH9X3cSiieZNtfBr8cE8Fi1';
|
||||
|
||||
export function MixTunnelSetup({
|
||||
onReady,
|
||||
onDisconnect,
|
||||
clientIdPrefix = 'docs-demo',
|
||||
}: {
|
||||
onReady: (mixFetch: MixFetchFn) => void;
|
||||
onDisconnect?: () => void;
|
||||
clientIdPrefix?: string;
|
||||
}) {
|
||||
const { log, lines } = useLogs();
|
||||
const [mods, setMods] = useState<MixFetchModule | null>(null);
|
||||
const [connected, setConnected] = useState(false);
|
||||
const [busy, setBusy] = useState(false);
|
||||
// The tunnel is one-shot per page (smolmix OnceLock + single worker), so once
|
||||
// it has been torn down, Connect stays disabled until a reload.
|
||||
const [terminated, setTerminated] = useState(false);
|
||||
const [status, setStatus] = useState<Status>({ text: 'Not started', colour: 'gray' });
|
||||
const [showAdvanced, setShowAdvanced] = useState(false);
|
||||
|
||||
// Connection options.
|
||||
const [useRandomIpr, setUseRandomIpr] = useState(false);
|
||||
const [iprAddress, setIprAddress] = useState(DEFAULT_IPR);
|
||||
const [clientId, setClientId] = useState('');
|
||||
const [forceTls, setForceTls] = useState(true);
|
||||
const [disablePoisson, setDisablePoisson] = useState(false);
|
||||
const [disableCover, setDisableCover] = useState(false);
|
||||
const [debug, setDebug] = useState(true);
|
||||
const [openSurbs, setOpenSurbs] = useState(10);
|
||||
const [dataSurbs, setDataSurbs] = useState(2);
|
||||
const [primaryDns, setPrimaryDns] = useState('');
|
||||
const [fallbackDns, setFallbackDns] = useState('');
|
||||
const [dnsTimeout, setDnsTimeout] = useState('');
|
||||
const [connectTimeout, setConnectTimeout] = useState('');
|
||||
const [maxRedirects, setMaxRedirects] = useState('');
|
||||
const [storagePassphrase, setStoragePassphrase] = useState('');
|
||||
|
||||
// Generate the client id after mount (not at render) so SSG and client
|
||||
// hydration agree: Math.random at render would differ between the two.
|
||||
useEffect(() => {
|
||||
setClientId((c) => c || `${clientIdPrefix}-${Math.random().toString(36).slice(2, 8)}`);
|
||||
}, [clientIdPrefix]);
|
||||
|
||||
const optInt = (v: string): number | undefined => {
|
||||
const n = parseInt(v, 10);
|
||||
return Number.isNaN(n) ? undefined : n;
|
||||
};
|
||||
const optStr = (v: string): string | undefined => v.trim() || undefined;
|
||||
|
||||
async function connect() {
|
||||
if (!useRandomIpr && !iprAddress.trim()) {
|
||||
setStatus({ text: "IPR address required (or tick 'Use random IPR')", colour: 'red' });
|
||||
return;
|
||||
}
|
||||
setBusy(true);
|
||||
setStatus({ text: 'Connecting (building the client, connecting to the IPR exit)...', colour: 'orange' });
|
||||
log('tunnel', `Connecting (clientId=${clientId}, IPR: ${useRandomIpr ? 'auto-discover' : iprAddress.trim().slice(0, 28) + '...'}, SURBs open=${openSurbs} data=${dataSurbs})`, 'orange');
|
||||
try {
|
||||
const m = mods ?? (await loadMixFetch());
|
||||
if (!mods) setMods(m);
|
||||
// One WASM instance per browser tab, shared across demo pages by the
|
||||
// bundler. If another page already brought the tunnel up, reuse it rather
|
||||
// than calling setupMixTunnel again (which throws "already initialised").
|
||||
const existing = await m.getTunnelState().catch(() => null);
|
||||
if (existing && existing.state === 'ready') {
|
||||
log('tunnel', 'Tunnel already up from another page; reusing it (its original options apply).', 'green');
|
||||
} else {
|
||||
await m.setupMixTunnel({
|
||||
...(useRandomIpr ? {} : { preferredIpr: iprAddress.trim() }),
|
||||
clientId,
|
||||
forceTls,
|
||||
disablePoissonTraffic: disablePoisson,
|
||||
disableCoverTraffic: disableCover,
|
||||
openReplySurbs: clampSurbs(openSurbs, 1),
|
||||
dataReplySurbs: clampSurbs(dataSurbs, 0),
|
||||
primaryDns: optStr(primaryDns),
|
||||
fallbackDns: optStr(fallbackDns),
|
||||
dnsTimeoutMs: optInt(dnsTimeout),
|
||||
connectTimeoutMs: optInt(connectTimeout),
|
||||
maxRedirects: optInt(maxRedirects),
|
||||
storagePassphrase: storagePassphrase || undefined,
|
||||
debug,
|
||||
});
|
||||
log('tunnel', 'Tunnel ready', 'green');
|
||||
}
|
||||
setConnected(true);
|
||||
setStatus({ text: 'Connected', colour: 'green' });
|
||||
onReady(m.mixFetch);
|
||||
} catch (e) {
|
||||
const msg = String((e as any)?.message ?? e);
|
||||
if (/already initialised/i.test(msg)) {
|
||||
log('tunnel', 'Tunnel already initialised in this tab; reload the page if it does not connect.', 'orange');
|
||||
setStatus({ text: 'Failed (already initialised, reload)', colour: 'red' });
|
||||
} else {
|
||||
setStatus({ text: 'Failed', colour: 'red' });
|
||||
log('tunnel', `Connection failed: ${msg}`, 'red');
|
||||
log('tunnel', "Timeouts and IPR rate-limits are common. Try again, or tick 'Use random IPR' and reload.", 'orange');
|
||||
}
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function disconnect() {
|
||||
if (!mods) return;
|
||||
setBusy(true);
|
||||
log('tunnel', 'Disconnecting...');
|
||||
try {
|
||||
await mods.disconnectMixTunnel();
|
||||
log('tunnel', 'Disconnected. Reload the page to reconnect.', 'green');
|
||||
setStatus({ text: 'Disconnected (reload to reconnect)', colour: 'gray' });
|
||||
} catch (e) {
|
||||
log('tunnel', `Disconnect failed: ${e}`, 'red');
|
||||
setStatus({ text: 'Disconnected after error (reload to reconnect)', colour: 'red' });
|
||||
} finally {
|
||||
// The tunnel is one-shot per page: smolmix uses a OnceLock and the package
|
||||
// owns one worker, so there is no fresh-client path without a reload. Keep
|
||||
// Connect disabled and say so rather than failing on a second connect.
|
||||
setConnected(false);
|
||||
setTerminated(true);
|
||||
setBusy(false);
|
||||
onDisconnect?.();
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={box}>
|
||||
<div style={legend}>Mixnet tunnel</div>
|
||||
<div style={row}>
|
||||
<label style={{ ...sub, display: 'flex', gap: 6, alignItems: 'center' }}>
|
||||
<input type="checkbox" checked={useRandomIpr} onChange={(e) => setUseRandomIpr(e.target.checked)} disabled={connected || busy} />
|
||||
Use random IPR
|
||||
</label>
|
||||
<input
|
||||
style={input}
|
||||
value={iprAddress}
|
||||
onChange={(e) => setIprAddress(e.target.value)}
|
||||
placeholder="<nym-address of IPR exit node>"
|
||||
disabled={useRandomIpr || connected || busy}
|
||||
/>
|
||||
</div>
|
||||
<div style={row}>
|
||||
<Button onClick={connect} disabled={connected || busy || terminated}>{busy && !connected ? 'Connecting...' : 'Connect to mixnet'}</Button>
|
||||
<Button onClick={disconnect} disabled={!connected || busy}>Disconnect</Button>
|
||||
<StatusText status={status} />
|
||||
<button
|
||||
type="button"
|
||||
aria-expanded={showAdvanced}
|
||||
style={{ ...sub, marginLeft: 'auto', cursor: 'pointer', background: 'none', border: 'none', padding: 0, fontFamily: 'inherit', fontWeight: 'inherit', color: 'inherit' }}
|
||||
onClick={() => setShowAdvanced((v) => !v)}
|
||||
>
|
||||
{showAdvanced ? '▾ advanced' : '▸ advanced'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{showAdvanced && (
|
||||
<div style={{ ...row, flexDirection: 'column', alignItems: 'stretch', gap: 6 }}>
|
||||
<div style={row}>
|
||||
<label style={sub}>client id</label>
|
||||
<input style={input} value={clientId} onChange={(e) => setClientId(e.target.value)} disabled={connected || busy} />
|
||||
</div>
|
||||
<div style={row}>
|
||||
<label style={{ ...sub, display: 'flex', gap: 6, alignItems: 'center' }}>
|
||||
<input type="checkbox" checked={forceTls} onChange={(e) => setForceTls(e.target.checked)} disabled={connected || busy} /> forceTls (WSS to gateway)
|
||||
</label>
|
||||
<label style={{ ...sub, display: 'flex', gap: 6, alignItems: 'center' }}>
|
||||
<input type="checkbox" checked={disablePoisson} onChange={(e) => setDisablePoisson(e.target.checked)} disabled={connected || busy} /> disable Poisson
|
||||
</label>
|
||||
<label style={{ ...sub, display: 'flex', gap: 6, alignItems: 'center' }}>
|
||||
<input type="checkbox" checked={disableCover} onChange={(e) => setDisableCover(e.target.checked)} disabled={connected || busy} /> disable cover traffic
|
||||
</label>
|
||||
<label style={{ ...sub, display: 'flex', gap: 6, alignItems: 'center' }}>
|
||||
<input type="checkbox" checked={debug} onChange={(e) => setDebug(e.target.checked)} disabled={connected || busy} /> verbose console logs
|
||||
</label>
|
||||
</div>
|
||||
<div style={row}>
|
||||
<label style={sub}>open SURBs</label>
|
||||
<input style={num} type="number" min={1} value={openSurbs} onChange={(e) => setOpenSurbs(+e.target.value)} disabled={connected || busy} />
|
||||
<label style={sub}>data SURBs</label>
|
||||
<input style={num} type="number" min={0} value={dataSurbs} onChange={(e) => setDataSurbs(+e.target.value)} disabled={connected || busy} />
|
||||
</div>
|
||||
<div style={row}>
|
||||
<label style={sub}>primary DNS</label>
|
||||
<input style={input} value={primaryDns} onChange={(e) => setPrimaryDns(e.target.value)} placeholder="8.8.8.8:53" disabled={connected || busy} />
|
||||
<label style={sub}>fallback DNS</label>
|
||||
<input style={input} value={fallbackDns} onChange={(e) => setFallbackDns(e.target.value)} placeholder="1.1.1.1:53" disabled={connected || busy} />
|
||||
</div>
|
||||
<div style={row}>
|
||||
<label style={sub}>dns timeout ms</label>
|
||||
<input style={num} value={dnsTimeout} onChange={(e) => setDnsTimeout(e.target.value)} placeholder="30000" disabled={connected || busy} />
|
||||
<label style={sub}>connect timeout ms</label>
|
||||
<input style={num} value={connectTimeout} onChange={(e) => setConnectTimeout(e.target.value)} placeholder="60000" disabled={connected || busy} />
|
||||
<label style={sub}>max redirects</label>
|
||||
<input style={num} value={maxRedirects} onChange={(e) => setMaxRedirects(e.target.value)} placeholder="5" disabled={connected || busy} />
|
||||
</div>
|
||||
<div style={row}>
|
||||
<label style={sub}>storage passphrase</label>
|
||||
<input style={input} type="password" value={storagePassphrase} onChange={(e) => setStoragePassphrase(e.target.value)} placeholder="(plaintext if empty)" disabled={connected || busy} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<LogPanel lines={lines('tunnel')} placeholder="Press Connect to bring up the tunnel." />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
// Shared mixnet + ethers helpers for demos that route HTTP through mixFetch.
|
||||
// ens has its own copies of the small helpers (it predates this file); railgun
|
||||
// uses these. Consolidate ens onto this later.
|
||||
|
||||
import { FetchRequest, JsonRpcProvider, type Networkish } from 'ethers';
|
||||
import type { MixFetchFn } from './mixTunnel';
|
||||
|
||||
export interface FlatResponse {
|
||||
status: number;
|
||||
statusText: string;
|
||||
headers: Record<string, string>;
|
||||
body: Uint8Array;
|
||||
}
|
||||
|
||||
export function headersToObj(headers: unknown): Record<string, string> {
|
||||
const out: Record<string, string> = {};
|
||||
if (!headers) return out;
|
||||
if (headers instanceof Headers || headers instanceof Map) {
|
||||
for (const [k, v] of (headers as Headers).entries()) out[k.toLowerCase()] = v;
|
||||
return out;
|
||||
}
|
||||
if (Array.isArray(headers)) {
|
||||
for (const [k, v] of headers) out[String(k).toLowerCase()] = v;
|
||||
return out;
|
||||
}
|
||||
if (typeof headers === 'object') {
|
||||
for (const [k, v] of Object.entries(headers as Record<string, string>)) out[k.toLowerCase()] = v;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export async function callMixFetch(mixFetch: MixFetchFn, url: string, init?: RequestInit): Promise<FlatResponse> {
|
||||
const res = await mixFetch(url, init || {});
|
||||
const body = new Uint8Array(await res.arrayBuffer());
|
||||
return { status: res.status, statusText: res.statusText, headers: headersToObj(res.headers), body };
|
||||
}
|
||||
|
||||
export async function decompressBody(body: Uint8Array, headers: Record<string, string>): Promise<Uint8Array> {
|
||||
if (!body || body.byteLength === 0) return body;
|
||||
const enc = (headers['content-encoding'] || '').toLowerCase().trim();
|
||||
if (!enc || enc === 'identity') return body;
|
||||
let format: 'gzip' | 'deflate' | 'deflate-raw' | null = null;
|
||||
if (enc === 'gzip' || enc === 'x-gzip') format = 'gzip';
|
||||
else if (enc === 'deflate') format = 'deflate';
|
||||
else if (enc === 'deflate-raw') format = 'deflate-raw';
|
||||
if (!format) return body;
|
||||
// body is always a plain ArrayBuffer-backed Uint8Array at runtime; the cast
|
||||
// sidesteps the TS 5.7 generic-typed-array vs BlobPart (ArrayBuffer) mismatch.
|
||||
const stream = new Blob([body as BlobPart]).stream().pipeThrough(new DecompressionStream(format));
|
||||
return new Uint8Array(await new Response(stream).arrayBuffer());
|
||||
}
|
||||
|
||||
export function stripContentEncoding(headers: Record<string, string>): Record<string, string> {
|
||||
const out = { ...headers };
|
||||
delete out['content-encoding'];
|
||||
return out;
|
||||
}
|
||||
|
||||
// The getUrl adapter ethers uses: route the request through mixFetch, decompress,
|
||||
// rename the response fields ethers expects.
|
||||
function makeGetUrl(mixFetch: MixFetchFn) {
|
||||
return async (req: FetchRequest) => {
|
||||
const raw = await callMixFetch(mixFetch, req.url, {
|
||||
method: req.method,
|
||||
headers: req.headers,
|
||||
body: (req.body ?? undefined) as BodyInit | undefined,
|
||||
});
|
||||
const body = await decompressBody(raw.body, raw.headers);
|
||||
return {
|
||||
statusCode: raw.status,
|
||||
statusMessage: raw.statusText,
|
||||
headers: stripContentEncoding(raw.headers),
|
||||
body,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
// A JsonRpcProvider whose transport is mixFetch (per-instance override).
|
||||
export function buildProvider(rpcUrl: string, mixFetch: MixFetchFn, network: Networkish): JsonRpcProvider {
|
||||
const base = new FetchRequest(rpcUrl);
|
||||
base.getUrlFunc = makeGetUrl(mixFetch);
|
||||
return new JsonRpcProvider(base, network, { staticNetwork: true });
|
||||
}
|
||||
|
||||
// Install a single global FetchRequest URL handler so EVERY ethers HTTP request
|
||||
// (including providers a library constructs internally from URL strings) routes
|
||||
// through mixFetch. Requires a single ethers instance across the bundle: the
|
||||
// `ethers$` alias in next.config.js enforces that.
|
||||
let globalRoutingInstalled = false;
|
||||
export function installGlobalMixFetchRouting(mixFetch: MixFetchFn): void {
|
||||
if (globalRoutingInstalled) return;
|
||||
FetchRequest.registerGetUrl(makeGetUrl(mixFetch));
|
||||
globalRoutingInstalled = true;
|
||||
}
|
||||
|
||||
// Retry wrapper for mixnet-routed RPC calls: the first request on a cold mixnet
|
||||
// path pays TCP-connect + TLS-handshake time, and Railgun's hardcoded timeouts
|
||||
// can fire on it; the retry finds the pool warm.
|
||||
export async function withRetry<T>(
|
||||
fn: () => Promise<T>,
|
||||
label: string,
|
||||
opts: { attempts?: number; delayMs?: number; log?: (msg: string, colour?: string) => void } = {},
|
||||
): Promise<T> {
|
||||
const { attempts = 3, delayMs = 3000, log } = opts;
|
||||
let lastErr: unknown;
|
||||
for (let i = 1; i <= attempts; i++) {
|
||||
try {
|
||||
return await fn();
|
||||
} catch (e: any) {
|
||||
lastErr = e;
|
||||
const msg = e.shortMessage || e.message || String(e);
|
||||
if (i < attempts) {
|
||||
log?.(`${label} attempt ${i}/${attempts} failed (${msg}), retrying in ${delayMs / 1000}s...`, 'orange');
|
||||
await new Promise((r) => setTimeout(r, delayMs));
|
||||
} else {
|
||||
log?.(`${label} failed after ${attempts} attempts: ${msg}`, 'red');
|
||||
}
|
||||
}
|
||||
}
|
||||
throw lastErr;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
// Shared UI primitives for the in-docs demos (ens, railgun). These live with
|
||||
// the playground today; re-exported here so the demo components import from one
|
||||
// stable place rather than reaching into ../playground. If the primitives ever
|
||||
// move to a neutral home, only this file changes.
|
||||
export * from '../../playground/ui';
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"nodes": 683,
|
||||
"nodes": 685,
|
||||
"locations": 75,
|
||||
"mixnodes": 240,
|
||||
"exit_gateways": 435
|
||||
"exit_gateways": 437
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
Tuesday, June 9th 2026, 13:20:08 UTC
|
||||
Tuesday, June 9th 2026, 16:06:04 UTC
|
||||
|
||||
@@ -4,117 +4,95 @@ Start this nym-node
|
||||
Usage: nym-node run [OPTIONS]
|
||||
|
||||
Options:
|
||||
--id <ID>
|
||||
Id of the nym-node to use [env: NYMNODE_ID=] [default: default-nym-node]
|
||||
--config-file <CONFIG_FILE>
|
||||
Path to a configuration file of this node [env: NYMNODE_CONFIG=]
|
||||
--accept-operator-terms-and-conditions
|
||||
Explicitly specify whether you agree with the terms and conditions of a nym node operator as defined at <https://nymtech.net/terms-and-conditions/operators/v1.0.0> [env:
|
||||
NYMNODE_ACCEPT_OPERATOR_TERMS=]
|
||||
--deny-init
|
||||
Forbid a new node from being initialised if configuration file for the provided specification doesn't already exist [env: NYMNODE_DENY_INIT=]
|
||||
--init-only
|
||||
If this is a brand new nym-node, specify whether it should only be initialised without actually running the subprocesses [env: NYMNODE_INIT_ONLY=]
|
||||
--local
|
||||
Flag specifying this node will be running in a local setting [env: NYMNODE_LOCAL=]
|
||||
--mode [<MODE>...]
|
||||
Specifies the current mode(s) of this nym-node [env: NYMNODE_MODE=] [possible values: mixnode, entry-gateway, exit-gateway, exit-providers-only]
|
||||
--modes <MODES>
|
||||
Specifies the current mode(s) of this nym-node as a single flag [env: NYMNODE_MODES=] [possible values: mixnode, entry-gateway, exit-gateway, exit-providers-only]
|
||||
-w, --write-changes
|
||||
If this node has been initialised before, specify whether to write any new changes to the config file [env: NYMNODE_WRITE_CONFIG_CHANGES=]
|
||||
--bonding-information-output <BONDING_INFORMATION_OUTPUT>
|
||||
Specify output file for bonding information of this nym-node, i.e. its encoded keys. NOTE: the required bonding information is still a subject to change and this argument should be
|
||||
treated only as a preview of future features [env: NYMNODE_BONDING_INFORMATION_OUTPUT=]
|
||||
-o, --output <OUTPUT>
|
||||
Specify the output format of the bonding information (`text` or `json`) [env: NYMNODE_OUTPUT=] [default: text] [possible values: text, json]
|
||||
--public-ips <PUBLIC_IPS>
|
||||
Comma separated list of public ip addresses that will be announced to the nym-api and subsequently to the clients. In nearly all circumstances, it's going to be identical to the
|
||||
address you're going to use for bonding [env: NYMNODE_PUBLIC_IPS=]
|
||||
--hostname <HOSTNAME>
|
||||
Optional hostname associated with this gateway that will be announced to the nym-api and subsequently to the clients [env: NYMNODE_HOSTNAME=]
|
||||
--location <LOCATION>
|
||||
Optional **physical** location of this node's server. Either full country name (e.g. 'Poland'), two-letter alpha2 (e.g. 'PL'), three-letter alpha3 (e.g. 'POL') or three-digit
|
||||
numeric-3 (e.g. '616') can be provided [env: NYMNODE_LOCATION=]
|
||||
--http-bind-address <HTTP_BIND_ADDRESS>
|
||||
Socket address this node will use for binding its http API. default: `[::]:8080` [env: NYMNODE_HTTP_BIND_ADDRESS=]
|
||||
--landing-page-assets-path <LANDING_PAGE_ASSETS_PATH>
|
||||
Path to assets directory of custom landing page of this node [env: NYMNODE_HTTP_LANDING_ASSETS=]
|
||||
--http-access-token <HTTP_ACCESS_TOKEN>
|
||||
An optional bearer token for accessing certain http endpoints. Currently only used for prometheus metrics [env: NYMNODE_HTTP_ACCESS_TOKEN=]
|
||||
--expose-system-info <EXPOSE_SYSTEM_INFO>
|
||||
Specify whether basic system information should be exposed. default: true [env: NYMNODE_HTTP_EXPOSE_SYSTEM_INFO=] [possible values: true, false]
|
||||
--expose-system-hardware <EXPOSE_SYSTEM_HARDWARE>
|
||||
Specify whether basic system hardware information should be exposed. default: true [env: NYMNODE_HTTP_EXPOSE_SYSTEM_HARDWARE=] [possible values: true, false]
|
||||
--expose-crypto-hardware <EXPOSE_CRYPTO_HARDWARE>
|
||||
Specify whether detailed system crypto hardware information should be exposed. default: true [env: NYMNODE_HTTP_EXPOSE_CRYPTO_HARDWARE=] [possible values: true, false]
|
||||
--nyxd-urls <NYXD_URLS>
|
||||
Addresses to nyxd chain endpoint which the node will use for chain interactions [env: NYMNODE_NYXD=]
|
||||
--nyxd-websocket-url <NYXD_WEBSOCKET_URL>
|
||||
Url to the websocket endpoint of a nyx validator, for example `wss://rpc.nymtech.net/websocket`. It is used for subscribing to new block events [env: NYMNODE_NYXD_WEBSOCKET=]
|
||||
--mixnet-bind-address <MIXNET_BIND_ADDRESS>
|
||||
Address this node will bind to for listening for mixnet packets default: `[::]:1789` [env: NYMNODE_MIXNET_BIND_ADDRESS=]
|
||||
--mixnet-announce-port <MIXNET_ANNOUNCE_PORT>
|
||||
If applicable, custom port announced in the self-described API that other clients and nodes will use. Useful when the node is behind a proxy [env: NYMNODE_MIXNET_ANNOUNCE_PORT=]
|
||||
--nym-api-urls <NYM_API_URLS>
|
||||
Addresses to nym APIs from which the node gets the view of the network [env: NYMNODE_NYM_APIS=]
|
||||
--enable-console-logging <ENABLE_CONSOLE_LOGGING>
|
||||
Specify whether running statistics of this node should be logged to the console [env: NYMNODE_ENABLE_CONSOLE_LOGGING=] [possible values: true, false]
|
||||
--wireguard-enabled <WIREGUARD_ENABLED>
|
||||
Specifies whether the wireguard service is enabled on this node [env: NYMNODE_WG_ENABLED=] [possible values: true, false]
|
||||
--wireguard-bind-address <WIREGUARD_BIND_ADDRESS>
|
||||
Socket address this node will use for binding its wireguard interface. default: `[::]:51822` [env: NYMNODE_WG_BIND_ADDRESS=]
|
||||
--wireguard-tunnel-announced-port <WIREGUARD_TUNNEL_ANNOUNCED_PORT>
|
||||
Tunnel port announced to external clients wishing to connect to the wireguard interface. Useful in the instances where the node is behind a proxy [env: NYMNODE_WG_ANNOUNCED_PORT=]
|
||||
--wireguard-private-network-prefix <WIREGUARD_PRIVATE_NETWORK_PREFIX>
|
||||
The prefix denoting the maximum number of the clients that can be connected via Wireguard. The maximum value for IPv4 is 32 and for IPv6 is 128 [env:
|
||||
NYMNODE_WG_PRIVATE_NETWORK_PREFIX=]
|
||||
--wireguard-userspace <WIREGUARD_USERSPACE>
|
||||
Use userspace implementation of WireGuard (wireguard-go) instead of kernel module. Useful in containerized environments without kernel WireGuard support [env: NYMNODE_WG_USERSPACE=]
|
||||
[possible values: true, false]
|
||||
--verloc-bind-address <VERLOC_BIND_ADDRESS>
|
||||
Socket address this node will use for binding its verloc API. default: `[::]:1790` [env: NYMNODE_VERLOC_BIND_ADDRESS=]
|
||||
--verloc-announce-port <VERLOC_ANNOUNCE_PORT>
|
||||
If applicable, custom port announced in the self-described API that other clients and nodes will use. Useful when the node is behind a proxy [env: NYMNODE_VERLOC_ANNOUNCE_PORT=]
|
||||
--entry-bind-address <ENTRY_BIND_ADDRESS>
|
||||
Socket address this node will use for binding its client websocket API. default: `[::]:9000` [env: NYMNODE_ENTRY_BIND_ADDRESS=]
|
||||
--announce-ws-port <ANNOUNCE_WS_PORT>
|
||||
Custom announced port for listening for websocket client traffic. If unspecified, the value from the `bind_address` will be used instead [env: NYMNODE_ENTRY_ANNOUNCE_WS_PORT=]
|
||||
--announce-wss-port <ANNOUNCE_WSS_PORT>
|
||||
If applicable, announced port for listening for secure websocket client traffic [env: NYMNODE_ENTRY_ANNOUNCE_WSS_PORT=]
|
||||
--enforce-zk-nyms <ENFORCE_ZK_NYMS>
|
||||
Indicates whether this gateway is accepting only coconut credentials for accessing the mixnet or if it also accepts non-paying clients [env: NYMNODE_ENFORCE_ZK_NYMS=] [possible
|
||||
values: true, false]
|
||||
--mnemonic <MNEMONIC>
|
||||
Custom cosmos wallet mnemonic used for zk-nym redemption. If no value is provided, a fresh mnemonic is going to be generated [env: NYMNODE_MNEMONIC=]
|
||||
--upgrade-mode-attestation-url <UPGRADE_MODE_ATTESTATION_URL>
|
||||
Endpoint to query to retrieve current upgrade mode attestation. This argument should never be set outside testnets and local networks [env: NYMNODE_UPGRADE_MODE_ATTESTATION_URL=]
|
||||
--upgrade-mode-attester-public-key <UPGRADE_MODE_ATTESTER_PUBLIC_KEY>
|
||||
Expected public key of the entity signing the published attestation. This argument should never be set outside testnets and local networks [env:
|
||||
NYMNODE_UPGRADE_MODE_ATTESTER_PUBKEY=]
|
||||
--upstream-exit-policy-url <UPSTREAM_EXIT_POLICY_URL>
|
||||
Specifies the url for an upstream source of the exit policy used by this node [env: NYMNODE_UPSTREAM_EXIT_POLICY=]
|
||||
--open-proxy <OPEN_PROXY>
|
||||
Specifies whether this exit node should run in 'open-proxy' mode and thus would attempt to resolve **ANY** request it receives [env: NYMNODE_OPEN_PROXY=] [possible values: true,
|
||||
false]
|
||||
--nr-allow-local-ips <NR_ALLOW_LOCAL_IPS>
|
||||
Allow the network requester to forward traffic to non-globally-routable addresses. Intended for local development, private-network deployments, and testnet scenarios. Not
|
||||
recommended on production exit gateway unless you know what you're doing [env: NYMNODE_NR_ALLOW_LOCAL_IPS=] [possible values: true, false]
|
||||
--ipr-allow-local-ips <IPR_ALLOW_LOCAL_IPS>
|
||||
Allow the IP packet router to forward traffic to non-globally-routable addresses. Intended for local development, private-network deployments, and testnet scenarios. Not recommended
|
||||
on production exit gateway unless you know what you're doing [env: NYMNODE_IPR_ALLOW_LOCAL_IPS=] [possible values: true, false]
|
||||
--lp-control-bind-address <LP_CONTROL_BIND_ADDRESS>
|
||||
Bind address for the TCP LP control traffic. default: `[::]:41264` [env: NYMNODE_LP_CONTROL_BIND_ADDRESS=]
|
||||
--lp-control-announce-port <LP_CONTROL_ANNOUNCE_PORT>
|
||||
Custom announced port for listening for the TCP LP control traffic. If unspecified, the value from the `lp_control_bind_address` will be used instead [env:
|
||||
NYMNODE_LP_CONTROL_ANNOUNCE_PORT=]
|
||||
--lp-data-bind-address <LP_DATA_BIND_ADDRESS>
|
||||
Bind address for the UDP LP data traffic. default: `[::]:51264` [env: NYMNODE_LP_DATA_BIND_ADDRESS=]
|
||||
--lp-data-announce-port <LP_DATA_ANNOUNCE_PORT>
|
||||
Custom announced port for listening for the UDP LP data traffic. If unspecified, the value from the `lp_data_bind_address` will be used instead [env: NYMNODE_LP_DATA_ANNOUNCE_PORT=]
|
||||
--lp-use-mock-ecash <LP_USE_MOCK_ECASH>
|
||||
Use mock ecash manager for LP testing. WARNING: Only use this for local testing! Never enable in production. When enabled, the LP listener will accept any credential without
|
||||
blockchain verification [env: NYMNODE_LP_USE_MOCK_ECASH=] [possible values: true, false]
|
||||
-h, --help
|
||||
Print help
|
||||
--id <ID> Id of the nym-node to use [env: NYMNODE_ID=] [default: default-nym-node]
|
||||
--config-file <CONFIG_FILE> Path to a configuration file of this node [env: NYMNODE_CONFIG=]
|
||||
--accept-operator-terms-and-conditions Explicitly specify whether you agree with the terms and conditions of a nym node operator as defined at
|
||||
<https://nymtech.net/terms-and-conditions/operators/v1.0.0> [env: NYMNODE_ACCEPT_OPERATOR_TERMS=]
|
||||
--deny-init Forbid a new node from being initialised if configuration file for the provided specification doesn't already exist
|
||||
[env: NYMNODE_DENY_INIT=]
|
||||
--init-only If this is a brand new nym-node, specify whether it should only be initialised without actually running the subprocesses
|
||||
[env: NYMNODE_INIT_ONLY=]
|
||||
--local Flag specifying this node will be running in a local setting [env: NYMNODE_LOCAL=]
|
||||
--mode [<MODE>...] Specifies the current mode(s) of this nym-node [env: NYMNODE_MODE=] [possible values: mixnode, entry-gateway,
|
||||
exit-gateway, exit-providers-only]
|
||||
--modes <MODES> Specifies the current mode(s) of this nym-node as a single flag [env: NYMNODE_MODES=] [possible values: mixnode,
|
||||
entry-gateway, exit-gateway, exit-providers-only]
|
||||
-w, --write-changes If this node has been initialised before, specify whether to write any new changes to the config file [env:
|
||||
NYMNODE_WRITE_CONFIG_CHANGES=]
|
||||
--bonding-information-output <BONDING_INFORMATION_OUTPUT> Specify output file for bonding information of this nym-node, i.e. its encoded keys. NOTE: the required bonding
|
||||
information is still a subject to change and this argument should be treated only as a preview of future features [env:
|
||||
NYMNODE_BONDING_INFORMATION_OUTPUT=]
|
||||
-o, --output <OUTPUT> Specify the output format of the bonding information (`text` or `json`) [env: NYMNODE_OUTPUT=] [default: text] [possible
|
||||
values: text, json]
|
||||
--public-ips <PUBLIC_IPS> Comma separated list of public ip addresses that will be announced to the nym-api and subsequently to the clients. In
|
||||
nearly all circumstances, it's going to be identical to the address you're going to use for bonding [env:
|
||||
NYMNODE_PUBLIC_IPS=]
|
||||
--hostname <HOSTNAME> Optional hostname associated with this gateway that will be announced to the nym-api and subsequently to the clients
|
||||
[env: NYMNODE_HOSTNAME=]
|
||||
--location <LOCATION> Optional **physical** location of this node's server. Either full country name (e.g. 'Poland'), two-letter alpha2 (e.g.
|
||||
'PL'), three-letter alpha3 (e.g. 'POL') or three-digit numeric-3 (e.g. '616') can be provided [env: NYMNODE_LOCATION=]
|
||||
--http-bind-address <HTTP_BIND_ADDRESS> Socket address this node will use for binding its http API. default: `[::]:8080` [env: NYMNODE_HTTP_BIND_ADDRESS=]
|
||||
--landing-page-assets-path <LANDING_PAGE_ASSETS_PATH> Path to assets directory of custom landing page of this node [env: NYMNODE_HTTP_LANDING_ASSETS=]
|
||||
--http-access-token <HTTP_ACCESS_TOKEN> An optional bearer token for accessing certain http endpoints. Currently only used for prometheus metrics [env:
|
||||
NYMNODE_HTTP_ACCESS_TOKEN=]
|
||||
--expose-system-info <EXPOSE_SYSTEM_INFO> Specify whether basic system information should be exposed. default: true [env: NYMNODE_HTTP_EXPOSE_SYSTEM_INFO=]
|
||||
[possible values: true, false]
|
||||
--expose-system-hardware <EXPOSE_SYSTEM_HARDWARE> Specify whether basic system hardware information should be exposed. default: true [env:
|
||||
NYMNODE_HTTP_EXPOSE_SYSTEM_HARDWARE=] [possible values: true, false]
|
||||
--expose-crypto-hardware <EXPOSE_CRYPTO_HARDWARE> Specify whether detailed system crypto hardware information should be exposed. default: true [env:
|
||||
NYMNODE_HTTP_EXPOSE_CRYPTO_HARDWARE=] [possible values: true, false]
|
||||
--nyxd-urls <NYXD_URLS> Addresses to nyxd chain endpoint which the node will use for chain interactions [env: NYMNODE_NYXD=]
|
||||
--nyxd-websocket-url <NYXD_WEBSOCKET_URL> Url to the websocket endpoint of a nyx validator, for example `wss://rpc.nymtech.net/websocket`. It is used for
|
||||
subscribing to new block events [env: NYMNODE_NYXD_WEBSOCKET=]
|
||||
--mixnet-bind-address <MIXNET_BIND_ADDRESS> Address this node will bind to for listening for mixnet packets default: `[::]:1789` [env: NYMNODE_MIXNET_BIND_ADDRESS=]
|
||||
--mixnet-announce-port <MIXNET_ANNOUNCE_PORT> If applicable, custom port announced in the self-described API that other clients and nodes will use. Useful when the
|
||||
node is behind a proxy [env: NYMNODE_MIXNET_ANNOUNCE_PORT=]
|
||||
--nym-api-urls <NYM_API_URLS> Addresses to nym APIs from which the node gets the view of the network [env: NYMNODE_NYM_APIS=]
|
||||
--enable-console-logging <ENABLE_CONSOLE_LOGGING> Specify whether running statistics of this node should be logged to the console [env: NYMNODE_ENABLE_CONSOLE_LOGGING=]
|
||||
[possible values: true, false]
|
||||
--wireguard-enabled <WIREGUARD_ENABLED> Specifies whether the wireguard service is enabled on this node [env: NYMNODE_WG_ENABLED=] [possible values: true,
|
||||
false]
|
||||
--wireguard-bind-address <WIREGUARD_BIND_ADDRESS> Socket address this node will use for binding its wireguard interface. default: `[::]:51822` [env:
|
||||
NYMNODE_WG_BIND_ADDRESS=]
|
||||
--wireguard-tunnel-announced-port <WIREGUARD_TUNNEL_ANNOUNCED_PORT> Tunnel port announced to external clients wishing to connect to the wireguard interface. Useful in the instances where
|
||||
the node is behind a proxy [env: NYMNODE_WG_ANNOUNCED_PORT=]
|
||||
--wireguard-private-network-prefix <WIREGUARD_PRIVATE_NETWORK_PREFIX> The prefix denoting the maximum number of the clients that can be connected via Wireguard. The maximum value for IPv4 is
|
||||
32 and for IPv6 is 128 [env: NYMNODE_WG_PRIVATE_NETWORK_PREFIX=]
|
||||
--wireguard-userspace <WIREGUARD_USERSPACE> Use userspace implementation of WireGuard (wireguard-go) instead of kernel module. Useful in containerized environments
|
||||
without kernel WireGuard support [env: NYMNODE_WG_USERSPACE=] [possible values: true, false]
|
||||
--verloc-bind-address <VERLOC_BIND_ADDRESS> Socket address this node will use for binding its verloc API. default: `[::]:1790` [env: NYMNODE_VERLOC_BIND_ADDRESS=]
|
||||
--verloc-announce-port <VERLOC_ANNOUNCE_PORT> If applicable, custom port announced in the self-described API that other clients and nodes will use. Useful when the
|
||||
node is behind a proxy [env: NYMNODE_VERLOC_ANNOUNCE_PORT=]
|
||||
--entry-bind-address <ENTRY_BIND_ADDRESS> Socket address this node will use for binding its client websocket API. default: `[::]:9000` [env:
|
||||
NYMNODE_ENTRY_BIND_ADDRESS=]
|
||||
--announce-ws-port <ANNOUNCE_WS_PORT> Custom announced port for listening for websocket client traffic. If unspecified, the value from the `bind_address` will
|
||||
be used instead [env: NYMNODE_ENTRY_ANNOUNCE_WS_PORT=]
|
||||
--announce-wss-port <ANNOUNCE_WSS_PORT> If applicable, announced port for listening for secure websocket client traffic [env: NYMNODE_ENTRY_ANNOUNCE_WSS_PORT=]
|
||||
--enforce-zk-nyms <ENFORCE_ZK_NYMS> Indicates whether this gateway is accepting only coconut credentials for accessing the mixnet or if it also accepts
|
||||
non-paying clients [env: NYMNODE_ENFORCE_ZK_NYMS=] [possible values: true, false]
|
||||
--mnemonic <MNEMONIC> Custom cosmos wallet mnemonic used for zk-nym redemption. If no value is provided, a fresh mnemonic is going to be
|
||||
generated [env: NYMNODE_MNEMONIC=]
|
||||
--upgrade-mode-attestation-url <UPGRADE_MODE_ATTESTATION_URL> Endpoint to query to retrieve current upgrade mode attestation. This argument should never be set outside testnets and
|
||||
local networks [env: NYMNODE_UPGRADE_MODE_ATTESTATION_URL=]
|
||||
--upgrade-mode-attester-public-key <UPGRADE_MODE_ATTESTER_PUBLIC_KEY> Expected public key of the entity signing the published attestation. This argument should never be set outside testnets
|
||||
and local networks [env: NYMNODE_UPGRADE_MODE_ATTESTER_PUBKEY=]
|
||||
--upstream-exit-policy-url <UPSTREAM_EXIT_POLICY_URL> Specifies the url for an upstream source of the exit policy used by this node [env: NYMNODE_UPSTREAM_EXIT_POLICY=]
|
||||
--open-proxy <OPEN_PROXY> Specifies whether this exit node should run in 'open-proxy' mode and thus would attempt to resolve **ANY** request it
|
||||
receives [env: NYMNODE_OPEN_PROXY=] [possible values: true, false]
|
||||
--nr-allow-local-ips <NR_ALLOW_LOCAL_IPS> Allow the network requester to forward traffic to non-globally-routable addresses. Intended for local development,
|
||||
private-network deployments, and testnet scenarios. Not recommended on production exit gateway unless you know what
|
||||
you're doing [env: NYMNODE_NR_ALLOW_LOCAL_IPS=] [possible values: true, false]
|
||||
--ipr-allow-local-ips <IPR_ALLOW_LOCAL_IPS> Allow the IP packet router to forward traffic to non-globally-routable addresses. Intended for local development,
|
||||
private-network deployments, and testnet scenarios. Not recommended on production exit gateway unless you know what
|
||||
you're doing [env: NYMNODE_IPR_ALLOW_LOCAL_IPS=] [possible values: true, false]
|
||||
--lp-control-bind-address <LP_CONTROL_BIND_ADDRESS> Bind address for the TCP LP control traffic. default: `[::]:41264` [env: NYMNODE_LP_CONTROL_BIND_ADDRESS=]
|
||||
--lp-control-announce-port <LP_CONTROL_ANNOUNCE_PORT> Custom announced port for listening for the TCP LP control traffic. If unspecified, the value from the
|
||||
`lp_control_bind_address` will be used instead [env: NYMNODE_LP_CONTROL_ANNOUNCE_PORT=]
|
||||
--lp-data-bind-address <LP_DATA_BIND_ADDRESS> Bind address for the UDP LP data traffic. default: `[::]:51264` [env: NYMNODE_LP_DATA_BIND_ADDRESS=]
|
||||
--lp-data-announce-port <LP_DATA_ANNOUNCE_PORT> Custom announced port for listening for the UDP LP data traffic. If unspecified, the value from the
|
||||
`lp_data_bind_address` will be used instead [env: NYMNODE_LP_DATA_ANNOUNCE_PORT=]
|
||||
--lp-use-mock-ecash <LP_USE_MOCK_ECASH> Use mock ecash manager for LP testing. WARNING: Only use this for local testing! Never enable in production. When
|
||||
enabled, the LP listener will accept any credential without blockchain verification [env: NYMNODE_LP_USE_MOCK_ECASH=]
|
||||
[possible values: true, false]
|
||||
-h, --help Print help
|
||||
```
|
||||
|
||||
@@ -34,6 +34,48 @@ nextra.webpack = (config, options) => {
|
||||
// }),
|
||||
// );
|
||||
|
||||
// --- Railgun demo: browser polyfills for the @railgun-community SDK ---
|
||||
// Railgun pulls libp2p / pouchdb / crypto transitively and expects Node-stdlib
|
||||
// globals that webpack 5 no longer auto-polyfills. Client build only; the SSR
|
||||
// build resolves these natively. Mirrors wasm/railgun-demo/webpack.config.js.
|
||||
if (!options.isServer) {
|
||||
newConfig.resolve.fallback = {
|
||||
...newConfig.resolve.fallback,
|
||||
buffer: require.resolve("buffer/"),
|
||||
crypto: require.resolve("crypto-browserify"),
|
||||
http: require.resolve("stream-http"),
|
||||
https: require.resolve("https-browserify"),
|
||||
stream: require.resolve("stream-browserify"),
|
||||
url: require.resolve("url/"),
|
||||
vm: require.resolve("vm-browserify"),
|
||||
zlib: require.resolve("browserify-zlib"),
|
||||
};
|
||||
// Force single instances of ethers and shared-models. ethers: so Railgun's
|
||||
// global FetchRequest.registerGetUrl shares static state with our import.
|
||||
// shared-models: so our `NETWORK_CONFIG[...].poi = undefined` POI sidestep
|
||||
// mutates the SAME object the engine's loadProvider reads (otherwise an
|
||||
// ESM/CJS split gives two copies and the POI gate still fires).
|
||||
newConfig.resolve.alias = {
|
||||
...newConfig.resolve.alias,
|
||||
ethers$: require.resolve("ethers"),
|
||||
"@railgun-community/shared-models$": require.resolve("@railgun-community/shared-models"),
|
||||
};
|
||||
newConfig.plugins.push(
|
||||
new options.webpack.ProvidePlugin({
|
||||
Buffer: ["buffer", "Buffer"],
|
||||
process: "process/browser",
|
||||
}),
|
||||
);
|
||||
// Railgun ships its zk-SNARK circuits as async WASM.
|
||||
newConfig.experiments = { ...newConfig.experiments, asyncWebAssembly: true };
|
||||
}
|
||||
// Silence "Critical dependency" warnings from Railgun's GraphQL subgraph plumbing.
|
||||
newConfig.ignoreWarnings = [
|
||||
...(newConfig.ignoreWarnings || []),
|
||||
{ module: /@graphql-mesh/, message: /Critical dependency/ },
|
||||
{ module: /@graphql-tools\/url-loader/, message: /Critical dependency/ },
|
||||
];
|
||||
|
||||
return newConfig;
|
||||
};
|
||||
|
||||
@@ -1484,7 +1526,7 @@ const config = {
|
||||
form-action 'self';
|
||||
frame-ancestors 'none';
|
||||
upgrade-insecure-requests;
|
||||
connect-src 'self' wss://nym-node-cli.devrel.nymte.ch:9001 https://github.com *.vercel.app *.nymtech.net *.nymvpn.com *.nymte.ch *.nyx.network *.nym.com https://nym.com nymvpn.com https://nymvpn.com *.nymtech.cc;
|
||||
connect-src 'self' wss://* wss://nym-node-cli.devrel.nymte.ch:9001 https://github.com *.vercel.app *.nymtech.net *.nymvpn.com *.nymte.ch *.nyx.network *.nym.com https://nym.com nymvpn.com https://nymvpn.com *.nymtech.cc https://ipinfo.io;
|
||||
frame-src 'self' https://vercel.live *.vercel.app *.nym.com https://nym.com;
|
||||
worker-src 'self' blob: https://vercel.live *.vercel.app *.nym.com https://nym.com;
|
||||
`;
|
||||
|
||||
@@ -44,12 +44,16 @@
|
||||
"@nymproject/mix-tunnel": "^0.1.0",
|
||||
"@nymproject/mix-websocket": "^0.1.0",
|
||||
"@nymproject/sdk-full-fat": ">=1.5.1-rc.0 || ^1.4.1",
|
||||
"@railgun-community/shared-models": "7.5.0",
|
||||
"@railgun-community/wallet": "10.4.0",
|
||||
"@redocly/cli": "^1.25.15",
|
||||
"@types/mdx": "^2.0.13",
|
||||
"chain-registry": "^1.19.0",
|
||||
"cosmjs-types": "^0.9.0",
|
||||
"ethers": "^6.13.1",
|
||||
"framer-motion": "^12.34.5",
|
||||
"lucide-react": "^0.438.0",
|
||||
"memory-level": "^1.0.0",
|
||||
"next": "15.5.10",
|
||||
"nextra": "2",
|
||||
"nextra-theme-docs": "2",
|
||||
@@ -65,6 +69,15 @@
|
||||
"@types/react": "^18.3.26",
|
||||
"@types/react-dom": "^18.3.7",
|
||||
"copy-webpack-plugin": "^11.0.0",
|
||||
"browserify-zlib": "^0.2.0",
|
||||
"buffer": "^6.0.3",
|
||||
"crypto-browserify": "^3.12.0",
|
||||
"https-browserify": "^1.0.0",
|
||||
"process": "^0.11.10",
|
||||
"stream-browserify": "^3.0.0",
|
||||
"stream-http": "^3.2.0",
|
||||
"url": "^0.11.4",
|
||||
"vm-browserify": "^1.1.2",
|
||||
"eslint": "8.46.0",
|
||||
"eslint-config-next": "13.4.13",
|
||||
"next-sitemap": "4.2.3",
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"title": "TypeScript"
|
||||
},
|
||||
"playground": "Playground (embedded clients)",
|
||||
"demos": "Demos",
|
||||
"mix-tunnel": "mix-tunnel (shared tunnel)",
|
||||
"mix-fetch": "mix-fetch (HTTPS requests)",
|
||||
"mix-dns": "mix-dns (DNS resolution)",
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"ens": "ENS over the mixnet",
|
||||
"railgun": "Shielding ETH with Railgun"
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
---
|
||||
title: "Demo: ENS resolution over the Nym mixnet"
|
||||
description: "Resolve an ENS name to its address and IPFS contenthash, then fetch the site, with every JSON-RPC and gateway request routed through the mixnet via mix-fetch. Shows the ethers-to-mixFetch adapter."
|
||||
schemaType: "TechArticle"
|
||||
section: "Developers"
|
||||
lastUpdated: "2026-06-09"
|
||||
---
|
||||
|
||||
import dynamic from 'next/dynamic'
|
||||
|
||||
export const EnsDemo = dynamic(
|
||||
() => import('../../../components/demos/ens/EnsDemo').then((m) => m.EnsDemo),
|
||||
{ ssr: false },
|
||||
)
|
||||
|
||||
import { NetworkTabCallout } from '../../../components/demos/shared/NetworkTabCallout'
|
||||
import { MixnetGlossary } from '../../../components/demos/shared/MixnetGlossary'
|
||||
|
||||
# ENS over the mixnet
|
||||
|
||||
A normal ENS lookup (name to address to IPFS website) built with
|
||||
[ethers.js](https://docs.ethers.org/v6/), except every network request goes
|
||||
through the Nym mixnet instead of leaving over your normal connection. The
|
||||
Ethereum RPC node and the IPFS gateway see the [IPR](/network/infrastructure/exit-services#ip-packet-router)
|
||||
exit's IP, not yours, and your ISP cannot see which names or sites you reach.
|
||||
The trade-off is latency: every packet takes a multi-relay path, so requests are
|
||||
slower than a direct route.
|
||||
|
||||
## How it works
|
||||
|
||||
The whole integration is one adapter. ethers v6 exposes
|
||||
`FetchRequest.getUrlFunc` as a settable property, so you replace its HTTP
|
||||
transport with a function that calls [`mixFetch`](/developers/mix-fetch). To
|
||||
ethers it looks like an ordinary fetch; to the mixnet, ethers looks like any
|
||||
other caller.
|
||||
|
||||
```ts
|
||||
import { JsonRpcProvider, FetchRequest } from 'ethers';
|
||||
import { mixFetch } from '@nymproject/mix-fetch';
|
||||
|
||||
function buildProvider(rpcUrl: string): JsonRpcProvider {
|
||||
const base = new FetchRequest(rpcUrl);
|
||||
|
||||
// Route every JSON-RPC call through mixFetch, renaming the response
|
||||
// fields ethers expects (status -> statusCode, statusText -> statusMessage).
|
||||
base.getUrlFunc = async (req) => {
|
||||
const res = await mixFetch(req.url, {
|
||||
method: req.method,
|
||||
headers: req.headers,
|
||||
body: req.body ?? undefined,
|
||||
});
|
||||
return {
|
||||
statusCode: res.status,
|
||||
statusMessage: res.statusText,
|
||||
headers: Object.fromEntries(res.headers),
|
||||
body: new Uint8Array(await res.arrayBuffer()),
|
||||
};
|
||||
};
|
||||
|
||||
// staticNetwork skips the eth_chainId probe: one mixnet round trip saved.
|
||||
return new JsonRpcProvider(base, 'mainnet', { staticNetwork: true });
|
||||
}
|
||||
```
|
||||
|
||||
One caveat: browser `fetch` decompresses gzip transparently and `mixFetch` does
|
||||
not, so the demo adds a `DecompressionStream` step after each response (Cloudflare
|
||||
gzips RPC replies). The full version with decompression and per-call logging is in
|
||||
[`components/demos/ens/lib.ts`](https://github.com/nymtech/nym/tree/develop/documentation/docs/components/demos/ens).
|
||||
|
||||
On npm: [`@nymproject/mix-fetch`](https://www.npmjs.com/package/@nymproject/mix-fetch) and [`ethers`](https://www.npmjs.com/package/ethers).
|
||||
|
||||
The lookup itself is three steps, each an Ethereum call or HTTPS GET over the same
|
||||
tunnel:
|
||||
|
||||
1. **Resolve address.** Two `eth_call`s: the ENS Registry's `resolver(node)`
|
||||
returns the resolver contract for the name, then `resolver.addr(node)` returns
|
||||
the Ethereum address. `node` is the namehash (recursive keccak256 over the
|
||||
labels).
|
||||
2. **Get contenthash.** One more `eth_call`: `resolver.contenthash(node)`. ethers
|
||||
decodes the EIP-1577 multicodec bytes to a URI; this demo handles `ipfs://`.
|
||||
3. **Fetch from IPFS.** A plain HTTPS GET to a gateway with the CID as a subdomain
|
||||
or path label. CIDv0 (`Qm...`) is re-encoded as CIDv1 (`bafy...`) for subdomain
|
||||
gateways, since DNS is case-insensitive.
|
||||
|
||||
## Try it
|
||||
|
||||
Connect to bring the tunnel up (a default IPR exit is pinned; tick **Use random
|
||||
IPR** for auto-discovery), click **Verify IP routing** to confirm traffic exits
|
||||
through Nym, then run the three steps.
|
||||
|
||||
<NetworkTabCallout />
|
||||
|
||||
<EnsDemo />
|
||||
|
||||
## What to expect
|
||||
|
||||
- **The first request is the slow one.** Connecting builds the mixnet client and
|
||||
handshakes with the IPR; no TCP or TLS yet. The first request to a host then
|
||||
runs a TCP and TLS handshake carried as IP packets over the mixnet (several
|
||||
sequential round trips). smolmix keeps that connection warm and reuses it, so
|
||||
later requests to the same host are much quicker. A long pause is handshakes in
|
||||
flight, not a hang.
|
||||
- **You will not see the tunnelled requests in DevTools.** The RPC and IPFS
|
||||
requests never touch the browser's `fetch`. They leave the worker as encrypted
|
||||
packets over a single WebSocket to the entry gateway, which is the one
|
||||
connection the Network tab shows. The exception is **Verify IP routing**, which
|
||||
deliberately makes one direct clearnet call to ipinfo.io for comparison.
|
||||
- **Rate limiting.** Public IPFS gateways and Ethereum RPCs rate-limit shared IP
|
||||
addresses. If requests start failing with 403, 429, or connection errors, the
|
||||
exit IP is likely flagged: tick **Use random IPR** and reload for a fresh exit.
|
||||
|
||||
## Glossary
|
||||
|
||||
<MixnetGlossary />
|
||||
@@ -0,0 +1,111 @@
|
||||
---
|
||||
title: "Demo: Shielding testnet ETH into Railgun over the Nym mixnet"
|
||||
description: "Shield testnet ETH into a Railgun private note with every Ethereum RPC call routed through the Nym mixnet. Shows the global ethers-to-mixFetch routing that covers a whole SDK."
|
||||
schemaType: "TechArticle"
|
||||
section: "Developers"
|
||||
lastUpdated: "2026-06-09"
|
||||
---
|
||||
|
||||
import dynamic from 'next/dynamic'
|
||||
|
||||
export const RailgunDemo = dynamic(
|
||||
() => import('../../../components/demos/railgun/RailgunDemo').then((m) => m.RailgunDemo),
|
||||
{ ssr: false },
|
||||
)
|
||||
|
||||
import { NetworkTabCallout } from '../../../components/demos/shared/NetworkTabCallout'
|
||||
import { MixnetGlossary } from '../../../components/demos/shared/MixnetGlossary'
|
||||
|
||||
# Shielding testnet ETH into Railgun over the mixnet
|
||||
|
||||
**Nym** hides the network layer: every Ethereum RPC
|
||||
call goes through the mixnet via [`mixFetch`](/developers/mix-fetch), so the RPC
|
||||
node and your ISP cannot link you to the query, and **Railgun** hides the
|
||||
application layer, as shielded notes break the on-chain link between sender,
|
||||
receiver, and amount. This demo covers just the **shield** step on Sepolia:
|
||||
depositing testnet ETH into a private note. It does not do private transfers or
|
||||
unshielding.
|
||||
|
||||
## What you can do here
|
||||
|
||||
This page is interactive. You bring up a mixnet tunnel, derive a Railgun wallet,
|
||||
and broadcast a **real shield transaction** on the Sepolia testnet, with every
|
||||
Ethereum RPC call routed through the mixnet. The shield lands on chain (you can
|
||||
open it on Etherscan), but the IP that submitted it is the Nym exit's, not yours.
|
||||
|
||||
The entire integration is a single ethers shim (shown below). Because the
|
||||
Railgun engine talks to the chain through the `ethers` library, routing through
|
||||
`mixFetch` is enough to put a whole privacy SDK behind the mixnet. The same
|
||||
pattern drops into any `ethers`-based app or library.
|
||||
|
||||
## How it works
|
||||
|
||||
The [ENS demo](/developers/demos/ens) swapped one provider's transport, but Railgun
|
||||
constructs its own providers internally, so routing only our provider would leak
|
||||
the engine's RPC to clearnet. Instead this demo installs a **global** `ethers`
|
||||
transport: `FetchRequest.registerGetUrl` routes every ethers HTTP call in the
|
||||
page through `mixFetch`, including the ones the Railgun engine makes.
|
||||
|
||||
```ts
|
||||
import { FetchRequest } from 'ethers';
|
||||
import { mixFetch } from '@nymproject/mix-fetch';
|
||||
|
||||
// Every ethers HTTP request in the process now goes through the mixnet.
|
||||
FetchRequest.registerGetUrl(async (req) => {
|
||||
const res = await mixFetch(req.url, {
|
||||
method: req.method,
|
||||
headers: req.headers,
|
||||
body: req.body ?? undefined,
|
||||
});
|
||||
return {
|
||||
statusCode: res.status,
|
||||
statusMessage: res.statusText,
|
||||
headers: Object.fromEntries(res.headers),
|
||||
body: new Uint8Array(await res.arrayBuffer()),
|
||||
};
|
||||
});
|
||||
```
|
||||
|
||||
`registerGetUrl` is global static state on the `FetchRequest` class, so this only
|
||||
works if `ethers` is a **single instance** across your bundle. If your app and
|
||||
Railgun resolve to different ethers copies, the handler installs on one and the
|
||||
engine uses the other. Pin the exact version Railgun peer-depends on.
|
||||
|
||||
On npm: [`@nymproject/mix-fetch`](https://www.npmjs.com/package/@nymproject/mix-fetch), [`@railgun-community/wallet`](https://www.npmjs.com/package/@railgun-community/wallet), and [`ethers`](https://www.npmjs.com/package/ethers).
|
||||
|
||||
Shielding is a four-step flow, all over the mixnet: sign a shield key, estimate
|
||||
gas, populate the transaction, then sign and broadcast. The broadcast that lands
|
||||
on Sepolia is observable on Etherscan, but the IP that submitted it stays hidden.
|
||||
|
||||
## Try it
|
||||
|
||||
<div className="nx-mt-4" />
|
||||
|
||||
The demo auto-loads a funded Sepolia testnet wallet. Connect the tunnel (the
|
||||
Railgun address derives once the engine is up), check the balance, then shield a
|
||||
small amount. If the wallet is low, top it up at a
|
||||
[Sepolia faucet](https://sepoliafaucet.com/) using the public address shown.
|
||||
|
||||
**Sepolia testnet only.** The wallet holds test ETH and the mnemonic is
|
||||
stored in plain browser storage. Never paste a mainnet mnemonic.
|
||||
|
||||
<NetworkTabCallout />
|
||||
|
||||
<RailgunDemo />
|
||||
|
||||
## What to expect
|
||||
|
||||
- **Engine init is the slow part.** `loadProvider` hits Sepolia over a cold
|
||||
mixnet route, which can exceed Railgun's internal timeout on the first try; the
|
||||
demo retries and the second attempt finds the connection pool warm.
|
||||
- **Shielding makes several RPC calls** (gas estimate, fee data, broadcast,
|
||||
receipt), each a mixnet round trip. The broadcast step retries idempotently:
|
||||
the tx hash is fixed before broadcasting, so a dropped response can be re-sent
|
||||
or detected as already-on-chain.
|
||||
- **Rate limiting.** If RPC calls start failing with 403/429 or connection
|
||||
errors, the exit IP is flagged: disconnect, tick **Use random IPR**, reload,
|
||||
and reconnect for a fresh exit.
|
||||
|
||||
## Glossary
|
||||
|
||||
<MixnetGlossary />
|
||||
@@ -9,6 +9,7 @@ lastUpdated: "2026-06-09"
|
||||
import { Callout } from 'nextra/components'
|
||||
import { MixPlayground } from '../../components/playground/MixPlayground'
|
||||
import { MessagingDemo } from '../../components/playground/messaging-section'
|
||||
import { NetworkTabCallout } from '../../components/demos/shared/NetworkTabCallout'
|
||||
|
||||
# Mixnet playground
|
||||
|
||||
@@ -19,18 +20,11 @@ This playground runs Nym's browser TypeScript packages against the live mixnet.
|
||||
|
||||
Some sections send the same request over the tunnel and over the clearnet, so you can compare the two.
|
||||
|
||||
On npm: [`@nymproject/mix-fetch`](https://www.npmjs.com/package/@nymproject/mix-fetch), [`@nymproject/mix-dns`](https://www.npmjs.com/package/@nymproject/mix-dns), [`@nymproject/mix-tunnel`](https://www.npmjs.com/package/@nymproject/mix-tunnel), [`@nymproject/mix-websocket`](https://www.npmjs.com/package/@nymproject/mix-websocket), and [`@nymproject/sdk`](https://www.npmjs.com/package/@nymproject/sdk).
|
||||
|
||||
## HTTPS / DNS / WebSockets
|
||||
|
||||
<Callout type="info">
|
||||
**Watch the Network tab.** Open DevTools → Network before you connect. Once
|
||||
`setupMixTunnel` reports ready, every tunnel operation here (`mixFetch`,
|
||||
`mixDNS`, `MixWebSocket`) adds **no new request** to that tab: it is multiplexed
|
||||
inside the single WebSocket to the entry gateway. Only the *clearnet* comparison
|
||||
buttons add rows. (Setup also fetches the network topology over HTTPS and
|
||||
refreshes it periodically, so those nym-api calls and the gateway WebSocket are
|
||||
the only clearnet requests you will see.) Your real traffic never leaves the
|
||||
browser as an identifiable, per-destination request.
|
||||
</Callout>
|
||||
<NetworkTabCallout />
|
||||
|
||||
<Callout type="info">
|
||||
Everything here runs client-side over the live Nym mixnet. The first
|
||||
|
||||
Generated
+5499
-98
File diff suppressed because it is too large
Load Diff
@@ -7,3 +7,16 @@ allowBuilds:
|
||||
sharp: false
|
||||
tiny-secp256k1: false
|
||||
unrs-resolver: false
|
||||
# Pulled in by the railgun demo deps. Native crypto / ws speedups (the browser
|
||||
# bundle uses the JS implementations) and postinstall scripts the webpack-built
|
||||
# demo does not need; skip their builds (and avoid native-compile failures in CI).
|
||||
"@railgun-community/wallet": false
|
||||
blake-hash: false
|
||||
bufferutil: false
|
||||
es5-ext: false
|
||||
keccak: false
|
||||
secp256k1: false
|
||||
utf-8-validate: false
|
||||
web3: false
|
||||
web3-bzz: false
|
||||
web3-shh: false
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
@version: 1.20.4
|
||||
@generated: 2026-06-09
|
||||
@pages: 163
|
||||
@pages: 165
|
||||
@source: https://github.com/nymtech/nym/tree/develop/documentation/docs
|
||||
|
||||
---
|
||||
@@ -3671,16 +3671,9 @@ This playground runs Nym's browser TypeScript packages against the live mixnet.
|
||||
|
||||
Some sections send the same request over the tunnel and over the clearnet, so you can compare the two.
|
||||
|
||||
## HTTPS / DNS / WebSockets
|
||||
On npm: [`@nymproject/mix-fetch`](https://www.npmjs.com/package/@nymproject/mix-fetch), [`@nymproject/mix-dns`](https://www.npmjs.com/package/@nymproject/mix-dns), [`@nymproject/mix-tunnel`](https://www.npmjs.com/package/@nymproject/mix-tunnel), [`@nymproject/mix-websocket`](https://www.npmjs.com/package/@nymproject/mix-websocket), and [`@nymproject/sdk`](https://www.npmjs.com/package/@nymproject/sdk).
|
||||
|
||||
**Watch the Network tab.** Open DevTools → Network before you connect. Once
|
||||
`setupMixTunnel` reports ready, every tunnel operation here (`mixFetch`,
|
||||
`mixDNS`, `MixWebSocket`) adds **no new request** to that tab: it is multiplexed
|
||||
inside the single WebSocket to the entry gateway. Only the *clearnet* comparison
|
||||
buttons add rows. (Setup also fetches the network topology over HTTPS and
|
||||
refreshes it periodically, so those nym-api calls and the gateway WebSocket are
|
||||
the only clearnet requests you will see.) Your real traffic never leaves the
|
||||
browser as an identifiable, per-destination request.
|
||||
## HTTPS / DNS / WebSockets
|
||||
|
||||
Everything here runs client-side over the live Nym mixnet. The first
|
||||
`setupMixTunnel` is slow (a few seconds): it loads the WebAssembly client,
|
||||
@@ -3702,6 +3695,200 @@ For the API of each package, see
|
||||
[mix-tunnel](/developers/mix-tunnel), [mix-fetch](/developers/mix-fetch),
|
||||
[mix-dns](/developers/mix-dns), and [mix-websocket](/developers/mix-websocket).
|
||||
|
||||
---
|
||||
title: Demo: ENS resolution over the Nym mixnet
|
||||
description: Resolve an ENS name to its address and IPFS contenthash, then fetch the site, with every JSON-RPC and gateway request routed through the mixnet via mix-fetch. Shows the ethers-to-mixFetch adapter.
|
||||
url: https://nym.com/docs/developers/demos/ens
|
||||
---
|
||||
|
||||
export const EnsDemo = dynamic(
|
||||
() => import('../../../components/demos/ens/EnsDemo').then((m) => m.EnsDemo),
|
||||
{ ssr: false },
|
||||
)
|
||||
|
||||
# ENS over the mixnet
|
||||
|
||||
A normal ENS lookup (name to address to IPFS website) built with
|
||||
[ethers.js](https://docs.ethers.org/v6/), except every network request goes
|
||||
through the Nym mixnet instead of leaving over your normal connection. The
|
||||
Ethereum RPC node and the IPFS gateway see the [IPR](/network/infrastructure/exit-services#ip-packet-router)
|
||||
exit's IP, not yours, and your ISP cannot see which names or sites you reach.
|
||||
The trade-off is latency: every packet takes a multi-relay path, so requests are
|
||||
slower than a direct route.
|
||||
|
||||
## How it works
|
||||
|
||||
The whole integration is one adapter. ethers v6 exposes
|
||||
`FetchRequest.getUrlFunc` as a settable property, so you replace its HTTP
|
||||
transport with a function that calls [`mixFetch`](/developers/mix-fetch). To
|
||||
ethers it looks like an ordinary fetch; to the mixnet, ethers looks like any
|
||||
other caller.
|
||||
|
||||
```ts
|
||||
|
||||
function buildProvider(rpcUrl: string): JsonRpcProvider {
|
||||
const base = new FetchRequest(rpcUrl);
|
||||
|
||||
// Route every JSON-RPC call through mixFetch, renaming the response
|
||||
// fields ethers expects (status -> statusCode, statusText -> statusMessage).
|
||||
base.getUrlFunc = async (req) => {
|
||||
const res = await mixFetch(req.url, {
|
||||
method: req.method,
|
||||
headers: req.headers,
|
||||
body: req.body ?? undefined,
|
||||
});
|
||||
return {
|
||||
statusCode: res.status,
|
||||
statusMessage: res.statusText,
|
||||
headers: Object.fromEntries(res.headers),
|
||||
body: new Uint8Array(await res.arrayBuffer()),
|
||||
};
|
||||
};
|
||||
|
||||
// staticNetwork skips the eth_chainId probe: one mixnet round trip saved.
|
||||
return new JsonRpcProvider(base, 'mainnet', { staticNetwork: true });
|
||||
}
|
||||
```
|
||||
|
||||
One caveat: browser `fetch` decompresses gzip transparently and `mixFetch` does
|
||||
not, so the demo adds a `DecompressionStream` step after each response (Cloudflare
|
||||
gzips RPC replies). The full version with decompression and per-call logging is in
|
||||
[`components/demos/ens/lib.ts`](https://github.com/nymtech/nym/tree/develop/documentation/docs/components/demos/ens).
|
||||
|
||||
On npm: [`@nymproject/mix-fetch`](https://www.npmjs.com/package/@nymproject/mix-fetch) and [`ethers`](https://www.npmjs.com/package/ethers).
|
||||
|
||||
The lookup itself is three steps, each an Ethereum call or HTTPS GET over the same
|
||||
tunnel:
|
||||
|
||||
1. **Resolve address.** Two `eth_call`s: the ENS Registry's `resolver(node)`
|
||||
returns the resolver contract for the name, then `resolver.addr(node)` returns
|
||||
the Ethereum address. `node` is the namehash (recursive keccak256 over the
|
||||
labels).
|
||||
2. **Get contenthash.** One more `eth_call`: `resolver.contenthash(node)`. ethers
|
||||
decodes the EIP-1577 multicodec bytes to a URI; this demo handles `ipfs://`.
|
||||
3. **Fetch from IPFS.** A plain HTTPS GET to a gateway with the CID as a subdomain
|
||||
or path label. CIDv0 (`Qm...`) is re-encoded as CIDv1 (`bafy...`) for subdomain
|
||||
gateways, since DNS is case-insensitive.
|
||||
|
||||
## Try it
|
||||
|
||||
Connect to bring the tunnel up (a default IPR exit is pinned; tick **Use random
|
||||
IPR** for auto-discovery), click **Verify IP routing** to confirm traffic exits
|
||||
through Nym, then run the three steps.
|
||||
|
||||
## What to expect
|
||||
|
||||
- **The first request is the slow one.** Connecting builds the mixnet client and
|
||||
handshakes with the IPR; no TCP or TLS yet. The first request to a host then
|
||||
runs a TCP and TLS handshake carried as IP packets over the mixnet (several
|
||||
sequential round trips). smolmix keeps that connection warm and reuses it, so
|
||||
later requests to the same host are much quicker. A long pause is handshakes in
|
||||
flight, not a hang.
|
||||
- **You will not see the tunnelled requests in DevTools.** The RPC and IPFS
|
||||
requests never touch the browser's `fetch`. They leave the worker as encrypted
|
||||
packets over a single WebSocket to the entry gateway, which is the one
|
||||
connection the Network tab shows. The exception is **Verify IP routing**, which
|
||||
deliberately makes one direct clearnet call to ipinfo.io for comparison.
|
||||
- **Rate limiting.** Public IPFS gateways and Ethereum RPCs rate-limit shared IP
|
||||
addresses. If requests start failing with 403, 429, or connection errors, the
|
||||
exit IP is likely flagged: tick **Use random IPR** and reload for a fresh exit.
|
||||
|
||||
## Glossary
|
||||
|
||||
---
|
||||
title: Demo: Railgun private payments over the Nym mixnet
|
||||
description: Shield testnet ETH into a Railgun private note with every Ethereum RPC call routed through the Nym mixnet. Shows the global ethers-to-mixFetch routing that covers a whole SDK.
|
||||
url: https://nym.com/docs/developers/demos/railgun
|
||||
---
|
||||
|
||||
export const RailgunDemo = dynamic(
|
||||
() => import('../../../components/demos/railgun/RailgunDemo').then((m) => m.RailgunDemo),
|
||||
{ ssr: false },
|
||||
)
|
||||
|
||||
# Railgun over the mixnet
|
||||
|
||||
Two privacy layers stacked. **Nym** hides the network layer: every Ethereum RPC
|
||||
call goes through the mixnet via [`mixFetch`](/developers/mix-fetch), so the RPC
|
||||
node and your ISP cannot link you to the query. **Railgun** hides the
|
||||
application layer: shielded notes break the on-chain link between sender,
|
||||
receiver, and amount. This demo shields testnet ETH on Sepolia.
|
||||
|
||||
## What you can do here
|
||||
|
||||
This page is interactive. You bring up a mixnet tunnel, derive a Railgun wallet,
|
||||
and broadcast a **real shield transaction** on the Sepolia testnet, with every
|
||||
Ethereum RPC call routed through the mixnet. The shield lands on chain (you can
|
||||
open it on Etherscan), but the IP that submitted it is the Nym exit's, not yours.
|
||||
|
||||
The entire integration is a single ethers shim (shown below). Because the
|
||||
Railgun engine talks to the chain through ethers, routing ethers through
|
||||
`mixFetch` is enough to put a whole privacy SDK behind the mixnet. The same
|
||||
pattern drops into any ethers-based app or library.
|
||||
|
||||
## How it works
|
||||
|
||||
The [ENS demo](/developers/demos/ens) swapped one provider's transport. Railgun
|
||||
constructs its own providers internally, so routing only our provider would leak
|
||||
the engine's RPC to clearnet. Instead this demo installs a **global** ethers
|
||||
transport: `FetchRequest.registerGetUrl` routes every ethers HTTP call in the
|
||||
page through `mixFetch`, including the ones the Railgun engine makes.
|
||||
|
||||
```ts
|
||||
|
||||
// Every ethers HTTP request in the process now goes through the mixnet.
|
||||
FetchRequest.registerGetUrl(async (req) => {
|
||||
const res = await mixFetch(req.url, {
|
||||
method: req.method,
|
||||
headers: req.headers,
|
||||
body: req.body ?? undefined,
|
||||
});
|
||||
return {
|
||||
statusCode: res.status,
|
||||
statusMessage: res.statusText,
|
||||
headers: Object.fromEntries(res.headers),
|
||||
body: new Uint8Array(await res.arrayBuffer()),
|
||||
};
|
||||
});
|
||||
```
|
||||
|
||||
`registerGetUrl` is global static state on the `FetchRequest` class, so this only
|
||||
works if ethers is a **single instance** across your bundle. If your app and
|
||||
Railgun resolve to different ethers copies, the handler installs on one and the
|
||||
engine uses the other. Pin the exact ethers version Railgun peer-depends on (this
|
||||
demo aliases ethers to one instance in the bundler).
|
||||
|
||||
On npm: [`@nymproject/mix-fetch`](https://www.npmjs.com/package/@nymproject/mix-fetch), [`@railgun-community/wallet`](https://www.npmjs.com/package/@railgun-community/wallet), and [`ethers`](https://www.npmjs.com/package/ethers).
|
||||
|
||||
Shielding is a four-step flow, all over the mixnet: sign a shield key, estimate
|
||||
gas, populate the transaction, then sign and broadcast. The broadcast that lands
|
||||
on Sepolia is observable on Etherscan, but the IP that submitted it stays hidden.
|
||||
|
||||
## Try it
|
||||
|
||||
The demo auto-loads a funded Sepolia testnet wallet. Connect the tunnel (the
|
||||
Railgun address derives once the engine is up), check the balance, then shield a
|
||||
small amount. If the wallet is low, top it up at a
|
||||
[Sepolia faucet](https://sepoliafaucet.com/) using the public address shown.
|
||||
|
||||
**Sepolia testnet only.** The wallet holds only test ETH and the mnemonic is
|
||||
stored in plain browser storage. Never paste a mainnet mnemonic.
|
||||
|
||||
## What to expect
|
||||
|
||||
- **Engine init is the slow part.** `loadProvider` hits Sepolia over a cold
|
||||
mixnet route, which can exceed Railgun's internal timeout on the first try; the
|
||||
demo retries and the second attempt finds the connection pool warm.
|
||||
- **Shielding makes several RPC calls** (gas estimate, fee data, broadcast,
|
||||
receipt), each a mixnet round trip. The broadcast step retries idempotently:
|
||||
the tx hash is fixed before broadcasting, so a dropped response can be re-sent
|
||||
or detected as already-on-chain.
|
||||
- **Rate limiting.** If RPC calls start failing with 403/429 or connection
|
||||
errors, the exit IP is flagged: disconnect, tick **Use random IPR**, reload,
|
||||
and reconnect for a fresh exit.
|
||||
|
||||
## Glossary
|
||||
|
||||
---
|
||||
title: mix-tunnel: Shared Mixnet Tunnel for the Browser
|
||||
description: TypeScript package that owns the shared Nym mixnet tunnel in the browser. The base layer for mix-fetch, mix-dns, and mix-websocket.
|
||||
@@ -4089,7 +4276,7 @@ Consequences:
|
||||
|
||||
- **One WASM module, smaller bundle.** v1's Go runtime accounted for ~6 MB of the full-fat bundle; v2 drops it.
|
||||
- **Shared infrastructure with `mix-dns` and `mix-websocket`.** The same tunnel handles all three.
|
||||
- **IPR exit policies apply.** What was allowed by your previous Network Requester may not be allowed by your default IPR; pin one with `preferredIpr` if you need a specific exit policy.
|
||||
- **IPR exit policies apply.** What was allowed by your previous Network Requester may not be allowed by your default IPR, which applies the current [Nym exit policy](https://nymtech.net/.wellknown/network-requester/exit-policy.txt).
|
||||
|
||||
---
|
||||
title: mix-dns: Hostname Resolution Over the Nym Mixnet
|
||||
@@ -10096,7 +10283,7 @@ The outcome of [NIP-10: Nym Exit Policy Update – Opening Ports for Dash, SIP,
|
||||
- [New documentation logic to `network/`, `developers/` and `apis/`](https://github.com/nymtech/nym/pull/6494) according to the [diataxis.fr](https://diataxis.fr/) framework, making basis for adding Lewes Protocol documentation. Additionally developer docs now include tutorials for the [Rust SDK modules](/developers/rust), and documentation on the `stream` [Mixnet module](/developers/rust/mixnet). See the pages at:
|
||||
|
||||
- [Network docs](/network)
|
||||
- [Developer docs](/developers/integrations)
|
||||
- [Developer docs](/developers)
|
||||
- [APIs docs](/apis/introduction)
|
||||
|
||||
#### Update Nym exit policy
|
||||
@@ -10748,7 +10935,7 @@ cargo Profile: release
|
||||
|
||||
- [Typescript SDK 1.4.1](https://github.com/nymtech/nym/pull/6146): This PR is a new release of the Typescript SDK, `mixFetch` and `WASM` client. It also removes the Harbour Master client from `mixFetch`, replacing it with the Nym API's described endpoint for nym-nodes
|
||||
|
||||
- [Overhauled **developer integrations** pages](/developers/integrations) explaining the different restrictions for the different SDK options on offer
|
||||
- [Overhauled **developer integrations** pages](/developers) explaining the different restrictions for the different SDK options on offer
|
||||
|
||||
- [Fixed `mixFetch` and `WASM Client` playground + examples](/developers/typescript): new versions of the Typescript SDK and `mixFetch` have been published, examples and live playground have been updated accordingly
|
||||
|
||||
@@ -20358,9 +20545,109 @@ ansible-playbook deploy.yml
|
||||
|
||||
###### 2. Bond
|
||||
|
||||
Anyone having acces to your account mnemonic can take all your funds and manage manage your node, be careful where you store it!
|
||||
|
||||
Bonding can be managed via two playbooks:
|
||||
|
||||
1. `bond.yml`: an interactive way, requiring operator to use own wallet (desktop or CLI)
|
||||
2. `auto-bond.yml`: automatic bonding flow requiring operator to prepare `nodes.csv` and have `nym-cli` installed
|
||||
|
||||
<Tabs items={[
|
||||
<code>bond.yml</code>,
|
||||
<code>auto-bond.yml</code>,
|
||||
]} defaultIndex="1">
|
||||
|
||||
A playbook to *interactively* register your nodes to Nym network by bonding it to Nyx blockchain accounts.
|
||||
This playbook is intercative as it prompts user for data from Nym wallet to sign a message. It will run roles on one inventory entry at a time by default.
|
||||
|
||||
**Requirements**
|
||||
|
||||
- Nym Wallet or `nym-cli` to be used as a CLI wallet
|
||||
- An account per each node
|
||||
- At least 101 NYM per account
|
||||
|
||||
**Usage**
|
||||
|
||||
1. Sign in to the wallet per node
|
||||
2. Follow steps in `Bond` section
|
||||
3. Run the playbook on a side and follow the prompts
|
||||
|
||||
```sh
|
||||
cd playbooks
|
||||
ansible-playbook bond.yml
|
||||
```
|
||||
|
||||
Your nodes are bonded and will show in the network in the next epoch (max 60min).
|
||||
|
||||
A playbook to *automatically* register your nodes to Nym Network by bonding it to Nyx blockchain accounts.
|
||||
This automatic flow is slightly harder to setup in the beginning and it's recommended for operators bonding many nodes, as the initial work is worth it by saving the time of bonding a node at a time.
|
||||
|
||||
**Requirements**
|
||||
|
||||
- Installed [`nym-cli`](/developers/tools/nym-cli)
|
||||
- Python3
|
||||
- Nym repository with directory `scripts/nym-node-setup/auto-bond/`, containing:
|
||||
- Python program [`auto_bond_all.py`](https://github.com/nymtech/nym/tree/develop/scripts/nym-node-setup/auto-bond/auto_bond_all.py)
|
||||
- `nodes.csv.example` with correct data
|
||||
|
||||
**Usage**
|
||||
|
||||
1. Copy `nodes.csv.example` to your prefered location without `.example` suffix
|
||||
2. Fill correctly the csv columns for each node you want to bond:
|
||||
- `inventory_node_id`: Your Ansible inventory node ID (ie `node1`)
|
||||
- `hostname`: same like in your `playbooks/inventory/all` (without `https://` !)
|
||||
- `ip`: same like `ansible_host` value in your Ansible inventory
|
||||
- `account`: Nyx account to bond this node with
|
||||
- `mnemonic`: Your nyx acount mnemonic
|
||||
- `identity_key`: node identity key - the easiest way to get it is to navigate to `playbooks/` and run:
|
||||
|
||||
```sh
|
||||
ansible all -i inventory/all -a "/root/nym-binaries/nym-node bonding-information"
|
||||
```
|
||||
|
||||
- `amount`: Amount to bond in `uNYM` (1 NYM = 1 000 000 uNYM), Make sure to leave extra 1 NYM (1 000 000 uNYM) for fees
|
||||
- `operator_cost`: [Operator cost](/operators/tokenomics/mixnet-rewards#rewards-distribution) in `uNYM` (1 NYM = 1 000 000 uNYM)
|
||||
|
||||
3. Save the csv
|
||||
4. Run `auto_bond_all.py` with all needed arguments.
|
||||
|
||||
- To see help menu:
|
||||
|
||||
```sh
|
||||
python3 ./auto_bond_all.py --help
|
||||
```
|
||||
|
||||
- To test your paths run with `--dry-run`
|
||||
|
||||
- Argument usage:
|
||||
|
||||
```sh
|
||||
--ansible-repo ANSIBLE_REPO
|
||||
Path to ansible playbooks directory (contains auto-bond.yml and inventory/)
|
||||
--cli-dir CLI_DIR Directory containing the nym-cli binary
|
||||
--dry-run Print commands without executing
|
||||
```
|
||||
|
||||
- Example (note that the `nodes.csv` has no flag as it's a required argument):
|
||||
|
||||
```sh
|
||||
python ./auto_bond_all.py \
|
||||
--ansible-repo ~/admin/nym-nodes/nym-nodes-ansible/playbooks \
|
||||
--cli-dir ~/repos/nymtech/nym/target/release \
|
||||
~/admin/nym-nodes/nodes.csv
|
||||
```
|
||||
|
||||
5. Your nodes should be bonded and come up in the next epoch (max 60min)
|
||||
|
||||
**Additional scripts**
|
||||
|
||||
Your `nodes.csv` can be used for other operations:
|
||||
- [`show_balances.py`](https://github.com/nymtech/nym/tree/develop/scripts/nym-node-setup/auto-bond/auto_bond_all.py): Shows all accounts balances if provided with Nyx accounts (`account` column)
|
||||
- [`unbond_all.py`](https://github.com/nymtech/nym/tree/develop/scripts/nym-node-setup/auto-bond/unbond_all.py): Unbond all nodes in the csv if provided with mnemonics (`mnemonic` column)
|
||||
|
||||
A playbook to interactively register your node to Nym network by bonding it to Nyx blockchain account.
|
||||
|
||||
This playbook is intercative as it prompts user for data from Nym wallet to sign a message. It will run roles on one inventory entry at a time by default.
|
||||
This playbook is interactive as it prompts user for data from Nym wallet to sign a message. It will run roles on one inventory entry at a time by default.
|
||||
|
||||
```sh
|
||||
cd playbooks
|
||||
@@ -20463,6 +20750,20 @@ ansible-playbook <PLAYBOOK>.yml --list-tags
|
||||
ansible-playbook deploy.yml --list-tags
|
||||
```
|
||||
|
||||
###### Arbitrary command output
|
||||
|
||||
You can use ansible to read a `STDOUT` from any command, using this logic:
|
||||
```sh
|
||||
ansible all -i inventory/all -a "<COMMAND>>"
|
||||
|
||||
# for example to get all node ID keys
|
||||
ansible all -i inventory/all -a "/root/nym-binaries/nym-node bonding-information"
|
||||
```
|
||||
|
||||
- Note that the command gets also run, be mindful what you executing
|
||||
|
||||
- This logic can be combined with the arguments above, for example to limit the range of nodes
|
||||
|
||||
###### nocows
|
||||
|
||||
Yes, by default there is a cow printed under each task, you can turn it off by opening `playbooks/ansible.cfg` and un-commenting the `nocows` line:
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
- **Developers can only access Mixnet mode** via the SDKs. dVPN mode is exclusively provided by the NymVPN product, which can be downloaded and purchased at [nym.com](https://nym.com). There is no SDK for dVPN mode.
|
||||
- The primary developer SDK is the **Rust SDK** (`nym-sdk` crate). The TypeScript SDK wraps the Rust SDK via WebAssembly.
|
||||
- For Rust integrations, prefer the **mixnet** module for simple send/receive, **stream** for TCP-like `AsyncRead`/`AsyncWrite` connections, and **client_pool** for bursty workloads. The **tcp_proxy** module is unmaintained; recommend stream instead.
|
||||
- For browser/TypeScript proxy integrations, the **mix-*** packages share one tunnel via **`@nymproject/mix-tunnel`**: **`mix-fetch`** (HTTP/S), **`mix-dns`** (DNS), **`mix-websocket`** (WS/WSS). For raw end-to-end messaging where you control both ends, use the **TypeScript SDK** (`@nymproject/sdk`).
|
||||
- Node operators should use the **`nym-node`** binary. Legacy `nym-mixnode` and `nym-gateway` binaries are deprecated and no longer supported.
|
||||
- When referring to token collateral for nodes, use **"bonding"** not "staking". Nodes operate in **modes** (mixnode, entry gateway, exit gateway), not "roles".
|
||||
- The Nym blockchain is called **Nyx** and runs on Cosmos SDK. The native token is **NYM**.
|
||||
@@ -23,7 +24,7 @@
|
||||
|
||||
## Developers
|
||||
|
||||
- [Integration Overview](https://nym.com/docs/developers/integrations): Start here for building on Nym: choosing an SDK, integration patterns
|
||||
- [Developer Overview](https://nym.com/docs/developers): Start here for building on Nym: runtime (native vs browser) and approach (end-to-end vs proxy) map onto the right crate or package
|
||||
- [Rust SDK Overview](https://nym.com/docs/developers/rust): Module overview and quick-start for the Rust SDK
|
||||
- [Rust SDK Installation](https://nym.com/docs/developers/rust/importing): Adding nym-sdk to your Cargo.toml
|
||||
- [Mixnet Module](https://nym.com/docs/developers/rust/mixnet): Send and receive Sphinx-encrypted messages
|
||||
@@ -35,8 +36,15 @@
|
||||
- [Client Pool Tutorial](https://nym.com/docs/developers/rust/client-pool/tutorial): Handle bursty traffic with pooled clients
|
||||
- [TcpProxy Module](https://nym.com/docs/developers/rust/tcpproxy): Tunnel existing TCP services through the mixnet (unmaintained; use stream instead)
|
||||
- [smolmix](https://nym.com/docs/developers/smolmix): TCP/UDP over the mixnet via a userspace IP stack, drop-in TcpStream and UdpSocket (see examples in source)
|
||||
- [TypeScript SDK](https://nym.com/docs/developers/typescript): Browser and Node.js SDK using WebAssembly
|
||||
- [mix-fetch](https://nym.com/docs/developers/typescript/examples/mix-fetch): Drop-in fetch() replacement that routes HTTP through the mixnet
|
||||
- [TypeScript SDK](https://nym.com/docs/developers/typescript): Browser-side raw end-to-end messaging (@nymproject/sdk) and Nyx smart-contract bindings
|
||||
- [mix-tunnel](https://nym.com/docs/developers/mix-tunnel): Shared browser mixnet tunnel; the base layer for mix-fetch, mix-dns, and mix-websocket
|
||||
- [mix-fetch](https://nym.com/docs/developers/mix-fetch): Drop-in fetch() replacement routing HTTP(S) through the mixnet via an IPR exit
|
||||
- [mix-dns](https://nym.com/docs/developers/mix-dns): Hostname-to-IP resolution through the mixnet (UDP DNS via an IPR exit)
|
||||
- [mix-websocket](https://nym.com/docs/developers/mix-websocket): WebSocket-like class for WS and WSS over the mixnet
|
||||
- [mix-* Architecture](https://nym.com/docs/developers/mix-architecture): How the shared browser tunnel, Web Worker, Comlink boundary, and smoltcp+rustls stack are wired
|
||||
- [Playground](https://nym.com/docs/developers/playground): Interactive browser playground driving the mix-* packages and the raw messaging SDK against the live mixnet
|
||||
- [ENS Demo](https://nym.com/docs/developers/demos/ens): Resolve ENS names and fetch IPFS sites over the mixnet via an ethers-to-mixFetch shim
|
||||
- [Railgun Demo](https://nym.com/docs/developers/demos/railgun): Shield testnet ETH into a Railgun private note with every Ethereum RPC routed through the mixnet
|
||||
|
||||
## Operators
|
||||
|
||||
|
||||
@@ -395,8 +395,8 @@ impl From<nym_node_requests::api::v1::node::models::AnnouncePorts> for AnnounceP
|
||||
}
|
||||
}
|
||||
|
||||
impl From<nym_node_requests::api::v1::node::models::AuxiliaryDetailsV1> for AuxiliaryDetailsV1 {
|
||||
fn from(value: nym_node_requests::api::v1::node::models::AuxiliaryDetailsV1) -> Self {
|
||||
impl From<nym_node_requests::api::v1::node::models::AuxiliaryDetails> for AuxiliaryDetailsV1 {
|
||||
fn from(value: nym_node_requests::api::v1::node::models::AuxiliaryDetails) -> Self {
|
||||
AuxiliaryDetailsV1 {
|
||||
location: value.location,
|
||||
announce_ports: value.announce_ports.into(),
|
||||
|
||||
@@ -41,7 +41,7 @@ use utoipauto::utoipauto;
|
||||
nym_config::defaults::NymContracts,
|
||||
ContractVersionSchemaResponse,
|
||||
nym_bin_common::build_information::BinaryBuildInformationOwned,
|
||||
nym_node_requests::api::v1::node::models::AuxiliaryDetailsV1,
|
||||
nym_node_requests::api::v1::node::models::AuxiliaryDetails,
|
||||
nym_contracts_common::ContractBuildInformation
|
||||
))
|
||||
)]
|
||||
|
||||
@@ -16,7 +16,7 @@ use nym_lp::peer::{DHPublicKey, LpRemotePeer};
|
||||
use nym_lp_data::packet::version;
|
||||
use nym_network_defaults::DEFAULT_NYM_NODE_HTTP_PORT;
|
||||
use nym_node_requests::api::client::NymNodeApiClientExt;
|
||||
use nym_node_requests::api::v1::node::models::AuxiliaryDetailsV1 as NodeAuxiliaryDetails;
|
||||
use nym_node_requests::api::v1::node::models::AuxiliaryDetails as NodeAuxiliaryDetails;
|
||||
use nym_sdk::mixnet::NodeIdentity;
|
||||
use nym_sdk::mixnet::Recipient;
|
||||
use nym_validator_client::client::NymApiClientExt;
|
||||
|
||||
@@ -24,9 +24,6 @@ pub struct NetworkStats {
|
||||
|
||||
// outgoing LP control connections to nodes
|
||||
active_lp_egress_node_connections: AtomicUsize,
|
||||
|
||||
// cumulative count of ingress mixnet connections closed due to the idle timeout
|
||||
idle_closed_ingress_mixnet_connections: AtomicUsize,
|
||||
}
|
||||
|
||||
impl NetworkStats {
|
||||
@@ -40,16 +37,6 @@ impl NetworkStats {
|
||||
.fetch_sub(1, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
pub fn ingress_mixnet_idle_closed(&self) {
|
||||
self.idle_closed_ingress_mixnet_connections
|
||||
.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
pub fn idle_closed_ingress_mixnet_connections_count(&self) -> usize {
|
||||
self.idle_closed_ingress_mixnet_connections
|
||||
.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
pub fn new_ingress_websocket_client(&self) {
|
||||
self.active_ingress_websocket_connections
|
||||
.fetch_add(1, Ordering::Relaxed);
|
||||
|
||||
@@ -195,11 +195,6 @@ pub enum PrometheusMetric {
|
||||
#[strum(props(help = "The number of active egress mixnet connections"))]
|
||||
NetworkActiveEgressMixnetConnections,
|
||||
|
||||
#[strum(props(
|
||||
help = "The cumulative number of ingress mixnet connections closed due to the idle timeout"
|
||||
))]
|
||||
NetworkIdleClosedIngressMixnetConnections,
|
||||
|
||||
// # PROCESS
|
||||
#[strum(props(help = "The current number of packets being delayed"))]
|
||||
ProcessForwardHopPacketsBeingDelayed,
|
||||
@@ -223,26 +218,6 @@ pub enum PrometheusMetric {
|
||||
help = "The current number of forward hop packets stuck in channels waiting to get delivered to appropriate TCP connections"
|
||||
))]
|
||||
ProcessForwardHopPacketsPendingDelivery,
|
||||
|
||||
// # TOKIO RUNTIME
|
||||
#[strum(props(help = "Number of tokio worker threads"))]
|
||||
TokioRuntimeNumWorkers,
|
||||
|
||||
#[strum(props(help = "Currently alive (spawned, not yet completed) tokio tasks"))]
|
||||
TokioRuntimeAliveTasks,
|
||||
|
||||
#[strum(props(
|
||||
help = "Tasks waiting in the tokio global run queue (runtime scheduling pressure)"
|
||||
))]
|
||||
TokioRuntimeGlobalQueueDepth,
|
||||
|
||||
// the per-worker timing below is only exposed by tokio when the binary is built with
|
||||
// `--cfg tokio_unstable`; without that flag the handler can't sample it and these stay at 0.
|
||||
#[strum(props(help = "Fraction of worker-thread time spent busy over the last interval"))]
|
||||
TokioRuntimeBusyRatio,
|
||||
|
||||
#[strum(props(help = "Cumulative tokio worker poll count across all workers"))]
|
||||
TokioRuntimeWorkerPollCount,
|
||||
}
|
||||
|
||||
impl PrometheusMetric {
|
||||
@@ -375,9 +350,6 @@ impl PrometheusMetric {
|
||||
PrometheusMetric::NetworkActiveEgressMixnetConnections => {
|
||||
Metric::new_int_gauge(&name, help)
|
||||
}
|
||||
PrometheusMetric::NetworkIdleClosedIngressMixnetConnections => {
|
||||
Metric::new_int_gauge(&name, help)
|
||||
}
|
||||
PrometheusMetric::ProcessForwardHopPacketsBeingDelayed => {
|
||||
Metric::new_int_gauge(&name, help)
|
||||
}
|
||||
@@ -391,11 +363,6 @@ impl PrometheusMetric {
|
||||
PrometheusMetric::ProcessForwardHopPacketsPendingDelivery => {
|
||||
Metric::new_int_gauge(&name, help)
|
||||
}
|
||||
PrometheusMetric::TokioRuntimeNumWorkers => Metric::new_int_gauge(&name, help),
|
||||
PrometheusMetric::TokioRuntimeAliveTasks => Metric::new_int_gauge(&name, help),
|
||||
PrometheusMetric::TokioRuntimeGlobalQueueDepth => Metric::new_int_gauge(&name, help),
|
||||
PrometheusMetric::TokioRuntimeBusyRatio => Metric::new_float_gauge(&name, help),
|
||||
PrometheusMetric::TokioRuntimeWorkerPollCount => Metric::new_int_gauge(&name, help),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -491,7 +458,7 @@ mod tests {
|
||||
// a sanity check for anyone adding new metrics. if this test fails,
|
||||
// make sure any methods on `PrometheusMetric` enum don't need updating
|
||||
// or require custom Display impl
|
||||
assert_eq!(53, PrometheusMetric::COUNT)
|
||||
assert_eq!(47, PrometheusMetric::COUNT)
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -11,7 +11,7 @@ use crate::api::v1::ip_packet_router::models::IpPacketRouter;
|
||||
use crate::api::v1::network_requester::exit_policy::models::UsedExitPolicy;
|
||||
use crate::api::v1::network_requester::models::NetworkRequester;
|
||||
use crate::api::v1::node::models::{
|
||||
AuxiliaryDetailsV1, NodeDescription, NodeRoles, SignedHostInformation,
|
||||
AuxiliaryDetails, NodeDescription, NodeRoles, SignedHostInformation,
|
||||
};
|
||||
use crate::api::v1::node_load::models::NodeLoad;
|
||||
use crate::routes;
|
||||
@@ -55,7 +55,7 @@ pub trait NymNodeApiClientExt: ApiClient {
|
||||
self.get_json_from(routes::api::v1::roles_absolute()).await
|
||||
}
|
||||
|
||||
async fn get_auxiliary_details(&self) -> Result<AuxiliaryDetailsV1, NymNodeApiClientError> {
|
||||
async fn get_auxiliary_details(&self) -> Result<AuxiliaryDetails, NymNodeApiClientError> {
|
||||
self.get_json_from(routes::api::v1::auxiliary_absolute())
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ use std::ops::Deref;
|
||||
pub mod client;
|
||||
pub mod helpers;
|
||||
pub mod v1;
|
||||
pub mod v2;
|
||||
|
||||
#[cfg(feature = "client")]
|
||||
pub use client::Client;
|
||||
|
||||
@@ -12,7 +12,6 @@ use serde::{Deserialize, Serialize};
|
||||
use std::net::IpAddr;
|
||||
|
||||
pub use crate::api::SignedHostInformation;
|
||||
use crate::api::v2::node::models::AuxiliaryDetailsV2;
|
||||
pub use nym_bin_common::build_information::BinaryBuildInformationOwned;
|
||||
|
||||
#[derive(Clone, Default, Debug, Copy, Serialize, Deserialize, JsonSchema)]
|
||||
@@ -367,7 +366,7 @@ pub struct NodeDescription {
|
||||
/// Auxiliary details of the associated Nym Node.
|
||||
#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)]
|
||||
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
|
||||
pub struct AuxiliaryDetailsV1 {
|
||||
pub struct AuxiliaryDetails {
|
||||
/// Optional ISO 3166 alpha-2 two-letter country code of the node's **physical** location
|
||||
#[cfg_attr(feature = "openapi", schema(example = "PL", value_type = Option<String>))]
|
||||
#[schemars(with = "Option<String>")]
|
||||
@@ -384,16 +383,6 @@ pub struct AuxiliaryDetailsV1 {
|
||||
pub accepted_operator_terms_and_conditions: bool,
|
||||
}
|
||||
|
||||
impl From<AuxiliaryDetailsV2> for AuxiliaryDetailsV1 {
|
||||
fn from(v2: AuxiliaryDetailsV2) -> Self {
|
||||
Self {
|
||||
location: v2.location,
|
||||
announce_ports: v2.announce_ports,
|
||||
accepted_operator_terms_and_conditions: v2.accepted_operator_terms_and_conditions,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
pub mod node;
|
||||
@@ -1,4 +0,0 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
pub mod models;
|
||||
@@ -1,30 +0,0 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::api::v1::node::models::AnnouncePorts;
|
||||
use celes::Country;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Auxiliary details of the associated Nym Node.
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
|
||||
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
|
||||
pub struct AuxiliaryDetailsV2 {
|
||||
/// Optional ISO 3166 alpha-2 two-letter country code of the node's **physical** location
|
||||
#[cfg_attr(feature = "openapi", schema(example = "PL", value_type = Option<String>))]
|
||||
#[schemars(with = "Option<String>")]
|
||||
#[schemars(length(equal = 2))]
|
||||
pub location: Option<Country>,
|
||||
|
||||
/// On-chain address of this node
|
||||
pub address: String,
|
||||
|
||||
#[serde(default)]
|
||||
pub announce_ports: AnnouncePorts,
|
||||
|
||||
/// Specifies whether this node operator has agreed to the terms and conditions
|
||||
/// as defined at <https://nymtech.net/terms-and-conditions/operators/v1.0.0>
|
||||
// make sure to include the default deserialisation as this field hasn't existed when the struct was first created
|
||||
#[serde(default)]
|
||||
pub accepted_operator_terms_and_conditions: bool,
|
||||
}
|
||||
@@ -23,14 +23,8 @@ pub mod routes {
|
||||
|
||||
pub mod api {
|
||||
pub const V1: &str = "/v1";
|
||||
pub const V2: &str = "/v2";
|
||||
|
||||
// canonical, version-neutral Swagger UI mount
|
||||
pub const SWAGGER: &str = "/swagger";
|
||||
|
||||
absolute_route!(v1_absolute, super::API, V1);
|
||||
absolute_route!(v2_absolute, super::API, V2);
|
||||
absolute_route!(swagger_absolute, super::API, SWAGGER);
|
||||
|
||||
pub mod v1 {
|
||||
use super::*;
|
||||
@@ -158,14 +152,6 @@ pub mod routes {
|
||||
// use super::*;
|
||||
}
|
||||
}
|
||||
|
||||
pub mod v2 {
|
||||
use super::*;
|
||||
|
||||
pub const AUXILIARY: &str = "/auxiliary-details";
|
||||
|
||||
absolute_route!(auxiliary_absolute, v2_absolute(), AUXILIARY);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -693,18 +693,6 @@ pub struct MixnetDebug {
|
||||
#[serde(with = "humantime_serde")]
|
||||
pub initial_connection_timeout: Duration,
|
||||
|
||||
/// How long a mixnet connection (ingress or egress) may sit with no packets before it is
|
||||
/// closed to free the lingering task/socket. Reset by any traffic on that connection.
|
||||
/// 0 disables idle reaping.
|
||||
#[serde(with = "humantime_serde")]
|
||||
pub connection_idle_timeout: Duration,
|
||||
|
||||
/// Max time a single egress batch flush may block on a peer socket before the batch is
|
||||
/// abandoned. A few consecutive timeouts drop the (congested) connection; a single one is
|
||||
/// treated as transient and the connection is retained. 0 disables the bound.
|
||||
#[serde(with = "humantime_serde")]
|
||||
pub connection_write_timeout: Duration,
|
||||
|
||||
/// Maximum number of packets buffered per egress connection awaiting a socket write.
|
||||
/// This is a short-term burst absorber, not a queue: buffer depth converts directly into
|
||||
/// added latency (roughly `depth / per-peer send rate`), so an oversized value is just
|
||||
@@ -905,13 +893,6 @@ impl MixnetDebug {
|
||||
const DEFAULT_PACKET_FORWARDING_INITIAL_BACKOFF: Duration = Duration::from_millis(10_000);
|
||||
const DEFAULT_PACKET_FORWARDING_MAXIMUM_BACKOFF: Duration = Duration::from_secs(16);
|
||||
const DEFAULT_INITIAL_CONNECTION_TIMEOUT: Duration = Duration::from_millis(1_500);
|
||||
// reap a mixnet connection after 5min of no traffic; under cover traffic real neighbours
|
||||
// exchange packets far more often, so only genuinely-silent/half-open peers are closed
|
||||
const DEFAULT_CONNECTION_IDLE_TIMEOUT: Duration = Duration::from_secs(300);
|
||||
// bound a single egress flush; healthy flushes are sub-ms, so this only trips on genuine
|
||||
// peer congestion. A single trip just drops the batch (connection retained); see
|
||||
// MAX_CONSECUTIVE_WRITE_TIMEOUTS in the mixnet client for the teardown threshold.
|
||||
const DEFAULT_CONNECTION_WRITE_TIMEOUT: Duration = Duration::from_millis(500);
|
||||
// small enough to keep worst-case egress queuing in the tens-of-ms range at a few thousand
|
||||
// pps per peer (vs. the old 2000, which was hundreds of ms of bufferbloat)
|
||||
const DEFAULT_MAXIMUM_CONNECTION_BUFFER_SIZE: usize = 192;
|
||||
@@ -925,8 +906,6 @@ impl Default for MixnetDebug {
|
||||
packet_forwarding_initial_backoff: Self::DEFAULT_PACKET_FORWARDING_INITIAL_BACKOFF,
|
||||
packet_forwarding_maximum_backoff: Self::DEFAULT_PACKET_FORWARDING_MAXIMUM_BACKOFF,
|
||||
initial_connection_timeout: Self::DEFAULT_INITIAL_CONNECTION_TIMEOUT,
|
||||
connection_idle_timeout: Self::DEFAULT_CONNECTION_IDLE_TIMEOUT,
|
||||
connection_write_timeout: Self::DEFAULT_CONNECTION_WRITE_TIMEOUT,
|
||||
maximum_connection_buffer_size: Self::DEFAULT_MAXIMUM_CONNECTION_BUFFER_SIZE,
|
||||
egress_trace_sample_rate: Self::DEFAULT_EGRESS_TRACE_SAMPLE_RATE,
|
||||
// TODO: update this in few releases...
|
||||
|
||||
@@ -531,8 +531,6 @@ pub async fn try_upgrade_config_v13<P: AsRef<Path>>(
|
||||
.debug
|
||||
.packet_forwarding_maximum_backoff,
|
||||
initial_connection_timeout: old_cfg.mixnet.debug.initial_connection_timeout,
|
||||
connection_idle_timeout: MixnetDebug::DEFAULT_CONNECTION_IDLE_TIMEOUT,
|
||||
connection_write_timeout: MixnetDebug::DEFAULT_CONNECTION_WRITE_TIMEOUT,
|
||||
maximum_connection_buffer_size: old_cfg.mixnet.debug.maximum_connection_buffer_size,
|
||||
egress_trace_sample_rate: MixnetDebug::DEFAULT_EGRESS_TRACE_SAMPLE_RATE,
|
||||
unsafe_disable_noise: old_cfg.mixnet.debug.unsafe_disable_noise,
|
||||
|
||||
@@ -5,20 +5,15 @@ use crate::node::http::state::AppState;
|
||||
use axum::Router;
|
||||
use nym_node_requests::routes;
|
||||
|
||||
pub mod openapi;
|
||||
pub mod v1;
|
||||
pub mod v2;
|
||||
|
||||
pub use nym_node_requests::api as api_requests;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Config {
|
||||
pub v1_config: v1::Config,
|
||||
pub v2_config: v2::Config,
|
||||
}
|
||||
|
||||
pub(super) fn routes(config: Config) -> Router<AppState> {
|
||||
Router::new()
|
||||
.nest(routes::api::V1, v1::routes(config.v1_config))
|
||||
.nest(routes::api::V2, v2::routes(config.v2_config))
|
||||
Router::new().nest(routes::api::V1, v1::routes(config.v1_config))
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ use nym_node_requests::api::v1::authenticator::models::Authenticator;
|
||||
get,
|
||||
path = "",
|
||||
context_path = "/api/v1/authenticator",
|
||||
tag = "v1 / Authenticator",
|
||||
tag = "Authenticator",
|
||||
responses(
|
||||
(status = 501, description = "the endpoint hasn't been implemented yet"),
|
||||
(status = 200, content(
|
||||
|
||||
@@ -41,7 +41,7 @@ pub(crate) fn routes<S: Send + Sync + 'static + Clone>(
|
||||
get,
|
||||
path = "/client-interfaces",
|
||||
context_path = "/api/v1/gateway",
|
||||
tag = "v1 / Gateway",
|
||||
tag = "Gateway",
|
||||
responses(
|
||||
(status = 501, description = "the endpoint hasn't been implemented yet"),
|
||||
(status = 200, content(
|
||||
@@ -67,7 +67,7 @@ pub type ClientInterfacesResponse = FormattedResponse<ClientInterfaces>;
|
||||
get,
|
||||
path = "/mixnet-websockets",
|
||||
context_path = "/api/v1/gateway/client-interfaces",
|
||||
tag = "v1 / Gateway",
|
||||
tag = "Gateway",
|
||||
responses(
|
||||
(status = 501, description = "the endpoint hasn't been implemented yet"),
|
||||
(status = 200, content(
|
||||
@@ -93,7 +93,7 @@ pub type MixnetWebSocketsResponse = FormattedResponse<WebSockets>;
|
||||
get,
|
||||
path = "/wireguard",
|
||||
context_path = "/api/v1/gateway/client-interfaces",
|
||||
tag = "v1 / Gateway",
|
||||
tag = "Gateway",
|
||||
responses(
|
||||
(status = 501, description = "the endpoint hasn't been implemented yet"),
|
||||
(status = 200, content(
|
||||
|
||||
@@ -11,7 +11,7 @@ use nym_node_requests::api::v1::gateway::models::Gateway;
|
||||
get,
|
||||
path = "",
|
||||
context_path = "/api/v1/gateway",
|
||||
tag = "v1 / Gateway",
|
||||
tag = "Gateway",
|
||||
responses(
|
||||
(status = 501, description = "the endpoint hasn't been implemented yet"),
|
||||
(status = 200, content(
|
||||
|
||||
@@ -11,7 +11,7 @@ use nym_node_requests::api::v1::health::models::NodeHealth;
|
||||
get,
|
||||
path = "/health",
|
||||
context_path = "/api/v1",
|
||||
tag = "v1 / Health",
|
||||
tag = "Health",
|
||||
responses(
|
||||
(status = 200, content(
|
||||
(NodeHealth = "application/json"),
|
||||
|
||||
@@ -11,7 +11,7 @@ use nym_node_requests::api::v1::ip_packet_router::models::IpPacketRouter;
|
||||
get,
|
||||
path = "",
|
||||
context_path = "/api/v1/ip-packet-router",
|
||||
tag = "v1 / IP Packet Router",
|
||||
tag = "IP Packet Router",
|
||||
responses(
|
||||
(status = 501, description = "the endpoint hasn't been implemented yet"),
|
||||
(status = 200, content(
|
||||
|
||||
@@ -1,15 +1,23 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::node::http::state::AppState;
|
||||
use axum::Router;
|
||||
use axum::routing::get;
|
||||
use nym_node_requests::api::SignedLewesProtocol;
|
||||
|
||||
pub mod root;
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct Config {}
|
||||
|
||||
pub(crate) fn routes(_config: Config) -> Router<AppState> {
|
||||
Router::new().route("/", get(root::root_lewes_protocol))
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Config {
|
||||
pub details: SignedLewesProtocol,
|
||||
}
|
||||
|
||||
pub(crate) fn routes<S: Send + Sync + 'static + Clone>(config: Config) -> Router<S> {
|
||||
Router::new().route(
|
||||
"/",
|
||||
get({
|
||||
let lp_config = config.details;
|
||||
move |query| root::root_lewes_protocol(lp_config, query)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::node::http::state::AppState;
|
||||
use axum::extract::{Query, State};
|
||||
use axum::extract::Query;
|
||||
use axum::http::StatusCode;
|
||||
use nym_http_api_common::{FormattedResponse, OutputParams};
|
||||
use nym_node_requests::api::{SignedLewesProtocol, SignedLewesProtocolInfo};
|
||||
@@ -12,7 +11,7 @@ use nym_node_requests::api::{SignedLewesProtocol, SignedLewesProtocolInfo};
|
||||
get,
|
||||
path = "/lewes-protocol",
|
||||
context_path = "/api/v1",
|
||||
tag = "v1 / Lewes Protocol",
|
||||
tag = "Lewes Protocol",
|
||||
responses(
|
||||
(status = 501, description = "the endpoint hasn't been implemented yet"),
|
||||
(status = 200, content(
|
||||
@@ -24,10 +23,10 @@ use nym_node_requests::api::{SignedLewesProtocol, SignedLewesProtocolInfo};
|
||||
params(OutputParams)
|
||||
)]
|
||||
pub(crate) async fn root_lewes_protocol(
|
||||
config: SignedLewesProtocol,
|
||||
Query(output): Query<OutputParams>,
|
||||
State(state): State<AppState>,
|
||||
) -> Result<LewesProtocolResponse, StatusCode> {
|
||||
Ok(output.to_response(state.static_information.lewes_protocol.clone()))
|
||||
Ok(output.to_response(config))
|
||||
}
|
||||
|
||||
pub type LewesProtocolResponse = FormattedResponse<SignedLewesProtocol>;
|
||||
|
||||
@@ -11,7 +11,7 @@ use nym_node_requests::api::v1::node_load::models::NodeLoad;
|
||||
get,
|
||||
path = "/load",
|
||||
context_path = "/api/v1",
|
||||
tag = "v1 / Node",
|
||||
tag = "Node",
|
||||
responses(
|
||||
(status = 200, content(
|
||||
(NodeLoad = "application/json"),
|
||||
|
||||
@@ -13,7 +13,7 @@ use nym_node_requests::api::v1::metrics::models::LegacyMixingStats;
|
||||
get,
|
||||
path = "/mixing",
|
||||
context_path = "/api/v1/metrics",
|
||||
tag = "v1 / Metrics",
|
||||
tag = "Metrics",
|
||||
responses(
|
||||
(status = 200, content(
|
||||
(LegacyMixingStats = "application/json"),
|
||||
|
||||
@@ -15,7 +15,7 @@ use nym_node_requests::api::v1::metrics::models::packets::{
|
||||
get,
|
||||
path = "/packets-stats",
|
||||
context_path = "/api/v1/metrics",
|
||||
tag = "v1 / Metrics",
|
||||
tag = "Metrics",
|
||||
responses(
|
||||
(status = 200, content(
|
||||
(PacketsStats = "application/json"),
|
||||
|
||||
@@ -8,7 +8,7 @@ use nym_metrics::metrics;
|
||||
get,
|
||||
path = "/prometheus",
|
||||
context_path = "/api/v1/metrics",
|
||||
tag = "v1 / Metrics",
|
||||
tag = "Metrics",
|
||||
responses(
|
||||
(status = 200, body = String),
|
||||
(status = 400, description = "`Authorization` header was missing"),
|
||||
|
||||
@@ -14,7 +14,7 @@ use time::macros::time;
|
||||
get,
|
||||
path = "/sessions",
|
||||
context_path = "/api/v1/metrics",
|
||||
tag = "v1 / Metrics",
|
||||
tag = "Metrics",
|
||||
responses(
|
||||
(status = 200, content(
|
||||
(SessionStats = "application/json"),
|
||||
|
||||
@@ -15,7 +15,7 @@ use crate::node::http::state::metrics::MetricsAppState;
|
||||
get,
|
||||
path = "/verloc",
|
||||
context_path = "/api/v1/metrics",
|
||||
tag = "v1 / Metrics",
|
||||
tag = "Metrics",
|
||||
responses(
|
||||
(status = 200, content(
|
||||
(VerlocStats = "application/json"),
|
||||
|
||||
@@ -13,7 +13,7 @@ use nym_node_requests::api::v1::metrics::models::WireguardStats;
|
||||
get,
|
||||
path = "/wireguard-stats",
|
||||
context_path = "/api/v1/metrics",
|
||||
tag = "v1 / Metrics",
|
||||
tag = "Metrics",
|
||||
responses(
|
||||
(status = 200, content(
|
||||
(WireguardStats = "application/json"),
|
||||
|
||||
@@ -11,7 +11,7 @@ use nym_node_requests::api::v1::mixnode::models::Mixnode;
|
||||
get,
|
||||
path = "",
|
||||
context_path = "/api/v1/mixnode",
|
||||
tag = "v1 / Mixnode",
|
||||
tag = "Mixnode",
|
||||
responses(
|
||||
(status = 501, description = "the endpoint hasn't been implemented yet"),
|
||||
(status = 200, content(
|
||||
|
||||
@@ -3,9 +3,7 @@
|
||||
|
||||
use crate::node::http::state::AppState;
|
||||
use axum::Router;
|
||||
use axum::response::Redirect;
|
||||
use axum::routing::get;
|
||||
use nym_node_requests::routes;
|
||||
use nym_node_requests::routes::api::v1;
|
||||
|
||||
pub mod authenticator;
|
||||
@@ -20,6 +18,7 @@ pub mod mixnode;
|
||||
pub mod network;
|
||||
pub mod network_requester;
|
||||
pub mod node;
|
||||
pub mod openapi;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Config {
|
||||
@@ -35,12 +34,7 @@ pub struct Config {
|
||||
}
|
||||
|
||||
pub(super) fn routes(config: Config) -> Router<AppState> {
|
||||
// legacy redirects: the Swagger UI moved to a version-neutral /api/swagger
|
||||
let swagger_redirect = get(|| async { Redirect::temporary(&routes::api::swagger_absolute()) });
|
||||
|
||||
Router::new()
|
||||
.route(v1::SWAGGER, swagger_redirect.clone())
|
||||
.route(&format!("{}/", v1::SWAGGER), swagger_redirect)
|
||||
.route(v1::HEALTH, get(health::root_health))
|
||||
.route(v1::LOAD, get(load::root_load))
|
||||
.nest(v1::NETWORK, network::routes())
|
||||
@@ -65,4 +59,5 @@ pub(super) fn routes(config: Config) -> Router<AppState> {
|
||||
lewes_protocol::routes(config.lewes_protocol),
|
||||
)
|
||||
.merge(node::routes(config.node))
|
||||
.merge(openapi::route())
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ use nym_node_requests::api::v1::network::models::UpgradeModeStatus;
|
||||
get,
|
||||
path = "/upgrade-mode-status",
|
||||
context_path = "/api/v1/network",
|
||||
tag = "v1 / Network",
|
||||
tag = "Network",
|
||||
responses(
|
||||
(status = 200, content(
|
||||
(UpgradeModeStatus = "application/json"),
|
||||
|
||||
@@ -10,7 +10,7 @@ use nym_node_requests::api::v1::network_requester::exit_policy::models::UsedExit
|
||||
get,
|
||||
path = "/exit-policy",
|
||||
context_path = "/api/v1/network-requester",
|
||||
tag = "v1 / Network Requester",
|
||||
tag = "Network Requester",
|
||||
responses(
|
||||
(status = 200, content(
|
||||
(UsedExitPolicy = "application/json"),
|
||||
|
||||
@@ -11,7 +11,7 @@ use nym_node_requests::api::v1::network_requester::models::NetworkRequester;
|
||||
get,
|
||||
path = "",
|
||||
context_path = "/api/v1/network-requester",
|
||||
tag = "v1 / Network Requester",
|
||||
tag = "Network Requester",
|
||||
responses(
|
||||
(status = 501, description = "the endpoint hasn't been implemented yet"),
|
||||
(status = 200, content(
|
||||
|
||||
@@ -2,31 +2,30 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::node::http::router::types::RequestError;
|
||||
use crate::node::http::state::AppState;
|
||||
use axum::extract::{Query, State};
|
||||
use axum::extract::Query;
|
||||
use nym_http_api_common::{FormattedResponse, OutputParams};
|
||||
use nym_node_requests::api::v1::node::models::AuxiliaryDetailsV1;
|
||||
use nym_node_requests::api::v1::node::models::AuxiliaryDetails;
|
||||
|
||||
/// Returns auxiliary details of this node.
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/auxiliary-details",
|
||||
context_path = "/api/v1",
|
||||
tag = "v1 / Node",
|
||||
tag = "Node",
|
||||
responses(
|
||||
(status = 200, content(
|
||||
(AuxiliaryDetailsV1 = "application/json"),
|
||||
(AuxiliaryDetailsV1 = "application/yaml")
|
||||
(AuxiliaryDetails = "application/json"),
|
||||
(AuxiliaryDetails = "application/yaml")
|
||||
)),
|
||||
),
|
||||
params(OutputParams)
|
||||
)]
|
||||
pub(crate) async fn auxiliary(
|
||||
description: AuxiliaryDetails,
|
||||
Query(output): Query<OutputParams>,
|
||||
State(state): State<AppState>,
|
||||
) -> Result<AuxiliaryDetailsResponse, RequestError> {
|
||||
let output = output.output.unwrap_or_default();
|
||||
Ok(output.to_response(state.static_information.auxiliary_data.clone().into()))
|
||||
Ok(output.to_response(description))
|
||||
}
|
||||
|
||||
pub type AuxiliaryDetailsResponse = FormattedResponse<AuxiliaryDetailsV1>;
|
||||
pub type AuxiliaryDetailsResponse = FormattedResponse<AuxiliaryDetails>;
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::node::http::state::AppState;
|
||||
use axum::extract::{Query, State};
|
||||
use axum::extract::Query;
|
||||
use nym_http_api_common::{FormattedResponse, OutputParams};
|
||||
use nym_node_requests::api::v1::node::models::BinaryBuildInformationOwned;
|
||||
|
||||
@@ -11,7 +10,7 @@ use nym_node_requests::api::v1::node::models::BinaryBuildInformationOwned;
|
||||
get,
|
||||
path = "/build-information",
|
||||
context_path = "/api/v1",
|
||||
tag = "v1 / Node",
|
||||
tag = "Node",
|
||||
responses(
|
||||
(status = 200, content(
|
||||
(BinaryBuildInformationOwned = "application/json"),
|
||||
@@ -21,11 +20,11 @@ use nym_node_requests::api::v1::node::models::BinaryBuildInformationOwned;
|
||||
params(OutputParams)
|
||||
)]
|
||||
pub(crate) async fn build_information(
|
||||
build_information: BinaryBuildInformationOwned,
|
||||
Query(output): Query<OutputParams>,
|
||||
State(state): State<AppState>,
|
||||
) -> BuildInformationResponse {
|
||||
let output = output.output.unwrap_or_default();
|
||||
output.to_response(state.static_information.build_information.clone())
|
||||
output.to_response(build_information)
|
||||
}
|
||||
|
||||
pub type BuildInformationResponse = FormattedResponse<BinaryBuildInformationOwned>;
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::node::http::router::types::RequestError;
|
||||
use crate::node::http::state::AppState;
|
||||
use axum::extract::{Query, State};
|
||||
use axum::extract::Query;
|
||||
use nym_http_api_common::{FormattedResponse, OutputParams};
|
||||
use nym_node_requests::api::v1::node::models::NodeDescription;
|
||||
|
||||
@@ -12,7 +11,7 @@ use nym_node_requests::api::v1::node::models::NodeDescription;
|
||||
get,
|
||||
path = "/description",
|
||||
context_path = "/api/v1",
|
||||
tag = "v1 / Node",
|
||||
tag = "Node",
|
||||
responses(
|
||||
(status = 200, content(
|
||||
(NodeDescription = "application/json"),
|
||||
@@ -22,11 +21,11 @@ use nym_node_requests::api::v1::node::models::NodeDescription;
|
||||
params(OutputParams)
|
||||
)]
|
||||
pub(crate) async fn description(
|
||||
description: NodeDescription,
|
||||
Query(output): Query<OutputParams>,
|
||||
State(state): State<AppState>,
|
||||
) -> Result<NodeDescriptionResponse, RequestError> {
|
||||
let output = output.output.unwrap_or_default();
|
||||
Ok(output.to_response(state.static_information.description.clone()))
|
||||
Ok(output.to_response(description))
|
||||
}
|
||||
|
||||
pub type NodeDescriptionResponse = FormattedResponse<NodeDescription>;
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::node::http::router::types::RequestError;
|
||||
use crate::node::http::state::AppState;
|
||||
use axum::extract::{Query, State};
|
||||
use axum::extract::Query;
|
||||
use axum::http::StatusCode;
|
||||
use nym_http_api_common::{FormattedResponse, OutputParams};
|
||||
use nym_node_requests::api::v1::node::models::HostSystem;
|
||||
@@ -13,7 +12,7 @@ use nym_node_requests::api::v1::node::models::HostSystem;
|
||||
get,
|
||||
path = "/system-info",
|
||||
context_path = "/api/v1",
|
||||
tag = "v1 / Node",
|
||||
tag = "Node",
|
||||
responses(
|
||||
(status = 200, content(
|
||||
(HostSystem = "application/json"),
|
||||
@@ -24,12 +23,12 @@ use nym_node_requests::api::v1::node::models::HostSystem;
|
||||
params(OutputParams)
|
||||
)]
|
||||
pub(crate) async fn host_system(
|
||||
system_info: Option<HostSystem>,
|
||||
Query(output): Query<OutputParams>,
|
||||
State(state): State<AppState>,
|
||||
) -> Result<HostSystemResponse, RequestError> {
|
||||
let output = output.output.unwrap_or_default();
|
||||
|
||||
let Some(system_info) = state.static_information.system_info.clone() else {
|
||||
let Some(system_info) = system_info else {
|
||||
return Err(RequestError::new(
|
||||
"this nym-node does not wish to expose the system information",
|
||||
StatusCode::FORBIDDEN,
|
||||
|
||||
@@ -12,7 +12,7 @@ use nym_node_requests::api::{SignedDataHostInfo, v1::node::models::SignedHostInf
|
||||
get,
|
||||
path = "/host-information",
|
||||
context_path = "/api/v1",
|
||||
tag = "v1 / Node",
|
||||
tag = "Node",
|
||||
responses(
|
||||
(status = 200, content(
|
||||
(SignedDataHostInfo = "application/json"),
|
||||
|
||||
@@ -10,6 +10,7 @@ use crate::node::http::api::v1::node::roles::roles;
|
||||
use crate::node::http::state::AppState;
|
||||
use axum::Router;
|
||||
use axum::routing::get;
|
||||
use nym_node_requests::api::v1::node::models;
|
||||
use nym_node_requests::routes::api::v1;
|
||||
|
||||
pub mod auxiliary;
|
||||
@@ -19,15 +20,51 @@ pub mod hardware;
|
||||
pub mod host_information;
|
||||
pub mod roles;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Config {}
|
||||
|
||||
pub(super) fn routes(_config: Config) -> Router<AppState> {
|
||||
Router::new()
|
||||
.route(v1::BUILD_INFO, get(build_information))
|
||||
.route(v1::ROLES, get(roles))
|
||||
.route(v1::HOST_INFO, get(host_information))
|
||||
.route(v1::SYSTEM_INFO, get(host_system))
|
||||
.route(v1::NODE_DESCRIPTION, get(description))
|
||||
.route(v1::AUXILIARY, get(auxiliary))
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Config {
|
||||
pub build_information: models::BinaryBuildInformationOwned,
|
||||
pub system_info: Option<models::HostSystem>,
|
||||
pub roles: models::NodeRoles,
|
||||
pub description: models::NodeDescription,
|
||||
pub auxiliary_details: models::AuxiliaryDetails,
|
||||
}
|
||||
|
||||
pub(super) fn routes(config: Config) -> Router<AppState> {
|
||||
Router::new()
|
||||
.route(
|
||||
v1::BUILD_INFO,
|
||||
get({
|
||||
let build_info = config.build_information;
|
||||
move |query| build_information(build_info, query)
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
v1::ROLES,
|
||||
get({
|
||||
let node_roles = config.roles;
|
||||
move |query| roles(node_roles, query)
|
||||
}),
|
||||
)
|
||||
.route(v1::HOST_INFO, get(host_information))
|
||||
.route(
|
||||
v1::SYSTEM_INFO,
|
||||
get({
|
||||
let system_info = config.system_info;
|
||||
move |query| host_system(system_info, query)
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
v1::NODE_DESCRIPTION,
|
||||
get({
|
||||
let node_description = config.description;
|
||||
move |query| description(node_description, query)
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
v1::AUXILIARY,
|
||||
get({
|
||||
let auxiliary_details = config.auxiliary_details;
|
||||
move |query| auxiliary(auxiliary_details, query)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::node::http::state::AppState;
|
||||
use axum::extract::{Query, State};
|
||||
use axum::extract::Query;
|
||||
use nym_http_api_common::{FormattedResponse, OutputParams};
|
||||
use nym_node_requests::api::v1::node::models::NodeRoles;
|
||||
|
||||
@@ -11,7 +10,7 @@ use nym_node_requests::api::v1::node::models::NodeRoles;
|
||||
get,
|
||||
path = "/roles",
|
||||
context_path = "/api/v1",
|
||||
tag = "v1 / Node",
|
||||
tag = "Node",
|
||||
responses(
|
||||
(status = 200, content(
|
||||
(NodeRoles = "application/json"),
|
||||
@@ -21,11 +20,11 @@ use nym_node_requests::api::v1::node::models::NodeRoles;
|
||||
params(OutputParams)
|
||||
)]
|
||||
pub(crate) async fn roles(
|
||||
node_roles: NodeRoles,
|
||||
Query(output): Query<OutputParams>,
|
||||
State(state): State<AppState>,
|
||||
) -> RolesResponse {
|
||||
let output = output.output.unwrap_or_default();
|
||||
output.to_response(state.static_information.roles)
|
||||
output.to_response(node_roles)
|
||||
}
|
||||
|
||||
pub type RolesResponse = FormattedResponse<NodeRoles>;
|
||||
|
||||
+8
-14
@@ -3,7 +3,7 @@
|
||||
|
||||
use axum::Router;
|
||||
use nym_node_requests::api as api_requests;
|
||||
use nym_node_requests::routes;
|
||||
use nym_node_requests::routes::api::{v1, v1_absolute};
|
||||
use utoipa::openapi::security::{Http, HttpAuthScheme};
|
||||
use utoipa::{Modify, OpenApi, openapi::security::SecurityScheme};
|
||||
use utoipa_swagger_ui::SwaggerUi;
|
||||
@@ -37,14 +37,12 @@ use utoipa_swagger_ui::SwaggerUi;
|
||||
crate::node::http::router::api::v1::gateway::client_interfaces::wireguard_details,
|
||||
crate::node::http::router::api::v1::gateway::root::root_gateway,
|
||||
crate::node::http::router::api::v1::lewes_protocol::root::root_lewes_protocol,
|
||||
crate::node::http::router::api::v2::node::auxiliary::auxiliary,
|
||||
|
||||
),
|
||||
components(
|
||||
schemas(
|
||||
nym_http_api_common::Output,
|
||||
nym_http_api_common::OutputParams,
|
||||
nym_http_api_common::OutputV2,
|
||||
nym_http_api_common::OutputParamsV2,
|
||||
api_requests::v1::health::models::NodeHealth,
|
||||
api_requests::v1::health::models::NodeStatus,
|
||||
api_requests::v1::node_load::models::NodeLoad,
|
||||
@@ -58,7 +56,7 @@ use utoipa_swagger_ui::SwaggerUi;
|
||||
api_requests::v1::node::models::Cpu,
|
||||
api_requests::v1::node::models::CryptoHardware,
|
||||
api_requests::v1::node::models::NodeDescription,
|
||||
api_requests::v1::node::models::AuxiliaryDetailsV1,
|
||||
api_requests::v1::node::models::AuxiliaryDetails,
|
||||
api_requests::v1::metrics::models::LegacyMixingStats,
|
||||
api_requests::v1::metrics::models::VerlocStats,
|
||||
api_requests::v1::metrics::models::VerlocResult,
|
||||
@@ -79,7 +77,6 @@ use utoipa_swagger_ui::SwaggerUi;
|
||||
api_requests::v1::network_requester::exit_policy::models::UsedExitPolicy,
|
||||
api_requests::v1::ip_packet_router::models::IpPacketRouter,
|
||||
api_requests::v1::lewes_protocol::models::LewesProtocol,
|
||||
api_requests::v2::node::models::AuxiliaryDetailsV2,
|
||||
),
|
||||
),
|
||||
modifiers(&SecurityAddon),
|
||||
@@ -100,14 +97,11 @@ impl Modify for SecurityAddon {
|
||||
}
|
||||
|
||||
pub(crate) fn route<S: Send + Sync + 'static + Clone>() -> Router<S> {
|
||||
// SwaggerUi must be mounted with its absolute path: it emits internal redirects
|
||||
// (e.g. `/swagger` → `/swagger/`) whose `Location` header uses this string
|
||||
// literally and is not aware of any `.nest()` prefix above it. For the same
|
||||
// reason, this router must be merged at the outer router level — not nested.
|
||||
let openapi_json = format!("{}/api-docs/openapi.json", routes::API);
|
||||
let config = utoipa_swagger_ui::Config::from(openapi_json.clone());
|
||||
SwaggerUi::new(routes::api::swagger_absolute())
|
||||
.url(openapi_json, ApiDoc::openapi())
|
||||
// provide absolute path to the openapi.json
|
||||
let config =
|
||||
utoipa_swagger_ui::Config::from(format!("{}/api-docs/openapi.json", v1_absolute()));
|
||||
SwaggerUi::new(v1::SWAGGER)
|
||||
.url("/api-docs/openapi.json", ApiDoc::openapi())
|
||||
.config(config)
|
||||
.into()
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::node::http::state::AppState;
|
||||
use axum::Router;
|
||||
|
||||
pub mod node;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Config {
|
||||
pub node: node::Config,
|
||||
}
|
||||
|
||||
pub(super) fn routes(config: Config) -> Router<AppState> {
|
||||
Router::new().merge(node::routes(config.node))
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::node::http::router::types::RequestError;
|
||||
use crate::node::http::state::AppState;
|
||||
use axum::extract::{Query, State};
|
||||
use nym_http_api_common::{FormattedResponse, OutputParamsV2};
|
||||
use nym_node_requests::api::v2::node::models::AuxiliaryDetailsV2;
|
||||
|
||||
/// Returns auxiliary details of this node.
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/auxiliary-details",
|
||||
context_path = "/api/v2",
|
||||
tag = "v2 / Node",
|
||||
// distinct from v1's `auxiliary`: OpenAPI requires operationId to be unique
|
||||
// across the whole document, and Swagger UI routes "Try it out" by operationId
|
||||
operation_id = "v2_auxiliary",
|
||||
responses(
|
||||
(status = 200, content(
|
||||
(AuxiliaryDetailsV2 = "application/json"),
|
||||
(AuxiliaryDetailsV2 = "application/yaml")
|
||||
)),
|
||||
),
|
||||
params(OutputParamsV2)
|
||||
)]
|
||||
pub(crate) async fn auxiliary(
|
||||
Query(output): Query<OutputParamsV2>,
|
||||
State(state): State<AppState>,
|
||||
) -> Result<AuxiliaryDetailsResponse, RequestError> {
|
||||
let output = output.output.unwrap_or_default();
|
||||
Ok(output.to_response(state.static_information.auxiliary_data.clone()))
|
||||
}
|
||||
|
||||
pub type AuxiliaryDetailsResponse = FormattedResponse<AuxiliaryDetailsV2>;
|
||||
@@ -1,17 +0,0 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::node::http::api::v2::node::auxiliary::auxiliary;
|
||||
use crate::node::http::state::AppState;
|
||||
use axum::Router;
|
||||
use axum::routing::get;
|
||||
use nym_node_requests::routes::api::v2;
|
||||
|
||||
pub mod auxiliary;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Config {}
|
||||
|
||||
pub(super) fn routes(_config: Config) -> Router<AppState> {
|
||||
Router::new().route(v2::AUXILIARY, get(auxiliary))
|
||||
}
|
||||
@@ -27,7 +27,7 @@ pub(super) async fn default() -> Html<&'static str> {
|
||||
<div>
|
||||
<p> default page of the nym node - you can customize it by setting the 'assets' path under '[http]' section of your config. </p>
|
||||
|
||||
You can explore the REST API at <a href = "/api/swagger/">/api/swagger/</a>
|
||||
You can explore the REST API at <a href = "/api/v1/swagger/">/api/v1/swagger/</a>
|
||||
</div>
|
||||
"#,
|
||||
)
|
||||
|
||||
@@ -2,18 +2,22 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::node::http::NymNodeHttpServer;
|
||||
use crate::node::http::api::v1::lewes_protocol;
|
||||
use crate::node::http::error::NymNodeHttpError;
|
||||
use crate::node::http::state::AppState;
|
||||
use axum::Router;
|
||||
use axum::response::Redirect;
|
||||
use axum::routing::get;
|
||||
use nym_bin_common::bin_info_owned;
|
||||
use nym_http_api_common::middleware::logging;
|
||||
use nym_node_requests::api::SignedLewesProtocol;
|
||||
use nym_node_requests::api::v1::authenticator::models::Authenticator;
|
||||
use nym_node_requests::api::v1::gateway::models::{Bridges, Gateway};
|
||||
use nym_node_requests::api::v1::ip_packet_router::models::IpPacketRouter;
|
||||
use nym_node_requests::api::v1::mixnode::models::Mixnode;
|
||||
use nym_node_requests::api::v1::network_requester::exit_policy::models::UsedExitPolicy;
|
||||
use nym_node_requests::api::v1::network_requester::models::NetworkRequester;
|
||||
use nym_node_requests::api::v1::node::models::{AuxiliaryDetails, HostSystem, NodeDescription};
|
||||
use nym_node_requests::routes;
|
||||
use std::net::SocketAddr;
|
||||
use std::path::Path;
|
||||
@@ -32,12 +36,18 @@ pub struct HttpServerConfig {
|
||||
}
|
||||
|
||||
impl HttpServerConfig {
|
||||
pub fn new() -> Self {
|
||||
pub fn new(signed_lewes_protocol: SignedLewesProtocol) -> Self {
|
||||
HttpServerConfig {
|
||||
landing: Default::default(),
|
||||
api: api::Config {
|
||||
v1_config: api::v1::Config {
|
||||
node: api::v1::node::Config {},
|
||||
node: api::v1::node::Config {
|
||||
build_information: bin_info_owned!(),
|
||||
system_info: None,
|
||||
roles: Default::default(),
|
||||
description: Default::default(),
|
||||
auxiliary_details: Default::default(),
|
||||
},
|
||||
metrics: Default::default(),
|
||||
gateway: Default::default(),
|
||||
mixnode: Default::default(),
|
||||
@@ -45,10 +55,9 @@ impl HttpServerConfig {
|
||||
network_requester: Default::default(),
|
||||
ip_packet_router: Default::default(),
|
||||
authenticator: Default::default(),
|
||||
lewes_protocol: Default::default(),
|
||||
},
|
||||
v2_config: api::v2::Config {
|
||||
node: api::v2::node::Config {},
|
||||
lewes_protocol: lewes_protocol::Config {
|
||||
details: signed_lewes_protocol,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -60,6 +69,24 @@ impl HttpServerConfig {
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_system_info(mut self, info: HostSystem) -> Self {
|
||||
self.api.v1_config.node.system_info = Some(info);
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_description(mut self, description: NodeDescription) -> Self {
|
||||
self.api.v1_config.node.description = description;
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_auxiliary_details(mut self, auxiliary_details: AuxiliaryDetails) -> Self {
|
||||
self.api.v1_config.node.auxiliary_details = auxiliary_details;
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_gateway_details(mut self, gateway: Gateway) -> Self {
|
||||
self.api.v1_config.gateway.details = Some(gateway);
|
||||
@@ -152,10 +179,6 @@ impl NymNodeRouter {
|
||||
)
|
||||
.merge(landing_page::routes(config.landing))
|
||||
.nest(routes::API, api::routes(config.api))
|
||||
// openapi must be merged at the outer router level (not nested) —
|
||||
// SwaggerUi emits internal redirects that use absolute paths
|
||||
// unaware of any `.nest()` prefix
|
||||
.merge(api::openapi::route())
|
||||
.layer(axum::middleware::from_fn(logging::log_request_info))
|
||||
.with_state(state),
|
||||
}
|
||||
@@ -185,10 +208,20 @@ impl NymNodeRouter {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use nym_crypto::asymmetric::{ed25519, x25519};
|
||||
use nym_node_requests::api::SignedData;
|
||||
use nym_node_requests::api::v1::lewes_protocol::models::LewesProtocol;
|
||||
use nym_test_utils::helpers::deterministic_rng;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
#[test]
|
||||
fn router_constructs_without_panic() {
|
||||
let config = HttpServerConfig::new();
|
||||
let mut rng = deterministic_rng();
|
||||
let signing = ed25519::KeyPair::new(&mut rng);
|
||||
let x25519_pub: x25519::DHPublicKey = x25519::PrivateKey::new(&mut rng).public_key().into();
|
||||
let lp = LewesProtocol::new(false, 0, 0, x25519_pub, BTreeMap::new());
|
||||
let signed = SignedData::new(lp, signing.private_key()).unwrap();
|
||||
let config = HttpServerConfig::new(signed);
|
||||
let _ = NymNodeRouter::new(config, AppState::dummy());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,13 +4,9 @@
|
||||
use crate::node::http::state::load::CachedNodeLoad;
|
||||
use crate::node::http::state::metrics::MetricsAppState;
|
||||
use crate::node::key_rotation::active_keys::ActiveSphinxKeys;
|
||||
use nym_bin_common::build_information::BinaryBuildInformationOwned;
|
||||
use nym_credential_verification::UpgradeModeState;
|
||||
use nym_crypto::asymmetric::ed25519;
|
||||
use nym_node_metrics::NymNodeMetrics;
|
||||
use nym_node_requests::api::SignedLewesProtocol;
|
||||
use nym_node_requests::api::v1::node::models::{HostSystem, NodeDescription, NodeRoles};
|
||||
use nym_node_requests::api::v2::node::models::AuxiliaryDetailsV2;
|
||||
use nym_noise_keys::VersionedNoiseKeyV1;
|
||||
use nym_verloc::measurements::SharedVerlocStats;
|
||||
use std::net::IpAddr;
|
||||
@@ -27,14 +23,6 @@ pub(crate) struct StaticNodeInformation {
|
||||
pub(crate) x25519_versioned_noise_key: Option<VersionedNoiseKeyV1>,
|
||||
pub(crate) ip_addresses: Vec<IpAddr>,
|
||||
pub(crate) hostname: Option<String>,
|
||||
|
||||
// TODO: move other fields here too
|
||||
pub(crate) build_information: BinaryBuildInformationOwned,
|
||||
pub(crate) system_info: Option<HostSystem>,
|
||||
pub(crate) roles: NodeRoles,
|
||||
pub(crate) description: NodeDescription,
|
||||
pub(crate) auxiliary_data: AuxiliaryDetailsV2,
|
||||
pub(crate) lewes_protocol: SignedLewesProtocol,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -89,34 +77,15 @@ impl AppState {
|
||||
#[cfg(test)]
|
||||
pub(crate) fn dummy() -> Self {
|
||||
use crate::node::key_rotation::key::SphinxPrivateKey;
|
||||
use nym_crypto::asymmetric::x25519;
|
||||
use rand::rngs::OsRng;
|
||||
|
||||
let mut rng = nym_test_utils::helpers::deterministic_rng();
|
||||
let ed25519_keys = ed25519::KeyPair::new(&mut rng);
|
||||
let x25519_pub: x25519::DHPublicKey = x25519::PrivateKey::new(&mut rng).public_key().into();
|
||||
let lp = nym_node_requests::api::v1::lewes_protocol::models::LewesProtocol::new(
|
||||
false,
|
||||
0,
|
||||
0,
|
||||
x25519_pub,
|
||||
std::collections::BTreeMap::new(),
|
||||
);
|
||||
let signed =
|
||||
nym_node_requests::api::SignedData::new(lp, ed25519_keys.private_key()).unwrap();
|
||||
|
||||
let ed25519_keys = ed25519::KeyPair::new(&mut OsRng);
|
||||
let attester_pk = *ed25519_keys.public_key();
|
||||
let static_information = StaticNodeInformation {
|
||||
ed25519_identity_keys: Arc::new(ed25519_keys),
|
||||
x25519_versioned_noise_key: None,
|
||||
ip_addresses: vec![],
|
||||
hostname: None,
|
||||
build_information: nym_bin_common::bin_info_owned!(),
|
||||
system_info: None,
|
||||
roles: Default::default(),
|
||||
description: Default::default(),
|
||||
auxiliary_data: Default::default(),
|
||||
lewes_protocol: signed,
|
||||
};
|
||||
let active_sphinx = ActiveSphinxKeys::new_fresh(SphinxPrivateKey::new(&mut OsRng, 0));
|
||||
|
||||
|
||||
@@ -145,17 +145,11 @@ impl OnUpdateMetricsHandler for PrometheusGlobalNodeMetricsRegistryUpdater {
|
||||
.active_ingress_websocket_connections_count() as i64,
|
||||
);
|
||||
self.prometheus_wrapper.set(
|
||||
NetworkActiveEgressMixnetConnections,
|
||||
NetworkActiveIngressWebSocketConnections,
|
||||
self.metrics
|
||||
.network
|
||||
.active_egress_mixnet_connections_count() as i64,
|
||||
);
|
||||
self.prometheus_wrapper.set(
|
||||
NetworkIdleClosedIngressMixnetConnections,
|
||||
self.metrics
|
||||
.network
|
||||
.idle_closed_ingress_mixnet_connections_count() as i64,
|
||||
);
|
||||
|
||||
// # PROCESS
|
||||
self.prometheus_wrapper.set(
|
||||
|
||||
@@ -14,7 +14,6 @@ pub(crate) mod legacy_packet_data;
|
||||
pub(crate) mod mixnet_data_cleaner;
|
||||
pub(crate) mod pending_egress_packets_updater;
|
||||
pub(crate) mod prometheus_events_handler;
|
||||
pub(crate) mod tokio_runtime_updater;
|
||||
|
||||
pub(crate) trait RegistrableHandler:
|
||||
Downcast + OnStartMetricsHandler + OnUpdateMetricsHandler + Send + Sync + 'static
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
//! Samples tokio runtime metrics (scheduling pressure, busy ratio) into the prometheus registry
|
||||
//! on each aggregator update tick.
|
||||
//!
|
||||
//! `num_workers` / `alive_tasks` / `global_queue_depth` are always available. The per-worker
|
||||
//! timing (`busy_ratio`, `worker_poll_count`) is only exposed by tokio when the binary is built
|
||||
//! with `RUSTFLAGS="--cfg tokio_unstable"`; without that flag those two gauges are left at 0.
|
||||
|
||||
use crate::node::metrics::handler::{
|
||||
MetricsHandler, OnStartMetricsHandler, OnUpdateMetricsHandler,
|
||||
};
|
||||
use async_trait::async_trait;
|
||||
use nym_node_metrics::prometheus_wrapper::{
|
||||
NymNodePrometheusMetrics, PROMETHEUS_METRICS, PrometheusMetric,
|
||||
};
|
||||
use tokio::runtime::Handle;
|
||||
|
||||
// unique marker type so the aggregator can key this handler (it has no real events)
|
||||
pub struct TokioRuntimeData;
|
||||
|
||||
// a snapshot of cumulative worker-busy time, used to derive the busy ratio over the interval
|
||||
// between two samples
|
||||
#[cfg(tokio_unstable)]
|
||||
#[derive(Clone, Copy)]
|
||||
struct BusySample {
|
||||
/// summed busy duration across all workers at the time of the sample
|
||||
busy: std::time::Duration,
|
||||
/// when the sample was taken
|
||||
at: tokio::time::Instant,
|
||||
}
|
||||
|
||||
pub struct TokioRuntimeMetricsUpdater {
|
||||
prometheus_wrapper: &'static NymNodePrometheusMetrics,
|
||||
|
||||
// previous busy snapshot, for deriving the busy ratio
|
||||
#[cfg(tokio_unstable)]
|
||||
prev_busy: Option<BusySample>,
|
||||
}
|
||||
|
||||
impl TokioRuntimeMetricsUpdater {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self {
|
||||
prometheus_wrapper: &PROMETHEUS_METRICS,
|
||||
#[cfg(tokio_unstable)]
|
||||
prev_busy: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl OnStartMetricsHandler for TokioRuntimeMetricsUpdater {}
|
||||
|
||||
#[async_trait]
|
||||
impl OnUpdateMetricsHandler for TokioRuntimeMetricsUpdater {
|
||||
async fn on_update(&mut self) {
|
||||
use PrometheusMetric::*;
|
||||
let m = Handle::current().metrics();
|
||||
|
||||
self.prometheus_wrapper
|
||||
.set(TokioRuntimeNumWorkers, m.num_workers() as i64);
|
||||
self.prometheus_wrapper
|
||||
.set(TokioRuntimeAliveTasks, m.num_alive_tasks() as i64);
|
||||
self.prometheus_wrapper
|
||||
.set(TokioRuntimeGlobalQueueDepth, m.global_queue_depth() as i64);
|
||||
|
||||
// left at their registered 0 unless built with `--cfg tokio_unstable`
|
||||
#[cfg(tokio_unstable)]
|
||||
{
|
||||
let workers = m.num_workers();
|
||||
let busy: std::time::Duration =
|
||||
(0..workers).map(|w| m.worker_total_busy_duration(w)).sum();
|
||||
let now = tokio::time::Instant::now();
|
||||
if let Some(prev) = self.prev_busy {
|
||||
let elapsed = now.duration_since(prev.at).as_secs_f64();
|
||||
let ratio = if workers > 0 && elapsed > 0.0 {
|
||||
busy.saturating_sub(prev.busy).as_secs_f64() / (elapsed * workers as f64)
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
self.prometheus_wrapper
|
||||
.set_float(TokioRuntimeBusyRatio, ratio);
|
||||
}
|
||||
self.prev_busy = Some(BusySample { busy, at: now });
|
||||
|
||||
let polls: u64 = (0..workers).map(|w| m.worker_poll_count(w)).sum();
|
||||
self.prometheus_wrapper
|
||||
.set(TokioRuntimeWorkerPollCount, polls as i64);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl MetricsHandler for TokioRuntimeMetricsUpdater {
|
||||
type Events = TokioRuntimeData;
|
||||
|
||||
// SAFETY: this handler has no associated events; it only acts on the periodic `on_update`.
|
||||
#[allow(clippy::panic)]
|
||||
async fn handle_event(&mut self, _event: Self::Events) {
|
||||
panic!(
|
||||
"MetricsHandler::handle_event incorrectly called on TokioRuntimeMetricsUpdater - it has no events"
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
use crate::node::key_rotation::active_keys::SphinxKeyGuard;
|
||||
use crate::node::mixnet::shared::SharedData;
|
||||
use futures::StreamExt;
|
||||
use nym_mixnet_client::metrics::{MixnetMetric, PacketTrace, Traced};
|
||||
use nym_mixnet_client::trace::{PacketTrace, TraceStage, Traced};
|
||||
use nym_noise::connection::Connection;
|
||||
use nym_noise::upgrade_noise_responder;
|
||||
use nym_sphinx_forwarding::packet::MixPacket;
|
||||
@@ -28,12 +28,6 @@ use tracing::{Span, debug, error, instrument, trace, warn};
|
||||
/// How often (in packets) the stream-level span updates its packet count.
|
||||
const SPAN_UPDATE_INTERVAL: u64 = 10_000;
|
||||
|
||||
/// Instant at which a connection idle since `last_activity` should be closed, or `None` if idle
|
||||
/// reaping is disabled (`timeout` is zero).
|
||||
fn idle_deadline(last_activity: Instant, timeout: Duration) -> Option<Instant> {
|
||||
(!timeout.is_zero()).then(|| last_activity + timeout)
|
||||
}
|
||||
|
||||
struct PendingReplayCheckPackets {
|
||||
// map of rotation id used for packet creation to the packets (each carrying the latency
|
||||
// trace started at receive, so the deferral wait is attributed to the ReplayCheck stage)
|
||||
@@ -482,7 +476,7 @@ impl ConnectionHandler {
|
||||
};
|
||||
|
||||
// close out the Unwrap stage (partial unwrap: shared secret + header MAC)
|
||||
trace.record(MixnetMetric::Unwrap);
|
||||
trace.record(TraceStage::Unwrap);
|
||||
self.pending_packets.push(now, partially_unwrapped, trace);
|
||||
|
||||
// 2. check for packet replay
|
||||
@@ -573,7 +567,7 @@ impl ConnectionHandler {
|
||||
rotation_id,
|
||||
"dropping replayed packet"
|
||||
);
|
||||
trace.record(MixnetMetric::ReplayCheck);
|
||||
trace.record(TraceStage::ReplayCheck);
|
||||
self.handle_unwrapped_packet(
|
||||
now,
|
||||
Err(PacketProcessingError::PacketReplay),
|
||||
@@ -587,7 +581,7 @@ impl ConnectionHandler {
|
||||
// finalise the (expensive) full unwrapping, then close out the ReplayCheck stage:
|
||||
// it spans partial-unwrap -> deferral -> replay check -> finalise
|
||||
let unwrapped_packet = packet.finalise_unwrapping();
|
||||
trace.record(MixnetMetric::ReplayCheck);
|
||||
trace.record(TraceStage::ReplayCheck);
|
||||
self.handle_unwrapped_packet(now, unwrapped_packet, network_monitor_packet, trace)
|
||||
.await;
|
||||
}
|
||||
@@ -684,7 +678,7 @@ impl ConnectionHandler {
|
||||
let packet = packet.inner;
|
||||
let unwrapped_packet = self.try_full_unwrap_packet(packet);
|
||||
// no replay batching on this path: the Unwrap stage covers the full unwrapping
|
||||
trace.record(MixnetMetric::Unwrap);
|
||||
trace.record(TraceStage::Unwrap);
|
||||
|
||||
let is_network_monitor_packet = self.is_from_authorised_network_monitor_agent();
|
||||
self.handle_unwrapped_packet(now, unwrapped_packet, is_network_monitor_packet, trace)
|
||||
@@ -763,8 +757,6 @@ impl ConnectionHandler {
|
||||
mut mixnet_connection: Framed<Connection<TcpStream>, NymCodec>,
|
||||
) {
|
||||
let mut packets_processed: u64 = 0;
|
||||
// reset by every received packet; drives the idle-connection reaping below
|
||||
let mut last_activity = Instant::now();
|
||||
loop {
|
||||
// make sure pending packets are not stuck in the queue if we don't get any more packets
|
||||
// from this sender
|
||||
@@ -773,12 +765,6 @@ impl ConnectionHandler {
|
||||
.processing_config
|
||||
.maximum_replay_detection_deferral,
|
||||
);
|
||||
// close the connection (freeing the task/socket) if no packets arrive for too long;
|
||||
// ingress is read-only, so without this a silently-gone peer would linger forever
|
||||
let idle_deadline = idle_deadline(
|
||||
last_activity,
|
||||
self.shared.processing_config.connection_idle_timeout,
|
||||
);
|
||||
|
||||
tokio::select! {
|
||||
biased;
|
||||
@@ -792,7 +778,6 @@ impl ConnectionHandler {
|
||||
maybe_framed_nym_packet = mixnet_connection.next() => {
|
||||
match maybe_framed_nym_packet {
|
||||
Some(Ok(packet)) => {
|
||||
last_activity = Instant::now();
|
||||
self.handle_received_nym_packet(packet).await;
|
||||
packets_processed += 1;
|
||||
if packets_processed.is_multiple_of(SPAN_UPDATE_INTERVAL) {
|
||||
@@ -832,25 +817,6 @@ impl ConnectionHandler {
|
||||
} => {
|
||||
self.handle_pending_packets_batch(Instant::now()).await;
|
||||
}
|
||||
// 4. reap the connection if it has been idle for too long
|
||||
_ = async move {
|
||||
match idle_deadline {
|
||||
Some(d) => tokio::time::sleep_until(d).await,
|
||||
None => std::future::pending::<()>().await,
|
||||
}
|
||||
} => {
|
||||
debug!(
|
||||
event = "connection.idle_timeout",
|
||||
remote_addr = %self.remote_address,
|
||||
packets_processed,
|
||||
idle_secs = self.shared.processing_config.connection_idle_timeout.as_secs(),
|
||||
"closing idle ingress mixnet connection"
|
||||
);
|
||||
Span::current().record("exit_reason", "idle_timeout");
|
||||
Span::current().record("packets_processed", packets_processed);
|
||||
self.shared.metrics.network.ingress_mixnet_idle_closed();
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -921,18 +887,6 @@ mod tests {
|
||||
assert!(pending.flush_deadline(Duration::from_millis(50)).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn idle_deadline_disabled_when_timeout_zero() {
|
||||
assert!(idle_deadline(Instant::now(), Duration::ZERO).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn idle_deadline_is_last_activity_plus_timeout() {
|
||||
let now = Instant::now();
|
||||
let timeout = Duration::from_secs(300);
|
||||
assert_eq!(idle_deadline(now, timeout), Some(now + timeout));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flush_deadline_is_batch_start_plus_deferral() {
|
||||
let key = PrivateKey::random();
|
||||
|
||||
@@ -7,9 +7,7 @@ use nym_mixnet_client::SendWithoutResponse;
|
||||
use nym_mixnet_client::forwarder::{
|
||||
MixForwardingReceiver, MixForwardingSender, PacketToForward, mix_forwarding_channels,
|
||||
};
|
||||
use nym_mixnet_client::metrics::{
|
||||
MixnetMetric, Traced, observe_delay_drain_batch_size, observe_drain_batch_size,
|
||||
};
|
||||
use nym_mixnet_client::trace::{TraceStage, Traced};
|
||||
use nym_node_metrics::NymNodeMetrics;
|
||||
use nym_nonexhaustive_delayqueue::{Expired, NonExhaustiveDelayQueue};
|
||||
use nym_sphinx_forwarding::packet::MixPacket;
|
||||
@@ -18,35 +16,6 @@ use std::io;
|
||||
use tokio::time::Instant;
|
||||
use tracing::{debug, error, trace, warn};
|
||||
|
||||
/// Max packets handled per `select!` wakeup, per drainable branch (ingress channel and expired
|
||||
/// delay-queue items), before yielding back to the biased select so shutdown stays responsive.
|
||||
/// Per-packet work is sub-µs to low-µs, so 256 bounds the worst-case stall to <~1ms.
|
||||
const MAX_DRAIN_BATCH: usize = 256;
|
||||
|
||||
/// The node's single forward-hop egress engine - the last in-node stage of the mixnet pipeline.
|
||||
///
|
||||
/// **Where it sits.** Inbound packets are accepted by the mixnet listener and processed
|
||||
/// per-connection by a `ConnectionHandler`: sphinx unwrap, replay check, and - for *forward* hops -
|
||||
/// computation of the intended (Poisson) mix delay. The handler hands each one off as a
|
||||
/// [`PacketToForward`] over the unbounded ingress-to-forwarder channel. This forwarder is the sole
|
||||
/// consumer of that channel: every forward-hop packet in the node (plus acks) funnels through it.
|
||||
/// Final-hop packets never reach here; they are delivered to local clients instead.
|
||||
///
|
||||
/// **What it does**, per packet:
|
||||
/// 1. drops it if the [`RoutingFilter`] doesn't recognise the next hop;
|
||||
/// 2. holds it in the delay queue until its target release instant (the mix delay), or forwards it
|
||||
/// immediately when the delay is zero or has already elapsed;
|
||||
/// 3. on release, forwards it to the next hop via the mixnet client (`C: SendWithoutResponse`),
|
||||
/// which owns the per-connection egress TCP sockets.
|
||||
///
|
||||
/// **Design notes.** It runs as one dedicated task owning both the ingress channel and the delay
|
||||
/// queue. Its [`run`](Self::run) loop wakes on either a new packet or a delay-queue release, then
|
||||
/// services *both* branches every wakeup - draining queued ingress packets first (to bring the
|
||||
/// delay queue current) and then releasing everything now due - so a burst on one branch can never
|
||||
/// starve the other (the failure mode a biased "one branch per wakeup" select would have). Each
|
||||
/// drain is bounded by [`MAX_DRAIN_BATCH`] so shutdown stays responsive. Along the way it stamps
|
||||
/// the latency-trace stages it owns (`ForwarderQueue`, `DelayQueue`, `DelayQueueOverrun`), feeding
|
||||
/// the `mixnet_packet_*` metrics family.
|
||||
pub struct PacketForwarder<C, F> {
|
||||
delay_queue: NonExhaustiveDelayQueue<Traced<MixPacket>>,
|
||||
mixnet_client: C,
|
||||
@@ -79,6 +48,7 @@ impl<C, F> PacketForwarder<C, F> {
|
||||
fn forward_packet(&mut self, packet: Traced<MixPacket>)
|
||||
where
|
||||
C: SendWithoutResponse,
|
||||
F: RoutingFilter,
|
||||
{
|
||||
let next_hop = packet.inner.next_hop_address();
|
||||
|
||||
@@ -107,42 +77,25 @@ impl<C, F> PacketForwarder<C, F> {
|
||||
fn handle_done_delaying(&mut self, packet: Expired<Traced<MixPacket>>)
|
||||
where
|
||||
C: SendWithoutResponse,
|
||||
F: RoutingFilter,
|
||||
{
|
||||
// how late beyond the target release the queue actually handed the packet back: the
|
||||
// delay-queue's own scheduling/retrieval overhead (timer granularity + task wakeup)
|
||||
let overrun = Instant::now().saturating_duration_since(packet.deadline());
|
||||
let mut delayed_packet = packet.into_inner();
|
||||
// close out the DelayQueue stage (the full wait: intended mix delay + overrun)
|
||||
delayed_packet.record(MixnetMetric::DelayQueue);
|
||||
delayed_packet.record_value(MixnetMetric::DelayQueueOverrun, overrun.as_secs_f64());
|
||||
delayed_packet.record(TraceStage::DelayQueue);
|
||||
delayed_packet.record_value(TraceStage::DelayQueueOverrun, overrun.as_secs_f64());
|
||||
self.forward_packet(delayed_packet);
|
||||
}
|
||||
|
||||
/// Drain every packet whose release deadline has already passed (a burst of simultaneous
|
||||
/// releases), bounded by [`MAX_DRAIN_BATCH`]. `try_next_expired` never blocks, so this is a
|
||||
/// no-op (returns 0) when nothing is due. Returns how many packets were released.
|
||||
fn drain_expired(&mut self) -> usize
|
||||
where
|
||||
C: SendWithoutResponse,
|
||||
{
|
||||
let mut released = 0;
|
||||
while released < MAX_DRAIN_BATCH {
|
||||
let Some(expired) = self.delay_queue.try_next_expired() else {
|
||||
break;
|
||||
};
|
||||
self.handle_done_delaying(expired);
|
||||
released += 1;
|
||||
}
|
||||
released
|
||||
}
|
||||
|
||||
fn handle_new_packet(&mut self, mut new_packet: PacketToForward)
|
||||
where
|
||||
C: SendWithoutResponse,
|
||||
F: RoutingFilter,
|
||||
{
|
||||
// close out the ForwarderQueue stage (wait in the ingress -> forwarder channel)
|
||||
new_packet.trace.record(MixnetMetric::ForwarderQueue);
|
||||
new_packet.trace.record(TraceStage::ForwarderQueue);
|
||||
|
||||
let next_hop = new_packet.packet.next_hop();
|
||||
|
||||
@@ -166,44 +119,23 @@ impl<C, F> PacketForwarder<C, F> {
|
||||
|
||||
// in case of a zero delay packet, don't bother putting it in the delay queue,
|
||||
// just forward it immediately
|
||||
let Some(instant) = delay_target else {
|
||||
self.forward_packet(traced);
|
||||
return;
|
||||
};
|
||||
|
||||
// check if the delay has already expired, if so, don't bother putting it through the delay
|
||||
// queue only to retrieve it immediately. Just forward it.
|
||||
if instant.checked_duration_since(Instant::now()).is_none() {
|
||||
// the target elapsed before we could even queue it: upstream overhead already
|
||||
// ate the whole intended delay, so the overrun is now - target
|
||||
let overrun = Instant::now().saturating_duration_since(instant);
|
||||
traced.record_value(MixnetMetric::DelayQueueOverrun, overrun.as_secs_f64());
|
||||
self.forward_packet(traced);
|
||||
if let Some(instant) = delay_target {
|
||||
// check if the delay has already expired, if so, don't bother putting it through
|
||||
// the delay queue only to retrieve it immediately. Just forward it.
|
||||
if instant.checked_duration_since(Instant::now()).is_none() {
|
||||
// the target elapsed before we could even queue it: upstream overhead already
|
||||
// ate the whole intended delay, so the overrun is now - target
|
||||
let overrun = Instant::now().saturating_duration_since(instant);
|
||||
traced.record_value(TraceStage::DelayQueueOverrun, overrun.as_secs_f64());
|
||||
self.forward_packet(traced)
|
||||
} else {
|
||||
self.delay_queue.insert_at(traced, instant);
|
||||
}
|
||||
} else {
|
||||
self.delay_queue.insert_at(traced, instant);
|
||||
self.forward_packet(traced)
|
||||
}
|
||||
}
|
||||
|
||||
/// Drain every packet currently queued in the ingress channel, bounded by [`MAX_DRAIN_BATCH`]
|
||||
/// so the per-wakeup `select!`/waker/coop overhead is amortised across the burst rather than
|
||||
/// paid per packet. `try_recv` never blocks. Returns how many packets were handled.
|
||||
fn drain_ingress(&mut self) -> usize
|
||||
where
|
||||
C: SendWithoutResponse,
|
||||
F: RoutingFilter,
|
||||
{
|
||||
let mut batch_size = 0;
|
||||
while batch_size < MAX_DRAIN_BATCH {
|
||||
// Err = channel empty (or closed, which is unreachable since we hold a sender)
|
||||
let Ok(packet) = self.packet_receiver.try_recv() else {
|
||||
break;
|
||||
};
|
||||
self.handle_new_packet(packet);
|
||||
batch_size += 1;
|
||||
}
|
||||
batch_size
|
||||
}
|
||||
|
||||
fn update_queue_len_metric(&self) {
|
||||
self.metrics
|
||||
.process
|
||||
@@ -216,95 +148,62 @@ impl<C, F> PacketForwarder<C, F> {
|
||||
.update_packet_forwarder_queue_size(channel_size)
|
||||
}
|
||||
|
||||
/// Log the forwarder's queue depth at a severity reflecting how overloaded it is. Called
|
||||
/// periodically (~every 1000 packets), not per packet.
|
||||
fn log_queue_status(
|
||||
&self,
|
||||
channel_depth: usize,
|
||||
packets_processed: u64,
|
||||
last_drain_batch: usize,
|
||||
) {
|
||||
let delay_queue_depth = self.delay_queue.len();
|
||||
match channel_depth {
|
||||
n if n > 1000 => error!(
|
||||
event = "forwarder.queue_overload",
|
||||
channel_depth = n,
|
||||
delay_queue_depth,
|
||||
packets_processed,
|
||||
last_drain_batch,
|
||||
"there are currently {n} mix packets waiting to get forwarded - the node seems to be significantly overloaded!"
|
||||
),
|
||||
n if n > 500 => warn!(
|
||||
event = "forwarder.queue_high",
|
||||
channel_depth = n,
|
||||
delay_queue_depth,
|
||||
packets_processed,
|
||||
last_drain_batch,
|
||||
"there are currently {n} mix packets waiting to get forwarded - is the node overloaded?"
|
||||
),
|
||||
n => trace!(
|
||||
channel_depth = n,
|
||||
delay_queue_depth, packets_processed, last_drain_batch, "forwarder queue status"
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn run(&mut self, shutdown_token: ShutdownToken)
|
||||
where
|
||||
C: SendWithoutResponse,
|
||||
F: RoutingFilter,
|
||||
{
|
||||
let mut processed: u64 = 0;
|
||||
let mut last_logged: u64 = 0;
|
||||
trace!("starting PacketForwarder");
|
||||
loop {
|
||||
// packets handled this wakeup per branch; the select arm seeds the one it consumed
|
||||
let mut ingress = 0usize;
|
||||
let mut released = 0usize;
|
||||
tokio::select! {
|
||||
biased;
|
||||
_ = shutdown_token.cancelled() => {
|
||||
debug!("PacketForwarder: Received shutdown");
|
||||
break;
|
||||
}
|
||||
new_packet = self.packet_receiver.next() => {
|
||||
// impossible to panic: the struct holds a sender, so not all senders can drop
|
||||
#[allow(clippy::unwrap_used)]
|
||||
self.handle_new_packet(new_packet.unwrap());
|
||||
ingress = 1;
|
||||
}
|
||||
delayed = self.delay_queue.next() => {
|
||||
// SAFETY: `stream` implementation of `NonExhaustiveDelayQueue` never returns `None`
|
||||
#[allow(clippy::unwrap_used)]
|
||||
self.handle_done_delaying(delayed.unwrap());
|
||||
released = 1;
|
||||
}
|
||||
new_packet = self.packet_receiver.next() => {
|
||||
// this one is impossible to ever panic - the struct itself contains a sender
|
||||
// and hence it can't happen that ALL senders are dropped
|
||||
#[allow(clippy::unwrap_used)]
|
||||
self.handle_new_packet(new_packet.unwrap());
|
||||
let channel_len = self.packet_sender.len();
|
||||
let delay_queue_len = self.delay_queue.len();
|
||||
if processed.is_multiple_of(1000) {
|
||||
match channel_len {
|
||||
n if n > 1000 => error!(
|
||||
event = "forwarder.queue_overload",
|
||||
channel_depth = n,
|
||||
delay_queue_depth = delay_queue_len,
|
||||
packets_processed = processed,
|
||||
"there are currently {n} mix packets waiting to get forwarded - the node seems to be significantly overloaded!"
|
||||
),
|
||||
n if n > 500 => warn!(
|
||||
event = "forwarder.queue_high",
|
||||
channel_depth = n,
|
||||
delay_queue_depth = delay_queue_len,
|
||||
packets_processed = processed,
|
||||
"there are currently {n} mix packets waiting to get forwarded - is the node overloaded?"
|
||||
),
|
||||
n => trace!(
|
||||
channel_depth = n,
|
||||
delay_queue_depth = delay_queue_len,
|
||||
packets_processed = processed,
|
||||
"forwarder queue status"
|
||||
),
|
||||
}
|
||||
}
|
||||
self.update_channel_size_metric(channel_len);
|
||||
processed += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// service both branches every wakeup so neither can starve the other: drain queued
|
||||
// ingress packets first (routing + insert/forward, bringing the delay queue current),
|
||||
// then release everything now due
|
||||
ingress += self.drain_ingress();
|
||||
released += self.drain_expired();
|
||||
|
||||
if ingress > 0 {
|
||||
observe_drain_batch_size(ingress);
|
||||
processed += ingress as u64;
|
||||
|
||||
let channel_len = self.packet_sender.len();
|
||||
// log roughly every 1000 packets; `processed` advances in batches, so use a
|
||||
// crossing test rather than an exact modulo (which a batch could step over)
|
||||
if processed - last_logged >= 1000 {
|
||||
last_logged = processed;
|
||||
self.log_queue_status(channel_len, processed, ingress);
|
||||
}
|
||||
self.update_channel_size_metric(channel_len);
|
||||
}
|
||||
if released > 0 {
|
||||
observe_delay_drain_batch_size(released);
|
||||
}
|
||||
|
||||
// update the metric on either a new packet being inserted or a packet being released
|
||||
// update the metrics on either new packet being inserted or packet being removed
|
||||
self.update_queue_len_metric();
|
||||
}
|
||||
trace!("PacketForwarder: Exiting");
|
||||
|
||||
@@ -9,7 +9,7 @@ use crate::node::replay_protection::bloomfilter::ReplayProtectionBloomfilters;
|
||||
use crate::node::routing_filter::network_filter::RoutableNetworkMonitors;
|
||||
use nym_gateway::node::GatewayStorageError;
|
||||
use nym_mixnet_client::forwarder::{MixForwardingSender, PacketToForward};
|
||||
use nym_mixnet_client::metrics::PacketTrace;
|
||||
use nym_mixnet_client::trace::PacketTrace;
|
||||
use nym_node_metrics::NymNodeMetrics;
|
||||
use nym_node_metrics::mixnet::PacketKind;
|
||||
use nym_noise::config::NoiseConfig;
|
||||
@@ -45,9 +45,6 @@ pub(crate) struct ProcessingConfig {
|
||||
|
||||
/// sample 1-in-N forwarded packets for per-stage latency tracing (0 disables)
|
||||
pub(crate) egress_trace_sample_rate: u64,
|
||||
|
||||
/// close an ingress connection after this long with no received packets (0 disables)
|
||||
pub(crate) connection_idle_timeout: Duration,
|
||||
}
|
||||
|
||||
impl ProcessingConfig {
|
||||
@@ -68,7 +65,6 @@ impl ProcessingConfig {
|
||||
final_hop_processing_enabled: config.modes.expects_final_hop_traffic()
|
||||
|| config.wireguard.enabled,
|
||||
egress_trace_sample_rate: config.mixnet.debug.egress_trace_sample_rate,
|
||||
connection_idle_timeout: config.mixnet.debug.connection_idle_timeout,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+29
-56
@@ -31,7 +31,6 @@ use crate::node::metrics::handler::global_prometheus_updater::PrometheusGlobalNo
|
||||
use crate::node::metrics::handler::legacy_packet_data::LegacyMixingStatsUpdater;
|
||||
use crate::node::metrics::handler::mixnet_data_cleaner::MixnetMetricsCleaner;
|
||||
use crate::node::metrics::handler::pending_egress_packets_updater::PendingEgressPacketsUpdater;
|
||||
use crate::node::metrics::handler::tokio_runtime_updater::TokioRuntimeMetricsUpdater;
|
||||
use crate::node::mixnet::SharedFinalHopData;
|
||||
use crate::node::mixnet::packet_forwarding::PacketForwarder;
|
||||
use crate::node::mixnet::shared::ProcessingConfig;
|
||||
@@ -45,10 +44,12 @@ use crate::node::routing_filter::{OpenFilter, RoutingFilter};
|
||||
use crate::node::shared_network::CachedNetwork;
|
||||
use crate::node::shared_network::refresher::{NetworkRefresher, NetworkRefresherConfig};
|
||||
use crate::node::shared_network::topology_provider::{CachedTopologyProvider, LocalGatewayNode};
|
||||
use nym_bin_common::{bin_info, bin_info_owned};
|
||||
use nym_bin_common::bin_info;
|
||||
use nym_config::defaults::NymNetworkDetails;
|
||||
use nym_credential_verification::UpgradeModeState;
|
||||
use nym_crypto::asymmetric::{ed25519, x25519};
|
||||
pub use nym_gateway::node::ActiveClientsStore;
|
||||
pub use nym_gateway::node::GatewayStorage;
|
||||
use nym_gateway::node::wireguard::PeerRegistrator;
|
||||
use nym_gateway::node::{GatewayTasksBuilder, UpgradeModeCheckRequestSender};
|
||||
use nym_kkt::key_utils::{
|
||||
@@ -67,18 +68,15 @@ use nym_node_metrics::NymNodeMetrics;
|
||||
use nym_node_metrics::events::MetricEventsSender;
|
||||
use nym_node_requests::api::SignedData;
|
||||
use nym_node_requests::api::v1::lewes_protocol::models::{LPHashFunction, LPKEM, LewesProtocol};
|
||||
use nym_node_requests::api::v1::node::models::{AnnouncePorts, NodeDescription, NodeRoles};
|
||||
use nym_node_requests::api::v1::node::models::{AnnouncePorts, NodeDescription};
|
||||
use nym_noise::config::{NetworkMonitorAgentNode, NoiseConfig, NoiseNetworkView};
|
||||
use nym_noise_keys::VersionedNoiseKeyV1;
|
||||
use nym_sphinx_acknowledgements::AckKey;
|
||||
use nym_sphinx_addressing::Recipient;
|
||||
use nym_task::{ShutdownManager, ShutdownToken, ShutdownTracker};
|
||||
use nym_validator_client::nyxd::AccountId;
|
||||
use nym_validator_client::nyxd::contract_traits::PagedNetworkMonitorsQueryClient;
|
||||
use nym_validator_client::nyxd::error::NyxdError;
|
||||
use nym_validator_client::nyxd::nym_network_monitors_contract_common::AuthorisedNetworkMonitor;
|
||||
use nym_validator_client::signing::signer::OfflineSigner;
|
||||
use nym_validator_client::{DirectSecp256k1HdWallet, QueryHttpRpcNyxdClient, UserAgent};
|
||||
use nym_validator_client::{QueryHttpRpcNyxdClient, UserAgent};
|
||||
use nym_verloc::measurements::SharedVerlocStats;
|
||||
use nym_verloc::{self, measurements::VerlocMeasurer};
|
||||
use nym_wireguard::{WireguardGatewayData, peer_controller::PeerControlRequest};
|
||||
@@ -96,9 +94,6 @@ use tokio_util::sync::WaitForCancellationFutureOwned;
|
||||
use tracing::{debug, error, info, trace};
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
pub use nym_gateway::node::ActiveClientsStore;
|
||||
pub use nym_gateway::node::GatewayStorage;
|
||||
|
||||
pub mod bonding_information;
|
||||
pub mod description;
|
||||
pub mod helpers;
|
||||
@@ -896,27 +891,12 @@ impl NymNode {
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn node_chain_address(&self) -> Result<AccountId, NymNodeError> {
|
||||
let network_details = NymNetworkDetails::new_from_env();
|
||||
|
||||
// derive the address (annoyingly, this will derive our private keys that we will rederive
|
||||
// when starting the gateway, but changing this behaviour requires too much refactoring)
|
||||
let wallet = DirectSecp256k1HdWallet::checked_from_mnemonic(
|
||||
&network_details.chain_details.bech32_account_prefix,
|
||||
(**self.entry_gateway.mnemonic).clone(),
|
||||
)
|
||||
.map_err(NyxdError::from)?;
|
||||
|
||||
Ok(wallet.get_accounts()[0].address.clone())
|
||||
}
|
||||
|
||||
pub(crate) async fn build_http_server(
|
||||
&self,
|
||||
shutdown: WaitForCancellationFutureOwned,
|
||||
) -> Result<NymNodeHttpServer, NymNodeError> {
|
||||
let auxiliary_data = api_requests::v2::node::models::AuxiliaryDetailsV2 {
|
||||
let auxiliary_details = api_requests::v1::node::models::AuxiliaryDetails {
|
||||
location: self.config.host.location,
|
||||
address: self.node_chain_address()?.to_string(),
|
||||
announce_ports: AnnouncePorts {
|
||||
verloc_port: self.config.verloc.announce_port,
|
||||
mix_port: self.config.mixnet.announce_port,
|
||||
@@ -1001,7 +981,7 @@ impl NymNode {
|
||||
let signed_lewes_protocol =
|
||||
SignedData::new(lewes_protocol, self.ed25519_identity_keys.private_key()).unwrap();
|
||||
|
||||
let mut config = HttpServerConfig::new()
|
||||
let mut config = HttpServerConfig::new(signed_lewes_protocol)
|
||||
.with_landing_page_assets(self.config.http.landing_page_assets_path.as_ref())
|
||||
.with_mixnode_details(mixnode_details)
|
||||
.with_gateway_details(gateway_details)
|
||||
@@ -1009,16 +989,28 @@ impl NymNode {
|
||||
.with_ip_packet_router_details(ipr_details)
|
||||
.with_authenticator_details(auth_details)
|
||||
.with_used_exit_policy(exit_policy_details)
|
||||
.with_description(self.description.clone())
|
||||
.with_auxiliary_details(auxiliary_details)
|
||||
.with_prometheus_bearer_token(self.config.http.access_token.clone());
|
||||
|
||||
let system_info = if self.config.http.expose_system_info {
|
||||
Some(get_system_info(
|
||||
if self.config.http.expose_system_info {
|
||||
config = config.with_system_info(get_system_info(
|
||||
self.config.http.expose_system_hardware,
|
||||
self.config.http.expose_crypto_hardware,
|
||||
))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
}
|
||||
if self.config.modes.mixnode {
|
||||
config.api.v1_config.node.roles.mixnode_enabled = true;
|
||||
}
|
||||
|
||||
if self.config.modes.entry {
|
||||
config.api.v1_config.node.roles.gateway_enabled = true
|
||||
}
|
||||
|
||||
if self.config.modes.exit {
|
||||
config.api.v1_config.node.roles.network_requester_enabled = true;
|
||||
config.api.v1_config.node.roles.ip_packet_router_enabled = true;
|
||||
}
|
||||
|
||||
if let Some(path) = &self.config.gateway_tasks.storage_paths.bridge_client_params {
|
||||
config = config.with_bridge_client_params_file(path);
|
||||
@@ -1039,17 +1031,6 @@ impl NymNode {
|
||||
x25519_versioned_noise_key,
|
||||
ip_addresses: self.config.host.public_ips.clone(),
|
||||
hostname: self.config.host.hostname.clone(),
|
||||
build_information: bin_info_owned!(),
|
||||
system_info,
|
||||
roles: NodeRoles {
|
||||
mixnode_enabled: self.config.modes.mixnode,
|
||||
gateway_enabled: self.config.modes.entry,
|
||||
network_requester_enabled: self.config.modes.exit,
|
||||
ip_packet_router_enabled: self.config.modes.exit,
|
||||
},
|
||||
description: self.description.clone(),
|
||||
auxiliary_data,
|
||||
lewes_protocol: signed_lewes_protocol,
|
||||
},
|
||||
self.active_sphinx_keys()?.clone(),
|
||||
self.metrics.clone(),
|
||||
@@ -1170,14 +1151,6 @@ impl NymNode {
|
||||
.global_prometheus_counters_update_rate,
|
||||
);
|
||||
|
||||
// handler sampling tokio runtime scheduling metrics (run-queue depth, busy ratio) into
|
||||
// the prometheus registry. run-queue depth is a transient gauge, so we sample at the base
|
||||
// aggregator cadence (~5s) rather than the coarse 30s global-prometheus-counters rate.
|
||||
metrics_aggregator.register_handler(
|
||||
TokioRuntimeMetricsUpdater::new(),
|
||||
self.config.metrics.debug.aggregator_update_rate,
|
||||
);
|
||||
|
||||
// handler for handling prometheus metrics events
|
||||
// metrics_aggregator.register_handler(PrometheusEventsHandler{}, None);
|
||||
|
||||
@@ -1297,9 +1270,10 @@ impl NymNode {
|
||||
{
|
||||
let processing_config = ProcessingConfig::new(&self.config);
|
||||
|
||||
// pre-register the whole mixnet_packet_* histogram family so it's present on the
|
||||
// prometheus endpoint at zero from boot (not just after the first sampled packet)
|
||||
nym_mixnet_client::metrics::register_all();
|
||||
// pre-register the per-stage packet-latency histograms so the whole mixnet_packet_* family
|
||||
// is present on the prometheus endpoint at zero from boot (not just after the first
|
||||
// sampled packet)
|
||||
nym_mixnet_client::trace::register_stage_metrics();
|
||||
|
||||
// we're ALWAYS listening for mixnet packets, either for forward or final hops (or both)
|
||||
info!(
|
||||
@@ -1315,8 +1289,6 @@ impl NymNode {
|
||||
self.config.mixnet.debug.initial_connection_timeout,
|
||||
self.config.mixnet.debug.maximum_connection_buffer_size,
|
||||
self.config.mixnet.debug.use_legacy_packet_encoding,
|
||||
self.config.mixnet.debug.connection_idle_timeout,
|
||||
self.config.mixnet.debug.connection_write_timeout,
|
||||
);
|
||||
let mixnet_client = nym_mixnet_client::Client::new(
|
||||
mixnet_client_config,
|
||||
@@ -1332,6 +1304,7 @@ impl NymNode {
|
||||
let mix_packet_sender = packet_forwarder.sender();
|
||||
|
||||
let shutdown_token = self.shutdown_token();
|
||||
|
||||
self.shutdown_tracker().try_spawn_named(
|
||||
async move { packet_forwarder.run(shutdown_token).await },
|
||||
"PacketForwarder",
|
||||
|
||||
Generated
+34
-34
@@ -4927,7 +4927,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-api-requests"
|
||||
version = "1.21.1"
|
||||
version = "1.21.0"
|
||||
dependencies = [
|
||||
"bs58",
|
||||
"celes",
|
||||
@@ -4967,7 +4967,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-bin-common"
|
||||
version = "1.21.1"
|
||||
version = "1.21.0"
|
||||
dependencies = [
|
||||
"const-str",
|
||||
"log",
|
||||
@@ -4998,7 +4998,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-coconut-dkg-common"
|
||||
version = "1.21.1"
|
||||
version = "1.21.0"
|
||||
dependencies = [
|
||||
"cosmwasm-schema",
|
||||
"cosmwasm-std",
|
||||
@@ -5011,7 +5011,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-compact-ecash"
|
||||
version = "1.21.1"
|
||||
version = "1.21.0"
|
||||
dependencies = [
|
||||
"bincode",
|
||||
"bs58",
|
||||
@@ -5033,7 +5033,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-config"
|
||||
version = "1.21.1"
|
||||
version = "1.21.0"
|
||||
dependencies = [
|
||||
"dirs 6.0.0",
|
||||
"handlebars",
|
||||
@@ -5047,7 +5047,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-contracts-common"
|
||||
version = "1.21.1"
|
||||
version = "1.21.0"
|
||||
dependencies = [
|
||||
"bs58",
|
||||
"cosmwasm-schema",
|
||||
@@ -5061,7 +5061,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-credentials-interface"
|
||||
version = "1.21.1"
|
||||
version = "1.21.0"
|
||||
dependencies = [
|
||||
"nym-bls12_381-fork",
|
||||
"nym-compact-ecash",
|
||||
@@ -5079,7 +5079,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-crypto"
|
||||
version = "1.21.1"
|
||||
version = "1.21.0"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bs58",
|
||||
@@ -5101,7 +5101,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-ecash-contract-common"
|
||||
version = "1.21.1"
|
||||
version = "1.21.0"
|
||||
dependencies = [
|
||||
"bs58",
|
||||
"cosmwasm-schema",
|
||||
@@ -5114,7 +5114,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-ecash-signer-check-types"
|
||||
version = "1.21.1"
|
||||
version = "1.21.0"
|
||||
dependencies = [
|
||||
"nym-coconut-dkg-common",
|
||||
"nym-crypto",
|
||||
@@ -5129,14 +5129,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-ecash-time"
|
||||
version = "1.21.1"
|
||||
version = "1.21.0"
|
||||
dependencies = [
|
||||
"time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nym-exit-policy"
|
||||
version = "1.21.1"
|
||||
version = "1.21.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -5147,7 +5147,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-group-contract-common"
|
||||
version = "1.21.1"
|
||||
version = "1.21.0"
|
||||
dependencies = [
|
||||
"cosmwasm-schema",
|
||||
"cw-controllers",
|
||||
@@ -5158,7 +5158,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-http-api-client"
|
||||
version = "1.21.1"
|
||||
version = "1.21.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"bincode",
|
||||
@@ -5190,7 +5190,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-http-api-client-macro"
|
||||
version = "1.21.1"
|
||||
version = "1.21.0"
|
||||
dependencies = [
|
||||
"proc-macro-crate 3.3.0",
|
||||
"proc-macro2",
|
||||
@@ -5201,7 +5201,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-http-api-common"
|
||||
version = "1.21.1"
|
||||
version = "1.21.0"
|
||||
dependencies = [
|
||||
"bincode",
|
||||
"serde",
|
||||
@@ -5211,7 +5211,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-kkt-ciphersuite"
|
||||
version = "1.21.1"
|
||||
version = "1.21.0"
|
||||
dependencies = [
|
||||
"num_enum",
|
||||
"semver",
|
||||
@@ -5222,7 +5222,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-mixnet-contract-common"
|
||||
version = "1.21.1"
|
||||
version = "1.21.0"
|
||||
dependencies = [
|
||||
"bs58",
|
||||
"cosmwasm-schema",
|
||||
@@ -5243,7 +5243,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-multisig-contract-common"
|
||||
version = "1.21.1"
|
||||
version = "1.21.0"
|
||||
dependencies = [
|
||||
"cosmwasm-schema",
|
||||
"cosmwasm-std",
|
||||
@@ -5258,7 +5258,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-network-defaults"
|
||||
version = "1.21.1"
|
||||
version = "1.21.0"
|
||||
dependencies = [
|
||||
"cargo_metadata 0.19.2",
|
||||
"dotenvy",
|
||||
@@ -5273,7 +5273,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-network-monitors-contract-common"
|
||||
version = "1.21.1"
|
||||
version = "1.21.0"
|
||||
dependencies = [
|
||||
"cosmwasm-schema",
|
||||
"cosmwasm-std",
|
||||
@@ -5285,7 +5285,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-node-families-contract-common"
|
||||
version = "1.21.1"
|
||||
version = "1.21.0"
|
||||
dependencies = [
|
||||
"cosmwasm-schema",
|
||||
"cosmwasm-std",
|
||||
@@ -5300,7 +5300,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-node-requests"
|
||||
version = "1.21.1"
|
||||
version = "1.21.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"celes",
|
||||
@@ -5327,7 +5327,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-noise-keys"
|
||||
version = "1.21.1"
|
||||
version = "1.21.0"
|
||||
dependencies = [
|
||||
"nym-crypto",
|
||||
"schemars",
|
||||
@@ -5337,7 +5337,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-pemstore"
|
||||
version = "1.21.1"
|
||||
version = "1.21.0"
|
||||
dependencies = [
|
||||
"pem",
|
||||
"tracing",
|
||||
@@ -5346,7 +5346,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-performance-contract-common"
|
||||
version = "1.21.1"
|
||||
version = "1.21.0"
|
||||
dependencies = [
|
||||
"cosmwasm-schema",
|
||||
"cosmwasm-std",
|
||||
@@ -5359,7 +5359,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-serde-helpers"
|
||||
version = "1.21.1"
|
||||
version = "1.21.0"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bs58",
|
||||
@@ -5370,7 +5370,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-store-cipher"
|
||||
version = "1.21.1"
|
||||
version = "1.21.0"
|
||||
dependencies = [
|
||||
"aes-gcm",
|
||||
"argon2",
|
||||
@@ -5385,7 +5385,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-ticketbooks-merkle"
|
||||
version = "1.21.1"
|
||||
version = "1.21.0"
|
||||
dependencies = [
|
||||
"nym-credentials-interface",
|
||||
"nym-serde-helpers",
|
||||
@@ -5399,7 +5399,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-types"
|
||||
version = "1.21.1"
|
||||
version = "1.21.0"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"cosmrs",
|
||||
@@ -5429,7 +5429,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-upgrade-mode-check"
|
||||
version = "1.21.1"
|
||||
version = "1.21.0"
|
||||
dependencies = [
|
||||
"jwt-simple",
|
||||
"nym-crypto",
|
||||
@@ -5445,7 +5445,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-validator-client"
|
||||
version = "1.21.1"
|
||||
version = "1.21.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"base64 0.22.1",
|
||||
@@ -5496,7 +5496,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-vesting-contract-common"
|
||||
version = "1.21.1"
|
||||
version = "1.21.0"
|
||||
dependencies = [
|
||||
"cosmwasm-schema",
|
||||
"cosmwasm-std",
|
||||
@@ -5542,7 +5542,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-wireguard-types"
|
||||
version = "1.21.1"
|
||||
version = "1.21.0"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"nym-crypto",
|
||||
|
||||
@@ -95,7 +95,7 @@ a:hover {
|
||||
You are most likely accessing this website because you've had some issue with
|
||||
the traffic coming from this IP. This router is part of the <a
|
||||
href="https://nym.com/">NYM project</a>, which is
|
||||
dedicated to <a href="https://nym.com/features">create</a> outstanding
|
||||
dedicated to <a href="https://nym.com/about/mission">create</a> outstanding
|
||||
privacy software that is legally compliant without sacrificing integrity or
|
||||
having any backdoors.
|
||||
This router IP should be generating no other traffic, unless it has been
|
||||
@@ -168,7 +168,7 @@ a:hover {
|
||||
</svg>
|
||||
</p>
|
||||
|
||||
<p><a href="https://nym.com/docs/network/overview">Read more about how Nym works.</a></p>
|
||||
<p><a href="https://nym.com/about/mixnet">Read more about how Nym works.</a></p>
|
||||
|
||||
<p>
|
||||
Nym relies on a growing ecosystem of users, developers and researcher partners
|
||||
@@ -227,7 +227,7 @@ a:hover {
|
||||
<p style="text-align:center">
|
||||
<img
|
||||
class="logo"
|
||||
src="https://raw.githubusercontent.com/nymtech/nym/develop/explorer-v2/public/images/Network.webp"
|
||||
src="https://raw.githubusercontent.com/nymtech/websites/main/www/nym.com/public/images/Nym_meta_Image.png"
|
||||
alt=""
|
||||
style="max-width:320px;width:100%;height:auto"
|
||||
onerror="this.onerror=null;this.src='/images/nym_logo.png';"
|
||||
|
||||
Reference in New Issue
Block a user