Compare commits

..

42 Commits

Author SHA1 Message Date
Simon Wicky 9a38f1c3a6 parking branch 2026-05-29 15:07:59 +02:00
Simon Wicky fc79fe4738 trait rework, removed Ts and NdId generic 2026-05-28 14:09:36 +02:00
Simon Wicky 187c6a51fd nymnode pipeline in mix-sim pre trait rework, not optimal 2026-05-27 13:33:06 +02:00
Simon Wicky c93d106ca3 misc 2026-05-26 14:29:46 +02:00
Simon Wicky 5f1553d589 gateway_forwarding sphinx packet 2026-05-26 11:48:53 +02:00
Simon Wicky 258ceded26 back to separate pipelines 2026-05-22 15:47:23 +02:00
Simon Wicky be76065c66 tweak 2026-05-22 15:11:00 +02:00
Simon Wicky d2558d96e0 tiny fix 2026-05-22 11:53:29 +02:00
Simon Wicky 05ed775686 new addressing + pipeline unification and routing stubs 2026-05-22 11:49:37 +02:00
Simon Wicky c8f9959d7a stub gateway pipeline 2026-05-20 14:05:51 +02:00
Simon Wicky 8293870461 lock 2026-05-20 11:44:00 +02:00
Simon Wicky c0a8f97a20 different fragmentation 2026-05-20 11:42:32 +02:00
Simon Wicky 804b17517f tweak 2026-05-20 11:42:32 +02:00
Simon Wicky 2722544c86 lp metrics 2026-05-20 11:42:32 +02:00
Simon Wicky 732a09aa41 worker pool for processing 2026-05-20 11:42:32 +02:00
Simon Wicky e1c4085217 routing filter 2026-05-20 11:42:32 +02:00
Simon Wicky 34045d02b9 tweak 2026-05-20 11:42:31 +02:00
Simon Wicky b7a36373e5 pipeline unit test 2026-05-20 11:42:31 +02:00
Simon Wicky 17d16503a7 metrics 2026-05-20 11:42:31 +02:00
Simon Wicky df566933ba prepare multi threaded node 2026-05-20 11:42:31 +02:00
Simon Wicky f73f1a5219 sphinx and outfox processing 2026-05-20 11:42:31 +02:00
Simon Wicky 62a5d1437d tweak 2026-05-20 11:42:31 +02:00
Simon Wicky e952f9df24 clean reconstruction buffer 2026-05-20 11:42:31 +02:00
Simon Wicky 525e9314b4 prototype mixnode with dummy pipeline 2026-05-20 11:42:30 +02:00
Simon Wicky 8573004c34 rebasing cleanup 2026-05-20 11:38:35 +02:00
Simon Wicky 5636c5afc4 name change 2026-05-20 11:34:03 +02:00
Simon Wicky f505c29926 some PR review 2026-05-20 11:33:44 +02:00
Simon Wicky 95bec7422c tweaks and checked arithmetic 2026-05-20 11:33:31 +02:00
Simon Wicky c02c28f7cb add mut to transport layer 2026-05-20 11:33:31 +02:00
Simon Wicky 6fb4a98667 comments update 2026-05-20 11:33:30 +02:00
Simon Wicky 4a50f6dcd0 options in framing layer 2026-05-20 11:33:30 +02:00
Simon Wicky 53dec68378 remove anyhow error for in trait one 2026-05-20 11:33:30 +02:00
Simon Wicky f0ecdfd295 delete unnecessary unfinished type 2026-05-20 11:33:30 +02:00
Simon Wicky 668477c5c3 remove unnecessary imports 2026-05-20 11:33:30 +02:00
Simon Wicky 53aaa71178 cargo fmt 2026-05-20 11:33:30 +02:00
Simon Wicky 35517f1df6 nym-mix-sim crate 2026-05-20 11:33:29 +02:00
Simon Wicky ed5ddf0170 nym-lp-data crate 2026-05-20 11:33:14 +02:00
Simon Wicky 644e669a15 helper changes 2026-05-20 11:32:02 +02:00
Simon Wicky 1fd25529ce crate description 2026-05-20 11:28:36 +02:00
Simon Wicky 8677b98bcb fmt 2026-05-20 11:18:04 +02:00
Simon Wicky ca031af69a one more bit 2026-05-20 11:14:44 +02:00
Simon Wicky 7c0264b839 moving lp packets in lp-data crate 2026-05-20 11:10:46 +02:00
1288 changed files with 82897 additions and 88801 deletions
-2
View File
@@ -1,2 +0,0 @@
[target.wasm32-unknown-unknown]
rustflags = ["--cfg=getrandom_backend=\"wasm_js\""]
+2 -2
View File
@@ -23,10 +23,10 @@ jobs:
- name: Setup pnpm
uses: pnpm/action-setup@v5.0.0
with:
version: 11.1.2
version: 9
- uses: actions/setup-node@v4
with:
node-version: 24
node-version: 20
- name: Install Rust toolchain
uses: actions-rs/toolchain@v1
with:
+4 -7
View File
@@ -17,16 +17,13 @@ jobs:
run: sudo apt-get install rsync
continue-on-error: true
- uses: rlespinasse/github-slug-action@v3.x
- name: Setup pnpm
uses: pnpm/action-setup@v5.0.0
with:
version: 11.1.2
- uses: actions/setup-node@v4
with:
node-version: 24
cache: pnpm
node-version: 20
- name: Setup yarn
run: npm install -g yarn
- name: Build
run: pnpm install && pnpm build && pnpm build:ci:storybook
run: yarn && yarn build && yarn build:ci:storybook
- name: Deploy branch to CI www (storybook)
continue-on-error: true
uses: easingthemes/ssh-deploy@main
+1
View File
@@ -23,6 +23,7 @@ on:
- 'sdk/ffi/**'
- 'sdk/rust/**'
- 'service-providers/**'
- 'nym-browser-extension/storage/**'
- 'tools/**'
- 'wasm/**'
- 'Cargo.toml'
@@ -40,7 +40,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 24
node-version: "20"
- name: Validate version format
run: |
+1 -1
View File
@@ -42,7 +42,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 24
node-version: "20"
- name: Validate version format
run: |
+5 -11
View File
@@ -7,10 +7,7 @@ on:
paths:
- "documentation/docs/**"
- "sdk/typescript/packages/sdk/src/**"
- "sdk/typescript/packages/mix-tunnel/src/**"
- "sdk/typescript/packages/mix-fetch/src/**"
- "sdk/typescript/packages/mix-dns/src/**"
- "sdk/typescript/packages/mix-websocket/src/**"
- ".github/workflows/ci-docs.yml"
jobs:
@@ -33,10 +30,10 @@ jobs:
- name: Setup pnpm
uses: pnpm/action-setup@v5.0.0
with:
version: 11.1.2
version: 9
- uses: actions/setup-node@v4
with:
node-version: 24
node-version: 20
- name: Install Rust toolchain
uses: actions-rs/toolchain@v1
with:
@@ -50,7 +47,7 @@ jobs:
- name: Check if TypeScript SDK source changed
id: check-ts-sdk
run: |
if git diff --name-only ${{ github.event.before }} ${{ github.sha }} | grep -qE '^sdk/typescript/packages/(sdk|mix-tunnel|mix-fetch|mix-dns|mix-websocket)/src/'; then
if git diff --name-only ${{ github.event.before }} ${{ github.sha }} | grep -qE '^sdk/typescript/packages/(sdk|mix-fetch)/src/'; then
echo "changed=true" >> $GITHUB_OUTPUT
else
echo "changed=false" >> $GITHUB_OUTPUT
@@ -61,11 +58,8 @@ jobs:
if: steps.check-ts-sdk.outputs.changed == 'true'
run: |
npm install -g typedoc@0.25.13 typedoc-plugin-markdown@4.0.3
cd ${{ github.workspace }}/sdk/typescript/packages/sdk && typedoc --skipErrorChecking
cd ${{ github.workspace }}/sdk/typescript/packages/mix-tunnel && typedoc --skipErrorChecking
cd ${{ github.workspace }}/sdk/typescript/packages/mix-fetch && typedoc --skipErrorChecking
cd ${{ github.workspace }}/sdk/typescript/packages/mix-dns && typedoc --skipErrorChecking
cd ${{ github.workspace }}/sdk/typescript/packages/mix-websocket && typedoc --skipErrorChecking
cd ${{ github.workspace }}/sdk/typescript/packages/sdk && typedoc --skipErrorChecking
cd ${{ github.workspace }}/sdk/typescript/packages/mix-fetch && typedoc --skipErrorChecking
- name: Verify doc versions
run: ${{ github.workspace }}/documentation/scripts/verify-doc-versions.sh
+13 -17
View File
@@ -20,14 +20,12 @@ jobs:
- uses: actions/checkout@v6
- uses: rlespinasse/github-slug-action@v3.x
- name: Setup pnpm
uses: pnpm/action-setup@v5.0.0
with:
version: 11.1.2
- uses: actions/setup-node@v4
with:
node-version: 24
cache: pnpm
node-version: 20
- name: Setup yarn
run: npm install -g yarn
- name: Install Rust toolchain
uses: actions-rs/toolchain@v1
@@ -40,24 +38,22 @@ jobs:
- name: Install wasm-opt
run: cargo install wasm-opt
# Produce wasm/smolmix/pkg/package.json before any pnpm step. The
# `pnpm dev:on` in `prebuild:ci` adds wasm/smolmix/pkg to the dynamic
# workspace; mix-tunnel's `workspace:*` lookup against @nymproject/
# smolmix-wasm needs the package.json to be present.
- name: Build smolmix wasm
run: make -C wasm/smolmix
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version: "1.24.6"
- name: Install
run: pnpm i
run: yarn
- name: Build packages
run: pnpm build:ci
run: yarn build:ci
- name: Install again
run: pnpm i
run: yarn
- name: Lint
run: pnpm lint
run: yarn lint
- name: Typecheck with tsc
run: pnpm tsc
run: yarn tsc
+10 -15
View File
@@ -1,7 +1,6 @@
name: ci-nym-wallet-frontend
on:
workflow_dispatch:
pull_request:
paths:
- 'nym-wallet/**'
@@ -13,34 +12,30 @@ jobs:
steps:
- uses: actions/checkout@v6
- name: Setup pnpm
uses: pnpm/action-setup@v5.0.0
with:
version: 11.1.2
- uses: actions/setup-node@v4
with:
node-version-file: nym-wallet/.nvmrc
cache: pnpm
cache: yarn
cache-dependency-path: yarn.lock
- name: Install dependencies
run: pnpm install
run: yarn install --network-timeout 100000
- name: Build TypeScript packages (wallet depends on @nymproject/types, etc.)
run: pnpm build:types
run: yarn build:types
- name: Build @nymproject/mui-theme and @nymproject/react (wallet imports subpaths)
run: pnpm build:packages
run: yarn build:packages
- name: Typecheck nym-wallet
run: pnpm --filter @nymproject/nym-wallet-app tsc
run: yarn --cwd nym-wallet tsc
- name: Lint nym-wallet
run: pnpm --filter @nymproject/nym-wallet-app lint
run: yarn --cwd nym-wallet lint
- name: pnpm audit (workspace lockfile; informational)
run: pnpm audit --audit-level critical
- name: Yarn audit (workspace lockfile; informational)
run: yarn audit --level critical
continue-on-error: true
- name: Unit tests (nym-wallet)
run: pnpm --filter @nymproject/nym-wallet-app test
run: yarn --cwd nym-wallet test
+6 -1
View File
@@ -20,7 +20,7 @@ jobs:
- uses: actions/setup-node@v4
with:
node-version: 24
node-version: 20
- uses: actions-rs/toolchain@v1
with:
@@ -30,6 +30,11 @@ jobs:
override: true
components: rustfmt, clippy
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version: "1.24.6"
- name: Install wasm-pack
run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
+9 -12
View File
@@ -23,13 +23,10 @@ jobs:
steps:
- uses: actions/checkout@v6
- name: Setup pnpm
uses: pnpm/action-setup@v5.0.0
- name: Node
uses: actions/setup-node@v4
with:
version: 11.1.2
- uses: actions/setup-node@v4
with:
node-version: 24
node-version: 22.13.0
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
@@ -71,17 +68,17 @@ jobs:
fileName: '.env'
encodedString: ${{ secrets.WALLET_ADMIN_ADDRESS }}
- name: pnpm cache clean
- name: Yarn cache clean
shell: bash
run: cd .. && pnpm cache delete
run: cd .. && yarn cache clean
- name: Install project dependencies
shell: bash
run: cd .. && pnpm i
run: cd .. && yarn --network-timeout 100000
- name: Build
- name: Yarn build
shell: bash
run: cd .. && pnpm build
run: cd .. && yarn build
- name: Install dependencies and build it
env:
@@ -100,7 +97,7 @@ jobs:
TAURI_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
TAURI_NOTARIZATION_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
run: |
pnpm build-macx86
yarn build-macx86
- name: Create app tarball
run: |
@@ -26,17 +26,12 @@ jobs:
libwebkit2gtk-4.1-dev build-essential curl wget libssl-dev jq \
libgtk-3-dev squashfs-tools libayatana-appindicator3-dev make libfuse2 unzip librsvg2-dev file \
libsoup-3.0-dev libjavascriptcoregtk-4.1-dev
- name: Setup pnpm
uses: pnpm/action-setup@v5.0.0
with:
version: 11.1.2
- name: Node
uses: actions/setup-node@v4
with:
node-version: 24
cache: 'pnpm'
node-version: 22.13.0
cache: 'yarn'
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
@@ -45,10 +40,10 @@ jobs:
- name: Install project dependencies
shell: bash
run: cd .. && pnpm i
run: cd .. && yarn --network-timeout 100000
- name: Install app dependencies
run: pnpm i
run: yarn
- name: Create env file
uses: timheuer/base64-to-file@v1.2
@@ -57,7 +52,7 @@ jobs:
encodedString: ${{ secrets.WALLET_ADMIN_ADDRESS }}
- name: Build app
run: pnpm build
run: yarn build
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
+12 -9
View File
@@ -40,13 +40,16 @@ jobs:
- name: Setup MSBuild.exe
uses: microsoft/setup-msbuild@v3
- name: Setup pnpm
uses: pnpm/action-setup@v5.0.0
# No cache:yarn here: setup-node needs yarn on PATH to populate the cache, but this runner
# only gets yarn from the step below.
- name: Node
uses: actions/setup-node@v4
with:
version: 11.1.2
- uses: actions/setup-node@v4
with:
node-version: 24
node-version: 22.13.0
- name: Install Yarn (classic)
shell: bash
run: npm install -g yarn@1.22.22
- name: Strip Authenticode thumbprint (avoid signtool on runner)
working-directory: nym-wallet/src-tauri
@@ -115,11 +118,11 @@ jobs:
' tauri.conf.json
- name: Install project dependencies
shell: bash
run: cd .. && pnpm i
run: cd .. && yarn --network-timeout 100000
- name: Install app dependencies
shell: bash
run: pnpm i
run: yarn --network-timeout 100000
- name: Build and sign it
shell: bash
@@ -133,7 +136,7 @@ jobs:
SSL_COM_TOTP_SECRET: ${{ env.SIGN_WINDOWS == 'true' && secrets.SSL_COM_TOTP_SECRET }}
run: |
echo "Starting build process..."
pnpm build
yarn build
- name: Check bundle directory
shell: bash
+14 -23
View File
@@ -1,19 +1,6 @@
name: publish-sdk-npm
on:
workflow_dispatch:
inputs:
dry_run:
description: "Rehearse the publish (pnpm publish --dry-run, no tarballs uploaded). Untick to publish for real."
type: boolean
default: true
dist_tag:
description: "Tag mode. 'auto' picks per package: new packages and same-major releases -> latest; a breaking major (e.g. mix-fetch v2 over v1) -> next, promote later with `npm dist-tag add`. 'next'/'latest' force that tag on all four."
type: choice
options:
- auto
- next
- latest
default: auto
jobs:
publish:
@@ -21,17 +8,15 @@ jobs:
steps:
- uses: actions/checkout@v6
- name: Setup pnpm
uses: pnpm/action-setup@v5.0.0
with:
version: 11.1.2
- name: Install Node
uses: actions/setup-node@v4
with:
node-version: 24
node-version: 20
registry-url: "https://registry.npmjs.org"
- name: Setup yarn
run: npm install -g yarn
- name: Install rust toolchain
uses: actions-rs/toolchain@v1
with:
@@ -46,15 +31,21 @@ jobs:
- name: Install wasm-opt
run: cargo install wasm-opt
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version: "1.24.6"
- name: Update root CA certificate bundle
run: ./wasm/mix-fetch/go-mix-conn/scripts/update-root-certs.sh
- name: Install dependencies
run: pnpm i
run: yarn
- name: Build WASM and Typescript SDK
run: pnpm sdk:build
run: yarn sdk:build
- name: Publish to NPM
env:
NODE_AUTH_TOKEN: ${{ secrets.NODE_AUTH_TOKEN }}
DRY_RUN: ${{ inputs.dry_run && '1' || '0' }}
NPM_DIST_TAG: ${{ inputs.dist_tag }}
run: ./sdk/typescript/scripts/publish.sh
+1 -1
View File
@@ -23,7 +23,7 @@ jobs:
uses: actions/checkout@v6
- uses: actions/setup-node@v4
with:
node-version: 24
node-version: 20
- uses: nymtech/nym/.github/actions/nym-hash-releases@develop
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-7
View File
@@ -79,10 +79,3 @@ CLAUDE.md
/notes
/target-otel
test-tutorials/
# pnpm
.pnpm-store/
tmp/
# operator tools
scripts/nym-node-setup/auto-bond/nodes.csv
-9
View File
@@ -1,9 +0,0 @@
shamefully-hoist=false
prefer-workspace-packages=true
hoist-pattern[]=*eslint*
hoist-pattern[]=*prettier*
hoist-pattern[]=*typescript*
hoist-pattern[]=*@types*
auto-install-peers=true
strict-peer-dependencies=false
-134
View File
@@ -4,140 +4,6 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
## [Unreleased]
## [2026.11-xynomizithra] (2026-06-08)
- bugfix: allow re-inviting expired members ([#6863])
- feat: disable Nagle's algorithm for LP between nym-nodes ([#6857])
- Keep peer in wg table when updating psk ([#6856])
- chore: minor nym-node improvements ([#6850])
- chore: LP registration adjustments ([#6845])
- crates release: bump version to 1.21.1 ([#6844])
- fix gateways being penalised for no stress testing ([#6843])
- fix score inflation for throttled nodes ([#6842])
- Bugfix/cherry pick/waterloo stres testing floats ([#6841])
- bugfix: NMv3 race condition ([#6837])
- feat: implement UpdateFamily for the node families contract ([#6834])
- Bugfix/cherry pick/waterloo ns api ([#6833])
- experiment: attempt to retroactively generate specs for node families and ecash contracts ([#6813])
- moving lp packets in lp-data crate ([#6810])
- upgrade axum to 0.8.9 (and side deps) ([#6808])
- chore: expose admin method for migrating vesting delegations/mixnodes ([#6795])
- [chore] fix clippy 1.95 lints for future version update ([#6794])
- Handle Rate Limit Challenge Response ([#6786])
- NYM-583: Avoid corrupted database on Windows. ([#6785])
- Max/smolmix wasm ([#6784])
- Chore/bugfixes ([#6783])
- Switch from yarn to pnpm ([#6779])
- feat: Node Families: expose stake information inside DVpnGateway ([#6778])
- feat: Node Families: expose family information for NS API consumers ([#6777])
- feat: Node Families: cache and expose family data within nym API ([#6774])
- Re-order default API urls for network details ([#6767])
- add ci for NM agent binary ([#6764])
- feat/refactor: introduce shared contract caches within Nym API ([#6760])
- chore: removed dead code for redundant mixnet-vesting integration tests ([#6759])
- feat: Node Families: remove nodes upon unbonding ([#6752])
- feat: Node Families: contract transactions ([#6750])
- feat: Node Families: contract queries ([#6731])
- feat: Node Families: initial contract storage ([#6717])
- start node families topic branch ([#6715])
- Bump rand from 0.8.5 to 0.8.6 in /contracts ([#6702])
- Testing port checks in NS Agents ([#6694])
- build(deps): bump microsoft/setup-msbuild from 2 to 3 ([#6602])
- build(deps): bump tar from 0.4.44 to 0.4.45 ([#6595])
- build(deps): bump quinn-proto from 0.11.12 to 0.11.14 ([#6549])
- build(deps): bump docker/login-action from 3 to 4 ([#6518])
- build(deps): bump actions/download-artifact from 7 to 8 ([#6497])
- build(deps): bump actions/upload-artifact from 6 to 7 ([#6496])
[#6863]: https://github.com/nymtech/nym/pull/6863
[#6857]: https://github.com/nymtech/nym/pull/6857
[#6856]: https://github.com/nymtech/nym/pull/6856
[#6850]: https://github.com/nymtech/nym/pull/6850
[#6845]: https://github.com/nymtech/nym/pull/6845
[#6844]: https://github.com/nymtech/nym/pull/6844
[#6843]: https://github.com/nymtech/nym/pull/6843
[#6842]: https://github.com/nymtech/nym/pull/6842
[#6841]: https://github.com/nymtech/nym/pull/6841
[#6837]: https://github.com/nymtech/nym/pull/6837
[#6834]: https://github.com/nymtech/nym/pull/6834
[#6833]: https://github.com/nymtech/nym/pull/6833
[#6813]: https://github.com/nymtech/nym/pull/6813
[#6810]: https://github.com/nymtech/nym/pull/6810
[#6808]: https://github.com/nymtech/nym/pull/6808
[#6795]: https://github.com/nymtech/nym/pull/6795
[#6794]: https://github.com/nymtech/nym/pull/6794
[#6786]: https://github.com/nymtech/nym/pull/6786
[#6785]: https://github.com/nymtech/nym/pull/6785
[#6784]: https://github.com/nymtech/nym/pull/6784
[#6783]: https://github.com/nymtech/nym/pull/6783
[#6779]: https://github.com/nymtech/nym/pull/6779
[#6778]: https://github.com/nymtech/nym/pull/6778
[#6777]: https://github.com/nymtech/nym/pull/6777
[#6774]: https://github.com/nymtech/nym/pull/6774
[#6767]: https://github.com/nymtech/nym/pull/6767
[#6764]: https://github.com/nymtech/nym/pull/6764
[#6760]: https://github.com/nymtech/nym/pull/6760
[#6759]: https://github.com/nymtech/nym/pull/6759
[#6752]: https://github.com/nymtech/nym/pull/6752
[#6750]: https://github.com/nymtech/nym/pull/6750
[#6731]: https://github.com/nymtech/nym/pull/6731
[#6717]: https://github.com/nymtech/nym/pull/6717
[#6715]: https://github.com/nymtech/nym/pull/6715
[#6702]: https://github.com/nymtech/nym/pull/6702
[#6694]: https://github.com/nymtech/nym/pull/6694
[#6602]: https://github.com/nymtech/nym/pull/6602
[#6595]: https://github.com/nymtech/nym/pull/6595
[#6549]: https://github.com/nymtech/nym/pull/6549
[#6518]: https://github.com/nymtech/nym/pull/6518
[#6497]: https://github.com/nymtech/nym/pull/6497
[#6496]: https://github.com/nymtech/nym/pull/6496
## [2026.10-waterloo] (2026-05-27)
- Re-order default API urls for network details - Waterloo release ([#6799])
- [bugfix] IPR v8<->v9 mismatch on Waterloo ([#6772])
- Migrate to hickory 0.26.1 ([#6751])
- add workflows for NM3 ([#6729])
- credential proxy pool ([#6726])
- chore: made sphinx version threshold assertion a compile time check ([#6718])
- Feat/nmv3 updated performance calculation ([#6714])
- feat: NMv3: submission of stress testing result into nym-api ([#6709])
- feat: NMv3: Prometheus metrics for network monitor ([#6693])
- feat: NMv3: add read-only results API to orchestrator ([#6689])
- feat: NMv3: Eviction of stale testrun data ([#6685])
- feat: NMv3: Wire up testrun assignment and result submission flow ([#6680])
- feat: NMv3: Support multiple network monitor agents per host ([#6679])
- Feat/nmv3 agent announcement ([#6673])
- add node refresher for periodic scraping of bonded nym-node details ([#6626])
- Feat/nmv3 orchestrator queue ([#6597])
- feat: network monitor agent - standalone node stress-testing ([#6582])
- [feat] propagate NM agent noise keys to nym-node routing ([#6577])
- start mix stress testing topic branch ([#6575])
- Feat/nmv3 agents subscription ([#6567])
- Feat/nmv3 agents contract ([#6555])
[#6799]: https://github.com/nymtech/nym/pull/6799
[#6772]: https://github.com/nymtech/nym/pull/6772
[#6751]: https://github.com/nymtech/nym/pull/6751
[#6729]: https://github.com/nymtech/nym/pull/6729
[#6726]: https://github.com/nymtech/nym/pull/6726
[#6718]: https://github.com/nymtech/nym/pull/6718
[#6714]: https://github.com/nymtech/nym/pull/6714
[#6709]: https://github.com/nymtech/nym/pull/6709
[#6693]: https://github.com/nymtech/nym/pull/6693
[#6689]: https://github.com/nymtech/nym/pull/6689
[#6685]: https://github.com/nymtech/nym/pull/6685
[#6680]: https://github.com/nymtech/nym/pull/6680
[#6679]: https://github.com/nymtech/nym/pull/6679
[#6673]: https://github.com/nymtech/nym/pull/6673
[#6626]: https://github.com/nymtech/nym/pull/6626
[#6597]: https://github.com/nymtech/nym/pull/6597
[#6582]: https://github.com/nymtech/nym/pull/6582
[#6577]: https://github.com/nymtech/nym/pull/6577
[#6575]: https://github.com/nymtech/nym/pull/6575
[#6567]: https://github.com/nymtech/nym/pull/6567
[#6555]: https://github.com/nymtech/nym/pull/6555
## [2026.9-venaco] (2026-05-06)
- Fix for v9 IPR ([#6710])
Generated
+1937 -1880
View File
File diff suppressed because it is too large Load Diff
+139 -139
View File
@@ -44,7 +44,6 @@ members = [
"common/cosmwasm-smart-contracts/nym-performance-contract",
"common/cosmwasm-smart-contracts/nym-pool-contract",
"common/cosmwasm-smart-contracts/vesting-contract",
"common/cosmwasm-smart-contracts/network-monitors-contract",
"common/credential-proxy",
"common/credential-storage",
"common/credential-utils",
@@ -131,11 +130,13 @@ members = [
"nym-api",
"nym-api/nym-api-requests",
"nym-authenticator-client",
"nym-browser-extension/storage",
"nym-credential-proxy/nym-credential-proxy",
"nym-credential-proxy/nym-credential-proxy-requests",
"nym-data-observatory",
"nym-gateway-probe",
"nym-ip-packet-client",
"nym-mix-sim",
"nym-network-monitor",
"nym-node",
"nym-node-status-api/nym-node-status-agent",
@@ -174,11 +175,10 @@ members = [
"tools/nymvisor",
"tools/ts-rs-cli",
"wasm/client",
"wasm/smolmix",
# "wasm/full-nym-wasm", # If we uncomment this again, remember to also uncomment the profile settings below
"wasm/mix-fetch",
"wasm/node-tester",
"wasm/zknym-lib",
"nym-network-monitor-v3/nym-network-monitor-orchestrator",
"nym-network-monitor-v3/nym-network-monitor-agent",
"nym-network-monitor-v3/nym-network-monitor-orchestrator-requests",
]
default-members = [
@@ -187,6 +187,7 @@ default-members = [
"nym-api",
"nym-authenticator-client",
"nym-credential-proxy/nym-credential-proxy",
"nym-mix-sim",
"nym-node",
"nym-registration-client",
"nym-statistics-api",
@@ -196,8 +197,6 @@ default-members = [
"service-providers/network-requester",
"tools/internal/localnet-orchestrator",
"tools/nymvisor",
"nym-network-monitor-v3/nym-network-monitor-orchestrator",
"nym-network-monitor-v3/nym-network-monitor-agent",
]
exclude = ["contracts", "nym-wallet", "cpu-cycles"]
@@ -211,7 +210,7 @@ edition = "2024"
license = "Apache-2.0"
rust-version = "1.87.0"
readme = "README.md"
version = "1.21.1"
version = "1.21.0"
[workspace.dependencies]
addr = "0.15.6"
@@ -225,11 +224,10 @@ anyhow = "1.0.98"
arc-swap = "1.7.1"
argon2 = "0.5.0"
async-trait = "0.1.88"
async-tungstenite = { version = "0.24", default-features = false }
axum = "0.8.9"
axum-client-ip = "1.3.1"
axum-extra = "0.12.6"
axum-test = "20.0.0"
axum = "0.7.5"
axum-client-ip = "0.6.1"
axum-extra = "0.9.4"
axum-test = "16.2.0"
base64 = "0.22.1"
base85rs = "0.1.3"
bincode = "1.3.3"
@@ -254,7 +252,7 @@ clap_complete_fig = "4.5"
colored = "2.2"
comfy-table = "7.1.4"
console = "0.16.0"
console-subscriber = "0.5.0"
console-subscriber = "0.4.1"
console_error_panic_hook = "0.1"
const-str = "0.5.6"
const_format = "0.2.34"
@@ -279,27 +277,24 @@ eyre = "0.6.9"
fastrand = "2.1.1"
flate2 = "1.1.1"
futures = "0.3.31"
futures-rustls = { version = "0.26", default-features = false }
futures-util = "0.3"
generic-array = "0.14.7"
getrandom = "0.2.10"
getrandom03 = { package = "getrandom", version = "=0.3.3" }
getrandom04 = { package = "getrandom", version = "0.4" }
glob = "0.3"
handlebars = "3.5.5"
hex = "0.4.3"
hickory-proto = { version = "0.26.1", default-features = false }
hickory-proto = "0.26.1"
hickory-resolver = "0.26.1"
hkdf = "0.12.3"
hmac = "0.12.1"
http = "1"
http-body-util = "0.1"
httparse = "1.10"
httpcodec = "0.2.3"
human-repr = "1.1.0"
humantime = "2.2.0"
humantime-serde = "1.1.1"
hyper = { version = "1.6.0", default-features = false }
hyper = "1.6.0"
hyper-util = "0.1"
indicatif = "0.18.0"
inquire = "0.6.2"
@@ -345,14 +340,12 @@ regex = "1.10.6"
reqwest = { version = "0.13.1", default-features = false }
rs_merkle = "1.5.0"
rustls = { version = "0.23.37", default-features = false }
rustls-pki-types = "1"
rustls-rustcrypto = "0.0.2-alpha"
schemars = "0.8.22"
semver = "1.0.26"
serde = "1.0.219"
serde_bytes = "0.11.17"
serde_derive = "1.0"
serde_json = { version = "1.0.140", features = ["float_roundtrip"] }
serde_json = "1.0.140"
serde_json_path = "0.7.2"
serde_repr = "0.1"
serde_with = "3.9.0"
@@ -360,7 +353,6 @@ serde_yaml = "0.9.25"
serde_plain = "1.0.2"
sha2 = "0.10.3"
si-scale = "0.2.3"
simple-dns = "0.7"
smoltcp = "0.12"
snow = "0.9.6"
sphinx-packet = "=0.6.0"
@@ -369,7 +361,7 @@ strum = "0.28.0"
strum_macros = "0.28.0"
subtle-encoding = "0.5"
syn = "2"
sysinfo = "0.38.4"
sysinfo = "0.37.0"
tap = "1.0.1"
tar = "0.4.45"
test-with = { version = "0.15.4", default-features = false }
@@ -384,7 +376,7 @@ tokio-test = "0.4.4"
tokio-tun = "0.11.5"
tokio-rustls = "0.26"
tokio-smoltcp = "0.5"
tokio-tungstenite = "0.20.1"
tokio-tungstenite = { version = "0.20.1" }
tokio-util = "0.7.15"
toml = "0.8.22"
tower = "0.5.2"
@@ -402,20 +394,17 @@ uniffi = "0.29.2"
uniffi_build = "0.29.0"
url = "2.5"
utoipa = "5.2"
utoipa-swagger-ui = "9.0.2"
utoipa-swagger-ui = "8.1"
utoipauto = "0.2"
uuid = "1.19.0"
vergen = { version = "=8.3.1", default-features = false }
vergen-gitcl = { version = "1.0.8", default-features = false }
walkdir = "2"
x25519-dalek = "2.0.0"
zeroize = "1.7.0"
prometheus = { version = "0.14.0" }
# recreating lioness
# we don't care about particular versions - just pull whatever is used by sphinx
lioness = "*"
arrayref = "*"
# libcrux
libcrux-kem = "0.0.7"
@@ -428,115 +417,114 @@ libcrux-sha3 = "0.0.8"
libcrux-traits = "0.0.6"
# Workspace dep definitions required by crates.io publication - we need a workspace version since `cargo workspaces` doesn't work with path imports from crate manifests
nym-api-requests = { version = "1.21.1", path = "nym-api/nym-api-requests" }
nym-authenticator-requests = { version = "1.21.1", path = "common/authenticator-requests" }
nym-async-file-watcher = { version = "1.21.1", path = "common/async-file-watcher" }
nym-authenticator-client = { version = "1.21.1", path = "nym-authenticator-client" }
nym-bandwidth-controller = { version = "1.21.1", path = "common/bandwidth-controller" }
nym-bin-common = { version = "1.21.1", path = "common/bin-common" }
nym-cache = { version = "1.21.1", path = "common/nym-cache" }
nym-client-core = { version = "1.21.1", path = "common/client-core", default-features = false }
nym-client-core-config-types = { version = "1.21.1", path = "common/client-core/config-types" }
nym-client-core-gateways-storage = { version = "1.21.1", path = "common/client-core/gateways-storage" }
nym-client-core-surb-storage = { version = "1.21.1", path = "common/client-core/surb-storage" }
nym-client-websocket-requests = { version = "1.21.1", path = "clients/native/websocket-requests" }
nym-common = { version = "1.21.1", path = "common/nym-common" }
nym-compact-ecash = { version = "1.21.1", path = "common/nym_offline_compact_ecash" }
nym-config = { version = "1.21.1", path = "common/config" }
nym-contracts-common = { version = "1.21.1", path = "common/cosmwasm-smart-contracts/contracts-common" }
nym-coconut-dkg-common = { version = "1.21.1", path = "common/cosmwasm-smart-contracts/coconut-dkg" }
nym-credential-storage = { version = "1.21.1", path = "common/credential-storage" }
nym-credential-utils = { version = "1.21.1", path = "common/credential-utils" }
nym-credential-proxy-lib = { version = "1.21.1", path = "common/credential-proxy" }
nym-credentials = { version = "1.21.1", path = "common/credentials", default-features = false }
nym-credentials-interface = { version = "1.21.1", path = "common/credentials-interface" }
nym-credential-proxy-requests = { version = "1.21.1", path = "nym-credential-proxy/nym-credential-proxy-requests", default-features = false }
nym-credential-verification = { version = "1.21.1", path = "common/credential-verification" }
nym-crypto = { version = "1.21.1", path = "common/crypto", default-features = false }
nym-dkg = { version = "1.21.1", path = "common/dkg" }
nym-ecash-contract-common = { version = "1.21.1", path = "common/cosmwasm-smart-contracts/ecash-contract" }
nym-ecash-signer-check = { version = "1.21.1", path = "common/ecash-signer-check" }
nym-ecash-signer-check-types = { version = "1.21.1", path = "common/ecash-signer-check-types" }
nym-ecash-time = { version = "1.21.1", path = "common/ecash-time" }
nym-exit-policy = { version = "1.21.1", path = "common/exit-policy" }
nym-ffi-shared = { version = "1.21.1", path = "sdk/ffi/shared" }
nym-gateway-client = { version = "1.21.1", path = "common/client-libs/gateway-client", default-features = false }
nym-gateway-probe = { version = "1.21.1", path = "nym-gateway-probe" }
nym-gateway-requests = { version = "1.21.1", path = "common/gateway-requests" }
nym-gateway-storage = { version = "1.21.1", path = "common/gateway-storage" }
nym-gateway-stats-storage = { version = "1.21.1", path = "common/gateway-stats-storage" }
nym-group-contract-common = { version = "1.21.1", path = "common/cosmwasm-smart-contracts/group-contract" }
nym-http-api-client = { version = "1.21.1", path = "common/http-api-client" }
nym-http-api-client-macro = { version = "1.21.1", path = "common/http-api-client-macro" }
nym-http-api-common = { version = "1.21.1", path = "common/http-api-common", default-features = false }
nym-id = { version = "1.21.1", path = "common/nym-id" }
nym-ip-packet-client = { version = "1.21.1", path = "nym-ip-packet-client" }
nym-ip-packet-requests = { version = "1.21.1", path = "common/ip-packet-requests" }
nym-lp = { version = "1.21.1", path = "common/nym-lp" }
nym-lp-data = { version = "1.21.1", path = "common/nym-lp-data" }
nym-kkt = { version = "1.21.1", path = "common/nym-kkt" }
nym-kkt-ciphersuite = { version = "1.21.1", path = "common/nym-kkt-ciphersuite" }
nym-kkt-context = { version = "1.21.1", path = "common/nym-kkt-context" }
nym-metrics = { version = "1.21.1", path = "common/nym-metrics" }
nym-mixnet-client = { version = "1.21.1", path = "common/client-libs/mixnet-client" }
nym-mixnet-contract-common = { version = "1.21.1", path = "common/cosmwasm-smart-contracts/mixnet-contract" }
nym-multisig-contract-common = { version = "1.21.1", path = "common/cosmwasm-smart-contracts/multisig-contract" }
nym-network-defaults = { version = "1.21.1", path = "common/network-defaults" }
nym-node-tester-utils = { version = "1.21.1", path = "common/node-tester-utils" }
nym-noise = { version = "1.21.1", path = "common/nymnoise" }
nym-noise-keys = { version = "1.21.1", path = "common/nymnoise/keys" }
nym-nonexhaustive-delayqueue = { version = "1.21.1", path = "common/nonexhaustive-delayqueue" }
nym-node-requests = { version = "1.21.1", path = "nym-node/nym-node-requests", default-features = false }
nym-node-metrics = { version = "1.21.1", path = "nym-node/nym-node-metrics" }
nym-node-families-contract-common = { version = "1.21.1", path = "common/cosmwasm-smart-contracts/node-families-contract" }
nym-ordered-buffer = { version = "1.21.1", path = "common/socks5/ordered-buffer" }
nym-outfox = { version = "1.21.1", path = "nym-outfox" }
nym-registration-common = { version = "1.21.1", path = "common/registration" }
nym-pemstore = { version = "1.21.1", path = "common/pemstore" }
nym-performance-contract-common = { version = "1.21.1", path = "common/cosmwasm-smart-contracts/nym-performance-contract" }
nym-sdk = { version = "1.21.1", path = "sdk/rust/nym-sdk" }
nym-serde-helpers = { version = "1.21.1", path = "common/serde-helpers" }
nym-service-providers-common = { version = "1.21.1", path = "service-providers/common" }
nym-service-provider-requests-common = { version = "1.21.1", path = "common/service-provider-requests-common" }
nym-socks5-client-core = { version = "1.21.1", path = "common/socks5-client-core" }
nym-socks5-proxy-helpers = { version = "1.21.1", path = "common/socks5/proxy-helpers" }
nym-socks5-requests = { version = "1.21.1", path = "common/socks5/requests" }
nym-sphinx = { version = "1.21.1", path = "common/nymsphinx" }
nym-sphinx-acknowledgements = { version = "1.21.1", path = "common/nymsphinx/acknowledgements" }
nym-sphinx-addressing = { version = "1.21.1", path = "common/nymsphinx/addressing" }
nym-sphinx-anonymous-replies = { version = "1.21.1", path = "common/nymsphinx/anonymous-replies" }
nym-sphinx-chunking = { version = "1.21.1", path = "common/nymsphinx/chunking" }
nym-sphinx-cover = { version = "1.21.1", path = "common/nymsphinx/cover" }
nym-sphinx-forwarding = { version = "1.21.1", path = "common/nymsphinx/forwarding" }
nym-sphinx-framing = { version = "1.21.1", path = "common/nymsphinx/framing" }
nym-sphinx-params = { version = "1.21.1", path = "common/nymsphinx/params" }
nym-sphinx-routing = { version = "1.21.1", path = "common/nymsphinx/routing" }
nym-sphinx-types = { version = "1.21.1", path = "common/nymsphinx/types" }
nym-statistics-common = { version = "1.21.1", path = "common/statistics" }
nym-store-cipher = { version = "1.21.1", path = "common/store-cipher" }
nym-task = { version = "1.21.1", path = "common/task" }
nym-tun = { version = "1.21.1", path = "common/tun" }
nym-test-utils = { version = "1.21.1", path = "common/test-utils" }
nym-ticketbooks-merkle = { version = "1.21.1", path = "common/ticketbooks-merkle" }
nym-topology = { version = "1.21.1", path = "common/topology" }
nym-types = { version = "1.21.1", path = "common/types" }
nym-upgrade-mode-check = { version = "1.21.1", path = "common/upgrade-mode-check" }
nym-validator-client = { version = "1.21.1", path = "common/client-libs/validator-client", default-features = false }
nym-vesting-contract-common = { version = "1.21.1", path = "common/cosmwasm-smart-contracts/vesting-contract" }
nym-network-monitors-contract-common = { version = "1.21.1", path = "common/cosmwasm-smart-contracts/network-monitors-contract" }
nym-verloc = { version = "1.21.1", path = "common/verloc" }
nym-wireguard = { version = "1.21.1", path = "common/wireguard" }
nym-wireguard-types = { version = "1.21.1", path = "common/wireguard-types" }
nym-wireguard-private-metadata-shared = { version = "1.21.1", path = "common/wireguard-private-metadata/shared" }
nym-wireguard-private-metadata-client = { version = "1.21.1", path = "common/wireguard-private-metadata/client" }
nym-wireguard-private-metadata-server = { version = "1.21.1", path = "common/wireguard-private-metadata/server" }
nym-sqlx-pool-guard = { version = "1.21.1", path = "nym-sqlx-pool-guard" }
nym-wasm-client-core = { version = "1.21.1", path = "common/wasm/client-core" }
nym-wasm-storage = { version = "1.21.1", path = "common/wasm/storage" }
nym-wasm-utils = { version = "1.21.1", path = "common/wasm/utils", default-features = false }
nyxd-scraper-shared = { version = "1.21.1", path = "common/nyxd-scraper-shared" }
nym-api-requests = { version = "1.21.0", path = "nym-api/nym-api-requests" }
nym-authenticator-requests = { version = "1.21.0", path = "common/authenticator-requests" }
nym-async-file-watcher = { version = "1.21.0", path = "common/async-file-watcher" }
nym-authenticator-client = { version = "1.21.0", path = "nym-authenticator-client" }
nym-bandwidth-controller = { version = "1.21.0", path = "common/bandwidth-controller" }
nym-bin-common = { version = "1.21.0", path = "common/bin-common" }
nym-cache = { version = "1.21.0", path = "common/nym-cache" }
nym-client-core = { version = "1.21.0", path = "common/client-core", default-features = false }
nym-client-core-config-types = { version = "1.21.0", path = "common/client-core/config-types" }
nym-client-core-gateways-storage = { version = "1.21.0", path = "common/client-core/gateways-storage" }
nym-client-core-surb-storage = { version = "1.21.0", path = "common/client-core/surb-storage" }
nym-client-websocket-requests = { version = "1.21.0", path = "clients/native/websocket-requests" }
nym-common = { version = "1.21.0", path = "common/nym-common" }
nym-compact-ecash = { version = "1.21.0", path = "common/nym_offline_compact_ecash" }
nym-config = { version = "1.21.0", path = "common/config" }
nym-contracts-common = { version = "1.21.0", path = "common/cosmwasm-smart-contracts/contracts-common" }
nym-coconut-dkg-common = { version = "1.21.0", path = "common/cosmwasm-smart-contracts/coconut-dkg" }
nym-credential-storage = { version = "1.21.0", path = "common/credential-storage" }
nym-credential-utils = { version = "1.21.0", path = "common/credential-utils" }
nym-credential-proxy-lib = { version = "1.21.0", path = "common/credential-proxy" }
nym-credentials = { version = "1.21.0", path = "common/credentials", default-features = false }
nym-credentials-interface = { version = "1.21.0", path = "common/credentials-interface" }
nym-credential-proxy-requests = { version = "1.21.0", path = "nym-credential-proxy/nym-credential-proxy-requests", default-features = false }
nym-credential-verification = { version = "1.21.0", path = "common/credential-verification" }
nym-crypto = { version = "1.21.0", path = "common/crypto", default-features = false }
nym-dkg = { version = "1.21.0", path = "common/dkg" }
nym-ecash-contract-common = { version = "1.21.0", path = "common/cosmwasm-smart-contracts/ecash-contract" }
nym-ecash-signer-check = { version = "1.21.0", path = "common/ecash-signer-check" }
nym-ecash-signer-check-types = { version = "1.21.0", path = "common/ecash-signer-check-types" }
nym-ecash-time = { version = "1.21.0", path = "common/ecash-time" }
nym-exit-policy = { version = "1.21.0", path = "common/exit-policy" }
nym-ffi-shared = { version = "1.21.0", path = "sdk/ffi/shared" }
nym-gateway-client = { version = "1.21.0", path = "common/client-libs/gateway-client", default-features = false }
nym-gateway-probe = { version = "1.18.0", path = "nym-gateway-probe" }
nym-gateway-requests = { version = "1.21.0", path = "common/gateway-requests" }
nym-gateway-storage = { version = "1.21.0", path = "common/gateway-storage" }
nym-gateway-stats-storage = { version = "1.21.0", path = "common/gateway-stats-storage" }
nym-group-contract-common = { version = "1.21.0", path = "common/cosmwasm-smart-contracts/group-contract" }
nym-http-api-client = { version = "1.21.0", path = "common/http-api-client" }
nym-http-api-client-macro = { version = "1.21.0", path = "common/http-api-client-macro" }
nym-http-api-common = { version = "1.21.0", path = "common/http-api-common", default-features = false }
nym-id = { version = "1.21.0", path = "common/nym-id" }
nym-ip-packet-client = { version = "1.21.0", path = "nym-ip-packet-client" }
nym-ip-packet-requests = { version = "1.21.0", path = "common/ip-packet-requests" }
nym-lp = { version = "1.21.0", path = "common/nym-lp" }
nym-lp-data = { version = "1.21.0", path = "common/nym-lp-data" }
nym-kkt = { version = "1.21.0", path = "common/nym-kkt" }
nym-kkt-ciphersuite = { version = "1.21.0", path = "common/nym-kkt-ciphersuite" }
nym-kkt-context = { version = "1.21.0", path = "common/nym-kkt-context" }
nym-metrics = { version = "1.21.0", path = "common/nym-metrics" }
nym-mixnet-client = { version = "1.21.0", path = "common/client-libs/mixnet-client" }
nym-mixnet-contract-common = { version = "1.21.0", path = "common/cosmwasm-smart-contracts/mixnet-contract" }
nym-multisig-contract-common = { version = "1.21.0", path = "common/cosmwasm-smart-contracts/multisig-contract" }
nym-network-defaults = { version = "1.21.0", path = "common/network-defaults" }
nym-node-tester-utils = { version = "1.21.0", path = "common/node-tester-utils" }
nym-noise = { version = "1.21.0", path = "common/nymnoise" }
nym-noise-keys = { version = "1.21.0", path = "common/nymnoise/keys" }
nym-nonexhaustive-delayqueue = { version = "1.21.0", path = "common/nonexhaustive-delayqueue" }
nym-node-requests = { version = "1.21.0", path = "nym-node/nym-node-requests", default-features = false }
nym-node-metrics = { version = "1.21.0", path = "nym-node/nym-node-metrics" }
nym-node-families-contract-common = { version = "1.21.0", path = "common/cosmwasm-smart-contracts/node-families-contract" }
nym-ordered-buffer = { version = "1.21.0", path = "common/socks5/ordered-buffer" }
nym-outfox = { version = "1.21.0", path = "nym-outfox" }
nym-registration-common = { version = "1.21.0", path = "common/registration" }
nym-pemstore = { version = "1.21.0", path = "common/pemstore" }
nym-performance-contract-common = { version = "1.21.0", path = "common/cosmwasm-smart-contracts/nym-performance-contract" }
nym-sdk = { version = "1.21.0", path = "sdk/rust/nym-sdk" }
nym-serde-helpers = { version = "1.21.0", path = "common/serde-helpers" }
nym-service-providers-common = { version = "1.21.0", path = "service-providers/common" }
nym-service-provider-requests-common = { version = "1.21.0", path = "common/service-provider-requests-common" }
nym-socks5-client-core = { version = "1.21.0", path = "common/socks5-client-core" }
nym-socks5-proxy-helpers = { version = "1.21.0", path = "common/socks5/proxy-helpers" }
nym-socks5-requests = { version = "1.21.0", path = "common/socks5/requests" }
nym-sphinx = { version = "1.21.0", path = "common/nymsphinx" }
nym-sphinx-acknowledgements = { version = "1.21.0", path = "common/nymsphinx/acknowledgements" }
nym-sphinx-addressing = { version = "1.21.0", path = "common/nymsphinx/addressing" }
nym-sphinx-anonymous-replies = { version = "1.21.0", path = "common/nymsphinx/anonymous-replies" }
nym-sphinx-chunking = { version = "1.21.0", path = "common/nymsphinx/chunking" }
nym-sphinx-cover = { version = "1.21.0", path = "common/nymsphinx/cover" }
nym-sphinx-forwarding = { version = "1.21.0", path = "common/nymsphinx/forwarding" }
nym-sphinx-framing = { version = "1.21.0", path = "common/nymsphinx/framing" }
nym-sphinx-params = { version = "1.21.0", path = "common/nymsphinx/params" }
nym-sphinx-routing = { version = "1.21.0", path = "common/nymsphinx/routing" }
nym-sphinx-types = { version = "1.21.0", path = "common/nymsphinx/types" }
nym-statistics-common = { version = "1.21.0", path = "common/statistics" }
nym-store-cipher = { version = "1.21.0", path = "common/store-cipher" }
nym-task = { version = "1.21.0", path = "common/task" }
nym-tun = { version = "1.21.0", path = "common/tun" }
nym-test-utils = { version = "1.21.0", path = "common/test-utils" }
nym-ticketbooks-merkle = { version = "1.21.0", path = "common/ticketbooks-merkle" }
nym-topology = { version = "1.21.0", path = "common/topology" }
nym-types = { version = "1.21.0", path = "common/types" }
nym-upgrade-mode-check = { version = "1.21.0", path = "common/upgrade-mode-check" }
nym-validator-client = { version = "1.21.0", path = "common/client-libs/validator-client", default-features = false }
nym-vesting-contract-common = { version = "1.21.0", path = "common/cosmwasm-smart-contracts/vesting-contract" }
nym-verloc = { version = "1.21.0", path = "common/verloc" }
nym-wireguard = { version = "1.21.0", path = "common/wireguard" }
nym-wireguard-types = { version = "1.21.0", path = "common/wireguard-types" }
nym-wireguard-private-metadata-shared = { version = "1.21.0", path = "common/wireguard-private-metadata/shared" }
nym-wireguard-private-metadata-client = { version = "1.21.0", path = "common/wireguard-private-metadata/client" }
nym-wireguard-private-metadata-server = { version = "1.21.0", path = "common/wireguard-private-metadata/server" }
nym-sqlx-pool-guard = { version = "1.2.0", path = "nym-sqlx-pool-guard" }
nym-wasm-client-core = { version = "1.21.0", path = "common/wasm/client-core" }
nym-wasm-storage = { version = "1.21.0", path = "common/wasm/storage" }
nym-wasm-utils = { version = "1.21.0", path = "common/wasm/utils", default-features = false }
nyxd-scraper-shared = { version = "1.21.0", path = "common/nyxd-scraper-shared" }
smolmix = { version = "1.21.1", path = "smolmix/core" }
smolmix = { version = "1.21.0", path = "smolmix/core" }
# coconut/DKG related
# unfortunately until https://github.com/zkcrypto/nym-bls12_381-fork/issues/10 is resolved, we have to rely on the fork
@@ -603,7 +591,18 @@ opt-level = 3
# lto = true
opt-level = 'z'
[profile.release.package.smolmix-wasm]
[profile.release.package.nym-node-tester-wasm]
# lto = true
opt-level = 'z'
# Commented out since the crate is also commented out from the inclusion in the
# workspace above. We should uncomment this if we re-include it in the
# workspace
#[profile.release.package.nym-wasm-sdk]
## lto = true
#opt-level = 'z'
[profile.release.package.mix-fetch-wasm]
# lto = true
opt-level = 'z'
@@ -624,3 +623,4 @@ exit = "deny"
panic = "deny"
unimplemented = "deny"
unreachable = "deny"
+11 -11
View File
@@ -104,30 +104,30 @@ $(eval $(call add_cargo_workspace,wallet,nym-wallet))
sdk-wasm: sdk-wasm-build sdk-wasm-test sdk-wasm-lint
sdk-wasm-build:
# $(MAKE) -C nym-browser-extension/storage wasm-pack
$(MAKE) -C wasm/client
$(MAKE) -C wasm/smolmix
$(MAKE) -C wasm/node-tester
$(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
#
# `mix-tunnel` must build before the three feature packages — they import it
# via `workspace:*` and the lerna topological sort will respect that as long
# as we keep them in the same `--scope` invocation.
sdk-typescript-build:
npx lerna run --scope @nymproject/sdk build --stream
npx lerna run --scope '{@nymproject/mix-tunnel,@nymproject/mix-fetch,@nymproject/mix-dns,@nymproject/mix-websocket}' build --stream
pnpm --pwd 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 = nym-client-wasm
# WASM_CRATES = extension-storage nym-client-wasm nym-node-tester-wasm zknym-lib
WASM_CRATES = nym-client-wasm nym-node-tester-wasm
sdk-wasm-test:
#cargo test $(addprefix -p , $(WASM_CRATES)) --target wasm32-unknown-unknown -- -Dwarnings
sdk-wasm-lint:
RUSTFLAGS='--cfg getrandom_backend="wasm_js"' cargo clippy $(addprefix -p , $(WASM_CRATES)) --target wasm32-unknown-unknown -- -Dwarnings
$(MAKE) -C wasm/smolmix check-fmt
$(MAKE) -C wasm/mix-fetch check-fmt
# Add to top-level targets
build: sdk-wasm-build
@@ -223,7 +223,7 @@ build-nym-cli:
generate-typescript:
cd tools/ts-rs-cli && cargo run && cd ../..
pnpm types:lint:fix
yarn types:lint:fix
# Run the integration tests for public nym-api endpoints
run-api-tests:
+2 -2
View File
@@ -74,9 +74,9 @@ Nym Node Operators and Validators Terms and Conditions can be found [here](https
## Getting Started
```bash
pnpm install
yarn install
```
```bash
pnpm build
yarn build
```
-8
View File
@@ -1,8 +0,0 @@
---
- name: Nym node auto-bonding
hosts: all
gather_facts: false
serial: 1
roles:
- role: postinstall-auto
+40 -30
View File
@@ -1,4 +1,21 @@
---
ansible_ssh_private_key_file: ~/.ssh/<SSH_KEY>
cli_url: "https://github.com/nymtech/nym/releases/download/nym-binaries-{{ nym_version }}/nym-cli"
tunnel_manager_url: "https://github.com/nymtech/nym/raw/refs/heads/develop/scripts/nym-node-setup/network-tunnel-manager.sh"
quic_bridge_deployment_url: "https://raw.githubusercontent.com/nymtech/nym/refs/heads/develop/scripts/nym-node-setup/quic_bridge_deployment.sh"
###############################################################################
## GLOBAL VARS
## These values will be used globally unless overwritten per node in inventory/all
###############################################################################
ansible_user: root # used for ssh, like `ssh root@nym-exit.ch-1.mynodes.net`
email: "<EMAIL>" # used in certbot, description.toml and landing page
website: "<WEBSITE>" # it is used in the description.toml
description: "<NODE_PUBLIC_DESCRIPTION>" # or define per node in inventory/all
# operator_name: "<OPERATOR_NAME>" # used in landing page if provided
###############################################################################
## GLOBAL VARS
## These values will be used globally unless overwritten per node in inventory/all
@@ -6,41 +23,16 @@
## Per node changes in inventory/all will overwrite these global vars
###############################################################################
## MANDATORY - uncomment & define
## --SSH--
#ansible_user: root # used for ssh, like `ssh root@nym-exit.ch-1.mynodes.net`
# ansible_ssh_private_key_file: ~/.ssh/<SSH_KEY>
## --Operator info--
# email: "<EMAIL>" # used in certbot, description.toml and landing page
# website: "<WEBSITE>" # it is used in the description.toml
# description: "<NODE_PUBLIC_DESCRIPTION>" # or define per node in inventory/all
# moniker: "<MONIKER>"
## --Node defaults (can override per node in inventory/all)--
# accept_operator_terms: true # controls --accept-operator-terms-and-conditions, read: https://nym.com/docs/operators/nodes/nym-node/setup#terms--conditions
# mode: exit-gateway # entry-gateway/exit-gateway/mixnode
# wireguard_enabled: true # true/false
hostname: "" # keep this fallback, keep it and setup hostname per node in inventory/all
## OPTIONAL - uncomment & define
# operator_name: "<OPERATOR_NAME>" # used in landing page if provided
# nym_version: "nym-binaries-v2026.7-tola" # to use particular version instead of Latest, provide in such form:
## alternative SSH key var setting, instead of a hardcoded path
## useful if the playbook is shared in a repo by more admins with each having own local key
# ansible_ssh_private_key_file: "{{ lookup('env', '<YOUR_ANSIBLE SSH_KEY_ENV_VAR>') }}"
# moniker: "<MONIKER>" # if not setup here not in inventory/all it get's derived from the hostname
# mode: <MODE> # entry-gateway/exit-gateway/mixnode
# wireguard_enabled: <WIREGUARD_ENABLED> # true/false
hostname: "" # this is a fallback, keep it and setup hostname per node in inventory/all
###############################################################################
## GLOBAL PACKAGES & URLs
## GLOBAL PACKAGES
## These will be installed during deployment
###############################################################################
nym_cli_url: "https://github.com/nymtech/nym/releases/download/{{ nym_version }}/nym-cli"
tunnel_manager_url: "https://github.com/nymtech/nym/raw/refs/heads/develop/scripts/nym-node-setup/network-tunnel-manager.sh"
quic_bridge_deployment_url: "https://raw.githubusercontent.com/nymtech/nym/refs/heads/develop/scripts/nym-node-setup/quic_bridge_deployment.sh"
packages:
- tmux
@@ -58,6 +50,24 @@ packages:
- ufw
###############################################################################
## OPTIONAL OVERRIDES
## All values below already have defaults in the playbook/roles
## Uncomment only if you want to override them
###############################################################################
###############################################################################
## SYSTEM MAINTENANCE PLAYBOOK KNOBS
###############################################################################
# To use particular version instead of Latest, provide in such form:
# nym_version: "nym-binaries-v2026.7-tola"
## NOTE:
## if you want to pin Nym to a specific version instead of using the
## latest release from GitHub in /tasks/main.yml then
## uncomment the line above and set the tag
###############################################################################
## SYSTEM MAINTENANCE PLAYBOOK KNOBS
###############################################################################
+23 -28
View File
@@ -1,39 +1,34 @@
[nym_nodes]
## READ CONFIGURATION GUIDE:
## https://nym.com/docs/operators/orchestration/ansible#configuration
# READ CONFIGURATION GUIDE:
# https://nym.com//docs/operators/orchestration/ansible#configuration
##############
## TEMPLATE ##
##############
## uncomment and exchange the <VARIABLES> with your real values for each node without the <> brackets
# VARIABLES INFO
# required vars to set values per node:
# `ansible_host`, `hostname`, `location`
# global vars can be set in the group_vars/all.yml, for example:
# `email`, `ansible_user`, `moniker`, `description`, `mode`, `wireguard_enabled`
# othersise they must be set per node!
############
# TEMPLATE #
############
# node1 ansible_host=<YOUR_SERVER_IP> ansible_user=<USER> hostname=<HOSTNAME> location=<LOCATION> email=<EMAIL> mode=<MODE> wireguard_enabled=<true/false> moniker=<MONIKER> description=<DESCRIPTION>
####################
## VARIABLES INFO ##
####################
# remove all comments and exchange the <VARIABLES> with your real values for each node
# without <> brackets
## --REQUIRED VARS--
## required per node:
## ansible_host, hostname, location
# PRIORITY ORDER
# anything setup globaly can be overwritten in this file per node
# if provided here, it takes priority over the global setting
## --OPTIONAL VARS--
## can be set in the group_vars/all.yml or per node here:
## email, ansible_user, moniker, description, mode, wireguard_enabled
## --PRIORITY ORDER--
## anything setup globaly can be overwritten in this file per node
## if provided here, it takes priority over the global setting
## --EXAMPLES--
## exit + wireguard gateway:
# EXAMPLES
# exit + wireguard gateway:
# node2 ansible_host=11.12.13.14 hostname=nym-exit.ch-1.mydomain.net mode=exit-gateway location=CH wireguard_enabled=true
## entry gateway, no wireguard:
# entry gateway, no wireguard:
# node3 ansible_host=12.13.14.15 hostname=nym-entry.ch-2.mydomain.net mode=entry-gateway location=CH wireguard_enabled=false
## mixnode (comment out tunnel+quic roles in deploy.yml for these)
# mix-de-1 ansible_host=13.14.15.16 hostname=nym-mix.de-1.example.net location=DE mode=mixnode wireguard_enabled=false
## NOTE:
## all examples above don't have defined user, email nor description as we use global vars from playbooks/group_vars/all.yml
# NOTE:
# all examples above don't have defined user, email nor description as we use the definition from group_vars/main.yml without an attempt of overwriting it
# all examples above don't have moniker defined as there is a function in /templates/description.toml.j2 deriving it from the hostname
+11 -5
View File
@@ -89,6 +89,7 @@
loop:
- "/etc/nginx/sites-enabled/{{ hostname }}-ssl"
- "/etc/nginx/sites-enabled/nym-wss-config"
when: not le_cert.stat.exists
notify: Restart nginx
- name: Ensure nginx is enabled and running (needed for ACME http-01)
@@ -110,13 +111,18 @@
- name: Obtain/renew certificate
command:
cmd: >-
certbot certonly --nginx
{% if le_cert.stat.exists %}
certbot certonly --webroot
-w /var/www/{{ hostname }}
--non-interactive --agree-tos --keep-until-expiring
-m {{ email }} -d {{ hostname }}
{% else %}
certbot --nginx
--non-interactive --agree-tos --redirect
-m {{ email }} -d {{ hostname }}
{% endif %}
register: certbot_result
failed_when: false
failed_when: false
# re-check cert after certbot attempt
- name: Re-check whether certificate exists after certbot
@@ -164,4 +170,4 @@
changed_when: false
- name: Flush handlers (apply restart after successful tests)
meta: flush_handlers
meta: flush_handlers
+2 -2
View File
@@ -10,7 +10,7 @@ mixnet_bind_address: "0.0.0.0:1789" # maps to --mixnet-bind-address
landing_page_assets_base_dir: "/var/www"
# Flag toggles
accept_operator_terms: false # override in group_vars or inventory
# accept_operator_terms: true # controls --accept-operator-terms-and-conditions
nym_write_flag: true # controls -w
nym_init_only_flag: true # controls --init-only
wss_port: 9001 # controlls --announce-wss-port
@@ -18,7 +18,7 @@ wss_port: 9001 # controlls --announce-wss-port
# Optional: extra flags if you want to append more later
nym_extra_flags: ""
# CLI URL
# CLI URL (nym_version can be set elsewhere / via GitHub API)
nym_cli_url: "https://github.com/nymtech/nym/releases/download/{{ nym_version }}/nym-cli"
# UFW
@@ -1,38 +0,0 @@
- name: Show which node is being bonded
tags: bonding
debug:
msg: "Bonding Nym node: {{ hostname }}"
- name: Get bonding details
tags: bonding
command: "/root/nym-binaries/nym-node bonding-information"
register: bondinfo
changed_when: false
- name: Display bonding info
tags: bonding
debug:
msg: "{{ item }}"
loop: "{{ bondinfo.stdout_lines }}"
- name: Sign bonding contract message on the node
tags: bonding
command:
argv:
- /root/nym-binaries/nym-node
- sign
- --contract-msg
- "{{ contract_msg }}"
- --output
- json
register: sign_output
- name: Display full signed message exactly as returned
tags: bonding
debug:
msg: "{{ sign_output.stdout }}"
- name: Display encoded signature
tags: bonding
debug:
msg: "ENCODED_SIGNATURE={{ (sign_output.stdout | from_json).encoded_signature }}"
+6 -10
View File
@@ -1,20 +1,16 @@
- name: Download quic_bridge_deployment.sh
command:
cmd: "curl -fsSL {{ quic_bridge_deployment_url }} -o /root/nym-binaries/quic_bridge_deployment.sh"
tags: quic
- name: Set quic_bridge_deployment permissions
file:
path: /root/nym-binaries/quic_bridge_deployment.sh
tags: quic bridge deployment
get_url:
url: "{{ quic_bridge_deployment_url }}"
dest: "/root/nym-binaries/quic_bridge_deployment.sh"
mode: "0755"
tags: quic
- name: Configure tunnel manager
tags: quic bridge deployment
become: true
command:
cmd: "/root/nym-binaries/quic_bridge_deployment.sh {{ item }}"
environment:
NONINTERACTIVE: "1"
loop:
- full_bridge_setup
tags: quic
- full_bridge_setup
+4 -10
View File
@@ -10,17 +10,11 @@
- ntm
- name: Download network tunnel manager
command:
cmd: "curl -fsSL {{ tunnel_manager_url }} -o /root/nym-binaries/network-tunnel-manager.sh"
tags:
- tunnel
- network_tunnel_manager
- ntm
- name: Set network tunnel manager permissions
file:
path: /root/nym-binaries/network-tunnel-manager.sh
get_url:
url: "{{ tunnel_manager_url }}"
dest: /root/nym-binaries/network-tunnel-manager.sh
mode: "0755"
force: yes
tags:
- tunnel
- network_tunnel_manager
+1 -1
View File
@@ -1,7 +1,7 @@
[package]
name = "nym-client"
description = "Implementation of the Nym Client"
version = "1.1.78"
version = "1.1.76"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>", "Jędrzej Stuczyński <andrew@nymtech.net>"]
edition = "2021"
license.workspace = true
+1 -1
View File
@@ -1,7 +1,7 @@
[package]
name = "nym-socks5-client"
description = "A SOCKS5 localhost proxy that converts incoming messages to Sphinx and sends them to a Nym address"
version = "1.1.78"
version = "1.1.76"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>"]
edition = "2021"
license.workspace = true
-10
View File
@@ -25,8 +25,6 @@ pub trait BandwidthTicketProvider: Send + Sync {
) -> Result<PreparedCredential, BandwidthControllerError>;
async fn get_upgrade_mode_token(&self) -> Result<Option<String>, BandwidthControllerError>;
async fn close(&self) {}
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
@@ -58,10 +56,6 @@ where
.map_err(|_| BandwidthControllerError::MalformedUpgradeModeToken)?;
Ok(Some(token))
}
async fn close(&self) {
self.storage.close().await;
}
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
@@ -81,8 +75,4 @@ impl<T: BandwidthTicketProvider + ?Sized + Send> BandwidthTicketProvider for Box
async fn get_upgrade_mode_token(&self) -> Result<Option<String>, BandwidthControllerError> {
(**self).get_upgrade_mode_token().await
}
async fn close(&self) {
(**self).close().await;
}
}
+1
View File
@@ -60,6 +60,7 @@ nym-client-core-surb-storage = { workspace = true }
nym-client-core-gateways-storage = { workspace = true }
nym-ecash-time = { workspace = true }
nym-mixnet-contract-common = { workspace = true }
nym-lp-data = { workspace = true }
[target."cfg(not(target_arch = \"wasm32\"))".dependencies]
nym-mixnet-client = { workspace = true }
@@ -11,6 +11,8 @@ use crate::client::event_control::EventControl;
use crate::client::inbound_messages::{InputMessage, InputMessageReceiver, InputMessageSender};
use crate::client::key_manager::ClientKeys;
use crate::client::key_manager::persistence::KeyStore;
use crate::client::lp::data::LpDataSetup;
use crate::client::lp::data::shared::SharedLpDataState;
use crate::client::mix_traffic::transceiver::{GatewayReceiver, GatewayTransceiver, RemoteGateway};
use crate::client::mix_traffic::{BatchMixMessageSender, MixTrafficController, MixTrafficEvent};
use crate::client::real_messages_control;
@@ -636,7 +638,6 @@ where
{
Err(ClientCoreError::CustomGatewaySelectionExpected)
} else {
// and make sure to invalidate the task client, so we wouldn't cause premature shutdown
custom_gateway_transceiver.set_packet_router(packet_router)?;
Ok(custom_gateway_transceiver)
};
@@ -817,6 +818,24 @@ where
(mix_tx, client_tx)
}
#[allow(dead_code)]
fn build_lp_data_tasks(
config: &Config,
encryption_keys: Arc<x25519::KeyPair>,
identity_keys: Arc<ed25519::KeyPair>,
input_receiver: InputMessageReceiver,
shutdown_tracker: &ShutdownTracker,
) -> Result<LpDataSetup, ClientCoreError> {
let shared_state = SharedLpDataState::new(
config.debug,
encryption_keys,
identity_keys,
shutdown_tracker.clone_shutdown_token(),
);
LpDataSetup::new(shared_state, input_receiver, shutdown_tracker.clone())
}
// TODO: rename it as it implies the data is persistent whilst one can use InMemBackend
async fn setup_persistent_reply_storage(
backend: S::ReplyStore,
@@ -1023,16 +1042,6 @@ where
let encryption_keys = init_res.client_keys.encryption_keypair();
let identity_keys = init_res.client_keys.identity_keypair();
let credential_store_for_close = credential_store.clone();
let close_credential_token = shutdown_tracker.clone_shutdown_token();
shutdown_tracker.try_spawn_named(
async move {
close_credential_token.cancelled().await;
credential_store_for_close.close().await;
},
"CredentialStorage::close_on_shutdown",
);
// the components are started in very specific order. Unless you know what you are doing,
// do not change that.
let bandwidth_controller = self
@@ -1073,12 +1082,27 @@ where
)
.await?;
// SW keep all the above
// LP Data channel
// let lp_data_tasks = Self::build_lp_data_tasks(
// &self.config,
// encryption_keys.clone(),
// identity_keys.clone(),
// input_receiver,
// &shutdown_tracker.clone(),
// )?;
// lp_data_tasks.start_tasks();
// SW Piping between inbound and outbound
let gateway_packet_router = PacketRouter::new(
ack_sender,
mixnet_messages_sender,
shutdown_tracker.clone_shutdown_token(),
);
// SW this needs to become the IO handler
let gateway_transceiver = Self::setup_gateway_transceiver(
self.custom_gateway_transceiver,
&self.config,
@@ -1100,6 +1124,7 @@ where
)
.await?;
// SW turn into inbound pipeline
Self::start_received_messages_buffer_controller(
encryption_keys,
received_buffer_request_receiver,
@@ -1110,6 +1135,8 @@ where
&shutdown_tracker.clone(),
);
// SW the rest below is outbound pipeline
// The message_sender is the transmitter for any component generating sphinx packets
// that are to be sent to the mixnet. They are used by cover traffic stream and real
// traffic stream.
@@ -11,17 +11,11 @@ use nym_bandwidth_controller::BandwidthController;
use nym_client_core_gateways_storage::OnDiskGatewaysDetails;
use nym_credential_storage::storage::Storage as CredentialStorage;
use nym_validator_client::{QueryHttpRpcNyxdClient, nyxd};
use std::{io, path::Path, time::Duration};
use std::{io, path::Path};
use time::OffsetDateTime;
use tracing::{error, info, trace};
use url::Url;
/// Maximum rename retry attempts when the database file is temporarily locked.
const ARCHIVE_MAX_RETRY_ATTEMPTS: u8 = 15;
/// Delay between archive rename retry attempts.
const ARCHIVE_RETRY_DELAY: Duration = Duration::from_millis(200);
async fn setup_fresh_backend<P: AsRef<Path>>(
db_path: P,
surb_config: &config::ReplySurbs,
@@ -80,58 +74,13 @@ async fn archive_corrupted_database<P: AsRef<Path>>(db_path: P) -> io::Result<()
};
let renamed = db_path.with_extension(new_extension);
// On Windows, sqlx may release its OS file handles asynchronously after
// pool.close() returns, briefly keeping the file locked
// (ERROR_SHARING_VIOLATION, os error 32). Retry with a short delay to
// give the OS time to flush the remaining handles.
for attempt in 0..ARCHIVE_MAX_RETRY_ATTEMPTS {
match tokio::fs::rename(db_path, &renamed).await {
Ok(()) => return Ok(()),
Err(e) if is_file_locked_error(&e) && (attempt + 1) < ARCHIVE_MAX_RETRY_ATTEMPTS => {
trace!(
"Database file is temporarily locked, retrying archive \
(attempt {}/{}): {e}",
attempt + 1,
ARCHIVE_MAX_RETRY_ATTEMPTS
);
tokio::time::sleep(ARCHIVE_RETRY_DELAY).await;
}
Err(e) => {
error!(
"Failed to rename corrupt database file: {} to {}",
db_path.display(),
renamed.display()
);
return Err(e);
}
}
}
// Reached only when every attempt was blocked by a file lock.
error!(
"Failed to rename corrupt database file after {} attempts: {} to {}",
ARCHIVE_MAX_RETRY_ATTEMPTS,
db_path.display(),
renamed.display()
);
Err(io::Error::other(
"corrupt database archive blocked by persistent file lock",
))
}
/// Returns `true` when the IO error indicates a temporary file lock held by another handle
/// within the same process. Only meaningful on Windows; always `false` elsewhere.
fn is_file_locked_error(e: &io::Error) -> bool {
#[cfg(windows)]
{
// ERROR_SHARING_VIOLATION = 32, ERROR_LOCK_VIOLATION = 33
matches!(e.raw_os_error(), Some(32) | Some(33))
}
#[cfg(not(windows))]
{
let _ = e;
false
}
tokio::fs::rename(db_path, &renamed).await.inspect_err(|_| {
error!(
"Failed to rename corrupt database file: {} to {}",
db_path.display(),
renamed.display()
);
})
}
pub async fn setup_fs_reply_surb_backend<P: AsRef<Path>>(
@@ -0,0 +1,52 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use nym_lp_data::packet::frame::LpFrameKind;
use nym_sphinx::addressing::nodes::NymNodeRoutingAddressError;
use nym_sphinx::forwarding::packet::MixPacketFormattingError;
use nym_sphinx::framing::processing::PacketProcessingError;
use nym_sphinx::{OutfoxError, SphinxError};
use thiserror::Error;
#[derive(Debug, Error)]
pub enum LpDataHandlerError {
#[error(transparent)]
PacketFormattingError(#[from] MixPacketFormattingError),
#[error(transparent)]
PacketProcessingError(#[from] PacketProcessingError),
#[error(transparent)]
NymNodeRoutingAddressError(#[from] NymNodeRoutingAddressError),
#[error("failed to process received sphinx packet: {0}")]
SphinxProcessingError(#[from] SphinxError),
#[error("failed to process received outfox packet: {0}")]
OutfoxProcessingError(#[from] OutfoxError),
#[error("received payload type of an unexpected type: {typ:?}")]
UnexpectedLpPayload { typ: LpFrameKind },
#[error("received an Lp Frame kind that we don't support: {typ:?}")]
UnsupportedLpFrameKind { typ: LpFrameKind },
#[error("unwrapped a packet into a forward hop packet. This is no longer supported")]
ForwardHop,
#[error("{0}")]
Internal(String),
#[error("{0}")]
Other(String),
}
impl LpDataHandlerError {
pub fn internal(message: impl Into<String>) -> Self {
LpDataHandlerError::Internal(message.into())
}
pub fn other(message: impl Into<String>) -> Self {
LpDataHandlerError::Other(message.into())
}
}
@@ -0,0 +1,56 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use nym_lp_data::packet::frame::{LpFrameAttributes, LpFrameHeader, LpFrameKind};
use nym_sphinx::forwarding::packet::MixPacketFormattingError;
use nym_sphinx::params::SphinxKeyRotation;
use crate::client::lp::data::handler::error::LpDataHandlerError;
/// Message types supported by clients
#[derive(Debug, Clone, Copy)]
pub enum ClientMessage {
Sphinx(SphinxMessage),
Outfox(OutfoxMessage),
}
impl ClientMessage {
pub fn from_frame_header(header: LpFrameHeader) -> Result<Self, LpDataHandlerError> {
match header.kind {
LpFrameKind::SphinxPacket => {
Ok(ClientMessage::Sphinx(header.frame_attributes.try_into()?))
}
LpFrameKind::OutfoxPacket => {
Ok(ClientMessage::Outfox(header.frame_attributes.try_into()?))
}
_ => Err(LpDataHandlerError::UnsupportedLpFrameKind { typ: header.kind }),
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct SphinxMessage {
pub key_rotation: SphinxKeyRotation,
}
impl TryFrom<LpFrameAttributes> for SphinxMessage {
type Error = LpDataHandlerError;
fn try_from(value: LpFrameAttributes) -> Result<Self, Self::Error> {
let key_rotation = value[0]
.try_into()
.map_err(MixPacketFormattingError::InvalidKeyRotation)?;
Ok(SphinxMessage { key_rotation })
}
}
impl From<SphinxMessage> for LpFrameAttributes {
fn from(value: SphinxMessage) -> Self {
let mut attrs = [0; 14];
attrs[0] = value.key_rotation as u8;
attrs
}
}
// For now there are no differences. We can augment this variant when we will need it
pub type OutfoxMessage = SphinxMessage;
@@ -0,0 +1,216 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::client::inbound_messages::InputMessageReceiver;
use crate::client::lp::LpDataHandlerError;
use crate::client::lp::data::PACKET_BUFFER_SIZE;
use crate::client::lp::data::shared::SharedLpDataState;
use nym_lp_data::clients::traits::ClientUnwrappingPipeline;
use nym_lp_data::common::traits::TransportUnwrap;
use nym_lp_data::packet::{EncryptedLpPacket, MalformedLpPacketError};
use nym_lp_data::{AddressedTimedData, TimedData};
use std::sync::{Arc, mpsc};
use std::time::Instant;
use std::{net::SocketAddr, time::Duration};
use tokio::sync::mpsc::error::TrySendError;
use tokio::time::interval;
use tracing::*;
pub mod error;
pub mod messages;
pub mod pipeline;
mod processing;
const PIPELINE_TICKING_DURATION: Duration = Duration::from_millis(1);
/// Bounded queue depth in front of each worker; keeps memory bounded under
/// bursty load and provides drop-based backpressure.
const WORKER_QUEUE_DEPTH: usize = 128;
type WorkerOutput = Result<Option<Vec<u8>>, MalformedLpPacketError>;
/// LP Data Handler for UDP data plane, acts as a pipeline driver and buffer
/// for delaying packets. Heavy per-packet processing is fanned out across a
/// pool of worker threads spawned on the shared blocking pool tracked by the
/// surrounding [`nym_task::ShutdownTracker`].
pub struct LpDataHandler {
/// Shared state
shared_state: Arc<SharedLpDataState>,
// Outbound pipeline
/// Channel to receive data for the outbound pipeline
outbound_input_rx: InputMessageReceiver,
/// Buffer for outbound packet
outbound_pkt_buffer: Vec<AddressedTimedData<EncryptedLpPacket>>,
/// Channel to send outgoing data from the outbound pipeline
outbound_output_tx: tokio::sync::mpsc::Sender<(EncryptedLpPacket, SocketAddr)>,
// Inbound pipeline
/// Channel to receive incoming data for the inbound pipeline
inbound_input_rx: mpsc::Receiver<EncryptedLpPacket>,
/// Per-worker job queues (round-robin dispatch).
worker_input_txs: Vec<mpsc::SyncSender<TimedData<EncryptedLpPacket>>>,
/// Aggregated processed packets returned by the workers. (Inbound data)
worker_output_rx: mpsc::Receiver<WorkerOutput>,
/// Shutdown token
shutdown: nym_task::ShutdownToken,
}
impl LpDataHandler {
pub(crate) fn new(
shared_state: Arc<SharedLpDataState>,
outbound_input_rx: InputMessageReceiver,
outbound_output_tx: tokio::sync::mpsc::Sender<(EncryptedLpPacket, SocketAddr)>,
inbound_input_rx: mpsc::Receiver<EncryptedLpPacket>,
// SW TODO : inbound output (worker_output_rx)
shutdown_tracker: &nym_task::ShutdownTracker,
) -> Result<Self, LpDataHandlerError> {
let (worker_output_tx, worker_output_rx) = mpsc::sync_channel(PACKET_BUFFER_SIZE);
// Allow at least one worker, even if the config says 0
let worker_count = 4; // SW Put that in the config
// Create workers. They will stop naturally when worker_output_rx is dropped.
// The mode is decided once here; each closure picks the right pipeline type so
// the worker loop monomorphizes against a single concrete pipeline.
let worker_input_txs = (0..worker_count)
.map(|_| {
let (worker_input_tx, _worker_input_rx) = mpsc::sync_channel(WORKER_QUEUE_DEPTH);
let _worker_state = shared_state.clone();
let _worker_output = worker_output_tx.clone();
shutdown_tracker.spawn_blocking(move || {
// Instantiat pipeline
todo!()
//Self::run_worker(pipeline, worker_input_rx, worker_output);
});
worker_input_tx
})
.collect();
Ok(Self {
shared_state,
outbound_input_rx,
outbound_pkt_buffer: Vec::new(),
outbound_output_tx,
inbound_input_rx,
worker_input_txs,
worker_output_rx,
shutdown: shutdown_tracker.clone_shutdown_token(),
})
}
pub async fn run(&mut self) {
info!(
workers = self.worker_input_txs.len(),
"Starting LP data handler"
);
let mut ticking_interval = interval(PIPELINE_TICKING_DURATION);
let mut next_worker = 0;
loop {
tokio::select! {
biased;
_ = self.shutdown.cancelled() => {
info!("LP data handler: received shutdown signal");
break;
}
timestamp = ticking_interval.tick() => {
let std_timestamp: Instant = timestamp.into();
// Drain processed packets returned by workers.
while let Ok(processing_result) = self.worker_output_rx.try_recv() {
match processing_result {
Ok(_packets) => {
// Dispatch to application
todo!()
},
Err(e) => {
warn!("LP data worker: error processing packet : {e}");
},
}
}
// Dispatch incoming packets to workers.
while let Ok(input) = self.inbound_input_rx.try_recv() {
next_worker = self.dispatch_to_workers(
TimedData::new(std_timestamp, input),
next_worker,
);
}
// Run outbound pipeline
while let Ok(_input) = self.outbound_input_rx.try_recv() {
// Run outbound pipeline and stack result in outbound_pkt_buffer
todo!()
}
// Send packets that needs sending
for pkt in self.outbound_pkt_buffer.extract_if(.., |p| p.data.timestamp <= std_timestamp) {
if let Err(e) = self.outbound_output_tx.try_send((pkt.data.data, pkt.dst)) {
match e {
TrySendError::Full(_) => {
warn!("LP data handler: packet sending buffer is full, the client might be overloaded");
},
TrySendError::Closed(_) => {
break;
},
}
}
}
}
}
}
// Workers will stop because we are dropping the receiving channel
info!("LP data handler shutdown complete");
}
/// Round-robin dispatch a job across worker queues. If the chosen worker is
/// full, fall through to the next one; if all are saturated, drop the packet
/// (UDP-style) and bump a metric. Returns the worker index to start from on
/// the next dispatch.
fn dispatch_to_workers(&self, mut job: TimedData<EncryptedLpPacket>, start: usize) -> usize {
let n = self.worker_input_txs.len();
for offset in 0..n {
let idx = (start + offset) % n;
match self.worker_input_txs[idx].try_send(job) {
Ok(()) => return (idx + 1) % n,
Err(mpsc::TrySendError::Full(returned)) => {
job = returned;
}
Err(mpsc::TrySendError::Disconnected(returned)) => {
error!(
"LP data worker {idx} disconnected; this shouldn't happen outside of shut down"
);
job = returned;
}
}
}
warn!("LP data handler: all workers saturated, dropping packet");
start
}
fn run_worker<P>(
mut pipeline: P,
input_rx: mpsc::Receiver<TimedData<EncryptedLpPacket>>,
output_tx: mpsc::SyncSender<WorkerOutput>,
) where
P: ClientUnwrappingPipeline<EncryptedLpPacket, ()> // SW fill in message kind
+ TransportUnwrap<EncryptedLpPacket, Error = MalformedLpPacketError>, // This is needed to specify the error type
{
while let Ok(input) = input_rx.recv() {
// Blocking is fine, we don't want to unclog ourself and process a new packet that will be dropped anyway
if let Err(e) = output_tx.send(pipeline.unwrap(input.data, input.timestamp)) {
trace!(
"Failed to send processing data back to handler : {e}. We are probably shutting down"
);
return;
}
}
}
}
@@ -1,2 +1,4 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
// TODO
@@ -0,0 +1,5 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
pub(crate) mod outfox;
pub(crate) mod sphinx;
@@ -0,0 +1,37 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use nym_lp_data::TimedPayload;
use nym_sphinx::OutfoxPacket;
use tracing::warn;
use crate::client::lp::data::{
handler::{error::LpDataHandlerError, messages::OutfoxMessage},
shared::SharedLpDataState,
};
pub(crate) fn process(
shared_state: &SharedLpDataState,
outfox_packet: TimedPayload,
_metadata: OutfoxMessage,
) -> Result<TimedPayload, LpDataHandlerError> {
let TimedPayload {
data: outfox_bytes,
timestamp: arrival_timestamp,
} = outfox_packet;
let mut outfox_packet = OutfoxPacket::try_from(outfox_bytes.as_slice())?;
let _next_address =
outfox_packet.decode_next_layer(shared_state.encryption_keys.private_key().as_ref())?;
if outfox_packet.is_final_hop() {
Ok(TimedPayload::new(
arrival_timestamp,
outfox_packet.payload().to_vec(),
))
} else {
warn!("Dropping forward hop packet in a client");
Err(LpDataHandlerError::ForwardHop)
}
}
@@ -0,0 +1,39 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use nym_lp_data::TimedPayload;
use nym_sphinx::{ProcessedPacketData, SphinxPacket};
use tracing::warn;
use crate::client::lp::data::{
handler::{error::LpDataHandlerError, messages::SphinxMessage},
shared::SharedLpDataState,
};
pub(crate) fn process(
shared_state: &SharedLpDataState,
sphinx_packet: TimedPayload,
_metadata: SphinxMessage,
) -> Result<TimedPayload, LpDataHandlerError> {
let TimedPayload {
data: sphinx_bytes,
timestamp: arrival_timestamp,
} = sphinx_packet;
let sphinx_packet = SphinxPacket::from_bytes(&sphinx_bytes)?;
// Final processing
let processed_packet =
sphinx_packet.process(shared_state.encryption_keys.private_key().as_ref())?;
match processed_packet.data {
ProcessedPacketData::ForwardHop { .. } => {
warn!("Dropping forward hop packet in a client");
Err(LpDataHandlerError::ForwardHop)
}
ProcessedPacketData::FinalHop { payload, .. } => Ok(TimedPayload::new(
arrival_timestamp,
payload.recover_plaintext()?,
)),
}
}
@@ -0,0 +1,107 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::client::lp::data::MAX_UDP_PACKET_SIZE;
use crate::client::lp::data::shared::SharedLpDataState;
use crate::error::ClientCoreError;
use nym_lp_data::packet::EncryptedLpPacket;
use std::net::SocketAddr;
use std::sync::{Arc, mpsc, mpsc::TrySendError};
use tokio::net::UdpSocket;
use tracing::log::warn;
use tracing::{error, info};
/// LP UDP listener that accepts TCP connections on port 51264 (by default)
pub(crate) struct LpDataListener {
/// Shared state
shared_state: Arc<SharedLpDataState>,
/// Channel to send incoming data to the processing pipeline
inbound_input_tx: mpsc::SyncSender<EncryptedLpPacket>,
// This has to be a tokio channel, to be async and bounded
/// Channel to receive outgoing data from the processling pipeline
outbound_output_rx: tokio::sync::mpsc::Receiver<(EncryptedLpPacket, SocketAddr)>,
/// Shutdown token
shutdown: nym_task::ShutdownToken,
}
impl LpDataListener {
pub fn new(
shared_state: Arc<SharedLpDataState>,
inbound_input_tx: mpsc::SyncSender<EncryptedLpPacket>,
outbound_output_rx: tokio::sync::mpsc::Receiver<(EncryptedLpPacket, SocketAddr)>,
shutdown: nym_task::ShutdownToken,
) -> Self {
Self {
shared_state,
inbound_input_tx,
outbound_output_rx,
shutdown,
}
}
pub async fn run(&mut self) -> Result<(), ClientCoreError> {
let socket = UdpSocket::bind("[::]:0").await.map_err(|source| {
error!("Failed to bind LP data socket: {source}");
ClientCoreError::LpBindFailure { source }
})?;
info!("Started LP data socket on {}", socket.local_addr()?);
let mut buf = vec![0u8; MAX_UDP_PACKET_SIZE];
loop {
tokio::select! {
biased;
_ = self.shutdown.cancelled() => {
info!("LP data listener: received shutdown signal");
break;
}
result = self.outbound_output_rx.recv() => {
match result {
Some((payload, dst_addr)) => {
if let Err(e) = socket.send_to(&payload.to_bytes(), dst_addr).await {
warn!("LP data packet error to {dst_addr}: {e}");
}
}
None => {
warn!("LP outgoing packet channel closed");
break;
}
}
}
result = socket.recv_from(&mut buf) => {
match result {
Ok((len, src_addr)) => {
info!("received {len} bytes from {src_addr} on the LP Data socket");
if let Ok(encrypted_packet) = EncryptedLpPacket::decode(&buf[..len]) {
if let Err(e) = self.inbound_input_tx.try_send(encrypted_packet) {
match e {
TrySendError::Full(_) => {
warn!("LP data listener: packet sending buffer is full, the client might be overloaded");
},
TrySendError::Disconnected(_) => {
warn!("LP data listener: incoming packet channel is closed");
break;
},
}
}
} else {
warn!("Error reading LP packet from wire");
}
}
Err(e) => {
warn!("LP data socket recv error: {e}");
}
}
}
}
}
info!("LP data handler shutdown complete");
Ok(())
}
}
@@ -0,0 +1,103 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
// Parking the branch
#![allow(clippy::todo)]
#![allow(dead_code)]
#![allow(clippy::incompatible_msrv)]
use std::sync::{Arc, mpsc};
use crate::client::inbound_messages::InputMessageReceiver;
use crate::client::lp::data::handler::LpDataHandler;
use crate::client::lp::data::listener::LpDataListener;
use crate::client::lp::data::shared::SharedLpDataState;
use crate::error::ClientCoreError;
use nym_task::ShutdownTracker;
use tracing::error;
/// Maximum UDP packet size we'll accept
/// Sphinx packets are typically ~2KB, LP overhead is ~50 bytes, so 4KB is plenty
const MAX_UDP_PACKET_SIZE: usize = 4096;
pub(crate) const PACKET_BUFFER_SIZE: usize = 100;
pub mod handler;
mod listener;
pub mod shared;
pub struct LpDataSetup {
listener: LpDataListener,
handler: LpDataHandler,
/// Shutdown coordination
shutdown: ShutdownTracker,
}
impl LpDataSetup {
pub(crate) fn new(
shared_state: SharedLpDataState,
outbound_input_rx: InputMessageReceiver,
shutdown: ShutdownTracker,
) -> Result<Self, ClientCoreError> {
let (inbound_input_tx, inbound_input_rx) = mpsc::sync_channel(PACKET_BUFFER_SIZE);
let (outbound_output_tx, outbound_output_rx) =
tokio::sync::mpsc::channel(PACKET_BUFFER_SIZE);
let shared_state = Arc::new(shared_state);
let listener = LpDataListener::new(
shared_state.clone(),
inbound_input_tx,
outbound_output_rx,
shutdown.clone_shutdown_token(),
);
let handler = LpDataHandler::new(
shared_state,
outbound_input_rx,
outbound_output_tx,
inbound_input_rx,
&shutdown,
)?;
Ok(LpDataSetup {
listener,
handler,
shutdown,
})
}
pub fn start_tasks(mut self) {
// Spawn the UDP data handler for LP data plane
// The data handler listens on UDP port 51264 and processes LP-wrapped Sphinx packets
// from registered clients. It decrypts the LP layer and forwards the Sphinx packets
let shutdown_token = self.shutdown.clone_shutdown_token();
let mut listener = self.listener;
self.shutdown.try_spawn_named(
async move {
if let Err(err) = listener.run().await {
shutdown_token.cancel();
error!("LP data listener error: {err}");
}
},
"LP::LpDataListener",
);
self.shutdown
.try_spawn_named(async move { self.handler.run().await }, "LP::LpDataHandler");
}
}
#[cfg(test)]
mod tests {
use super::*;
// Sphinx packets are typically around 2KB
// 4KB should be plenty with room to spare
const _: () = {
assert!(MAX_UDP_PACKET_SIZE >= 2048 + 100);
};
}
@@ -0,0 +1,38 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use std::sync::Arc;
use nym_client_core_config_types::DebugConfig;
use nym_crypto::asymmetric::{ed25519, x25519};
use nym_lp_data::fragmentation::reconstruction::MessageReconstructor;
use nym_task::ShutdownToken;
/// Shared state for LP data plane
pub struct SharedLpDataState {
pub(crate) config: DebugConfig,
pub(crate) encryption_keys: Arc<x25519::KeyPair>,
pub(crate) identity_keys: Arc<ed25519::KeyPair>,
pub(crate) message_reconstructor: MessageReconstructor,
pub(crate) shutdown_token: ShutdownToken,
}
impl SharedLpDataState {
pub(crate) fn new(
config: DebugConfig,
encryption_keys: Arc<x25519::KeyPair>,
identity_keys: Arc<ed25519::KeyPair>,
shutdown_token: ShutdownToken,
) -> Self {
SharedLpDataState {
config,
encryption_keys,
identity_keys,
message_reconstructor: Default::default(),
shutdown_token,
}
}
}
+6
View File
@@ -0,0 +1,6 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
pub use data::handler::error::LpDataHandlerError;
pub mod data;
@@ -240,7 +240,7 @@ mod nonwasm_sealed {
impl GatewaySender for LocalGateway {
async fn send_mix_packet(&mut self, packet: MixPacket) -> Result<(), ErasedGatewayError> {
self.packet_forwarder
.forward_client_packet_without_delay(packet)
.forward_packet(packet)
.map_err(erase_err)
}
}
+1
View File
@@ -7,6 +7,7 @@ pub(crate) mod event_control;
pub(crate) mod helpers;
pub mod inbound_messages;
pub mod key_manager;
pub mod lp;
pub mod mix_traffic;
pub mod real_messages_control;
pub mod received_buffer;
+7
View File
@@ -1,6 +1,7 @@
// Copyright 2022-2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::client::lp::LpDataHandlerError;
use crate::client::mix_traffic::transceiver::ErasedGatewayError;
use nym_crypto::asymmetric::ed25519::Ed25519RecoveryError;
use nym_gateway_client::error::GatewayClientError;
@@ -263,6 +264,12 @@ pub enum ClientCoreError {
#[error("Could not access task registry, {0}")]
RegistryAccess(#[from] RegistryAccessError),
#[error("failed to bind LP UDP socket: {source}")]
LpBindFailure { source: std::io::Error },
#[error(transparent)]
LpFailure(#[from] LpDataHandlerError),
}
impl From<tungstenite::Error> for ClientCoreError {
@@ -337,8 +337,6 @@ impl ReplyStorageBackend for Backend {
}
async fn stop_storage_session(self) -> Result<(), Self::StorageError> {
let result = self.stop_client_use().await;
self.shutdown().await;
result
self.stop_client_use().await
}
}
+3 -5
View File
@@ -48,7 +48,6 @@ where
debug!("Started PersistentReplyStorage");
if let Err(err) = self.backend.start_storage_session().await {
error!("failed to start the storage session - {err}");
self.backend.stop_storage_session().await.ok();
return;
}
@@ -56,11 +55,10 @@ where
info!("PersistentReplyStorage is flushing all reply-related data to underlying storage");
if let Err(err) = self.backend.flush_surb_storage(&mem_state).await {
error!("failed to flush our reply-related data to the persistent storage: {err}");
self.backend.stop_storage_session().await.ok();
return;
error!("failed to flush our reply-related data to the persistent storage: {err}")
} else {
info!("Data flush is complete")
}
info!("Data flush is complete");
if let Err(err) = self.backend.stop_storage_session().await {
error!("failed to properly stop the storage session - {err}. We might not be able to smoothly restore it")
+1 -4
View File
@@ -17,7 +17,6 @@ publish = true
[dependencies]
dashmap = { workspace = true }
futures = { workspace = true }
strum = { workspace = true }
tracing = { workspace = true }
tokio = { workspace = true, features = ["time", "sync"] }
tokio-util = { workspace = true, features = ["codec"], optional = true }
@@ -27,13 +26,11 @@ tokio-stream = { workspace = true }
nym-noise = { workspace = true }
nym-sphinx = { workspace = true }
nym-task = { workspace = true, optional = true }
nym-metrics = { workspace = true, optional = true }
[features]
default = ["client"]
client = ["tokio-util", "nym-task", "nym-metrics", "tokio/net", "tokio/rt"]
client = ["tokio-util", "nym-task", "tokio/net", "tokio/rt"]
[dev-dependencies]
nym-crypto = { workspace = true }
rand = { workspace = true }
tokio = { workspace = true, features = ["macros", "io-util", "rt", "rt-multi-thread", "test-util"] }
+88 -529
View File
@@ -1,25 +1,24 @@
// Copyright 2021-2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::metrics::{MixnetMetric, Traced};
use dashmap::DashMap;
use futures::{Sink, SinkExt, StreamExt};
use futures::StreamExt;
use nym_noise::config::NoiseConfig;
use nym_noise::upgrade_noise_initiator;
use nym_sphinx::addressing::nodes::NymNodeRoutingAddress;
use nym_sphinx::forwarding::packet::MixPacket;
use nym_sphinx::framing::codec::NymCodec;
use nym_sphinx::framing::packet::FramedNymPacket;
use std::io;
use std::net::SocketAddr;
use std::ops::{ControlFlow, Deref};
use std::ops::Deref;
use std::sync::atomic::{AtomicU32, AtomicUsize, Ordering};
use std::sync::Arc;
use std::time::Duration;
use tokio::io::{AsyncRead, AsyncWrite};
use tokio::net::TcpStream;
use tokio::sync::mpsc;
use tokio::sync::mpsc::error::TrySendError;
use tokio::time::{sleep, Instant};
use tokio::time::sleep;
use tokio_stream::wrappers::ReceiverStream;
use tokio_util::codec::Framed;
use tracing::*;
@@ -31,13 +30,6 @@ pub struct Config {
pub initial_connection_timeout: Duration,
pub maximum_connection_buffer_size: usize,
pub use_legacy_packet_encoding: bool,
/// Close an egress connection after this long with no packets sent (0 disables). The cache
/// entry is evicted on close and the next packet to that peer transparently reconnects.
pub connection_idle_timeout: Duration,
/// Max time a single batch flush may block on the peer socket before we give up on it
/// (0 disables). One timeout is treated as transient congestion - the batch is abandoned but
/// the connection is retained (no re-handshake); only a few *consecutive* timeouts tear it down.
pub connection_write_timeout: Duration,
}
impl Config {
@@ -47,8 +39,6 @@ impl Config {
initial_connection_timeout: Duration,
maximum_connection_buffer_size: usize,
use_legacy_packet_encoding: bool,
connection_idle_timeout: Duration,
connection_write_timeout: Duration,
) -> Self {
Config {
initial_reconnection_backoff,
@@ -56,18 +46,14 @@ impl Config {
initial_connection_timeout,
maximum_connection_buffer_size,
use_legacy_packet_encoding,
connection_idle_timeout,
connection_write_timeout,
}
}
}
pub trait SendWithoutResponse {
// Without response in this context means we will not listen for anything we might get back (not
// that we should get anything), including any possible io errors.
// The packet carries the latency trace started upstream (at receive); the egress stages are
// stamped here and are a no-op for unsampled packets.
fn send_without_response(&self, packet: Traced<MixPacket>) -> io::Result<()>;
// that we should get anything), including any possible io errors
fn send_without_response(&self, packet: MixPacket) -> io::Result<()>;
}
pub struct Client {
@@ -103,19 +89,15 @@ impl Deref for ActiveConnections {
}
pub struct ConnectionSender {
channel: mpsc::Sender<Traced<FramedNymPacket>>,
channel: mpsc::Sender<FramedNymPacket>,
current_reconnection_attempt: Arc<AtomicU32>,
// Identifies the `ManagedConnection` task currently owning this entry; used
// to ensure drop-time eviction only fires on the still-owning task.
handle_token: Arc<()>,
}
impl ConnectionSender {
fn new(channel: mpsc::Sender<Traced<FramedNymPacket>>, handle_token: Arc<()>) -> Self {
fn new(channel: mpsc::Sender<FramedNymPacket>) -> Self {
ConnectionSender {
channel,
current_reconnection_attempt: Arc::new(AtomicU32::new(0)),
handle_token,
}
}
}
@@ -123,85 +105,91 @@ impl ConnectionSender {
struct ManagedConnection {
address: SocketAddr,
noise_config: NoiseConfig,
message_receiver: ReceiverStream<Traced<FramedNymPacket>>,
message_receiver: ReceiverStream<FramedNymPacket>,
connection_timeout: Duration,
idle_timeout: Duration,
write_timeout: Duration,
current_reconnection: Arc<AtomicU32>,
active_connections: ActiveConnections,
handle_token: Arc<()>,
}
// Evicts the cache entry on task exit (only if still owned by this task).
// Without this, a stale `ConnectionSender` survives after the peer disconnects
// and the next outbound packet is silently swallowed by the dead TCP.
struct EvictOnDrop {
active_connections: ActiveConnections,
address: SocketAddr,
handle_token: Arc<()>,
}
impl Drop for EvictOnDrop {
fn drop(&mut self) {
let address = self.address;
let handle_token = &self.handle_token;
self.active_connections.remove_if(&address, |_, sender| {
Arc::ptr_eq(&sender.handle_token, handle_token)
});
trace!(
peer = %address,
"managed connection task exited; evicted owning cache entry"
);
}
}
impl ManagedConnection {
#[allow(clippy::too_many_arguments)]
fn new(
address: SocketAddr,
noise_config: NoiseConfig,
message_receiver: mpsc::Receiver<Traced<FramedNymPacket>>,
message_receiver: mpsc::Receiver<FramedNymPacket>,
connection_timeout: Duration,
idle_timeout: Duration,
write_timeout: Duration,
current_reconnection: Arc<AtomicU32>,
active_connections: ActiveConnections,
handle_token: Arc<()>,
) -> Self {
ManagedConnection {
address,
noise_config,
message_receiver: ReceiverStream::new(message_receiver),
connection_timeout,
idle_timeout,
write_timeout,
current_reconnection,
active_connections,
handle_token,
}
}
async fn run(self) {
let address = self.address;
let idle_timeout = self.idle_timeout;
let write_timeout = self.write_timeout;
let _evict_guard = EvictOnDrop {
active_connections: self.active_connections,
address,
handle_token: self.handle_token,
};
let reconnection_attempt = self.current_reconnection.load(Ordering::Acquire);
let connect_start = tokio::time::Instant::now();
let connection_fut = TcpStream::connect(address);
// 1. attempt to establish the connection with timeout
let maybe_stream = match tokio::time::timeout(self.connection_timeout, connection_fut).await
{
Ok(stream) => stream,
let conn = match tokio::time::timeout(self.connection_timeout, connection_fut).await {
Ok(stream_res) => match stream_res {
Ok(stream) => {
let connect_ms = connect_start.elapsed().as_millis() as u64;
debug!(
peer = %address,
connect_ms,
"Managed to establish connection to {}", self.address
);
let noise_start = tokio::time::Instant::now();
let noise_stream =
match upgrade_noise_initiator(stream, &self.noise_config).await {
Ok(noise_stream) => noise_stream,
Err(err) => {
let noise_handshake_ms = noise_start.elapsed().as_millis() as u64;
warn!(
event = "connection.failed.noise",
peer = %address,
error = %err,
connect_ms,
noise_handshake_ms,
reconnection_attempt,
exit_reason = "noise_error",
"Failed to perform Noise initiator handshake with {address}"
);
self.current_reconnection.fetch_add(1, Ordering::SeqCst);
return;
}
};
let noise_handshake_ms = noise_start.elapsed().as_millis() as u64;
self.current_reconnection.store(0, Ordering::Release);
debug!(
peer = %address,
connect_ms,
noise_handshake_ms,
"Noise initiator handshake completed for {:?}", address
);
Framed::new(noise_stream, NymCodec)
}
Err(err) => {
let connect_ms = connect_start.elapsed().as_millis() as u64;
warn!(
event = "connection.failed.connect",
peer = %address,
error = %err,
connect_ms,
reconnection_attempt,
exit_reason = "connect_error",
"failed to establish connection to {address}"
);
return;
}
},
Err(_) => {
let connect_ms = connect_start.elapsed().as_millis() as u64;
debug!(
warn!(
event = "connection.failed.timeout",
peer = %address,
timeout_ms = self.connection_timeout.as_millis() as u64,
@@ -216,295 +204,21 @@ impl ManagedConnection {
}
};
// 2. check if it actually succeeded
let stream = match maybe_stream {
Ok(stream) => stream,
Err(err) => {
let connect_ms = connect_start.elapsed().as_millis() as u64;
debug!(
event = "connection.failed.connect",
peer = %address,
error = %err,
connect_ms,
reconnection_attempt,
exit_reason = "connect_error",
"failed to establish connection to {address}"
);
return;
}
};
let connect_ms = connect_start.elapsed().as_millis() as u64;
debug!(
peer = %address,
connect_ms,
"Managed to establish connection to {}", self.address
);
// disable Nagle: mix packets are latency-sensitive and flushed one at a time.
if let Err(err) = stream.set_nodelay(true) {
warn!(peer = %address, error = %err, "failed to set TCP_NODELAY on outbound mixnet connection");
}
// 3. perform noise handshake (if applicable)
let noise_start = tokio::time::Instant::now();
let noise_stream = match upgrade_noise_initiator(stream, &self.noise_config).await {
Ok(noise_stream) => noise_stream,
Err(err) => {
let noise_handshake_ms = noise_start.elapsed().as_millis() as u64;
debug!(
event = "connection.failed.noise",
peer = %address,
error = %err,
connect_ms,
noise_handshake_ms,
reconnection_attempt,
exit_reason = "noise_error",
"Failed to perform Noise initiator handshake with {address}"
);
self.current_reconnection.fetch_add(1, Ordering::SeqCst);
return;
}
};
let noise_handshake_ms = noise_start.elapsed().as_millis() as u64;
self.current_reconnection.store(0, Ordering::Release);
debug!(
peer = %address,
connect_ms,
noise_handshake_ms,
"Noise initiator handshake completed for {:?}", address
);
let mut conn = Framed::new(noise_stream, NymCodec);
// let the write buffer accumulate several packets before flushing (see run_io_loop)
conn.set_backpressure_boundary(OUTBOUND_WRITE_BUFFER);
// 4. start handling the framed stream
run_io_loop(
conn,
self.message_receiver,
address,
idle_timeout,
write_timeout,
)
.await;
}
}
/// Upper bound on how many already-queued packets we drain into a single flush.
/// Bounds the per-batch allocation and how often we re-check the read side; the actual
/// write coalescing is governed by the Framed backpressure boundary below.
const OUTBOUND_FLUSH_BATCH: usize = 1024;
/// Write-buffer high-water mark for the egress `Framed`: packets are coalesced up to
/// roughly this many bytes before a flush, trading a larger write burst for far fewer
/// syscalls (and noise frames) under load. Kept under the ~64KiB noise frame ceiling so
/// a flush is usually a single frame.
const OUTBOUND_WRITE_BUFFER: usize = 32 * 1024;
/// Drive the read half solely to notice peer FIN/RST (the connection is send-only). Returns
/// `Break` when the peer closed the connection or the read errored, `Continue` otherwise.
fn handle_peer_read<P, E: std::fmt::Display>(
msg: Option<Result<P, E>>,
address: SocketAddr,
) -> ControlFlow<()> {
match msg {
None => {
debug!(
peer = %address,
exit_reason = "peer_closed",
"peer closed mixnet connection to {address}"
);
ControlFlow::Break(())
}
Some(Err(err)) => {
debug!(
event = "connection.read_error",
peer = %address,
error = %err,
exit_reason = "read_error",
"read error on mixnet connection to {address}: {err}"
);
ControlFlow::Break(())
}
Some(Ok(_)) => {
trace!(
peer = %address,
"unexpected inbound packet on mixnet connection to {address}; discarding"
);
ControlFlow::Continue(())
}
}
}
/// Number of consecutive flush timeouts to the same peer we tolerate before dropping the
/// connection. A single timeout is transient congestion (batch abandoned, connection retained to
/// avoid a re-handshake); this many in a row means the peer is persistently unable to keep up, so
/// we tear the connection down (it reconnects on the next packet).
const MAX_CONSECUTIVE_WRITE_TIMEOUTS: u32 = 3;
/// Outcome of attempting to flush one batch to the peer.
enum BatchOutcome {
/// the batch was flushed to the socket
Sent,
/// the flush exceeded the write timeout (peer congested): the un-fed tail of the batch is
/// dropped, but the already-encoded frames stay buffered for a later flush and the connection
/// is left intact - the noise transport stays nonce-consistent across the cancelled flush, so
/// resuming the write is sound
WriteTimedOut,
/// the sink errored: the connection is dead
Failed,
}
/// Feed a ready batch into the sink and flush it once (far fewer syscalls than per-packet), then
/// stamp the egress latency stages: `EgressQueue` before each feed, then `SocketWrite` + the
/// end-to-end total once the batch has hit the wire. The flush is bounded by `write_timeout`
/// (0 disables) so a congested peer can't block this connection's egress queue into the
/// multi-second range. The caller decides what a timeout means (see [`MAX_CONSECUTIVE_WRITE_TIMEOUTS`]).
async fn forward_batch<S>(
sink: &mut S,
batch: Vec<Traced<FramedNymPacket>>,
address: SocketAddr,
write_timeout: Duration,
) -> BatchOutcome
where
S: Sink<FramedNymPacket> + Unpin,
S::Error: std::fmt::Display,
{
let mut traces = Vec::with_capacity(batch.len());
let write = async {
for mut traced in batch {
// time spent waiting in this connection's egress buffer
traced.record(MixnetMetric::EgressQueue);
sink.feed(traced.inner).await?;
traces.push(traced.trace);
}
sink.flush().await
};
// bound how long we block on a slow/congested peer socket. On timeout the `write` future is
// cancelled, which is safe: every already-encoded frame is buffered (nonce-consistent), so a
// later flush resumes the byte stream in order.
let write_result = if write_timeout.is_zero() {
Ok(write.await)
} else {
tokio::time::timeout(write_timeout, write).await
};
// socket-write time + end-to-end total for whatever was fed (on a timeout, those frames are
// buffered and will hit the wire on a subsequent flush)
for mut trace in traces {
trace.record(MixnetMetric::SocketWrite);
trace.record_total();
}
match write_result {
Ok(Ok(())) => BatchOutcome::Sent,
Ok(Err(err)) => {
debug!(
if let Err(err) = self.message_receiver.map(Ok).forward(conn).await {
warn!(
event = "connection.forward_error",
peer = %address,
error = %err,
exit_reason = "forward_error",
"failed to forward packet batch to {address}: {err}"
"Failed to forward packets to {address}: {err}"
);
BatchOutcome::Failed
}
Err(_elapsed) => BatchOutcome::WriteTimedOut,
}
}
/// Instant at which a connection idle since `last_activity` should be closed, or `None` if idle
/// reaping is disabled (`timeout` is zero).
fn idle_deadline(last_activity: Instant, timeout: Duration) -> Option<Instant> {
(!timeout.is_zero()).then(|| last_activity + timeout)
}
// The connection is unidirectional (send-only); we read from it solely to
// notice peer FIN/RST while idle so we can evict the cache entry before the
// next outbound send finds it stale.
async fn run_io_loop<T>(
conn: Framed<T, NymCodec>,
receiver: ReceiverStream<Traced<FramedNymPacket>>,
address: SocketAddr,
idle_timeout: Duration,
write_timeout: Duration,
) where
T: AsyncRead + AsyncWrite + Unpin,
{
let (mut sink, mut stream) = conn.split();
// drain all currently-queued packets into one flush rather than flushing per packet,
// which otherwise caps egress throughput and backs up the per-connection queue under load
let mut receiver = receiver.ready_chunks(OUTBOUND_FLUSH_BATCH);
// reset by every batch we send; drives the idle-connection reaping below
let mut last_send = tokio::time::Instant::now();
// consecutive flush timeouts; a run of them (a persistently congested peer) drops the connection
let mut consecutive_write_timeouts = 0u32;
loop {
tokio::select! {
msg = stream.next() => {
if handle_peer_read(msg, address).is_break() {
break;
}
}
outgoing = receiver.next() => {
let Some(batch) = outgoing else {
debug!(
peer = %address,
exit_reason = "sender_dropped",
"connection manager to {address} finished"
);
break;
};
match forward_batch(&mut sink, batch, address, write_timeout).await {
BatchOutcome::Sent => {
consecutive_write_timeouts = 0;
last_send = Instant::now();
}
BatchOutcome::WriteTimedOut => {
consecutive_write_timeouts += 1;
warn!(
event = "connection.write_congested",
peer = %address,
write_ms = write_timeout.as_millis() as u64,
attempt = consecutive_write_timeouts,
max_attempts = MAX_CONSECUTIVE_WRITE_TIMEOUTS,
"egress flush to {address} timed out (peer congested); abandoned batch, retaining connection"
);
if consecutive_write_timeouts >= MAX_CONSECUTIVE_WRITE_TIMEOUTS {
debug!(
peer = %address,
exit_reason = "write_timeout",
"egress connection to {address} congested for {MAX_CONSECUTIVE_WRITE_TIMEOUTS} consecutive flushes; dropping it"
);
break;
}
// keep the connection: a single congestion spike shouldn't cost a
// re-handshake. `last_send` is deliberately not bumped, so a peer that goes
// congested-then-silent still idle-reaps on schedule.
}
BatchOutcome::Failed => break,
}
}
// close the connection (freeing the task/socket) if we haven't sent anything for too
// long; EvictOnDrop then clears the cache entry and the next packet reconnects
_ = async {
match idle_deadline(last_send, idle_timeout) {
Some(d) => tokio::time::sleep_until(d).await,
None => std::future::pending::<()>().await,
}
} => {
debug!(
peer = %address,
exit_reason = "idle_timeout",
idle_secs = idle_timeout.as_secs(),
"closing idle egress mixnet connection to {address}"
);
break;
}
}
debug!(
peer = %address,
exit_reason = "sender_dropped",
"connection manager to {address} finished"
);
}
}
@@ -543,7 +257,7 @@ impl Client {
}
}
fn make_connection(&self, address: SocketAddr, pending_packet: Traced<FramedNymPacket>) {
fn make_connection(&self, address: SocketAddr, pending_packet: FramedNymPacket) {
let (sender, receiver) = mpsc::channel(self.config.maximum_connection_buffer_size);
// this CAN'T fail because we just created the channel which has a non-zero capacity
@@ -551,18 +265,13 @@ impl Client {
sender.try_send(pending_packet).unwrap();
}
// Ownership token for the task we're about to spawn; lets it tell
// on exit whether the cache entry still names it.
let handle_token = Arc::new(());
// if we already tried to connect to `address` before, grab the current attempt count
let current_reconnection_attempt =
if let Some(mut existing) = self.active_connections.get_mut(&address) {
existing.channel = sender;
existing.handle_token = Arc::clone(&handle_token);
Arc::clone(&existing.current_reconnection_attempt)
} else {
let new_entry = ConnectionSender::new(sender, Arc::clone(&handle_token));
let new_entry = ConnectionSender::new(sender);
let current_attempt = Arc::clone(&new_entry.current_reconnection_attempt);
self.active_connections.insert(address, new_entry);
current_attempt
@@ -572,14 +281,11 @@ impl Client {
let reconnection_attempt = current_reconnection_attempt.load(Ordering::Acquire);
let backoff = self.determine_backoff(reconnection_attempt);
// copy the values before moving into another task
// copy the value before moving into another task
let initial_connection_timeout = self.config.initial_connection_timeout;
let connection_idle_timeout = self.config.connection_idle_timeout;
let connection_write_timeout = self.config.connection_write_timeout;
let connections_count = self.connections_count.clone();
let noise_config = self.noise_config.clone();
let active_connections = self.active_connections.clone();
tokio::spawn(async move {
// before executing the manager, wait for what was specified, if anything
if let Some(backoff) = backoff {
@@ -593,11 +299,7 @@ impl Client {
noise_config,
receiver,
initial_connection_timeout,
connection_idle_timeout,
connection_write_timeout,
current_reconnection_attempt,
active_connections,
handle_token,
)
.run()
.await;
@@ -607,17 +309,20 @@ impl Client {
}
impl SendWithoutResponse for Client {
fn send_without_response(&self, packet: Traced<MixPacket>) -> io::Result<()> {
let address = packet.inner.next_hop_address();
fn send_without_response(&self, packet: MixPacket) -> io::Result<()> {
let address = match packet.next_hop() {
NymNodeRoutingAddress::Client(_) => {
warn!("mix packet addressed to a client in the legacy send_without_response path. This should never happen!");
return Ok(());
}
NymNodeRoutingAddress::Node(address) => address,
};
trace!("Sending packet to {address}");
// capture the sample state before the trace is moved into `queued`
let sampled = packet.trace.is_sampled();
// TODO: optimisation for the future: rather than constantly using legacy encoding,
// use the mix packet type / flags to pick encoding per packet
let legacy = self.config.use_legacy_packet_encoding;
let queued = packet.map(|p| FramedNymPacket::from_mix_packet(p, legacy));
let framed_packet =
FramedNymPacket::from_mix_packet(packet, self.config.use_legacy_packet_encoding);
let Some(sender) = self.active_connections.get_mut(&address) else {
// there was never a connection to begin with
@@ -627,7 +332,7 @@ impl SendWithoutResponse for Client {
result = "not_connected",
"establishing initial connection to {address}"
);
self.make_connection(address, queued);
self.make_connection(address, framed_packet);
return Err(io::Error::new(
io::ErrorKind::NotConnected,
"connection is in progress",
@@ -638,12 +343,7 @@ impl SendWithoutResponse for Client {
let channel_available = sender.channel.capacity();
let channel_used = channel_capacity - channel_available;
// record how full this peer's egress buffer was (sampled packets only, to bound cost)
if sampled {
crate::metrics::observe_egress_buffer_fill(channel_used, channel_capacity);
}
let sending_res = sender.channel.try_send(queued);
let sending_res = sender.channel.try_send(framed_packet);
drop(sender);
sending_res.map_err(|err| {
@@ -698,8 +398,6 @@ mod tests {
initial_connection_timeout: Duration::from_millis(1_500),
maximum_connection_buffer_size: 128,
use_legacy_packet_encoding: false,
connection_idle_timeout: Duration::from_secs(300),
connection_write_timeout: Duration::from_millis(500),
},
NoiseConfig::new(
Arc::new(x25519::KeyPair::new(&mut rng)),
@@ -737,143 +435,4 @@ mod tests {
client.config.maximum_reconnection_backoff
);
}
fn test_addr() -> SocketAddr {
"127.0.0.1:1".parse().unwrap()
}
fn insert_with_token(
active: &ActiveConnections,
addr: SocketAddr,
token: Arc<()>,
) -> mpsc::Receiver<Traced<FramedNymPacket>> {
let (tx, rx) = mpsc::channel(1);
active.insert(addr, ConnectionSender::new(tx, token));
rx
}
#[test]
fn evict_on_drop_removes_entry_when_token_still_matches() {
let active = ActiveConnections::default();
let addr = test_addr();
let token = Arc::new(());
let _rx = insert_with_token(&active, addr, Arc::clone(&token));
assert!(active.get(&addr).is_some());
{
let _guard = EvictOnDrop {
active_connections: active.clone(),
address: addr,
handle_token: token,
};
}
assert!(
active.get(&addr).is_none(),
"owning task's drop should evict the entry"
);
}
#[test]
fn evict_on_drop_preserves_entry_replaced_by_newer_make_connection() {
// Simulates the race: old task's run() has returned, but before its
// drop guard fires, a concurrent `make_connection` replaced the
// entry's channel + handle_token with a fresh task's token.
let active = ActiveConnections::default();
let addr = test_addr();
let old_token = Arc::new(());
let new_token = Arc::new(());
let _rx_new = insert_with_token(&active, addr, Arc::clone(&new_token));
{
let _guard = EvictOnDrop {
active_connections: active.clone(),
address: addr,
handle_token: old_token,
};
}
assert!(
active.get(&addr).is_some(),
"old task's drop must not clobber the newer entry"
);
}
#[tokio::test]
async fn io_loop_exits_when_peer_closes_idle_connection() {
// The fix's second half: while no packets are flowing, peer FIN/RST
// must still be observed so the cache entry can be evicted before the
// next send finds it stale.
let (a, b) = tokio::io::duplex(64);
let conn = Framed::new(a, NymCodec);
let (_tx, rx) = mpsc::channel(1);
// idle reaping disabled so only the peer-close path is exercised
let task = tokio::spawn(run_io_loop(
conn,
ReceiverStream::new(rx),
test_addr(),
Duration::ZERO,
Duration::ZERO,
));
// Simulate peer closing both directions of the connection.
drop(b);
tokio::time::timeout(Duration::from_secs(1), task)
.await
.expect("io_loop must notice peer close while idle")
.expect("io_loop task must not panic");
}
#[tokio::test]
async fn io_loop_exits_when_sender_dropped() {
let (a, _b) = tokio::io::duplex(64);
let conn = Framed::new(a, NymCodec);
let (tx, rx) = mpsc::channel(1);
let task = tokio::spawn(run_io_loop(
conn,
ReceiverStream::new(rx),
test_addr(),
Duration::ZERO,
Duration::ZERO,
));
drop(tx);
tokio::time::timeout(Duration::from_secs(1), task)
.await
.expect("io_loop must exit when the upstream sender is dropped")
.expect("io_loop task must not panic");
}
#[tokio::test(start_paused = true)]
async fn io_loop_closes_idle_connection() {
// With no packets sent and the peer still connected, the idle timeout must eventually
// close the connection so the task/socket don't linger forever. The paused clock is
// virtual - it auto-advances to the next timer, so this completes instantly despite the
// durations below (no real waiting).
let (a, _b) = tokio::io::duplex(64);
let conn = Framed::new(a, NymCodec);
// keep the sender alive so the sender-dropped path can't fire instead
let (_tx, rx) = mpsc::channel(1);
let idle_timeout = Duration::from_millis(50);
let task = tokio::spawn(run_io_loop(
conn,
ReceiverStream::new(rx),
test_addr(),
idle_timeout,
Duration::ZERO,
));
// auto-advance fires the nearest timer (the 50ms idle deadline, sooner than this 500ms
// guard) once the task is otherwise idle, reaping the connection
tokio::time::timeout(Duration::from_millis(500), task)
.await
.expect("io_loop must close the connection after the idle timeout")
.expect("io_loop task must not panic");
}
}
@@ -1,7 +1,6 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::metrics::PacketTrace;
use futures::channel::mpsc;
use futures::channel::mpsc::SendError;
use nym_sphinx::forwarding::packet::MixPacket;
@@ -22,16 +21,12 @@ impl From<mpsc::UnboundedSender<PacketToForward>> for MixForwardingSender {
}
impl MixForwardingSender {
pub fn forward_packet(&self, packet: PacketToForward) -> Result<(), SendError> {
pub fn forward_packet(&self, packet: impl Into<PacketToForward>) -> Result<(), SendError> {
self.0
.unbounded_send(packet)
.unbounded_send(packet.into())
.map_err(|err| err.into_send_error())
}
pub fn forward_client_packet_without_delay(&self, packet: MixPacket) -> Result<(), SendError> {
self.forward_packet(PacketToForward::client_packet_without_delay(packet))
}
#[allow(clippy::len_without_is_empty)]
pub fn len(&self) -> usize {
self.0.len()
@@ -43,28 +38,35 @@ pub type MixForwardingReceiver = mpsc::UnboundedReceiver<PacketToForward>;
pub struct PacketToForward {
pub packet: MixPacket,
pub forward_delay_target: Option<Instant>,
pub network_monitor_packet: bool,
/// Latency breadcrumb started at packet receive; stamped as the packet moves through the
/// forwarder and egress stages. `PacketTrace::Off` for untraced packets (e.g. acks).
pub trace: PacketTrace,
}
impl From<MixPacket> for PacketToForward {
fn from(packet: MixPacket) -> Self {
PacketToForward::new_no_delay(packet)
}
}
impl From<(MixPacket, Option<Instant>)> for PacketToForward {
fn from((packet, delay_until): (MixPacket, Option<Instant>)) -> Self {
PacketToForward::new(packet, delay_until)
}
}
impl From<(MixPacket, Instant)> for PacketToForward {
fn from((packet, delay_until): (MixPacket, Instant)) -> Self {
PacketToForward::new(packet, Some(delay_until))
}
}
impl PacketToForward {
pub fn new(
packet: MixPacket,
forward_delay_target: Option<Instant>,
network_monitor_packet: bool,
trace: PacketTrace,
) -> Self {
pub fn new(packet: MixPacket, forward_delay_target: Option<Instant>) -> Self {
PacketToForward {
packet,
forward_delay_target,
network_monitor_packet,
trace,
}
}
pub fn client_packet_without_delay(packet: MixPacket) -> Self {
Self::new(packet, None, false, PacketTrace::Off)
pub fn new_no_delay(packet: MixPacket) -> Self {
Self::new(packet, None)
}
}
@@ -4,7 +4,6 @@
#[cfg(feature = "client")]
pub mod client;
pub mod forwarder;
pub mod metrics;
#[cfg(feature = "client")]
pub use client::{Client, Config, SendWithoutResponse};
@@ -1,311 +0,0 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use strum::{AsRefStr, EnumIter, EnumProperty, IntoEnumIterator};
use tokio::time::Instant;
/// Histogram buckets (seconds) for per-stage and total packet latency: exponential, ~100us .. ~6.5s.
/// Shared by every latency stage so the waterfall is directly comparable; the top finite bucket is
/// intentionally high so a rare multi-second processing spike is measured with magnitude rather than
/// being clipped into the `+Inf` overflow.
const STAGE_LATENCY_BUCKETS: [f64; 17] = [
0.0001, 0.0002, 0.0004, 0.0008, 0.0016, 0.0032, 0.0064, 0.0128, 0.0256, 0.0512, 0.1024, 0.2048,
0.4096, 0.8192, 1.6384, 3.2768, 6.5536,
];
/// Count buckets (1 .. MAX_DRAIN_BATCH) for the forwarder drain-batch-size histogram.
const DRAIN_BATCH_BUCKETS: [f64; 9] = [1.0, 2.0, 4.0, 8.0, 16.0, 32.0, 64.0, 128.0, 256.0];
/// Fill-ratio buckets (used/capacity) for the per-connection egress buffer. A ratio near 1.0 means
/// the buffer is close to full and packets to that peer are about to be dropped.
const EGRESS_FILL_BUCKETS: [f64; 9] = [0.05, 0.1, 0.25, 0.5, 0.75, 0.9, 0.95, 0.99, 1.0];
/// Every histogram this crate emits, defined in one place. `AsRefStr` (`#[strum(to_string=...)]`)
/// gives the prometheus metric name - the bare `mixnet_packet_*` family, with no per-crate prefix
/// since this is a shared library writing straight to the process-global registry. The `help` prop
/// gives the description and [`MixnetMetric::buckets`] gives the bucket layout.
///
/// Register the whole family at boot with [`register_all`]. Latency-stage variants are observed via
/// the [`PacketTrace`] stopwatch; the auxiliary variants via the `observe_*` helpers. (Passing an
/// auxiliary variant to `PacketTrace::record` is meaningless but harmless.)
#[derive(Clone, Copy, EnumIter, AsRefStr, EnumProperty)]
pub enum MixnetMetric {
// ----- latency stages: the per-packet waterfall, recorded via `PacketTrace` -----
/// receive -> sphinx unwrap (partial: shared secret + header MAC)
#[strum(to_string = "mixnet_packet_stage_unwrap_seconds")]
#[strum(props(help = "Seconds spent unwrapping a received sphinx packet"))]
Unwrap,
/// unwrap -> replay-check + finalise (includes the deferral wait)
#[strum(to_string = "mixnet_packet_stage_replay_check_seconds")]
#[strum(props(
help = "Seconds from partial-unwrap to replay-check + finalise (includes the deferral wait)"
))]
ReplayCheck,
/// wait in the ingress -> forwarder channel
#[strum(to_string = "mixnet_packet_stage_forwarder_queue_seconds")]
#[strum(props(
help = "Seconds a forwarded packet waited in the ingress-to-forwarder channel"
))]
ForwarderQueue,
/// the (intended) mix delay
#[strum(to_string = "mixnet_packet_stage_delay_queue_seconds")]
#[strum(props(help = "Seconds a forwarded packet spent in the (intended) mix delay queue"))]
DelayQueue,
/// diagnostic overlay on `DelayQueue`: how late beyond the target release the packet was
/// actually forwarded (delay-queue scheduling/retrieval overhead, measured vs the deadline)
#[strum(to_string = "mixnet_packet_stage_delay_queue_overrun_seconds")]
#[strum(props(
help = "Seconds a delayed packet was forwarded beyond its target release time (delay-queue scheduling/retrieval overhead)"
))]
DelayQueueOverrun,
/// wait in the per-connection egress buffer
#[strum(to_string = "mixnet_packet_stage_egress_queue_seconds")]
#[strum(props(
help = "Seconds a forwarded packet waited in the per-connection egress buffer"
))]
EgressQueue,
/// flushing the packet batch to the socket
#[strum(to_string = "mixnet_packet_stage_socket_write_seconds")]
#[strum(props(help = "Seconds spent flushing a forwarded packet batch to the socket"))]
SocketWrite,
/// end-to-end: receive -> socket write
#[strum(to_string = "mixnet_packet_total_latency_seconds")]
#[strum(props(help = "Total in-node latency of a forwarded packet, receive to socket write"))]
Total,
// ----- auxiliary histograms: observed directly, not part of the latency waterfall -----
/// number of packets the forwarder drained from the ingress channel per wakeup
#[strum(to_string = "mixnet_packet_forwarder_drain_batch_size")]
#[strum(props(
help = "Number of ingress packets the forwarder drained per select! wakeup (batch size)"
))]
ForwarderDrainBatchSize,
/// number of expired packets the forwarder drained from the delay queue per wakeup
#[strum(to_string = "mixnet_packet_forwarder_delay_drain_batch_size")]
#[strum(props(
help = "Number of expired delay-queue packets the forwarder drained per select! wakeup (batch size)"
))]
ForwarderDelayDrainBatchSize,
/// per-connection egress buffer occupancy (used/capacity) at send time
#[strum(to_string = "mixnet_packet_egress_buffer_fill_ratio")]
#[strum(props(
help = "Per-connection egress buffer fill ratio (used/capacity) sampled at packet send time"
))]
EgressBufferFillRatio,
}
impl MixnetMetric {
/// Histogram bucket layout for this metric.
fn buckets(&self) -> &'static [f64] {
match self {
MixnetMetric::ForwarderDrainBatchSize | MixnetMetric::ForwarderDelayDrainBatchSize => {
&DRAIN_BATCH_BUCKETS
}
MixnetMetric::EgressBufferFillRatio => &EGRESS_FILL_BUCKETS,
// every latency stage shares the seconds buckets
_ => &STAGE_LATENCY_BUCKETS,
}
}
}
/// Pre-register every histogram (at zero) into the global metrics registry so the whole
/// `mixnet_packet_*` family is present on the prometheus endpoint from boot, before anything has
/// been observed. Idempotent.
pub fn register_all() {
let registry = nym_metrics::metrics_registry();
for metric in MixnetMetric::iter() {
registry.register_histogram(
metric.as_ref(),
metric.get_str("help"),
Some(metric.buckets()),
);
}
}
/// Observe a value into a metric's histogram in the process-global registry.
fn observe(metric: MixnetMetric, value: f64) {
nym_metrics::metrics_registry().maybe_register_and_add_to_histogram(
metric.as_ref(),
value,
Some(metric.buckets()),
metric.get_str("help"),
);
}
/// Observe how many ingress-channel packets the forwarder drained in a single wakeup.
pub fn observe_drain_batch_size(batch_size: usize) {
observe(MixnetMetric::ForwarderDrainBatchSize, batch_size as f64);
}
/// Observe how many expired delay-queue packets the forwarder drained in a single wakeup.
pub fn observe_delay_drain_batch_size(batch_size: usize) {
observe(
MixnetMetric::ForwarderDelayDrainBatchSize,
batch_size as f64,
);
}
/// Observe how full a per-connection egress buffer was when a packet was queued for it.
pub fn observe_egress_buffer_fill(used: usize, capacity: usize) {
if capacity == 0 {
return;
}
observe(
MixnetMetric::EgressBufferFillRatio,
used as f64 / capacity as f64,
);
}
/// A lightweight per-packet stopwatch for attributing forwarding latency to pipeline
/// stages. Unsampled packets carry the `Off` variant and do zero clock reads, so the only
/// cost on the hot path is moving a small `Copy` value and a branch.
#[derive(Clone, Copy)]
pub enum PacketTrace {
Off,
On {
received_at: Instant,
stage_at: Instant,
},
}
impl PacketTrace {
/// Begin tracing. Reads the clock only for sampled packets.
pub fn start(sampled: bool) -> Self {
if sampled {
let now = Instant::now();
PacketTrace::On {
received_at: now,
stage_at: now,
}
} else {
PacketTrace::Off
}
}
/// Whether this packet is being traced (sampled).
pub fn is_sampled(&self) -> bool {
matches!(self, PacketTrace::On { .. })
}
/// Seconds spent in the stage just completed, advancing the cursor to now.
/// Returns `None` for unsampled packets.
fn lap(&mut self) -> Option<f64> {
match self {
PacketTrace::Off => None,
PacketTrace::On { stage_at, .. } => {
let now = Instant::now();
let secs = now.duration_since(*stage_at).as_secs_f64();
*stage_at = now;
Some(secs)
}
}
}
/// Seconds since tracing began (i.e. since the packet was received), or `None` if unsampled.
fn total(&self) -> Option<f64> {
match self {
PacketTrace::Off => None,
PacketTrace::On { received_at, .. } => {
Some(Instant::now().duration_since(*received_at).as_secs_f64())
}
}
}
/// Close out the stage just completed: lap the timer and, only if the packet is sampled,
/// observe `stage`'s latency histogram.
pub fn record(&mut self, stage: MixnetMetric) {
if let Some(secs) = self.lap() {
observe(stage, secs);
}
}
/// Observe the end-to-end [`MixnetMetric::Total`] latency (since receive) if sampled. Unlike
/// [`PacketTrace::record`] this does not lap, so it can be called at the very end.
pub fn record_total(&self) {
if let Some(secs) = self.total() {
observe(MixnetMetric::Total, secs);
}
}
/// Observe an explicit `secs` value for `stage` if the packet is sampled, without lapping the
/// stage cursor. For diagnostics that don't fit the sequential waterfall (e.g. delay-queue
/// overrun, measured against the target deadline rather than the previous stage).
pub fn record_value(&self, stage: MixnetMetric, secs: f64) {
if matches!(self, PacketTrace::On { .. }) {
observe(stage, secs);
}
}
}
/// A value paired with its in-flight latency trace, so the trace rides along as the value is
/// moved between pipeline stages (and transformed via [`Traced::map`]). Used wherever a packet
/// crosses a queue/channel: replay batch, delay queue, egress channel.
pub struct Traced<T> {
pub inner: T,
pub trace: PacketTrace,
}
impl<T> Traced<T> {
pub fn new(inner: T, trace: PacketTrace) -> Self {
Traced { inner, trace }
}
/// Transform the carried value, keeping the same trace.
pub fn map<U>(self, f: impl FnOnce(T) -> U) -> Traced<U> {
Traced {
inner: f(self.inner),
trace: self.trace,
}
}
/// Record the stage just completed for the carried trace (see [`PacketTrace::record`]).
pub fn record(&mut self, stage: MixnetMetric) {
self.trace.record(stage)
}
/// Observe an explicit value for the carried trace (see [`PacketTrace::record_value`]).
pub fn record_value(&self, stage: MixnetMetric, secs: f64) {
self.trace.record_value(stage, secs)
}
}
#[cfg(test)]
mod tests {
use super::*;
// guards that AsRefStr honours `#[strum(to_string = ...)]` (rather than falling back to the
// variant name), that every metric is in the `mixnet_packet_*` family, and carries a help
// string, and that each metric resolves to a bucket layout.
#[test]
fn every_metric_has_a_mixnet_packet_name_help_and_buckets() {
for metric in MixnetMetric::iter() {
assert!(
metric.as_ref().starts_with("mixnet_packet_"),
"unexpected metric name: {}",
metric.as_ref()
);
assert!(
metric.get_str("help").is_some(),
"missing help for {}",
metric.as_ref()
);
assert!(
!metric.buckets().is_empty(),
"missing buckets for {}",
metric.as_ref()
);
}
assert_eq!(
MixnetMetric::Unwrap.as_ref(),
"mixnet_packet_stage_unwrap_seconds"
);
assert_eq!(
MixnetMetric::Total.as_ref(),
"mixnet_packet_total_latency_seconds"
);
assert_eq!(
MixnetMetric::ForwarderDrainBatchSize.as_ref(),
"mixnet_packet_forwarder_drain_batch_size"
);
}
}
@@ -26,7 +26,6 @@ nym-ecash-contract-common = { workspace = true }
nym-multisig-contract-common = { workspace = true }
nym-group-contract-common = { workspace = true }
nym-performance-contract-common = { workspace = true }
nym-network-monitors-contract-common = { workspace = true }
nym-node-families-contract-common = { workspace = true }
nym-serde-helpers = { workspace = true, features = ["hex", "base64"] }
serde = { workspace = true, features = ["derive"] }
@@ -104,14 +104,6 @@ impl TryFrom<NymNetworkDetails> for Config {
}
impl Config {
pub fn new(nyxd_url: Url, api_url: Url, nyxd_config: nyxd::Config) -> Self {
Config {
api_url,
nyxd_url,
nyxd_config,
}
}
pub fn try_from_nym_network_details(
details: &NymNetworkDetails,
) -> Result<Self, ValidatorClientError> {
@@ -122,15 +114,6 @@ impl Config {
.map(|url| Url::parse(url))
.collect::<Result<Vec<_>, _>>()?;
if let Some(nym_api_urls) = details.nym_api_urls.as_ref() {
api_url.extend(
nym_api_urls
.iter()
.map(|url| url.url.parse())
.collect::<Result<Vec<_>, _>>()?,
);
}
if api_url.is_empty() {
return Err(ValidatorClientError::NoAPIUrlAvailable);
}
@@ -15,16 +15,12 @@ use nym_api_requests::ecash::models::{
VerifyEcashTicketBody,
};
use nym_api_requests::ecash::VerificationKeyResponse;
use nym_api_requests::models::network_monitor::{
KnownNetworkMonitorResponse, StressTestBatchSubmission,
};
use nym_api_requests::models::node_families::NodeFamily;
use nym_api_requests::models::{
AnnotationResponseV1, AnnotationResponseV2, ApiHealthResponse, BinaryBuildInformationOwned,
ChainBlocksStatusResponse, ChainStatusResponse, KeyRotationInfoResponse,
NodePerformanceResponse, NodeRefreshBody, NymNodeDescriptionV1, NymNodeDescriptionV2,
PerformanceHistoryResponse, RewardedSetResponse, SignerInformationResponse,
StressTestBatchSubmissionResponse,
AnnotationResponse, ApiHealthResponse, BinaryBuildInformationOwned, ChainBlocksStatusResponse,
ChainStatusResponse, KeyRotationInfoResponse, NodePerformanceResponse, NodeRefreshBody,
NymNodeDescriptionV1, NymNodeDescriptionV2, PerformanceHistoryResponse, RewardedSetResponse,
SignerInformationResponse,
};
use nym_api_requests::pagination::PaginatedResponse;
use nym_http_api_client::{ApiClient, NO_PARAMS};
@@ -1020,7 +1016,7 @@ pub trait NymApiClientExt: ApiClient {
async fn get_node_annotation(
&self,
node_id: NodeId,
) -> Result<AnnotationResponseV1, NymAPIError> {
) -> Result<AnnotationResponse, NymAPIError> {
self.get_json(
&[
routes::V1_API_VERSION,
@@ -1033,22 +1029,6 @@ pub trait NymApiClientExt: ApiClient {
.await
}
async fn get_node_annotation_v2(
&self,
node_id: NodeId,
) -> Result<AnnotationResponseV2, NymAPIError> {
self.get_json(
&[
routes::V2_API_VERSION,
routes::NYM_NODES_ROUTES,
routes::NYM_NODES_ANNOTATION,
&node_id.to_string(),
],
NO_PARAMS,
)
.await
}
#[deprecated]
async fn get_mixnode_avg_uptime(&self, mix_id: NodeId) -> Result<UptimeResponse, NymAPIError> {
self.get_json(
@@ -1419,53 +1399,6 @@ pub trait NymApiClientExt: ApiClient {
Ok(SemiSkimmedNodesWithMetadata::new(nodes, metadata))
}
/// Queries the nym-api for whether a particular ed25519 identity key is currently recognised
/// as an authorised network monitor permitted to submit stress testing results.
///
/// `identity_key` is expected to be the base58-encoded form of the ed25519 public key.
#[instrument(level = "debug", skip(self))]
async fn get_known_network_monitor(
&self,
identity_key: IdentityKeyRef<'_>,
) -> Result<KnownNetworkMonitorResponse, NymAPIError> {
self.get_json(
&[
routes::V3_API_VERSION,
routes::NYM_NODES_ROUTES,
routes::STRESS_TESTING,
routes::STRESS_TESTING_KNOWN_MONITORS,
identity_key,
],
NO_PARAMS,
)
.await
}
/// Submit a signed batch of stress-testing results to nym-api on behalf of a network monitor
/// orchestrator.
///
/// The caller is expected to have produced `request` via
/// `StressTestBatchSubmissionContent::new(...)` and signed it with the orchestrator's ed25519
/// key; nym-api will reject submissions that are stale, replayed, unauthorised, or whose
/// signature fails to verify.
#[instrument(level = "debug", skip(self, request))]
async fn submit_stress_testing_results(
&self,
request: &StressTestBatchSubmission,
) -> Result<StressTestBatchSubmissionResponse, NymAPIError> {
self.post_json(
&[
routes::V3_API_VERSION,
routes::NYM_NODES_ROUTES,
routes::STRESS_TESTING,
routes::STRESS_TESTING_BATCH_SUBMIT,
],
NO_PARAMS,
request,
)
.await
}
}
// Client is already nym_http_api_client::Client (re-exported above), so just one impl needed
@@ -50,9 +50,6 @@ pub mod nym_nodes {
pub const NYM_NODES_REWARDED_SET: &str = "rewarded-set";
pub const NYM_NODES_REFRESH_DESCRIBED: &str = "refresh-described";
pub const BY_ADDRESSES: &str = "by-addresses";
pub const STRESS_TESTING: &str = "stress-testing";
pub const STRESS_TESTING_KNOWN_MONITORS: &str = "known-monitors";
pub const STRESS_TESTING_BATCH_SUBMIT: &str = "batch-submit";
}
pub const STATUS_ROUTES: &str = "status";
@@ -13,7 +13,6 @@ pub mod ecash_query_client;
pub mod group_query_client;
pub mod mixnet_query_client;
pub mod multisig_query_client;
pub mod network_monitors_query_client;
pub mod node_families_query_client;
pub mod performance_query_client;
pub mod vesting_query_client;
@@ -24,7 +23,6 @@ pub mod ecash_signing_client;
pub mod group_signing_client;
pub mod mixnet_signing_client;
pub mod multisig_signing_client;
pub mod network_monitors_signing_client;
pub mod node_families_signing_client;
pub mod performance_signing_client;
pub mod vesting_signing_client;
@@ -35,9 +33,6 @@ pub use ecash_query_client::{EcashQueryClient, PagedEcashQueryClient};
pub use group_query_client::{GroupQueryClient, PagedGroupQueryClient};
pub use mixnet_query_client::{MixnetQueryClient, PagedMixnetQueryClient};
pub use multisig_query_client::{MultisigQueryClient, PagedMultisigQueryClient};
pub use network_monitors_query_client::{
NetworkMonitorsQueryClient, PagedNetworkMonitorsQueryClient,
};
pub use node_families_query_client::{NodeFamiliesQueryClient, PagedNodeFamiliesQueryClient};
pub use performance_query_client::{PagedPerformanceQueryClient, PerformanceQueryClient};
pub use vesting_query_client::{PagedVestingQueryClient, VestingQueryClient};
@@ -48,7 +43,6 @@ pub use ecash_signing_client::EcashSigningClient;
pub use group_signing_client::GroupSigningClient;
pub use mixnet_signing_client::MixnetSigningClient;
pub use multisig_signing_client::MultisigSigningClient;
pub use network_monitors_signing_client::NetworkMonitorsSigningClient;
pub use node_families_signing_client::NodeFamiliesSigningClient;
pub use performance_signing_client::PerformanceSigningClient;
pub use vesting_signing_client::VestingSigningClient;
@@ -59,7 +53,6 @@ pub trait NymContractsProvider {
fn mixnet_contract_address(&self) -> Option<&AccountId>;
fn vesting_contract_address(&self) -> Option<&AccountId>;
fn performance_contract_address(&self) -> Option<&AccountId>;
fn network_monitors_contract_address(&self) -> Option<&AccountId>;
fn node_families_contract_address(&self) -> Option<&AccountId>;
// coconut-related
@@ -74,7 +67,6 @@ pub struct TypedNymContracts {
pub mixnet_contract_address: Option<AccountId>,
pub vesting_contract_address: Option<AccountId>,
pub performance_contract_address: Option<AccountId>,
pub network_monitors_contract_address: Option<AccountId>,
pub node_families_contract_address: Option<AccountId>,
pub ecash_contract_address: Option<AccountId>,
@@ -100,10 +92,6 @@ impl TryFrom<NymContracts> for TypedNymContracts {
.performance_contract_address
.map(|addr| addr.parse())
.transpose()?,
network_monitors_contract_address: value
.network_monitors_contract_address
.map(|addr| addr.parse())
.transpose()?,
node_families_contract_address: value
.node_families_contract_address
.map(|addr| addr.parse())
@@ -1,107 +0,0 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::collect_paged;
use crate::nyxd::contract_traits::NymContractsProvider;
use crate::nyxd::error::NyxdError;
use crate::nyxd::CosmWasmClient;
use async_trait::async_trait;
use nym_network_monitors_contract_common::{
AuthorisedNetworkMonitor, AuthorisedNetworkMonitorOrchestratorsResponse,
AuthorisedNetworkMonitorsPagedResponse, QueryMsg as NetworkMonitorsQueryMsg,
};
use serde::Deserialize;
use std::net::SocketAddr;
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait NetworkMonitorsQueryClient {
async fn query_network_monitors_contract<T>(
&self,
query: NetworkMonitorsQueryMsg,
) -> Result<T, NyxdError>
where
for<'a> T: Deserialize<'a>;
async fn get_admin(&self) -> Result<cw_controllers::AdminResponse, NyxdError> {
self.query_network_monitors_contract(NetworkMonitorsQueryMsg::Admin {})
.await
}
async fn get_network_monitor_orchestrators(
&self,
) -> Result<AuthorisedNetworkMonitorOrchestratorsResponse, NyxdError> {
self.query_network_monitors_contract(
NetworkMonitorsQueryMsg::NetworkMonitorOrchestrators {},
)
.await
}
async fn get_network_monitor_agents_paged(
&self,
start_next_after: Option<SocketAddr>,
limit: Option<u32>,
) -> Result<AuthorisedNetworkMonitorsPagedResponse, NyxdError> {
self.query_network_monitors_contract(NetworkMonitorsQueryMsg::NetworkMonitorAgents {
start_next_after,
limit,
})
.await
}
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait PagedNetworkMonitorsQueryClient: NetworkMonitorsQueryClient {
async fn get_all_network_monitor_agents(
&self,
) -> Result<Vec<AuthorisedNetworkMonitor>, NyxdError> {
collect_paged!(self, get_network_monitor_agents_paged, authorised)
}
}
#[async_trait]
impl<T> PagedNetworkMonitorsQueryClient for T where T: NetworkMonitorsQueryClient {}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl<C> NetworkMonitorsQueryClient for C
where
C: CosmWasmClient + NymContractsProvider + Send + Sync,
{
async fn query_network_monitors_contract<T>(
&self,
query: NetworkMonitorsQueryMsg,
) -> Result<T, NyxdError>
where
for<'a> T: Deserialize<'a>,
{
let contract_address = &self
.network_monitors_contract_address()
.ok_or_else(|| NyxdError::unavailable_contract_address("network monitors contract"))?;
self.query_contract_smart(contract_address, &query).await
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::nyxd::contract_traits::tests::IgnoreValue;
// it's enough that this compiles and clippy is happy about it
#[allow(dead_code)]
fn all_query_variants_are_covered<C: NetworkMonitorsQueryClient + Send + Sync>(
client: C,
msg: NetworkMonitorsQueryMsg,
) {
match msg {
NetworkMonitorsQueryMsg::Admin {} => client.get_admin().ignore(),
NetworkMonitorsQueryMsg::NetworkMonitorOrchestrators {} => {
client.get_network_monitor_orchestrators().ignore()
}
NetworkMonitorsQueryMsg::NetworkMonitorAgents { .. } => {
client.get_network_monitor_agents_paged(None, None).ignore()
}
};
}
}
@@ -1,205 +0,0 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::nyxd::contract_traits::NymContractsProvider;
use crate::nyxd::cosmwasm_client::types::ExecuteResult;
use crate::nyxd::error::NyxdError;
use crate::nyxd::{Coin, Fee, SigningCosmWasmClient};
use crate::signing::signer::OfflineSigner;
use async_trait::async_trait;
use nym_network_monitors_contract_common::ExecuteMsg as NetworkMonitorsExecuteMsg;
use std::net::SocketAddr;
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait NetworkMonitorsSigningClient {
async fn execute_network_monitors_contract(
&self,
fee: Option<Fee>,
msg: NetworkMonitorsExecuteMsg,
memo: String,
funds: Vec<Coin>,
) -> Result<ExecuteResult, NyxdError>;
async fn update_admin(
&self,
admin: String,
fee: Option<Fee>,
) -> Result<ExecuteResult, NyxdError> {
let msg = NetworkMonitorsExecuteMsg::UpdateAdmin { admin };
self.execute_network_monitors_contract(
fee,
msg,
"NetworkMonitorsExecuteMsg::UpdateAdmin".into(),
vec![],
)
.await
}
async fn authorise_network_monitor_orchestrator(
&self,
address: String,
fee: Option<Fee>,
) -> Result<ExecuteResult, NyxdError> {
let msg = NetworkMonitorsExecuteMsg::AuthoriseNetworkMonitorOrchestrator { address };
self.execute_network_monitors_contract(
fee,
msg,
"NetworkMonitorsExecuteMsg::AuthoriseNetworkMonitorOrchestrator".into(),
vec![],
)
.await
}
/// Announce (or rotate) the ed25519 identity key of the calling network monitor orchestrator.
///
/// The caller must already be an authorised orchestrator; the contract validates that
/// `identity_key` is a well-formed base-58 encoding of a 32-byte ed25519 public key.
async fn update_orchestrator_identity_key(
&self,
identity_key: String,
fee: Option<Fee>,
) -> Result<ExecuteResult, NyxdError> {
let msg = NetworkMonitorsExecuteMsg::UpdateOrchestratorIdentityKey { key: identity_key };
self.execute_network_monitors_contract(
fee,
msg,
"NetworkMonitorsExecuteMsg::UpdateOrchestratorIdentityKey".into(),
vec![],
)
.await
}
async fn revoke_network_monitor_orchestrator(
&self,
address: String,
fee: Option<Fee>,
) -> Result<ExecuteResult, NyxdError> {
let msg = NetworkMonitorsExecuteMsg::RevokeNetworkMonitorOrchestrator { address };
self.execute_network_monitors_contract(
fee,
msg,
"NetworkMonitorsExecuteMsg::RevokeNetworkMonitorOrchestrator".into(),
vec![],
)
.await
}
async fn authorise_network_monitor(
&self,
mixnet_address: SocketAddr,
bs58_x25519_noise: String,
noise_version: u8,
fee: Option<Fee>,
) -> Result<ExecuteResult, NyxdError> {
let msg = NetworkMonitorsExecuteMsg::AuthoriseNetworkMonitor {
mixnet_address,
bs58_x25519_noise,
noise_version,
};
self.execute_network_monitors_contract(
fee,
msg,
"NetworkMonitorsExecuteMsg::AuthoriseNetworkMonitor".into(),
vec![],
)
.await
}
async fn revoke_network_monitor(
&self,
address: SocketAddr,
fee: Option<Fee>,
) -> Result<ExecuteResult, NyxdError> {
let msg = NetworkMonitorsExecuteMsg::RevokeNetworkMonitor { address };
self.execute_network_monitors_contract(
fee,
msg,
"NetworkMonitorsExecuteMsg::RevokeNetworkMonitor".into(),
vec![],
)
.await
}
async fn revoke_all_network_monitors(
&self,
fee: Option<Fee>,
) -> Result<ExecuteResult, NyxdError> {
let msg = NetworkMonitorsExecuteMsg::RevokeAllNetworkMonitors;
self.execute_network_monitors_contract(
fee,
msg,
"NetworkMonitorsExecuteMsg::RevokeAllNetworkMonitors".into(),
vec![],
)
.await
}
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl<C> NetworkMonitorsSigningClient for C
where
C: SigningCosmWasmClient + NymContractsProvider + Sync,
NyxdError: From<<Self as OfflineSigner>::Error>,
{
async fn execute_network_monitors_contract(
&self,
fee: Option<Fee>,
msg: NetworkMonitorsExecuteMsg,
memo: String,
funds: Vec<Coin>,
) -> Result<ExecuteResult, NyxdError> {
let contract_address = &self
.network_monitors_contract_address()
.ok_or_else(|| NyxdError::unavailable_contract_address("network monitors contract"))?;
let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier())));
let signer_address = &self.signer_addresses()[0];
self.execute(signer_address, contract_address, &msg, fee, memo, funds)
.await
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::nyxd::contract_traits::tests::IgnoreValue;
use nym_network_monitors_contract_common::ExecuteMsg;
// it's enough that this compiles and clippy is happy about it
#[allow(dead_code)]
fn all_execute_variants_are_covered<C: NetworkMonitorsSigningClient + Send + Sync>(
client: C,
msg: NetworkMonitorsExecuteMsg,
) {
match msg {
NetworkMonitorsExecuteMsg::UpdateAdmin { admin } => {
client.update_admin(admin, None).ignore()
}
ExecuteMsg::AuthoriseNetworkMonitorOrchestrator { address } => client
.authorise_network_monitor_orchestrator(address, None)
.ignore(),
ExecuteMsg::UpdateOrchestratorIdentityKey { key } => {
client.update_orchestrator_identity_key(key, None).ignore()
}
ExecuteMsg::RevokeNetworkMonitorOrchestrator { address } => client
.revoke_network_monitor_orchestrator(address, None)
.ignore(),
ExecuteMsg::AuthoriseNetworkMonitor {
mixnet_address: address,
bs58_x25519_noise,
noise_version,
} => client
.authorise_network_monitor(address, bs58_x25519_noise, noise_version, None)
.ignore(),
ExecuteMsg::RevokeNetworkMonitor { address } => {
client.revoke_network_monitor(address, None).ignore()
}
ExecuteMsg::RevokeAllNetworkMonitors => {
client.revoke_all_network_monitors(None).ignore()
}
};
}
}
@@ -7,12 +7,12 @@ use crate::nyxd::error::NyxdError;
use crate::nyxd::CosmWasmClient;
use async_trait::async_trait;
use cosmrs::AccountId;
use nym_mixnet_contract_common::NodeId;
use serde::Deserialize;
use nym_mixnet_contract_common::NodeId;
pub use nym_node_families_contract_common::{
msg::QueryMsg as NodeFamiliesQueryMsg, AllFamilyMembersPagedResponse,
AllPastFamilyInvitationsPagedResponse, Config, FamiliesPagedResponse, FamilyMemberRecord,
AllPastFamilyInvitationsPagedResponse, FamiliesPagedResponse, FamilyMemberRecord,
FamilyMembersPagedResponse, GlobalPastFamilyInvitationCursor, NodeFamily,
NodeFamilyByNameResponse, NodeFamilyByOwnerResponse, NodeFamilyId,
NodeFamilyMembershipResponse, NodeFamilyResponse, PastFamilyInvitation,
@@ -35,11 +35,6 @@ pub trait NodeFamiliesQueryClient {
where
for<'a> T: Deserialize<'a>;
async fn get_config(&self) -> Result<Config, NyxdError> {
self.query_node_families_contract(NodeFamiliesQueryMsg::GetConfig {})
.await
}
async fn get_family_by_id(
&self,
family_id: NodeFamilyId,
@@ -365,7 +360,6 @@ mod tests {
msg: NodeFamiliesQueryMsg,
) {
match msg {
NodeFamiliesQueryMsg::GetConfig {} => client.get_config().ignore(),
NodeFamiliesQueryMsg::GetFamilyById { family_id } => {
client.get_family_by_id(family_id).ignore()
}
@@ -54,27 +54,6 @@ pub trait NodeFamiliesSigningClient {
.await
}
/// Update the name and/or description of the caller's family. Each
/// argument follows `None = keep` / `Some(_) = replace` semantics; a
/// call with both `None` is a server-side no-op.
async fn update_family(
&self,
updated_name: Option<String>,
updated_description: Option<String>,
fee: Option<Fee>,
) -> Result<ExecuteResult, NyxdError> {
self.execute_node_families_contract(
fee,
NodeFamiliesExecuteMsg::UpdateFamily {
updated_name,
updated_description,
},
"NodeFamiliesContract::UpdateFamily".to_string(),
vec![],
)
.await
}
async fn disband_family(&self, fee: Option<Fee>) -> Result<ExecuteResult, NyxdError> {
self.execute_node_families_contract(
fee,
@@ -245,12 +224,6 @@ mod tests {
NodeFamiliesExecuteMsg::CreateFamily { name, description } => client
.create_family(name, description, None, vec![])
.ignore(),
NodeFamiliesExecuteMsg::UpdateFamily {
updated_name,
updated_description,
} => client
.update_family(updated_name, updated_description, None)
.ignore(),
NodeFamiliesExecuteMsg::DisbandFamily {} => client.disband_family(None).ignore(),
NodeFamiliesExecuteMsg::InviteToFamily {
node_id,
@@ -36,7 +36,7 @@ pub mod logs;
pub mod module_traits;
pub mod types;
#[derive(Debug, Clone)]
#[derive(Debug)]
pub(crate) struct SigningClientOptions {
gas_price: GasPrice,
simulated_gas_multiplier: f32,
@@ -80,17 +80,6 @@ impl<C, S> MaybeSigningClient<C, S> {
opts,
}
}
pub(crate) fn clone_query_client(&self) -> MaybeSigningClient<C, NoSigner>
where
C: Clone,
{
MaybeSigningClient {
client: self.client.clone(),
signer: Default::default(),
opts: self.opts.clone(),
}
}
}
#[cfg(feature = "http-client")]
@@ -24,8 +24,6 @@ use async_trait::async_trait;
use cosmrs::tendermint::{abci, evidence::Evidence, Genesis};
use cosmrs::tx::{Raw, SignDoc};
use cosmwasm_std::Addr;
use nym_contracts_common::build_information::CONTRACT_BUILD_INFO_STORAGE_KEY;
use nym_contracts_common::ContractBuildInformation;
use nym_network_defaults::{ChainDetails, NymNetworkDetails};
use serde::{de::DeserializeOwned, Serialize};
use std::fmt::Debug;
@@ -42,7 +40,6 @@ pub use crate::nyxd::{
fee::Fee,
};
pub use crate::rpc::TendermintRpcClient;
pub use bip39;
pub use coin::Coin;
pub use cosmrs::{
bank::MsgSend,
@@ -73,19 +70,14 @@ pub use tendermint_rpc::{
Paging, Request, Response, SimpleRequest,
};
pub use nym_ecash_contract_common;
pub use nym_mixnet_contract_common;
pub use nym_multisig_contract_common;
pub use nym_network_monitors_contract_common;
pub use nym_performance_contract_common;
pub use nym_vesting_contract_common;
#[cfg(feature = "http-client")]
use crate::http_client;
#[cfg(feature = "http-client")]
use crate::{DirectSigningHttpRpcNyxdClient, QueryHttpRpcNyxdClient};
#[cfg(feature = "http-client")]
use cosmrs::rpc::{HttpClient, HttpClientUrl};
use nym_contracts_common::build_information::CONTRACT_BUILD_INFO_STORAGE_KEY;
use nym_contracts_common::ContractBuildInformation;
pub mod coin;
pub mod contract_traits;
@@ -270,16 +262,6 @@ impl<C, S> NyxdClient<C, S> {
}
}
pub fn clone_query_client(&self) -> NyxdClient<C>
where
C: Clone,
{
NyxdClient {
client: self.client.clone_query_client(),
config: self.config.clone(),
}
}
pub fn current_config(&self) -> &Config {
&self.config
}
@@ -311,10 +293,6 @@ impl<C, S> NyxdClient<C, S> {
pub fn set_simulated_gas_multiplier(&mut self, multiplier: f32) {
self.config.simulated_gas_multiplier = multiplier;
}
pub fn get_nym_contracts(&self) -> TypedNymContracts {
self.config.contracts.clone()
}
}
impl<C, S> NymContractsProvider for NyxdClient<C, S> {
@@ -329,12 +307,6 @@ impl<C, S> NymContractsProvider for NyxdClient<C, S> {
fn performance_contract_address(&self) -> Option<&AccountId> {
self.config.contracts.performance_contract_address.as_ref()
}
fn network_monitors_contract_address(&self) -> Option<&AccountId> {
self.config
.contracts
.network_monitors_contract_address
.as_ref()
}
fn node_families_contract_address(&self) -> Option<&AccountId> {
self.config
@@ -1,14 +1,8 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
//! Blacklist types. **The blacklist surface is stubbed today** - the execute
//! handlers always return `UnimplementedBlacklisting` and the storage map is
//! never populated. These types are kept for the planned redesign.
use cosmwasm_schema::cw_serde;
/// Public-key + metadata pair surfaced by `GetBlacklistedAccount` /
/// `GetBlacklistPaged`. Always empty on a freshly deployed contract.
#[cw_serde]
pub struct BlacklistedAccount {
pub public_key: String,
@@ -21,8 +15,6 @@ impl From<(String, Blacklisting)> for BlacklistedAccount {
}
}
/// Per-key blacklist record: the multisig proposal that approved it and the
/// block height at which finalisation landed (None until finalised).
#[cw_serde]
pub struct Blacklisting {
pub proposal_id: u64,
@@ -44,8 +36,6 @@ impl BlacklistedAccount {
}
}
/// Page of blacklist entries returned by `GetBlacklistPaged`. Always empty on
/// a freshly deployed contract.
#[cw_serde]
pub struct PagedBlacklistedAccountResponse {
pub accounts: Vec<BlacklistedAccount>,
@@ -69,8 +59,6 @@ impl PagedBlacklistedAccountResponse {
}
}
/// Response shape for `GetBlacklistedAccount`. `account` is `None` for any
/// key not present in the (currently always-empty) blacklist.
#[cw_serde]
pub struct BlacklistedAccountResponse {
pub account: Option<Blacklisting>,
@@ -4,9 +4,6 @@
use cosmwasm_schema::cw_serde;
use cosmwasm_std::Coin;
/// Pool-level deposit accounting. Updated by every successful
/// `DepositTicketBookFunds` and (for the unredeemed-tickets counter) by every
/// successful legacy `RedeemTickets`.
#[cw_serde]
pub struct PoolCounters {
/// Represents the total amount of funds deposited into the contract.
@@ -5,13 +5,8 @@ use crate::error::EcashContractError;
use cosmwasm_schema::cw_serde;
use cosmwasm_std::{StdError, StdResult};
/// Sequential identifier assigned to every accepted deposit. Starts at 0 and
/// is never recycled.
pub type DepositId = u32;
/// Opaque on-chain record of a deposit: the depositor-claimed bs58-encoded
/// ed25519 identity public key. The contract does not verify control of the
/// corresponding private key.
#[cw_serde]
pub struct Deposit {
pub bs58_encoded_ed25519_pubkey: String,
@@ -24,8 +19,6 @@ impl Deposit {
}
}
/// Decode a bs58-encoded ed25519 public key to its 32-byte raw form.
/// Surfaces `MalformedEd25519Identity` on any bs58 / length failure.
pub fn get_ed25519_pubkey_bytes(raw: &str) -> Result<[u8; 32], EcashContractError> {
let mut ed25519_pubkey_bytes = [0u8; 32];
bs58::decode(raw)
@@ -39,13 +32,10 @@ impl Deposit {
bs58::encode(raw).into_string()
}
/// Decode this deposit's identity key to its 32-byte raw form for storage.
pub fn to_bytes(&self) -> Result<[u8; 32], EcashContractError> {
Self::get_ed25519_pubkey_bytes(&self.bs58_encoded_ed25519_pubkey)
}
/// Reconstruct a `Deposit` from a raw 32-byte ed25519 pubkey as stored
/// under the `"deposit"` namespace.
pub fn try_from_bytes(bytes: &[u8]) -> StdResult<Self> {
if bytes.len() != 32 {
return Err(StdError::generic_err("malformed deposit data"));
@@ -57,16 +47,12 @@ impl Deposit {
}
}
/// Response shape for `GetLatestDeposit`. `deposit` is `None` on a freshly
/// deployed contract.
#[cw_serde]
#[derive(Default)]
pub struct LatestDepositResponse {
pub deposit: Option<DepositData>,
}
/// Response shape for `GetDeposit { deposit_id }`. `deposit` is `None` when
/// the id has not yet been assigned (`id >= total_deposits_made`).
#[cw_serde]
pub struct DepositResponse {
pub id: DepositId,
@@ -74,8 +60,6 @@ pub struct DepositResponse {
pub deposit: Option<Deposit>,
}
/// `(deposit_id, deposit)` pair surfaced by the latest-deposit and paginated
/// deposit queries.
#[cw_serde]
pub struct DepositData {
pub id: DepositId,
@@ -89,8 +73,6 @@ impl From<(DepositId, Deposit)> for DepositData {
}
}
/// Page of deposits returned by `GetDepositsPaged`. `start_next_after` is the
/// id of the last returned entry; pass it as the next call's `start_after`.
#[cw_serde]
pub struct PagedDepositsResponse {
pub deposits: Vec<DepositData>,
@@ -6,108 +6,69 @@ use cw_controllers::AdminError;
use cw_utils::PaymentError;
use thiserror::Error;
/// Errors surfaced by the ecash contract. Each reachable variant is named in at
/// least one scenario of `openspec/specs/ecash-contract/spec.md`.
#[derive(Error, Debug, PartialEq)]
pub enum EcashContractError {
/// Wrapper for any underlying `cosmwasm_std::StdError` (storage faults,
/// address validation, etc.).
#[error(transparent)]
Std(#[from] StdError),
/// Raised by `cw_utils::must_pay` on `DepositTicketBookFunds` when funds
/// are missing, multi-denom, or in the wrong denom. Inner variants
/// `NoFunds`, `MultipleDenoms`, `MissingDenom` are all reachable.
#[error("Invalid deposit")]
InvalidDeposit(#[from] PaymentError),
/// `DepositTicketBookFunds` with the right denom but a non-matching amount.
/// `amount` is the reduced amount (if the sender is whitelisted) or the
/// default amount.
#[error("received wrong amount for deposit. got: {received}. required: {amount}")]
WrongAmount { received: Coin, amount: Coin },
/// **Unreachable** - preserved for forward compatibility (no current
/// execute path triggers this).
#[error("There aren't enough funds in the contract")]
NotEnoughFunds,
/// Wrapper for `cw_controllers::AdminError`. Raised by every admin-gated
/// and multisig-gated handler when the sender is wrong.
#[error(transparent)]
Admin(#[from] AdminError),
/// Redemption-proposal reply could not find a `proposal_id` attribute on
/// the multisig `wasm` event.
#[error("could not find proposal id inside the multisig reply SubMsg")]
MissingProposalId,
/// Redemption-proposal reply found a `proposal_id` attribute that could
/// not be parsed as `u64`. Realistically unreachable.
// realistically this should NEVER be thrown
#[error("the proposal id returned by the multisig contract could not be parsed into an u64")]
MalformedProposalId,
/// Instantiation given a `group_addr` that failed bech32 validation.
#[error("Group contract invalid address '{addr}'")]
InvalidGroup { addr: String },
/// **Unreachable** - no current execute path triggers this.
#[error("Unauthorized")]
Unauthorized,
/// **Unreachable** - preserved for future SemVer comparisons during migration.
#[error("Failed to parse {value} into a valid SemVer version: {error_message}")]
SemVerFailure {
value: String,
error_message: String,
},
/// Reply dispatcher saw an `id` that does not match
/// `BLACKLIST_PROPOSAL_REPLY_ID` or `REDEMPTION_PROPOSAL_REPLY_ID`.
#[error("received an invalid reply id: {id}. it does not correspond to any sent SubMsg")]
InvalidReplyId { id: u64 },
/// **Unreachable** - preserved for the (future) typed-deposit-info feature.
#[error("reached the maximum of 255 different deposit types")]
MaximumDepositTypesReached,
/// **Unreachable** - preserved for the (future) typed-deposit-info feature.
#[error("compressed deposit info {typ} does not corresponds to any known type")]
UnknownCompressedDepositInfoType { typ: u8 },
/// **Unreachable** - preserved for the (future) typed-deposit-info feature.
#[error("deposit info {typ} does not corresponds to any previously seen type")]
UnknownDepositInfoType { typ: String },
/// `DepositTicketBookFunds` with an `identity_key` that fails to bs58-decode
/// to exactly 32 bytes. Raised inside `Deposit::to_bytes` during
/// `save_deposit`.
#[error("the provided ed25519 identity was malformed")]
MalformedEd25519Identity,
/// `nym_network_defaults::TICKETBOOK_SIZE` has diverged from the value
/// snapshotted at instantiation in `Item<Invariants>`. Tripwire for
/// uncoordinated network-defaults bumps.
#[error("the ticket book size has changed since the contract was created! This was not expected! It used to be {at_init} but it's {current} now! Please let the developers know ASAP!")]
TicketBookSizeChanged { at_init: u64, current: u64 },
/// `RequestRedemption` with a `commitment_bs58` that does not decode to a
/// 32-byte sha256 digest.
#[error("the provided tickets redemption commitment is malformed")]
MalformedRedemptionCommitment,
/// Always thrown by `ProposeToBlacklist` and `AddToBlacklist` until the
/// blacklist redesign lands.
#[error("the account blacklisting hasn't been fully implemented yet")]
UnimplementedBlacklisting,
/// `SetReducedDepositPrice` (or migration whitelist seeding) given a coin
/// whose denom does not match `Config::deposit_amount.denom`.
#[error("reduced deposit must use the same denom as the default deposit (expected '{expected}', got '{got}')")]
InvalidReducedDepositDenom { expected: String, got: String },
/// `SetReducedDepositPrice` (or migration whitelist seeding) given a
/// reduced amount not strictly less than the current default.
#[error(
"reduced deposit amount ({reduced}) must be strictly less than the default ({default})"
)]
@@ -116,13 +77,9 @@ pub enum EcashContractError {
default: cosmwasm_std::Uint128,
},
/// `RemoveReducedDepositPrice` invoked for an address with no current
/// reduced-deposit entry.
#[error("address '{address}' does not have a custom reduced deposit price set")]
NoReducedDepositPrice { address: String },
/// `UpdateDefaultDepositValue` or `SetReducedDepositPrice` given an amount
/// below `nym_network_defaults::TICKETBOOK_SIZE`.
#[error(
"deposit amount ({amount}) must be at least the ticket book size ({ticket_book_size})"
)]
@@ -1,7 +1,4 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
/// Duplicate of `events::PROPOSAL_ID_ATTRIBUTE_NAME`. **Dead code**: not
/// referenced anywhere in the workspace today; preserved here pending a
/// follow-on cleanup. Use `events::PROPOSAL_ID_ATTRIBUTE_NAME` instead.
pub const BANDWIDTH_PROPOSAL_ID: &str = "proposal_id";
@@ -1,21 +1,9 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
//! Event names and attribute keys emitted by the ecash contract. Renaming any
//! of these is a breaking change for indexers and downstream tooling.
/// Event type emitted by every successful `DepositTicketBookFunds`. Carries a
/// single `deposit-id` attribute with the assigned id as a decimal string.
// event types
pub const DEPOSITED_FUNDS_EVENT_TYPE: &str = "deposited-funds";
/// Attribute key on the `deposited-funds` event: the newly assigned deposit id.
pub const DEPOSIT_ID: &str = "deposit-id";
/// Name of the cosmwasm-std auto-generated event that carries handler
/// attributes (`updated_deposit`, `action`, `address`, `deposit`,
/// `proposal_id`).
pub const WASM_EVENT_NAME: &str = "wasm";
/// Attribute key carrying the multisig-issued `proposal_id` on the `wasm`
/// event from the redemption-proposal reply handler.
pub const PROPOSAL_ID_ATTRIBUTE_NAME: &str = "proposal_id";
@@ -1,12 +1,6 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
//! Shared types, messages, events, and errors for the ecash contract.
//!
//! Consumed by both the contract crate (`contracts/ecash`) and any off-chain
//! client (gateways, nym-api signers, indexers, validator-client). See
//! `openspec/specs/ecash-contract/spec.md` for the normative interface.
pub mod blacklist;
pub mod counters;
pub mod deposit;
@@ -15,134 +15,100 @@ use crate::reduced_deposit::WhitelistedAccountsResponse;
#[cfg(feature = "schema")]
use cosmwasm_schema::QueryResponses;
/// Instantiation payload. The sender of the instantiate transaction becomes the
/// contract admin; the three addresses are bech32-validated and persisted as
/// immutable cross-contract pointers (see spec requirement "Contract instantiation").
#[cw_serde]
pub struct InstantiateMsg {
/// Cosmos SDK address reserved for the future pool-contract transition.
/// Stored in `Config` but never debited by the current contract.
pub holding_account: String,
/// cw3 multisig contract that gates `RedeemTickets` and (in the redesign)
/// blacklist proposals. Not updatable through any execute path.
pub multisig_addr: String,
/// cw4 group contract referenced by the (stubbed) blacklist proposal flow.
pub group_addr: String,
/// Default per-deposit price. The denom of this coin is the contract's
/// canonical denom for the rest of its lifetime.
pub deposit_amount: Coin,
}
#[cw_serde]
pub enum ExecuteMsg {
/// Submitted by clients to escrow funds and register a claimed ed25519
/// identity key. Mints a sequential `deposit_id`. The contract does not
/// verify control of the identity key - that proof is checked off-chain by
/// nym-api signers at blind-sign time.
DepositTicketBookFunds { identity_key: String },
/// Used by clients to request ticket books from the signers
DepositTicketBookFunds {
identity_key: String,
},
/// Submitted by gateways to request batch redemption of spent tickets.
/// Dispatches a `Propose` SubMsg to the multisig contract; the actual
/// transfer effect is gated behind multisig approval.
/// Used by gateways to batch redeem tokens from the spent tickets
RequestRedemption {
commitment_bs58: String,
number_of_tickets: u16,
},
/// **Legacy / dead code.** Only callable by the multisig; bumps the
/// unredeemed-tickets counter and emits a `ticket_redemption` event with
/// `moved_to_holding_account = "false"`. No known consumer depends on the
/// side effects; candidate for removal in a follow-on breaking-schema
/// change.
RedeemTickets { n: u16, gw: String },
/// The actual message that gets executed, after multisig votes, that transfers the ticket tokens into gateway's (and the holding) account
RedeemTickets {
n: u16,
gw: String,
},
/// Transfer the contract admin role. Only the current admin may sign.
/// Dispatches via the cw_controllers `execute_update_admin` handshake.
UpdateAdmin { admin: String },
UpdateAdmin {
admin: String,
},
/// Overwrite `Config::deposit_amount`. Only callable by the contract admin.
/// Rejects values below `nym_network_defaults::TICKETBOOK_SIZE` and trips
/// `TicketBookSizeChanged` if the snapshotted invariant has diverged from
/// the current crate constant.
#[serde(alias = "update_deposit_value")]
UpdateDefaultDepositValue { new_deposit: Coin },
UpdateDefaultDepositValue {
new_deposit: Coin,
},
/// Set (or overwrite) a reduced deposit price for a specific address.
/// Only callable by the contract admin.
SetReducedDepositPrice { address: String, deposit: Coin },
SetReducedDepositPrice {
address: String,
deposit: Coin,
},
/// Remove the reduced deposit price for a specific address, reverting them to
/// the default price. Returns an error if the address has no custom price set.
/// Only callable by the contract admin.
RemoveReducedDepositPrice { address: String },
RemoveReducedDepositPrice {
address: String,
},
/// **Stubbed**: always returns `EcashContractError::UnimplementedBlacklisting`.
/// Storage, reply handler, and helper paths exist but are unreachable from
/// the public ExecuteMsg surface. Preserved for the redesign.
ProposeToBlacklist { public_key: String },
/// **Stubbed**: always returns `EcashContractError::UnimplementedBlacklisting`.
AddToBlacklist { public_key: String },
// TODO: properly implement
ProposeToBlacklist {
public_key: String,
},
AddToBlacklist {
public_key: String,
},
}
#[cw_serde]
#[cfg_attr(feature = "schema", derive(QueryResponses))]
pub enum QueryMsg {
/// Look up a blacklist entry by its bs58-encoded ed25519 public key. Always
/// returns `None` on a freshly deployed contract because the blacklist
/// execute paths are stubbed.
#[cfg_attr(feature = "schema", returns(BlacklistedAccountResponse))]
GetBlacklistedAccount { public_key: String },
/// Paginated listing of blacklist entries. Always empty today (see stubbed
/// blacklist surface). Defaults: limit 50, max 75.
#[cfg_attr(feature = "schema", returns(PagedBlacklistedAccountResponse))]
GetBlacklistPaged {
limit: Option<u32>,
start_after: Option<String>,
},
/// Default per-deposit price (`Config::deposit_amount`). The
/// `GetRequiredDepositAmount` aliases are kept for backwards compatibility.
#[cfg_attr(feature = "schema", returns(Coin))]
#[serde(alias = "get_required_deposit_amount")]
#[serde(alias = "GetRequiredDepositAmount")]
GetDefaultDepositAmount {},
/// Per-address reduced deposit price override, if any. `None` for any
/// non-whitelisted address.
#[cfg_attr(feature = "schema", returns(Option<Coin>))]
GetReducedDepositAmount { address: String },
/// Enumerate every reduced-deposit whitelist entry in ascending address
/// order. Unpaginated by design (the whitelist is expected to stay small).
#[cfg_attr(feature = "schema", returns(WhitelistedAccountsResponse))]
GetAllWhitelistedAccounts {},
/// Look up a deposit by id. Returns `{ id, deposit: None }` when the id has
/// not yet been assigned.
#[cfg_attr(feature = "schema", returns(DepositResponse))]
GetDeposit { deposit_id: u32 },
/// Most recently assigned deposit (or `{ deposit: None }` on a fresh
/// contract). See `DepositStorage::latest_deposit`.
#[cfg_attr(feature = "schema", returns(LatestDepositResponse))]
GetLatestDeposit {},
/// Paginated listing of deposits in ascending id order. Defaults: limit 50,
/// max 100.
#[cfg_attr(feature = "schema", returns(PagedDepositsResponse))]
GetDepositsPaged {
limit: Option<u32>,
start_after: Option<u32>,
},
/// Aggregate statistics: global totals + per-account custom-price
/// breakdowns. Reassembled in a single read pass from `PoolCounters` and
/// `DepositStatsStorage`.
#[cfg_attr(feature = "schema", returns(DepositsStatistics))]
GetDepositsStatistics {},
}
@@ -1,8 +1,5 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
/// Title used for the cw3 `Propose` message dispatched by `RequestRedemption`.
/// nym-api signers cross-check this exact string when validating that an
/// in-flight proposal originated from the ecash contract.
// TODO: to be moved to multisig
pub const BATCH_REDEMPTION_PROPOSAL_TITLE: &str = "ecash-redemption";
@@ -4,16 +4,12 @@
use cosmwasm_schema::cw_serde;
use cosmwasm_std::{Addr, Coin};
/// Whitelist entry: an address and the reduced deposit price it may pay.
/// Persisted in the `"reduced_deposits"` storage map.
#[cw_serde]
pub struct WhitelistedAccount {
pub address: Addr,
pub deposit: Coin,
}
/// Response shape for `GetAllWhitelistedAccounts`. Unpaginated - the whitelist
/// is expected to stay small.
#[cw_serde]
pub struct WhitelistedAccountsResponse {
pub whitelisted_accounts: Vec<WhitelistedAccount>,
@@ -1,26 +0,0 @@
[package]
name = "nym-network-monitors-contract-common"
description = "Common library for the Nym Network Monitors contract"
authors.workspace = true
repository.workspace = true
homepage.workspace = true
documentation.workspace = true
edition.workspace = true
license.workspace = true
readme.workspace = true
version.workspace = true
[dependencies]
thiserror = { workspace = true }
serde = { workspace = true }
schemars = { workspace = true }
cosmwasm-std = { workspace = true }
cosmwasm-schema = { workspace = true }
cw-controllers = { workspace = true }
[features]
schema = []
[lints]
workspace = true
@@ -1,8 +0,0 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
pub mod storage_keys {
pub const CONTRACT_ADMIN: &str = "contract-admin";
pub const AUTHORISED_ORCHESTRATORS: &str = "authorised-orchestrators";
pub const AUTHORISED_NETWORK_MONITORS: &str = "authorised-network-monitors";
}
@@ -1,30 +0,0 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use cosmwasm_std::Addr;
use cw_controllers::AdminError;
use thiserror::Error;
#[derive(Error, Debug, PartialEq)]
pub enum NetworkMonitorsContractError {
#[error("could not perform contract migration: {comment}")]
FailedMigration { comment: String },
#[error(transparent)]
Admin(#[from] AdminError),
#[error("unauthorised")]
Unauthorized,
#[error("address {addr} is not an authorised orchestrator")]
NotAnOrchestrator { addr: Addr },
#[error("Failed to recover x25519 public key from its base58 representation: {0}")]
MalformedX25519AgentNoiseKey(String),
#[error("Failed to recover ed25519 public key from its base58 representation: {0}")]
MalformedEd25519OrchestratorIdentityKey(String),
#[error(transparent)]
StdErr(#[from] cosmwasm_std::StdError),
}
@@ -1,11 +0,0 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
pub mod constants;
pub mod error;
pub mod msg;
pub mod types;
pub use error::*;
pub use msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg};
pub use types::*;
@@ -1,78 +0,0 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use cosmwasm_schema::cw_serde;
use std::net::SocketAddr;
#[cfg(feature = "schema")]
use crate::{
AuthorisedNetworkMonitorOrchestratorsResponse, AuthorisedNetworkMonitorsPagedResponse,
};
#[cw_serde]
pub struct InstantiateMsg {
/// Address of the initial network monitor orchestrator.
pub orchestrator_address: String,
}
#[cw_serde]
pub enum ExecuteMsg {
/// Change the admin
UpdateAdmin { admin: String },
/// Authorise new network monitor orchestrator
AuthoriseNetworkMonitorOrchestrator { address: String },
/// Attempt to update the announced identity key of this orchestrator
UpdateOrchestratorIdentityKey { key: String },
/// Revoke network monitor orchestrator authorisation.
RevokeNetworkMonitorOrchestrator { address: String },
/// Authorise new network monitor (or renew authorisation)
/// granting additional privileges when sending mixnet packets to Nym nodes.
AuthoriseNetworkMonitor {
/// Mixnet address of the agent.
/// The underlying ip address is going to be used as ingress to the nodes,
/// and the full socket address announces the egress and the association with the noise key
mixnet_address: SocketAddr,
/// Base-58 encoded noise key of the agent.
bs58_x25519_noise: String,
/// Version of the noise protocol used by the agent.
noise_version: u8,
},
/// Revoke network monitor authorisation.
RevokeNetworkMonitor { address: SocketAddr },
/// Revoke all network monitor authorisations.
RevokeAllNetworkMonitors,
}
#[cw_serde]
#[cfg_attr(feature = "schema", derive(cosmwasm_schema::QueryResponses))]
pub enum QueryMsg {
#[cfg_attr(feature = "schema", returns(cw_controllers::AdminResponse))]
Admin {},
// no need for pagination as we don't expect even a double digit of those
#[cfg_attr(
feature = "schema",
returns(AuthorisedNetworkMonitorOrchestratorsResponse)
)]
NetworkMonitorOrchestrators {},
#[cfg_attr(feature = "schema", returns(AuthorisedNetworkMonitorsPagedResponse))]
NetworkMonitorAgents {
/// Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response.
start_next_after: Option<SocketAddr>,
/// Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default.
limit: Option<u32>,
},
}
#[cw_serde]
pub struct MigrateMsg {}
@@ -1,53 +0,0 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use cosmwasm_schema::cw_serde;
use cosmwasm_std::{Addr, Timestamp};
use std::net::SocketAddr;
pub type OrchestratorAddress = Addr;
#[cw_serde]
pub struct AuthorisedNetworkMonitorOrchestrator {
/// The address associated with the network monitor orchestrator.
pub address: Addr,
/// Base-58 encoded identity key of the orchestrator, announced by the orchestrator itself
/// on startup.
pub identity_key: Option<String>,
/// Timestamp of when the network monitor was authorised.
pub authorised_at: Timestamp,
}
#[cw_serde]
pub struct AuthorisedNetworkMonitor {
/// Mixnet address of the agent.
/// The underlying ip address is going to be used as ingress to the nodes,
/// and the full socket address announces the egress and the association with the noise key
pub mixnet_address: SocketAddr,
/// The address of the orchestrator that authorised the network monitor agent.
pub authorised_by: OrchestratorAddress,
/// Timestamp of when the network monitor was authorised.
pub authorised_at: Timestamp,
/// Base-58 encoded noise key of the agent.
pub bs58_x25519_noise: String,
/// Version of the noise protocol used by the agent.
pub noise_version: u8,
}
#[cw_serde]
pub struct AuthorisedNetworkMonitorOrchestratorsResponse {
pub authorised: Vec<AuthorisedNetworkMonitorOrchestrator>,
}
#[cw_serde]
pub struct AuthorisedNetworkMonitorsPagedResponse {
pub authorised: Vec<AuthorisedNetworkMonitor>,
pub start_next_after: Option<SocketAddr>,
}
@@ -101,14 +101,4 @@ pub mod events {
pub const NODE_UNBOND_CLEANUP_EVENT_NAME: &str = "family_node_unbond_cleanup";
pub const NODE_UNBOND_CLEANUP_EVENT_NODE_ID: &str = "node_id";
pub const FAMILY_UPDATE_EVENT_NAME: &str = "family_update";
pub const FAMILY_UPDATE_EVENT_FAMILY_ID: &str = "family_id";
pub const FAMILY_UPDATE_EVENT_OWNER_ADDRESS: &str = "owner_address";
/// Attribute carrying the new family name. Only emitted when the
/// `UpdateFamily` message carried `updated_name = Some(_)`.
pub const FAMILY_UPDATE_EVENT_UPDATED_NAME: &str = "updated_name";
/// Attribute carrying the new family description. Only emitted when the
/// `UpdateFamily` message carried `updated_description = Some(_)`.
pub const FAMILY_UPDATE_EVENT_UPDATED_DESCRIPTION: &str = "updated_description";
}
@@ -38,16 +38,6 @@ pub enum ExecuteMsg {
/// `create_family_fee` must be attached as funds.
CreateFamily { name: String, description: String },
/// Update the name and/or description of the family owned by the message
/// sender. Each field is independently optional: `None` leaves the
/// existing value unchanged, `Some(_)` replaces it. Updated values are
/// validated against the same length / normalisation / global-uniqueness
/// rules as [`Self::CreateFamily`].
UpdateFamily {
updated_name: Option<String>,
updated_description: Option<String>,
},
/// Disband the family owned by the message sender. The family must have
/// no current members; any still-pending invitations are revoked.
DisbandFamily {},
@@ -96,10 +86,6 @@ pub enum ExecuteMsg {
#[cw_serde]
#[cfg_attr(feature = "schema", derive(cosmwasm_schema::QueryResponses))]
pub enum QueryMsg {
/// Retrieve current contract configuration values
#[cfg_attr(feature = "schema", returns(Config))]
GetConfig {},
/// Look up a single family by its id.
#[cfg_attr(feature = "schema", returns(NodeFamilyResponse))]
GetFamilyById { family_id: NodeFamilyId },
@@ -61,12 +61,9 @@ pub struct NodeFamily {
/// A pending invitation for a node to join a particular family.
///
/// Invitations are stored until they are accepted, rejected, or revoked. Once the
/// chain advances past `expires_at` an invitation becomes inert but stays in storage
/// — there is no background process clearing expired invitations. A timed-out
/// invitation is cleared either when explicitly revoked/rejected, or when the family
/// issues a fresh invitation for the same node, which archives the stale one as
/// `Expired` and supersedes it.
/// Invitations are stored until they are accepted, rejected, revoked, or until the
/// chain advances past `expires_at` (in which case they remain in storage but are
/// treated as inert — there is no background process clearing expired invitations).
#[cw_serde]
pub struct FamilyInvitation {
/// The family that issued the invitation.
@@ -110,10 +107,8 @@ pub struct PastFamilyMember {
/// Terminal status for an invitation that has been moved out of the pending set.
///
/// Note: an invitation that merely times out is **not** archived here on its own —
/// it is left inert in the pending set (see `FamilyInvitation::expires_at`). It only
/// reaches `Expired` if the family issues a fresh invitation for the same node, which
/// supersedes and archives the stale one.
/// Note: timed-out invitations are not represented here — they are simply left in
/// the pending set (see `FamilyInvitation::expires_at`).
#[cw_serde]
pub enum FamilyInvitationStatus {
/// Still awaiting a response. Recorded with a timestamp for completeness even
@@ -126,16 +121,11 @@ pub enum FamilyInvitationStatus {
/// The family revoked the invitation at the given timestamp before it could
/// be accepted or rejected.
Revoked { at: u64 },
/// The invitation had already expired and was superseded by a fresh invitation
/// for the same node from the same family, issued at the given timestamp. This is
/// the only path that archives a timed-out invitation.
Expired { at: u64 },
}
/// Historical record of an invitation that has reached a terminal state
/// (`Accepted`, `Rejected`, `Revoked`, or `Expired`). A timed-out invitation is
/// archived here only when a fresh invitation for the same node supersedes it
/// (status `Expired`); otherwise it stays in the pending map until explicitly cleared.
/// (`Accepted`, `Rejected`, or `Revoked`). Timed-out invitations are **not**
/// archived here — they remain in the pending map until explicitly cleared.
#[cw_serde]
pub struct PastFamilyInvitation {
/// The original invitation as it was issued.
+4 -47
View File
@@ -27,9 +27,6 @@ pub struct QuorumStateChecker {
cancellation_token: CancellationToken,
check_interval: Duration,
quorum_state: QuorumState,
/// indicates whether the last check has been a failure
last_failed: bool,
}
impl QuorumStateChecker {
@@ -45,7 +42,6 @@ impl QuorumStateChecker {
quorum_state: QuorumState {
available: Arc::new(Default::default()),
},
last_failed: false,
};
// first check MUST succeed, otherwise we shouldn't start
@@ -61,7 +57,6 @@ impl QuorumStateChecker {
}
async fn check_quorum_state(&self) -> Result<bool, CredentialProxyError> {
info!("checking the current quorum state");
let client_guard = self.client.query_chain().await;
// split the operation as we only need to hold the reference to chain client for the first part
@@ -69,8 +64,7 @@ impl QuorumStateChecker {
let dkg_details = dkg_details_with_client(client_guard.deref()).await?;
drop(client_guard);
let res = check_known_dealers(dkg_details, 4).await?;
info!("there are {} known DKG dealers", res.results.len());
let res = check_known_dealers(dkg_details).await?;
let Some(signing_threshold) = res.threshold else {
warn!(
@@ -82,36 +76,15 @@ impl QuorumStateChecker {
let mut working_issuer = 0;
for result in res.results {
let dealer = &result.information;
let info = format!("[id: {}] @ {}", dealer.node_index, dealer.announce_address);
if result.chain_available() && result.signing_available() {
info!("✅ {info} is fully available");
working_issuer += 1;
} else if !result.chain_available() && !result.signing_available() {
warn!("❌ {info} is not available for both chain and signing");
} else if !result.chain_available() {
warn!("❌ {info} is not available for chain");
} else {
warn!("❌ {info} is not available for signing");
}
}
let available = (working_issuer as u64) >= signing_threshold;
if available {
info!(
"✅ Quorum state is available with {working_issuer} out of {signing_threshold} issuers"
)
} else {
error!(
"❌ Quorum state is not available with {working_issuer} out of {signing_threshold} issuers"
)
}
Ok(available)
Ok((working_issuer as u64) >= signing_threshold)
}
pub async fn run_forever(mut self) {
pub async fn run_forever(self) {
info!("starting quorum state checker");
loop {
tokio::select! {
@@ -121,23 +94,7 @@ impl QuorumStateChecker {
}
_ = tokio::time::sleep(self.check_interval) => {
match self.check_quorum_state().await {
Ok(available) => {
let previous = self.quorum_state.available.load(Ordering::SeqCst);
// only update the quorum state to a failed state if we've had two consecutive failures
if available {
if !previous {
info!("quorum recovered");
}
self.quorum_state.available.store(true, Ordering::SeqCst);
} else if self.last_failed {
if previous {
warn!("quorum became unavailable after 2 consecutive failed checks");
}
self.quorum_state.available.store(false, Ordering::SeqCst);
}
self.last_failed = !available;
},
Ok(available) => self.quorum_state.available.store(available, Ordering::SeqCst),
Err(err) => error!("failed to check current quorum state: {err}"),
}
}
@@ -230,8 +230,8 @@ impl MemoryEcachTicketbookManager {
expiration_date: t.ticketbook.expiration_date(),
ticketbook_type: t.ticketbook.ticketbook_type().to_string(),
epoch_id: t.ticketbook.epoch_id() as u32,
total_tickets: t.total_tickets,
used_tickets: t.ticketbook.spent_tickets() as u32,
total_tickets: t.ticketbook.spent_tickets() as u32,
used_tickets: t.total_tickets,
})
.collect()
}
@@ -333,339 +333,3 @@ impl MemoryEcachTicketbookManager {
guard.emergency_credentials.remove(typ);
}
}
#[cfg(test)]
mod tests {
use super::*;
use nym_compact_ecash::tests::helpers::generate_expiration_date_signatures;
use nym_compact_ecash::{issue, ttp_keygen};
use nym_credentials_interface::TicketType;
use nym_crypto::asymmetric::ed25519;
use nym_ecash_time::EcashTime;
use nym_test_utils::helpers::deterministic_rng;
fn mock_issuance(deposit_id: u32) -> IssuanceTicketBook {
let identifier = "foomp";
let mut rng = deterministic_rng();
let key = ed25519::PrivateKey::new(&mut rng);
let typ = TicketType::V1MixnetEntry;
IssuanceTicketBook::new(deposit_id, identifier, key, typ)
}
fn mock_ticketbook() -> anyhow::Result<IssuedTicketBook> {
let signing_keys = ttp_keygen(1, 1)?.remove(0);
let issuance = mock_issuance(42);
let expiration_date = issuance.expiration_date();
let sig_req = issuance.prepare_for_signing();
let _exp_date_sigs = generate_expiration_date_signatures(
sig_req.expiration_date.ecash_unix_timestamp(),
&[signing_keys.secret_key()],
&[signing_keys.verification_key()],
&signing_keys.verification_key(),
&[1],
)?;
let blind_sig = issue(
signing_keys.secret_key(),
sig_req.ecash_pub_key,
&sig_req.withdrawal_request,
expiration_date.ecash_unix_timestamp(),
issuance.ticketbook_type().encode(),
)?;
let partial_wallet =
issuance.unblind_signature(&signing_keys.verification_key(), &sig_req, blind_sig, 1)?;
let wallet = issuance.aggregate_signature_shares(
&signing_keys.verification_key(),
&[partial_wallet],
sig_req,
)?;
Ok(issuance.into_issued_ticketbook(wallet, 1))
}
fn mock_verification_key() -> VerificationKeyAuth {
ttp_keygen(1, 1).unwrap().remove(0).verification_key()
}
#[tokio::test]
async fn get_ticketbooks_info_empty() {
let manager = MemoryEcachTicketbookManager::new();
let info = manager.get_ticketbooks_info().await;
assert!(info.is_empty());
}
#[tokio::test]
async fn get_ticketbooks_info_maps_inserted_ticketbook() -> anyhow::Result<()> {
let manager = MemoryEcachTicketbookManager::new();
let ticketbook = mock_ticketbook()?;
let total_tickets = 100;
let used_tickets = 25;
manager
.insert_new_ticketbook(&ticketbook, total_tickets, used_tickets)
.await;
let info = manager.get_ticketbooks_info().await;
assert_eq!(info.len(), 1);
let entry = &info[0];
assert_eq!(entry.id, 0);
assert_eq!(entry.expiration_date, ticketbook.expiration_date());
assert_eq!(
entry.ticketbook_type,
ticketbook.ticketbook_type().to_string()
);
assert_eq!(entry.epoch_id, ticketbook.epoch_id() as u32);
assert_eq!(entry.total_tickets, total_tickets);
assert_eq!(entry.used_tickets, used_tickets);
Ok(())
}
#[tokio::test]
async fn contains_ticketbook_reflects_insertion() -> anyhow::Result<()> {
let manager = MemoryEcachTicketbookManager::new();
let ticketbook = mock_ticketbook()?;
assert!(!manager.contains_ticketbook(&ticketbook).await);
manager.insert_new_ticketbook(&ticketbook, 100, 0).await;
assert!(manager.contains_ticketbook(&ticketbook).await);
Ok(())
}
#[tokio::test]
async fn insert_new_ticketbook_assigns_incrementing_ids() -> anyhow::Result<()> {
let manager = MemoryEcachTicketbookManager::new();
let ticketbook = mock_ticketbook()?;
manager.insert_new_ticketbook(&ticketbook, 100, 0).await;
manager.insert_new_ticketbook(&ticketbook, 100, 0).await;
let mut ids: Vec<i64> = manager
.get_ticketbooks_info()
.await
.into_iter()
.map(|i| i.id)
.collect();
ids.sort();
assert_eq!(ids, vec![0, 1]);
Ok(())
}
#[tokio::test]
async fn get_next_unspent_ticketbook_updates_spent_and_exhausts() -> anyhow::Result<()> {
let manager = MemoryEcachTicketbookManager::new();
let ticketbook = mock_ticketbook()?;
let typ = ticketbook.ticketbook_type().to_string();
// total = 3, used = 0 — leaves 3 tickets available
manager.insert_new_ticketbook(&ticketbook, 3, 0).await;
let first = manager
.get_next_unspent_ticketbook_and_update(typ.clone(), 2)
.await;
assert!(first.is_some());
let first = first.unwrap();
assert_eq!(first.total_tickets, 3);
// returned ticketbook reflects state *before* the update
assert_eq!(first.ticketbook.spent_tickets(), 0);
// next withdrawal of 2 should be rejected (only 1 left)
let second = manager
.get_next_unspent_ticketbook_and_update(typ.clone(), 2)
.await;
assert!(second.is_none());
// but a withdrawal of 1 succeeds
let third = manager
.get_next_unspent_ticketbook_and_update(typ.clone(), 1)
.await;
assert!(third.is_some());
// and now nothing left
let fourth = manager.get_next_unspent_ticketbook_and_update(typ, 1).await;
assert!(fourth.is_none());
Ok(())
}
#[tokio::test]
async fn get_next_unspent_ticketbook_filters_by_type() -> anyhow::Result<()> {
let manager = MemoryEcachTicketbookManager::new();
let ticketbook = mock_ticketbook()?;
manager.insert_new_ticketbook(&ticketbook, 5, 0).await;
let mismatched = manager
.get_next_unspent_ticketbook_and_update("nonexistent_type".to_string(), 1)
.await;
assert!(mismatched.is_none());
Ok(())
}
#[tokio::test]
async fn revert_ticketbook_withdrawal_resets_spent_only_when_expected_matches(
) -> anyhow::Result<()> {
let manager = MemoryEcachTicketbookManager::new();
let ticketbook = mock_ticketbook()?;
let typ = ticketbook.ticketbook_type().to_string();
manager.insert_new_ticketbook(&ticketbook, 10, 0).await;
manager
.get_next_unspent_ticketbook_and_update(typ.clone(), 4)
.await
.expect("should withdraw");
// stale expected_current_total_spent — should be rejected
assert!(!manager.revert_ticketbook_withdrawal(0, 4, 99).await);
// spent_tickets unchanged
let used_after_failed = manager.get_ticketbooks_info().await[0].used_tickets;
assert_eq!(used_after_failed, 4);
// matching expected — should succeed and restore
assert!(manager.revert_ticketbook_withdrawal(0, 4, 4).await);
let used_after_revert = manager.get_ticketbooks_info().await[0].used_tickets;
assert_eq!(used_after_revert, 0);
// unknown ticketbook_id is rejected
assert!(!manager.revert_ticketbook_withdrawal(999, 1, 0).await);
Ok(())
}
#[tokio::test]
async fn pending_ticketbook_round_trip() {
let manager = MemoryEcachTicketbookManager::new();
let issuance = mock_issuance(7);
let deposit_id = issuance.deposit_id() as i64;
assert!(manager.get_pending_ticketbooks().await.is_empty());
manager.insert_pending_ticketbook(&issuance).await;
let pending = manager.get_pending_ticketbooks().await;
assert_eq!(pending.len(), 1);
assert_eq!(pending[0].pending_id, deposit_id);
assert_eq!(
pending[0].pending_ticketbook.deposit_id(),
issuance.deposit_id()
);
manager.remove_pending_ticketbook(deposit_id).await;
assert!(manager.get_pending_ticketbooks().await.is_empty());
// removing a non-existent id is a no-op
manager.remove_pending_ticketbook(999).await;
}
#[tokio::test]
async fn emergency_credential_lifecycle() {
let manager = MemoryEcachTicketbookManager::new();
let cred_a = EmergencyCredentialContent {
typ: "type-a".to_string(),
content: vec![1, 2, 3],
expiration: None,
};
let cred_b = EmergencyCredentialContent {
typ: "type-a".to_string(),
content: vec![4, 5, 6],
expiration: None,
};
let cred_c = EmergencyCredentialContent {
typ: "type-b".to_string(),
content: vec![7, 8, 9],
expiration: None,
};
assert!(manager.get_emergency_credential("type-a").await.is_none());
manager.insert_emergency_credential(&cred_a).await;
manager.insert_emergency_credential(&cred_b).await;
manager.insert_emergency_credential(&cred_c).await;
// get returns the first inserted entry for the type
let first = manager.get_emergency_credential("type-a").await.unwrap();
assert_eq!(first.id, 0);
assert_eq!(first.data.content, vec![1, 2, 3]);
// remove by id drops only that entry; type-a now exposes cred_b
manager.remove_emergency_credential(0).await;
let after_remove = manager.get_emergency_credential("type-a").await.unwrap();
assert_eq!(after_remove.id, 1);
assert_eq!(after_remove.data.content, vec![4, 5, 6]);
// remove by type clears the bucket entirely
manager.remove_emergency_credentials_of_type("type-a").await;
assert!(manager.get_emergency_credential("type-a").await.is_none());
// unrelated type is untouched
assert!(manager.get_emergency_credential("type-b").await.is_some());
}
#[tokio::test]
async fn master_verification_key_round_trip() {
let manager = MemoryEcachTicketbookManager::new();
let key = mock_verification_key();
let epoch = EpochVerificationKey {
epoch_id: 7,
key: key.clone(),
};
assert!(manager.get_master_verification_key(7).await.is_none());
manager.insert_master_verification_key(&epoch).await;
assert_eq!(manager.get_master_verification_key(7).await, Some(key));
assert!(manager.get_master_verification_key(8).await.is_none());
}
#[tokio::test]
async fn coin_index_signatures_round_trip() {
let manager = MemoryEcachTicketbookManager::new();
let sigs = AggregatedCoinIndicesSignatures {
epoch_id: 3,
signatures: vec![],
};
assert!(manager.get_coin_index_signatures(3).await.is_none());
manager.insert_coin_index_signatures(&sigs).await;
let retrieved = manager.get_coin_index_signatures(3).await;
assert!(retrieved.is_some());
assert!(retrieved.unwrap().is_empty());
assert!(manager.get_coin_index_signatures(4).await.is_none());
}
#[tokio::test]
async fn expiration_date_signatures_round_trip() {
let manager = MemoryEcachTicketbookManager::new();
let date = nym_ecash_time::ecash_today().date();
let sigs = AggregatedExpirationDateSignatures {
epoch_id: 5,
expiration_date: date,
signatures: vec![],
};
assert!(manager
.get_expiration_date_signatures(date, 5)
.await
.is_none());
manager.insert_expiration_date_signatures(&sigs).await;
let retrieved = manager.get_expiration_date_signatures(date, 5).await;
assert!(retrieved.is_some());
assert!(retrieved.unwrap().is_empty());
// wrong epoch / wrong date → miss
assert!(manager
.get_expiration_date_signatures(date, 6)
.await
.is_none());
}
}
-8
View File
@@ -36,13 +36,5 @@ nym-ecash-contract-common = { workspace = true }
nym-network-defaults = { workspace = true }
nym-serde-helpers = { workspace = true, features = ["date"] }
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.tokio]
workspace = true
features = ["time"]
[target."cfg(target_arch = \"wasm32\")".dependencies.wasmtimer]
workspace = true
features = ["tokio"]
[dev-dependencies]
rand = { workspace = true }
@@ -6,7 +6,6 @@ use crate::ecash::bandwidth::serialiser::VersionedSerialise;
use crate::ecash::bandwidth::CredentialSigningData;
use crate::ecash::utils::cred_exp_date;
use crate::error::Error;
use log::{debug, warn};
use nym_api_requests::ecash::BlindSignRequestBody;
use nym_credentials_interface::{
aggregate_wallets, generate_keypair_user_from_seed, issue_verify, withdrawal_request,
@@ -18,15 +17,8 @@ use nym_ecash_contract_common::deposit::DepositId;
use nym_ecash_time::{ecash_default_expiration_date, ecash_today, EcashTime};
use nym_validator_client::nym_api::{EpochId, NymApiClientExt};
use serde::{Deserialize, Serialize};
use std::time::Duration;
use time::Date;
#[cfg(not(target_arch = "wasm32"))]
use tokio::time::sleep;
#[cfg(target_arch = "wasm32")]
use wasmtimer::tokio::sleep;
pub use nym_validator_client::nyxd::{Coin, Hash};
#[derive(Serialize, Deserialize)]
@@ -200,49 +192,6 @@ impl IssuanceTicketBook {
Ok(unblinded_signature)
}
// ideally this would have been generic over credential type, but we really don't need secp256k1 keys for bandwidth vouchers
pub async fn obtain_partial_ticketbook_credential_with_retries(
&self,
client: &nym_http_api_client::Client,
signer_index: u64,
validator_vk: &VerificationKeyAuth,
signing_data: CredentialSigningData,
max_attempts: usize,
) -> Result<PartialWallet, Error> {
let Some(client_url) = client.base_urls().first() else {
return Err(Error::CredentialShareObtainFailed);
};
let mut last_err = None;
for attempt in 0..max_attempts {
if attempt > 0 {
sleep(Duration::from_millis(500 * attempt as u64)).await;
}
debug!(
"attempt {} / {max_attempts} to obtain partial ticketbook credential from {client_url}",
attempt + 1,
);
match self
.obtain_partial_ticketbook_credential(
client,
signer_index,
validator_vk,
signing_data.clone(),
)
.await
{
Ok(partial_wallet) => return Ok(partial_wallet),
Err(err) => {
warn!(
"attempt {} / {max_attempts} to obtain partial ticketbook credential from {client_url} failed: {err}",
attempt + 1,
);
last_err = Some(err);
}
}
}
Err(last_err.unwrap_or(Error::CredentialShareObtainFailed))
}
// ideally this would have been generic over credential type, but we really don't need secp256k1 keys for bandwidth vouchers
pub async fn obtain_partial_ticketbook_credential(
&self,
+1 -9
View File
@@ -137,8 +137,6 @@ pub async fn obtain_aggregate_wallet(
ecash_api_clients: &[EcashApiClient],
threshold: u64,
) -> Result<WalletSignatures, Error> {
const MAX_ATTEMPTS: usize = 2;
if ecash_api_clients.len() < threshold as usize {
return Err(Error::NoValidatorsAvailable);
}
@@ -156,12 +154,11 @@ pub async fn obtain_aggregate_wallet(
);
match voucher
.obtain_partial_ticketbook_credential_with_retries(
.obtain_partial_ticketbook_credential(
&ecash_api_client.api_client,
ecash_api_client.node_id,
&ecash_api_client.verification_key,
request.clone(),
MAX_ATTEMPTS,
)
.await
{
@@ -170,11 +167,6 @@ pub async fn obtain_aggregate_wallet(
warn!("failed to obtain partial credential from API {ecash_api_client}: {err}",);
}
};
// we got sufficient number of shares
if wallets.len() >= threshold as usize {
break;
}
}
if wallets.len() < threshold as usize {
return Err(Error::NotEnoughShares);
-3
View File
@@ -63,9 +63,6 @@ pub enum Error {
#[error("failed to create a secp256k1 signature")]
Secp256k1SignFailure,
#[error("failed to obtain a valid credential share")]
CredentialShareObtainFailed,
}
impl From<NymAPIError> for Error {
@@ -1,7 +1,21 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use super::PublicKey;
use super::{PrivateKey, PublicKey};
pub mod bs58_x25519_private_key {
use super::*;
use serde::{Deserialize, Deserializer, Serializer};
pub fn serialize<S: Serializer>(key: &PrivateKey, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(&key.to_base58_string())
}
pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<PrivateKey, D::Error> {
let s = String::deserialize(deserializer)?;
PrivateKey::from_base58_string(s).map_err(serde::de::Error::custom)
}
}
pub mod bs58_x25519_pubkey {
use super::*;
-2
View File
@@ -31,5 +31,3 @@ pub use aes_gcm_siv::{Aes128GcmSiv, Aes256GcmSiv};
pub use blake3;
#[cfg(feature = "stream_cipher")]
pub use ctr;
#[cfg(feature = "hashing")]
pub use sha2;
@@ -6,7 +6,7 @@ use nym_coconut_dkg_common::verification_key::VerificationKeyShare;
use nym_crypto::asymmetric::ed25519;
use std::time::Duration;
use time::OffsetDateTime;
use tracing::warn;
use tracing::{debug, warn};
pub trait Verifiable {
fn verify_signature(&self, pub_key: &ed25519::PublicKey) -> bool;
@@ -36,7 +36,6 @@ pub trait ChainResponse: Verifiable + TimestampedResponse {
// we rely on information provided from the api itself AS LONG AS it's not too outdated
if self.timestamp() + stale_response_threshold < now {
warn!("chain status response is stale");
return false;
}
self.chain_synced()
@@ -97,27 +96,26 @@ pub trait SignerResponse: Verifiable + TimestampedResponse {
// we rely on information provided from the api itself AS LONG AS it's not too outdated
if self.timestamp() + stale_response_threshold < now {
warn!("stale signer response");
return false;
}
if !self.has_signing_keys() {
warn!("missing signing keys");
debug!("missing signing keys");
return false;
}
if self.signer_disabled() {
warn!("signer functionalities are explicitly disabled");
debug!("signer functionalities explicitly disabled");
return false;
}
if !self.is_ecash_signer() {
warn!("signer doesn't recognise it's a signer for this epoch");
debug!("signer doesn't recognise it's a signer for this epoch");
return false;
}
if dkg_epoch_id != self.dkg_ecash_epoch_id() {
warn!(
debug!(
"mismatched dkg epoch id. current: {dkg_epoch_id}, signer's: {}",
self.dkg_ecash_epoch_id()
);
@@ -11,11 +11,10 @@ use nym_crypto::asymmetric::ed25519;
use serde::{Deserialize, Serialize};
use std::time::Duration;
use time::OffsetDateTime;
use tracing::warn;
use utoipa::ToSchema;
pub(crate) const CHAIN_STALL_THRESHOLD: Duration = Duration::from_secs(10 * 60);
pub(crate) const STALE_RESPONSE_THRESHOLD: Duration = Duration::from_secs(10 * 60);
pub(crate) const CHAIN_STALL_THRESHOLD: Duration = Duration::from_secs(5 * 60);
pub(crate) const STALE_RESPONSE_THRESHOLD: Duration = Duration::from_secs(5 * 60);
// the reason for generics is not to remove duplication of code,
// but because without them, we'd be having problems with circular dependencies,
@@ -189,7 +188,6 @@ where
};
let SignerStatus::Tested { result } = &self.status else {
warn!("no valid chain response");
return false;
};
result
@@ -241,7 +239,6 @@ where
};
let SignerStatus::Tested { result } = &self.status else {
warn!("no valid signer response");
return false;
};
result.signing_status.signing_available(
@@ -195,9 +195,9 @@ impl ClientUnderTest {
pub(crate) async fn check_client(
dealer_details: DealerDetails,
dkg_epoch: u64,
contract_share: Option<ContractVKShare>,
contract_share: Option<&ContractVKShare>,
) -> TypedSignerResult {
let dealer_information = RawDealerInformation::new(&dealer_details, contract_share.as_ref());
let dealer_information = RawDealerInformation::new(&dealer_details, contract_share);
// 7. attempt to construct client instances out of them
let Ok(parsed_information) = dealer_information.parse() else {
+12 -16
View File
@@ -2,8 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
use crate::client_check::check_client;
use futures::stream;
use futures::stream::StreamExt;
use futures::stream::{FuturesUnordered, StreamExt};
use nym_ecash_signer_check_types::status::{SignerResult, Status};
use nym_network_defaults::NymNetworkDetails;
use nym_validator_client::QueryHttpRpcNyxdClient;
@@ -66,7 +65,7 @@ where
C: DkgQueryClient + Sync,
{
let dkg_details = dkg_details_with_client(client).await?;
check_known_dealers(dkg_details, None).await
check_known_dealers(dkg_details).await
}
pub async fn dkg_details_with_client<C>(client: &C) -> Result<DkgDetails, SignerCheckError>
@@ -110,21 +109,18 @@ where
pub async fn check_known_dealers(
dkg_details: DkgDetails,
concurrency: impl Into<Option<usize>>,
) -> Result<SignersTestResult, SignerCheckError> {
// 6. for each dealer attempt to perform the checks
let epoch_id = dkg_details.dkg_epoch.epoch_id;
let submitted = dkg_details.submitted_shared;
let dealers = dkg_details.network_dealers.len();
let tasks = dkg_details.network_dealers.into_iter().map(move |d| {
let share = submitted.get(&d.assigned_index).cloned();
check_client(d, epoch_id, share)
});
let limit = concurrency.into().filter(|&n| n > 0).unwrap_or(dealers);
let results = stream::iter(tasks).buffer_unordered(limit).collect().await;
let results = dkg_details
.network_dealers
.into_iter()
.map(|d| {
let share = dkg_details.submitted_shared.get(&d.assigned_index);
check_client(d, dkg_details.dkg_epoch.epoch_id, share)
})
.collect::<FuturesUnordered<_>>()
.collect::<Vec<_>>()
.await;
Ok(SignersTestResult {
threshold: dkg_details.threshold,
-2
View File
@@ -10,9 +10,7 @@ fn sanitize_fragment(segment: &str) -> &str {
segment.trim_matches(|c: char| c.is_whitespace() || c == '/')
}
/// Defines a path that can be used to make a request to an API.
pub trait RequestPath: Debug {
/// Sanitise the request path by removing empty segments and trimming whitespace and slashes
fn to_sanitized_segments(&self) -> Vec<&str>;
}

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