Compare commits

..

28 Commits

Author SHA1 Message Date
benedetta davico 4c449797eb Update Cargo.toml 2025-08-27 08:03:16 +02:00
benedetta davico fcdf0fb580 Update push-node-status-api.yaml 2025-08-27 08:02:55 +02:00
benedettadavico 66d0296f47 update dockerfile to pg 2025-08-26 16:02:11 +02:00
benedettadavico 03bbbf44e9 ns api ci fix 2025-08-26 16:02:11 +02:00
dynco-nym 0a48fa6172 Remove freshness check on testrun submit (#5977)
* Remove freshness check on testrun submit
- freshness is enforced by a background task
  that marks testruns as stale after a
  configured amount of time

* Move code around

* Add humantime

* Update launch script

* Fix typo

* Adjust agent run script

* Configure user agent

* Bump version
2025-08-26 12:26:13 +02:00
p17o 18d9d807f2 Added VPS provider hostraha.com (#5959) 2025-08-22 12:31:31 +00:00
import this 3a7393d316 [DOCs/operators]: Roll back from v2025.15-gruyere to v2025.14-feta (#5973)
* release notes and version bump up

* update stats

* roll version back and comment new notes
2025-08-22 11:23:16 +00:00
import this 6ce5f707c6 [DOCs/operators]: Release notes for v2025.15 gruyere (#5969)
* release notes and version bump up

* update stats
2025-08-21 11:49:52 +00:00
benedetta davico 766a1d4497 Merge pull request #5965 from nymtech/benny/fix-ns-agent-ci
fixing the ci for ns agent
2025-08-21 12:22:05 +02:00
benedetta davico 35c83f0a31 Merge pull request #5967 from nymtech/release/2025.15-gruyere
merge gruyere to develop
2025-08-21 12:20:43 +02:00
benedetta davico 01dd4a7972 Merge pull request #5958 from nymtech/bugfix/linux-build-ci
bugfix: fix ci-build for linux (and use updated runner)
2025-08-21 10:32:51 +02:00
Jędrzej Stuczyński c2e335557e Feature/testing utils (#5963)
* helper wrapper for stream-sink channel

* similar helper for async read/write

* example tests and clippy
2025-08-20 16:17:09 +01:00
benedettadavico 40e1cbc7a9 update changelog 2025-08-20 12:34:04 +02:00
Jędrzej Stuczyński c133e0e88b chore: updated refs to cheddar rev of nym repo (#5955)
* chore: updated refs to cheddar rev of nym repo

* update statistics-api version
2025-08-19 09:28:42 +01:00
Andrej Mihajlov 5b716633de Merge pull request #5960 from nymtech/am/update-strum
Migrate strum to 0.27.2
2025-08-18 18:28:22 +02:00
Andrej Mihajlov 834538300d Migrate to strum 0.27.2 2025-08-18 18:02:41 +02:00
Jędrzej Stuczyński bd0d70f7cd bugfix: fix ci-build for linux (and use updated runner) 2025-08-15 09:37:41 +01:00
Jędrzej Stuczyński 979485c582 http api client adjustment (#5953)
* missing feature lock for attempting to clone client

* added helper macro to generate user agent without additional imports
2025-08-13 12:52:16 +01:00
Bogdan-Ștefan Neacşu d95f66bd90 Move credential verifier in peer controller (#5938)
* Move credential verifier in peer controller

* Send back errors of peer controller
2025-08-13 13:09:44 +03:00
Jędrzej Stuczyński 906dfb2fb0 change PK/FK on expiration date signatures tables (#5934)
* update nym-credential-proxy

* update credential-storage

* update nym-api

* clippy
2025-08-12 09:03:53 +01:00
Jędrzej Stuczyński 7daa726626 feat: introduce additional checks when attempting to send to bounded channels (#5941)
* feat: introduce additional checks when attempting to send to bounded channels

or to a fallible gateway

* return error rather than panic when merging socket during shutdown
2025-08-11 09:15:12 +01:00
Jędrzej Stuczyński 067f492ad6 chore: fix rust 1.89 clippy issues (#5944) 2025-08-08 13:03:05 +01:00
Jędrzej Stuczyński ed73ec9ce6 chore: remove unused import (#5942) 2025-08-07 09:56:08 +01:00
import this 61606630bd [DOCs/operators]: Release notes v2025.14-feta (#5935)
* update release

* fix typos
2025-08-06 08:24:38 +00:00
benedettadavico 2d3deeb424 bump versions 2025-08-06 09:56:08 +02:00
benedetta davico 3827dc357d Merge pull request #5936 from nymtech/release/2025.14-feta
Merge release/2025.14-feta to develop
2025-08-06 09:54:29 +02:00
benedetta davico d01867ca8d save version 2025-07-25 11:27:42 +02:00
benedetta davico 502c63b291 Fix broken CI 2025-07-25 11:19:27 +02:00
150 changed files with 2377 additions and 2070 deletions
@@ -21,7 +21,7 @@ jobs:
strategy:
fail-fast: false
matrix:
platform: [ arc-linux-latest ]
platform: [ arc-ubuntu-22.04 ]
runs-on: ${{ matrix.platform }}
env:
+7 -7
View File
@@ -38,7 +38,7 @@ jobs:
strategy:
fail-fast: false
matrix:
os: [ arc-ubuntu-22.04, custom-windows-11, custom-macos-15 ]
os: [ arc-linux-latest, custom-windows-11, custom-macos-15 ]
runs-on: ${{ matrix.os }}
env:
CARGO_TERM_COLOR: always
@@ -46,9 +46,9 @@ jobs:
RUSTUP_PERMIT_COPY_RENAME: 1
steps:
- name: Install Dependencies (Linux)
run: sudo apt-get update && sudo apt-get -y install libwebkit2gtk-4.0-dev build-essential curl wget libssl-dev libgtk-3-dev libudev-dev squashfs-tools protobuf-compiler
run: sudo apt-get update && sudo apt-get -y install libwebkit2gtk-4.0-dev build-essential curl wget libssl-dev libgtk-3-dev libudev-dev squashfs-tools protobuf-compiler cmake
continue-on-error: true
if: contains(matrix.os, 'ubuntu')
if: contains(matrix.os, 'linux')
- name: Check out repository code
uses: actions/checkout@v4
@@ -63,7 +63,7 @@ jobs:
# To avoid running out of disk space, skip generating debug symbols
- name: Set debug to false (unix)
if: contains(matrix.os, 'ubuntu') || contains(matrix.os, 'mac')
if: contains(matrix.os, 'linux') || contains(matrix.os, 'mac')
run: |
sed -i.bak 's/\[profile.dev\]/\[profile.dev\]\ndebug = false/' Cargo.toml
git diff
@@ -93,14 +93,14 @@ jobs:
command: build
- name: Build all examples
if: contains(matrix.os, 'ubuntu')
if: contains(matrix.os, 'linux')
uses: actions-rs/cargo@v1
with:
command: build
args: --workspace --examples
- name: Run all tests
if: contains(matrix.os, 'ubuntu')
if: contains(matrix.os, 'linux')
uses: actions-rs/cargo@v1
env:
NYM_API: https://sandbox-nym-api1.nymtech.net/api
@@ -109,7 +109,7 @@ jobs:
args: --workspace
- name: Run expensive tests
if: (github.ref == 'refs/heads/develop' || github.event.pull_request.base.ref == 'develop' || github.event.pull_request.base.ref == 'master') && contains(matrix.os, 'ubuntu')
if: (github.ref == 'refs/heads/develop' || github.event.pull_request.base.ref == 'develop' || github.event.pull_request.base.ref == 'master') && contains(matrix.os, 'linux')
uses: actions-rs/cargo@v1
with:
command: test
@@ -40,7 +40,8 @@ jobs:
- name: Get version from cargo.toml
id: get_version
run: |
yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml
VERSION=$(yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml)
echo "result=$VERSION" >> $GITHUB_OUTPUT
- name: cleanup-gateway-probe-ref
id: cleanup_gateway_probe_ref
@@ -52,13 +53,16 @@ jobs:
- name: Set GIT_TAG variable
run: echo "GIT_TAG=${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }}-${{ steps.cleanup_gateway_probe_ref.outputs.git_ref }}" >> $GITHUB_ENV
- name: Set RELEASE_TAG variable
- name: Initialize RELEASE_TAG
run: echo "RELEASE_TAG=" >> $GITHUB_ENV
- name: Set RELEASE_TAG for release
if: github.event.inputs.release_image == 'true'
run: echo "RELEASE_TAG=golden-" >> $GITHUB_ENV
- name: Set IMAGE_NAME_AND_TAGS variable
run: echo "IMAGE_NAME_AND_TAGS=${{ env.CONTAINER_NAME }}:${{ env.RELEASE_TAG }}${{ steps.get_version.outputs.result }}-${{ steps.cleanup_gateway_probe_ref.outputs.git_ref }}" >> $GITHUB_ENV
- name: New env vars
run: echo "RELEASE_TAG='$RELEASE_TAG' GIT_TAG='$GIT_TAG' IMAGE_NAME_AND_TAGS='$IMAGE_NAME_AND_TAGS'"
+9 -5
View File
@@ -34,18 +34,22 @@ jobs:
- name: Get version from cargo.toml
id: get_version
run: |
yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml
VERSION=$(yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml)
echo "result=$VERSION" >> $GITHUB_OUTPUT
- name: Set GIT_TAG variable
run: echo "GIT_TAG=${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }}" >> $GITHUB_ENV
- name: Set RELEASE_TAG variable
- name: Initialise RELEASE_TAG
run: echo "RELEASE_TAG=" >> $GITHUB_ENV
- name: Set RELEASE_TAG for release
if: github.event.inputs.release_image == 'true'
run: echo "RELEASE_TAG=golden-" >> $GITHUB_ENV
- name: Set IMAGE_NAME_AND_TAGS variable
run: echo "IMAGE_NAME_AND_TAGS=${{ env.CONTAINER_NAME }}:${{ env.RELEASE_TAG }}${{ steps.get_version.outputs.result }}" >> $GITHUB_ENV
- name: New env vars
run: echo "RELEASE_TAG='$RELEASE_TAG' GIT_TAG='$GIT_TAG' IMAGE_NAME_AND_TAGS='$IMAGE_NAME_AND_TAGS'"
@@ -65,6 +69,6 @@ jobs:
- name: BuildAndPushImageOnHarbor
run: |
docker build -f ${{ env.WORKING_DIRECTORY }}/Dockerfile . -t harbor.nymte.ch/nym/${{ env.IMAGE_NAME_AND_TAGS }} -t harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }}:latest
docker build -f ${{ env.WORKING_DIRECTORY }}/Dockerfile-sqlite . -t harbor.nymte.ch/nym/${{ env.IMAGE_NAME_AND_TAGS }} -t harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }}:latest
docker push harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }} --all-tags
+36
View File
@@ -4,6 +4,42 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
## [Unreleased]
## [2025.15-gruyere] (2025-08-20)
- Migrate strum to 0.27.2 ([#5960])
- WG exit policy scripts update ([#5921])
- Make DNS Resolver fallback optional ([#5920])
- nym-node debug command to reset providers db ([#5914])
- basic zulip client for sending messages ([#5913])
- chore: allow compatibility with 'CDLA-Permissive-2.0' ([#5910])
- feat: ecash liveness check ([#5890])
- Remove old free credential handle ([#5864])
[#5960]: https://github.com/nymtech/nym/pull/5960
[#5921]: https://github.com/nymtech/nym/pull/5921
[#5920]: https://github.com/nymtech/nym/pull/5920
[#5914]: https://github.com/nymtech/nym/pull/5914
[#5913]: https://github.com/nymtech/nym/pull/5913
[#5910]: https://github.com/nymtech/nym/pull/5910
[#5890]: https://github.com/nymtech/nym/pull/5890
[#5864]: https://github.com/nymtech/nym/pull/5864
## [2025.14-feta] (2025-08-05)
- chore: nym node tokio console ([#5909])
- Feature/dkg snapshot epoch ([#5900])
- Feature/dkg epoch dealers query ([#5899])
- sqlx-pool-guard: allocate more memory on windows ([#5896])
- Support mnemonic in the NS agent ([#5883])
- Allow PG database backend ([#5880])
[#5909]: https://github.com/nymtech/nym/pull/5909
[#5900]: https://github.com/nymtech/nym/pull/5900
[#5899]: https://github.com/nymtech/nym/pull/5899
[#5896]: https://github.com/nymtech/nym/pull/5896
[#5883]: https://github.com/nymtech/nym/pull/5883
[#5880]: https://github.com/nymtech/nym/pull/5880
## [2025.13-emmental] (2025-07-22)
- fix: don't allow mixnode running in exit mode ([#5898])
Generated
+544 -1296
View File
File diff suppressed because it is too large Load Diff
+5 -5
View File
@@ -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",
@@ -102,7 +102,8 @@ members = [
"common/wasm/storage",
"common/wasm/utils",
"common/wireguard",
"common/wireguard-types", "common/zulip-client",
"common/wireguard-types",
"common/zulip-client",
"documentation/autodoc",
"gateway",
"nym-api",
@@ -320,8 +321,8 @@ si-scale = "0.2.3"
snow = "0.9.6"
sphinx-packet = "=0.6.0"
sqlx = "0.8.6"
strum = "0.26"
strum_macros = "0.26"
strum = "0.27.2"
strum_macros = "0.27.2"
subtle-encoding = "0.5"
syn = "1"
sysinfo = "0.33.0"
@@ -450,4 +451,3 @@ exit = "deny"
panic = "deny"
unimplemented = "deny"
unreachable = "deny"
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "nym-client"
version = "1.1.60"
version = "1.1.61"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>", "Jędrzej Stuczyński <andrew@nymtech.net>"]
description = "Implementation of the Nym Client"
edition = "2021"
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "nym-socks5-client"
version = "1.1.60"
version = "1.1.61"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>"]
description = "A SOCKS5 localhost proxy that converts incoming messages to Sphinx and sends them to a Nym address"
edition = "2021"
+2 -2
View File
@@ -207,7 +207,7 @@ where
<St as Storage>::StorageError: Send + Sync + 'static,
{
if let Some(stored) = storage
.get_expiration_date_signatures(expiration_date)
.get_expiration_date_signatures(expiration_date, epoch_id)
.await
.map_err(BandwidthControllerError::credential_storage_error)?
{
@@ -220,7 +220,7 @@ where
ecash_apis,
|api| async move {
api.api_client
.global_expiration_date_signatures(Some(expiration_date))
.global_expiration_date_signatures(Some(expiration_date), Some(epoch_id))
.await
},
format!("aggregated coin index signatures for date {expiration_date}"),
@@ -96,35 +96,59 @@ impl MixTrafficController {
mut mix_packets: Vec<MixPacket>,
) -> Result<(), ErasedGatewayError> {
debug_assert!(!mix_packets.is_empty());
let result = if mix_packets.len() == 1 {
let send_future = if mix_packets.len() == 1 {
// SAFETY: we just checked we have one packet
#[allow(clippy::unwrap_used)]
let mix_packet = mix_packets.pop().unwrap();
self.gateway_transceiver.send_mix_packet(mix_packet).await
self.gateway_transceiver.send_mix_packet(mix_packet)
} else {
self.gateway_transceiver
.batch_send_mix_packets(mix_packets)
.await
self.gateway_transceiver.batch_send_mix_packets(mix_packets)
};
if result.is_err() {
self.consecutive_gateway_failure_count += 1;
} else {
trace!("We *might* have managed to forward sphinx packet(s) to the gateway!");
self.consecutive_gateway_failure_count = 0;
}
tokio::select! {
biased;
_ = self.task_client.recv() => {
trace!("received shutdown while handling messages");
Ok(())
}
result = send_future => {
if result.is_err() {
self.consecutive_gateway_failure_count += 1;
} else {
trace!("We *might* have managed to forward sphinx packet(s) to the gateway!");
self.consecutive_gateway_failure_count = 0;
}
result
result
}
}
}
async fn on_client_request(&mut self, client_request: ClientRequest) {
tokio::select! {
biased;
_ = self.task_client.recv() => {
trace!("received shutdown while handling client request");
}
result = self.gateway_transceiver.send_client_request(client_request) => {
if let Err(err) = result {
error!("Failed to send client request: {err}")
}
}
}
}
pub fn start(mut self) {
spawn_future!(
async move {
debug!("Started MixTrafficController with graceful shutdown support");
while !self.task_client.is_shutdown() {
tokio::select! {
biased;
_ = self.task_client.recv() => {
tracing::trace!("MixTrafficController: Received shutdown");
break;
}
mix_packets = self.mix_rx.recv() => match mix_packets {
Some(mix_packets) => {
if let Err(err) = self.on_messages(mix_packets).await {
@@ -147,23 +171,15 @@ impl MixTrafficController {
},
client_request = self.client_rx.recv() => match client_request {
Some(client_request) => {
match self.gateway_transceiver.send_client_request(client_request).await {
Ok(_) => (),
Err(e) => error!("Failed to send client request: {e}"),
};
self.on_client_request(client_request).await;
},
None => {
tracing::trace!("MixTrafficController, client request channel closed");
}
},
_ = self.task_client.recv() => {
tracing::trace!("MixTrafficController: Received shutdown");
break;
}
}
}
self.task_client.recv_timeout().await;
tracing::debug!("MixTrafficController: Exiting");
},
"MixTrafficController"
@@ -226,6 +226,11 @@ where
while !self.task_client.is_shutdown() {
tokio::select! {
biased;
_ = self.task_client.recv() => {
tracing::trace!("InputMessageListener: Received shutdown");
break;
}
input_msg = self.input_receiver.recv() => match input_msg {
Some(input_msg) => {
self.on_input_message(input_msg).await;
@@ -235,9 +240,7 @@ where
break;
}
},
_ = self.task_client.recv() => {
tracing::trace!("InputMessageListener: Received shutdown");
}
}
}
self.task_client.recv_timeout().await;
@@ -179,6 +179,11 @@ where
while !self.task_client.is_shutdown() {
tokio::select! {
biased;
_ = self.task_client.recv() => {
tracing::trace!("RetransmissionRequestListener: Received shutdown");
break;
}
timed_out_ack = self.request_receiver.next() => match timed_out_ack {
Some(timed_out_ack) => self.on_retransmission_request(timed_out_ack, packet_type).await,
None => {
@@ -186,9 +191,7 @@ where
break;
}
},
_ = self.task_client.recv() => {
tracing::trace!("RetransmissionRequestListener: Received shutdown");
}
}
}
self.task_client.recv_timeout().await;
@@ -548,6 +548,7 @@ where
pending_acks.push(pending_ack);
}
drop(topology_permit);
self.insert_pending_acks(pending_acks);
self.forward_messages(real_messages, lane).await;
@@ -730,17 +731,21 @@ where
// tells real message sender (with the poisson timer) to send this to the mix network
pub(crate) async fn forward_messages(
&self,
&mut self,
messages: Vec<RealMessage>,
transmission_lane: TransmissionLane,
) {
if let Err(err) = self
.real_message_sender
.send((messages, transmission_lane))
.await
{
if !self.task_client.is_shutdown_poll() {
error!("Failed to forward messages to the real message sender: {err}");
tokio::select! {
biased;
_ = self.task_client.recv() => {
trace!("received shutdown while attempting to forward mixnet messages");
}
sending_res = self.real_message_sender.send((messages, transmission_lane)) => {
if sending_res.is_err() {
error!(
"failed to forward mixnet messages due to closed channel (outside of shutdown!)"
);
}
}
}
}
@@ -280,17 +280,33 @@ where
}
};
if let Err(err) = self.mix_tx.send(vec![next_message]).await {
if !self.task_client.is_shutdown_poll() {
tracing::error!("Failed to send: {err}");
let sending_res = tokio::select! {
biased;
_ = self.task_client.recv() => {
trace!("received shutdown signal while attempting to send mix message");
return
}
sending_res = self.mix_tx.send(vec![next_message]) => {
sending_res
}
};
match sending_res {
Err(_) => {
if !self.task_client.is_shutdown_poll() {
tracing::error!(
"failed to send mixnet packet due to closed channel (outside of shutdown!)"
);
}
}
Ok(_) => {
let event = if fragment_id.is_some() {
PacketStatisticsEvent::RealPacketSent(packet_size)
} else {
PacketStatisticsEvent::CoverPacketSent(packet_size)
};
self.stats_tx.report(event.into());
}
} else {
let event = if fragment_id.is_some() {
PacketStatisticsEvent::RealPacketSent(packet_size)
} else {
PacketStatisticsEvent::CoverPacketSent(packet_size)
};
self.stats_tx.report(event.into());
}
// notify ack controller about sending our message only after we actually managed to push it
@@ -126,7 +126,7 @@ impl TopologyAccessor {
.map(|p| p.topology.clone())
}
pub async fn current_route_provider(&self) -> Option<RwLockReadGuard<NymRouteProvider>> {
pub async fn current_route_provider(&self) -> Option<RwLockReadGuard<'_, NymRouteProvider>> {
let provider = self.inner.topology.read().await;
if provider.topology.is_empty() {
None
+1 -1
View File
@@ -148,7 +148,7 @@ async fn connect(endpoint: &str) -> Result<WsConn, ClientCoreError> {
JSWebsocket::new(endpoint).map_err(|_| ClientCoreError::GatewayJsConnectionFailure)
}
async fn measure_latency<G>(gateway: &G) -> Result<GatewayWithLatency<G>, ClientCoreError>
async fn measure_latency<G>(gateway: &G) -> Result<GatewayWithLatency<'_, G>, ClientCoreError>
where
G: ConnectableGateway,
{
@@ -201,7 +201,7 @@ impl<C, St> GatewayClient<C, St> {
#[cfg(not(target_arch = "wasm32"))]
pub async fn establish_connection(&mut self) -> Result<(), GatewayClientError> {
debug!(
"Attemting to establish connection to gateway at: {}",
"Attempting to establish connection to gateway at: {}",
self.gateway_address
);
let (ws_stream, _) = connect_async(
@@ -337,7 +337,7 @@ impl PartiallyDelegatedHandle {
// check if the split stream didn't error out
let receive_res = stream_receiver
.try_recv()
.expect("stream sender was somehow dropped without sending anything!");
.map_err(|_| GatewayClientError::ConnectionAbruptlyClosed)?;
if let Some(res) = receive_res {
let _res = res?;
@@ -719,10 +719,11 @@ impl NymApiClient {
pub async fn partial_expiration_date_signatures(
&self,
expiration_date: Option<Date>,
epoch_id: Option<EpochId>,
) -> Result<PartialExpirationDateSignatureResponse, ValidatorClientError> {
Ok(self
.nym_api
.partial_expiration_date_signatures(expiration_date)
.partial_expiration_date_signatures(expiration_date, epoch_id)
.await?)
}
@@ -739,10 +740,11 @@ impl NymApiClient {
pub async fn global_expiration_date_signatures(
&self,
expiration_date: Option<Date>,
epoch_id: Option<EpochId>,
) -> Result<AggregatedExpirationDateSignatureResponse, ValidatorClientError> {
Ok(self
.nym_api
.global_expiration_date_signatures(expiration_date)
.global_expiration_date_signatures(expiration_date, epoch_id)
.await?)
}
@@ -1103,8 +1103,9 @@ pub trait NymApiClientExt: ApiClient {
async fn partial_expiration_date_signatures(
&self,
expiration_date: Option<Date>,
epoch_id: Option<EpochId>,
) -> Result<PartialExpirationDateSignatureResponse, NymAPIError> {
let params = match expiration_date {
let mut params = match expiration_date {
None => Vec::new(),
Some(exp) => vec![(
ecash::EXPIRATION_DATE_PARAM,
@@ -1112,6 +1113,10 @@ pub trait NymApiClientExt: ApiClient {
)],
};
if let Some(epoch_id) = epoch_id {
params.push((ecash::EPOCH_ID_PARAM, epoch_id.to_string()));
}
self.get_json(
&[
routes::V1_API_VERSION,
@@ -1148,8 +1153,9 @@ pub trait NymApiClientExt: ApiClient {
async fn global_expiration_date_signatures(
&self,
expiration_date: Option<Date>,
epoch_id: Option<EpochId>,
) -> Result<AggregatedExpirationDateSignatureResponse, NymAPIError> {
let params = match expiration_date {
let mut params = match expiration_date {
None => Vec::new(),
Some(exp) => vec![(
ecash::EXPIRATION_DATE_PARAM,
@@ -1157,6 +1163,10 @@ pub trait NymApiClientExt: ApiClient {
)],
};
if let Some(epoch_id) = epoch_id {
params.push((ecash::EPOCH_ID_PARAM, epoch_id.to_string()));
}
self.get_json(
&[
routes::V1_API_VERSION,
+1 -1
View File
@@ -86,7 +86,7 @@ pub async fn execute(args: Args) -> anyhow::Result<()> {
anyhow!("ticketbook got incorrectly imported - the master verification key is missing")
})?;
let expiration_signatures = persistent_storage
.get_expiration_date_signatures(expiration_date)
.get_expiration_date_signatures(expiration_date, epoch_id)
.await?
.ok_or_else(|| {
anyhow!(
@@ -120,7 +120,7 @@ async fn issue_to_file(args: Args, client: SigningClient) -> anyhow::Result<()>
if args.include_expiration_date_signatures {
let signatures = credentials_store
.get_expiration_date_signatures(expiration_date)
.get_expiration_date_signatures(expiration_date, epoch_id)
.await?
.ok_or(anyhow!("missing expiration date signatures!"))?;
@@ -241,7 +241,7 @@ pub async fn delegate_to_multiple_mixnodes(args: Args, client: SigningClient) {
let node_id = row.node_id.clone().parse::<u32>().unwrap();
let coins: Vec<Coin> = vec![];
undelegation_msgs.push((ExecuteMsg::Undelegate { node_id }, coins));
undelegation_table.add_row(&[row.node_id.clone()]);
undelegation_table.add_row(std::slice::from_ref(&row.node_id));
if row.amount.amount > 0 {
delegation_msgs
@@ -188,7 +188,7 @@ impl<C> ContractTesterBuilder<C> {
*self.app.api()
}
pub fn querier(&self) -> QuerierWrapper {
pub fn querier(&self) -> QuerierWrapper<'_> {
self.app.wrap()
}
}
@@ -57,7 +57,7 @@ pub trait NodeBond {
fn is_unbonding(&self) -> bool;
fn identity(&self) -> IdentityKeyRef;
fn identity(&self) -> IdentityKeyRef<'_>;
fn original_pledge(&self) -> &Coin;
@@ -125,7 +125,7 @@ impl NodeBond for MixNodeBond {
self.is_unbonding
}
fn identity(&self) -> IdentityKeyRef {
fn identity(&self) -> IdentityKeyRef<'_> {
self.identity()
}
@@ -178,7 +178,7 @@ impl NodeBond for NymNodeBond {
self.is_unbonding
}
fn identity(&self) -> IdentityKeyRef {
fn identity(&self) -> IdentityKeyRef<'_> {
self.identity()
}
@@ -58,7 +58,7 @@ impl<'a> PrimaryKey<'a> for Role {
type Suffix = <u8 as PrimaryKey<'a>>::Suffix;
type SuperSuffix = <u8 as PrimaryKey<'a>>::SuperSuffix;
fn key(&self) -> Vec<Key> {
fn key(&self) -> Vec<Key<'_>> {
// I'm not sure why it wasn't possible to delegate the call to
// `(*self as u8).key()` directly...
// I guess because of the `Key::Ref(&'a [u8])` variant?
@@ -0,0 +1,123 @@
/*
* Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
* SPDX-License-Identifier: Apache-2.0
*/
-- 1. add temporary `epoch_id` column
ALTER TABLE pending_issuance
ADD COLUMN epoch_id INTEGER;
-- 2. populate the value
UPDATE pending_issuance
SET epoch_id = (SELECT epoch_id
FROM expiration_date_signatures
WHERE expiration_date_signatures.expiration_date = pending_issuance.expiration_date);
-- 3. create new expiration_date_signatures table (with changed constraints)
CREATE TABLE expiration_date_signatures_new
(
expiration_date DATE NOT NULL,
epoch_id INTEGER NOT NULL,
serialization_revision INTEGER NOT NULL,
-- combined signatures for all tuples issued for given day
serialised_signatures BLOB NOT NULL,
PRIMARY KEY (epoch_id, expiration_date)
);
-- 4. migrate the data
INSERT INTO expiration_date_signatures_new (expiration_date, epoch_id, serialization_revision, serialised_signatures)
SELECT expiration_date, epoch_id, serialization_revision, serialised_signatures
FROM expiration_date_signatures;
-- 5. drop and recreate the table references (due to new FK)
-- 5.1.
-- (data for ticketbooks that have an associated deposit, but failed to get issued)
CREATE TABLE pending_issuance_new
(
deposit_id INTEGER NOT NULL PRIMARY KEY,
-- introduce a way for us to introduce breaking changes in serialization of data
serialization_revision INTEGER NOT NULL,
pending_ticketbook_data BLOB NOT NULL UNIQUE,
-- for each ticketbook we MUST have corresponding expiration date signatures
expiration_date DATE NOT NULL,
epoch_id INTEGER NOT NULL,
-- for each ticketbook we MUST have corresponding expiration date signatures
FOREIGN KEY (epoch_id, expiration_date) REFERENCES expiration_date_signatures_new (epoch_id, expiration_date)
);
INSERT INTO pending_issuance_new (deposit_id, serialization_revision, pending_ticketbook_data, expiration_date,
epoch_id)
SELECT deposit_id, serialization_revision, pending_ticketbook_data, expiration_date, epoch_id
FROM pending_issuance;
-- 5.2.
CREATE TABLE ecash_ticketbook_new
(
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
-- introduce a way for us to introduce breaking changes in serialization of data
serialization_revision INTEGER NOT NULL,
-- the type of the associated ticketbook
ticketbook_type TEXT NOT NULL,
-- the actual crypto data of the ticketbook (wallet, keys, etc.)
ticketbook_data BLOB NOT NULL UNIQUE,
-- for each ticketbook we MUST have corresponding expiration date signatures
expiration_date DATE NOT NULL,
-- for each ticketbook we MUST have corresponding coin index signatures
epoch_id INTEGER NOT NULL,
-- the initial number of tickets the wallet has been created for
total_tickets INTEGER NOT NULL,
-- how many tickets have been used so far (the `l` value of the wallet)
used_tickets INTEGER NOT NULL,
-- FOREIGN KEYS:
-- for each ticketbook we MUST have corresponding coin index signatures
FOREIGN KEY (epoch_id) REFERENCES coin_indices_signatures (epoch_id),
-- for each ticketbook we MUST have corresponding expiration date signatures
FOREIGN KEY (expiration_date, epoch_id) REFERENCES expiration_date_signatures_new (expiration_date, epoch_id)
);
INSERT INTO ecash_ticketbook_new (id, serialization_revision, ticketbook_type, ticketbook_data, expiration_date,
epoch_id, total_tickets, used_tickets)
SELECT id,
serialization_revision,
ticketbook_type,
ticketbook_data,
expiration_date,
epoch_id,
total_tickets,
used_tickets
FROM ecash_ticketbook;
-- 6. finally swap out the old tables
-- drop old tables
DROP TABLE expiration_date_signatures;
DROP TABLE pending_issuance;
DROP TABLE ecash_ticketbook;
-- rename new tables
ALTER TABLE expiration_date_signatures_new
RENAME TO expiration_date_signatures;
ALTER TABLE pending_issuance_new
RENAME TO pending_issuance;
ALTER TABLE ecash_ticketbook_new
RENAME TO ecash_ticketbook;
@@ -28,7 +28,7 @@ struct EcashCredentialManagerInner {
pending: HashMap<i64, RetrievedPendingTicketbook>,
master_vk: HashMap<u64, VerificationKeyAuth>,
coin_indices_sigs: HashMap<u64, Vec<AnnotatedCoinIndexSignature>>,
expiration_date_sigs: HashMap<Date, Vec<AnnotatedExpirationDateSignature>>,
expiration_date_sigs: HashMap<(u64, Date), Vec<AnnotatedExpirationDateSignature>>,
_next_id: i64,
}
@@ -242,10 +242,14 @@ impl MemoryEcachTicketbookManager {
pub(crate) async fn get_expiration_date_signatures(
&self,
expiration_date: Date,
epoch_id: u64,
) -> Option<Vec<AnnotatedExpirationDateSignature>> {
let guard = self.inner.read().await;
guard.expiration_date_sigs.get(&expiration_date).cloned()
guard
.expiration_date_sigs
.get(&(epoch_id, expiration_date))
.cloned()
}
pub(crate) async fn insert_expiration_date_signatures(
@@ -254,8 +258,9 @@ impl MemoryEcachTicketbookManager {
) {
let mut guard = self.inner.write().await;
guard
.expiration_date_sigs
.insert(sigs.expiration_date, sigs.signatures.clone());
guard.expiration_date_sigs.insert(
(sigs.epoch_id, sigs.expiration_date),
sigs.signatures.clone(),
);
}
}
@@ -39,7 +39,7 @@ impl SqliteEcashTicketbookManager {
Ok(())
}
pub(crate) async fn begin_storage_tx(&self) -> Result<Transaction<Sqlite>, sqlx::Error> {
pub(crate) async fn begin_storage_tx(&self) -> Result<Transaction<'_, Sqlite>, sqlx::Error> {
self.connection_pool.begin().await
}
@@ -260,15 +260,17 @@ impl SqliteEcashTicketbookManager {
pub(crate) async fn get_expiration_date_signatures(
&self,
expiration_date: Date,
epoch_id: i64,
) -> Result<Option<RawExpirationDateSignatures>, sqlx::Error> {
sqlx::query_as!(
RawExpirationDateSignatures,
r#"
SELECT epoch_id as "epoch_id: u32", serialised_signatures, serialization_revision as "serialization_revision: u8"
SELECT serialised_signatures, serialization_revision as "serialization_revision: u8"
FROM expiration_date_signatures
WHERE expiration_date = ?
WHERE expiration_date = ? AND epoch_id = ?
"#,
expiration_date
expiration_date,
epoch_id
)
.fetch_optional(&*self.connection_pool)
.await
@@ -166,10 +166,11 @@ impl Storage for EphemeralStorage {
async fn get_expiration_date_signatures(
&self,
expiration_date: Date,
epoch_id: u64,
) -> Result<Option<Vec<AnnotatedExpirationDateSignature>>, Self::StorageError> {
Ok(self
.storage_manager
.get_expiration_date_signatures(expiration_date)
.get_expiration_date_signatures(expiration_date, epoch_id)
.await)
}
-1
View File
@@ -60,7 +60,6 @@ pub struct StoredPendingTicketbook {
#[cfg_attr(not(target_arch = "wasm32"), derive(sqlx::FromRow))]
pub struct RawExpirationDateSignatures {
pub epoch_id: u32,
pub serialised_signatures: Vec<u8>,
pub serialization_revision: u8,
}
@@ -325,10 +325,11 @@ impl Storage for PersistentStorage {
async fn get_expiration_date_signatures(
&self,
expiration_date: Date,
epoch_id: u64,
) -> Result<Option<Vec<AnnotatedExpirationDateSignature>>, Self::StorageError> {
let Some(raw) = self
.storage_manager
.get_expiration_date_signatures(expiration_date)
.get_expiration_date_signatures(expiration_date, epoch_id as i64)
.await?
else {
return Ok(None);
+1
View File
@@ -92,6 +92,7 @@ pub trait Storage: Clone + Send + Sync {
async fn get_expiration_date_signatures(
&self,
expiration_date: Date,
epoch_id: u64,
) -> Result<Option<Vec<AnnotatedExpirationDateSignature>>, Self::StorageError>;
async fn insert_expiration_date_signatures(
@@ -39,7 +39,7 @@ impl traits::EcashManager for EcashManager {
async fn verification_key(
&self,
epoch_id: EpochId,
) -> Result<RwLockReadGuard<VerificationKeyAuth>, EcashTicketError> {
) -> Result<RwLockReadGuard<'_, VerificationKeyAuth>, EcashTicketError> {
self.shared_state.verification_key(epoch_id).await
}
@@ -231,7 +231,7 @@ impl traits::EcashManager for MockEcashManager {
async fn verification_key(
&self,
_epoch_id: EpochId,
) -> Result<RwLockReadGuard<VerificationKeyAuth>, EcashTicketError> {
) -> Result<RwLockReadGuard<'_, VerificationKeyAuth>, EcashTicketError> {
Ok(self.verfication_key.read().await)
}
@@ -125,7 +125,7 @@ impl SharedState {
async fn set_epoch_data(
&self,
epoch_id: EpochId,
) -> Result<RwLockWriteGuard<BTreeMap<EpochId, EpochState>>, EcashTicketError> {
) -> Result<RwLockWriteGuard<'_, BTreeMap<EpochId, EpochState>>, EcashTicketError> {
let Some(threshold) = self.threshold(epoch_id).await? else {
return Err(EcashTicketError::DKGThresholdUnavailable { epoch_id });
};
@@ -186,7 +186,7 @@ impl SharedState {
pub(crate) async fn api_clients(
&self,
epoch_id: EpochId,
) -> Result<RwLockReadGuard<Vec<EcashApiClient>>, EcashTicketError> {
) -> Result<RwLockReadGuard<'_, Vec<EcashApiClient>>, EcashTicketError> {
let guard = self.epoch_data.read().await;
// the key was already in the map
@@ -212,7 +212,7 @@ impl SharedState {
pub(crate) async fn verification_key(
&self,
epoch_id: EpochId,
) -> Result<RwLockReadGuard<VerificationKeyAuth>, EcashTicketError> {
) -> Result<RwLockReadGuard<'_, VerificationKeyAuth>, EcashTicketError> {
let guard = self.epoch_data.read().await;
// the key was already in the map
@@ -235,11 +235,11 @@ impl SharedState {
}))
}
pub(crate) async fn start_tx(&self) -> RwLockWriteGuard<DirectSigningHttpRpcNyxdClient> {
pub(crate) async fn start_tx(&self) -> RwLockWriteGuard<'_, DirectSigningHttpRpcNyxdClient> {
self.nyxd_client.write().await
}
pub(crate) async fn start_query(&self) -> RwLockReadGuard<DirectSigningHttpRpcNyxdClient> {
pub(crate) async fn start_query(&self) -> RwLockReadGuard<'_, DirectSigningHttpRpcNyxdClient> {
self.nyxd_client.read().await
}
@@ -12,7 +12,7 @@ pub trait EcashManager {
async fn verification_key(
&self,
epoch_id: EpochId,
) -> Result<RwLockReadGuard<VerificationKeyAuth>, EcashTicketError>;
) -> Result<RwLockReadGuard<'_, VerificationKeyAuth>, EcashTicketError>;
fn storage(&self) -> Box<dyn BandwidthGatewayStorage + Send + Sync>;
async fn check_payment(
&self,
+1
View File
@@ -15,6 +15,7 @@ bls12_381 = { workspace = true, default-features = false }
serde = { workspace = true, features = ["derive"] }
thiserror = { workspace = true }
strum = { workspace = true, features = ["derive"] }
strum_macros = { workspace = true }
time = { workspace = true, features = ["serde"] }
utoipa = { workspace = true }
rand = { workspace = true }
+3 -3
View File
@@ -229,9 +229,9 @@ impl From<PayInfo> for NymPayInfo {
Serialize,
Deserialize,
Hash,
strum::Display,
strum::EnumString,
strum::EnumIter,
strum_macros::Display,
strum_macros::EnumString,
strum_macros::EnumIter,
)]
#[serde(rename_all = "kebab-case")]
#[strum(serialize_all = "kebab-case")]
+1 -1
View File
@@ -51,7 +51,7 @@ pub async fn obtain_expiration_date_signatures(
for ecash_api_client in ecash_api_clients.iter() {
match ecash_api_client
.api_client
.partial_expiration_date_signatures(None)
.partial_expiration_date_signatures(None, None)
.await
{
Ok(signature) => {
+3
View File
@@ -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"] }
@@ -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(())
}
}
+5
View File
@@ -173,6 +173,10 @@ mod path;
#[cfg(not(target_arch = "wasm32"))]
pub use dns::{HickoryDnsError, HickoryDnsResolver};
// helper for generating user agent based on binary information
#[doc(hidden)]
pub use nym_bin_common::bin_info;
/// Default HTTP request connection timeout.
///
/// The timeout is relatively high as we are often making requests over the mixnet, where latency is
@@ -608,6 +612,7 @@ impl Client {
current_idx: Arc::new(Default::default()),
reqwest_client: self.reqwest_client.clone(),
#[cfg(feature = "tunneling")]
front: self.front.clone(),
retry_limit: self.retry_limit,
+10
View File
@@ -20,6 +20,16 @@ pub struct UserAgent {
pub git_commit: String,
}
/// Create `UserAgent` based on the caller's crate information
// we can't use normal function as then `application` and `version` would correspond
// of that of `nym-http-api-client` lib
#[macro_export]
macro_rules! generate_user_agent {
() => {
$crate::UserAgent::from($crate::bin_info!())
};
}
#[derive(Clone, Debug, thiserror::Error)]
#[error("invalid user agent string: {0}")]
pub struct UserAgentError(String);
+2 -2
View File
@@ -25,8 +25,8 @@ pub enum NymIdError {
#[error("attempted to import an expired credential (it expired on {expiration})")]
ExpiredCredentialImport { expiration: Date },
#[error("could not import ticketbook expiring at {date} since we do not have corresponding expiration date signatures")]
MissingExpirationDateSignatures { date: Date },
#[error("could not import ticketbook expiring at {date} for epoch {epoch_id} since we do not have corresponding expiration date signatures")]
MissingExpirationDateSignatures { date: Date, epoch_id: u64 },
#[error("could not import ticketbook for epoch {epoch_id} since we do not have corresponding coin index signatures")]
MissingCoinIndexSignatures { epoch_id: u64 },
@@ -99,7 +99,7 @@ where
// in order to import the ticketbook we MUST have the appropriate signatures in the storage already
if credentials_store
.get_expiration_date_signatures(ticketbook.expiration_date())
.get_expiration_date_signatures(ticketbook.expiration_date(), ticketbook.epoch_id())
.await
.map_err(|source| NymIdError::StorageError {
source: Box::new(source),
@@ -108,6 +108,7 @@ where
{
return Err(NymIdError::MissingExpirationDateSignatures {
date: ticketbook.expiration_date(),
epoch_id: ticketbook.epoch_id(),
});
}
+2
View File
@@ -14,6 +14,7 @@ pin-project = { workspace = true }
sha2 = { workspace = true }
snow = { workspace = true }
strum = { workspace = true, features = ["derive"] }
strum_macros = { workspace = true }
thiserror = { workspace = true }
tokio = { workspace = true, features = ["net", "io-util", "time"] }
tokio-util = { workspace = true, features = ["codec"] }
@@ -27,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]
+1 -1
View File
@@ -13,7 +13,7 @@ use nym_crypto::asymmetric::x25519;
use nym_noise_keys::{NoiseVersion, VersionedNoiseKey};
use snow::params::NoiseParams;
use strum::{EnumIter, FromRepr};
use strum_macros::{EnumIter, FromRepr};
#[derive(Default, Debug, Clone, Copy, EnumIter, FromRepr, Eq, PartialEq)]
#[repr(u8)]
+1 -1
View File
@@ -5,7 +5,7 @@ use crate::config::NoisePattern;
use crate::error::NoiseError;
use bytes::{Buf, BufMut, Bytes, BytesMut};
use nym_noise_keys::NoiseVersion;
use strum::FromRepr;
use strum_macros::FromRepr;
#[derive(Debug)]
pub struct NymNoiseFrame {
+9 -117
View File
@@ -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();
@@ -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);
@@ -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);
+1
View File
@@ -21,6 +21,7 @@ time = { workspace = true }
tokio = { workspace = true }
si-scale = { workspace = true }
strum = { workspace = true }
strum_macros = { workspace = true }
nym-crypto = { path = "../crypto" }
nym-sphinx = { path = "../nymsphinx" }
+2 -2
View File
@@ -7,8 +7,8 @@ use serde::{Deserialize, Serialize};
PartialEq,
Copy,
Clone,
strum::Display,
strum::EnumString,
strum_macros::Display,
strum_macros::EnumString,
Serialize,
Deserialize,
Default,
+2
View File
@@ -10,6 +10,7 @@ where
}
#[cfg(not(target_arch = "wasm32"))]
#[track_caller]
pub fn spawn<F>(future: F)
where
F: Future + Send + 'static,
@@ -18,6 +19,7 @@ where
tokio::spawn(future);
}
#[track_caller]
pub fn spawn_with_report_error<F, T, E>(future: F, mut shutdown: TaskClient)
where
F: Future<Output = Result<T, E>> + Send + 'static,
+23
View File
@@ -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
View File
@@ -0,0 +1,33 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// 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
View File
@@ -0,0 +1,6 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
pub mod helpers;
pub mod mocks;
pub mod traits;
@@ -0,0 +1,161 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::mocks::shared::InnerWrapper;
use futures::ready;
use std::io;
use std::pin::Pin;
use std::task::{Context, Poll};
use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
// sending buffer of the first stream is the receiving buffer of the second stream
// and vice versa
pub fn mock_io_streams() -> (MockIOStream, MockIOStream) {
let ch1 = MockIOStream::default();
let ch2 = ch1.make_connection();
(ch1, ch2)
}
#[derive(Default)]
pub struct MockIOStream {
// messages to send
tx: InnerWrapper<Vec<u8>>,
// messages to receive
rx: InnerWrapper<Vec<u8>>,
}
impl MockIOStream {
fn make_connection(&self) -> Self {
MockIOStream {
tx: self.rx.cloned_buffer(),
rx: self.tx.cloned_buffer(),
}
}
// unwrap in test code is fine
#[allow(clippy::unwrap_used)]
pub fn unchecked_tx_data(&self) -> Vec<u8> {
self.tx.buffer.try_lock().unwrap().content.clone()
}
// unwrap in test code is fine
#[allow(clippy::unwrap_used)]
pub fn unchecked_rx_data(&self) -> Vec<u8> {
self.rx.buffer.try_lock().unwrap().content.clone()
}
}
impl AsyncRead for MockIOStream {
fn poll_read(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut ReadBuf<'_>,
) -> Poll<io::Result<()>> {
ready!(Pin::new(&mut self.rx).poll_guard_ready(cx));
// SAFETY: guard is ready
#[allow(clippy::unwrap_used)]
let guard = self.rx.guard().unwrap();
let data = guard.take_content();
if data.is_empty() {
// nothing to retrieve - store the waiter so that the sender could trigger it
guard.waker = Some(cx.waker().clone());
// drop the guard so that the sender could actually put messages in
self.rx.transition_to_idle();
return Poll::Pending;
}
// if let Some(waker) = guard.waker.take() {
// waker.wake();
// }
self.rx.transition_to_idle();
buf.put_slice(&data);
Poll::Ready(Ok(()))
}
}
impl AsyncWrite for MockIOStream {
fn poll_write(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<io::Result<usize>> {
// wait until we transition to the locked state
ready!(Pin::new(&mut self.tx).poll_guard_ready(cx));
// SAFETY: guard is ready
#[allow(clippy::unwrap_used)]
let guard = self.tx.guard().unwrap();
let len = buf.len();
guard.content.extend_from_slice(buf);
// TODO: if we wanted the behaviour of always reading everything before writing anything extra
// if !guard.content.is_empty() {
// // sanity check
// assert!(guard.waker.is_none());
// guard.waker = Some(cx.waker().clone());
// self.tx.transition_to_idle();
// return Poll::Pending;
// }
Poll::Ready(Ok(len))
}
fn poll_flush(mut self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<io::Result<()>> {
let Some(guard) = self.tx.guard() else {
return Poll::Ready(Err(io::Error::other(
"invalid lock state to send/flush messages",
)));
};
if let Some(waker) = guard.waker.take() {
// notify the receiver if it was waiting for messages
waker.wake();
}
// release the guard
self.tx.transition_to_idle();
Poll::Ready(Ok(()))
}
fn poll_shutdown(mut self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<io::Result<()>> {
// make sure our guard is always dropped on close
self.tx.transition_to_idle();
Poll::Ready(Ok(()))
}
}
#[cfg(test)]
mod tests {
use super::*;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
#[tokio::test]
async fn basic() {
let (mut stream1, mut stream2) = mock_io_streams();
stream1.write_all(&[1, 2, 3, 4, 5]).await.unwrap();
stream1.flush().await.unwrap();
let mut buf = [0u8; 5];
let read = stream2.read(&mut buf).await.unwrap();
assert_eq!(read, 5);
assert_eq!(&buf[0..5], &[1, 2, 3, 4, 5]);
let mut buf = [0u8; 5];
stream2.write_all(&[6, 7, 8, 9, 10]).await.unwrap();
stream2.flush().await.unwrap();
let read = stream1.read(&mut buf).await.unwrap();
assert_eq!(read, 5);
assert_eq!(&buf[0..5], &[6, 7, 8, 9, 10]);
}
}
+6
View File
@@ -0,0 +1,6 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
pub mod async_read_write;
mod shared;
pub mod stream_sink;
+109
View File
@@ -0,0 +1,109 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use futures::future::BoxFuture;
use futures::{ready, FutureExt};
use std::mem;
use std::pin::Pin;
use std::sync::Arc;
use std::task::{Context, Poll, Waker};
use tokio::sync::{Mutex, OwnedMutexGuard};
#[derive(Default)]
pub(crate) struct InnerWrapper<T: 'static> {
pub(crate) buffer: Arc<Mutex<ContentWrapper<T>>>,
lock_state: LockState<T>,
}
impl<T: Send> InnerWrapper<T> {
pub(crate) fn clone_buffer(&self) -> Arc<Mutex<ContentWrapper<T>>> {
Arc::clone(&self.buffer)
}
pub(crate) fn cloned_buffer(&self) -> Self {
assert!(matches!(self.lock_state, LockState::Idle));
InnerWrapper {
buffer: self.clone_buffer(),
lock_state: LockState::Idle,
}
}
// NOTE: it's responsibility of the caller to ensure the guard is released and state transitions to idle!
pub(crate) fn poll_guard_ready(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
match &mut self.lock_state {
LockState::Idle => {
// 1. first try to obtain the guard without locking
let Ok(guard) = self.buffer.clone().try_lock_owned() else {
// 2. if that fails, create the future for obtaining it
self.lock_state =
LockState::TryingToLock(self.buffer.clone().lock_owned().boxed());
return Poll::Pending;
};
// correctly transition to locked state and poll ourselves again
self.lock_state = LockState::Locked(guard);
cx.waker().wake_by_ref();
Poll::Ready(())
}
LockState::TryingToLock(lock_fut) => {
// see if the guard future has resolved, if so, transition to locked state and schedule for another poll
let guard = ready!(lock_fut.as_mut().poll(cx));
self.lock_state = LockState::Locked(guard);
cx.waker().wake_by_ref();
Poll::Pending
}
LockState::Locked(_) => Poll::Ready(()),
}
}
pub(crate) fn guard(&mut self) -> Option<&mut OwnedMutexGuard<ContentWrapper<T>>> {
match &mut self.lock_state {
LockState::Locked(guard) => Some(guard),
_ => None,
}
}
pub(crate) fn transition_to_idle(&mut self) {
self.lock_state = LockState::Idle
}
}
#[derive(Default)]
pub(crate) enum LockState<T> {
// We havent started locking yet
#[default]
Idle,
// Waiting for the mutex lock future to resolve
TryingToLock(BoxFuture<'static, OwnedMutexGuard<ContentWrapper<T>>>),
// We hold the mutex guard
Locked(OwnedMutexGuard<ContentWrapper<T>>),
}
#[derive(Default)]
pub struct ContentWrapper<T> {
pub(crate) content: T,
pub(crate) waker: Option<Waker>,
}
impl<T> ContentWrapper<T> {
pub fn into_content(self) -> T {
self.content
}
pub fn content(&self) -> &T {
&self.content
}
pub(crate) fn take_content(&mut self) -> T
where
T: Default,
{
mem::take(&mut self.content)
}
}
impl<T> LockState<T> {}
+181
View File
@@ -0,0 +1,181 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::mocks::shared::{ContentWrapper, InnerWrapper};
use anyhow::{anyhow, bail};
use futures::{ready, Sink, Stream};
use std::collections::VecDeque;
use std::pin::Pin;
use std::sync::Arc;
use std::task::{Context, Poll};
use tokio::sync::Mutex;
// sending buffer of the first stream is the receiving buffer of the second stream
// and vice versa
pub fn mock_streams<T>() -> (MockStream<T>, MockStream<T>)
where
T: Send,
{
let ch1 = MockStream::default();
let ch2 = ch1.make_connection();
(ch1, ch2)
}
pub struct MockStream<T: 'static> {
// messages to send
tx: InnerWrapper<VecDeque<T>>,
// messages to receive
rx: InnerWrapper<VecDeque<T>>,
}
impl<T> MockStream<T> {
pub fn clone_tx_buffer(&self) -> Arc<Mutex<ContentWrapper<VecDeque<T>>>>
where
T: Send,
{
self.tx.clone_buffer()
}
pub fn clone_rx_buffer(&self) -> Arc<Mutex<ContentWrapper<VecDeque<T>>>>
where
T: Send,
{
self.rx.clone_buffer()
}
fn make_connection(&self) -> Self
where
T: Send,
{
MockStream {
tx: self.rx.cloned_buffer(),
rx: self.tx.cloned_buffer(),
}
}
}
impl<T> Default for MockStream<T> {
fn default() -> Self {
MockStream {
tx: InnerWrapper::default(),
rx: InnerWrapper::default(),
}
}
}
impl<T> Stream for MockStream<T>
where
T: Send,
{
type Item = T;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
ready!(Pin::new(&mut self.rx).poll_guard_ready(cx));
// SAFETY: guard is ready
#[allow(clippy::unwrap_used)]
let guard = self.rx.guard().unwrap();
let Some(next) = guard.content.pop_front() else {
// nothing to retrieve - store the waiter so that the sender could trigger it
guard.waker = Some(cx.waker().clone());
// drop the guard so that the sender could actually put messages in
self.rx.transition_to_idle();
return Poll::Pending;
};
// there are more messages buffered waiting for us to retrieve
// keep the guard!
if !guard.content.is_empty() {
cx.waker().wake_by_ref();
} else {
// no more messages, drop the guard
self.rx.transition_to_idle();
}
Poll::Ready(Some(next))
}
fn size_hint(&self) -> (usize, Option<usize>) {
// that's just a minor optimisation, so don't sweat about it too much,
// if we can obtain the mutex, give precise information, otherwise return default values
let Ok(guard) = self.rx.buffer.try_lock() else {
return (0, None);
};
let items = guard.content.len();
(items, Some(items))
}
}
impl<T> Sink<T> for MockStream<T>
where
T: Send,
{
type Error = anyhow::Error;
fn poll_ready(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
// wait until we transition to the locked state
ready!(Pin::new(&mut self.tx).poll_guard_ready(cx));
Poll::Ready(Ok(()))
}
fn start_send(mut self: Pin<&mut Self>, item: T) -> Result<(), Self::Error> {
let Some(guard) = self.tx.guard() else {
bail!("invalid lock state to send messages");
};
guard.content.push_back(item);
Ok(())
}
fn poll_flush(
mut self: Pin<&mut Self>,
_cx: &mut Context<'_>,
) -> Poll<Result<(), Self::Error>> {
let Some(guard) = self.tx.guard() else {
return Poll::Ready(Err(anyhow!("invalid lock state to send/flush messages")));
};
if let Some(waker) = guard.waker.take() {
// notify the receiver if it was waiting for messages
waker.wake();
}
// release the guard
self.tx.transition_to_idle();
Poll::Ready(Ok(()))
}
fn poll_close(
mut self: Pin<&mut Self>,
_cx: &mut Context<'_>,
) -> Poll<Result<(), Self::Error>> {
// make sure our guard is always dropped on close
self.tx.transition_to_idle();
Poll::Ready(Ok(()))
}
}
#[cfg(test)]
mod tests {
use super::*;
use futures::{SinkExt, StreamExt};
#[tokio::test]
async fn basic() {
let (mut stream1, mut stream2) = mock_streams();
stream1.send("foomp").await.unwrap();
let received = stream2.next().await.unwrap();
assert_eq!(received, "foomp");
stream2.send("bar").await.unwrap();
let received = stream1.next().await.unwrap();
assert_eq!(received, "bar");
}
}
+57
View File
@@ -0,0 +1,57 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::helpers::{leak, spawn_timeboxed};
use std::future::{Future, IntoFuture};
use std::time::Duration;
use tokio::task::JoinHandle;
use tokio::time::error::Elapsed;
// a helper trait for use in tests to easily convert `T` into `&'static mut T`
pub trait Leak {
fn leak(self) -> &'static mut Self;
}
impl<T> Leak for T {
fn leak(self) -> &'static mut T {
leak(self)
}
}
// those are internal testing traits so we're not concerned about auto traits
#[allow(async_fn_in_trait)]
pub trait Timeboxed: IntoFuture + Sized {
async fn timeboxed(self) -> Result<Self::Output, Elapsed> {
self.execute_with_deadline(Duration::from_millis(200)).await
}
async fn execute_with_deadline(self, timeout: Duration) -> Result<Self::Output, Elapsed> {
tokio::time::timeout(timeout, self).await
}
}
impl<T> Timeboxed for T where T: IntoFuture + Sized {}
// those are internal testing traits so we're not concerned about auto traits
#[allow(async_fn_in_trait)]
pub trait Spawnable: Future + Sized + Send + 'static {
fn spawn(self) -> JoinHandle<Self::Output>
where
<Self as Future>::Output: Send + 'static,
{
tokio::spawn(self)
}
}
impl<T> Spawnable for T where T: Future + Sized + Send + 'static {}
pub trait TimeboxedSpawnable: Timeboxed + Spawnable {
fn spawn_timeboxed(self) -> JoinHandle<Result<<Self as Future>::Output, Elapsed>>
where
<Self as Future>::Output: Send,
{
spawn_timeboxed(self)
}
}
impl<T> TimeboxedSpawnable for T where T: Spawnable + Future + Send {}
+1
View File
@@ -19,6 +19,7 @@ serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
sha2 = { workspace = true }
strum = { workspace = true, features = ["derive"] }
strum_macros = { workspace = true }
thiserror = { workspace = true }
ts-rs = { workspace = true }
url = { workspace = true }
+1 -1
View File
@@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize};
use std::cmp::Ordering;
use std::collections::HashMap;
use std::fmt::{Display, Formatter};
use strum::{Display, EnumString, VariantNames};
use strum_macros::{Display, EnumString, VariantNames};
#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))]
#[cfg_attr(
+2 -1
View File
@@ -32,9 +32,11 @@ time = { workspace = true }
tracing = { workspace = true }
nym-authenticator-requests = { path = "../authenticator-requests" }
nym-credentials-interface = { path = "../credentials-interface" }
nym-credential-verification = { path = "../credential-verification" }
nym-crypto = { path = "../crypto", features = ["asymmetric"] }
nym-gateway-storage = { path = "../gateway-storage" }
nym-gateway-requests = { path = "../gateway-requests" }
nym-network-defaults = { path = "../network-defaults" }
nym-task = { path = "../task" }
nym-wireguard-types = { path = "../wireguard-types" }
@@ -46,4 +48,3 @@ nym-gateway-storage = { path = "../gateway-storage", features = ["mock"] }
[features]
default = []
mock = ["nym-gateway-storage/mock"]
+2
View File
@@ -21,3 +21,5 @@ pub enum Error {
#[error("{0}")]
SystemTime(#[from] std::time::SystemTimeError),
}
pub type Result<T> = std::result::Result<T, Error>;
+6 -5
View File
@@ -7,9 +7,9 @@
// #![warn(clippy::unwrap_used)]
use defguard_wireguard_rs::{host::Peer, key::Key, net::IpAddrMask, WGApi, WireguardInterfaceApi};
use nym_crypto::asymmetric::x25519::KeyPair;
#[cfg(target_os = "linux")]
use nym_gateway_storage::GatewayStorage;
use nym_credential_verification::ecash::EcashManager;
use nym_crypto::asymmetric::x25519::KeyPair;
use nym_wireguard_types::Config;
use peer_controller::PeerControlRequest;
use std::sync::Arc;
@@ -158,7 +158,7 @@ pub struct WireguardData {
/// Start wireguard device
#[cfg(target_os = "linux")]
pub async fn start_wireguard(
storage: GatewayStorage,
ecash_manager: Arc<EcashManager>,
metrics: nym_node_metrics::NymNodeMetrics,
peers: Vec<Peer>,
task_client: nym_task::TaskClient,
@@ -167,6 +167,7 @@ pub async fn start_wireguard(
use base64::{prelude::BASE64_STANDARD, Engine};
use defguard_wireguard_rs::{InterfaceConfiguration, WireguardInterfaceApi};
use ip_network::IpNetwork;
use nym_credential_verification::ecash::traits::EcashManager;
use peer_controller::PeerController;
use std::collections::HashMap;
use tokio::sync::RwLock;
@@ -178,7 +179,7 @@ pub async fn start_wireguard(
for peer in peers.iter() {
let bandwidth_manager = Arc::new(RwLock::new(
PeerController::generate_bandwidth_manager(Box::new(storage.clone()), &peer.public_key)
PeerController::generate_bandwidth_manager(ecash_manager.storage(), &peer.public_key)
.await?,
));
peer_bandwidth_managers.insert(peer.public_key.clone(), (bandwidth_manager, peer.clone()));
@@ -233,7 +234,7 @@ pub async fn start_wireguard(
let host = wg_api.read_interface_data()?;
let wg_api = std::sync::Arc::new(WgApiWrapper::new(wg_api));
let mut controller = PeerController::new(
Box::new(storage),
ecash_manager,
metrics,
wg_api.clone(),
host,
+92 -66
View File
@@ -9,9 +9,11 @@ use defguard_wireguard_rs::{
use futures::channel::oneshot;
use log::info;
use nym_credential_verification::{
bandwidth_storage_manager::BandwidthStorageManager, BandwidthFlushingBehaviourConfig,
ClientBandwidth,
bandwidth_storage_manager::BandwidthStorageManager, ecash::traits::EcashManager,
BandwidthFlushingBehaviourConfig, ClientBandwidth, CredentialVerifier,
};
use nym_credentials_interface::CredentialSpendingData;
use nym_gateway_requests::models::CredentialSpendingRequest;
use nym_gateway_storage::traits::BandwidthGatewayStorage;
use nym_node_metrics::NymNodeMetrics;
use nym_wireguard_types::DEFAULT_PEER_TIMEOUT_CHECK;
@@ -20,7 +22,10 @@ use std::{collections::HashMap, sync::Arc};
use tokio::sync::{mpsc, RwLock};
use tokio_stream::{wrappers::IntervalStream, StreamExt};
use crate::{error::Error, peer_handle::SharedBandwidthStorageManager};
use crate::{
error::{Error, Result},
peer_handle::SharedBandwidthStorageManager,
};
use crate::{peer_handle::PeerHandle, peer_storage_manager::CachedPeerManager};
pub enum PeerControlRequest {
@@ -40,27 +45,21 @@ pub enum PeerControlRequest {
key: Key,
response_tx: oneshot::Sender<GetClientBandwidthControlResponse>,
},
GetVerifier {
key: Key,
credential: Box<CredentialSpendingData>,
response_tx: oneshot::Sender<QueryVerifierControlResponse>,
},
}
pub struct AddPeerControlResponse {
pub success: bool,
}
pub struct RemovePeerControlResponse {
pub success: bool,
}
pub struct QueryPeerControlResponse {
pub success: bool,
pub peer: Option<Peer>,
}
pub struct GetClientBandwidthControlResponse {
pub client_bandwidth: Option<ClientBandwidth>,
}
pub type AddPeerControlResponse = Result<()>;
pub type RemovePeerControlResponse = Result<()>;
pub type QueryPeerControlResponse = Result<Option<Peer>>;
pub type GetClientBandwidthControlResponse = Result<ClientBandwidth>;
pub type QueryVerifierControlResponse = Result<CredentialVerifier>;
pub struct PeerController {
storage: Box<dyn BandwidthGatewayStorage + Send + Sync>,
ecash_verifier: Arc<dyn EcashManager + Send + Sync>,
// we have "all" metrics of a node, but they're behind a single Arc pointer,
// so the overhead is minimal
@@ -79,7 +78,7 @@ pub struct PeerController {
impl PeerController {
#[allow(clippy::too_many_arguments)]
pub fn new(
storage: Box<dyn BandwidthGatewayStorage + Send + Sync>,
ecash_verifier: Arc<dyn EcashManager + Send + Sync>,
metrics: NymNodeMetrics,
wg_api: Arc<dyn WireguardInterfaceApi + Send + Sync>,
initial_host_information: Host,
@@ -114,7 +113,7 @@ impl PeerController {
.collect();
PeerController {
storage,
ecash_verifier,
wg_api,
host_information,
bw_storage_managers,
@@ -127,8 +126,11 @@ impl PeerController {
}
// Function that should be used for peer removal, to handle both storage and kernel interaction
pub async fn remove_peer(&mut self, key: &Key) -> Result<(), Error> {
self.storage.remove_wireguard_peer(&key.to_string()).await?;
pub async fn remove_peer(&mut self, key: &Key) -> Result<()> {
self.ecash_verifier
.storage()
.remove_wireguard_peer(&key.to_string())
.await?;
self.bw_storage_managers.remove(key);
let ret = self.wg_api.remove_peer(key);
if ret.is_err() {
@@ -140,7 +142,7 @@ impl PeerController {
pub async fn generate_bandwidth_manager(
storage: Box<dyn BandwidthGatewayStorage + Send + Sync>,
public_key: &Key,
) -> Result<BandwidthStorageManager, Error> {
) -> Result<BandwidthStorageManager> {
let client_id = storage
.get_wireguard_peer(&public_key.to_string())
.await?
@@ -161,14 +163,11 @@ impl PeerController {
))
}
async fn handle_add_request(&mut self, peer: &Peer) -> Result<(), Error> {
async fn handle_add_request(&mut self, peer: &Peer) -> Result<()> {
self.wg_api.configure_peer(peer)?;
let bandwidth_storage_manager = Arc::new(RwLock::new(
Self::generate_bandwidth_manager(
dyn_clone::clone_box(&*self.storage),
&peer.public_key,
)
.await?,
Self::generate_bandwidth_manager(self.ecash_verifier.storage(), &peer.public_key)
.await?,
));
let cached_peer_manager = CachedPeerManager::new(peer);
let mut handle = PeerHandle::new(
@@ -193,21 +192,52 @@ impl PeerController {
Ok(())
}
async fn handle_query_peer(&self, key: &Key) -> Result<Option<Peer>, Error> {
async fn handle_query_peer(&self, key: &Key) -> Result<Option<Peer>> {
Ok(self
.storage
.ecash_verifier
.storage()
.get_wireguard_peer(&key.to_string())
.await?
.map(Peer::try_from)
.transpose()?)
}
async fn handle_get_client_bandwidth(&self, key: &Key) -> Option<ClientBandwidth> {
if let Some(bandwidth_storage_manager) = self.bw_storage_managers.get(key) {
Some(bandwidth_storage_manager.read().await.client_bandwidth())
} else {
None
}
async fn handle_get_client_bandwidth(&self, key: &Key) -> Result<ClientBandwidth> {
let bandwidth_storage_manager = self
.bw_storage_managers
.get(key)
.ok_or(Error::MissingClientBandwidthEntry)?;
Ok(bandwidth_storage_manager.read().await.client_bandwidth())
}
async fn handle_query_verifier(
&self,
key: &Key,
credential: CredentialSpendingData,
) -> Result<CredentialVerifier> {
let storage = self.ecash_verifier.storage();
let client_id = storage
.get_wireguard_peer(&key.to_string())
.await?
.ok_or(Error::MissingClientBandwidthEntry)?
.client_id;
let Some(bandwidth_storage_manager) = self.bw_storage_managers.get(key) else {
return Err(Error::MissingClientBandwidthEntry);
};
let client_bandwidth = bandwidth_storage_manager.read().await.client_bandwidth();
let verifier = CredentialVerifier::new(
CredentialSpendingRequest::new(credential),
self.ecash_verifier.clone(),
BandwidthStorageManager::new(
storage,
client_bandwidth,
client_id,
BandwidthFlushingBehaviourConfig::default(),
true,
),
);
Ok(verifier)
}
async fn update_metrics(&self, new_host: &Host) {
@@ -304,28 +334,19 @@ impl PeerController {
msg = self.request_rx.recv() => {
match msg {
Some(PeerControlRequest::AddPeer { peer, response_tx }) => {
let ret = self.handle_add_request(&peer).await;
if ret.is_ok() {
response_tx.send(AddPeerControlResponse { success: true }).ok();
} else {
response_tx.send(AddPeerControlResponse { success: false }).ok();
}
response_tx.send(self.handle_add_request(&peer).await).ok();
}
Some(PeerControlRequest::RemovePeer { key, response_tx }) => {
let success = self.remove_peer(&key).await.is_ok();
response_tx.send(RemovePeerControlResponse { success }).ok();
response_tx.send(self.remove_peer(&key).await).ok();
}
Some(PeerControlRequest::QueryPeer { key, response_tx }) => {
let ret = self.handle_query_peer(&key).await;
if let Ok(peer) = ret {
response_tx.send(QueryPeerControlResponse { success: true, peer }).ok();
} else {
response_tx.send(QueryPeerControlResponse { success: false, peer: None }).ok();
}
response_tx.send(self.handle_query_peer(&key).await).ok();
}
Some(PeerControlRequest::GetClientBandwidth { key, response_tx }) => {
let client_bandwidth = self.handle_get_client_bandwidth(&key).await;
response_tx.send(GetClientBandwidthControlResponse { client_bandwidth }).ok();
response_tx.send(self.handle_get_client_bandwidth(&key).await).ok();
}
Some(PeerControlRequest::GetVerifier { key, credential, response_tx }) => {
response_tx.send(self.handle_query_verifier(&key, *credential).await).ok();
}
None => {
log::trace!("PeerController [main loop]: stopping since channel closed");
@@ -349,21 +370,21 @@ struct MockWgApi {
impl WireguardInterfaceApi for MockWgApi {
fn create_interface(
&self,
) -> Result<(), defguard_wireguard_rs::error::WireguardInterfaceError> {
) -> std::result::Result<(), defguard_wireguard_rs::error::WireguardInterfaceError> {
todo!()
}
fn assign_address(
&self,
_address: &defguard_wireguard_rs::net::IpAddrMask,
) -> Result<(), defguard_wireguard_rs::error::WireguardInterfaceError> {
) -> std::result::Result<(), defguard_wireguard_rs::error::WireguardInterfaceError> {
todo!()
}
fn configure_peer_routing(
&self,
_peers: &[Peer],
) -> Result<(), defguard_wireguard_rs::error::WireguardInterfaceError> {
) -> std::result::Result<(), defguard_wireguard_rs::error::WireguardInterfaceError> {
todo!()
}
@@ -371,7 +392,7 @@ impl WireguardInterfaceApi for MockWgApi {
fn configure_interface(
&self,
_config: &defguard_wireguard_rs::InterfaceConfiguration,
) -> Result<(), defguard_wireguard_rs::error::WireguardInterfaceError> {
) -> std::result::Result<(), defguard_wireguard_rs::error::WireguardInterfaceError> {
todo!()
}
@@ -380,20 +401,20 @@ impl WireguardInterfaceApi for MockWgApi {
&self,
_config: &defguard_wireguard_rs::InterfaceConfiguration,
_dns: &[std::net::IpAddr],
) -> Result<(), defguard_wireguard_rs::error::WireguardInterfaceError> {
) -> std::result::Result<(), defguard_wireguard_rs::error::WireguardInterfaceError> {
todo!()
}
fn remove_interface(
&self,
) -> Result<(), defguard_wireguard_rs::error::WireguardInterfaceError> {
) -> std::result::Result<(), defguard_wireguard_rs::error::WireguardInterfaceError> {
todo!()
}
fn configure_peer(
&self,
peer: &Peer,
) -> Result<(), defguard_wireguard_rs::error::WireguardInterfaceError> {
) -> std::result::Result<(), defguard_wireguard_rs::error::WireguardInterfaceError> {
self.peers
.write()
.unwrap()
@@ -404,14 +425,14 @@ impl WireguardInterfaceApi for MockWgApi {
fn remove_peer(
&self,
peer_pubkey: &Key,
) -> Result<(), defguard_wireguard_rs::error::WireguardInterfaceError> {
) -> std::result::Result<(), defguard_wireguard_rs::error::WireguardInterfaceError> {
self.peers.write().unwrap().remove(peer_pubkey);
Ok(())
}
fn read_interface_data(
&self,
) -> Result<Host, defguard_wireguard_rs::error::WireguardInterfaceError> {
) -> std::result::Result<Host, defguard_wireguard_rs::error::WireguardInterfaceError> {
let mut host = Host::default();
host.peers = self.peers.read().unwrap().clone();
Ok(host)
@@ -420,7 +441,7 @@ impl WireguardInterfaceApi for MockWgApi {
fn configure_dns(
&self,
_dns: &[std::net::IpAddr],
) -> Result<(), defguard_wireguard_rs::error::WireguardInterfaceError> {
) -> std::result::Result<(), defguard_wireguard_rs::error::WireguardInterfaceError> {
todo!()
}
}
@@ -433,13 +454,18 @@ pub fn start_controller(
Arc<RwLock<nym_gateway_storage::traits::mock::MockGatewayStorage>>,
nym_task::TaskManager,
) {
use std::sync::Arc;
let storage = Arc::new(RwLock::new(
nym_gateway_storage::traits::mock::MockGatewayStorage::default(),
));
let ecash_manager = Arc::new(nym_credential_verification::ecash::MockEcashManager::new(
Box::new(storage.clone()),
));
let wg_api = Arc::new(MockWgApi::default());
let task_manager = nym_task::TaskManager::default();
let mut peer_controller = PeerController::new(
Box::new(storage.clone()),
ecash_manager,
Default::default(),
wg_api,
Default::default(),
+2 -1
View File
@@ -62,7 +62,8 @@ impl PeerHandle {
let success = response_rx
.await
.map_err(|_| Error::Internal("peer controller didn't respond".to_string()))?
.success;
.inspect_err(|err| tracing::error!("Could not remove peer: {err:?}"))
.is_ok();
Ok(success)
}
@@ -5,7 +5,7 @@
},
"mixmining_reserve": {
"denom": "unym",
"amount": "185782781887190"
"amount": "184339094131786"
},
"vesting_tokens": {
"denom": "unym",
@@ -13,6 +13,6 @@
},
"circulating_supply": {
"denom": "unym",
"amount": "814217218112810"
"amount": "815660905868214"
}
}
@@ -1 +1 @@
814_217_218
815_660_905
@@ -1 +1 @@
1_038_975
1_040_817
@@ -1 +1 @@
249_354_023
249_796_152
@@ -1 +1 @@
249_354_023
249_796_152
@@ -1,7 +1,7 @@
| **Item** | **Description** | **Amount in NYM** |
|:-------------------|:------------------------------------------------------|--------------------:|
| Total Supply | Maximum amount of NYM token in existence | 1_000_000_000 |
| Mixmining Reserve | Tokens releasing for operators rewards | 185_782_781 |
| Mixmining Reserve | Tokens releasing for operators rewards | 184_339_094 |
| Vesting Tokens | Tokens locked outside of cicrulation for future claim | 0 |
| Circulating Supply | Amount of unlocked tokens | 814_217_218 |
| Stake Saturation | Optimal size of node self-bond + delegation | 1_038_975 |
| Circulating Supply | Amount of unlocked tokens | 815_660_905 |
| Stake Saturation | Optimal size of node self-bond + delegation | 1_040_817 |
@@ -1,10 +1,10 @@
{
"interval": {
"reward_pool": "185782781887190.195531020579422623",
"staking_supply": "249354023047048.00261862494755182",
"reward_pool": "184339094131786.145886263466367605",
"staking_supply": "249796152422140.492822331813424919",
"staking_supply_scale_factor": "0.30625",
"epoch_reward_budget": "5160632830.199727653639460539",
"stake_saturation_point": "1038975096029.366677577603948132",
"epoch_reward_budget": "5120530392.54961516350731851",
"stake_saturation_point": "1040817301758.918720093049222603",
"sybil_resistance": "0.3",
"active_set_work_factor": "10",
"interval_pool_emission": "0.02"
@@ -1 +1 @@
Wednesday, July 30th 2025, 09:32:50 UTC
Friday, August 22nd 2025, 10:15:08 UTC
@@ -41,3 +41,4 @@
[Dataclub](https://www.dataclub.eu/),"Latvia, Sweden, Netherlands",Yes,,,07/2027
[Privex](https://www.privex.io/tor-exit-policy/),"USA, Germany, Sweden",Yes,Yes,,07/2025
[Svea](https://svea.net/vps),Sweden,Yes,,,07/2025
[Hostraha](https://hostraha.com),"Kenya and other African countries", "No, but advertised otherwise", "Yes, USDT TRC20", "Don't recommend. Unresponsive technical and billing support, never provided IPv6 even though advertised and paid for. When VPS cancelled, company still tried to bill the credit card on file multiple times.", 08/2025
1 **ISP** **Locations** **Public IPv6** **Crypto Payments** **Comments** **Last Updated**
41 [Dataclub](https://www.dataclub.eu/) Latvia, Sweden, Netherlands Yes 07/2027
42 [Privex](https://www.privex.io/tor-exit-policy/) USA, Germany, Sweden Yes Yes 07/2025
43 [Svea](https://svea.net/vps) Sweden Yes 07/2025
44 [Hostraha](https://hostraha.com) Kenya and other African countries No, but advertised otherwise Yes, USDT TRC20 Don't recommend. Unresponsive technical and billing support, never provided IPv6 even though advertised and paid for. When VPS cancelled, company still tried to bill the credit card on file multiple times. 08/2025
@@ -39,6 +39,8 @@ export const LoadEndpointInfo = () => (
</div>
);
{/*
# Changelog
This page displays a full list of all the changes during our release cycle from `v2024.3-eclipse` onward. Operators can find here the newest updates together with links to relevant documentation. The list is sorted so that the newest changes appear first.
@@ -47,7 +49,101 @@ This page displays a full list of all the changes during our release cycle from
<VarInfo />
## `v2025.15-gruyere`
- [Release Binaries](https://github.com/nymtech/nym/releases/tag/nym-binaries-v2025.15-gruyere)
- [`nym-node`](nodes/nym-node.mdx) version `1.17.0`
```sh
nym-node
Binary Name: nym-node
Build Timestamp: 2025-08-20T10:37:05.965300480Z
Build Version: 1.17.0
Commit SHA: 40e1cbc7a9f518eafbb5649c383626b096dd167d
Commit Date: 2025-08-20T12:34:04.000000000+02:00
Commit Branch: HEAD
rustc Version: 1.86.0
rustc Channel: stable
cargo Profile: release
```
### Operators Updates & Tools
- [**NIP-3: Nym Exit Policy Update**](https://forum.nym.com/t/nip-3-nym-exit-policy-update/1462/2) resulted by operators [governance](https://governator.nym.com/proposal/prop-d0c0d398-43bd-4a6f-b008-1921b64ae4ed) is implemented.
- If you operate a node routing wireguard, please re-run [these steps](nodes/nym-node/configuration#wireguard-exit-policy-configuration) to update your wireguard exit policy through IP tables rules.
- [**NIP-2: Changing stake saturation**](https://governator.nym.com/proposal/prop-8dfa4a28-55b1-45a5-968f-f2897c2670df) will be implemented soon, with that we are going to adjust rules of Delegation Program. **Join [Community call ](https://www.youtube.com/watch?v=KaO6-WLWzMo) on Thursday August 21st, 14:00UTC** to find out more info.
### Features
- [Remove old free credential handle](https://github.com/nymtech/nym/pull/5864): And create some traits for unit testing purposes.
- [Ecash liveness check](https://github.com/nymtech/nym/pull/5890)
- [Basic zulip client for sending messages](https://github.com/nymtech/nym/pull/5913): In order to be able to send zulip notifications about *emergency* upgrade mode being activated, we need some sort of client. Unfortunately there isn't any rust library that's maintained (the only one had last commit 4 years ago). This simple thing now currently only supports message sending
- [`nym-node` debug command to reset providers db](https://github.com/nymtech/nym/pull/5914)
- [Make DNS Resolver fallback optional](https://github.com/nymtech/nym/pull/5920): Default to no dns system fallback, but keep support in the custom hickory dns resolver used for resolving internal domains.
- [WG exit policy scripts update](https://github.com/nymtech/nym/pull/5921): This PR modifies the scripts `wireguard-exit-policy-manager.sh` and `exit-policy-tests.sh` supporting operators to easily configure and test their IP tables rules in order to have same exit policy for WG as the one for NR, adding the ports decided to be enabled in [NIP-3](https://governator.nym.com/proposal/prop-d0c0d398-43bd-4a6f-b008-1921b64ae4ed) protocol upgrade.
### Refactors & Maintenance
- [Allow compatibility with 'CDLA-Permissive-2.0'](https://github.com/nymtech/nym/pull/5910): This license is present in the included `webpki-roots`
- [Migrate strum to `0.27.2`](https://github.com/nymtech/nym/pull/5960): This PR migrates strum to the latest. Notably all macros' were moved into `strum_macros`. The rest stays the same.
*/}
## `v2025.14-feta`
- [Release Binaries](https://github.com/nymtech/nym/releases/tag/nym-binaries-v2025.14-feta)
- [`nym-node`](nodes/nym-node.mdx) version `1.16.0`
```sh
nym-node
Binary Name: nym-node
Build Timestamp: 2025-08-05T09:14:30.322593213Z
Build Version: 1.16.0
Commit SHA: 7f97f13799342f864e1b106e8cafc9f6d6c24c0f
Commit Date: 2025-07-24T11:00:58.000000000+01:00
Commit Branch: HEAD
rustc Version: 1.86.0
rustc Channel: stable
cargo Profile: release
```
### Operators Updates & Tools
- Stark Industries is on a sanction list by EU. IP addresses managed by Stark Ind. and their subsidies (ASN 44477 / ASN 33993) had been put on [spamhaus.org](http://spamhaus.org/) [list](https://www.spamhaus.org/drop/asndrop.json). The effect on NymVPN user experience is that Exit Gateways IPs hosted on Stark Ind. are seen as a spam proxies by many online services.
- We ask operators - especially Exit Gateways - to consider moving to another ISP. Visit an updated [ISP list](community-counsel/isp-list) and feel free to add more providers, following [these steps](community-counsel/add-content).
### Features
- [Allow PG database backend](https://github.com/nymtech/nym/pull/5880):
- Added PostgreSQL database support alongside existing SQLite through Cargo feature flags
- Implemented runtime query conversion from SQLite `?` placeholders to PostgreSQL `$1`, `$2`, ... format
- Single codebase now supports both databases without query duplication
- [Support mnemonic in the NS agent](https://github.com/nymtech/nym/pull/5883)
- [`sqlx-pool-guard`: allocate more memory on windows](https://github.com/nymtech/nym/pull/5896):
- Allocate 1.5x more memory than reported by the system to provide a safety margin
- Increase number of retry attempts to 5
- [dkg epoch dealers query](https://github.com/nymtech/nym/pull/5899)
- [dkg snapshot epoch](https://github.com/nymtech/nym/pull/5900): In order to determine if signer quorum has been down at particular height, we need to know with certainty the dkg epoch id corresponding to given block height. This PR makes it possible. Every time epoch state is changed (due to DKG progress), snapshot is saved and can be queried. This doesn't work for past data, but given mainnet has only had a single DKG instance, that's not an issue.
- [`sqlx-pool-guard`: obtain filename from connect options](https://github.com/nymtech/nym/pull/5905):
### Refactors & Maintenance
- [Nym node tokio console](https://github.com/nymtech/nym/pull/5909)
## `v2025.13-emmental`
@@ -20,10 +20,10 @@ This documentation page provides a guide on how to set up and run a [NYM NODE](.
```sh
nym-node
Binary Name: nym-node
Build Timestamp: 2025-07-22T09:24:35.790560275Z
Build Version: 1.15.0
Commit SHA: 578c9b0567656d86812aa21eb0b4c93b5a7235bd
Commit Date: 2025-07-22T11:09:35.000000000+02:00
Build Timestamp: 2025-08-20T10:37:05.965300480Z
Build Version: 1.17.0
Commit SHA: 40e1cbc7a9f518eafbb5649c383626b096dd167d
Commit Date: 2025-08-20T12:34:04.000000000+02:00
Commit Branch: HEAD
rustc Version: 1.86.0
rustc Channel: stable
+7 -1
View File
@@ -486,8 +486,14 @@ impl GatewayTasksBuilder {
);
};
let Some(ecash_manager) = self.ecash_manager.clone() else {
return Err(
GatewayError::InternalWireguardError("ecash manager not set".to_string()).into(),
);
};
let wg_handle = nym_wireguard::start_wireguard(
self.storage.clone(),
ecash_manager,
self.metrics.clone(),
all_peers,
self.shutdown.fork("wireguard"),
+1 -1
View File
@@ -4,7 +4,7 @@
[package]
name = "nym-api"
license = "GPL-3.0"
version = "1.1.63"
version = "1.1.64"
authors.workspace = true
edition = "2021"
rust-version.workspace = true
@@ -0,0 +1,51 @@
/*
* Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
* SPDX-License-Identifier: GPL-3.0-only
*/
-- Change performed in this migration:
-- remove PK on expiration_date and instead use composite (epoch_id, expiration_date) PK
CREATE TABLE global_expiration_date_signatures_new
(
expiration_date DATE NOT NULL,
epoch_id INTEGER NOT NULL,
-- combined signatures for all tuples issued for given day
serialised_signatures BLOB NOT NULL,
PRIMARY KEY (epoch_id, expiration_date)
);
CREATE TABLE partial_expiration_date_signatures_new
(
expiration_date DATE NOT NULL,
epoch_id INTEGER NOT NULL,
serialised_signatures BLOB NOT NULL,
PRIMARY KEY (epoch_id, expiration_date)
);
-- global
INSERT INTO global_expiration_date_signatures_new
SELECT *
FROM global_expiration_date_signatures;
DROP TABLE global_expiration_date_signatures;
ALTER TABLE global_expiration_date_signatures_new
RENAME TO global_expiration_date_signatures;
-- partial
INSERT INTO partial_expiration_date_signatures_new
SELECT *
FROM partial_expiration_date_signatures;
DROP TABLE partial_expiration_date_signatures;
ALTER TABLE partial_expiration_date_signatures_new
RENAME TO partial_expiration_date_signatures;
+9 -1
View File
@@ -12,6 +12,7 @@ use nym_api_requests::ecash::models::{
AggregatedCoinIndicesSignatureResponse, AggregatedExpirationDateSignatureResponse,
};
use nym_api_requests::ecash::VerificationKeyResponse;
use nym_coconut_dkg_common::types::EpochId;
use nym_ecash_time::{cred_exp_date, EcashTime};
use nym_http_api_common::{FormattedResponse, Output};
use nym_validator_client::nym_api::rfc_3339_date;
@@ -71,6 +72,7 @@ async fn master_verification_key(
#[derive(Deserialize, IntoParams)]
struct ExpirationDateParam {
expiration_date: Option<String>,
epoch_id: Option<EpochId>,
output: Option<Output>,
}
@@ -93,6 +95,7 @@ async fn expiration_date_signatures(
State(state): State<Arc<EcashState>>,
Query(ExpirationDateParam {
expiration_date,
epoch_id,
output,
}): Query<ExpirationDateParam>,
) -> AxumResult<FormattedResponse<AggregatedExpirationDateSignatureResponse>> {
@@ -108,8 +111,13 @@ async fn expiration_date_signatures(
// see if we're not in the middle of new dkg
state.ensure_dkg_not_in_progress().await?;
let epoch_id = match epoch_id {
Some(epoch_id) => epoch_id,
None => state.current_dkg_epoch().await?,
};
let expiration_date_signatures = state
.master_expiration_date_signatures(expiration_date)
.master_expiration_date_signatures(expiration_date, epoch_id)
.await?;
Ok(
@@ -13,6 +13,7 @@ use nym_api_requests::ecash::{
BlindSignRequestBody, BlindedSignatureResponse, PartialCoinIndicesSignatureResponse,
PartialExpirationDateSignatureResponse,
};
use nym_coconut_dkg_common::types::EpochId;
use nym_ecash_time::{cred_exp_date, EcashTime};
use nym_http_api_common::{FormattedResponse, Output, OutputParams};
use nym_validator_client::nym_api::rfc_3339_date;
@@ -114,6 +115,7 @@ async fn post_blind_sign(
#[derive(Deserialize, IntoParams)]
struct ExpirationDateParam {
expiration_date: Option<String>,
epoch_id: Option<EpochId>,
output: Option<Output>,
}
@@ -137,6 +139,7 @@ async fn partial_expiration_date_signatures(
State(state): State<Arc<EcashState>>,
Query(ExpirationDateParam {
expiration_date,
epoch_id,
output,
}): Query<ExpirationDateParam>,
) -> AxumResult<FormattedResponse<PartialExpirationDateSignatureResponse>> {
@@ -152,8 +155,13 @@ async fn partial_expiration_date_signatures(
// see if we're not in the middle of new dkg
state.ensure_dkg_not_in_progress().await?;
let epoch_id = match epoch_id {
Some(epoch_id) => epoch_id,
None => state.current_dkg_epoch().await?,
};
let expiration_date_signatures = state
.partial_expiration_date_signatures(expiration_date)
.partial_expiration_date_signatures(expiration_date, epoch_id)
.await?;
Ok(output.to_response(PartialExpirationDateSignatureResponse {
+1 -1
View File
@@ -83,7 +83,7 @@ impl QueryCommunicationChannel {
}
}
async fn update_epoch_cache(&self) -> Result<RwLockWriteGuard<CachedEpoch>> {
async fn update_epoch_cache(&self) -> Result<RwLockWriteGuard<'_, CachedEpoch>> {
let mut guard = self.cached_epoch.write().await;
let epoch = ecash::client::Client::get_current_epoch(&self.nyxd_client).await?;
+1 -1
View File
@@ -100,7 +100,7 @@ where
&self,
key: K,
f: F,
) -> Result<RwLockReadGuard<V>, EcashError>
) -> Result<RwLockReadGuard<'_, V>, EcashError>
where
F: FnOnce() -> U,
U: Future<Output = Result<V, EcashError>>,
+3 -3
View File
@@ -65,13 +65,13 @@ impl KeyPair {
}
}
pub async fn keys(&self) -> Result<RwLockReadGuard<KeyPairWithEpoch>, EcashError> {
pub async fn keys(&self) -> Result<RwLockReadGuard<'_, KeyPairWithEpoch>, EcashError> {
let keypair_guard = self.get().await.ok_or(EcashError::KeyPairNotDerivedYet)?;
RwLockReadGuard::try_map(keypair_guard, |keypair| keypair.as_ref())
.map_err(|_| EcashError::KeyPairNotDerivedYet)
}
pub async fn signing_key(&self) -> Result<RwLockReadGuard<SecretKeyAuth>, EcashError> {
pub async fn signing_key(&self) -> Result<RwLockReadGuard<'_, SecretKeyAuth>, EcashError> {
let keypair_guard = self.get().await.ok_or(EcashError::KeyPairNotDerivedYet)?;
RwLockReadGuard::try_map(keypair_guard, |keypair| {
@@ -80,7 +80,7 @@ impl KeyPair {
.map_err(|_| EcashError::KeyPairNotDerivedYet)
}
pub async fn verification_key(&self) -> Option<RwLockReadGuard<VerificationKeyAuth>> {
pub async fn verification_key(&self) -> Option<RwLockReadGuard<'_, VerificationKeyAuth>> {
RwLockReadGuard::try_map(self.get().await?, |maybe_keys| {
maybe_keys.as_ref().map(|k| k.keys.verification_key_ref())
})
+2 -1
View File
@@ -5,6 +5,7 @@ use crate::ecash::helpers::{
CachedImmutableEpochItem, CachedImmutableItems, IssuedCoinIndicesSignatures,
IssuedExpirationDateSignatures,
};
use nym_coconut_dkg_common::types::EpochId;
use nym_compact_ecash::VerificationKeyAuth;
use nym_validator_client::nyxd::AccountId;
use time::Date;
@@ -18,7 +19,7 @@ pub(crate) struct GlobalEcachState {
pub(crate) coin_index_signatures: CachedImmutableEpochItem<IssuedCoinIndicesSignatures>,
pub(crate) expiration_date_signatures:
CachedImmutableItems<Date, IssuedExpirationDateSignatures>,
CachedImmutableItems<(EpochId, Date), IssuedExpirationDateSignatures>,
}
impl GlobalEcachState {
+2 -1
View File
@@ -9,6 +9,7 @@ use crate::ecash::helpers::{
use crate::ecash::keys::KeyPair;
use crate::ecash::storage::models::IssuedHash;
use nym_api_requests::ecash::models::{CommitedDeposit, DepositId};
use nym_coconut_dkg_common::types::EpochId;
use nym_crypto::asymmetric::ed25519;
use nym_ticketbooks_merkle::{
IssuedTicketbook, IssuedTicketbooksFullMerkleProof, IssuedTicketbooksMerkleTree, MerkleLeaf,
@@ -143,7 +144,7 @@ pub(crate) struct LocalEcashState {
pub(crate) partial_coin_index_signatures: CachedImmutableEpochItem<IssuedCoinIndicesSignatures>,
pub(crate) partial_expiration_date_signatures:
CachedImmutableItems<Date, IssuedExpirationDateSignatures>,
CachedImmutableItems<(EpochId, Date), IssuedExpirationDateSignatures>,
// merkle trees for ticketbooks issued for particular expiration dates
pub(crate) issued_merkle_trees: Arc<RwLock<HashMap<Date, DailyMerkleTree>>>,
+20 -15
View File
@@ -198,21 +198,21 @@ impl EcashState {
Ok(())
}
pub(crate) async fn ecash_signing_key(&self) -> Result<RwLockReadGuard<SecretKeyAuth>> {
pub(crate) async fn ecash_signing_key(&self) -> Result<RwLockReadGuard<'_, SecretKeyAuth>> {
self.local.ecash_keypair.signing_key().await
}
#[allow(dead_code)]
pub(crate) async fn current_master_verification_key(
&self,
) -> Result<RwLockReadGuard<VerificationKeyAuth>> {
) -> Result<RwLockReadGuard<'_, VerificationKeyAuth>> {
self.master_verification_key(None).await
}
pub(crate) async fn master_verification_key(
&self,
epoch_id: Option<EpochId>,
) -> Result<RwLockReadGuard<VerificationKeyAuth>> {
) -> Result<RwLockReadGuard<'_, VerificationKeyAuth>> {
let epoch_id = match epoch_id {
Some(id) => id,
None => self.aux.current_epoch().await?,
@@ -258,7 +258,7 @@ impl EcashState {
pub(crate) async fn master_coin_index_signatures(
&self,
epoch_id: Option<EpochId>,
) -> Result<RwLockReadGuard<IssuedCoinIndicesSignatures>> {
) -> Result<RwLockReadGuard<'_, IssuedCoinIndicesSignatures>> {
let epoch_id = match epoch_id {
Some(id) => id,
None => self.aux.current_epoch().await?,
@@ -344,7 +344,7 @@ impl EcashState {
pub(crate) async fn partial_coin_index_signatures(
&self,
epoch_id: Option<EpochId>,
) -> Result<RwLockReadGuard<IssuedCoinIndicesSignatures>> {
) -> Result<RwLockReadGuard<'_, IssuedCoinIndicesSignatures>> {
let epoch_id = match epoch_id {
Some(id) => id,
None => self.aux.current_epoch().await?,
@@ -401,10 +401,11 @@ impl EcashState {
pub(crate) async fn master_expiration_date_signatures(
&self,
expiration_date: Date,
) -> Result<RwLockReadGuard<IssuedExpirationDateSignatures>> {
epoch_id: EpochId,
) -> Result<RwLockReadGuard<'_, IssuedExpirationDateSignatures>> {
self.global
.expiration_date_signatures
.get_or_init(expiration_date, || async {
.get_or_init((epoch_id, expiration_date), || async {
// 1. sanity check to see if the expiration_date is not nonsense
ensure_sane_expiration_date(expiration_date)?;
@@ -412,7 +413,7 @@ impl EcashState {
if let Some(master_sigs) = self
.aux
.storage
.get_master_expiration_date_signatures(expiration_date)
.get_master_expiration_date_signatures(expiration_date, epoch_id)
.await?
{
return Ok(master_sigs);
@@ -435,13 +436,16 @@ impl EcashState {
// check if we're attempting to query ourselves, in that case just get local signature
// rather than making the http query
let partial = if Some(api.cosmos_address) == cosmos_address {
self.partial_expiration_date_signatures(expiration_date)
self.partial_expiration_date_signatures(expiration_date, epoch_id)
.await?
.signatures
.clone()
} else {
api.api_client
.partial_expiration_date_signatures(Some(expiration_date))
.partial_expiration_date_signatures(
Some(expiration_date),
Some(epoch_id),
)
.await?
.signatures
};
@@ -480,10 +484,11 @@ impl EcashState {
pub(crate) async fn partial_expiration_date_signatures(
&self,
expiration_date: Date,
) -> Result<RwLockReadGuard<IssuedExpirationDateSignatures>> {
epoch_id: EpochId,
) -> Result<RwLockReadGuard<'_, IssuedExpirationDateSignatures>> {
self.local
.partial_expiration_date_signatures
.get_or_init(expiration_date, || async {
.get_or_init((epoch_id, expiration_date), || async {
// 1. sanity check to see if the expiration_date is not nonsense
ensure_sane_expiration_date(expiration_date)?;
@@ -491,7 +496,7 @@ impl EcashState {
if let Some(partial_sigs) = self
.aux
.storage
.get_partial_expiration_date_signatures(expiration_date)
.get_partial_expiration_date_signatures(expiration_date, epoch_id)
.await?
{
return Ok(partial_sigs);
@@ -721,7 +726,7 @@ impl EcashState {
async fn get_updated_merkle_read(
&self,
expiration_date: Date,
) -> Result<RwLockReadGuard<DailyMerkleTree>> {
) -> Result<RwLockReadGuard<'_, DailyMerkleTree>> {
let write_guard = self.get_updated_full_write(expiration_date).await?;
// SAFETY: the entry was either not empty or we just inserted data in there, whilst never dropping the lock
@@ -735,7 +740,7 @@ impl EcashState {
async fn get_updated_full_write(
&self,
expiration_date: Date,
) -> Result<RwLockWriteGuard<HashMap<Date, DailyMerkleTree>>> {
) -> Result<RwLockWriteGuard<'_, HashMap<Date, DailyMerkleTree>>> {
let mut write_guard = self.local.issued_merkle_trees.write().await;
// double check if it's still empty in case another task has already grabbed the write lock and performed the update
+10 -4
View File
@@ -128,6 +128,7 @@ pub trait EcashStorageManagerExt {
async fn get_partial_expiration_date_signatures(
&self,
expiration_date: Date,
epoch_id: i64,
) -> Result<Option<RawExpirationDateSignatures>, sqlx::Error>;
async fn insert_partial_expiration_date_signatures(
&self,
@@ -139,6 +140,7 @@ pub trait EcashStorageManagerExt {
async fn get_master_expiration_date_signatures(
&self,
expiration_date: Date,
epoch_id: i64,
) -> Result<Option<RawExpirationDateSignatures>, sqlx::Error>;
async fn insert_master_expiration_date_signatures(
&self,
@@ -501,15 +503,17 @@ impl EcashStorageManagerExt for StorageManager {
async fn get_partial_expiration_date_signatures(
&self,
expiration_date: Date,
epoch_id: i64,
) -> Result<Option<RawExpirationDateSignatures>, sqlx::Error> {
sqlx::query_as!(
RawExpirationDateSignatures,
r#"
SELECT epoch_id as "epoch_id: u32", serialised_signatures
FROM partial_expiration_date_signatures
WHERE expiration_date = ?
WHERE expiration_date = ? AND epoch_id = ?
"#,
expiration_date
expiration_date,
epoch_id
)
.fetch_optional(&self.connection_pool)
.await
@@ -535,15 +539,17 @@ impl EcashStorageManagerExt for StorageManager {
async fn get_master_expiration_date_signatures(
&self,
expiration_date: Date,
epoch_id: i64,
) -> Result<Option<RawExpirationDateSignatures>, sqlx::Error> {
sqlx::query_as!(
RawExpirationDateSignatures,
r#"
SELECT epoch_id as "epoch_id: u32", serialised_signatures
FROM global_expiration_date_signatures
WHERE expiration_date = ?
WHERE expiration_date = ? AND epoch_id = ?
"#,
expiration_date
expiration_date,
epoch_id
)
.fetch_optional(&self.connection_pool)
.await
+6 -2
View File
@@ -143,6 +143,7 @@ pub trait EcashStorageExt {
async fn get_partial_expiration_date_signatures(
&self,
expiration_date: Date,
epoch_id: EpochId,
) -> Result<Option<IssuedExpirationDateSignatures>, NymApiStorageError>;
async fn insert_partial_expiration_date_signatures(
@@ -154,6 +155,7 @@ pub trait EcashStorageExt {
async fn get_master_expiration_date_signatures(
&self,
expiration_date: Date,
epoch_id: EpochId,
) -> Result<Option<IssuedExpirationDateSignatures>, NymApiStorageError>;
async fn insert_master_expiration_date_signatures(
@@ -456,10 +458,11 @@ impl EcashStorageExt for NymApiStorage {
async fn get_partial_expiration_date_signatures(
&self,
expiration_date: Date,
epoch_id: EpochId,
) -> Result<Option<IssuedExpirationDateSignatures>, NymApiStorageError> {
let Some(raw) = self
.manager
.get_partial_expiration_date_signatures(expiration_date)
.get_partial_expiration_date_signatures(expiration_date, epoch_id as i64)
.await?
else {
return Ok(None);
@@ -491,10 +494,11 @@ impl EcashStorageExt for NymApiStorage {
async fn get_master_expiration_date_signatures(
&self,
expiration_date: Date,
epoch_id: EpochId,
) -> Result<Option<IssuedExpirationDateSignatures>, NymApiStorageError> {
let Some(raw) = self
.manager
.get_master_expiration_date_signatures(expiration_date)
.get_master_expiration_date_signatures(expiration_date, epoch_id as i64)
.await?
else {
return Ok(None);
+1 -1
View File
@@ -75,7 +75,7 @@ pub(super) fn stake_to_f64(stake: Decimal) -> f64 {
impl EpochAdvancer {
fn load_performance(
status_cache: &Option<RwLockReadGuard<Cache<HashMap<NodeId, NodeAnnotation>>>>,
status_cache: &Option<RwLockReadGuard<'_, Cache<HashMap<NodeId, NodeAnnotation>>>>,
node_id: NodeId,
) -> NodeWithPerformance {
let Some(status_cache) = status_cache.as_ref() else {
+4 -4
View File
@@ -66,7 +66,7 @@ impl MixnetContractCache {
pub async fn all_cached_legacy_mixnodes(
&self,
) -> Option<RwLockReadGuard<Vec<LegacyMixNodeDetailsWithLayer>>> {
) -> Option<RwLockReadGuard<'_, Vec<LegacyMixNodeDetailsWithLayer>>> {
self.get(|c| &c.legacy_mixnodes).await.ok()
}
@@ -84,11 +84,11 @@ impl MixnetContractCache {
pub async fn all_cached_legacy_gateways(
&self,
) -> Option<RwLockReadGuard<Vec<LegacyGatewayBondWithId>>> {
) -> Option<RwLockReadGuard<'_, Vec<LegacyGatewayBondWithId>>> {
self.get(|c| &c.legacy_gateways).await.ok()
}
pub async fn all_cached_nym_nodes(&self) -> Option<RwLockReadGuard<Vec<NymNodeDetails>>> {
pub async fn all_cached_nym_nodes(&self) -> Option<RwLockReadGuard<'_, Vec<NymNodeDetails>>> {
self.get(|c| &c.nym_nodes).await.ok()
}
@@ -125,7 +125,7 @@ impl MixnetContractCache {
Ok(Cache::as_mapped(&cache, |c| c.rewarded_set.clone()))
}
pub async fn rewarded_set(&self) -> Option<RwLockReadGuard<CachedEpochRewardedSet>> {
pub async fn rewarded_set(&self) -> Option<RwLockReadGuard<'_, CachedEpochRewardedSet>> {
self.get(|c| &c.rewarded_set).await.ok()
}
+3 -3
View File
@@ -112,7 +112,7 @@ impl NodeStatusCache {
pub(crate) async fn node_annotations(
&self,
) -> Option<RwLockReadGuard<Cache<HashMap<NodeId, NodeAnnotation>>>> {
) -> Option<RwLockReadGuard<'_, Cache<HashMap<NodeId, NodeAnnotation>>>> {
self.get(|c| &c.node_annotations).await
}
@@ -127,7 +127,7 @@ impl NodeStatusCache {
pub(crate) async fn annotated_legacy_mixnodes(
&self,
) -> Option<RwLockReadGuard<Cache<HashMap<NodeId, MixNodeBondAnnotated>>>> {
) -> Option<RwLockReadGuard<'_, Cache<HashMap<NodeId, MixNodeBondAnnotated>>>> {
self.get(|c| &c.mixnodes_annotated).await
}
@@ -150,7 +150,7 @@ impl NodeStatusCache {
pub(crate) async fn annotated_legacy_gateways(
&self,
) -> Option<RwLockReadGuard<Cache<HashMap<NodeId, GatewayBondAnnotated>>>> {
) -> Option<RwLockReadGuard<'_, Cache<HashMap<NodeId, GatewayBondAnnotated>>>> {
self.get(|c| &c.gateways_annotated).await
}
+1 -19
View File
@@ -8,31 +8,13 @@ use crate::{MixnetContractCache, NodeStatusCache};
use nym_api_requests::models::{
ComputeRewardEstParam, GatewayBondAnnotated, GatewayCoreStatusResponse,
GatewayStatusReportResponse, GatewayUptimeHistoryResponse, GatewayUptimeResponse,
MixNodeBondAnnotated, MixnodeCoreStatusResponse, MixnodeStatus, MixnodeStatusReportResponse,
MixNodeBondAnnotated, MixnodeCoreStatusResponse, MixnodeStatusReportResponse,
MixnodeStatusResponse, MixnodeUptimeHistoryResponse, RewardEstimationResponse,
StakeSaturationResponse, UptimeResponse,
};
use nym_mixnet_contract_common::rewarding::RewardEstimate;
use nym_mixnet_contract_common::NodeId;
pub(crate) enum RewardedSetStatus {
Active,
Standby,
Inactive,
}
impl From<MixnodeStatus> for RewardedSetStatus {
fn from(value: MixnodeStatus) -> Self {
match value {
MixnodeStatus::Active => RewardedSetStatus::Active,
MixnodeStatus::Standby => RewardedSetStatus::Standby,
// for all intents and purposes, missing node is treated as inactive for rewarding (since it wouldn't get anything
MixnodeStatus::Inactive => RewardedSetStatus::Inactive,
MixnodeStatus::NotFound => RewardedSetStatus::Inactive,
}
}
}
async fn gateway_identity_to_node_id(
cache: &NodeStatusCache,
identity: &str,
+5 -4
View File
@@ -148,7 +148,7 @@ impl AppState {
impl AppState {
pub(crate) async fn describe_nodes_cache_data(
&self,
) -> Result<RwLockReadGuard<Cache<DescribedNodes>>, AxumErrorResponse> {
) -> Result<RwLockReadGuard<'_, Cache<DescribedNodes>>, AxumErrorResponse> {
Ok(self.described_nodes_cache().get().await?)
}
@@ -160,7 +160,8 @@ impl AppState {
pub(crate) async fn node_annotations(
&self,
) -> Result<RwLockReadGuard<Cache<HashMap<NodeId, NodeAnnotation>>>, AxumErrorResponse> {
) -> Result<RwLockReadGuard<'_, Cache<HashMap<NodeId, NodeAnnotation>>>, AxumErrorResponse>
{
self.node_status_cache()
.node_annotations()
.await
@@ -169,7 +170,7 @@ impl AppState {
pub(crate) async fn legacy_mixnode_annotations(
&self,
) -> Result<RwLockReadGuard<Cache<HashMap<NodeId, MixNodeBondAnnotated>>>, AxumErrorResponse>
) -> Result<RwLockReadGuard<'_, Cache<HashMap<NodeId, MixNodeBondAnnotated>>>, AxumErrorResponse>
{
self.node_status_cache()
.annotated_legacy_mixnodes()
@@ -179,7 +180,7 @@ impl AppState {
pub(crate) async fn legacy_gateways_annotations(
&self,
) -> Result<RwLockReadGuard<Cache<HashMap<NodeId, GatewayBondAnnotated>>>, AxumErrorResponse>
) -> Result<RwLockReadGuard<'_, Cache<HashMap<NodeId, GatewayBondAnnotated>>>, AxumErrorResponse>
{
self.node_status_cache()
.annotated_legacy_gateways()

Some files were not shown because too many files have changed in this diff Show More