Compare commits

..

3 Commits

Author SHA1 Message Date
Jędrzej Stuczyński 351acb7875 expose cancelled 2025-10-13 15:46:29 +01:00
Jędrzej Stuczyński 7f4ef7f772 add drop guard to client tasks 2025-10-13 15:45:46 +01:00
Jędrzej Stuczyński 5025c49a0e using same hierarchy of trackers for client shutdown control 2025-10-13 15:45:46 +01:00
291 changed files with 9497 additions and 9887 deletions
-3
View File
@@ -8,13 +8,10 @@ on:
- 'gateway/**'
- 'integrations/**'
- 'nym-api/**'
- 'nym-authenticator-client/**'
- 'nym-credential-proxy/**'
- 'nym-ip-packet-client/**'
- 'nym-network-monitor/**'
- 'nym-node/**'
- 'nym-node-status-api/**'
- 'nym-registration-client/**'
- 'nym-statistics-api/**'
- 'nym-outfox/**'
- 'nym-validator-rewarder/**'
+1 -1
View File
@@ -54,7 +54,7 @@ jobs:
uses: actions-rs/cargo@v1
with:
command: test
args: --lib --manifest-path contracts/Cargo.toml --all-features
args: --lib --manifest-path contracts/Cargo.toml
- name: Check formatting
uses: actions-rs/cargo@v1
+1 -1
View File
@@ -10,7 +10,7 @@ on:
jobs:
build:
runs-on: arc-linux-latest
runs-on: arc-ubuntu-22.04
env:
RUSTUP_PERMIT_COPY_RENAME: 1
defaults:
+9 -10
View File
@@ -6,14 +6,16 @@ on:
paths:
- "ts-packages/**"
- "sdk/typescript/**"
- "nym-connect/desktop/src/**"
- "nym-connect/desktop/package.json"
- "nym-wallet/src/**"
- "nym-wallet/package.json"
- "explorer-v2/**"
- "explorer/**"
- ".github/workflows/ci-lint-typescript.yml"
jobs:
build:
runs-on: arc-linux-latest
runs-on: ubuntu-22.04
env:
RUSTUP_PERMIT_COPY_RENAME: 1
steps:
@@ -23,7 +25,6 @@ jobs:
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Setup yarn
run: npm install -g yarn
@@ -36,12 +37,14 @@ jobs:
run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
- name: Install wasm-opt
run: cargo install wasm-opt
uses: ./.github/actions/install-wasm-opt
with:
version: '116'
- name: Set up Go
uses: actions/setup-go@v6
uses: actions/setup-go@v5
with:
go-version: "1.24.6"
go-version: "1.23.7"
- name: Install
run: yarn
@@ -49,11 +52,7 @@ jobs:
- name: Build packages
run: yarn build:ci
- name: Install again
run: yarn
- name: Lint
run: yarn lint
- name: Typecheck with tsc
run: yarn tsc
@@ -8,7 +8,7 @@ on:
jobs:
build:
runs-on: arc-linux-latest
runs-on: custom-linux
steps:
- uses: actions/checkout@v4
+4 -7
View File
@@ -4,7 +4,7 @@ on:
jobs:
publish:
runs-on: ubuntu-latest
runs-on: arc-ubuntu-22.04
steps:
- uses: actions/checkout@v4
@@ -17,13 +17,10 @@ jobs:
- name: Setup yarn
run: npm install -g yarn
- name: Install rust toolchain
- name: Install Rust stable
uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
components: rustfmt, clippy
- name: Install wasm-pack
run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
@@ -32,9 +29,9 @@ jobs:
run: cargo install wasm-opt
- name: Set up Go
uses: actions/setup-go@v6
uses: actions/setup-go@v5
with:
go-version: "1.24.6"
go-version: "1.23.7"
- name: Install dependencies
run: yarn
+30 -3
View File
@@ -3,6 +3,11 @@ name: Build and upload Node Status agent container to harbor.nymte.ch
on:
workflow_dispatch:
inputs:
gateway_probe_git_ref:
type: string
default: nym-vpn-core-v1.4.0
required: true
description: Which gateway probe git ref to build the image with
release_image:
description: 'Tag image as a release'
required: true
@@ -38,6 +43,16 @@ jobs:
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
run: |
GATEWAY_PROBE_GIT_REF=${{ github.event.inputs.gateway_probe_git_ref }}
GIT_REF_SLUG="${GATEWAY_PROBE_GIT_REF//\//-}"
echo "git_ref=${GIT_REF_SLUG}" >> $GITHUB_OUTPUT
- 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: Initialize RELEASE_TAG
run: echo "RELEASE_TAG=" >> $GITHUB_ENV
@@ -46,12 +61,24 @@ jobs:
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
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' IMAGE_NAME_AND_TAGS='$IMAGE_NAME_AND_TAGS'"
run: echo "RELEASE_TAG='$RELEASE_TAG' GIT_TAG='$GIT_TAG' IMAGE_NAME_AND_TAGS='$IMAGE_NAME_AND_TAGS'"
# - name: Remove existing tag if exists
# run: |
# if git rev-parse $${{ env.GIT_TAG }} >/dev/null 2>&1; then
# git push --delete origin $${{ env.GIT_TAG }}
# git tag -d $${{ env.GIT_TAG }}
# fi
# - name: Create tag
# run: |
# git tag -a $${{ env.GIT_TAG }} -m "Version ${{ steps.get_version.outputs.result }}-${{ steps.cleanup_gateway_probe_ref.outputs.git_ref }}"
# git push origin $${{ env.GIT_TAG }}
- name: BuildAndPushImageOnHarbor
run: |
docker build -f ${{ env.WORKING_DIRECTORY }}/Dockerfile . -t harbor.nymte.ch/nym/${{ env.IMAGE_NAME_AND_TAGS }}
docker build --build-arg GIT_REF=${{ github.event.inputs.gateway_probe_git_ref }} -f ${{ env.WORKING_DIRECTORY }}/Dockerfile . -t harbor.nymte.ch/nym/${{ env.IMAGE_NAME_AND_TAGS }}
docker push harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }} --all-tags
-84
View File
@@ -4,90 +4,6 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
## [Unreleased]
## [2025.19-kase] (2025-10-30)
- update ns agent workflow ([#6154])
- Cherry pick - request #6143 from nymtech/bugfix/mix-tx-closed-v2 ([#6153])
- bugfix: nym-credential-proxy query params parsing regression ([#6121])
- bugfix: revert some dep updates introduced in #6043 ([#6120])
- Skip ipv6 metadata endpoint request ([#6118])
- update to no longer use 1mb files ([#6117])
- chore: restore pending dkg contract state migration ([#6116])
- Revert "Propagate cancel token to mixnet client" ([#6115])
- Update dirs to 6.0 ([#6109])
- Propagate cancel token to mixnet client ([#6105])
- bugfix: retrieve and update ticketbook in the same query ([#6101])
- bugfix: include network name in the default gateway probe config path ([#6100])
- Bugfix/incompatibility fixes ([#6099])
- [DOCs/operators] QUIC deployment script & docs ([#6098])
- bugfix: testnet manager 02sql migration ([#6096])
- feat: move gateway probe to monorepo (and update to rust edition 2024) ([#6094])
- bugfix: use custom topology provider for list of init gateways ([#6092])
- Max/fix wasm client + build commands ([#6043])
[#6154]: https://github.com/nymtech/nym/pull/6154
[#6153]: https://github.com/nymtech/nym/pull/6153
[#6121]: https://github.com/nymtech/nym/pull/6121
[#6120]: https://github.com/nymtech/nym/pull/6120
[#6118]: https://github.com/nymtech/nym/pull/6118
[#6117]: https://github.com/nymtech/nym/pull/6117
[#6116]: https://github.com/nymtech/nym/pull/6116
[#6115]: https://github.com/nymtech/nym/pull/6115
[#6109]: https://github.com/nymtech/nym/pull/6109
[#6105]: https://github.com/nymtech/nym/pull/6105
[#6101]: https://github.com/nymtech/nym/pull/6101
[#6100]: https://github.com/nymtech/nym/pull/6100
[#6099]: https://github.com/nymtech/nym/pull/6099
[#6098]: https://github.com/nymtech/nym/pull/6098
[#6096]: https://github.com/nymtech/nym/pull/6096
[#6094]: https://github.com/nymtech/nym/pull/6094
[#6092]: https://github.com/nymtech/nym/pull/6092
[#6043]: https://github.com/nymtech/nym/pull/6043
## [2025.18-jarlsberg] (2025-10-14)
- ns-api: add descriptions to dVPN gateway responses ([#6102])
- NS API: use new probe download filesize and milliseconds field ([#6097])
- ns-api: use download files size from probes instead of parsing filenames ([#6095])
- ns-api: add new fields for probe output for query_metadata and download file size and duration in ms ([#6091])
- Bugfix/bloomfilters purge ([#6089])
- Hotfix: Update API source in node ping tester script ([#6082])
- Get wireguard keypair as arg instead of reading it from disk ([#6078])
- Feature: Ping probe all nodes /described nodes from a server ([#6074])
- Node Status API: add bridge information to dVPN endpoint ([#6069])
- frontdoor typo fix ([#6067])
- Feature: Node rewards tracker ([#6064])
- [chore] Clippy fix ([#6060])
- Registration Client ([#6059])
- Bugfix: Nym node CLI download nym-node exception ([#6058])
- Feature: Nym node html landing page ([#6053])
- feat: DKG contract method for updating announce address ([#6050])
- feat: NS ticket faucet ([#6047])
- Bridge proto client params in Self-Described ([#6035])
- Node Status API: remove sqlite support ([#6004])
- Benny/ci contract fix ([#5962])
[#6102]: https://github.com/nymtech/nym/pull/6102
[#6097]: https://github.com/nymtech/nym/pull/6097
[#6095]: https://github.com/nymtech/nym/pull/6095
[#6091]: https://github.com/nymtech/nym/pull/6091
[#6089]: https://github.com/nymtech/nym/pull/6089
[#6082]: https://github.com/nymtech/nym/pull/6082
[#6078]: https://github.com/nymtech/nym/pull/6078
[#6074]: https://github.com/nymtech/nym/pull/6074
[#6069]: https://github.com/nymtech/nym/pull/6069
[#6067]: https://github.com/nymtech/nym/pull/6067
[#6064]: https://github.com/nymtech/nym/pull/6064
[#6060]: https://github.com/nymtech/nym/pull/6060
[#6059]: https://github.com/nymtech/nym/pull/6059
[#6058]: https://github.com/nymtech/nym/pull/6058
[#6053]: https://github.com/nymtech/nym/pull/6053
[#6050]: https://github.com/nymtech/nym/pull/6050
[#6047]: https://github.com/nymtech/nym/pull/6047
[#6035]: https://github.com/nymtech/nym/pull/6035
[#6004]: https://github.com/nymtech/nym/pull/6004
[#5962]: https://github.com/nymtech/nym/pull/5962
## [2025.17-isabirra] (2025-09-29)
- Bugfix | Fix the registration handshake ([#6062])
Generated
+815 -726
View File
File diff suppressed because it is too large Load Diff
+15 -9
View File
@@ -150,7 +150,7 @@ members = [
"tools/internal/contract-state-importer/importer-cli",
"tools/internal/contract-state-importer/importer-contract",
"tools/internal/mixnet-connectivity-check",
# "tools/internal/sdk-version-bump",
# "tools/internal/sdk-version-bump",
"tools/internal/ssl-inject",
"tools/internal/testnet-manager",
"tools/internal/testnet-manager/dkg-bypass-contract",
@@ -166,7 +166,7 @@ members = [
"wasm/node-tester",
"wasm/zknym-lib",
"nym-gateway-probe"
, "query-tester"]
]
default-members = [
"clients/native",
@@ -215,6 +215,7 @@ base64 = "0.22.1"
base85rs = "0.1.3"
bincode = "1.3.3"
bip39 = { version = "2.0.0", features = ["zeroize"] }
bit-vec = "0.7.0" # can we unify those?
bitvec = "1.0.0"
blake3 = "1.7.0"
bloomfilter = "3.0.1"
@@ -242,11 +243,13 @@ criterion = "0.5"
csv = "1.3.1"
ctr = "0.9.1"
cupid = "0.6.1"
curve25519-dalek = "4.1"
dashmap = "5.5.3"
# We want https://github.com/DefGuard/wireguard-rs/pull/64 , but there's no crates.io release being pushed out anymore
defguard_wireguard_rs = { git = "https://github.com/DefGuard/wireguard-rs.git", rev = "v0.4.7" }
digest = "0.10.7"
dirs = "6.0"
dirs = "5.0"
doc-comment = "0.3"
dotenvy = "0.15.6"
dyn-clone = "1.0.19"
ecdsa = "0.16"
@@ -262,8 +265,11 @@ futures = "0.3.31"
futures-util = "0.3"
generic-array = "0.14.7"
getrandom = "0.2.10"
getset = "0.1.5"
handlebars = "3.5.5"
headers = "0.4.0"
hex = "0.4.3"
hex-literal = "0.3.3"
hickory-resolver = "0.25"
hkdf = "0.12.3"
hmac = "0.12.1"
@@ -287,10 +293,12 @@ lazy_static = "1.5.0"
ledger-transport = "0.10.0"
ledger-transport-hid = "0.10.0"
log = "0.4"
maxminddb = "0.23.0"
mime = "0.3.17"
moka = { version = "0.12", features = ["future"] }
nix = "0.27.1"
notify = "5.1.0"
okapi = "0.7.0"
once_cell = "1.21.3"
opentelemetry = "0.19.0"
opentelemetry-jaeger = "0.18.0"
@@ -299,6 +307,7 @@ pem = "0.8"
petgraph = "0.6.5"
pin-project = "1.1"
pnet_packet = "0.35.0"
pin-project-lite = "0.2.16"
publicsuffix = "2.3.0"
proc_pidinfo = "0.1.3"
quote = "1"
@@ -306,10 +315,13 @@ rand = "0.8.5"
rand_chacha = "0.3"
rand_core = "0.6.3"
rand_distr = "0.4"
rand_pcg = "0.3.1"
rand_seeder = "0.2.3"
rayon = "1.5.1"
regex = "1.10.6"
reqwest = { version = "0.12.15", default-features = false }
rs_merkle = "1.5.0"
safer-ffi = "0.1.13"
schemars = "0.8.22"
semver = "1.0.26"
serde = "1.0.219"
@@ -356,7 +368,6 @@ tracing-indicatif = "0.3.9"
tracing-test = "0.2.5"
ts-rs = "10.1.0"
tungstenite = { version = "0.20.1", default-features = false }
typed-builder = "0.23.0"
uniffi = "0.29.2"
uniffi_build = "0.29.0"
url = "2.5"
@@ -453,11 +464,6 @@ opt-level = 'z'
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(tokio_unstable)'] }
[workspace.lints.clippy]
suspicious = "deny"
complexity = "deny"
perf = "deny"
style = "deny"
unwrap_used = "deny"
expect_used = "deny"
todo = "deny"
+5 -6
View File
@@ -107,16 +107,16 @@ sdk-wasm-build:
$(MAKE) -C nym-browser-extension/storage wasm-pack
$(MAKE) -C wasm/client
$(MAKE) -C wasm/node-tester
$(MAKE) -C wasm/mix-fetch
# $(MAKE) -C wasm/mix-fetch
$(MAKE) -C wasm/zknym-lib
# $(MAKE) -C wasm/full-nym-wasm
# run this from npm/yarn to ensure tools are in the path, e.g. yarn build:sdk from root of repo
sdk-typescript-build:
npx lerna run --scope @nymproject/sdk build --stream
npx lerna run --scope @nymproject/mix-fetch build --stream
npx lerna run --scope @nymproject/node-tester build --stream
yarn --cwd sdk/typescript/codegen/contract-clients build
# npx lerna run --scope @nymproject/mix-fetch build --stream
# npx lerna run --scope @nymproject/node-tester build --stream
# yarn --cwd sdk/typescript/codegen/contract-clients build
# NOTE: These targets are part of the main workspace (but not as wasm32-unknown-unknown)
WASM_CRATES = extension-storage nym-client-wasm nym-node-tester-wasm zknym-lib
@@ -140,8 +140,7 @@ clippy: sdk-wasm-lint
WASM_CONTRACT_DIR := contracts/target/wasm32-unknown-unknown/release
# Find every direct contract folder that contains a Cargo.toml
#CONTRACT_DIRS := $(shell find contracts -type f -name Cargo.toml \( ! -path "contracts/Cargo.toml" \) | grep -v integration-tests | xargs -n1 dirname | sort -u)
CONTRACT_DIRS := contracts/example-contract
CONTRACT_DIRS := $(shell find contracts -type f -name Cargo.toml \( ! -path "contracts/Cargo.toml" \) | grep -v integration-tests | xargs -n1 dirname | sort -u)
CONTRACTS_OUT_DIR = contracts/artifacts
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "nym-client"
version = "1.1.65"
version = "1.1.63"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>", "Jędrzej Stuczyński <andrew@nymtech.net>"]
description = "Implementation of the Nym Client"
edition = "2021"
-1
View File
@@ -60,7 +60,6 @@ impl SocketClient {
let ClientInput {
connection_command_sender,
input_sender,
..
} = client_input;
let ClientOutput {
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "nym-socks5-client"
version = "1.1.65"
version = "1.1.63"
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"
+1 -1
View File
@@ -36,7 +36,7 @@ nym-bandwidth-controller = { path = "../bandwidth-controller" }
nym-crypto = { path = "../crypto" }
nym-gateway-client = { path = "../client-libs/gateway-client" }
nym-gateway-requests = { path = "../gateway-requests" }
nym-http-api-client = { path = "../http-api-client", features = ["network-defaults"] }
nym-http-api-client = { path = "../http-api-client" }
nym-nonexhaustive-delayqueue = { path = "../nonexhaustive-delayqueue" }
nym-sphinx = { path = "../nymsphinx" }
nym-statistics-common = { path = "../statistics" }
@@ -114,12 +114,13 @@ where
})?;
hardcoded_topology.entry_capable_nodes().cloned().collect()
} else {
let mut rng = rand::thread_rng();
crate::init::helpers::gateways_for_init(
&mut rng,
&core.client.nym_api_urls,
user_agent,
core.debug.topology.minimum_gateway_performance,
core.debug.topology.ignore_ingress_epoch_role,
None,
)
.await?
};
@@ -173,12 +173,13 @@ where
})?;
hardcoded_topology.entry_capable_nodes().cloned().collect()
} else {
let mut rng = rand::thread_rng();
crate::init::helpers::gateways_for_init(
&mut rng,
&core.client.nym_api_urls,
user_agent,
core.debug.topology.minimum_gateway_performance,
core.debug.topology.ignore_ingress_epoch_role,
None,
)
.await?
};
+51 -211
View File
@@ -7,12 +7,11 @@ use super::statistics_control::StatisticsControl;
use crate::client::base_client::storage::helpers::store_client_keys;
use crate::client::base_client::storage::MixnetClientStorage;
use crate::client::cover_traffic_stream::LoopCoverTrafficStream;
use crate::client::event_control::EventControl;
use crate::client::inbound_messages::{InputMessage, InputMessageReceiver, InputMessageSender};
use crate::client::key_manager::persistence::KeyStore;
use crate::client::key_manager::ClientKeys;
use crate::client::mix_traffic::transceiver::{GatewayReceiver, GatewayTransceiver, RemoteGateway};
use crate::client::mix_traffic::{BatchMixMessageSender, MixTrafficController, MixTrafficEvent};
use crate::client::mix_traffic::{BatchMixMessageSender, MixTrafficController};
use crate::client::real_messages_control;
use crate::client::real_messages_control::RealMessagesController;
use crate::client::received_buffer::{
@@ -67,16 +66,13 @@ use std::path::Path;
use std::sync::Arc;
use time::OffsetDateTime;
use tokio::sync::mpsc::Sender;
use tracing::*;
use url::Url;
#[cfg(target_arch = "wasm32")]
#[cfg(debug_assertions)]
use wasm_utils::console_log;
/// Default number of retries for Nym API requests when using network details with domain fronting.
/// This allows the client to try alternative URLs if the primary endpoint is unavailable.
const DEFAULT_NYM_API_RETRIES: usize = 3;
#[cfg(all(
not(target_arch = "wasm32"),
feature = "fs-surb-storage",
@@ -87,28 +83,10 @@ pub mod non_wasm_helpers;
pub mod helpers;
pub mod storage;
#[derive(Clone, Copy, Debug)]
pub enum MixnetClientEvent {
Traffic(MixTrafficEvent),
}
pub type EventReceiver = mpsc::UnboundedReceiver<MixnetClientEvent>;
#[derive(Clone)]
pub struct EventSender(pub mpsc::UnboundedSender<MixnetClientEvent>);
impl EventSender {
pub fn send(&self, event: MixnetClientEvent) {
if let Err(err) = self.0.unbounded_send(event) {
tracing::warn!("Failed to send error event. The caller event reader was closed: {err}");
}
}
}
#[derive(Clone)]
pub struct ClientInput {
pub connection_command_sender: ConnectionCommandSender,
pub input_sender: InputMessageSender,
pub client_request_sender: ClientRequestSender,
}
impl ClientInput {
@@ -216,14 +194,10 @@ pub struct BaseClientBuilder<C, S: MixnetClientStorage> {
client_store: S,
dkg_query_client: Option<C>,
// Optional API URLs for domain fronting support
nym_api_urls: Option<Vec<nym_network_defaults::ApiUrl>>,
wait_for_gateway: bool,
custom_topology_provider: Option<Box<dyn TopologyProvider + Send + Sync>>,
custom_gateway_transceiver: Option<Box<dyn GatewayTransceiver + Send>>,
shutdown: Option<ShutdownTracker>,
event_tx: Option<EventSender>,
user_agent: Option<UserAgent>,
setup_method: GatewaySetup,
@@ -248,12 +222,10 @@ where
config: base_config,
client_store,
dkg_query_client,
nym_api_urls: None,
wait_for_gateway: false,
custom_topology_provider: None,
custom_gateway_transceiver: None,
shutdown: None,
event_tx: None,
user_agent: None,
setup_method: GatewaySetup::MustLoad { gateway_id: None },
#[cfg(unix)]
@@ -271,16 +243,6 @@ where
self
}
/// Set Nym API URLs for domain fronting support.
///
/// When provided, the client will use these API URLs (which include front_hosts)
/// to construct HTTP clients with domain fronting enabled.
#[must_use]
pub fn with_nym_api_urls(mut self, nym_api_urls: Vec<nym_network_defaults::ApiUrl>) -> Self {
self.nym_api_urls = Some(nym_api_urls);
self
}
#[must_use]
pub fn with_forget_me(mut self, forget_me: &ForgetMe) -> Self {
self.config.debug.forget_me = *forget_me;
@@ -326,12 +288,6 @@ where
self
}
#[must_use]
pub fn with_event_tx(mut self, event_tx: EventSender) -> Self {
self.event_tx = Some(event_tx);
self
}
#[must_use]
pub fn with_user_agent(mut self, user_agent: UserAgent) -> Self {
self.user_agent = Some(user_agent);
@@ -362,18 +318,6 @@ where
details.client_address()
}
fn start_event_control(
parent_event_tx: Option<EventSender>,
children_event_rx: EventReceiver,
shutdown_tracker: &ShutdownTracker,
) {
let event_control = EventControl::new(parent_event_tx, children_event_rx);
shutdown_tracker.try_spawn_named_with_shutdown(
async move { event_control.run().await },
"EventControl",
);
}
// future constantly pumping loop cover traffic at some specified average rate
// the pumped traffic goes to the MixTrafficController
fn start_cover_traffic_stream(
@@ -385,7 +329,7 @@ where
stats_tx: ClientStatsSender,
shutdown_tracker: &ShutdownTracker,
) {
tracing::info!("Starting loop cover traffic stream...");
info!("Starting loop cover traffic stream...");
let mut stream = LoopCoverTrafficStream::new(
ack_key,
@@ -397,8 +341,14 @@ where
debug_config.cover_traffic,
stats_tx,
);
shutdown_tracker
.try_spawn_named_with_shutdown(async move { stream.run().await }, "CoverTrafficStream");
let drop_guard = shutdown_tracker.clone_shutdown_token().drop_guard();
shutdown_tracker.try_spawn_named_with_shutdown(
async move {
let _ = drop_guard;
stream.run().await
},
"CoverTrafficStream",
);
}
#[allow(clippy::too_many_arguments)]
@@ -417,7 +367,7 @@ where
stats_tx: ClientStatsSender,
shutdown_tracker: &ShutdownTracker,
) {
tracing::info!("Starting real traffic stream...");
info!("Starting real traffic stream...");
let real_messages_controller = RealMessagesController::new(
controller_config,
@@ -475,8 +425,10 @@ where
"AcknowledgementController::RetransmissionRequestListener",
);
let drop_guard = shutdown_tracker.clone_shutdown_token().drop_guard();
shutdown_tracker.try_spawn_named_with_shutdown(
async move {
let _ = drop_guard;
sent_notification_listener.run().await;
},
"AcknowledgementController::SentNotificationListener",
@@ -487,8 +439,6 @@ where
async move { ack_action_controller.run(shutdown_token).await },
"AcknowledgementController::ActionController",
);
// .start(packet_type);
}
// buffer controlling all messages fetched from provider
@@ -502,7 +452,7 @@ where
metrics_reporter: ClientStatsSender,
shutdown_tracker: &ShutdownTracker,
) {
tracing::info!("Starting received messages buffer controller...");
info!("Starting received messages buffer controller...");
let controller = ReceivedMessagesBufferController::<SphinxMessageReceiver>::new(
local_encryption_keypair,
query_receiver,
@@ -613,7 +563,7 @@ where
details_store
.upgrade_stored_remote_gateway_key(gateway_client.gateway_identity(), &updated_key)
.await.map_err(|err| {
tracing::error!("failed to store upgraded gateway key! this connection might be forever broken now: {err}");
error!("failed to store upgraded gateway key! this connection might be forever broken now: {err}");
ClientCoreError::GatewaysDetailsStoreError { source: Box::new(err) }
})?
}
@@ -710,7 +660,7 @@ where
if topology_config.disable_refreshing {
// if we're not spawning the refresher, don't cause shutdown immediately
tracing::info!("The background topology refresher is not going to be started");
info!("The background topology refresher is not going to be started");
}
let mut topology_refresher = TopologyRefresher::new(
@@ -720,7 +670,7 @@ where
);
// before returning, block entire runtime to refresh the current network view so that any
// components depending on topology would see a non-empty view
tracing::info!("Obtaining initial network topology");
info!("Obtaining initial network topology");
topology_refresher.try_refresh().await;
if let Err(err) = topology_refresher.ensure_topology_is_routable().await {
@@ -746,13 +696,13 @@ where
.wait_for_gateway(local_gateway, waiting_timeout)
.await
{
tracing::error!(
error!(
"the gateway did not come back online within the specified timeout: {err}"
);
return Err(err.into());
}
} else {
tracing::error!("the gateway we're supposedly connected to does not exist. We'll not be able to send any packets to ourselves: {err}");
error!("the gateway we're supposedly connected to does not exist. We'll not be able to send any packets to ourselves: {err}");
return Err(err.into());
}
}
@@ -760,9 +710,13 @@ where
if !topology_config.disable_refreshing {
// don't spawn the refresher if we don't want to be refreshing the topology.
// only use the initial values obtained
tracing::info!("Starting topology refresher...");
info!("Starting topology refresher...");
let drop_guard = shutdown_tracker.clone_shutdown_token().drop_guard();
shutdown_tracker.try_spawn_named_with_shutdown(
async move { topology_refresher.run().await },
async move {
let _ = drop_guard;
topology_refresher.run().await
},
"TopologyRefresher",
);
}
@@ -777,7 +731,7 @@ where
input_sender: Sender<InputMessage>,
shutdown_tracker: &ShutdownTracker,
) -> ClientStatsSender {
tracing::info!("Starting statistics control...");
info!("Starting statistics control...");
StatisticsControl::create_and_start(
config.debug.stats_reporting,
user_agent
@@ -792,17 +746,10 @@ where
fn start_mix_traffic_controller(
gateway_transceiver: Box<dyn GatewayTransceiver + Send>,
shutdown_tracker: &ShutdownTracker,
event_tx: EventSender,
) -> (BatchMixMessageSender, ClientRequestSender) {
tracing::info!("Starting mix traffic controller...");
let mut mix_traffic_controller = MixTrafficController::new(
gateway_transceiver,
shutdown_tracker.clone_shutdown_token(),
event_tx,
);
let mix_tx = mix_traffic_controller.mix_tx();
let client_tx = mix_traffic_controller.client_tx();
info!("Starting mix traffic controller...");
let (mut mix_traffic_controller, mix_tx, client_tx) =
MixTrafficController::new(gateway_transceiver, shutdown_tracker.clone_shutdown_token());
shutdown_tracker.try_spawn_named(
async move { mix_traffic_controller.run().await },
@@ -866,7 +813,7 @@ where
{
// if client keys do not exist already, create and persist them
if key_store.load_keys().await.is_err() {
tracing::info!("could not find valid client keys - a new set will be generated");
info!("could not find valid client keys - a new set will be generated");
let mut rng = OsRng;
let keys = if let Some(derivation_material) = derivation_material {
ClientKeys::from_master_key(&mut rng, &derivation_material)
@@ -881,67 +828,21 @@ where
}
fn construct_nym_api_client(
nym_api_urls: Option<&Vec<nym_network_defaults::ApiUrl>>,
config: &Config,
user_agent: Option<UserAgent>,
) -> Result<nym_http_api_client::Client, ClientCoreError> {
tracing::debug!(
"construct_nym_api_client called with nym_api_urls: {}",
nym_api_urls.is_some()
);
// If API URLs are provided, use new_with_fronted_urls() which handles domain fronting
if let Some(nym_api_urls) = nym_api_urls {
if nym_api_urls.is_empty() {
tracing::warn!("Provided nym_api_urls is empty, falling back to config endpoints");
} else {
tracing::info!(
"Building nym-api client from provided URLs (with domain fronting support): {} URLs",
nym_api_urls.len()
);
let mut builder =
nym_http_api_client::ClientBuilder::new_with_fronted_urls(nym_api_urls.clone())
.map_err(ClientCoreError::from)?
.with_retries(DEFAULT_NYM_API_RETRIES);
if let Some(user_agent) = user_agent {
builder = builder.with_user_agent(user_agent);
}
return builder.build().map_err(ClientCoreError::from);
}
}
// Fallback to basic client for backwards compatibility
tracing::debug!("Building basic nym-api HTTP client from config endpoints");
let mut nym_api_urls = config.get_nym_api_endpoints();
if nym_api_urls.is_empty() {
tracing::warn!("No API endpoints configured in config, this may cause issues");
}
nym_api_urls.shuffle(&mut thread_rng());
// Convert config URLs to ApiUrl format for consistency
let api_urls: Vec<nym_network_defaults::ApiUrl> = nym_api_urls
.into_iter()
.map(|url| nym_network_defaults::ApiUrl {
url: url.to_string(),
front_hosts: None,
})
.collect();
tracing::debug!("Using {} config API endpoints", api_urls.len());
let mut builder = nym_http_api_client::ClientBuilder::new_with_fronted_urls(api_urls)
.map_err(ClientCoreError::from)?
.with_retries(DEFAULT_NYM_API_RETRIES)
.with_bincode();
let mut builder = nym_http_api_client::Client::builder(nym_api_urls[0].clone())
.map_err(ClientCoreError::from)?;
if let Some(user_agent) = user_agent {
builder = builder.with_user_agent(user_agent);
}
builder = builder.with_bincode();
builder.build().map_err(ClientCoreError::from)
}
@@ -959,7 +860,7 @@ where
<S::CredentialStore as CredentialStorage>::StorageError: Send + Sync + 'static,
<S::GatewaysDetailsStore as GatewaysDetailsStore>::StorageError: Sync + Send,
{
tracing::info!("Starting nym client");
info!("Starting nym client");
#[cfg(debug_assertions)]
#[cfg(target_arch = "wasm32")]
{
@@ -993,9 +894,6 @@ where
// channels responsible for controlling real messages
let (input_sender, input_receiver) = tokio::sync::mpsc::channel::<InputMessage>(1);
// channels responsible for event management
let (event_sender, event_receiver) = mpsc::unbounded();
// channels responsible for controlling ack messages
let (ack_sender, ack_receiver) = mpsc::unbounded();
let shared_topology_accessor =
@@ -1005,11 +903,9 @@ where
// or get one from the registry
let shutdown_tracker = match self.shutdown {
Some(parent_tracker) => parent_tracker.clone(),
None => nym_task::create_sdk_shutdown_tracker()?,
None => nym_task::get_sdk_shutdown_tracker()?,
};
Self::start_event_control(self.event_tx, event_receiver, &shutdown_tracker);
// channels responsible for dealing with reply-related fun
let (reply_controller_sender, reply_controller_receiver) =
reply_controller::requests::new_control_channels();
@@ -1025,11 +921,7 @@ where
.dkg_query_client
.map(|client| BandwidthController::new(credential_store, client));
let nym_api_client = Self::construct_nym_api_client(
self.nym_api_urls.as_ref(),
&self.config,
self.user_agent.clone(),
)?;
let nym_api_client = Self::construct_nym_api_client(&self.config, self.user_agent.clone())?;
let key_rotation_config = Self::determine_key_rotation_state(&nym_api_client).await?;
let topology_provider = Self::setup_topology_provider(
@@ -1044,7 +936,7 @@ where
self.user_agent.clone(),
generate_client_stats_id(*self_address.identity()),
input_sender.clone(),
&shutdown_tracker.clone(),
&shutdown_tracker,
);
// needs to be started as the first thing to block if required waiting for the gateway
@@ -1054,7 +946,7 @@ where
shared_topology_accessor.clone(),
self_address.gateway(),
self.wait_for_gateway,
&shutdown_tracker.clone(),
&shutdown_tracker,
)
.await?;
@@ -1074,7 +966,7 @@ where
stats_reporter.clone(),
#[cfg(unix)]
self.connection_fd_callback,
&shutdown_tracker.clone(),
&shutdown_tracker,
)
.await?;
let gateway_ws_fd = gateway_transceiver.ws_fd();
@@ -1082,7 +974,7 @@ where
let reply_storage = Self::setup_persistent_reply_storage(
reply_storage_backend,
key_rotation_config,
&shutdown_tracker.clone(),
&shutdown_tracker,
)
.await?;
@@ -1093,7 +985,7 @@ where
reply_storage.key_storage(),
reply_controller_sender.clone(),
stats_reporter.clone(),
&shutdown_tracker.clone(),
&shutdown_tracker,
);
// The message_sender is the transmitter for any component generating sphinx packets
@@ -1101,11 +993,8 @@ where
// traffic stream.
// The MixTrafficController then sends the actual traffic
let (message_sender, client_request_sender) = Self::start_mix_traffic_controller(
gateway_transceiver,
&shutdown_tracker.clone(),
EventSender(event_sender),
);
let (message_sender, client_request_sender) =
Self::start_mix_traffic_controller(gateway_transceiver, &shutdown_tracker);
// Channels that the websocket listener can use to signal downstream to the real traffic
// controller that connections are closed.
@@ -1134,7 +1023,7 @@ where
shared_lane_queue_lengths.clone(),
client_connection_rx,
stats_reporter.clone(),
&shutdown_tracker.clone(),
&shutdown_tracker,
);
if !self
@@ -1150,12 +1039,12 @@ where
shared_topology_accessor.clone(),
message_sender,
stats_reporter.clone(),
&shutdown_tracker.clone(),
&shutdown_tracker,
);
}
tracing::debug!("Core client startup finished!");
tracing::debug!("The address of this client is: {self_address}");
debug!("Core client startup finished!");
debug!("The address of this client is: {self_address}");
#[cfg(debug_assertions)]
#[cfg(target_arch = "wasm32")]
@@ -1171,7 +1060,6 @@ where
client_input: ClientInput {
connection_command_sender: client_connection_tx,
input_sender,
client_request_sender,
},
},
client_output: ClientOutputStatus::AwaitingConsumer {
@@ -1187,6 +1075,7 @@ where
},
stats_reporter,
shutdown_handle: shutdown_tracker, // The primary tracker for this client
client_request_sender,
forget_me: self.config.debug.forget_me,
remember_me: self.config.debug.remember_me,
})
@@ -1200,57 +1089,8 @@ pub struct BaseClient {
pub client_output: ClientOutputStatus,
pub client_state: ClientState,
pub stats_reporter: ClientStatsSender,
pub client_request_sender: ClientRequestSender,
pub shutdown_handle: ShutdownTracker,
pub forget_me: ForgetMe,
pub remember_me: RememberMe,
}
#[cfg(test)]
mod tests {
use super::*;
use nym_network_defaults::{ApiUrl, NymNetworkDetails};
#[test]
fn test_network_details_with_multiple_urls() {
// Verify that network details can be configured with multiple API URLs
let mut network_details = NymNetworkDetails::new_empty();
network_details.nym_api_urls = Some(vec![
ApiUrl {
url: "https://validator.nymtech.net/api/".to_string(),
front_hosts: None,
},
ApiUrl {
url: "https://nym-frontdoor.vercel.app/api/".to_string(),
front_hosts: Some(vec!["vercel.app".to_string(), "vercel.com".to_string()]),
},
]);
assert_eq!(network_details.nym_api_urls.as_ref().unwrap().len(), 2);
assert!(network_details.nym_api_urls.as_ref().unwrap()[1]
.front_hosts
.is_some());
}
#[test]
fn test_network_details_with_front_hosts() {
// Verify that ApiUrl can store domain fronting configuration
let api_url = ApiUrl {
url: "https://nym-frontdoor.vercel.app/api/".to_string(),
front_hosts: Some(vec!["vercel.app".to_string(), "vercel.com".to_string()]),
};
assert_eq!(api_url.url, "https://nym-frontdoor.vercel.app/api/");
assert_eq!(api_url.front_hosts.as_ref().unwrap().len(), 2);
assert!(api_url
.front_hosts
.as_ref()
.unwrap()
.contains(&"vercel.app".to_string()));
}
#[test]
fn test_default_nym_api_retries_constant() {
// Verify the retry constant is set correctly
assert_eq!(DEFAULT_NYM_API_RETRIES, 3);
}
}
@@ -205,7 +205,7 @@ impl LoopCoverTrafficStream<OsRng> {
TrySendError::Full(_) => {
// This isn't a problem, if the channel is full means we're already sending the
// max amount of messages downstream can handle.
tracing::trace!("Failed to send cover message - channel full");
tracing::debug!("Failed to send cover message - channel full");
}
TrySendError::Closed(_) => {
tracing::warn!("Failed to send cover message - channel closed");
@@ -1,40 +0,0 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use futures::StreamExt;
use crate::client::base_client::{EventReceiver, EventSender, MixnetClientEvent};
/// Launches and manages task events, propagating upwards what is not strictly internal.
pub(crate) struct EventControl {
parent_event_tx: Option<EventSender>,
children_event_rx: EventReceiver,
}
impl EventControl {
pub(crate) fn new(
parent_event_tx: Option<EventSender>,
children_event_rx: EventReceiver,
) -> Self {
EventControl {
parent_event_tx,
children_event_rx,
}
}
fn is_internal(event: MixnetClientEvent) -> bool {
match event {
MixnetClientEvent::Traffic(_) => false,
}
}
pub(crate) async fn run(mut self) {
while let Some(event) = self.children_event_rx.next().await {
if let Some(parent_event_tx) = &self.parent_event_tx {
if !Self::is_internal(event) {
parent_event_tx.send(event);
}
}
}
}
}
@@ -1,10 +1,7 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::client::{
base_client::{EventSender, MixnetClientEvent},
mix_traffic::transceiver::GatewayTransceiver,
};
use crate::client::mix_traffic::transceiver::GatewayTransceiver;
use nym_gateway_requests::ClientRequest;
use nym_sphinx::forwarding::packet::MixPacket;
use nym_task::ShutdownToken;
@@ -20,41 +17,33 @@ pub mod transceiver;
// We remind ourselves that 32 x 32kb = 1024kb, a reasonable size for a network buffer.
pub const MIX_MESSAGE_RECEIVER_BUFFER_SIZE: usize = 32;
/// Reduced from 100 to 20 to fail fast (~1-2 seconds instead of ~6 seconds).
/// If we can't send 20 packets in a row, the gateway is unreachable.
const MAX_FAILURE_COUNT: usize = 20;
const MAX_FAILURE_COUNT: usize = 100;
// that's also disgusting.
pub struct Empty;
#[derive(Clone, Copy, Debug)]
pub enum MixTrafficEvent {
FailedSendingSphinx,
}
pub struct MixTrafficController {
gateway_transceiver: Box<dyn GatewayTransceiver + Send>,
mix_tx: BatchMixMessageSender,
mix_rx: BatchMixMessageReceiver,
client_rx: ClientRequestReceiver,
client_tx: ClientRequestSender,
// TODO: this is temporary work-around.
// in long run `gateway_client` will be moved away from `MixTrafficController` anyway.
consecutive_gateway_failure_count: usize,
shutdown_token: ShutdownToken,
event_tx: EventSender,
}
impl MixTrafficController {
pub fn new<T>(
gateway_transceiver: T,
shutdown_token: ShutdownToken,
event_tx: EventSender,
) -> MixTrafficController
) -> (
MixTrafficController,
BatchMixMessageSender,
ClientRequestSender,
)
where
T: GatewayTransceiver + Send + 'static,
{
@@ -63,32 +52,41 @@ impl MixTrafficController {
let (client_sender, client_receiver) = tokio::sync::mpsc::channel(8);
MixTrafficController {
gateway_transceiver: Box::new(gateway_transceiver),
mix_tx: message_sender,
mix_rx: message_receiver,
client_rx: client_receiver,
client_tx: client_sender,
consecutive_gateway_failure_count: 0,
shutdown_token,
event_tx,
}
(
MixTrafficController {
gateway_transceiver: Box::new(gateway_transceiver),
mix_rx: message_receiver,
client_rx: client_receiver,
consecutive_gateway_failure_count: 0,
shutdown_token,
},
message_sender,
client_sender,
)
}
pub fn new_dynamic(
gateway_transceiver: Box<dyn GatewayTransceiver + Send>,
shutdown_token: ShutdownToken,
event_tx: EventSender,
) -> MixTrafficController {
Self::new(gateway_transceiver, shutdown_token, event_tx)
}
pub fn client_tx(&self) -> ClientRequestSender {
self.client_tx.clone()
}
pub fn mix_tx(&self) -> BatchMixMessageSender {
self.mix_tx.clone()
) -> (
MixTrafficController,
BatchMixMessageSender,
ClientRequestSender,
) {
let (message_sender, message_receiver) =
tokio::sync::mpsc::channel(MIX_MESSAGE_RECEIVER_BUFFER_SIZE);
let (client_sender, client_receiver) = tokio::sync::mpsc::channel(8);
(
MixTrafficController {
gateway_transceiver,
mix_rx: message_receiver,
client_rx: client_receiver,
consecutive_gateway_failure_count: 0,
shutdown_token,
},
message_sender,
client_sender,
)
}
async fn on_messages(
@@ -140,6 +138,7 @@ impl MixTrafficController {
pub async fn run(&mut self) {
debug!("Started MixTrafficController with graceful shutdown support");
let _drop_guard = self.shutdown_token.clone().drop_guard();
loop {
tokio::select! {
biased;
@@ -147,31 +146,34 @@ impl MixTrafficController {
trace!("MixTrafficController: Received shutdown");
break;
}
// mix_rx should never error out as we're holding one instance of the sender
Some(mix_packets) = self.mix_rx.recv() => {
if let Err(err) = self.on_messages(mix_packets).await {
error!("Failed to send sphinx packet(s) to the gateway: {err}");
if self.consecutive_gateway_failure_count == MAX_FAILURE_COUNT {
// Disconnect from the gateway. If we should try to re-connect
// is handled at a higher layer.
error!("Failed to send sphinx packet to the gateway {MAX_FAILURE_COUNT} times in a row - assuming the gateway is dead");
// Do we need to handle the embedded mixnet client case
// separately?
self.event_tx.send(MixnetClientEvent::Traffic(MixTrafficEvent::FailedSendingSphinx));
// IMO it shouldn't be signalled from there but it is how it is
// TODO : report the failure upwards and shutdown from upwards
// Gateway is dead, we have to shut down currently
error!("Signalling shutdown from the MixTrafficController");
self.shutdown_token.cancel();
break;
mix_packets = self.mix_rx.recv() => match mix_packets {
Some(mix_packets) => {
if let Err(err) = self.on_messages(mix_packets).await {
error!("Failed to send sphinx packet(s) to the gateway: {err}");
if self.consecutive_gateway_failure_count == MAX_FAILURE_COUNT {
// Disconnect from the gateway. If we should try to re-connect
// is handled at a higher layer.
error!("Failed to send sphinx packet to the gateway {MAX_FAILURE_COUNT} times in a row - assuming the gateway is dead");
// Do we need to handle the embedded mixnet client case
// separately?
break;
}
}
},
None => {
trace!("MixTrafficController: Stopping since channel closed");
break;
}
},
client_request = self.client_rx.recv() => match client_request {
Some(client_request) => {
self.on_client_request(client_request).await;
},
None => {
trace!("MixTrafficController, client request channel closed");
break
}
},
// client_rx should never error out as we're holding one instance of the sender
Some(client_request) = self.client_rx.recv() => {
self.on_client_request(client_request).await;
}
}
}
debug!("MixTrafficController: Exiting");
-1
View File
@@ -3,7 +3,6 @@
pub mod base_client;
pub mod cover_traffic_stream;
pub(crate) mod event_control;
pub(crate) mod helpers;
pub mod inbound_messages;
pub mod key_manager;
@@ -80,6 +80,7 @@ impl AcknowledgementListener {
pub(crate) async fn run(&mut self, shutdown_token: ShutdownToken) {
debug!("Started AcknowledgementListener with graceful shutdown support");
let _drop_guard = shutdown_token.clone().drop_guard();
loop {
tokio::select! {
biased;
@@ -245,6 +245,7 @@ impl ActionController {
pub(crate) async fn run(&mut self, shutdown_token: ShutdownToken) {
debug!("Started ActionController with graceful shutdown support");
let _drop_guard = shutdown_token.clone().drop_guard();
loop {
tokio::select! {
biased;
@@ -216,6 +216,7 @@ where
pub(crate) async fn run(&mut self, shutdown_token: ShutdownToken) {
debug!("Started InputMessageListener with graceful shutdown support");
let _drop_guard = shutdown_token.clone().drop_guard();
loop {
tokio::select! {
biased;
@@ -167,6 +167,7 @@ where
pub(crate) async fn run(&mut self, shutdown_token: ShutdownToken) {
debug!("Started RetransmissionRequestListener with graceful shutdown support");
let _drop_guard = shutdown_token.clone().drop_guard();
loop {
tokio::select! {
biased;
@@ -298,8 +298,6 @@ where
"failed to send mixnet packet due to closed channel (outside of shutdown!)"
);
}
// Early return to avoid further processing when channel is closed
return;
}
Ok(_) => {
let event = if fragment_id.is_some() {
@@ -587,6 +585,7 @@ where
// avoid borrow on self
let shutdown_token = self.shutdown_token.clone();
let _drop_guard = shutdown_token.clone().drop_guard();
#[cfg(not(target_arch = "wasm32"))]
{
let mut status_timer = tokio::time::interval(Duration::from_secs(5));
@@ -497,6 +497,8 @@ impl<R: MessageReceiver> RequestReceiver<R> {
pub(crate) async fn run(&mut self) {
debug!("Started RequestReceiver with graceful shutdown support");
let _drop_guard = self.shutdown_token.clone().drop_guard();
loop {
tokio::select! {
biased;
@@ -540,6 +542,8 @@ impl<R: MessageReceiver> FragmentedMessageReceiver<R> {
pub(crate) async fn run(&mut self) -> Result<(), MessageRecoveryError> {
debug!("Started FragmentedMessageReceiver with graceful shutdown support");
let _drop_guard = self.shutdown_token.clone().drop_guard();
loop {
tokio::select! {
biased;
@@ -152,6 +152,7 @@ where
let polling_rate = self.config.key_rotation.epoch_duration / 8;
let mut invalidation_inspection = new_interval_stream(polling_rate);
let _drop_guard = shutdown_token.clone().drop_guard();
loop {
tokio::select! {
biased;
@@ -119,6 +119,8 @@ impl StatisticsControl {
let mut snapshot_interval =
gloo_timers::future::IntervalStream::new(SNAPSHOT_INTERVAL.as_millis() as u32);
let _drop_guard = shutdown_token.clone().drop_guard();
loop {
tokio::select! {
biased;
+20 -77
View File
@@ -45,7 +45,6 @@ type WsConn = JSWebsocket;
const CONCURRENT_GATEWAYS_MEASURED: usize = 20;
const MEASUREMENTS: usize = 3;
const DEFAULT_NYM_API_RETRIES: usize = 3;
#[cfg(not(target_arch = "wasm32"))]
const CONN_TIMEOUT: Duration = Duration::from_millis(1500);
@@ -133,27 +132,25 @@ impl<'a, G: ConnectableGateway> GatewayWithLatency<'a, G> {
}
}
pub async fn gateways_for_init(
pub async fn gateways_for_init<R: Rng>(
rng: &mut R,
nym_apis: &[Url],
user_agent: Option<UserAgent>,
minimum_performance: u8,
ignore_epoch_roles: bool,
retry_count: Option<usize>,
) -> Result<Vec<RoutingNode>, ClientCoreError> {
// Build client with ALL URLs for fallback support
let nym_api_urls: Vec<nym_http_api_client::Url> = nym_apis
.iter()
.map(|url| nym_http_api_client::Url::from(url.clone()))
.collect();
let nym_api = nym_apis
.choose(rng)
.ok_or(ClientCoreError::ListOfNymApisIsEmpty)?;
if nym_api_urls.is_empty() {
return Err(ClientCoreError::ListOfNymApisIsEmpty);
}
let retry_count = retry_count.unwrap_or(DEFAULT_NYM_API_RETRIES);
let mut builder = nym_http_api_client::ClientBuilder::new_with_urls(nym_api_urls.clone())?
.with_retries(retry_count)
.with_bincode();
// Use the unified HTTP client directly with optional user agent
let mut builder = nym_http_api_client::Client::builder(nym_api.clone())
.map_err(|e| {
ClientCoreError::ValidatorClientError(nym_validator_client::ValidatorClientError::from(
e,
))
})?
.with_bincode(); // Use bincode for better performance
if let Some(user_agent) = user_agent {
builder = builder.with_user_agent(user_agent);
@@ -163,7 +160,7 @@ pub async fn gateways_for_init(
ClientCoreError::ValidatorClientError(nym_validator_client::ValidatorClientError::from(e))
})?;
tracing::debug!("Fetching list of gateways from: {:?}", nym_api_urls);
tracing::debug!("Fetching list of gateways from: {nym_api}");
// Use our helper to handle pagination
let gateways = get_all_basic_entry_nodes_with_metadata(&client, true)
@@ -175,15 +172,17 @@ pub async fn gateways_for_init(
// filter out gateways below minimum performance and ones that could operate as a mixnode
// (we don't want instability)
let valid_gateways: Vec<RoutingNode> = gateways
let valid_gateways = gateways
.iter()
.filter(|g| ignore_epoch_roles || !g.supported_roles.mixnode)
.filter(|g| g.performance.round_to_integer() >= minimum_performance)
.filter_map(|gateway| gateway.try_into().ok())
.collect();
.collect::<Vec<_>>();
tracing::debug!("After checking validity: {}", valid_gateways.len());
tracing::trace!("Valid gateways: {valid_gateways:#?}");
tracing::info!(
"Found {} valid gateways after filtering",
"and {} after validity and performance filtering",
valid_gateways.len()
);
@@ -346,20 +345,13 @@ pub(super) fn get_specified_gateway(
must_use_tls: bool,
) -> Result<RoutingNode, ClientCoreError> {
tracing::debug!("Requesting specified gateway: {gateway_identity}");
let user_gateway = ed25519::PublicKey::from_base58_string(gateway_identity)
.map_err(ClientCoreError::UnableToCreatePublicKeyFromGatewayId)?;
let gateway = gateways
.iter()
.find(|gateway| gateway.identity_key == user_gateway)
.ok_or_else(|| {
tracing::debug!(
"Gateway {gateway_identity} not found in {} available gateways",
gateways.len()
);
ClientCoreError::NoGatewayWithId(gateway_identity.to_string())
})?;
.ok_or_else(|| ClientCoreError::NoGatewayWithId(gateway_identity.to_string()))?;
let Some(entry_details) = gateway.entry.as_ref() else {
return Err(ClientCoreError::UnsupportedEntry {
@@ -422,52 +414,3 @@ pub(super) async fn register_with_gateway(
authenticated_ephemeral_client: gateway_client,
})
}
#[cfg(test)]
mod tests {
use url::Url;
#[test]
fn test_single_url_builds_without_retries() {
let urls = [Url::parse("https://api.nym.com").unwrap()];
let nym_api_urls: Vec<nym_http_api_client::Url> = urls
.iter()
.map(|url| nym_http_api_client::Url::from(url.clone()))
.collect();
assert_eq!(nym_api_urls.len(), 1, "Should have exactly one URL");
}
#[test]
fn test_multiple_urls_prepared_for_retries() {
let urls = [
Url::parse("https://api1.nym.com").unwrap(),
Url::parse("https://api2.nym.com").unwrap(),
Url::parse("https://api3.nym.com").unwrap(),
];
let nym_api_urls: Vec<nym_http_api_client::Url> = urls
.iter()
.map(|url| nym_http_api_client::Url::from(url.clone()))
.collect();
assert_eq!(nym_api_urls.len(), 3, "Should have all three URLs");
assert!(
nym_api_urls.len() > 1,
"Multiple URLs trigger retry behavior"
);
}
#[test]
fn test_empty_url_list_is_detected() {
let urls: Vec<Url> = vec![];
let nym_api_urls: Vec<nym_http_api_client::Url> = urls
.iter()
.map(|url| nym_http_api_client::Url::from(url.clone()))
.collect();
assert!(nym_api_urls.is_empty(), "Empty list should remain empty");
}
}
@@ -46,6 +46,7 @@ where
debug!("Started PersistentReplyStorage");
if let Err(err) = self.backend.start_storage_session().await {
shutdown.cancel();
error!("failed to start the storage session - {err}");
return;
}
@@ -89,6 +89,8 @@ impl PartiallyDelegatedRouter {
async fn run(mut self, mut split_stream: SplitStream<WsConn>, shutdown_token: ShutdownToken) {
let mut chunked_stream = (&mut split_stream).ready_chunks(8);
let drop_guard = shutdown_token.clone().drop_guard();
let ret: Result<_, GatewayClientError> = loop {
tokio::select! {
biased;
@@ -101,6 +103,7 @@ impl PartiallyDelegatedRouter {
// received request to stop the task and return the stream
_ = &mut self.stream_return_requester => {
log::debug!("received request to return the split ws stream");
drop_guard.disarm();
break Ok(())
}
socket_msgs = chunked_stream.next() => {
@@ -10,7 +10,7 @@ use cosmrs::tx;
use cosmrs::tx::SignDoc;
use nym_config::defaults;
use thiserror::Error;
use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing};
use zeroize::{Zeroize, ZeroizeOnDrop};
type Secp256k1Keypair = (SigningKey, PublicKey);
@@ -128,20 +128,9 @@ impl DirectSecp256k1HdWallet {
Ok(accounts)
}
pub fn secret(&self) -> &bip39::Mnemonic {
&self.secret
}
#[deprecated(
note = "use either .secret() for obtaining &bip39::Mnemonic or .mnemonic_string() for Zeroizing wrapper around the String"
)]
pub fn mnemonic(&self) -> String {
self.secret.to_string()
}
pub fn mnemonic_string(&self) -> Zeroizing<String> {
Zeroizing::new(self.secret.to_string())
}
}
#[must_use]
@@ -18,6 +18,6 @@ pub fn create_account(args: Args, prefix: &str) {
let wallet = DirectSecp256k1HdWallet::from_mnemonic(prefix, mnemonic);
// Output address and mnemonics into separate lines for easier parsing
println!("{}", wallet.mnemonic_string().as_str());
println!("{}", wallet.mnemonic());
println!("{}", wallet.try_derive_accounts().unwrap()[0].address());
}
@@ -241,28 +241,23 @@ impl Epoch {
//
// Note: It's important that the variant ordering is not changed otherwise it would mess up the derived `PartialOrd`
#[cw_serde]
#[derive(Copy, Default)]
#[derive(Copy)]
pub enum EpochState {
#[default]
WaitingInitialisation,
PublicKeySubmission {
resharing: bool,
},
DealingExchange {
resharing: bool,
},
VerificationKeySubmission {
resharing: bool,
},
VerificationKeyValidation {
resharing: bool,
},
VerificationKeyFinalization {
resharing: bool,
},
PublicKeySubmission { resharing: bool },
DealingExchange { resharing: bool },
VerificationKeySubmission { resharing: bool },
VerificationKeyValidation { resharing: bool },
VerificationKeyFinalization { resharing: bool },
InProgress,
}
impl Default for EpochState {
fn default() -> Self {
Self::WaitingInitialisation
}
}
impl Display for EpochState {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
+1 -1
View File
@@ -8,7 +8,7 @@ async fn main() -> anyhow::Result<()> {
use sqlx::{Connection, SqliteConnection};
use std::env;
let out_dir = env::var("OUT_DIR").context("missing OUT_DIR env variable")?;
let out_dir = env::var("OUT_DIR")?;
let database_path = format!("{out_dir}/nym-credential-proxy-example.sqlite");
// remove the db file if it already existed from previous build
-7
View File
@@ -127,13 +127,6 @@ pub enum CredentialProxyError {
#[error("failed to create deposit")]
DepositFailure,
#[error("failed to load jwt signing key from {path}: {err}")]
JWTSigningKeyLoadFailure {
path: String,
#[source]
err: std::io::Error,
},
#[error("can't obtain sufficient number of credential shares due to unavailable quorum")]
UnavailableSigningQuorum,
@@ -7,9 +7,9 @@ use crate::ticketbook_manager::TicketbookManager;
use nym_compact_ecash::Base58;
use nym_credential_proxy_requests::api::v1::ticketbook::models::{
CurrentEpochResponse, DepositResponse, GlobalDataParams, MasterVerificationKeyResponse,
ObtainTicketBookSharesAsyncResponse, PartialVerificationKey, PartialVerificationKeysResponse,
TicketbookAsyncRequest, TicketbookObtainParams, TicketbookRequest,
TicketbookWalletSharesAsyncResponse, TicketbookWalletSharesResponse,
PartialVerificationKey, PartialVerificationKeysResponse, TicketbookAsyncRequest,
TicketbookObtainParams, TicketbookRequest, TicketbookWalletSharesAsyncResponse,
TicketbookWalletSharesResponse,
};
use time::OffsetDateTime;
use tracing::{Instrument, Level, error, info, span, warn};
@@ -65,7 +65,7 @@ impl TicketbookManager {
uuid: Uuid,
request: TicketbookAsyncRequest,
params: TicketbookObtainParams,
) -> Result<ObtainTicketBookSharesAsyncResponse, CredentialProxyError> {
) -> Result<TicketbookWalletSharesAsyncResponse, CredentialProxyError> {
let requested_on = OffsetDateTime::now_utc();
let span = span!(Level::INFO, "[async] obtain ticketboook", uuid = %uuid);
async move {
@@ -110,7 +110,7 @@ impl TicketbookManager {
}
// 4. in the meantime, return the id to the user
Ok(TicketbookWalletSharesAsyncResponse { id, uuid }.into())
Ok(TicketbookWalletSharesAsyncResponse { id, uuid })
}
.instrument(span)
.await
@@ -246,7 +246,7 @@ mod tests {
let _exp_date_sigs = generate_expiration_date_signatures(
sig_req.expiration_date.ecash_unix_timestamp(),
&[signing_keys.secret_key()],
&[signing_keys.verification_key()],
&vec![signing_keys.verification_key()],
&signing_keys.verification_key(),
&[1],
)?;
@@ -263,7 +263,7 @@ mod tests {
let wallet = issuance.aggregate_signature_shares(
&signing_keys.verification_key(),
&[partial_wallet],
&vec![partial_wallet],
sig_req,
)?;
-4
View File
@@ -36,11 +36,7 @@ nym-sphinx-types = { path = "../nymsphinx/types", version = "0.2.0", default-fea
nym-pemstore = { path = "../../common/pemstore", version = "0.3.0" }
[dev-dependencies]
anyhow = { workspace = true }
rand_chacha = { workspace = true }
serde_json = { workspace = true }
nym-test-utils = { path = "../test-utils" }
[features]
default = []
@@ -17,51 +17,6 @@ pub mod bs58_ed25519_pubkey {
}
}
pub mod vec_bs58_ed25519_pubkey {
use super::*;
use serde::{Deserialize, Deserializer, Serializer, ser::SerializeSeq};
pub fn serialize<S: Serializer>(
keys: &Vec<PublicKey>,
serializer: S,
) -> Result<S::Ok, S::Error> {
let mut seq = serializer.serialize_seq(Some(keys.len()))?;
for key in keys {
seq.serialize_element(&Bs58KeyWrapper(*key))?;
}
seq.end()
}
pub fn deserialize<'de, D: Deserializer<'de>>(
deserializer: D,
) -> Result<Vec<PublicKey>, D::Error> {
let wrapped = Vec::<Bs58KeyWrapper>::deserialize(deserializer)?;
Ok(wrapped.into_iter().map(|k| k.0).collect())
}
struct Bs58KeyWrapper(PublicKey);
impl serde::Serialize for Bs58KeyWrapper {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
bs58_ed25519_pubkey::serialize(&self.0, serializer)
}
}
impl<'de> Deserialize<'de> for Bs58KeyWrapper {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
Ok(Bs58KeyWrapper(bs58_ed25519_pubkey::deserialize(
deserializer,
)?))
}
}
}
pub mod bs58_ed25519_signature {
use crate::asymmetric::ed25519::Signature;
use serde::{Deserialize, Deserializer, Serializer};
@@ -78,53 +33,3 @@ pub mod bs58_ed25519_signature {
Signature::from_base58_string(s).map_err(serde::de::Error::custom)
}
}
#[cfg(test)]
mod tests {
use super::*;
use jwt_simple::reexports::{anyhow, serde_json};
use nym_test_utils::helpers::deterministic_rng;
use serde::{Deserialize, Serialize};
#[test]
fn vec_bs58_ed25519_pubkey_json() -> anyhow::Result<()> {
#[derive(Serialize, Deserialize, Debug, PartialEq)]
struct KeysWrapper(#[serde(with = "vec_bs58_ed25519_pubkey")] Vec<PublicKey>);
use crate::asymmetric::ed25519;
let mut rng = deterministic_rng();
let empty = KeysWrapper(vec![]);
let single_key = KeysWrapper(vec![PublicKey::from_base58_string(
"Be9wH7xuXBRJAuV1pC7MALZv6a61RvWQ3SypsNarqTt",
)?]);
let three_keys = KeysWrapper(vec![
ed25519::KeyPair::new(&mut rng).public_key,
ed25519::KeyPair::new(&mut rng).public_key,
ed25519::KeyPair::new(&mut rng).public_key,
]);
let se_empty = serde_json::to_string(&empty)?;
let se_single_key = serde_json::to_string(&single_key)?;
let se_three_keys = serde_json::to_string(&three_keys)?;
assert_eq!(se_empty, r#"[]"#);
assert_eq!(
se_single_key,
r#"["Be9wH7xuXBRJAuV1pC7MALZv6a61RvWQ3SypsNarqTt"]"#
);
assert_eq!(
se_three_keys,
r#"["HmgHDV79LpnEaSUp8QZQwSroxVvS4RewF7yM9e7qu8y3","311xRh859qCd5MVqoPRCoNx26eYhLknGwtjzkkTJFGhf","A5BMp8WJ6Uk91U4JpWRv2Bc6X35AaRaSEy8QEWeAkaBv"]"#
);
let empty_de = serde_json::from_str::<KeysWrapper>(&se_empty)?;
let single_key_de = serde_json::from_str::<KeysWrapper>(&se_single_key)?;
let three_keys_de = serde_json::from_str::<KeysWrapper>(&se_three_keys)?;
assert_eq!(empty, empty_de);
assert_eq!(single_key, single_key_de);
assert_eq!(three_keys, three_keys_de);
Ok(())
}
}
+1
View File
@@ -17,6 +17,7 @@ nym-contracts-common = { path = "../cosmwasm-smart-contracts/contracts-common",
bs58 = { workspace = true }
lazy_static = { workspace = true }
rand = { workspace = true }
rand_chacha = { workspace = true }
rand_core = { workspace = true }
+41 -16
View File
@@ -13,9 +13,9 @@ use rand::CryptoRng;
use rand_core::RngCore;
use std::collections::HashMap;
use std::ops::Neg;
use zeroize::{Zeroize, ZeroizeOnDrop};
use zeroize::Zeroize;
#[derive(Debug, Clone, PartialEq, Eq, Zeroize, ZeroizeOnDrop)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Ciphertexts {
pub rr: [G1Projective; NUM_CHUNKS],
pub ss: [G1Projective; NUM_CHUNKS],
@@ -164,7 +164,8 @@ impl Ciphertexts {
}
}
#[derive(Zeroize, ZeroizeOnDrop)]
#[derive(Zeroize)]
#[zeroize(drop)]
/// Randomness generated during ciphertext generation that is required for proofs of knowledge.
///
/// It must be handled with extreme care as its misuse might help malicious parties to recover
@@ -398,7 +399,7 @@ pub fn baby_step_giant_step(
#[cfg(test)]
mod tests {
use super::*;
use crate::bte::{keygen, setup, BSGS_TABLE};
use crate::bte::{keygen, setup, DEFAULT_BSGS_TABLE};
use rand_core::SeedableRng;
fn verify_hazmat_rand(ciphertext: &Ciphertexts, randomness: &HazmatRandomness) {
@@ -456,6 +457,8 @@ mod tests {
let (decryption_key1, public_key1) = keygen(&params, &mut rng);
let (decryption_key2, public_key2) = keygen(&params, &mut rng);
let lookup_table = &DEFAULT_BSGS_TABLE;
for _ in 0..10 {
let m1 = Share::random(&mut rng);
let m2 = Share::random(&mut rng);
@@ -464,12 +467,22 @@ mod tests {
let (ciphertext, hazmat) = encrypt_shares(shares, &params, &mut rng);
verify_hazmat_rand(&ciphertext, &hazmat);
let recovered1 =
decrypt_share(&params, &decryption_key1, 0, &ciphertext, Some(&BSGS_TABLE))
.unwrap();
let recovered2 =
decrypt_share(&params, &decryption_key2, 1, &ciphertext, Some(&BSGS_TABLE))
.unwrap();
let recovered1 = decrypt_share(
&params,
&decryption_key1,
0,
&ciphertext,
Some(lookup_table),
)
.unwrap();
let recovered2 = decrypt_share(
&params,
&decryption_key2,
1,
&ciphertext,
Some(lookup_table),
)
.unwrap();
assert_eq!(m1, recovered1);
assert_eq!(m2, recovered2);
}
@@ -485,6 +498,8 @@ mod tests {
let (decryption_key1, public_key1) = keygen(&params, &mut rng);
let (decryption_key2, public_key2) = keygen(&params, &mut rng);
let lookup_table = &DEFAULT_BSGS_TABLE;
for _ in 0..10 {
let m1 = Share::random(&mut rng);
let m2 = Share::random(&mut rng);
@@ -493,12 +508,22 @@ mod tests {
let (ciphertext, hazmat) = encrypt_shares(shares, &params, &mut rng);
verify_hazmat_rand(&ciphertext, &hazmat);
let recovered1 =
decrypt_share(&params, &decryption_key1, 0, &ciphertext, Some(&BSGS_TABLE))
.unwrap();
let recovered2 =
decrypt_share(&params, &decryption_key2, 1, &ciphertext, Some(&BSGS_TABLE))
.unwrap();
let recovered1 = decrypt_share(
&params,
&decryption_key1,
0,
&ciphertext,
Some(lookup_table),
)
.unwrap();
let recovered2 = decrypt_share(
&params,
&decryption_key2,
1,
&ciphertext,
Some(lookup_table),
)
.unwrap();
assert_eq!(m1, recovered1);
assert_eq!(m2, recovered2);
}
+5 -5
View File
@@ -11,7 +11,7 @@ use group::GroupEncoding;
use nym_pemstore::traits::{PemStorableKey, PemStorableKeyPair};
use rand::CryptoRng;
use rand_core::RngCore;
use zeroize::{Zeroize, ZeroizeOnDrop};
use zeroize::Zeroize;
// produces public key and a decryption key for the root of the tree
pub fn keygen(
@@ -48,7 +48,7 @@ pub fn keygen(
(dk, key_with_proof)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Zeroize)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PublicKey(pub(crate) G1Projective);
impl PublicKey {
@@ -57,7 +57,7 @@ impl PublicKey {
}
}
#[derive(Clone, Debug, PartialEq, Eq, Zeroize, ZeroizeOnDrop)]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PublicKeyWithProof {
pub(crate) key: PublicKey,
pub(crate) proof: ProofOfDiscreteLog,
@@ -136,7 +136,8 @@ impl PublicKeyWithProof {
}
}
#[derive(Debug, Zeroize, ZeroizeOnDrop)]
#[derive(Debug, Zeroize)]
#[zeroize(drop)]
#[cfg_attr(test, derive(PartialEq, Eq))]
pub struct DecryptionKey {
// g1^rho
@@ -241,7 +242,6 @@ impl DecryptionKey {
}
}
#[derive(Zeroize, ZeroizeOnDrop)]
pub struct KeyPair {
pub(crate) private_key: DecryptionKey,
pub(crate) public_key: PublicKeyWithProof,
+9 -9
View File
@@ -1,13 +1,11 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use std::sync::LazyLock;
use crate::bte::encryption::BabyStepGiantStepLookup;
use crate::utils::hash_g2;
use crate::{Chunk, Share};
use bls12_381::{G1Affine, G2Affine, G2Prepared, G2Projective, Gt};
use group::Curve;
use lazy_static::lazy_static;
pub mod encryption;
pub mod keys;
@@ -18,12 +16,14 @@ pub mod proof_sharing;
pub use encryption::{decrypt_share, encrypt_shares, Ciphertexts};
pub use keys::{keygen, DecryptionKey, PublicKey, PublicKeyWithProof};
pub(crate) static PAIRING_BASE: LazyLock<Gt> =
LazyLock::new(|| bls12_381::pairing(&G1Affine::generator(), &G2Affine::generator()));
pub(crate) static G2_GENERATOR_PREPARED: LazyLock<G2Prepared> =
LazyLock::new(|| G2Prepared::from(G2Affine::generator()));
pub static BSGS_TABLE: LazyLock<BabyStepGiantStepLookup> =
LazyLock::new(BabyStepGiantStepLookup::default);
lazy_static! {
pub(crate) static ref PAIRING_BASE: Gt =
bls12_381::pairing(&G1Affine::generator(), &G2Affine::generator());
pub(crate) static ref G2_GENERATOR_PREPARED: G2Prepared =
G2Prepared::from(G2Affine::generator());
pub(crate) static ref DEFAULT_BSGS_TABLE: encryption::BabyStepGiantStepLookup =
encryption::BabyStepGiantStepLookup::default();
}
// Domain tries to follow guidelines specified by:
// https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-11#section-3.1
+1 -2
View File
@@ -12,7 +12,6 @@ use ff::Field;
use group::{Group, GroupEncoding};
use rand::{CryptoRng, Rng};
use rand_core::{RngCore, SeedableRng};
use zeroize::{Zeroize, ZeroizeOnDrop};
const CHUNKING_ORACLE_DOMAIN: &[u8] =
b"NYM_COCONUT_NIDKG_V01_CS01_SHA-256_CHACHA20_CHUNKING_ORACLE";
@@ -68,7 +67,7 @@ impl<'a> Instance<'a> {
}
}
#[derive(Debug, Clone, PartialEq, Eq, Zeroize, ZeroizeOnDrop)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProofOfChunking {
y0: G1Projective,
bb: Vec<G1Projective>,
+5 -5
View File
@@ -7,14 +7,14 @@ use ff::Field;
use group::GroupEncoding;
use rand::CryptoRng;
use rand_core::RngCore;
use zeroize::{Zeroize, ZeroizeOnDrop};
use zeroize::Zeroize;
// Domain tries to follow guidelines specified by:
// https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-11#section-3.1
const DISCRETE_LOG_DOMAIN: &[u8] =
b"NYM_COCONUT_NIDKG_V01_CS01_WITH_BLS12381_XMD:SHA-256_SSWU_RO_PROOF_DISCRETE_LOG";
#[derive(Clone, Debug, PartialEq, Eq, Zeroize, ZeroizeOnDrop)]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ProofOfDiscreteLog {
pub(crate) rand_commitment: G1Projective,
pub(crate) response: Scalar,
@@ -52,9 +52,9 @@ impl ProofOfDiscreteLog {
let public_bytes = public.to_bytes();
let rand_commit_bytes = rand_commit.to_bytes();
let mut bytes = [0u8; 96];
bytes[0..48].copy_from_slice(public_bytes.as_ref());
bytes[48..96].copy_from_slice(rand_commit_bytes.as_ref());
let mut bytes = Vec::with_capacity(96);
bytes.extend_from_slice(public_bytes.as_ref());
bytes.extend_from_slice(rand_commit_bytes.as_ref());
hash_to_scalar(bytes, DISCRETE_LOG_DOMAIN)
}
+1 -2
View File
@@ -12,7 +12,6 @@ use group::GroupEncoding;
use rand::CryptoRng;
use rand_core::RngCore;
use std::collections::BTreeMap;
use zeroize::{Zeroize, ZeroizeOnDrop};
// Domain tries to follow guidelines specified by:
// https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-11#section-3.1
@@ -78,7 +77,7 @@ impl<'a> Instance<'a> {
}
}
#[derive(Debug, Clone, PartialEq, Eq, Zeroize, ZeroizeOnDrop)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProofOfSecretSharing {
ff: G1Projective,
aa: G2Projective,
+2 -2
View File
@@ -18,7 +18,7 @@ use rand_core::RngCore;
use std::collections::BTreeMap;
use zeroize::Zeroize;
#[derive(Clone, Debug, Zeroize)]
#[derive(Clone, Debug)]
#[cfg_attr(test, derive(PartialEq, Eq))]
pub struct RecoveredVerificationKeys {
pub recovered_master: G2Projective,
@@ -83,7 +83,7 @@ impl RecoveredVerificationKeys {
}
}
#[derive(Debug, Clone, PartialEq, Eq, Zeroize)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Dealing {
pub public_coefficients: PublicCoefficients,
pub ciphertexts: Ciphertexts,
+4 -3
View File
@@ -9,9 +9,9 @@ use group::GroupEncoding;
use rand::CryptoRng;
use rand_core::RngCore;
use std::ops::{Add, Index, IndexMut};
use zeroize::{Zeroize, ZeroizeOnDrop};
use zeroize::Zeroize;
#[derive(Clone, Debug, PartialEq, Eq, Zeroize, ZeroizeOnDrop)]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PublicCoefficients {
pub(crate) coefficients: Vec<G2Projective>,
}
@@ -111,7 +111,8 @@ impl IndexMut<usize> for PublicCoefficients {
}
}
#[derive(Clone, Debug, PartialEq, Eq, Zeroize, ZeroizeOnDrop)]
#[derive(Clone, Debug, PartialEq, Eq, Zeroize)]
#[zeroize(drop)]
pub struct Polynomial {
coefficients: Vec<Scalar>,
}
+5 -3
View File
@@ -6,13 +6,14 @@ use crate::error::DkgError;
use crate::interpolation::perform_lagrangian_interpolation_at_origin;
use crate::NodeIndex;
use bls12_381::Scalar;
use zeroize::{Zeroize, ZeroizeOnDrop};
use zeroize::Zeroize;
// if this type is changed, one must ensure all values can fit in it
pub type Chunk = u16;
#[derive(PartialEq, Eq, Debug, Zeroize, ZeroizeOnDrop)]
#[derive(PartialEq, Eq, Debug, Zeroize)]
#[cfg_attr(test, derive(Clone))]
#[zeroize(drop)]
pub struct Share(pub(crate) Scalar);
pub fn combine_shares(shares: Vec<Share>, node_indices: &[NodeIndex]) -> Result<Scalar, DkgError> {
@@ -65,8 +66,9 @@ impl From<Scalar> for Share {
}
}
#[derive(Default, Zeroize, ZeroizeOnDrop)]
#[derive(Default, Zeroize)]
#[cfg_attr(test, derive(Clone))]
#[zeroize(drop)]
pub(crate) struct ChunkedShare {
pub(crate) chunks: [Chunk; NUM_CHUNKS],
}
+3 -3
View File
@@ -82,7 +82,7 @@ mod tests {
let exp_date_sigs = generate_expiration_date_signatures(
sig_req.expiration_date.ecash_unix_timestamp(),
&[keypair.secret_key()],
&[keypair.verification_key()],
&vec![keypair.verification_key()],
&keypair.verification_key(),
&[keypair.index.unwrap()],
)
@@ -106,14 +106,14 @@ mod tests {
.unwrap();
let wallet = issuance
.aggregate_signature_shares(&keypair.verification_key(), &[partial_wallet], sig_req)
.aggregate_signature_shares(&keypair.verification_key(), &vec![partial_wallet], sig_req)
.unwrap();
let mut issued = issuance.into_issued_ticketbook(wallet, 1);
let coin_indices_signatures = generate_coin_indices_signatures(
nym_credentials_interface::ecash_parameters(),
&[keypair.secret_key()],
&[keypair.verification_key()],
&vec![keypair.verification_key()],
&keypair.verification_key(),
&[keypair.index.unwrap()],
)
+20 -38
View File
@@ -296,9 +296,6 @@ impl std::error::Error for ReqwestErrorWrapper {}
#[derive(Debug, Error)]
#[allow(missing_docs)]
pub enum HttpClientError {
#[error("did not provide any valid client URLs")]
NoUrlsProvided,
#[error("failed to construct inner reqwest client: {source}")]
ReqwestBuildError {
#[source]
@@ -585,29 +582,24 @@ impl ClientBuilder {
Self::new(alt)
} else {
let url = url.to_url()?;
Self::new_with_urls(vec![url])
Ok(Self::new_with_urls(vec![url]))
}
}
/// Create a client builder from network details with sensible defaults
#[cfg(feature = "network-defaults")]
// deprecating function since it's not clear from its signature whether the client
// would be constructed using `nym_api_urls` or `nym_vpn_api_urls`
#[deprecated(note = "use explicit Self::new_with_fronted_urls instead")]
pub fn from_network(
network: &nym_network_defaults::NymNetworkDetails,
) -> Result<Self, HttpClientError> {
let urls = network.nym_api_urls.as_ref().cloned().unwrap_or_default();
Self::new_with_fronted_urls(urls.clone())
}
/// Create a client builder using the provided set of domain-fronted URLs
#[cfg(feature = "network-defaults")]
pub fn new_with_fronted_urls(
urls: Vec<nym_network_defaults::ApiUrl>,
) -> Result<Self, HttpClientError> {
let urls = urls
.into_iter()
let urls = network
.nym_api_urls
.as_ref()
.ok_or_else(|| {
HttpClientError::GenericRequestFailure(
"No API URLs configured in network details".to_string(),
)
})?
.iter()
.map(|api_url| {
// Convert ApiUrl to our Url type with fronting support
let mut url = Url::parse(&api_url.url)?;
@@ -619,19 +611,15 @@ impl ClientBuilder {
.iter()
.map(|host| format!("https://{}", host))
.collect();
url = Url::new(api_url.url.clone(), Some(fronts)).map_err(|source| {
HttpClientError::MalformedUrl {
raw: api_url.url.clone(),
source,
}
})?;
url = Url::new(api_url.url.clone(), Some(fronts))
.map_err(|e| HttpClientError::GenericRequestFailure(e.to_string()))?;
}
Ok(url)
})
.collect::<Result<Vec<_>, HttpClientError>>()?;
let mut builder = Self::new_with_urls(urls)?;
let mut builder = Self::new_with_urls(urls);
// Enable domain fronting by default (on retry)
#[cfg(feature = "tunneling")]
@@ -643,11 +631,7 @@ impl ClientBuilder {
}
/// Constructs a new http `ClientBuilder` from a valid url.
pub fn new_with_urls(urls: Vec<Url>) -> Result<Self, HttpClientError> {
if urls.is_empty() {
return Err(HttpClientError::NoUrlsProvided);
}
pub fn new_with_urls(urls: Vec<Url>) -> Self {
let urls = Self::check_urls(urls);
#[cfg(target_arch = "wasm32")]
@@ -656,7 +640,7 @@ impl ClientBuilder {
#[cfg(not(target_arch = "wasm32"))]
let reqwest_client_builder = default_builder();
Ok(ClientBuilder {
ClientBuilder {
urls,
timeout: None,
custom_user_agent: false,
@@ -667,7 +651,7 @@ impl ClientBuilder {
retry_limit: 0,
serialization: SerializationFormat::Json,
})
}
}
/// Add an additional URL to the set usable by this constructed `Client`
@@ -902,8 +886,6 @@ impl Client {
if self.base_urls.len() > 1 {
let orig = self.current_idx.load(Ordering::Relaxed);
#[allow(unused_mut)]
let mut next = (orig + 1) % self.base_urls.len();
// if fronting is enabled we want to update to a host that has fronts configured
@@ -966,13 +948,13 @@ impl Client {
return (url.as_str(), url.front_str());
} else {
tracing::debug!(
"Domain fronting is enabled, but no host_url is defined for current URL"
warn!(
"Domain fronting is enabled, but no host_url is defined! Domain fronting WILL NOT WORK"
)
}
} else {
tracing::debug!(
"Domain fronting is enabled, but current URL has no front_hosts configured"
warn!(
"Domain fronting is enabled, but no front_url is defined! Domain fronting WILL NOT WORK"
)
}
}
+1 -9
View File
@@ -21,10 +21,6 @@ inventory::collect!(ConfigRecord);
/// Returns the default builder with all registered configurations applied.
pub fn default_builder() -> ReqwestClientBuilder {
let mut b = ReqwestClientBuilder::new();
#[cfg(feature = "debug-inventory")]
let mut test_client = ReqwestClientBuilder::new();
let mut records: Vec<&'static ConfigRecord> =
inventory::iter::<ConfigRecord>.into_iter().collect();
records.sort_by_key(|r| r.priority); // lower runs first
@@ -39,10 +35,6 @@ pub fn default_builder() -> ReqwestClientBuilder {
for r in records {
b = (r.apply)(b);
#[cfg(feature = "debug-inventory")]
{
test_client = (r.apply)(test_client);
}
}
#[cfg(feature = "debug-inventory")]
@@ -55,7 +47,7 @@ pub fn default_builder() -> ReqwestClientBuilder {
eprintln!("[HTTP-INVENTORY] Building test client to verify configuration...");
// Try to build a client to see if it works
match test_client.build() {
match b.try_clone().unwrap().build() {
Ok(client) => {
eprintln!("[HTTP-INVENTORY] ✓ Client built successfully");
eprintln!("[HTTP-INVENTORY] Client debug info: {:#?}", client);
+49 -110
View File
@@ -2,77 +2,77 @@ use super::*;
#[test]
fn sanitizing_urls() {
let base_url: Url = "http://api.test".parse().unwrap();
let base_url: Url = "http://foomp.com".parse().unwrap();
// works with a full string
assert_eq!(
"http://api.test/foo/bar",
"http://foomp.com/foo/bar",
sanitize_url(&base_url, "/foo//bar/", NO_PARAMS).as_str()
);
// (and leading slash doesn't matter)
assert_eq!(
"http://api.test/foo/bar",
"http://foomp.com/foo/bar",
sanitize_url(&base_url, "foo//bar/", NO_PARAMS).as_str()
);
// works with 1 segment
assert_eq!(
"http://api.test/foo",
"http://foomp.com/foo",
sanitize_url(&base_url, &["foo"], NO_PARAMS).as_str()
);
// works with 2 segments
assert_eq!(
"http://api.test/foo/bar",
"http://foomp.com/foo/bar",
sanitize_url(&base_url, &["foo", "bar"], NO_PARAMS).as_str()
);
// works with leading slash
assert_eq!(
"http://api.test/foo",
"http://foomp.com/foo",
sanitize_url(&base_url, &["/foo"], NO_PARAMS).as_str()
);
assert_eq!(
"http://api.test/foo/bar",
"http://foomp.com/foo/bar",
sanitize_url(&base_url, &["/foo", "bar"], NO_PARAMS).as_str()
);
assert_eq!(
"http://api.test/foo/bar",
"http://foomp.com/foo/bar",
sanitize_url(&base_url, &["foo", "/bar"], NO_PARAMS).as_str()
);
// works with trailing slash
assert_eq!(
"http://api.test/foo",
"http://foomp.com/foo",
sanitize_url(&base_url, &["foo/"], NO_PARAMS).as_str()
);
assert_eq!(
"http://api.test/foo/bar",
"http://foomp.com/foo/bar",
sanitize_url(&base_url, &["foo/", "bar"], NO_PARAMS).as_str()
);
assert_eq!(
"http://api.test/foo/bar",
"http://foomp.com/foo/bar",
sanitize_url(&base_url, &["foo", "bar/"], NO_PARAMS).as_str()
);
// works with both leading and trailing slash
assert_eq!(
"http://api.test/foo",
"http://foomp.com/foo",
sanitize_url(&base_url, &["/foo/"], NO_PARAMS).as_str()
);
assert_eq!(
"http://api.test/foo/bar",
"http://foomp.com/foo/bar",
sanitize_url(&base_url, &["/foo/", "/bar/"], NO_PARAMS).as_str()
);
// adds params
assert_eq!(
"http://api.test/foo/bar?foomp=baz",
"http://foomp.com/foo/bar?foomp=baz",
sanitize_url(&base_url, &["foo", "bar"], &[("foomp", "baz")]).as_str()
);
assert_eq!(
"http://api.test/foo/bar?arg1=val1&arg2=val2",
"http://foomp.com/foo/bar?arg1=val1&arg2=val2",
sanitize_url(
&base_url,
&["/foo/", "/bar/"],
@@ -91,87 +91,83 @@ fn sanitizing_urls() {
#[tokio::test]
async fn api_client_retry() -> Result<(), Box<dyn std::error::Error>> {
let client = ClientBuilder::new_with_urls(vec![
"http://broken.nym.test".parse()?, // This will fail
"https://httpbin.org/status/200".parse()?, // This will succeed
])?
"http://broken.nym.badurl".parse()?,
"http://example.com/".parse()?,
])
.with_retries(3)
.build()?;
let req = client.create_get_request(&[], NO_PARAMS).unwrap();
let req = client.create_get_request(&["/"], NO_PARAMS).unwrap();
let resp = client.send(req).await?;
// The main test is that we successfully retried and switched to the working URL
// We accept any response from the working endpoint since external services can be unreliable
assert_eq!(
client.current_url().as_str(),
"https://httpbin.org/status/200"
);
assert_eq!(resp.status(), 200);
println!("Response status: {}", resp.status());
// check that the url was updated
assert_eq!(client.current_url().as_str(), "http://example.com/");
Ok(())
}
#[test]
fn host_updating() {
let url = Url::new("http://nym-api1.test", None).unwrap();
let url = Url::new("http://example.com", None).unwrap();
let mut client = ClientBuilder::new(url).unwrap().build().unwrap();
// check that the url is set correctly
let current_url = client.current_url();
assert_eq!(current_url.as_str(), "http://nym-api1.test/");
assert_eq!(current_url.as_str(), "http://example.com/");
assert_eq!(current_url.front_str(), None);
// update the url
client.update_host();
// check that the url is still the same since there is one URL
assert_eq!(client.current_url().as_str(), "http://nym-api1.test/");
assert_eq!(client.current_url().as_str(), "http://example.com/");
// =======================================
// we rotate through urls when available
let new_urls = vec![
Url::new("http://nym-api1.test", None).unwrap(),
Url::new("http://nym-api2.test", None).unwrap(),
Url::new("http://example.com", None).unwrap(),
Url::new("http://example.org", None).unwrap(),
];
client.change_base_urls(new_urls);
assert_eq!(client.current_url().as_str(), "http://nym-api1.test/");
assert_eq!(client.current_url().as_str(), "http://example.com/");
client.update_host();
// check that the url got updated now that there are multiple URLs
assert_eq!(client.current_url().as_str(), "http://nym-api2.test/");
assert_eq!(client.current_url().as_str(), "http://example.org/");
assert_eq!(client.current_url().front_str(), None);
client.update_host();
assert_eq!(client.current_url().as_str(), "http://nym-api1.test/");
assert_eq!(client.current_url().as_str(), "http://example.com/");
// =======================================
// we rotate through urls when available if fronting is disabled
let new_urls = vec![
Url::new(
"http://nym-api1.test",
Some(vec!["http://cdn1.test", "http://cdn2.test"]),
"http://example.com",
Some(vec!["http://front1.com", "http://front2.com"]),
)
.unwrap(),
Url::new("http://nym-api2.test", None).unwrap(),
Url::new("http://example.org", None).unwrap(),
];
client.change_base_urls(new_urls);
assert_eq!(client.current_url().as_str(), "http://nym-api1.test/");
assert_eq!(client.current_url().as_str(), "http://example.com/");
client.update_host();
// check that the url got updated now that there are multiple URLs
assert_eq!(client.current_url().as_str(), "http://nym-api2.test/");
assert_eq!(client.current_url().as_str(), "http://example.org/");
}
#[test]
#[cfg(feature = "tunneling")]
fn fronted_host_updating() {
let url = Url::new("http://nym-api.test", Some(vec!["http://cdn1.test"])).unwrap();
let url = Url::new("http://example.com", Some(vec!["http://front1.com"])).unwrap();
let mut client = ClientBuilder::new(url)
.unwrap()
.with_fronting(crate::fronted::FrontPolicy::Always)
@@ -180,103 +176,46 @@ fn fronted_host_updating() {
// check that the url is set correctly
let current_url = client.current_url();
assert_eq!(current_url.as_str(), "http://nym-api.test/");
assert_eq!(current_url.front_str(), Some("cdn1.test"));
assert_eq!(current_url.as_str(), "http://example.com/");
assert_eq!(current_url.front_str(), Some("front1.com"));
// update the url
client.update_host();
// check that the url is still the same since there is one URL and one front
let current_url = client.current_url();
assert_eq!(current_url.as_str(), "http://nym-api.test/");
assert_eq!(current_url.front_str(), Some("cdn1.test"));
assert_eq!(current_url.as_str(), "http://example.com/");
assert_eq!(current_url.front_str(), Some("front1.com"));
// =======================================
// we rotate through front urls when available if fronting is enabled
let new_urls = vec![
Url::new(
"http://nym-api.test",
Some(vec!["http://cdn1.test", "http://cdn2.test"]),
"http://example.com",
Some(vec!["http://front1.com", "http://front2.com"]),
)
.unwrap(),
Url::new("http://nym-api2.test", None).unwrap(),
Url::new("http://example.org", None).unwrap(),
];
client.change_base_urls(new_urls);
let current_url = client.current_url();
assert_eq!(current_url.as_str(), "http://nym-api.test/");
assert_eq!(current_url.front_str(), Some("cdn1.test"));
assert_eq!(current_url.as_str(), "http://example.com/");
assert_eq!(current_url.front_str(), Some("front1.com"));
// update the url - this should keep the same host but change the front
client.update_host();
let current_url = client.current_url();
// check that the url is still the same since there is one URL
assert_eq!(current_url.as_str(), "http://nym-api.test/");
assert_eq!(current_url.front_str(), Some("cdn2.test"));
assert_eq!(current_url.as_str(), "http://example.com/");
assert_eq!(current_url.front_str(), Some("front2.com"));
// update the url - this should wrap around to the first front as the second url is not fronted
client.update_host();
let current_url = client.current_url();
assert_eq!(current_url.as_str(), "http://nym-api.test/");
assert_eq!(current_url.front_str(), Some("cdn1.test"));
}
#[test]
#[cfg(feature = "network-defaults")]
fn from_network_configures_multiple_urls_and_retries() {
use nym_network_defaults::{ApiUrl, NymNetworkDetails};
// Create network details with multiple URLs and fronting
let mut network_details = NymNetworkDetails::new_empty();
network_details.nym_api_urls = Some(vec![
ApiUrl {
url: "https://validator.nymtech.net/api/".to_string(),
front_hosts: None,
},
ApiUrl {
url: "https://nym-frontdoor.vercel.app/api/".to_string(),
front_hosts: Some(vec!["vercel.app".to_string(), "vercel.com".to_string()]),
},
ApiUrl {
url: "https://nym-frontdoor.global.ssl.fastly.net/api/".to_string(),
front_hosts: Some(vec!["yelp.global.ssl.fastly.net".to_string()]),
},
]);
// Build client from network details
let client = ClientBuilder::new_with_fronted_urls(
network_details.nym_api_urls.clone().unwrap_or_default(),
)
.expect("Failed to create client from network")
.build()
.expect("Failed to build client");
// Verify all URLs were configured
assert_eq!(
client.base_urls().len(),
3,
"Expected 3 URLs to be configured from network details"
);
// Verify the URLs have fronting configured where appropriate
assert_eq!(
client.base_urls()[0].as_str(),
"https://validator.nymtech.net/api/"
);
assert!(client.base_urls()[0].front_str().is_none());
assert_eq!(
client.base_urls()[1].as_str(),
"https://nym-frontdoor.vercel.app/api/"
);
assert!(client.base_urls()[1].front_str().is_some());
assert_eq!(
client.base_urls()[2].as_str(),
"https://nym-frontdoor.global.ssl.fastly.net/api/"
);
assert!(client.base_urls()[2].front_str().is_some());
assert_eq!(current_url.as_str(), "http://example.com/");
assert_eq!(current_url.front_str(), Some("front1.com"));
}
-10
View File
@@ -183,11 +183,6 @@ impl Url {
})
}
/// Returns the underlying URL
pub fn inner_url(&self) -> &url::Url {
&self.url
}
/// Returns true if the URL has a front domain set
pub fn has_front(&self) -> bool {
if let Some(fronts) = &self.fronts {
@@ -206,11 +201,6 @@ impl Url {
.and_then(|url| url.host_str())
}
/// Returns the fronts
pub fn fronts(&self) -> Option<&[url::Url]> {
self.fronts.as_deref()
}
/// Return the string representation of the host (domain or IP address) for this URL, if any.
pub fn host_str(&self) -> Option<&str> {
self.url.host_str()
+1 -13
View File
@@ -124,8 +124,6 @@ impl NymNetworkDetails {
}
}
let nym_api = var(var_names::NYM_API).expect("nym api not set");
NymNetworkDetails::new_empty()
.with_network_name(var(var_names::NETWORK_NAME).expect("network name not set"))
.with_bech32_account_prefix(
@@ -151,7 +149,7 @@ impl NymNetworkDetails {
})
.with_additional_validator_endpoint(ValidatorDetails::new(
var(var_names::NYXD).expect("nyxd validator not set"),
Some(nym_api.clone()),
Some(var(var_names::NYM_API).expect("nym api not set")),
get_optional_env(var_names::NYXD_WEBSOCKET),
))
.with_mixnet_contract(get_optional_env(var_names::MIXNET_CONTRACT_ADDRESS))
@@ -161,10 +159,6 @@ impl NymNetworkDetails {
.with_multisig_contract(get_optional_env(var_names::MULTISIG_CONTRACT_ADDRESS))
.with_coconut_dkg_contract(get_optional_env(var_names::COCONUT_DKG_CONTRACT_ADDRESS))
.with_nym_vpn_api_url(get_optional_env(var_names::NYM_VPN_API))
.with_nym_api_urls(Some(vec![ApiUrl {
url: nym_api,
front_hosts: None,
}]))
}
pub fn new_mainnet() -> Self {
@@ -354,12 +348,6 @@ impl NymNetworkDetails {
self
}
#[must_use]
pub fn with_nym_api_urls(mut self, urls: Option<Vec<ApiUrl>>) -> Self {
self.nym_api_urls = urls;
self
}
pub fn nym_vpn_api_url(&self) -> Option<Url> {
self.nym_vpn_api_url.as_ref().map(|url| {
url.parse()
-1
View File
@@ -119,7 +119,6 @@ where
let ClientInput {
connection_command_sender,
input_sender,
..
} = client_input;
let ClientOutput {
+2 -2
View File
@@ -24,6 +24,6 @@ pub use crate::runtime_registry::RegistryAccessError;
/// Get or create a ShutdownTracker for SDK use.
/// This provides automatic task management without requiring manual setup.
pub fn create_sdk_shutdown_tracker() -> Result<ShutdownTracker, RegistryAccessError> {
Ok(runtime_registry::RuntimeRegistry::create_sdk()?.shutdown_tracker_owned())
pub fn get_sdk_shutdown_tracker() -> Result<ShutdownTracker, RegistryAccessError> {
Ok(runtime_registry::RuntimeRegistry::get_or_create_sdk()?.shutdown_tracker_owned())
}
+16 -34
View File
@@ -19,45 +19,30 @@ pub(crate) struct RuntimeRegistry {
pub enum RegistryAccessError {
#[error("the runtime registry is poisoned")]
Poisoned,
#[error("The SDK ShutdownManager already exists")]
ExistingShutdownManager,
#[error("No existing SDK ShutdownManager")]
MissingShutdownManager,
}
impl RuntimeRegistry {
/// Create a ShutdownManager for SDK use.
/// Get or create a ShutdownManager for SDK use.
/// This manager doesn't listen to OS signals, making it suitable for library use.
/// This function overwrite any existing manager!
pub(crate) fn create_sdk() -> Result<Arc<ShutdownManager>, RegistryAccessError> {
let mut guard = REGISTRY
.sdk_manager
.write()
.map_err(|_| RegistryAccessError::Poisoned)?;
Ok(guard
.insert(Arc::new(
ShutdownManager::new_without_signals().with_cancel_on_panic(),
))
.clone())
}
/// Get the ShutdownManager for SDK use.
/// This manager doesn't listen to OS signals, making it suitable for library use.
/// Not yet used, but maybe in the future
#[allow(dead_code)]
pub(crate) fn get_sdk() -> Result<Arc<ShutdownManager>, RegistryAccessError> {
pub(crate) fn get_or_create_sdk() -> Result<Arc<ShutdownManager>, RegistryAccessError> {
let guard = REGISTRY
.sdk_manager
.read()
.map_err(|_| RegistryAccessError::Poisoned)?;
if let Some(manager) = guard.as_ref() {
Ok(manager.clone())
} else {
Err(RegistryAccessError::MissingShutdownManager)
return Ok(manager.clone());
}
drop(guard);
let mut guard = REGISTRY
.sdk_manager
.write()
.map_err(|_| RegistryAccessError::Poisoned)?;
Ok(guard
.get_or_insert_with(|| {
Arc::new(ShutdownManager::new_without_signals().with_cancel_on_panic())
})
.clone())
}
/// Check if an SDK manager has been created.
@@ -100,13 +85,10 @@ mod tests {
assert!(!RuntimeRegistry::has_sdk_manager().unwrap());
// Error if nothing was created
assert!(RuntimeRegistry::get_sdk().is_err());
let manager1 = RuntimeRegistry::create_sdk().unwrap();
let manager1 = RuntimeRegistry::get_or_create_sdk().unwrap();
assert!(RuntimeRegistry::has_sdk_manager().unwrap());
let manager2 = RuntimeRegistry::get_sdk().unwrap();
let manager2 = RuntimeRegistry::get_or_create_sdk().unwrap();
// Should return the same instance
assert!(Arc::ptr_eq(&manager1, &manager2));
+1 -8
View File
@@ -15,10 +15,9 @@ jwt-simple = { workspace = true }
reqwest = { workspace = true, features = ["rustls-tls"] }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
time = { workspace = true, features = ["serde", "formatting", "parsing"] }
time = { workspace = true, features = ["serde"] }
thiserror = { workspace = true }
tracing = { workspace = true }
utoipa = { workspace = true, optional = true }
nym-http-api-client = { path = "../http-api-client", default-features = false }
nym-crypto = { path = "../crypto", features = ["asymmetric", "serde", "naive_jwt"] }
@@ -26,12 +25,6 @@ nym-crypto = { path = "../crypto", features = ["asymmetric", "serde", "naive_jwt
[dev-dependencies]
anyhow = { workspace = true }
time = { workspace = true, features = ["macros"] }
nym-test-utils = { path = "../test-utils" }
nym-crypto = { path = "../crypto", features = ["rand"] }
[features]
openapi = ["utoipa"]
[lints]
workspace = true
+16 -46
View File
@@ -1,43 +1,31 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::UpgradeModeCheckError;
use nym_crypto::asymmetric::ed25519;
use nym_http_api_client::generate_user_agent;
use serde::{Deserialize, Serialize};
use std::time::Duration;
use time::OffsetDateTime;
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone, Copy)]
pub struct UpgradeModeAttestation {
#[serde(flatten)]
pub content: UpgradeModeAttestationContent,
#[serde(with = "ed25519::bs58_ed25519_signature")]
#[cfg_attr(feature = "openapi", schema(value_type = String))]
pub signature: ed25519::Signature,
}
impl UpgradeModeAttestation {
pub fn authorised_to_issue_jwt(&self, key: &ed25519::PublicKey) -> bool {
self.content.authorised_jwt_issuers.contains(key)
}
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone, Copy)]
#[serde(tag = "type")]
#[serde(rename = "upgrade_mode")]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct UpgradeModeAttestationContent {
#[serde(with = "time::serde::rfc3339")]
#[cfg_attr(feature = "openapi", schema(value_type = String))]
#[serde(with = "time::serde::timestamp")]
pub starting_time: OffsetDateTime,
#[serde(with = "ed25519::bs58_ed25519_pubkey")]
#[cfg_attr(feature = "openapi", schema(value_type = String))]
pub attester_public_key: ed25519::PublicKey,
#[serde(with = "ed25519::vec_bs58_ed25519_pubkey")]
#[cfg_attr(feature = "openapi", schema(value_type = Vec<String>))]
pub authorised_jwt_issuers: Vec<ed25519::PublicKey>,
}
impl UpgradeModeAttestation {
@@ -57,26 +45,17 @@ impl UpgradeModeAttestationContent {
}
}
pub fn generate_new_attestation(
key: &ed25519::PrivateKey,
authorised_jwt_issuers: Vec<ed25519::PublicKey>,
) -> UpgradeModeAttestation {
generate_new_attestation_with_starting_time(
key,
authorised_jwt_issuers,
OffsetDateTime::now_utc(),
)
pub fn generate_new_attestation(key: &ed25519::PrivateKey) -> UpgradeModeAttestation {
generate_new_attestation_with_starting_time(key, OffsetDateTime::now_utc())
}
pub fn generate_new_attestation_with_starting_time(
key: &ed25519::PrivateKey,
authorised_jwt_issuers: Vec<ed25519::PublicKey>,
starting_time: OffsetDateTime,
) -> UpgradeModeAttestation {
let content = UpgradeModeAttestationContent {
starting_time,
attester_public_key: key.into(),
authorised_jwt_issuers,
};
UpgradeModeAttestation {
signature: key.sign(content.as_json()),
@@ -84,19 +63,17 @@ pub fn generate_new_attestation_with_starting_time(
}
}
#[cfg(not(target_arch = "wasm32"))]
pub async fn attempt_retrieve_attestation(
pub async fn attempt_retrieve(
url: &str,
user_agent: Option<nym_http_api_client::UserAgent>,
) -> Result<Option<UpgradeModeAttestation>, crate::UpgradeModeCheckError> {
let retrieval_failure = |source| crate::UpgradeModeCheckError::AttestationRetrievalFailure {
) -> Result<Option<UpgradeModeAttestation>, UpgradeModeCheckError> {
let retrieval_failure = |source| UpgradeModeCheckError::AttestationRetrievalFailure {
url: url.to_string(),
source,
};
let attestation = reqwest::ClientBuilder::new()
.user_agent(user_agent.unwrap_or_else(|| nym_http_api_client::generate_user_agent!()))
.timeout(std::time::Duration::from_secs(5))
.user_agent(generate_user_agent!())
.timeout(Duration::from_secs(5))
.build()
.map_err(retrieval_failure)?
.get(url)
@@ -124,20 +101,13 @@ mod tests {
163, 122, 170, 79, 198, 87, 85, 36, 29, 243, 92, 64, 161,
])?;
let authorised_jwt_issuers = vec![ed25519::PublicKey::from_base58_string(
"Be9wH7xuXBRJAuV1pC7MALZv6a61RvWQ3SypsNarqTt",
)?];
let attestation = generate_new_attestation_with_starting_time(
&key,
authorised_jwt_issuers,
starting_time,
);
let attestation = generate_new_attestation_with_starting_time(&key, starting_time);
let attestation_json = serde_json::to_string(&attestation)?;
let attestation_content_json = attestation.content.as_json();
let expected_attestation = r#"{"type":"upgrade_mode","starting_time":"2021-08-23T12:00:00Z","attester_public_key":"3pkFcBXCEmbmXBT2G8CkFMuKisJcH54mbBGvncHaDibt","authorised_jwt_issuers":["Be9wH7xuXBRJAuV1pC7MALZv6a61RvWQ3SypsNarqTt"],"signature":"5Kt9dfwvnkdnDcENbwNyitrxghyckWUYycBv8jUUn7hJUMohWEMc6otb3scXQfCrAGSE7FD5m7kr6auBmkAmfczY"}"#;
let expected_content = r#"{"type":"upgrade_mode","starting_time":"2021-08-23T12:00:00Z","attester_public_key":"3pkFcBXCEmbmXBT2G8CkFMuKisJcH54mbBGvncHaDibt","authorised_jwt_issuers":["Be9wH7xuXBRJAuV1pC7MALZv6a61RvWQ3SypsNarqTt"]}"#;
let expected_attestation = r#"{"type":"upgrade_mode","starting_time":1629720000,"attester_public_key":"3pkFcBXCEmbmXBT2G8CkFMuKisJcH54mbBGvncHaDibt","signature":"5rWUr2ypaDTtrMKegMP3tQkkZGFAuhNTnEVCVe5Azv6QqvLzoGdQiMkFmeyhDd1XSfoXpL9fFM58rsdA1kf4GYMM"}"#;
let expected_content = r#"{"type":"upgrade_mode","starting_time":1629720000,"attester_public_key":"3pkFcBXCEmbmXBT2G8CkFMuKisJcH54mbBGvncHaDibt"}"#;
assert_eq!(attestation_content_json, expected_content);
assert_eq!(attestation_json, expected_attestation);
-3
View File
@@ -12,9 +12,6 @@ pub enum UpgradeModeCheckError {
#[error("the jwt metadata didn't contain explicit public key")]
MissingTokenPublicKey,
#[error("the jwt signer does not appear in the authorised attestation set")]
UnauthorisedIssuer,
#[error("the attached public key was not valid ed25519 public key")]
MalformedEd25519PublicKey { source: Ed25519RecoveryError },
+5 -27
View File
@@ -65,12 +65,6 @@ pub fn validate_upgrade_mode_jwt(
.map_err(|source| UpgradeModeCheckError::JwtVerificationFailure { source })?
.custom;
// jwt itself is cryptographically valid,
// but let's see if this entity has been permitted to issue the token in the first place
if !attestation.authorised_to_issue_jwt(&ed25519_pub_key) {
return Err(UpgradeModeCheckError::UnauthorisedIssuer);
}
Ok(attestation)
}
@@ -79,7 +73,6 @@ mod tests {
use super::*;
use crate::generate_new_attestation;
use nym_crypto::asymmetric::ed25519;
use nym_test_utils::helpers::deterministic_rng;
#[test]
fn generate_and_validate_jwt() {
@@ -93,25 +86,15 @@ mod tests {
2, 52, 215, 241, 219, 200, 18, 159, 241, 76, 111, 42, 32,
])
.unwrap();
let jwt_keys = ed25519::KeyPair::from(jwt_key);
let keys = ed25519::KeyPair::from(jwt_key);
let mut rng = deterministic_rng();
let unauthorised_jwt_keys = ed25519::KeyPair::new(&mut rng);
let attestation = generate_new_attestation(&attestation_key, vec![*jwt_keys.public_key()]);
let attestation = generate_new_attestation(&attestation_key);
let jwt_issuer = generate_jwt_for_upgrade_mode_attestation(
attestation.clone(),
attestation,
Duration::from_secs(60 * 60),
&jwt_keys,
&keys,
Some("nym-credential-proxy"),
);
let unauthorised_jwt = generate_jwt_for_upgrade_mode_attestation(
attestation.clone(),
Duration::from_secs(60 * 60),
&unauthorised_jwt_keys,
Some("nym-credential-proxy"),
);
// we expect 'nym-credential-proxy' issuer
assert!(validate_upgrade_mode_jwt(&jwt_issuer, Some("nym-credential-proxy")).is_ok());
@@ -121,15 +104,10 @@ mod tests {
// we expect another-issuer
assert!(validate_upgrade_mode_jwt(&jwt_issuer, Some("another-issuer")).is_err());
// the key is not in the authorised set inside the attestation
assert!(
validate_upgrade_mode_jwt(&unauthorised_jwt, Some("nym-credential-proxy")).is_err()
);
let jwt_no_issuer = generate_jwt_for_upgrade_mode_attestation(
attestation,
Duration::from_secs(60 * 60),
&jwt_keys,
&keys,
None,
);
// we expect 'nym-credential-proxy' issuer
+2 -4
View File
@@ -6,10 +6,8 @@ pub(crate) mod error;
pub(crate) mod jwt;
pub use attestation::{
UpgradeModeAttestation, generate_new_attestation, generate_new_attestation_with_starting_time,
UpgradeModeAttestation, attempt_retrieve, generate_new_attestation,
generate_new_attestation_with_starting_time,
};
pub use error::UpgradeModeCheckError;
pub use jwt::{generate_jwt_for_upgrade_mode_attestation, validate_upgrade_mode_jwt};
#[cfg(not(target_arch = "wasm32"))]
pub use attestation::attempt_retrieve_attestation;
+4 -2
View File
@@ -160,12 +160,13 @@ pub async fn setup_gateway_from_api(
minimum_performance: u8,
ignore_epoch_roles: bool,
) -> Result<InitialisationResult, WasmCoreError> {
let mut rng = thread_rng();
let gateways = gateways_for_init(
&mut rng,
nym_apis,
None,
minimum_performance,
ignore_epoch_roles,
None,
)
.await?;
setup_gateway_wasm(client_store, force_tls, chosen_gateway, gateways).await
@@ -177,12 +178,13 @@ pub async fn current_gateways_wasm(
minimum_performance: u8,
ignore_epoch_roles: bool,
) -> Result<Vec<RoutingNode>, ClientCoreError> {
let mut rng = thread_rng();
gateways_for_init(
&mut rng,
nym_apis,
user_agent,
minimum_performance,
ignore_epoch_roles,
None,
)
.await
}
-9
View File
@@ -832,15 +832,6 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]]
name = "example-contract"
version = "0.1.0"
dependencies = [
"cosmwasm-std",
"cw-storage-plus",
"serde",
]
[[package]]
name = "ff"
version = "0.13.0"
+1 -1
View File
@@ -9,7 +9,7 @@ members = [
"multisig/cw3-flex-multisig",
"multisig/cw4-group",
"vesting",
"performance", "example-contract",
"performance",
]
[workspace.package]
+1 -3
View File
@@ -255,12 +255,10 @@ pub fn query(deps: Deps<'_>, env: Env, msg: QueryMsg) -> Result<QueryResponse, C
}
#[entry_point]
pub fn migrate(deps: DepsMut<'_>, env: Env, _msg: MigrateMsg) -> Result<Response, ContractError> {
pub fn migrate(deps: DepsMut<'_>, _env: Env, _msg: MigrateMsg) -> Result<Response, ContractError> {
set_build_information!(deps.storage)?;
cw2::ensure_from_older_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
crate::queued_migrations::introduce_historical_epochs(deps, env)?;
Ok(Response::new())
}
@@ -9,7 +9,6 @@ use crate::epoch_state::storage::{load_current_epoch, save_epoch};
use crate::epoch_state::utils::check_epoch_state;
use crate::error::ContractError;
use crate::state::storage::STATE;
use crate::verification_key_shares::storage::vk_shares;
use crate::Dealer;
use cosmwasm_std::{Deps, DepsMut, Env, Event, MessageInfo, Response};
use nym_coconut_dkg_common::dealer::{DealerRegistrationDetails, OwnershipTransfer};
@@ -110,7 +109,7 @@ pub fn try_transfer_ownership(
DEALERS_INDICES.save(deps.storage, &transfer_to, &current_index)?;
DEALERS_INDICES.remove(deps.storage, &info.sender);
// update registration detail and share information for every epoch the current dealer has participated in the protocol
// update registration detail for every epoch the current dealer has participated in the protocol
// ideally, we'd have only updated the current epoch, but the way the contract is constructed
// forbids that otherwise we'd have introduced inconsistency
for epoch_id in 0..=epoch.epoch_id {
@@ -118,11 +117,6 @@ pub fn try_transfer_ownership(
EPOCH_DEALERS_MAP.remove(deps.storage, (epoch_id, &info.sender));
EPOCH_DEALERS_MAP.save(deps.storage, (epoch_id, &transfer_to), &details)?;
}
if let Some(mut vk_share) = vk_shares().may_load(deps.storage, (&info.sender, epoch_id))? {
vk_shares().remove(deps.storage, (&info.sender, epoch_id))?;
vk_share.owner = transfer_to.clone();
vk_shares().save(deps.storage, (&transfer_to, epoch_id), &vk_share)?;
}
}
let Some(transaction_info) = env.transaction else {
@@ -167,14 +161,6 @@ pub fn try_update_announce_address(
details.announce_address = new_address.clone();
EPOCH_DEALERS_MAP.save(deps.storage, (epoch.epoch_id, &info.sender), &details)?;
let mut contract_share = vk_shares().load(deps.storage, (&info.sender, epoch.epoch_id))?;
contract_share.announce_address = new_address.clone();
vk_shares().save(
deps.storage,
(&info.sender, epoch.epoch_id),
&contract_share,
)?;
Ok(Response::new().add_event(
Event::new("dkg-announce-address-update")
.add_attribute("dealer", info.sender)
@@ -242,14 +228,9 @@ pub(crate) mod tests {
#[cfg(feature = "testable-dkg-contract")]
mod tests_with_mock {
use super::*;
use crate::testable_dkg_contract::{
init_contract_tester, init_contract_tester_with_group_members, DkgContractTesterExt,
};
use anyhow::Context;
use crate::testable_dkg_contract::{init_contract_tester, DkgContractTesterExt};
use cosmwasm_std::testing::message_info;
use nym_coconut_dkg_common::msg::QueryMsg;
use nym_coconut_dkg_common::verification_key::PagedVKSharesResponse;
use nym_contracts_common_testing::{ChainOpts, ContractOpts};
use nym_contracts_common_testing::ContractOpts;
#[test]
fn transferring_ownership() -> anyhow::Result<()> {
@@ -267,7 +248,6 @@ mod tests_with_mock {
contract.run_initial_dummy_dkg();
let old_index = DEALERS_INDICES.load(&contract, &group_member)?;
let old_details = EPOCH_DEALERS_MAP.load(&contract, (0, &group_member))?;
let old_share = vk_shares().load(&contract, (&group_member, 0))?;
let not_group_member = contract.addr_make("not_group_member");
let (deps, env) = contract.deps_mut_env();
@@ -297,20 +277,13 @@ mod tests_with_mock {
assert!(EPOCH_DEALERS_MAP
.may_load(&contract, (0, &group_member))?
.is_none());
assert!(vk_shares()
.may_load(&contract, (&group_member, 0))?
.is_none());
let new_index = DEALERS_INDICES.load(&contract, &new_group_member)?;
let new_details = EPOCH_DEALERS_MAP.load(&contract, (0, &new_group_member))?;
let new_share = vk_shares().load(&contract, (&new_group_member, 0))?;
// the underlying info hasn't changed
assert_eq!(old_index, new_index);
assert_eq!(old_details, new_details);
assert_ne!(old_share, new_share);
assert_eq!(old_share.owner, group_member);
assert_eq!(new_share.owner, new_group_member);
assert_eq!(
OWNERSHIP_TRANSFER_LOG.load(
@@ -463,91 +436,9 @@ mod tests_with_mock {
assert_eq!(old_details1, new_details1);
assert_eq!(old_details2, new_details2);
// most recent entry is updated
// most recent entry is updated
assert_eq!(new_details3.announce_address, new_address);
Ok(())
}
#[test]
fn updating_announce_address_updates_vk_shares() -> anyhow::Result<()> {
let mut contract = init_contract_tester_with_group_members(3);
let group_member = contract.random_group_member();
contract.run_initial_dummy_dkg(); // => epoch 0
contract.run_reset_dkg(); // => epoch 1
// LEAVE DKG MEMBERSHIP
contract.remove_group_member(group_member.clone());
contract.run_reset_dkg(); // => epoch 2
// COME BACK
contract.add_group_member(group_member.clone());
contract.run_reset_dkg(); // => epoch 3
let old_address = EPOCH_DEALERS_MAP
.load(&contract, (3, &group_member))?
.announce_address;
let old_share0 = vk_shares().load(&contract, (&group_member, 0))?;
let old_share1 = vk_shares().load(&contract, (&group_member, 1))?;
let old_share2 = vk_shares().may_load(&contract, (&group_member, 2))?;
assert!(old_share2.is_none());
let old_share3 = vk_shares().may_load(&contract, (&group_member, 3))?;
assert!(old_share3.is_some());
let new_address = "https://new-address.com".to_string();
try_update_announce_address(
contract.deps_mut(),
message_info(&group_member, &[]),
new_address.clone(),
)?;
let new_share0 = vk_shares().load(&contract, (&group_member, 0))?;
let new_share1 = vk_shares().load(&contract, (&group_member, 1))?;
let new_share2 = vk_shares().may_load(&contract, (&group_member, 2))?;
assert!(new_share2.is_none());
let new_share3 = vk_shares().load(&contract, (&group_member, 3))?;
// old epoch data is unchanged
assert_eq!(old_share0, new_share0);
assert_eq!(old_share1, new_share1);
assert_eq!(old_share2, new_share2);
// most recent entry is updated
assert_eq!(new_share3.announce_address, new_address);
// finally an integration check against query endpoint
let epoch0_shares: PagedVKSharesResponse =
contract.query(&QueryMsg::GetVerificationKeys {
epoch_id: 0,
limit: None,
start_after: None,
})?;
assert_eq!(epoch0_shares.shares.len(), 3);
let member_share = epoch0_shares
.shares
.iter()
.find(|s| s.owner == group_member)
.context("failed to find member's share")?;
assert_eq!(member_share.announce_address, old_address);
let epoch0_shares: PagedVKSharesResponse =
contract.query(&QueryMsg::GetVerificationKeys {
epoch_id: 3,
limit: None,
start_after: None,
})?;
assert_eq!(epoch0_shares.shares.len(), 3);
let member_share = epoch0_shares
.shares
.iter()
.find(|s| s.owner == group_member)
.context("failed to find member's share")?;
assert_eq!(member_share.announce_address, new_address);
Ok(())
}
}
@@ -1,21 +1,2 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::epoch_state::storage::HISTORICAL_EPOCH;
use crate::error::ContractError;
use cosmwasm_std::{DepsMut, Env};
pub fn introduce_historical_epochs(deps: DepsMut, env: Env) -> Result<(), ContractError> {
if HISTORICAL_EPOCH.may_load(deps.storage)?.is_some() {
return Err(ContractError::FailedMigration {
comment: "this migration has already been run before".to_string(),
});
}
#[allow(deprecated)]
let current = crate::epoch_state::storage::CURRENT_EPOCH.load(deps.storage)?;
// we won't have information on intermediate states prior to now, but that's not the end of the world
HISTORICAL_EPOCH.save(deps.storage, &current, env.block.height)?;
Ok(())
}
@@ -62,18 +62,12 @@ impl TestableNymContract for DkgContract {
where
Self: Sized,
{
init_contract_tester()
init_contract_tester_with_group_members(DEFAULT_GROUP_MEMBERS)
}
}
pub fn init_contract_tester() -> ContractTester<DkgContract> {
init_contract_tester_with_group_members(DEFAULT_GROUP_MEMBERS)
}
pub fn init_contract_tester_with_group_members(members: usize) -> ContractTester<DkgContract> {
prepare_contract_tester_builder_with_group_members(members)
.build()
.with_common_storage_key(CommonStorageKeys::Admin, "dkg-admin")
DkgContract::init().with_common_storage_key(CommonStorageKeys::Admin, "dkg-admin")
}
pub fn prepare_contract_tester_builder_with_group_members<C>(
@@ -143,6 +137,12 @@ where
builder
}
pub fn init_contract_tester_with_group_members(members: usize) -> ContractTester<DkgContract> {
prepare_contract_tester_builder_with_group_members(members)
.build()
.with_common_storage_key(CommonStorageKeys::Admin, "dkg-admin")
}
pub trait DkgContractTesterExt:
ContractOpts<ExecuteMsg = ExecuteMsg, QueryMsg = QueryMsg, ContractError = ContractError>
+ ChainOpts
-30
View File
@@ -1,30 +0,0 @@
[package]
name = "example-contract"
version = "0.1.0"
authors.workspace = true
repository.workspace = true
homepage.workspace = true
documentation.workspace = true
edition.workspace = true
license.workspace = true
exclude = [
# Those files are rust-optimizer artifacts. You might want to commit them for convenience but they should not be part of the source code publication.
"contract.wasm",
"hash.txt",
"artifacts",
]
[lib]
name = "example_contract"
crate-type = ["cdylib", "rlib"]
[dependencies]
cosmwasm-std = { workspace = true }
cw-storage-plus = { workspace = true }
serde = { workspace = true, features = ["derive"] }
[lints]
workspace = true
-5
View File
@@ -1,5 +0,0 @@
wasm:
RUSTFLAGS='-C link-arg=-s' cargo build --release --target wasm32-unknown-unknown
generate-schema:
cargo schema
-123
View File
@@ -1,123 +0,0 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use cosmwasm_std::{
entry_point, to_json_binary, Addr, Coin, Deps, DepsMut, Env, Event, MessageInfo, QueryResponse,
Response,
};
use cw_storage_plus::Item;
use serde::{Deserialize, Serialize};
pub(crate) const COUNTER: Item<u32> = Item::new("counter");
#[derive(Serialize, Deserialize)]
pub struct InstantiateMsg {
initial_counter: u32,
}
#[derive(Serialize, Deserialize)]
pub enum ExecuteMsg {
IncrementCounter {},
DecrementCounter {},
SetCounter { to: u32 },
}
#[derive(Serialize, Deserialize)]
pub enum QueryMsg {
GetCounter {},
}
#[derive(Serialize, Deserialize)]
pub struct MigrateMsg {}
#[entry_point]
pub fn instantiate(
mut deps: DepsMut<'_>,
env: Env,
info: MessageInfo,
msg: InstantiateMsg,
) -> Result<Response, String> {
COUNTER
.save(deps.storage, &msg.initial_counter)
.map_err(|err| err.to_string())?;
Ok(Response::default())
}
#[entry_point]
pub fn execute(
deps: DepsMut<'_>,
env: Env,
info: MessageInfo,
msg: ExecuteMsg,
) -> Result<Response, String> {
match msg {
ExecuteMsg::IncrementCounter {} => {
let current_value = COUNTER.load(deps.storage).map_err(|err| err.to_string())?;
COUNTER
.save(deps.storage, &(current_value + 1))
.map_err(|err| err.to_string())?;
}
ExecuteMsg::DecrementCounter {} => {
let current_value = COUNTER.load(deps.storage).map_err(|err| err.to_string())?;
COUNTER
.save(deps.storage, &(current_value - 1))
.map_err(|err| err.to_string())?;
}
ExecuteMsg::SetCounter { to } => {
COUNTER
.save(deps.storage, &(to))
.map_err(|err| err.to_string())?;
}
}
Ok(Response::new().add_event(Event::new("my-amazing-event").add_attribute("key1", "value1")))
}
#[entry_point]
pub fn query(deps: Deps<'_>, env: Env, msg: QueryMsg) -> Result<QueryResponse, String> {
let query_res = match msg {
QueryMsg::GetCounter {} => {
to_json_binary(&COUNTER.load(deps.storage).map_err(|err| err.to_string())?)
}
};
Ok(query_res.map_err(|err| err.to_string())?)
}
#[entry_point]
pub fn migrate(mut deps: DepsMut<'_>, _env: Env, msg: MigrateMsg) -> Result<Response, String> {
Ok(Response::default())
}
#[cfg(test)]
mod tests {
use super::*;
use cosmwasm_std::from_json;
use cosmwasm_std::testing::{message_info, mock_dependencies, mock_env};
#[test]
fn it_works() {
let mut deps = mock_dependencies();
let env = mock_env();
let init_msg = InstantiateMsg {
initial_counter: 123,
};
let sender = message_info(&deps.api.addr_make("mock-sender"), &[]);
instantiate(deps.as_mut(), env, sender, init_msg).unwrap();
assert_eq!(COUNTER.load(deps.as_ref().storage).unwrap(), 123);
let msg = ExecuteMsg::IncrementCounter {};
let sender = message_info(&deps.api.addr_make("mock-sender"), &[]);
execute(deps.as_mut(), mock_env(), sender, msg).unwrap();
assert_eq!(COUNTER.load(deps.as_ref().storage).unwrap(), 124);
let msg = QueryMsg::GetCounter {};
let query_res = query(deps.as_ref(), mock_env(), msg).unwrap();
println!("raw binary: {:?}", query_res);
println!("string: {:?}", String::from_utf8_lossy(&query_res));
println!("deserialised: {:?}", from_json::<u32>(query_res).unwrap());
}
}
-1
View File
@@ -1 +0,0 @@
pub mod contract;
+12
View File
@@ -0,0 +1,12 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn legacy_mixnode_bonding() {
todo!()
}
}
+4
View File
@@ -2,3 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
pub(crate) mod transactions;
// the purpose of that module is to keep track of tests of legacy features that will eventually be phased out
// such as standalone mixnode/gateway bonding
pub(crate) mod legacy;
@@ -1,26 +1,27 @@
```tsx copy filename="mixFetchExample.tsx"
import React, { useState } from "react";
import CircularProgress from "@mui/material/CircularProgress";
import Button from "@mui/material/Button";
import TextField from "@mui/material/TextField";
import Typography from "@mui/material/Typography";
import Box from "@mui/material/Box";
import { mixFetch } from "@nymproject/mix-fetch-full-fat";
import Stack from "@mui/material/Stack";
import Paper from "@mui/material/Paper";
import type { SetupMixFetchOps } from "@nymproject/mix-fetch-full-fat";
```ts copy filename="mixFetchExample.tsx"
import React, { useState } from 'react';
import CircularProgress from '@mui/material/CircularProgress';
import Button from '@mui/material/Button';
import TextField from '@mui/material/TextField';
import Typography from '@mui/material/Typography';
import Box from '@mui/material/Box';
import { mixFetch } from '@nymproject/mix-fetch-full-fat';
import Stack from '@mui/material/Stack';
import Paper from '@mui/material/Paper';
import type { SetupMixFetchOps } from '@nymproject/mix-fetch';
const defaultUrl = "https://nymtech.net/.wellknown/network-requester/exit-policy.txt";
const args = { mode: "unsafe-ignore-cors" };
const defaultUrl = 'https://nym.com/favicon.svg';
const args = { mode: 'unsafe-ignore-cors' };
const mixFetchOptions: SetupMixFetchOps = {
preferredGateway: "2xU4CBE6QiiYt6EyBXSALwxkNvM7gqJfjHXaMkjiFmYW", // with WSS
// preferredNetworkRequester:
// "CTDxrcXgrZHWyCWnuCgjpJPghQUcEVz1HkhUr5mGdFnT.3UAww1YWNyVNYNWFQL1LaHYouQtDiXBGK5GiDZgpXkTK@2RFtU5BwxvJJXagAWAEuaPgb5ZVPRoy2542TT93Edw6v",
preferredGateway: '6Gb7ftQdKveMjPyrxDXeAtfYAX7Zg5mVZHtnRC5MmZ1B', // with WSS
preferredNetworkRequester:
'8rRGWy54oC8drFL9DepMegBt2DLrsqQwCoHMXt9nsnTo.2XjCPVbb4FpQ9hNRcXwb9mTzEAVVk1zf1tcch3wdtNEA@6Gb7ftQdKveMjPyrxDXeAtfYAX7Zg5mVZHtnRC5MmZ1B',
mixFetchOverride: {
requestTimeoutMs: 60_000,
},
forceTls: true, // force WSS
extra: {},
};
export const MixFetch = () => {
@@ -44,7 +45,7 @@ export const MixFetch = () => {
};
return (
<div style={{ marginTop: "1rem" }}>
<div style={{ marginTop: '1rem' }}>
<Stack direction="row">
<TextField
disabled={busy}
@@ -55,12 +56,7 @@ export const MixFetch = () => {
defaultValue={defaultUrl}
onChange={(e) => setUrl(e.target.value)}
/>
<Button
variant="outlined"
disabled={busy}
sx={{ marginLeft: "1rem" }}
onClick={handleFetch}
>
<Button variant="outlined" disabled={busy} sx={{ marginLeft: '1rem' }} onClick={handleFetch}>
Fetch
</Button>
</Stack>
@@ -1,19 +1,15 @@
```ts copy filename="MixnetWASMClientExample.tsx"
import React, { useEffect, useState } from "react";
import {
createNymMixnetClient,
NymMixnetClient,
Payload,
} from "@nymproject/sdk-full-fat";
import Box from "@mui/material/Box";
import CircularProgress from "@mui/material/CircularProgress";
import Paper from "@mui/material/Paper";
import Typography from "@mui/material/Typography";
import Stack from "@mui/material/Stack";
import TextField from "@mui/material/TextField";
import Button from "@mui/material/Button";
import React, { useEffect, useState } from 'react';
import { createNymMixnetClient, NymMixnetClient, Payload } from '@nymproject/sdk-full-fat';
import Box from '@mui/material/Box';
import CircularProgress from '@mui/material/CircularProgress';
import Paper from '@mui/material/Paper';
import Typography from '@mui/material/Typography';
import Stack from '@mui/material/Stack';
import TextField from '@mui/material/TextField';
import Button from '@mui/material/Button';
const nymApiUrl = "https://validator.nymtech.net/api";
const nymApiUrl = 'https://validator.nymtech.net/api';
export const Traffic = () => {
const [nym, setNym] = useState<NymMixnetClient>();
@@ -21,31 +17,27 @@ export const Traffic = () => {
const [recipient, setRecipient] = useState<string>();
const [payload, setPayload] = useState<Payload>();
const [receivedMessage, setReceivedMessage] = useState<string>();
const [buttonEnabled, setButtonEnabled] = useState<boolean>(false);
const init = async () => {
const client = await createNymMixnetClient();
setNym(client);
// start the client and connect to a gateway
await client?.client.start({
clientId: crypto.randomUUID(),
nymApiUrl,
forceTls: true, // force WSS
});
// check when is connected and set the self address
client?.events.subscribeToConnected((e) => {
const { address } = e.args;
setSelfAddress(address);
});
// show whether the client is ready or not
client?.events.subscribeToLoaded((e) => {
console.log("Client ready: ", e.args);
console.log('Client ready: ', e.args);
});
// show message payload content when received
client?.events.subscribeToTextMessageReceivedEvent((e) => {
console.log(e.args.payload);
setReceivedMessage(e.args.payload);
@@ -56,8 +48,7 @@ export const Traffic = () => {
await nym?.client.stop();
};
const send = () =>
payload && recipient && nym?.client.send({ payload, recipient });
const send = () => nym.client.send({ payload, recipient });
useEffect(() => {
init();
@@ -66,17 +57,9 @@ export const Traffic = () => {
};
}, []);
useEffect(() => {
if (recipient && payload) {
setButtonEnabled(true);
} else {
setButtonEnabled(false);
}
}, [recipient, payload]);
if (!nym || !selfAddress) {
return (
<Box sx={{ display: "flex" }}>
<Box sx={{ display: 'flex' }}>
<CircularProgress />
</Box>
);
@@ -84,10 +67,10 @@ export const Traffic = () => {
return (
<Box padding={3}>
<Paper style={{ marginTop: "1rem", padding: "2rem" }}>
<Paper style={{ marginTop: '1rem', padding: '2rem' }}>
<Stack spacing={3}>
<Typography variant="body1">My self address is:</Typography>
<Typography variant="body1">{selfAddress || "loading"}</Typography>
<Typography variant="body1">{selfAddress || 'loading'}</Typography>
<Typography variant="h5">Communication through the Mixnet</Typography>
<TextField
type="text"
@@ -100,22 +83,15 @@ export const Traffic = () => {
placeholder="Message to send"
multiline
rows={4}
onChange={(e) =>
setPayload({ message: e.target.value, mimeType: "text/plain" })
}
onChange={(e) => setPayload({ message: e.target.value, mimeType: 'text/plain' })}
size="small"
/>
<Button
variant="outlined"
onClick={() => send()}
disabled={!buttonEnabled}
sx={{ width: "fit-content" }}
>
<Button variant="outlined" onClick={() => send()} disabled={!payload || !recipient} sx={{width: 'fit-content'}}>
Send
</Button>
</Stack>
{receivedMessage && (
<Stack spacing={3} style={{ marginTop: "1rem" }}>
<Stack spacing={3} style={{ marginTop: '1rem' }}>
<Typography variant="h5">Message Received!</Typography>
<Typography fontFamily="monospace">{receivedMessage}</Typography>
</Stack>
@@ -11,7 +11,6 @@ import { Alert, AlertTitle } from '@mui/material';
import { Wallet } from '@cosmos-kit/core';
import { CosmosKitLedger } from './ledger';
import { CosmosKitSign } from './sign';
import type { SignerOptions } from '@cosmos-kit/core';
const CosmosKitSetup: FC<{ children: React.ReactNode }> = ({ children }) => {
const assetsFixedUp = React.useMemo(() => {
@@ -41,16 +40,14 @@ const CosmosKitSetup: FC<{ children: React.ReactNode }> = ({ children }) => {
return chains;
}, [chains]);
// components/cosmos-kit/index.tsx
return (
<ChainProvider
chains={chainsFixedUp}
assetLists={assetsFixedUp}
wallets={[...ledger, ...keplr]}
signerOptions={{ preferredSignType: () => 'amino' }}
throwErrors={false}
signerOptions={{
preferredSignType: () => 'amino',
}}
>
{children}
</ChainProvider>
+4 -4
View File
@@ -9,13 +9,13 @@ import Stack from "@mui/material/Stack";
import Paper from "@mui/material/Paper";
import type { SetupMixFetchOps } from "@nymproject/mix-fetch-full-fat";
const defaultUrl = "https://nymtech.net/.wellknown/network-requester/exit-policy.txt";
const defaultUrl = "https://nym.com/favicon.svg";
const args = { mode: "unsafe-ignore-cors" };
const mixFetchOptions: SetupMixFetchOps = {
preferredGateway: "2xU4CBE6QiiYt6EyBXSALwxkNvM7gqJfjHXaMkjiFmYW", // with WSS
// preferredNetworkRequester:
// "CTDxrcXgrZHWyCWnuCgjpJPghQUcEVz1HkhUr5mGdFnT.3UAww1YWNyVNYNWFQL1LaHYouQtDiXBGK5GiDZgpXkTK@2RFtU5BwxvJJXagAWAEuaPgb5ZVPRoy2542TT93Edw6v",
preferredGateway: "6Gb7ftQdKveMjPyrxDXeAtfYAX7Zg5mVZHtnRC5MmZ1B", // with WSS
preferredNetworkRequester:
"8rRGWy54oC8drFL9DepMegBt2DLrsqQwCoHMXt9nsnTo.2XjCPVbb4FpQ9hNRcXwb9mTzEAVVk1zf1tcch3wdtNEA@6Gb7ftQdKveMjPyrxDXeAtfYAX7Zg5mVZHtnRC5MmZ1B",
mixFetchOverride: {
requestTimeoutMs: 60_000,
},
@@ -1,2 +0,0 @@
export const ICMP =
"ICMP (Internet Control Message Protocol) is a network layer protocol used for sending error messages and operational information regarding network conditions. It is commonly utilized for diagnostic purposes, such as sending ping requests to check the reachability of a host. ICMP helps in managing and controlling network traffic by providing feedback about issues in the communication environment.";
@@ -1,3 +0,0 @@
export const IPR =
"IPR (IP Packet Router) is a component that routes packets to the Internet on behalf of clients within the Nym network. It uses multiplexed streams, NAT, and SURBs for anonymity.";
@@ -1,93 +0,0 @@
import { Callout } from 'nextra/components';
## Mixnet: Node Performance Calculation
Performance is a value between `0` and `1`. The final performance number is a result of multiplying [config score](#config-score-calculation) and [routing score](#routing-score-calculation).
<Callout type="info" emoji="📌">
> **node_performance = config_score \* routing_score**
</Callout>
Performance value is an average of last 24h.
<Callout>
All parameters regarding performance score can be browsed or pull live from:
`https://validator.nymtech.net/api/v1/nym-nodes/annotation/<NODE_ID>`
In case you don't know your nodes `NODE_ID`, it's easy to find as long as your node is bonded. Visit [validator.nymtech.net/api/v1/nym-nodes/bonded](https://validator.nymtech.net/api/v1/nym-nodes/bonded) and search your node using `identity_key` or bonding Nyx account address (denoted as `owner`).
</Callout>
### Config Score Calculation
Config score is in place to ensure that the node configuration is done properly so the node is eligible for taking part in Nym network. The API looks into these paramteres:
1. If the node binary is `nym-node` (not legacy `nym-mixnode` or `nym-gateway`): `1` if `True`, `0` if `False`
2. If [Terms & Conditions](/operators/nodes/nym-node/setup.mdx#terms--conditions) are accepted: `1` if `True`, `0` if `False`
3. If the nodes self described endpoint is available: `1` if `True`, `0` if `False`
4. Version of `nym-node` binary: decreasing weight for outdated versions, as [explained below](#versions-behind-calculation)
**The `config_score` calculation formula:**
<Callout type="info" emoji="📌">
> **config_score = is_tc_accepted \* is_nym-node_binary \* self_described_api_available \* ( 0.995 ^ ( ( X * versions_behind) ^ 1.65 ) )**
</Callout>
First three points have binary values of either `0` or `1`, with a following logic:
| **Run `nym-node` binary** | **T&C's accepted** | **Self described available** | **Value** |
| :-- | :-- | :-- | ---: |
| **True** | **True** | **True** | **1** |
| True | False | False | 0 |
| True | True | False | 0 |
| False | True | True | 0 |
| False | False | True | 0 |
| True | False | True | 0 |
| False | False | False | 0 |
| False | True | False | 0 |
**Only if ALL conditions above are `True` the node can have any chance to be selected, as otherwise the probability will always be 0.**
<Callout type="info" emoji="️">
Besides these values, the API also checks whether the node is bonded in Mixnet smart contract as a Nym Node or legacy node (Mixnode or Gateway). **Only nodes bonded as Nym Node in Mixnet smart contract can be selected to the Rewrded set. Thus, if you haven't migrated your node yet, please [follow these steps](/operators/nodes/nym-node/bonding#migrate-to-nym-node-in-mixnet-smart-contract)!**
</ Callout>
#### Versions Behind Calculation
From release `2024.14-crunch` (`nym-node v1.2.0`), the `config_score` parameter takes into account also nodes version (denoted as `versions_behind`). The "current version" is the one marked as `Latest` in the [repository](https://github.com/nymtech/nym/releases/). The parameter `versions_behind` indicates the number of versions between the `Latest` version and the version run by the node, and it is factored into the config score with this formula:
<Callout type="info" emoji="📌">
> **0.995 ^ ( ( X * versions_behind ) ^ 1.65 )**
>
> where: <br />
> **X = 1; for patches** <br />
> **X = 10; for minor versions** <br />
> **X = 100; for major versions**
</Callout>
> The exact parameters are live accessible on [`/v1/status/config-score-details`](https://validator.nymtech.net/api/swagger/index.html#/Status/config_score_details).
Our versioning convention is: `major_version . minor_version . patch`
For example `nym-node` on version `1.2.0` is on 1st major version, 2nd minor and 0 patches.
Note that the `X` multiplier heavily lowers the `config_score` when nodes are outdated with respect to more significant updates. See the the table and graph below:
| **Version behind** | **Patches (X = 1)** | **Minor versions (X = 10)** | **Major versions (X = 100)** |
| :-- | --: | --: | --: |
| 0 (current version) | 1.0 | 1.0 | 1.0 |
| 1 | 0.995 | 0.7994 | 0.0000 |
| 2 | 0.9844 | 0.4953 | 0.0000 |
| 3 | 0.9698 | 0.2536 | 0.0000 |
| 4 | 0.9518 | 0.1102 | 0.0000 |
| 5 | 0.9311 | 0.0413 | 0.0000 |
![](/images/operators/tokenomics/reward_version_graph.png)
As you can see above, the algorithm is designed to give maximum selection score (`1`) to the latest version, while non-upgraded nodes receive a lower score. The score decreases faster when the node has failed to make a major version upgrade, and slower when the node is behind only with minor updates. This scoring de-prioritizes the selection of outdated nodes, even if their saturation and performance are high. Nodes are selected probabilistically in each epoch (60 min), according to their scores, to be part of the Rewarded set. This scoring mechanism gives priority to the operators running up-to-date nodes, ensuring that the network is as updated as possible.
### Routing Score Calculation
Routing score is measured by Nym Network Monitor which sends thousands of packages through different routes every 15 minutes and measures how many were dropped on the way. Test result represents percentage of packets succesfully returned which are then converted into floats bettween `0` and `1`.
@@ -1,223 +0,0 @@
import { Callout } from 'nextra/components';
import { Steps } from 'nextra/components';
import { Green, Yellow, Red, Gray } from 'components/severity.jsx'
import { IPR } from 'components/operators/snippets/ipr.js';
import { ICMP } from 'components/operators/snippets/icmp.js';
## WireGuard: Gateways Performance Calculation
> If you are looking for a page with a guide to run your own `gateway-probe` instance, go [here](/operators/performance-and-testing/gateway-probe).
<Callout type="info" emoji="️">
**Note that there is only one binary `nym-node` for [all network functionalities](/operators/nodes/nym-node/setup#functionality-mode) (defined by a positional argument `--mode`).**
When we say *Exit Gateway* in connection to Wireguard, we talk about a `nym-node` running in a mode Exit Gateway (`--mode exit-gateway`) with Wireguard enabled. Such node is listed in [NymVPN application](https://nym.com) as an Exit Gateway and an Entry Gateway for both dVPN (2-hop) and Mixnet (5-hop) options. That's because every Exit Gateway can serve as an Entry, not vice versa for Mixnet and every Gateway with Wireguard enabled option (`--wireguard-enabled true`) can always serve as both Entry and Exit for dVPN (provided that they are correctly [configured](/operators/nodes/nym-node/configuration)).
</Callout>
**Please note that even the most perfectly configured Gateway, running on the strongest machine may not always be listed as a suggested node in the app. If you are interested to learn the details why, check out the [Gateway Probe Details page](/operators/performance-and-testing/gateway-probe-details).**
### Summary
A simplified explanation of node performance measuring is that the [Node Status API](https://node-status.nym.com) runs [gateway probes](/operators/performance-and-testing/gateway-probe) that connect to gateways to:
- Check the configuration of gateways
- Checks a list of capabilities (e.g. can route IPv4 traffic in mixnet mode)
- Checks a list of configuration (e.g. runs <abbr title={IPR}>IPR</abbr>, has exit policy)
- Acts like a user:
- Registers a mixnet client
- Registers a wireguard peer and tops up bandwidth with a zk-nym
- Sends <abbr title={ICMP}>ICMP</abbr> ping packets
- Downloads files
The results are collected and stored in the [Node Status API](https://node-status.nym.com) and can be also veiwed per node in [Node Status Observatory](https://harbourmaster.nymtech.net).
The [NymVPN API directory](https://nymvpn.com/api/public/v1/directory/gateways) cache uses the output of the gateway probes to calculate and display hints to users about the contention on each gateway and what they might expect if they use the gateway. The section [*Gateway Roles & Requirements* below](#gateway-roles--requirements) explains how are these stats measured and what is their significance.
### Performance Score
Gateways are listed in NymVPN application displaying various stats.
![](/images/operators/nym-vpn-sceenshots/nym-vpn-score1.png)
There are four colored score bars.
![](/images/operators/nym-vpn-sceenshots/nym-vpn-score2.png)
This is how they are defined:
<Steps>
###### 1. Overall performance:
Calculated by mixnet performance score multiplied by WireGuard performance, this is the formula:
<Callout type="info" emoji="📌">
> **mixnet_performance * (download_speed_score * ping_ips_performance_v4)**
</Callout>
- <Green>High</Green>: `> 75%`
- <Yellow>Medium</Yellow>: `> 50%`
- <Red>Low</Red>: > `10%`
- <Gray>Offline</Gray>: `≤ 10%`
- **Download speed scoring:**
<Callout type="info" emoji="📌">
> **\> 5 Mbps = 1.0** <br />
> **\> 2 Mbps = 0.75** <br />
> **\> 1 Mbps = 0.5** <br />
> **\> 0.5 Mbps = 0.25** <br />
> **otherwise = 0.1**
</Callout>
###### 2. Server Load:
A load indicator based on ping success
- <Green>Low</Green>: `> 80%` ping success (low load)
- <Yellow>Medium</Yellow>: `> 40%` ping success (medium load)
- <Red>High</Red>: `≤ 40%` ping success (node under stress, high load)
###### 3. Uptime:
A routing score of Mixnet (5-hop) is indicated this way:
- <Green>High</Green>: `> 80%` mixnet packet delivery success
- <Yellow>Medium</Yellow>: `> 60%` mixnet packet delivery success
- <Red>Low</Red>: `> 10%` mixnet packet delivery success
- <Gray>Offline</Gray>: `≤ 10%` mixnet packet delivery success
</Steps>
Note that there are caches in place to keep various dept of details to provide info for different pieces of the software. Their timing differs and it can lead to small discrepancies of outcome in between multiple clients observed by users at the same time.
See the [*Gateway Roles & Requirements*](#gateway-roles--requirements) to understand how these stats get measured.
<Callout>
You can find more details [directly in the code](https://github.com/nymtech/nym-vpn-client/blob/develop/nym-vpn-app/src/hooks/useScore.ts#L10).
</Callout>
### Gateway Roles & Requirements
A node configuration break down coming alongside basic [server configuration](/operators/nodes/preliminary-steps/vps-setup) required for the nodes in specific roles:
- **Entry Gateway - Mixnet:**
- Node configured (running with `--mode` argument) as `entry-gateway` or `exit-gateway` (as Exit works as Entry too)
- `WS/WSS` ports (9000/9001) accessible
- [IPv6 configured](/operators/nodes/nym-node/configuration#quick-ipv6-check)
- **Exit Gateway - Mixnet:**
- Node configured (running with `--mode` argument) as `exit-gateway`, that configuration ensures that:
- <abbr title={IPR}>IPR</abbr> address present
- NR address present
- [IPv6 configured](/operators/nodes/nym-node/configuration#quick-ipv6-check)
- NymTun configured - using [NetworkTunnelManager](/operators/nodes/nym-node/configuration#routing-configuration)
- **Entry & Exit Gateway - dVPN (WireGuard mode):**
- Node configured as Wireguard node: `--wireguard-enabled true`
- Authenticator address present
- NymWG configured - using [NetworkTunnelManager](/operators/nodes/nym-node/configuration#routing-configuration)
- [Metadata port open for WG](/operators/nodes/nym-node/configuration#fixing-metadata-port-showing-not-open-in-probe-results)
- Wireguard exit policy [configured](/operators/nodes/nym-node/configuration#wireguard-exit-policy-configuration)
- [IPv6 configured](/operators/nodes/nym-node/configuration#quick-ipv6-check)
- Recommended: [QUIC bridge setup](/operators/nodes/nym-node/configuration#quic-transport-bridge-deployment)
### How Probe Works
Nym Gateway probe is perfomance measurement program running tests through various routes. Operators can run their [local instance of Gateway probe](/operators/performance-and-testing/gateway-probe) or use visit of our dashboards to conveniently see probe results.
- [Node Status dashboard](https://node-status.nym.com/dvpn): Shows latest probe results on one board
- [Nym Node Status Observatory](https://harbourmaster.nymtech.net): New version of a good old Nym Harbourmaster, allowing operators preview stats of each node
The following sections explain what is measured in order to collect stats to the [API enpoint](https://nymvpn.com/api/public/v1/directory/gateways) and in the section [*Complete Setup Checklist*](#complete-setup-checklist) below you can see the order of measurements.
#### Entry Gateway - Mixnet (5-hop)
**Tests:**
- `can_connect`: Can connect to client WS/WSS endpoint (ports 9000/9001)
- `can_route`: Entry gateway correctly forwards packets into the mixnet
**To Pass:**
- Expose correct `WS` and or `WSS` ports (`9000`/ `9001`)
- Ensure DNS A/AAAA records resolve correctly
- Verify IPv6 is actually reachable (not just in DNS)
- Keep node online and monitor packet loss
#### Exit Gateway (IPR) - Mixnet (5-hop)
**Tests:**
- `can_connect`: Can connect to <abbr title={IPR}>IPR</abbr>
- `can_route_ip_v4`: IPv4 routing works
- `can_route_ip_external_v4`: IPv4 routing to external internet works
- `can_route_ip_v6`: IPv6 routing works
- `can_route_ip_external_v6`: IPv6 routing to external internet works
**To Pass:**
- Ensure upstream routing and NAT allow both IPv4 and IPv6 traffic to external internet
- Check system firewall rules (bidirectional)
- Confirm outbound reachability to common external hosts on both stacks
- Run node with `mode=exit-gateway` - this by default includes entry-gateway mode.
#### Gateways - WireGuard (dVPN / 2-hop)
**Tests:**
- `can_register`: Authentication flow completes successfully
- `can_handshake`: WireGuard handshake succeeds
- `can_resolve_dns`: DNS resolution works
- `can_query_metadata_v4`: Can query metadata endpoint
- `ping_hosts_performance` / `ping_ips_performance`: Latency and packet loss metrics
**To Pass:**
- Open WireGuard UDP port `51822` (public)
- Ensure metadata TCP port `51830` is accessible inside the WireGuard tunnel (at `10.1.0.1:51830`) for bandwidth queries/topup
- Keep QUIC bridge (`UDP 4443`) healthy if deployed
- Reduce server latency/jitter (depends on provider and location)
- Ensure DNS resolvers are reliable
- Pass the `--wireguard-enabled true` flag in the node config
### Probe Freshness
- Probes run continuously; each node is typically tested every ~2 hours
- If a probe fails or times out, the gateway will have stale probe data until the next successful probe completes
- `last_updated_utc`: Timestamp indicating when the last successful probe test completed
See [*Complete Setup Checklist*](#complete-setup-checklist) to understand the order of tests.
### Complete Setup Checklist
**If one test fails, the following points don't get checked!**
#### On-Chain & Configuration
- [ ] [Bonded on-chain](/operators/nodes/nym-node/bonding) with [correct role](/operators/nodes/nym-node/setup#functionality-mode) (`exit-gateway`) and correct ports opened exposed
- [ ] Self-description endpoint is reachable
- [ ] [T&Cs accepted](/operators/nodes/nym-node/setup#terms--conditions)
#### Network & Ports
- [ ] **Mixnet**: Port `1789` (mixnet) accessible; ports `9000` (`WS`) and `9001` (`WSS`) accessible with valid [TLS for WSS](/operators/nodes/nym-node/configuration/proxy-configuration#reverse-proxy-configuration)
- [ ] [**QUIC bridge**](/operators/nodes/nym-node/configuration#quic-transport-bridge-deployment) (if used): UDP 4443 with valid addresses and SNI host
- [ ] [**WireGuard**](/operators/nodes/nym-node/configuration#routing-configuration): `UDP 51822` open (public); `TCP 51830` accessible [inside WireGuard tunnel](/operators/nodes/nym-node/configuration#fixing-metadata-port-showing-not-open-in-probe-results) (`10.1.0.1:51830`); `--wireguard-enabled true` set in node config
- [ ] **Exit (<abbr title={IPR}>IPR</abbr>)**: [IPv4 and IPv6](/operators/nodes/nym-node/configuration#routing-configuration) routing to external internet configured
- [ ] **DNS**: A/AAAA records correct; IPv6 actually reachable (test, don't just add DNS)
#### Performance Targets
- [ ] Achieve `≥ 60%` mixnet performance (Medium)
- [ ] Aim for `≥ 80%` (High) for optimal visibility
- [ ] Keep latency and packet loss low (excessive load reduces performance scores)
- [ ] Keep uptime high (network monitor tracks this)
#### Probe Results (Verify All Pass)
- [ ] Entry: `can_connect=true` and `can_route=true`
- [ ] Exit: `can_connect=true` and all v4/v6 routing flags are `true`
- [ ] WireGuard: `can_register`, `can_handshake`, `can_resolve_dns`, and `can_query_metadata_v4` all pass; ping metrics good
#### Recommended
- [ ] [WSS enabled](/operators/nodes/nym-node/configuration/proxy-configuration) (connections may fail on clients if only WS is available)
- [ ] [QUIC bridge](/operators/nodes/nym-node/configuration#quic-transport-bridge-deployment) operational (ensures your node appears when users enable QUIC only filter on the app)
- [ ] Accurate geo/IP information (country/region/residential filters rely on this)
- [ ] Stay on most recent supported binary version - [old versions are penalised](/operators/performance-and-testing#versions-behind-calculation)
@@ -1,126 +0,0 @@
import { Steps } from 'nextra/components';
import { Callout } from 'nextra/components';
import { AccordionTemplate } from 'components/accordion-template.tsx';
<Callout type="info" emoji="️">
**QUIC bridge is a requirement for all nodes which enable Wireguard functionality. Note that it this feature is compatible with nodes from `v1.19.0` (platform release [`v2025.18-jarlsberg`](https://github.com/nymtech/nym/releases/tag/nym-binaries-v2025.17-isabirra)) and newer!**
</ Callout>
Nym Network uses various [transport bridges](https://github.com/nymtech/nym-bridges/blob/main/README.md) for routing the packets. Right now operators need to configure [our implementation](https://github.com/nymtech/nym-bridges/tree/main/nym-bridge) of general-purpose transport layer network protocol called [QUIC](https://en.wikipedia.org/wiki/QUIC).
Operators can use [Nym Bridge Configuration Tool](https://github.com/nymtech/nym-bridges/tree/main/bridge-cfg) and compile the [Bridge Runner binary](https://github.com/nymtech/nym-bridges/tree/main/nym-bridge) themselves.
**We recommend a more convenient QUIC bridge deployment using a script [`quic_bridge_deployment.sh`](https://github.com/nymtech/nym/blob/develop/scripts/nym-node-setup/quic_bridge_deployment.sh), following the steps below.**
<Callout type="warning" emoji="⚠️">
This script assumes that you follow the convention of running a `nym-node` as `root`. In case you run as a [non-root user](/operators/nodes/nym-node/configuration#running-nym-node-as-a-non-root), please follow [manual QUIC bridge setup](https://github.com/nymtech/nym-bridges/blob/main/README.md) and ask for support in the [Matrix channel](https://matrix.to/#/#operators:nymtech.chat).
</Callout>
<Steps>
###### 1. Download [`quic_bridge_deployment.sh`](https://github.com/nymtech/nym/blob/develop/scripts/nym-node-setup/quic_bridge_deployment.sh) script
- SSH to your server
- **Run as root**
- Download the script and make executable
```sh
curl -L https://raw.githubusercontent.com/nymtech/nym/refs/heads/develop/scripts/nym-node-setup/quic_bridge_deployment.sh -o quic_bridge_deployment.sh && \
chmod +x quic_bridge_deployment.sh
```
###### 2. Run the script with `full_bridge_setup` command
- Optional: open `tmux` in case you will need to run another commands on the VPS
- Run the script with a command `full_bridge_setup`
```sh
./quic_bridge_deployment.sh full_bridge_setup
```
###### 3. Follow the interactive prompts
- Make sure you don't just press enter to insert default values if your setup is different, for example in case of path to the config file
###### 4. Validate
- After running the script, ensure that the bridges are running correctly:
```sh
systemctl status nym-bridge.service
```
###### 5. Restart `nym-node` service
- When done with the deployment, please restart your node systemd service
```sh
service nym-node restart && journalctl -u nym-node.service -f --all
```
</Steps>
Congratulation, you deployed QUIC transport bridge! The script offers a standalone tweaks and checks, you can always run it without any argument to see all the options:
```sh
./quic_bridge_deployment.sh
```
<br/>
<AccordionTemplate name="Command output">
```shell
root@localhost:~# ./quic_bridge_deployment.sh
Logs are being saved locally to: /var/log/nym-bridge-helper.log
These logs never leave your machine.
Usage: ./quic_bridge_deployment.sh [command]
Commands:
full_bridge_setup - Run full setup
install_bridge_binary - Install nym-bridge (.deb; falls back to source build if libc too old)
install_bridge_cfg_tool - Install bridge-cfg (prebuilt; falls back to source build if libc too old)
run_bridge_cfg_generate - Generate bridges.toml
create_bridge_service - Setup systemd service (respects .deb-provided service)
adjust_ip_forwarding - Enable forwarding
apply_bridge_iptables_rules - NAT rules
configure_dns_and_icmp - Allow ICMP/DNS
------------------------------------------
Script exited with errors (code: 1).
Check the log at: /var/log/nym-bridge-helper.log
------------------------------------------
```
</AccordionTemplate>
### Fixing Metadata Port Showing Not Open in Probe Results
If you have followed the steps outlined above, but the metadata port is not shown as open in either the Node Status API's probe results or an explorer that gets its data from the API, see below:
<Steps>
**Run as root**
###### 1. Confirm correct `config.toml` private IPv4 address
Ensure that in your `config.toml` file, this value is set to the default one - any other value here will cause the metadata endpoint to fail:
```
# Private IP address of the wireguard gateway.
# default: '10.1.0.1'
private_ipv4 = '10.1.0.1'
```
Then restart your node.
###### 2. Open the needed port
Run this command if not already done:
```
ufw allow in on nymwg to any port 51830 proto tcp
```
Check if the port is open with:
```
iptables -S | grep 51830
```
Then ensure the metadata endpoint is listening from the correct address with:
```
netstat -an | egrep LISTEN | egrep "51830"
```
###### 3. Validate the setup
Once the Node Status API has run a probe on your node, the probe results will reflect this - `can_query_metadata_v4` will have `true` as a value.
The quickest way to check this is by using the [NymVPN API](https://nymvpn.com/api/public/v1/directory/gateways?show_vpn_only=true) and checking the same field:
![](/images/operators/wg-metadata-port-api.png)
</Steps>
@@ -5,7 +5,7 @@
},
"mixmining_reserve": {
"denom": "unym",
"amount": "178754510529387"
"amount": "180875972213757"
},
"vesting_tokens": {
"denom": "unym",
@@ -13,6 +13,6 @@
},
"circulating_supply": {
"denom": "unym",
"amount": "821245489470613"
"amount": "819124027786243"
}
}
@@ -1 +1 @@
821_245_489
819_124_027
@@ -1 +1 @@
60_303_169
60_147_392
@@ -1 +1 @@
60_303_168
60_147_391
@@ -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 | 178_754_510 |
| Mixmining Reserve | Tokens releasing for operators rewards | 180_875_972 |
| Vesting Tokens | Tokens locked outside of cicrulation for future claim | 0 |
| Circulating Supply | Amount of unlocked tokens | 821_245_489 |
| Stake Saturation | Optimal size of node self-bond + delegation | 251_263 |
| Circulating Supply | Amount of unlocked tokens | 819_124_027 |
| Stake Saturation | Optimal size of node self-bond + delegation | 250_614 |
@@ -1,10 +1,10 @@
{
"interval": {
"reward_pool": "178754510529387.687865580967403579",
"staking_supply": "60303168385204.800235781003503301",
"reward_pool": "180875972213757.135840914936141948",
"staking_supply": "60147391744900.170789956043539529",
"staking_supply_scale_factor": "0.07342892",
"epoch_reward_budget": "4965403070.260769107377249094",
"stake_saturation_point": "251263201605.02000098242084793",
"epoch_reward_budget": "5024332561.493253773358748226",
"stake_saturation_point": "250614132270.417378291483514748",
"sybil_resistance": "0.3",
"active_set_work_factor": "10",
"interval_pool_emission": "0.02"
@@ -1 +1 @@
Monday, November 10th 2025, 13:30:27 UTC
Sunday, October 5th 2025, 09:29:02 UTC
@@ -11,7 +11,7 @@ options:
--no_routing_history Display node stats without routing history
--no_verloc_metrics Display node stats without verloc metrics
-m, --markdown Display results in markdown format
-o [OUTPUT], --output [OUTPUT]
-o, --output [OUTPUT]
Save results to file (in current dir or supply with
path without filename)
```
@@ -18,23 +18,23 @@
| [Hostslick](https://hostslick.com) | Netherlands, Germany | Yes, on by default | Yes | Good amount of bandwidth for the price. Make sure you open the ticket if you want to run Exit node | 07/2024 |
| [Incognet](https://incognet.io) | Netherlands and USA | Yes, on by default | Yes | They allow Tor exit nodes but you must adhere to their rules https://incognet.io/tor-exits | 07/2024 |
| [Incognet](https://incognet.io/kansas-city-dedicated-servers) | USA, Netherlands | Yes | nan | nan | 07/2025 |
| [Ionos](https://www.ionos.com/servers/amd-servers) | USA, DE, UK, ESP, FR | nan | No | nan | 07/2025 |
| [Ionos](https://www.ionos.com/servers/amd-servers) | US, DE, UK, ESP, FR | nan | No | nan | 07/2025 |
| [IsHosting](https://ishosting.com/en) | Brazil, Netherlands | Yes, based on ticket | Yes | Expensive | 05/2024 |
| [Leaseweb](https://www.leaseweb.com/en/configure/vc/product/entityKey/DEDSER02_NEW_ORDER_BUSINESS_R740XD-24SFF-6134) | USA, NL, DE, UK, CA, SG, JP, AUS, HK | nan | No | KYC mandatory | 07/2025 |
| [Leaseweb](https://www.leaseweb.com/en/configure/vc/product/entityKey/DEDSER02_NEW_ORDER_BUSINESS_R740XD-24SFF-6134) | US, NL, DE, UK, CA, SG, JP, AUS, HK | nan | No | KYC mandatory | 07/2025 |
| [Linode](https://linode.com) | USA, Canada, Japan, India, Indonesia, Sweden, Netherlands, Germany, Brazil, France, UK, Australia, Italy | Yes out of the box | No, only through [BitLAunch](https://bitlaunch.io) | IPv6 sometimes need to be re-added in Networking tab, no reboot needed | 05/2024 |
| [LiteServer](https://liteserver.nl) | Netherlands | Yes, on by default | Yes | Very reliable Dutch provider. They do allow Relay nodes but for Exit nodes you need to contact them. Always check T&C https://liteserver.nl/legal | 07/2024 |
| [Lowendbox](https://lowendbox.com/category/dedicated-servers) | | | | Just an aggregator with good offers | 07/2025 |
| [M247](https://m247.com/eu/services/host/dedicated-servers/) | UK, Austria, Br, Sw, Jp, Poland, Fr, USA, Netherlands | Yes | No | nan | 07/2025 |
| [Mebilcom](https://www.melbicom.net/dedicatedserver/) | NL, USA, DE, UAE, NG, ESP, IN, IT, FR, LT, SG, BG, LV, PL | nan | No | nan | 07/2025 |
| [Mebilcom](https://www.melbicom.net/dedicatedserver/) | NL, US, DE, UAE, NG, ESP, IN, IT, FR, LT, SG, BG, LV, PL | nan | No | nan | 07/2025 |
| [Mevspace](https://mevspace.com) | Poland | Yes, on by default | Yes | Flexible Polish providers with 3 DCs in Poland. They do allow Tor Exit nodes but you may need a dedicated server for this. Make sure you open a ticket to check. As of today's date, they have 48h for 1 EUR tariff | 07/2024 |
| [Misaka](https://www.misaka.io/) | South Africa | Yes, native support | No | Very Expensive | 05/2024 |
| [NiceVPS](https://nicevps.net/) | Netherlands | Yes | nan | nan | 07/2025 |
| [Njalla](https://nja.la) | Sweden | Yes | Yes | Privacy vandguards! The biggest VPS 45 is 3 cores only, but it works better than many “larger” servers on the market. | 05/2024 |
| [OVH](https://us.ovhcloud.com/bare-metal/rise/rise-3/) | USA, DE, FR, UK, PL, CA | | No | Not all locations always available | 07/2025 |
| [Oneprovider](https://oneprovider.com/en/dedicated-servers/ipv6) | PL, FR, NL, UA, USA, BG, RO, DK, ESP, NO, CZ, RS, IE, IT, UK, HU, CH, SK, AT, BE, BA, HK, JP, SG, LU, AU, SWE, UAE, BR, CR, MX, GR, CL, MA, AR | Yes | No | nan | 07/2025 |
| [Oneprovider](https://oneprovider.com/en/dedicated-servers/ipv6) | PL, FR, NL, UA, US, BG, RO, DK, ESP, NO, CZ, RS, IE, IT, UK, HU, CH, SK, AT, BE, BA, HK, JP, SG, LU, AU, SWE, UAE, BR, CR, MX, GR, CL, MA, AR | Yes | No | nan | 07/2025 |
| [PrivateLayer](https://privatelayer.com) | Swiss | Yes | Yes | Slow customer response | 07/2025 |
| [Privex](https://www.privex.io/tor-exit-policy/) | USA, Germany, Sweden | Yes | Yes | nan | 07/2025 |
| [Psychz](https://www.psychz.net) | USA, UK, Brazil, Japan, Russia, South Africa and many more | Yes | nan | nan | 07/2025 |
| [Psychz](https://www.psychz.net) | US, UK, Brazil, Japan, Russia, South Africa and many more | Yes | nan | nan | 07/2025 |
| [RDP](https://rdp.sh) | Netherlands, USA, Poland | Yes, on by default | Yes | German provider. Exit nodes are allowed, policy is here https://rdp.sh/docs/faq/tor ports 25,465,587 must be closed. Make sure you open a ticket before running an exit node. | 07/2024 |
| [Servermania](https://www.servermania.com/dedicated-servers-hosting.htm) | USA, Canada | nan | No | nan | 07/2025 |
| [Svea](https://svea.net/vps) | Sweden | Yes | nan | nan | 07/2025 |
@@ -1,12 +0,0 @@
export const Green = ({ children }) => (
<span style={{ color: '#22C55E', fontWeight: 600 }}>{children}</span>
);
export const Yellow = ({ children }) => (
<span style={{ color: '#FACC15', fontWeight: 600 }}>{children}</span>
);
export const Red = ({ children }) => (
<span style={{ color: '#EF4444', fontWeight: 600 }}>{children}</span>
);
export const Gray = ({ children }) => (
<span style={{ color: '#6B7280', fontWeight: 600 }}>{children}</span>
);

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