Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ members = [
"common/socks5/requests",
"common/statistics",
"common/store-cipher",
"common/task",
"common/task", "common/test-utils",
"common/ticketbooks-merkle",
"common/topology",
"common/tun",
Expand Down
3 changes: 3 additions & 0 deletions common/gateway-requests/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,7 @@ workspace = true
default-features = false

[dev-dependencies]
anyhow = { workspace = true }
nym-compact-ecash = { path = "../nym_offline_compact_ecash" } # we need specific imports in tests
nym-test-utils = { path = "../test-utils" }
tokio = { workspace = true, features = ["full"] }
82 changes: 82 additions & 0 deletions common/gateway-requests/src/registration/handshake/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,3 +109,85 @@ GATEWAY -> CLIENT
DONE(status)

*/

#[cfg(test)]
mod tests {
use super::*;
use crate::ClientControlRequest;
use futures::StreamExt;
use nym_test_utils::helpers::u64_seeded_rng;
use nym_test_utils::mocks::stream_sink::mock_streams;
use nym_test_utils::traits::{Leak, Timeboxed, TimeboxedSpawnable};
use tokio::join;
use tungstenite::Message;

#[tokio::test]
async fn basic_handshake() -> anyhow::Result<()> {
use anyhow::Context as _;

// solve the lifetime issue by just leaking the contents of the boxes
// which is perfectly fine in test
let client_rng = u64_seeded_rng(42).leak();
let gateway_rng = u64_seeded_rng(69).leak();

let client_keys = ed25519::KeyPair::new(client_rng).leak();
let gateway_keys = ed25519::KeyPair::new(gateway_rng).leak();

let (client_ws, gateway_ws) = mock_streams::<Message>();

// we need streams that return Result<Message, WsError>
let client_ws = client_ws.map(Ok);
let gateway_ws = gateway_ws.map(Ok);

let client_ws = client_ws.leak();
let gateway_ws = gateway_ws.leak();

let handshake_client = client_handshake(
client_rng,
client_ws,
client_keys,
*gateway_keys.public_key(),
false,
true,
TaskClient::dummy(),
);

let client_fut = handshake_client.spawn_timeboxed();

// we need to receive the first message so that it could be propagated to the gateway side of the handshake
let ClientControlRequest::RegisterHandshakeInitRequest {
protocol_version: _,
data,
} = (gateway_ws.next())
.timeboxed()
.await
.context("timeout")?
.context("no message!")??
.into_text()?
.parse::<ClientControlRequest>()?
else {
panic!("bad message")
};

let init_msg = data;

let handshake_gateway = gateway_handshake(
gateway_rng,
gateway_ws,
gateway_keys,
init_msg,
TaskClient::dummy(),
);

let gateway_fut = handshake_gateway.spawn_timeboxed();
let (client, gateway) = join!(client_fut, gateway_fut);

let client_key = client???;
let gateway_key = gateway???;

// ensure the created keys are the same
assert_eq!(client_key, gateway_key);

Ok(())
}
}
1 change: 1 addition & 0 deletions common/nymnoise/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ anyhow = { workspace = true }
tokio = { workspace = true, features = ["full"] }
rand_chacha = { workspace = true }
nym-crypto = { path = "../crypto", features = ["rand"] }
nym-test-utils = { path = "../test-utils" }


[lints]
Expand Down
126 changes: 9 additions & 117 deletions common/nymnoise/src/stream/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -411,122 +411,21 @@ where
mod tests {
use super::*;
use nym_crypto::asymmetric::x25519;
use rand_chacha::rand_core::SeedableRng;
use std::io::Error;
use std::mem;
use nym_test_utils::helpers::deterministic_rng;
use nym_test_utils::mocks::async_read_write::mock_io_streams;
use nym_test_utils::traits::{Timeboxed, TimeboxedSpawnable};
use std::sync::Arc;
use std::task::{Context, Waker};
use std::time::Duration;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::join;
use tokio::sync::Mutex;
use tokio::time::timeout;

fn mock_streams() -> (MockStream, MockStream) {
let ch1 = Arc::new(Mutex::new(Default::default()));
let ch2 = Arc::new(Mutex::new(Default::default()));

(
MockStream {
inner: MockStreamInner {
tx: ch1.clone(),
rx: ch2.clone(),
},
},
MockStream {
inner: MockStreamInner { tx: ch2, rx: ch1 },
},
)
}

struct MockStream {
inner: MockStreamInner,
}

#[allow(dead_code)]
impl MockStream {
fn unchecked_tx_data(&self) -> Vec<u8> {
self.inner.tx.try_lock().unwrap().data.clone()
}

fn unchecked_rx_data(&self) -> Vec<u8> {
self.inner.rx.try_lock().unwrap().data.clone()
}
}

struct MockStreamInner {
tx: Arc<Mutex<DataWrapper>>,
rx: Arc<Mutex<DataWrapper>>,
}

#[derive(Default)]
struct DataWrapper {
data: Vec<u8>,
waker: Option<Waker>,
}

impl AsyncRead for MockStream {
fn poll_read(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut ReadBuf<'_>,
) -> Poll<io::Result<()>> {
let mut inner = self.inner.rx.try_lock().unwrap();
let data = mem::take(&mut inner.data);
if data.is_empty() {
inner.waker = Some(cx.waker().clone());
return Poll::Pending;
}

if let Some(waker) = inner.waker.take() {
waker.wake();
}

buf.put_slice(&data);
Poll::Ready(Ok(()))
}
}

impl AsyncWrite for MockStream {
fn poll_write(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<Result<usize, Error>> {
let mut inner = self.inner.tx.try_lock().unwrap();
let len = buf.len();

if !inner.data.is_empty() {
assert!(inner.waker.is_none());
inner.waker = Some(cx.waker().clone());
return Poll::Pending;
}

inner.data.extend_from_slice(buf);
if let Some(waker) = inner.waker.take() {
waker.wake();
}
Poll::Ready(Ok(len))
}

fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Result<(), Error>> {
Poll::Ready(Ok(()))
}

fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Result<(), Error>> {
Poll::Ready(Ok(()))
}
}

#[tokio::test]
async fn noise_handshake() -> anyhow::Result<()> {
let dummy_seed = [42u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
let mut rng = deterministic_rng();

let initiator_keys = Arc::new(x25519::KeyPair::new(&mut rng));
let responder_keys = Arc::new(x25519::KeyPair::new(&mut rng));

let (initiator_stream, responder_stream) = mock_streams();
let (initiator_stream, responder_stream) = mock_io_streams();

let psk = generate_psk(*responder_keys.public_key(), NoiseVersion::V1)?;
let pattern = NoisePattern::default();
Expand All @@ -547,14 +446,8 @@ mod tests {
*responder_keys.public_key(),
);

let initiator_fut =
tokio::spawn(
async move { timeout(Duration::from_millis(200), stream_initiator).await },
);
let responder_fut =
tokio::spawn(
async move { timeout(Duration::from_millis(200), stream_responder).await },
);
let initiator_fut = stream_initiator.spawn_timeboxed();
let responder_fut = stream_responder.spawn_timeboxed();

let (initiator, responder) = join!(initiator_fut, responder_fut);

Expand All @@ -563,14 +456,13 @@ mod tests {

let msg = b"hello there";
// if noise was successful we should be able to write a proper message across
timeout(Duration::from_millis(200), initiator.write_all(msg)).await??;

initiator.write_all(msg).timeboxed().await??;
initiator.inner_stream.flush().await?;

let inner_buf = initiator.inner_stream.get_ref().unchecked_tx_data();

let mut buf = [0u8; 11];
timeout(Duration::from_millis(200), responder.read(&mut buf)).await??;
responder.read(&mut buf).timeboxed().await??;

assert_eq!(&buf[..], msg);

Expand Down
23 changes: 23 additions & 0 deletions common/test-utils/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
[package]
name = "nym-test-utils"
version = "0.1.0"
authors.workspace = true
repository.workspace = true
homepage.workspace = true
documentation.workspace = true
edition.workspace = true
license.workspace = true
rust-version.workspace = true
readme.workspace = true

[dependencies]
anyhow = { workspace = true }
futures = { workspace = true }
rand_chacha = { workspace = true }
tokio = { workspace = true, features = ["sync", "time", "rt"] }

[dev-dependencies]
tokio = { workspace = true, features = ["full"] }

[lints]
workspace = true
33 changes: 33 additions & 0 deletions common/test-utils/src/helpers.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Copyright 2025 - Nym Technologies SA <[email protected]>
// SPDX-License-Identifier: Apache-2.0

use crate::traits::Timeboxed;
use rand_chacha::rand_core::SeedableRng;
use rand_chacha::ChaCha20Rng;
use std::future::Future;
use tokio::task::JoinHandle;
use tokio::time::error::Elapsed;

pub fn leak<T>(val: T) -> &'static mut T {
Box::leak(Box::new(val))
}

pub fn spawn_timeboxed<F>(fut: F) -> JoinHandle<Result<F::Output, Elapsed>>
where
F: Future + Send + 'static,
<F as Future>::Output: Send,
{
tokio::spawn(async move { fut.timeboxed().await })
}

pub fn deterministic_rng() -> ChaCha20Rng {
seeded_rng([42u8; 32])
}

pub fn seeded_rng(seed: [u8; 32]) -> ChaCha20Rng {
ChaCha20Rng::from_seed(seed)
}

pub fn u64_seeded_rng(seed: u64) -> ChaCha20Rng {
ChaCha20Rng::seed_from_u64(seed)
}
6 changes: 6 additions & 0 deletions common/test-utils/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// Copyright 2025 - Nym Technologies SA <[email protected]>
// SPDX-License-Identifier: Apache-2.0

pub mod helpers;
pub mod mocks;
pub mod traits;
Loading
Loading