Compare commits

..

145 Commits

Author SHA1 Message Date
mfahampshire 0f8a8ddf7e Trim obvious comments, add architecture.md stub 2026-03-17 15:56:04 +00:00
mfahampshire 3c92ce60ca sdk: remove superseded stream_wrapper module 2026-03-17 15:29:34 +00:00
mfahampshire 846dbba363 sdk: add ipr_wrapper module with IpMixStream
- IpMixStream wraps MixnetStream for IPR tunnel over mixnet
- LP Stream framing handled automatically by MixnetStream
- Gateway discovery, connect handshake, IP packet send/receive
2026-03-17 15:29:07 +00:00
mfahampshire 94ab9d5466 IPR: support LP Stream-framed client connections
- Detect and route LP Stream frames in mixnet_listener
- Wrap inline responses in LP Stream frames
- Thread stream_id to ConnectedClientHandler for TUN responses
2026-03-17 15:28:39 +00:00
mfahampshire c78d942383 Replace MixnetStream with LP framing
- Replace custom header with LpFrameHeader
- Added sequence number for message ordering
2026-03-17 12:03:12 +00:00
mfahampshire 0b6166d20e Add LpFrameKind::Stream variant with StreamFrameAttributes
- Define LP wire format for stream multiplexing
- Handle new variant in entry gateway match arm
2026-03-17 12:02:12 +00:00
mfahampshire 6384467526 Reset rebase contamination: restore develop state for shared code
Mass-reset ~50 files that were accidentally modified during rebase
(PollSender/InputMessageCodec/&mut self changes from old experimental
commits). Disable stream_wrapper module (will be rebuilt on MixnetStream
+ LP frame envelope). Remove IpMixStream refs from ip_packet_client
helpers temporarily.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 16:15:40 +00:00
mfahampshire fdd3823585 clean 2026-03-16 15:35:30 +00:00
mfahampshire 892a3bd826 add scratch notes to gitignore 2026-03-16 15:35:29 +00:00
mfahampshire 59ff7d6588 comment 2026-03-16 15:35:29 +00:00
mfahampshire 20c4553bca clippy 2026-03-16 15:35:29 +00:00
mfahampshire 4c38481c36 Fix env discovery 2026-03-16 15:35:29 +00:00
mfahampshire 07680db2c7 mod ignore 2026-03-16 15:35:29 +00:00
mfahampshire 59cbce50f7 mod logging to with poisoning retry 2026-03-16 15:35:29 +00:00
mfahampshire ac13ddbda8 remove unnecessary logging from unit tests 2026-03-16 15:35:29 +00:00
mfahampshire 67803930b6 dont always run dns ping tests 2026-03-16 15:35:29 +00:00
mfahampshire 7052e2e902 fix ipmixstream new() 2026-03-16 15:35:28 +00:00
mfahampshire cccfa76336 clippy 2026-03-16 15:35:28 +00:00
mfahampshire a946336e67 add missing network env 2026-03-16 15:35:28 +00:00
mfahampshire e5836bc1cb fmt 2026-03-16 15:35:28 +00:00
mfahampshire f12108a7db clippy 2026-03-16 15:35:28 +00:00
mfahampshire 70bdbce23f Clippy 2026-03-16 15:35:28 +00:00
mfahampshire e6f9b551ed clippy 2026-03-16 15:35:28 +00:00
mfahampshire fcfa0b604e rustfmt 2026-03-16 15:35:28 +00:00
mfahampshire 8b086e0239 Fix useragent 2026-03-16 15:35:27 +00:00
mfahampshire 6c76834b6c Example code 2026-03-16 15:35:27 +00:00
mfahampshire 071589237b Add network_envs 2026-03-16 15:35:27 +00:00
mfahampshire 771ee10ba2 Update inline examples 2026-03-16 15:35:27 +00:00
mfahampshire 33ce05a3df Add bootstrap network config 2026-03-16 15:35:27 +00:00
mfahampshire 73016ed687 Docs first pass 2026-03-16 15:35:27 +00:00
mfahampshire 8a5205ac4c Include err for no surb tag or peer 2026-03-16 15:35:27 +00:00
mfahampshire aaa7e317bf Fix Mixstream::new() with new configurable network 2026-03-16 15:35:27 +00:00
mfahampshire f28c49e9d6 Update docs + make network configurable 2026-03-16 15:35:27 +00:00
mfahampshire e2ceaf48ed new message borrow 2026-03-16 15:35:27 +00:00
mfahampshire 3e2137a33e First pass rework to bytes in bytes out 2026-03-16 15:35:26 +00:00
mfahampshire 984fa065e3 remove .expect()s and add some encode and decode tests 2026-03-16 15:35:26 +00:00
mfahampshire da46ea7485 remove unused connction type enum 2026-03-16 15:35:26 +00:00
mfahampshire b1bc359806 Fix double copy + deserialisation -> none loop 2026-03-16 15:35:26 +00:00
mfahampshire b338644620 Switch frm default bincode in nymsphinx 2026-03-16 15:35:26 +00:00
mfahampshire 1ec0bf868b use make_bincode_serializer instead of bincode default in client-core 2026-03-16 15:35:26 +00:00
mfahampshire 07842661b9 properly fail on version checks 2026-03-16 15:35:26 +00:00
mfahampshire 0cd4dd5747 Removed unneccesary panics with self.peer_surb_tag 2026-03-16 15:35:26 +00:00
Jędrzej Stuczyński abdd960b20 removed dependency on nym-gateway-directory 2026-03-16 15:35:25 +00:00
mfahampshire db2f3bff05 Use workspace import for mixtcp rustls 2026-03-16 15:35:14 +00:00
mfahampshire be56c79106 remove commented out code 2026-03-16 15:34:54 +00:00
mfahampshire 3ccfbee834 add doc info for other sdk modules 2026-03-16 15:34:53 +00:00
mfahampshire 942ab3c8e8 follow convention for to_v2_bytes 2026-03-16 15:34:53 +00:00
mfahampshire 9ec937dd30 fix comment and duplication in root cargo 2026-03-16 15:34:53 +00:00
mfahampshire 6ccc4a988a use workspace base64 version 2026-03-16 15:34:44 +00:00
mfahampshire 27890eb1a3 remove external patch 2026-03-16 15:34:43 +00:00
mfahampshire fa327a1b2a add license to mixtcp cargo 2026-03-16 15:34:43 +00:00
mfahampshire cea66c1237 edition matches workspace 2026-03-16 15:34:43 +00:00
mfahampshire 757a89c5d7 clippy 2026-03-16 15:34:43 +00:00
mfahampshire 1e3f531e15 remove last nym vpn api deps 2026-03-16 15:34:43 +00:00
mfahampshire 7cc33d8df7 remove nymvpnapi - always use http fallback 2026-03-16 15:34:43 +00:00
mfahampshire 1bd0bfeee1 temp before big mod 2026-03-16 15:34:43 +00:00
mfahampshire f297af2a8c cont removing unnecessary types 2026-03-16 15:34:43 +00:00
mfahampshire d9190e5899 remove unused 2026-03-16 15:34:43 +00:00
mfahampshire a562812ad9 added stream module to mixnet readme 2026-03-16 15:34:43 +00:00
mfahampshire 7368692629 remove external dep on nymvpn repo in sdk 2026-03-16 15:34:42 +00:00
mfahampshire c185f485a7 lint 2026-03-16 15:34:42 +00:00
mfahampshire 6930968e88 lock 2026-03-16 15:34:42 +00:00
mfahampshire 8294191913 remove old commented out imports 2026-03-16 15:34:42 +00:00
mfahampshire 9b2fb45270 temp get rid of logging for ci again 2026-03-16 15:34:42 +00:00
mfahampshire cb8747abb8 temp get rid of logging for ci 2026-03-16 15:34:42 +00:00
mfahampshire 47d37d8aed clippy 2026-03-16 15:34:42 +00:00
mfahampshire d452932b18 clippy 2026-03-16 15:34:42 +00:00
mfahampshire 702dfdc927 clippy warnings: remove 2026-03-16 15:34:30 +00:00
mfahampshire 18e8dfe394 Fix FFI shared lib 2026-03-16 15:34:29 +00:00
mfahampshire 0208a84b77 Mod to mixnet client mutability from traits elsewhere 2026-03-16 15:34:29 +00:00
mfahampshire 7105bbf4b4 Add RwLock to wasm client helper 2026-03-16 15:34:11 +00:00
mfahampshire 39692502df remove accidental import from merge 2026-03-16 15:34:10 +00:00
mfahampshire fcefa079b0 reintroduce import 2026-03-16 15:34:10 +00:00
mfahampshire 371422f27b lint 2026-03-16 15:34:10 +00:00
mfahampshire 5541f242ff smol mixtcp readme 2026-03-16 15:34:10 +00:00
mfahampshire 348e93dd70 rename smolmix - mixtcp 2026-03-16 15:34:10 +00:00
mfahampshire 7f8b7eea8c strip down commenting that is triggering compiler err 2026-03-16 15:34:10 +00:00
mfahampshire 8760c40d46 info -> debug logging for serialised bytes written by stream_wrapper 2026-03-16 15:34:10 +00:00
mfahampshire 8ae4b8fee2 Move files to examples + split examples apart 2026-03-16 15:34:10 +00:00
mfahampshire 4f4885fe50 Remove unused imports 2026-03-16 15:34:09 +00:00
mfahampshire bc52db53b7 remove comments and unused imports 2026-03-16 15:34:09 +00:00
mfahampshire 08d49a6f2e remove unwraps in place of error types 2026-03-16 15:34:09 +00:00
mfahampshire 6f53192dbf deprecate notice for tcpproxy module 2026-03-16 15:34:09 +00:00
mfahampshire b5afb77f19 Clean up unused imports 2026-03-16 15:34:09 +00:00
mfahampshire 29714dea76 Fix gw directory api change in ipr wrapper 2026-03-16 15:34:09 +00:00
mfahampshire 8fd9cee189 almost sorted new version gw dir 2026-03-16 15:34:09 +00:00
mfahampshire 2b4a11e273 linting 2026-03-16 15:34:09 +00:00
mfahampshire a58b32703c add missed stuff from rebase 2026-03-16 15:34:09 +00:00
mfahampshire de80b4ce48 Made explicit error types 2026-03-16 15:34:08 +00:00
mfahampshire 85a3b25be9 Fix logging in tests 2026-03-16 15:33:44 +00:00
mfahampshire 708bd71a56 framing > byte buffer 2026-03-16 15:33:44 +00:00
mfahampshire 40b886e0bd Fix inverted buffer slice logic 2026-03-16 15:33:44 +00:00
mfahampshire 23c1c4bdac Tests + getting reuable client in new() for speedup 2026-03-16 15:33:44 +00:00
mfahampshire 2dd8707725 rough first reqwest client poc 2026-03-16 15:33:44 +00:00
mfahampshire 0bb3c4b2bf remove clunky old examples in place of unit tests 2026-03-16 15:33:43 +00:00
mfahampshire 72e8180abe TLS first version 2026-03-16 15:33:43 +00:00
mfahampshire 2d5b1d577c update readme with new logging 2026-03-16 15:33:43 +00:00
mfahampshire b5e45040ca change logging for nym provider 2026-03-16 15:33:43 +00:00
mfahampshire e420081512 remove old note 2026-03-16 15:33:43 +00:00
mfahampshire 0da4ee985b smolmix device + example 2026-03-16 15:33:43 +00:00
mfahampshire 6d8cacc900 commenting 2026-03-16 15:33:43 +00:00
mfahampshire 49543fcd98 export extra types from ipmixstream 2026-03-16 15:33:43 +00:00
mfahampshire 7b80716c9a split ipmixstream + tests 2026-03-16 15:33:43 +00:00
mfahampshire a4a48c60ae err handling on surb send between split 2026-03-16 15:33:42 +00:00
mfahampshire e027b5a1fe removed IpMixSocket; was a bit unnecessary given connection logic 2026-03-16 15:33:42 +00:00
mfahampshire 723df5584e Remove unnecessary MixnetClient from IpSocket: streamlining 2026-03-16 15:33:42 +00:00
mfahampshire 2ca5155748 more comments 2026-03-16 15:33:42 +00:00
mfahampshire 4f0cc58a11 commenting 2026-03-16 15:33:42 +00:00
mfahampshire 2ccdfedd65 commenting format change + comment out logging in test 2026-03-16 15:33:42 +00:00
mfahampshire d7ddb7592c comment out logging in test 2026-03-16 15:33:42 +00:00
mfahampshire 7371ce3e36 * got ipr pings working with stream_wrapper_ipr
* updated stream_wrapper with debug methods
2026-03-16 15:33:42 +00:00
mfahampshire cd7bb9931e pull in + mod nym-gateway 2026-03-16 15:33:24 +00:00
mfahampshire b77dbdd87e * pulled in helpers from various files
* added readme to explain
2026-03-16 15:33:23 +00:00
mfahampshire 83dcf3fd13 got ipr wrapper connected 2026-03-16 15:33:23 +00:00
mfahampshire a5c6e9d0e2 mod ip_packet_client 2026-03-16 15:33:23 +00:00
mfahampshire a417411184 out of dependency hell 2026-03-16 15:33:23 +00:00
mfahampshire 24d5e4aba9 removed circular dep from gateway-directory 2026-03-16 15:33:23 +00:00
mfahampshire 6cb2fc8445 before directory modification 2026-03-16 15:33:23 +00:00
mfahampshire 4ea2c3beb3 temp commit: got gateway dir dependency working, moving on to vpn-api-client 2026-03-16 15:33:23 +00:00
mfahampshire be8c1191f3 commit before messing with reexport stuff 2026-03-16 15:32:53 +00:00
mfahampshire d969979c8c reorg 2026-03-16 15:32:53 +00:00
mfahampshire c6fd3c8527 added surbs to split r/w + some streamlining + comments + tests 2026-03-16 15:32:53 +00:00
mfahampshire 6ac4d93909 adding surbs + anon reply functionality 2026-03-16 15:32:53 +00:00
mfahampshire 197a7eaec8 make inputmessage anonymous type over simple 2026-03-16 15:32:53 +00:00
mfahampshire f598ee2916 first full pass @ stream + split wrappers 2026-03-16 15:32:53 +00:00
mfahampshire b2fa6cdf8f temp 2026-03-16 15:32:53 +00:00
mfahampshire 97dbef155d initial pass streamwrapper 2026-03-16 15:32:52 +00:00
durch 9dbd91d93e Address part of PR comments 2026-03-16 15:32:52 +00:00
durch 7914cbdbb7 fmt 2026-03-16 15:32:52 +00:00
durch 99febfb3aa Log decoding error 2026-03-16 15:32:52 +00:00
durch 2b00188983 Cleanup prints 2026-03-16 15:32:52 +00:00
durch 82f270329f Update IPR sig 2026-03-16 15:32:52 +00:00
mfahampshire 3cb17e76bd tweaks to tcpproxy example 2026-03-16 15:32:52 +00:00
durch 7b2f8a4ed1 WASM changes 2026-03-16 15:32:40 +00:00
durch 438e745cb3 AsyncWrite 2026-03-16 15:32:40 +00:00
mfahampshire 674fd511f4 remove double asyncwrite 2026-03-16 15:32:40 +00:00
durch 66d85a7c0d Use tokio AsyncRead 2026-03-16 15:32:39 +00:00
durch d12a5d754a ReconstructedMessageCodec 2026-03-16 15:32:21 +00:00
Drazen 3a78d62240 InputMessageCodec, Serde for MixPacket 2026-03-16 15:32:21 +00:00
mfahampshire 5e651b55fc minor changes with new files / fixes 2026-03-16 15:32:05 +00:00
Drazen 8a6bf4a03d Use Sink always 2026-03-16 15:32:04 +00:00
mfahampshire 6a2f1a67ed temp 2026-03-16 15:31:39 +00:00
Drazen d56ab91a2e Switch to PollSender 2026-03-16 15:31:33 +00:00
durch 8f670f467b AsyncRead for MixnetClient 2026-03-16 15:31:08 +00:00
durch d013168823 serde for ReconstructedMessage 2026-03-16 15:29:35 +00:00
1151 changed files with 29341 additions and 72995 deletions
+3 -3
View File
@@ -15,8 +15,10 @@ jobs:
- uses: actions/checkout@v6
- name: Install Dependencies (Linux)
run: sudo apt-get update && sudo apt-get install -y build-essential curl wget libssl-dev libudev-dev squashfs-tools protobuf-compiler git python3 && sudo apt-get update --fix-missing
- name: Install pip3
run: sudo apt install -y python3-pip
- name: Install Python3 modules
run: sudo apt install -y python3-pandas python3-tabulate
run: sudo pip3 install pandas tabulate
- name: Install rsync
run: sudo apt-get install -y rsync
- uses: rlespinasse/github-slug-action@v3.x
@@ -39,8 +41,6 @@ jobs:
- name: Install project dependencies
run: pnpm i
- name: Generate llms-full.txt
run: pnpm run generate:llms
- name: Build project
run: pnpm run build
- name: Generate sitemap
@@ -36,7 +36,7 @@ jobs:
strategy:
fail-fast: false
matrix:
platform: [arc-ubuntu-22.04]
platform: [arc-linux-latest]
runs-on: ${{ matrix.platform }}
env:
+4 -5
View File
@@ -90,7 +90,7 @@ jobs:
uses: actions-rs/cargo@v1
with:
command: clippy
args: --workspace --all-targets --exclude nym-gateway-probe --exclude nym-node-status-api --exclude nym-node-status-agent --exclude nym-node-status-client -- -D warnings
args: --workspace --all-targets --exclude nym-gateway-probe --exclude nym-node-status-api -- -D warnings
- name: Clippy (non-macos)
if: contains(matrix.os, 'linux') || contains(matrix.os, 'windows')
@@ -104,15 +104,14 @@ jobs:
uses: actions-rs/cargo@v1
with:
command: build
args: --workspace --exclude nym-gateway-probe --exclude nym-node-status-api --exclude nym-node-status-agent --exclude nym-node-status-client
# Build Go FFI-dependent crates separately (requires Go, only available on Linux CI)
- name: Build nym-node-status-api and nym-node-status-agent (linux only)
# only build on linux because of wg FFI bindings of its dependency (network probe)
- name: Build nym-node-status-api (linux only)
if: runner.os == 'Linux'
uses: actions-rs/cargo@v1
with:
command: build
args: -p nym-node-status-api -p nym-node-status-agent
args: -p nym-node-status-api
- name: Build all examples
if: contains(matrix.os, 'linux')
+1 -1
View File
@@ -35,7 +35,7 @@ jobs:
components: rustfmt, clippy
- name: Install cosmwasm-check
run: cargo install cosmwasm-check --locked
run: cargo install cosmwasm-check
- name: Install wasm-opt
uses: ./.github/actions/install-wasm-opt
@@ -15,9 +15,6 @@ env:
jobs:
publish-dry-run:
runs-on: arc-linux-latest
timeout-minutes: 35
env:
RUSTUP_PERMIT_COPY_RENAME: 1
steps:
- name: Checkout repo
uses: actions/checkout@v6
@@ -62,60 +59,20 @@ jobs:
- name: Bump versions (local only)
run: |
cargo workspaces version custom ${{ inputs.version }} \
--allow-branch ${{ github.ref_name }} \
--no-git-commit \
--yes
- name: Preflight publish checks
run: |
python3 tools/internal/check_publish_preflight.py
# Dry run may show cascading dependency errors because packages aren't
# actually uploaded - these are expected and ignored. We check for real
# errors like packaging failures, missing metadata, or invalid Cargo.toml.
- name: Publish (dry run)
run: |
set +e
publish_status=1
max_attempts=2
attempt=1
rm -f /tmp/publish-dry-run.log
output=$(cargo workspaces publish --dry-run --allow-dirty 2>&1) || true
echo "$output"
while [ "$attempt" -le "$max_attempts" ]; do
echo "Dry-run publish attempt ${attempt}/${max_attempts}"
cargo workspaces publish --dry-run --allow-dirty 2>&1 | tee /tmp/publish-dry-run.log
publish_status=${PIPESTATUS[0]}
if [ "$publish_status" -eq 0 ]; then
break
fi
# Retry once for interruption/runner issues.
if [ "$attempt" -lt "$max_attempts" ] && \
{ [ "$publish_status" -eq 130 ] || [ "$publish_status" -eq 137 ]; }; then
echo "Publish dry-run interrupted (exit ${publish_status}), retrying in 10s..."
sleep 10
attempt=$((attempt + 1))
continue
fi
break
done
set -e
if grep -Eiq \
"failed to verify manifest|failed to parse manifest|invalid Cargo.toml|error: package .* has no (description|license|repository)" \
/tmp/publish-dry-run.log; then
echo "Detected real packaging/manifest errors"
exit 1
fi
# In dry-run mode, non-zero publish status is expected due to
# dependency-cascade failures against crates.io index.
if [ "$publish_status" -ne 0 ]; then
echo "Dry-run publish returned non-zero (${publish_status}) but no real manifest blockers were detected."
fi
echo "Only expected dry-run dependency cascade errors detected (if any)."
# Check for real errors (not cascading dependency errors)
# Cascading errors mention "crates.io index", real errors mention "Cargo.toml"
echo "$output" | grep -i "Cargo.toml" && exit 1 || true
# Show the list of packages published
- name: Show package versions
@@ -17,8 +17,6 @@ on:
jobs:
publish:
runs-on: arc-linux-latest
env:
RUSTUP_PERMIT_COPY_RENAME: 1
steps:
- name: Checkout repo
uses: actions/checkout@v6
-2
View File
@@ -17,8 +17,6 @@ on:
jobs:
publish:
runs-on: arc-linux-latest
env:
RUSTUP_PERMIT_COPY_RENAME: 1
steps:
- name: Checkout repo
uses: actions/checkout@v6
@@ -15,8 +15,6 @@ env:
jobs:
version-bump:
runs-on: arc-linux-latest
env:
RUSTUP_PERMIT_COPY_RENAME: 1
permissions:
contents: write
steps:
+3 -22
View File
@@ -6,8 +6,6 @@ on:
branches-ignore: [master]
paths:
- "documentation/docs/**"
- "sdk/typescript/packages/sdk/src/**"
- "sdk/typescript/packages/mix-fetch/src/**"
- ".github/workflows/ci-docs.yml"
jobs:
@@ -22,8 +20,10 @@ jobs:
- uses: actions/checkout@v6
- name: Install Dependencies (Linux)
run: sudo apt-get update && sudo apt-get install -y build-essential curl wget libssl-dev libudev-dev squashfs-tools protobuf-compiler git python3 && sudo apt-get update --fix-missing
- name: Install pip3
run: sudo apt install -y python3-pip
- name: Install Python3 modules
run: sudo apt install -y python3-pandas python3-tabulate
run: sudo pip3 install pandas tabulate
- name: Install rsync
run: sudo apt-get install -y rsync
- uses: rlespinasse/github-slug-action@v3.x
@@ -44,27 +44,8 @@ jobs:
command: build
args: --workspace --release
- 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-fetch)/src/'; then
echo "changed=true" >> $GITHUB_OUTPUT
else
echo "changed=false" >> $GITHUB_OUTPUT
fi
working-directory: ${{ github.workspace }}
- name: Regenerate TypeDoc API reference
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-fetch && typedoc --skipErrorChecking
- name: Install project dependencies
run: pnpm i
- name: Generate llms-full.txt
run: pnpm run generate:llms
- name: Build project
run: pnpm run build
- name: Generate sitemap
@@ -1,41 +0,0 @@
name: ci-nym-wallet-frontend
on:
pull_request:
paths:
- 'nym-wallet/**'
- '.github/workflows/ci-nym-wallet-frontend.yml'
jobs:
types-lint:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v4
with:
node-version-file: nym-wallet/.nvmrc
cache: yarn
cache-dependency-path: yarn.lock
- name: Install dependencies
run: yarn install --network-timeout 100000
- name: Build TypeScript packages (wallet depends on @nymproject/types, etc.)
run: yarn build:types
- name: Build @nymproject/mui-theme and @nymproject/react (wallet imports subpaths)
run: yarn build:packages
- name: Typecheck nym-wallet
run: yarn --cwd nym-wallet tsc
- name: Lint nym-wallet
run: yarn --cwd nym-wallet lint
- name: Yarn audit (workspace lockfile; informational)
run: yarn audit --level critical
continue-on-error: true
- name: Unit tests (nym-wallet)
run: yarn --cwd nym-wallet test
-16
View File
@@ -41,9 +41,6 @@ jobs:
sed -i.bak '1s/^/\[profile.dev\]\ndebug = false\n\n/' Cargo.toml
git diff
- name: Ensure nym-wallet/dist exists for Tauri
run: mkdir -p nym-wallet/dist
- name: Build all binaries
uses: actions-rs/cargo@v1
with:
@@ -74,16 +71,3 @@ jobs:
with:
command: clippy
args: --manifest-path nym-wallet/Cargo.toml --workspace --all-features --all-targets -- -D warnings
- name: Install cargo-audit
uses: actions-rs/cargo@v1
with:
command: install
args: cargo-audit --locked
- name: Cargo audit (nym-wallet workspace)
uses: actions-rs/cargo@v1
with:
command: audit
working-directory: nym-wallet
continue-on-error: true
@@ -0,0 +1,53 @@
name: ci-nym-wallet-storybook
on:
pull_request:
paths:
- 'nym-wallet/**'
- '.github/workflows/ci-nym-wallet-storybook.yml'
jobs:
build:
runs-on: arc-linux-latest-dind
steps:
- uses: actions/checkout@v6
- name: Install rsync
run: sudo apt-get install rsync
continue-on-error: true
- uses: rlespinasse/github-slug-action@v3.x
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Setup yarn
run: npm install -g yarn
- name: Install Rust toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ vars.REQUIRED_RUSTC_VERSION }}
- name: Install wasm-pack
run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
- name: Build dependencies
run: yarn && yarn build
- name: Build storybook
run: yarn storybook:build
working-directory: ./nym-wallet
- name: Deploy branch to CI www (storybook)
continue-on-error: true
uses: easingthemes/ssh-deploy@main
env:
SSH_PRIVATE_KEY: ${{ secrets.CI_WWW_SSH_PRIVATE_KEY }}
ARGS: "-rltgoDzvO --delete"
SOURCE: "nym-wallet/storybook-static/"
REMOTE_HOST: ${{ secrets.CI_WWW_REMOTE_HOST }}
REMOTE_USER: ${{ secrets.CI_WWW_REMOTE_USER }}
TARGET: ${{ secrets.CI_WWW_REMOTE_TARGET }}/wallet-${{ env.GITHUB_REF_SLUG }}
EXCLUDE: "/dist/, /node_modules/"
@@ -21,7 +21,7 @@ jobs:
uses: actions/checkout@v6
- name: Install Dependencies (Linux)
run: sudo apt-get update && sudo apt-get install -y libwebkit2gtk-4.1-dev build-essential curl wget libssl-dev libgtk-3-dev squashfs-tools libsoup-3.0-dev libjavascriptcoregtk-4.1-dev
run: sudo apt-get update && sudo apt-get install -y libwebkit2gtk-4.0-dev build-essential curl wget libssl-dev libgtk-3-dev squashfs-tools
if: matrix.os == 'ubuntu-22.04'
- name: Install rust toolchain
+1 -1
View File
@@ -21,7 +21,7 @@ jobs:
fail-fast: false
matrix:
include:
- os: arc-ubuntu-22.04
- os: arc-linux-latest
target: x86_64-unknown-linux-gnu
runs-on: ${{ matrix.os }}
@@ -26,7 +26,7 @@ jobs:
- name: Node
uses: actions/setup-node@v4
with:
node-version: 22.13.0
node-version: 21
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
@@ -30,7 +30,7 @@ jobs:
- name: Node
uses: actions/setup-node@v4
with:
node-version: 22.13.0
node-version: 21
cache: 'yarn'
- name: Install Rust toolchain
@@ -72,41 +72,6 @@ jobs:
find target/release/bundle -type d -name "*appimage*" -o -name "*AppImage*" || echo "No AppImage directories found"
find target/release/bundle -name "*.AppImage" -o -name "*.appimage" || echo "No AppImage files found"
fi
- name: Inspect AppImage (hook + bundled graphics libs)
shell: bash
run: |
set -euo pipefail
APPIMAGE_REL=$(find target/release/bundle -name '*.AppImage' | head -n 1)
if [ -z "${APPIMAGE_REL}" ]; then
echo "No AppImage under target/release/bundle"
exit 1
fi
APPIMAGE_ABS="${GITHUB_WORKSPACE}/nym-wallet/${APPIMAGE_REL}"
chmod +x "${APPIMAGE_ABS}"
EXTRACT_DIR=$(mktemp -d)
cd "${EXTRACT_DIR}"
"${APPIMAGE_ABS}" --appimage-extract
# Tauri only stages appimage "files" under /usr/ into the AppDir; paths like /apprun-hooks/ never reach the image.
# Wayland + WEBKIT_DISABLE_DMABUF_RENDERER defaults are applied in main() instead (see configure_linux_wayland_defaults).
HOOK=$(find squashfs-root -name '99-nym-wayland.sh' 2>/dev/null | head -n 1)
if [ -n "${HOOK}" ]; then
echo "Found legacy apprun hook at ${HOOK}"
else
echo "No apprun-hooks/99-nym-wayland.sh (expected): Wayland defaults are set in-process."
fi
find squashfs-root/usr/lib -maxdepth 6 \
\( -name 'libwayland-client.so*' -o -name 'libEGL.so*' -o -name 'libgbm.so*' \) \
2>/dev/null | sort > "${GITHUB_WORKSPACE}/nym-wallet/appimage-bundled-graphics-libs.txt"
wc -l "${GITHUB_WORKSPACE}/nym-wallet/appimage-bundled-graphics-libs.txt"
head -50 "${GITHUB_WORKSPACE}/nym-wallet/appimage-bundled-graphics-libs.txt" || true
- name: Upload AppImage graphics lib inventory
uses: actions/upload-artifact@v6
with:
name: nym-wallet-appimage-lib-inventory
path: nym-wallet/appimage-bundled-graphics-libs.txt
retention-days: 30
- name: Create AppImage tarball if needed
run: |
+26 -63
View File
@@ -26,9 +26,6 @@ jobs:
outputs:
release_tag: ${{ github.ref_name }}
env:
SIGN_WINDOWS: ${{ github.event_name == 'release' || inputs.sign }}
steps:
- uses: actions/checkout@v6
@@ -40,82 +37,48 @@ jobs:
- name: Setup MSBuild.exe
uses: microsoft/setup-msbuild@v2
# 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:
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
if: ${{ env.SIGN_WINDOWS == 'true' || (github.event_name == 'workflow_dispatch' && !inputs.sign) }}
shell: bash
run: |
set -euo pipefail
if ! command -v yq >/dev/null 2>&1; then
echo "yq is required on this runner to edit tauri.conf.json"
exit 1
fi
yq eval --inplace '
del(.bundle.windows.certificateThumbprint) |
del(.bundle.windows.digestAlgorithm) |
del(.bundle.windows.timestampUrl)
' tauri.conf.json
node-version: 21
- name: Download EV CodeSignTool from ssl.com
working-directory: nym-wallet/src-tauri
if: env.SIGN_WINDOWS == 'true'
if: ${{ inputs.sign }}
shell: bash
run: |
curl -L0 https://www.ssl.com/download/codesigntool-for-linux-and-macos/ -o codesigntool.zip
unzip codesigntool.zip
- name: Get EV certificate credential id
working-directory: nym-wallet/src-tauri
if: env.SIGN_WINDOWS == 'true'
if: ${{ inputs.sign }}
id: get_credential_ids
shell: bash
run: |
echo "SSL_COM_CREDENTIAL_ID=$(./CodeSignTool.sh get_credential_ids -username=${{ secrets.SSL_COM_USERNAME }} -password=${{ secrets.SSL_COM_PASSWORD }} | sed -n '1!p' | sed 's/- //')" >> "$GITHUB_OUTPUT"
- name: Add custom sign command to tauri.conf.json
working-directory: nym-wallet/src-tauri
if: env.SIGN_WINDOWS == 'true'
if: ${{ inputs.sign }}
shell: bash
env:
SSL_SIGN_USER: ${{ secrets.SSL_COM_USERNAME }}
SSL_SIGN_PASS: ${{ secrets.SSL_COM_PASSWORD }}
SSL_SIGN_CRED: ${{ steps.get_credential_ids.outputs.SSL_COM_CREDENTIAL_ID }}
SSL_SIGN_TOTP: ${{ secrets.SSL_COM_TOTP_SECRET }}
run: |
set -euo pipefail
if ! command -v cygpath >/dev/null 2>&1; then
echo "cygpath not found; install Git for Windows or use bash from Git SDK"
exit 1
fi
export SCRIPT_UNIX="$(cygpath -u "$GITHUB_WORKSPACE/nym-wallet/src-tauri/CodeSignTool.sh")"
yq eval --inplace '
.bundle.windows += {
"signCommand": {
"cmd": "C:/Program Files/Git/bin/bash.exe",
"args": [
strenv(SCRIPT_UNIX),
"sign",
("-username " + strenv(SSL_SIGN_USER)),
("-password " + strenv(SSL_SIGN_PASS)),
("-credential_id " + strenv(SSL_SIGN_CRED)),
("-totp_secret " + strenv(SSL_SIGN_TOTP)),
"-program_name NymWallet",
"-input_file_path",
"%1",
"-override"
]
}
yq eval --inplace '.bundle.windows +=
{
"signCommand": {
"cmd": "C:\Program Files\Git\bin\bash.EXE",
"args": [
"/c/actions-runner/_work/nym/nym/nym-wallet/src-tauri/CodeSignTool.sh",
"sign",
"-username ${{ secrets.SSL_COM_USERNAME }}",
"-password ${{ secrets.SSL_COM_PASSWORD }}",
"-credential_id ${{ steps.get_credential_ids.outputs.SSL_COM_CREDENTIAL_ID }}",
"-totp_secret ${{ secrets.SSL_COM_TOTP_SECRET }}",
"-program_name NymWallet",
"-input_file_path",
"%1",
"-override"
]
}
' tauri.conf.json
}' tauri.conf.json
- name: Install project dependencies
shell: bash
run: cd .. && yarn --network-timeout 100000
@@ -130,10 +93,10 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
SSL_COM_USERNAME: ${{ env.SIGN_WINDOWS == 'true' && secrets.SSL_COM_USERNAME }}
SSL_COM_PASSWORD: ${{ env.SIGN_WINDOWS == 'true' && secrets.SSL_COM_PASSWORD }}
SSL_COM_CREDENTIAL_ID: ${{ env.SIGN_WINDOWS == 'true' && steps.get_credential_ids.outputs.SSL_COM_CREDENTIAL_ID }}
SSL_COM_TOTP_SECRET: ${{ env.SIGN_WINDOWS == 'true' && secrets.SSL_COM_TOTP_SECRET }}
SSL_COM_USERNAME: ${{ inputs.sign && secrets.SSL_COM_USERNAME }}
SSL_COM_PASSWORD: ${{ inputs.sign && secrets.SSL_COM_PASSWORD }}
SSL_COM_CREDENTIAL_ID: ${{ inputs.sign && steps.get_credential_ids.outputs.SSL_COM_CREDENTIAL_ID }}
SSL_COM_TOTP_SECRET: ${{ inputs.sign && secrets.SSL_COM_TOTP_SECRET }}
run: |
echo "Starting build process..."
yarn build
@@ -204,4 +167,4 @@ jobs:
needs: publish-tauri
with:
release_tag: ${{ needs.publish-tauri.outputs.release_tag || github.ref_name }}
secrets: inherit
secrets: inherit
@@ -1,61 +0,0 @@
name: Build and upload Network Monitor Agent container to harbor.nymte.ch
on:
workflow_dispatch:
inputs:
release_image:
description: 'Tag image as a release (prefix with golden-)'
required: true
default: false
type: boolean
env:
WORKING_DIRECTORY: "nym-network-monitor-v3/nym-network-monitor-agent"
CONTAINER_NAME: "network-monitor-agent"
jobs:
build-container:
runs-on: arc-linux-latest-dind
steps:
- name: Login to Harbor
uses: docker/login-action@v3
with:
registry: harbor.nymte.ch
username: ${{ secrets.HARBOR_ROBOT_USERNAME }}
password: ${{ secrets.HARBOR_ROBOT_SECRET }}
- name: Checkout repo
uses: actions/checkout@v6
- name: Configure git identity
run: |
git config --global user.email "lawrence@nymtech.net"
git config --global user.name "Lawrence Stalder"
- name: Get version from Cargo.toml
id: get_version
run: |
VERSION=$(yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml)
echo "result=$VERSION" >> $GITHUB_OUTPUT
- name: Set GIT_TAG variable
run: echo "GIT_TAG=${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }}" >> $GITHUB_ENV
- name: Initialize RELEASE_TAG
run: echo "RELEASE_TAG=" >> $GITHUB_ENV
- name: Set RELEASE_TAG for release
if: github.event.inputs.release_image == 'true'
run: echo "RELEASE_TAG=golden-" >> $GITHUB_ENV
- name: Set IMAGE_NAME_AND_TAGS variable
run: echo "IMAGE_NAME_AND_TAGS=${{ env.CONTAINER_NAME }}:${{ env.RELEASE_TAG }}${{ steps.get_version.outputs.result }}" >> $GITHUB_ENV
- name: New env vars
run: echo "RELEASE_TAG='$RELEASE_TAG' GIT_TAG='$GIT_TAG' IMAGE_NAME_AND_TAGS='$IMAGE_NAME_AND_TAGS'"
- name: Build and push image to Harbor
run: |
docker build -f ${{ env.WORKING_DIRECTORY }}/Dockerfile . -t harbor.nymte.ch/nym/${{ env.IMAGE_NAME_AND_TAGS }}
docker push harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }} --all-tags
@@ -1,57 +0,0 @@
name: Build and upload Network Monitor Orchestrator container to harbor.nymte.ch
on:
workflow_dispatch:
inputs:
release_image:
description: 'Tag image as a release (prefix with golden-)'
required: true
default: false
type: boolean
env:
WORKING_DIRECTORY: "nym-network-monitor-v3/nym-network-monitor-orchestrator"
CONTAINER_NAME: "network-monitor-orchestrator"
jobs:
build-container:
runs-on: arc-linux-latest-dind
steps:
- name: Login to Harbor
uses: docker/login-action@v3
with:
registry: harbor.nymte.ch
username: ${{ secrets.HARBOR_ROBOT_USERNAME }}
password: ${{ secrets.HARBOR_ROBOT_SECRET }}
- name: Checkout repo
uses: actions/checkout@v6
- name: Configure git identity
run: |
git config --global user.email "lawrence@nymtech.net"
git config --global user.name "Lawrence Stalder"
- name: Get version from Cargo.toml
id: get_version
run: |
VERSION=$(yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml)
echo "result=$VERSION" >> $GITHUB_OUTPUT
- name: Initialize RELEASE_TAG
run: echo "RELEASE_TAG=" >> $GITHUB_ENV
- name: Set RELEASE_TAG for release
if: github.event.inputs.release_image == 'true'
run: echo "RELEASE_TAG=golden-" >> $GITHUB_ENV
- name: Set IMAGE_NAME_AND_TAGS variable
run: echo "IMAGE_NAME_AND_TAGS=${{ env.CONTAINER_NAME }}:${{ env.RELEASE_TAG }}${{ steps.get_version.outputs.result }}" >> $GITHUB_ENV
- name: Log image name
run: echo "RELEASE_TAG='$RELEASE_TAG' IMAGE_NAME_AND_TAGS='$IMAGE_NAME_AND_TAGS'"
- name: Build and push image to Harbor
run: |
docker build -f ${{ env.WORKING_DIRECTORY }}/Dockerfile . -t harbor.nymte.ch/nym/${{ env.IMAGE_NAME_AND_TAGS }}
docker push harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }} --all-tags
@@ -25,10 +25,6 @@ jobs:
- name: Install cargo-workspaces
run: cargo install cargo-workspaces
- name: Preflight publish checks
run: |
python3 tools/internal/check_publish_preflight.py
- name: Publish remaining crates
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
+1 -1
View File
@@ -27,7 +27,6 @@ v6-topology.json
/explorer/public/downloads/mixmining.json
/explorer/public/downloads/topology.json
/nym-wallet/dist/*
/nym-wallet/appimage-bundled-graphics-libs.txt
/clients/validator/examples/nym-driver-example/current-contract.txt
validator-api/v4.json
validator-api/v6.json
@@ -47,6 +46,7 @@ storybook-static
**/.DS_Store
cpu-cycles/libcpucycles/build
foxyfox.env
scratch.txt
.next
ppa-private-key.b64
-144
View File
@@ -4,150 +4,6 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
## [Unreleased]
## [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])
- Only init SHARED_CLIENT if requested ([#6708])
- Fixes to crates and CI ([#6686])
- Return ipv6 addresses as well ([#6684])
- Fix invalid ticket spend ([#6683])
- Block non-public IPR/NR checks ([#6670])
[#6710]: https://github.com/nymtech/nym/pull/6710
[#6708]: https://github.com/nymtech/nym/pull/6708
[#6686]: https://github.com/nymtech/nym/pull/6686
[#6684]: https://github.com/nymtech/nym/pull/6684
[#6683]: https://github.com/nymtech/nym/pull/6683
[#6670]: https://github.com/nymtech/nym/pull/6670
## [2026.8-urda] (2026-04-20)
- Include all gateways in the returned list ([#6649])
- Optimize GW probe in NS agent ([#6636])
- Max/sdk docrs ([#6566])
- Max/sdk stream wrapper ([#6320])
[#6649]: https://github.com/nymtech/nym/pull/6649
[#6636]: https://github.com/nymtech/nym/pull/6636
[#6566]: https://github.com/nymtech/nym/pull/6566
[#6320]: https://github.com/nymtech/nym/pull/6320
## [2026.7-tola] (2026-04-07)
- Simon/ecash contract serde fix ([#6634])
- Update Fallback IP for Nym API ([#6622])
- Nym Node spam logging ([#6621])
- feat: multiple deposit prices ([#6608])
- move format_debug_bytes in common crate ([#6580])
- bugfix: make sure client keys are generated before requesting credentials ([#6579])
- Fix socks5 GW probe regression ([#6576])
- Max/lp stream framing ([#6573])
- HTTP domain rotation conditions ([#6570])
[#6634]: https://github.com/nymtech/nym/pull/6634
[#6622]: https://github.com/nymtech/nym/pull/6622
[#6621]: https://github.com/nymtech/nym/pull/6621
[#6608]: https://github.com/nymtech/nym/pull/6608
[#6580]: https://github.com/nymtech/nym/pull/6580
[#6579]: https://github.com/nymtech/nym/pull/6579
[#6576]: https://github.com/nymtech/nym/pull/6576
[#6573]: https://github.com/nymtech/nym/pull/6573
[#6570]: https://github.com/nymtech/nym/pull/6570
## [2026.6-stilton] (2026-03-25)
- lp fixes ([#6601])
- bugfix: allow deserialisation of LP data from either snake_case or lowercase ([#6586])
- bugfix: make sure to run cargo install cosmwasm-check with --locked flag during CI ([#6568])
- Add LP to NS UI ([#6562])
- feat: nyxd watcher ([#6561])
- Additional ticket for agent ([#6551])
- bugfix: make sure to use old values from metrics debug config during v12 migration (#6546) ([#6547])
- typo ([#6543])
- rng changes for a Send variant ([#6541])
- Add LP fields ([#6535])
- enable LP registration in registration client ([#6534])
- chore: rename LpMessage to LpFrame ([#6530])
- chore: LP improvements ([#6526])
- Remove dep leak of strum iterator ([#6522])
- chore: update ts-rs dep ([#6517])
- addressing LP PR comments ([#6513])
- remove redundant LP state machine in favour of in place processing ([#6512])
- chore: split up lp listener ([#6507])
- feat: enable mutual KKT exchange ([#6505])
- feat: introduce /v3/unstable/nym-nodes/semi-skimmed to aggregate LP information ([#6499])
- Max/asyncread asyncwrite nym client ([#6318])
- feat: localnet v2 ([#6277])
[#6601]: https://github.com/nymtech/nym/pull/6601
[#6586]: https://github.com/nymtech/nym/pull/6586
[#6568]: https://github.com/nymtech/nym/pull/6568
[#6562]: https://github.com/nymtech/nym/pull/6562
[#6561]: https://github.com/nymtech/nym/pull/6561
[#6551]: https://github.com/nymtech/nym/pull/6551
[#6547]: https://github.com/nymtech/nym/pull/6547
[#6543]: https://github.com/nymtech/nym/pull/6543
[#6541]: https://github.com/nymtech/nym/pull/6541
[#6535]: https://github.com/nymtech/nym/pull/6535
[#6534]: https://github.com/nymtech/nym/pull/6534
[#6530]: https://github.com/nymtech/nym/pull/6530
[#6526]: https://github.com/nymtech/nym/pull/6526
[#6522]: https://github.com/nymtech/nym/pull/6522
[#6517]: https://github.com/nymtech/nym/pull/6517
[#6513]: https://github.com/nymtech/nym/pull/6513
[#6512]: https://github.com/nymtech/nym/pull/6512
[#6507]: https://github.com/nymtech/nym/pull/6507
[#6505]: https://github.com/nymtech/nym/pull/6505
[#6499]: https://github.com/nymtech/nym/pull/6499
[#6318]: https://github.com/nymtech/nym/pull/6318
[#6277]: https://github.com/nymtech/nym/pull/6277
## [2026.5-raclette] (2026-03-10)
- bugfix: correctly populate gateway probe LP data ([#6533])
Generated
+1467 -1717
View File
File diff suppressed because it is too large Load Diff
+10 -27
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",
@@ -148,7 +147,6 @@ members = [
"sdk/ffi/go",
"sdk/ffi/shared",
"sdk/rust/nym-sdk",
"smolmix/core",
"service-providers/common",
"service-providers/ip-packet-router",
"service-providers/network-requester",
@@ -177,8 +175,6 @@ members = [
"integration-tests",
"common/nym-kkt-ciphersuite",
"common/nym-kkt-context",
"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 = [
@@ -188,6 +184,7 @@ default-members = [
"nym-api",
"nym-credential-proxy/nym-credential-proxy",
"nym-node",
"nym-node-status-api/nym-node-status-agent",
"nym-statistics-api",
"nym-validator-rewarder",
"nyx-chain-watcher",
@@ -195,8 +192,6 @@ default-members = [
"service-providers/network-requester",
"tools/nymvisor",
"nym-registration-client",
"nym-network-monitor-v3/nym-network-monitor-orchestrator",
"nym-network-monitor-v3/nym-network-monitor-agent",
"tools/internal/localnet-orchestrator"
]
@@ -285,8 +280,7 @@ getrandom03 = { package = "getrandom", version = "=0.3.3" }
glob = "0.3"
handlebars = "3.5.5"
hex = "0.4.3"
hickory-proto = "0.26.1"
hickory-resolver = "0.26.1"
hickory-resolver = "0.25.2"
hkdf = "0.12.3"
hmac = "0.12.1"
http = "1"
@@ -340,7 +334,6 @@ rayon = "1.5.1"
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 }
schemars = "0.8.22"
semver = "1.0.26"
serde = "1.0.219"
@@ -354,8 +347,6 @@ serde_yaml = "0.9.25"
serde_plain = "1.0.2"
sha2 = "0.10.3"
si-scale = "0.2.3"
smolmix = { version = "0.0.1", path = "smolmix/core" }
smoltcp = "0.12"
snow = "0.9.6"
sphinx-packet = "=0.6.0"
sqlx = "0.8.6"
@@ -376,8 +367,6 @@ tokio-postgres = "0.7"
tokio-stream = "0.1.17"
tokio-test = "0.4.4"
tokio-tun = "0.11.5"
tokio-rustls = "0.26"
tokio-smoltcp = "0.5"
tokio-tungstenite = { version = "0.20.1" }
tokio-util = "0.7.15"
toml = "0.8.22"
@@ -407,20 +396,16 @@ 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"
libcrux-ecdh = "0.0.6"
libcrux-curve25519 = "0.0.6"
libcrux-chacha20poly1305 = "0.0.7"
libcrux-psq = "0.0.8"
libcrux-ml-kem = "0.0.8"
libcrux-sha3 = "0.0.8"
libcrux-traits = "0.0.6"
libcrux-kem = { git = "https://github.com/cryspen/libcrux", rev = "b17f8687b67cdcfc10b55aeecc998bbbca28f775" }
libcrux-ecdh = { git = "https://github.com/cryspen/libcrux", rev = "b17f8687b67cdcfc10b55aeecc998bbbca28f775" }
libcrux-curve25519 = { git = "https://github.com/cryspen/libcrux", rev = "b17f8687b67cdcfc10b55aeecc998bbbca28f775" }
libcrux-chacha20poly1305 = { git = "https://github.com/cryspen/libcrux", rev = "b17f8687b67cdcfc10b55aeecc998bbbca28f775" }
libcrux-psq = { git = "https://github.com/cryspen/libcrux", rev = "b17f8687b67cdcfc10b55aeecc998bbbca28f775" }
libcrux-ml-kem = { git = "https://github.com/cryspen/libcrux", rev = "b17f8687b67cdcfc10b55aeecc998bbbca28f775" }
libcrux-sha3 = { git = "https://github.com/cryspen/libcrux", rev = "b17f8687b67cdcfc10b55aeecc998bbbca28f775" }
libcrux-traits = { git = "https://github.com/cryspen/libcrux", rev = "b17f8687b67cdcfc10b55aeecc998bbbca28f775" }
# 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.20.4", path = "nym-api/nym-api-requests" }
@@ -516,7 +501,6 @@ nym-types = { version = "1.20.4", path = "common/types" }
nym-upgrade-mode-check = { version = "1.20.4", path = "common/upgrade-mode-check" }
nym-validator-client = { version = "1.20.4", path = "common/client-libs/validator-client", default-features = false }
nym-vesting-contract-common = { version = "1.20.4", path = "common/cosmwasm-smart-contracts/vesting-contract" }
nym-network-monitors-contract-common = { version = "1.20.4", path = "common/cosmwasm-smart-contracts/network-monitors-contract" }
nym-verloc = { version = "1.20.4", path = "common/verloc" }
nym-wireguard = { version = "1.20.4", path = "common/wireguard" }
nym-wireguard-types = { version = "1.20.4", path = "common/wireguard-types" }
@@ -575,7 +559,6 @@ wasm-bindgen = "0.2.99"
wasm-bindgen-futures = "0.4.49"
wasm-bindgen-test = "0.3.49"
wasmtimer = "0.4.1"
webpki-roots = "0.26"
web-sys = "0.3.76"
# for local development:
@@ -60,8 +60,7 @@ packages:
## SYSTEM MAINTENANCE PLAYBOOK KNOBS
###############################################################################
# To use particular version instead of Latest, provide in such form:
# nym_version: "nym-binaries-v2026.7-tola"
# nym_version: "v2025.21-mozzarella"
## NOTE:
## if you want to pin Nym to a specific version instead of using the
@@ -118,4 +117,4 @@ packages:
# enable_writeback_tuning: true
# writeback_dirty_writeback_centisecs: 1500
# writeback_dirty_expire_centisecs: 6000
# writeback_dirty_expire_centisecs: 6000
+6 -25
View File
@@ -1,30 +1,11 @@
---
- name: Ensure nym binaries directory exists
file:
path: /root/nym-binaries
state: directory
mode: "0755"
- name: Configure tunnel manager
tags:
- tunnel
- network_tunnel_manager
- ntm
- name: Download network tunnel manager
get_url:
url: "{{ tunnel_manager_url }}"
dest: /root/nym-binaries/network-tunnel-manager.sh
mode: "0755"
force: yes
tags:
- tunnel
- network_tunnel_manager
- ntm
- name: Run network tunnel manager
command: "/root/nym-binaries/network-tunnel-manager.sh {{ item }}"
become: true
command:
cmd: "/root/nym-binaries/network-tunnel-manager.sh {{ item }}"
loop:
- complete_networking_configuration
tags:
- tunnel
- network_tunnel_manager
- ntm
register: tunnel_mgr
failed_when: false
+3 -3
View File
@@ -1,11 +1,11 @@
[package]
name = "nym-client"
description = "Implementation of the Nym Client"
version = "1.1.77"
version = "1.1.72"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>", "Jędrzej Stuczyński <andrew@nymtech.net>"]
description = "Implementation of the Nym Client"
edition = "2021"
license.workspace = true
rust-version = "1.85"
license.workspace = true
publish = false
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+1 -4
View File
@@ -1,16 +1,13 @@
[package]
name = "nym-client-websocket-requests"
description = "Request and response definitions for Nym client websocket connections"
version.workspace = true
authors = ["Jędrzej Stuczyński <andrew@nymtech.net>"]
edition = "2021"
license.workspace = true
description = "Request and response definitions for Nym client websocket connections"
repository.workspace = true
homepage.workspace = true
documentation.workspace = true
rust-version.workspace = true
readme.workspace = true
publish = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+3 -3
View File
@@ -1,11 +1,11 @@
[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.77"
version = "1.1.72"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>"]
description = "A SOCKS5 localhost proxy that converts incoming messages to Sphinx and sends them to a Nym address"
edition = "2021"
license.workspace = true
rust-version = "1.85"
license.workspace = true
publish = false
[dependencies]
+1 -5
View File
@@ -1,16 +1,12 @@
[package]
name = "nym-async-file-watcher"
description = "Simple file watcher that sends a notification whenever there was any change in the watched file"
version.workspace = true
authors.workspace = true
edition.workspace = true
license.workspace = true
description = "Simple file watcher that sends a notification whenever there was any change in the watched file"
repository.workspace = true
homepage.workspace = true
documentation.workspace = true
rust-version.workspace = true
readme.workspace = true
publish = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+3 -6
View File
@@ -1,16 +1,13 @@
[package]
name = "nym-authenticator-requests"
description = "Crate defining requests and responses for the Nym authenticator client"
version.workspace = true
authors.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
homepage.workspace = true
documentation.workspace = true
rust-version.workspace = true
readme.workspace = true
publish = true
edition.workspace = true
license.workspace = true
description = "Crate defining requests and responses for the Nym authenticator client"
[dependencies]
base64 = { workspace = true }
+1 -5
View File
@@ -1,16 +1,12 @@
[package]
name = "nym-bandwidth-controller"
description = "Crate for controlling the use of zknym credentials to ensure constant bandwidth availability for NymVPN app"
version.workspace = true
authors.workspace = true
edition = "2021"
license.workspace = true
description = "Crate for controlling the use of zknym credentials to ensure constant bandwidth availability for NymVPN app"
repository.workspace = true
homepage.workspace = true
documentation.workspace = true
rust-version.workspace = true
readme.workspace = true
publish = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@@ -34,7 +34,7 @@ where
let signing_key = ed25519::PrivateKey::new(&mut rng);
let expiration = expiration.unwrap_or_else(ecash_default_expiration_date);
let deposit_amount = client.get_default_deposit_amount().await?;
let deposit_amount = client.get_required_deposit_amount().await?;
info!("we'll need to deposit {deposit_amount} to obtain the ticketbook");
let result = client
.make_ticketbook_deposit(
+2 -8
View File
@@ -1,16 +1,11 @@
[package]
name = "nym-bin-common"
description = "Common code for nym binaries"
version.workspace = true
authors = { workspace = true }
description = "Common code for nym binaries"
edition = { workspace = true }
authors = { workspace = true }
license = { workspace = true }
repository = { workspace = true }
homepage.workspace = true
documentation.workspace = true
rust-version.workspace = true
readme.workspace = true
publish = true
[dependencies]
clap = { workspace = true, features = ["derive"], optional = true }
@@ -43,7 +38,6 @@ default = []
openapi = ["utoipa"]
output_format = ["serde_json", "dep:clap"]
bin_info_schema = ["schemars"]
ip_check = []
basic_tracing = ["dep:tracing", "dep:tracing-subscriber"]
otel-otlp = [
"basic_tracing",
-3
View File
@@ -9,6 +9,3 @@ pub mod completions;
#[cfg(feature = "output_format")]
pub mod output_format;
#[cfg(feature = "ip_check")]
pub mod ip_check;
+2 -4
View File
@@ -1,16 +1,14 @@
[package]
name = "nym-client-core"
description = "Crate containing core client functionality and configs, used by all other Nym client implentations"
version.workspace = true
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>"]
edition = "2024"
rust-version = "1.85"
license.workspace = true
description = "Crate containing core client functionality and configs, used by all other Nym client implentations"
repository.workspace = true
homepage.workspace = true
documentation.workspace = true
rust-version = "1.85"
readme.workspace = true
publish = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+1 -5
View File
@@ -1,16 +1,12 @@
[package]
name = "nym-client-core-config-types"
description = "Low level configs and constants used by Nym clients and nodes"
version.workspace = true
authors.workspace = true
edition = "2021"
license.workspace = true
description = "Low level configs and constants used by Nym clients and nodes"
repository.workspace = true
homepage.workspace = true
documentation.workspace = true
rust-version.workspace = true
readme.workspace = true
publish = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@@ -1,16 +1,13 @@
[package]
name = "nym-client-core-gateways-storage"
description = "Functionality for Nym clients to store and retrive Gateway connections"
version.workspace = true
authors.workspace = true
edition = "2021"
license.workspace = true
rust-version.workspace = true
description = "Functionality for Nym clients to store and retrive Gateway connections"
repository.workspace = true
homepage.workspace = true
documentation.workspace = true
rust-version.workspace = true
readme.workspace = true
publish = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@@ -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 -5
View File
@@ -1,16 +1,12 @@
[package]
name = "nym-client-core-surb-storage"
description = "Functionality for Nym clients to generate and use Single Use Reply Blocks (SURBs)"
version.workspace = true
authors.workspace = true
edition = "2021"
license.workspace = true
description = "Functionality for Nym clients to generate and use Single Use Reply Blocks (SURBs)"
repository.workspace = true
homepage.workspace = true
documentation.workspace = true
rust-version.workspace = true
readme.workspace = true
publish = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+1 -4
View File
@@ -1,16 +1,13 @@
[package]
name = "nym-gateway-client"
description = "Functions and types for Nym client <> Gateway connections"
version.workspace = true
authors = ["Jędrzej Stuczyński <andrew@nymtech.net>"]
edition = "2021"
license.workspace = true
description = "Functions and types for Nym client <> Gateway connections"
repository.workspace = true
homepage.workspace = true
documentation.workspace = true
rust-version.workspace = true
readme.workspace = true
publish = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+1 -5
View File
@@ -1,16 +1,13 @@
[package]
name = "nym-mixnet-client"
description = "Client for Mix Node <> Mix Node & Mix Node <> Gateway communication"
version.workspace = true
authors = ["Jedrzej Stuczynski <andrew@nymtech.net>"]
edition = "2021"
license.workspace = true
description = "Client for Mix Node <> Mix Node & Mix Node <> Gateway communication"
repository.workspace = true
homepage.workspace = true
documentation.workspace = true
rust-version.workspace = true
readme.workspace = true
publish = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@@ -34,4 +31,3 @@ 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"] }
+73 -281
View File
@@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
use dashmap::DashMap;
use futures::{SinkExt, StreamExt};
use futures::StreamExt;
use nym_noise::config::NoiseConfig;
use nym_noise::upgrade_noise_initiator;
use nym_sphinx::forwarding::packet::MixPacket;
@@ -14,7 +14,6 @@ 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;
@@ -91,17 +90,13 @@ impl Deref for ActiveConnections {
pub struct ConnectionSender {
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<FramedNymPacket>, handle_token: Arc<()>) -> Self {
fn new(channel: mpsc::Sender<FramedNymPacket>) -> Self {
ConnectionSender {
channel,
current_reconnection_attempt: Arc::new(AtomicU32::new(0)),
handle_token,
}
}
}
@@ -112,31 +107,6 @@ struct ManagedConnection {
message_receiver: ReceiverStream<FramedNymPacket>,
connection_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 {
@@ -146,8 +116,6 @@ impl ManagedConnection {
message_receiver: mpsc::Receiver<FramedNymPacket>,
connection_timeout: Duration,
current_reconnection: Arc<AtomicU32>,
active_connections: ActiveConnections,
handle_token: Arc<()>,
) -> Self {
ManagedConnection {
address,
@@ -155,30 +123,72 @@ impl ManagedConnection {
message_receiver: ReceiverStream::new(message_receiver),
connection_timeout,
current_reconnection,
active_connections,
handle_token,
}
}
async fn run(self) {
let address = self.address;
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,
@@ -193,133 +203,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
);
// 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 conn = Framed::new(noise_stream, NymCodec);
// 4. start handling the framed stream
run_io_loop(conn, self.message_receiver, address).await;
}
}
// 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>,
mut receiver: ReceiverStream<FramedNymPacket>,
address: SocketAddr,
) where
T: AsyncRead + AsyncWrite + Unpin,
{
let (mut sink, mut stream) = conn.split();
loop {
tokio::select! {
msg = stream.next() => {
match msg {
None => {
debug!(
peer = %address,
exit_reason = "peer_closed",
"peer closed mixnet connection to {address}"
);
break;
}
Some(Err(err)) => {
debug!(
event = "connection.read_error",
peer = %address,
error = %err,
exit_reason = "read_error",
"read error on mixnet connection to {address}: {err}"
);
break;
}
Some(Ok(_)) => {
trace!(
peer = %address,
"unexpected inbound packet on mixnet connection to {address}; discarding"
);
}
}
}
outgoing = receiver.next() => {
match outgoing {
None => {
debug!(
peer = %address,
exit_reason = "sender_dropped",
"connection manager to {address} finished"
);
break;
}
Some(packet) => {
if let Err(err) = sink.send(packet).await {
debug!(
event = "connection.forward_error",
peer = %address,
error = %err,
exit_reason = "forward_error",
"Failed to forward packet to {address}: {err}"
);
break;
}
}
}
}
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 packets to {address}: {err}"
);
}
debug!(
peer = %address,
exit_reason = "sender_dropped",
"connection manager to {address} finished"
);
}
}
@@ -366,18 +264,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
@@ -392,7 +285,6 @@ impl Client {
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 {
@@ -407,8 +299,6 @@ impl Client {
receiver,
initial_connection_timeout,
current_reconnection_attempt,
active_connections,
handle_token,
)
.run()
.await;
@@ -452,7 +342,7 @@ impl SendWithoutResponse for Client {
sending_res.map_err(|err| {
match err {
TrySendError::Full(_) => {
trace!(
warn!(
event = "mixclient.try_send",
peer = %address,
result = "full_dropped",
@@ -538,102 +428,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<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);
let task = tokio::spawn(run_io_loop(conn, ReceiverStream::new(rx), test_addr()));
// 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()));
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");
}
}
@@ -21,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()
@@ -42,23 +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,
}
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,
) -> Self {
pub fn new(packet: MixPacket, forward_delay_target: Option<Instant>) -> Self {
PacketToForward {
packet,
forward_delay_target,
network_monitor_packet,
}
}
pub fn client_packet_without_delay(packet: MixPacket) -> Self {
Self::new(packet, None, false)
pub fn new_no_delay(packet: MixPacket) -> Self {
Self::new(packet, None)
}
}
@@ -1,16 +1,14 @@
[package]
name = "nym-validator-client"
description = "Client for interacting with Nyx Cosmos SDK blockchain"
version.workspace = true
authors = ["Jędrzej Stuczyński <andrew@nymtech.net>"]
edition = "2021"
rust-version = "1.85"
license.workspace = true
description = "Client for interacting with Nyx Cosmos SDK blockchain"
repository.workspace = true
homepage.workspace = true
documentation.workspace = true
rust-version = "1.85"
readme.workspace = true
publish = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@@ -26,7 +24,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-serde-helpers = { workspace = true, features = ["hex", "base64"] }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
@@ -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,15 +15,11 @@ 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::{
AnnotationResponseV1, 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};
@@ -980,7 +976,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,
@@ -1363,53 +1359,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
@@ -49,9 +49,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";
@@ -8,7 +8,6 @@ use crate::nyxd::CosmWasmClient;
use async_trait::async_trait;
use cosmwasm_std::Coin;
use nym_ecash_contract_common::deposit::LatestDepositResponse;
use nym_ecash_contract_common::deposit_statistics::DepositsStatistics;
use nym_ecash_contract_common::msg::QueryMsg as EcashQueryMsg;
use serde::Deserialize;
@@ -18,9 +17,6 @@ pub use nym_ecash_contract_common::blacklist::{
pub use nym_ecash_contract_common::deposit::{
Deposit, DepositData, DepositId, DepositResponse, PagedDepositsResponse,
};
pub use nym_ecash_contract_common::reduced_deposit::{
WhitelistedAccount, WhitelistedAccountsResponse,
};
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
@@ -46,18 +42,8 @@ pub trait EcashQueryClient {
.await
}
async fn get_default_deposit_amount(&self) -> Result<Coin, NyxdError> {
self.query_ecash_contract(EcashQueryMsg::GetDefaultDepositAmount {})
.await
}
async fn get_reduced_deposit_amount(&self, address: String) -> Result<Option<Coin>, NyxdError> {
self.query_ecash_contract(EcashQueryMsg::GetReducedDepositAmount { address })
.await
}
async fn get_all_whitelisted_accounts(&self) -> Result<WhitelistedAccountsResponse, NyxdError> {
self.query_ecash_contract(EcashQueryMsg::GetAllWhitelistedAccounts {})
async fn get_required_deposit_amount(&self) -> Result<Coin, NyxdError> {
self.query_ecash_contract(EcashQueryMsg::GetRequiredDepositAmount {})
.await
}
@@ -79,11 +65,6 @@ pub trait EcashQueryClient {
self.query_ecash_contract(EcashQueryMsg::GetDepositsPaged { start_after, limit })
.await
}
async fn get_deposits_statistics(&self) -> Result<DepositsStatistics, NyxdError> {
self.query_ecash_contract(EcashQueryMsg::GetDepositsStatistics {})
.await
}
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
@@ -141,17 +122,10 @@ mod tests {
EcashQueryMsg::GetDepositsPaged { limit, start_after } => {
client.get_deposits_paged(start_after, limit).ignore()
}
EcashQueryMsg::GetDefaultDepositAmount {} => {
client.get_default_deposit_amount().ignore()
}
EcashQueryMsg::GetReducedDepositAmount { address } => {
client.get_reduced_deposit_amount(address).ignore()
}
EcashQueryMsg::GetAllWhitelistedAccounts {} => {
client.get_all_whitelisted_accounts().ignore()
EcashQueryMsg::GetRequiredDepositAmount {} => {
client.get_required_deposit_amount().ignore()
}
EcashQueryMsg::GetLatestDeposit {} => client.get_latest_deposit().ignore(),
EcashQueryMsg::GetDepositsStatistics {} => client.get_deposits_statistics().ignore(),
};
}
}
@@ -62,47 +62,13 @@ pub trait EcashSigningClient {
new_deposit: Coin,
fee: Option<Fee>,
) -> Result<ExecuteResult, NyxdError> {
let req = EcashExecuteMsg::UpdateDefaultDepositValue {
let req = EcashExecuteMsg::UpdateDepositValue {
new_deposit: new_deposit.into(),
};
self.execute_ecash_contract(fee, req, "Ecash::UpdateDepositValue".to_string(), vec![])
.await
}
async fn set_reduced_deposit_price(
&self,
address: String,
deposit: Coin,
fee: Option<Fee>,
) -> Result<ExecuteResult, NyxdError> {
let req = EcashExecuteMsg::SetReducedDepositPrice {
address,
deposit: deposit.into(),
};
self.execute_ecash_contract(
fee,
req,
"Ecash::SetReducedDepositPrice".to_string(),
vec![],
)
.await
}
async fn remove_reduced_deposit_price(
&self,
address: String,
fee: Option<Fee>,
) -> Result<ExecuteResult, NyxdError> {
let req = EcashExecuteMsg::RemoveReducedDepositPrice { address };
self.execute_ecash_contract(
fee,
req,
"Ecash::RemoveReducedDepositPrice".to_string(),
vec![],
)
.await
}
async fn propose_for_blacklist(
&self,
public_key: String,
@@ -175,15 +141,9 @@ mod tests {
.ignore(),
ExecuteMsg::RedeemTickets { .. } => unimplemented!(), // no redeem tickets method for the client
ExecuteMsg::UpdateAdmin { admin } => client.update_admin(admin, None).ignore(),
ExecuteMsg::UpdateDefaultDepositValue { new_deposit } => client
ExecuteMsg::UpdateDepositValue { new_deposit } => client
.update_deposit_value(new_deposit.into(), None)
.ignore(),
ExecuteMsg::SetReducedDepositPrice { address, deposit } => client
.set_reduced_deposit_price(address, deposit.into(), None)
.ignore(),
ExecuteMsg::RemoveReducedDepositPrice { address } => {
client.remove_reduced_deposit_price(address, None).ignore()
}
};
}
}
@@ -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 performance_query_client;
pub mod vesting_query_client;
@@ -23,7 +22,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 performance_signing_client;
pub mod vesting_signing_client;
@@ -33,9 +31,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 performance_query_client::{PagedPerformanceQueryClient, PerformanceQueryClient};
pub use vesting_query_client::{PagedVestingQueryClient, VestingQueryClient};
@@ -45,7 +40,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 performance_signing_client::PerformanceSigningClient;
pub use vesting_signing_client::VestingSigningClient;
@@ -55,7 +49,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>;
// coconut-related
fn ecash_contract_address(&self) -> Option<&AccountId>;
@@ -69,7 +62,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 ecash_contract_address: Option<AccountId>,
pub group_contract_address: Option<AccountId>,
@@ -94,10 +86,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()?,
ecash_contract_address: value
.ecash_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()
}
};
}
}
@@ -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
}
@@ -307,10 +289,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> {
@@ -325,12 +303,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 ecash_contract_address(&self) -> Option<&AccountId> {
self.config.contracts.ecash_contract_address.as_ref()
+1 -4
View File
@@ -1,16 +1,13 @@
[package]
name = "nym-cli-commands"
description = "Common commands crate used by the nym-cli tool for interacting with the Nyx Cosmos SDK blockchain and Mixnet endpoints"
version.workspace = true
authors.workspace = true
edition = "2021"
license.workspace = true
description = "Common commands crate used by the nym-cli tool for interacting with the Nyx Cosmos SDK blockchain and Mixnet endpoints"
repository.workspace = true
homepage.workspace = true
documentation.workspace = true
rust-version.workspace = true
readme.workspace = true
publish = true
[dependencies]
anyhow = { workspace = true }
+1 -6
View File
@@ -1,16 +1,11 @@
[package]
name = "nym-config"
description = "Config related helpers and functions"
version.workspace = true
authors = ["Jedrzej Stuczynski <andrew@nymtech.net>"]
edition = "2021"
license.workspace = true
repository.workspace = true
homepage.workspace = true
documentation.workspace = true
rust-version.workspace = true
readme.workspace = true
publish = true
description = "Config related helpers and functions"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@@ -1,16 +1,12 @@
[package]
name = "nym-coconut-dkg-common"
description = "Common crate for Nym's DKG cosmwasm contract"
version.workspace = true
authors.workspace = true
edition = "2021"
license.workspace = true
description = "Common crate for Nym's DKG cosmwasm contract"
repository.workspace = true
homepage.workspace = true
documentation.workspace = true
rust-version = "1.85"
readme.workspace = true
publish = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@@ -1,16 +1,15 @@
[package]
name = "nym-contracts-common-testing"
description = "Common crate for cosmwasm contract tests"
version.workspace = true
authors.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
homepage.workspace = true
documentation.workspace = true
rust-version = "1.85"
edition.workspace = true
license.workspace = true
rust-version.workspace = true
readme.workspace = true
publish = true
description = "Common crate for cosmwasm contract tests"
[dependencies]
anyhow = { workspace = true }
@@ -1,16 +1,11 @@
[package]
name = "nym-contracts-common"
description = "Common library for Nym cosmwasm contracts"
version.workspace = true
authors = { workspace = true }
description = "Common library for Nym cosmwasm contracts"
edition = { workspace = true }
authors = { workspace = true }
license = { workspace = true }
repository = { workspace = true }
homepage.workspace = true
documentation.workspace = true
rust-version = "1.85"
readme.workspace = true
publish = true
[dependencies]
bs58 = { workspace = true }
@@ -2,8 +2,8 @@
name = "easy-addr"
version.workspace = true
edition = "2021"
license.workspace = true
publish = false
license.workspace = true
[lib]
proc-macro = true
@@ -1,16 +1,12 @@
[package]
name = "nym-ecash-contract-common"
description = "Common crate for Nym's ecash/zknym cosmwasm contract"
version.workspace = true
authors.workspace = true
edition = "2021"
license.workspace = true
description = "Common crate for Nym's ecash/zknym cosmwasm contract"
repository.workspace = true
homepage.workspace = true
documentation.workspace = true
rust-version = "1.85"
readme.workspace = true
publish = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@@ -6,14 +6,6 @@ use cosmwasm_std::Coin;
#[cw_serde]
pub struct PoolCounters {
/// Represents the total amount of funds deposited into the contract.
pub total_deposited: Coin,
/// Represents the total amount of funds redeemed from the contract that got transferred into the holding account.
pub total_redeemed: Coin,
/// Represents the total amount of tickets requested to be redeemed from the contract and get moved into the holding account,
/// after that functionality got disabled.
#[serde(default)]
pub tickets_requested_and_not_redeemed: u64,
}
@@ -1,38 +0,0 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use cosmwasm_schema::cw_serde;
use cosmwasm_std::Coin;
use std::collections::HashMap;
/// Aggregate statistics about all deposits made through the ecash contract.
#[cw_serde]
pub struct DepositsStatistics {
/// Total number of deposits ever made (at any price tier),
/// derived from the deposit id counter.
pub total_deposits_made: u32,
/// Total value of all deposits ever made (at any price tier),
/// sourced from `PoolCounters::total_deposited`.
pub total_deposited: Coin,
/// Number of deposits made at the default (non-reduced) price.
pub total_deposits_made_with_default_price: u32,
/// Total value deposited at the default price.
pub total_deposited_with_default_price: Coin,
/// Number of deposits made at any custom (reduced) price, summed across all whitelisted accounts.
pub total_deposits_made_with_custom_price: u32,
/// Total value deposited at custom prices, summed across all whitelisted accounts.
pub total_deposited_with_custom_price: Coin,
/// Per-account breakdown of deposit counts for whitelisted addresses.
// note: we use String for addressing due to serialisation incompatibility
pub deposits_made_with_custom_price: HashMap<String, u32>,
/// Per-account breakdown of deposited amounts for whitelisted addresses.
// note: we use String for addressing due to serialisation incompatibility
pub deposited_with_custom_price: HashMap<String, Coin>,
}
@@ -65,26 +65,4 @@ pub enum EcashContractError {
#[error("the account blacklisting hasn't been fully implemented yet")]
UnimplementedBlacklisting,
#[error("reduced deposit must use the same denom as the default deposit (expected '{expected}', got '{got}')")]
InvalidReducedDepositDenom { expected: String, got: String },
#[error(
"reduced deposit amount ({reduced}) must be strictly less than the default ({default})"
)]
ReducedDepositNotReduced {
reduced: cosmwasm_std::Uint128,
default: cosmwasm_std::Uint128,
},
#[error("address '{address}' does not have a custom reduced deposit price set")]
NoReducedDepositPrice { address: String },
#[error(
"deposit amount ({amount}) must be at least the ticket book size ({ticket_book_size})"
)]
DepositBelowTicketBookSize {
amount: cosmwasm_std::Uint128,
ticket_book_size: u64,
},
}
@@ -4,12 +4,10 @@
pub mod blacklist;
pub mod counters;
pub mod deposit;
pub mod deposit_statistics;
pub mod error;
pub mod event_attributes;
pub mod events;
pub mod msg;
pub mod redeem_credential;
pub mod reduced_deposit;
pub use error::EcashContractError;
@@ -9,10 +9,6 @@ use crate::blacklist::{BlacklistedAccountResponse, PagedBlacklistedAccountRespon
#[cfg(feature = "schema")]
use crate::deposit::{DepositResponse, LatestDepositResponse, PagedDepositsResponse};
#[cfg(feature = "schema")]
use crate::deposit_statistics::DepositsStatistics;
#[cfg(feature = "schema")]
use crate::reduced_deposit::WhitelistedAccountsResponse;
#[cfg(feature = "schema")]
use cosmwasm_schema::QueryResponses;
#[cw_serde]
@@ -46,25 +42,10 @@ pub enum ExecuteMsg {
admin: String,
},
#[serde(alias = "update_deposit_value")]
UpdateDefaultDepositValue {
UpdateDepositValue {
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,
},
/// 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,
},
// TODO: properly implement
ProposeToBlacklist {
public_key: String,
@@ -87,15 +68,7 @@ pub enum QueryMsg {
},
#[cfg_attr(feature = "schema", returns(Coin))]
#[serde(alias = "get_required_deposit_amount")]
#[serde(alias = "GetRequiredDepositAmount")]
GetDefaultDepositAmount {},
#[cfg_attr(feature = "schema", returns(Option<Coin>))]
GetReducedDepositAmount { address: String },
#[cfg_attr(feature = "schema", returns(WhitelistedAccountsResponse))]
GetAllWhitelistedAccounts {},
GetRequiredDepositAmount {},
#[cfg_attr(feature = "schema", returns(DepositResponse))]
GetDeposit { deposit_id: u32 },
@@ -108,22 +81,7 @@ pub enum QueryMsg {
limit: Option<u32>,
start_after: Option<u32>,
},
#[cfg_attr(feature = "schema", returns(DepositsStatistics))]
GetDepositsStatistics {},
}
#[cw_serde]
pub struct MigrateMsg {
/// Initial set of whitelisted accounts with their reduced deposit prices.
/// Each entry is validated and stored during migration.
pub initial_whitelist: Vec<WhitelistedDeposit>,
}
/// An address and its reduced deposit price, used when seeding the whitelist
/// via migration.
#[cw_serde]
pub struct WhitelistedDeposit {
pub address: String,
pub deposit: Coin,
}
pub struct MigrateMsg {}
@@ -1,16 +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, Coin};
#[cw_serde]
pub struct WhitelistedAccount {
pub address: Addr,
pub deposit: Coin,
}
#[cw_serde]
pub struct WhitelistedAccountsResponse {
pub whitelisted_accounts: Vec<WhitelistedAccount>,
}
@@ -1,16 +1,12 @@
[package]
name = "nym-group-contract-common"
description = "Common crate for Nym's group cosmwasm contract"
version.workspace = true
authors.workspace = true
edition = "2021"
license.workspace = true
description = "Common crate for Nym's group cosmwasm contract"
repository.workspace = true
homepage.workspace = true
documentation.workspace = true
rust-version = "1.85"
readme.workspace = true
publish = true
[dependencies]
cosmwasm-schema = { workspace = true }
@@ -1,16 +1,12 @@
[package]
name = "nym-mixnet-contract-common"
description = "Common library for the Nym mixnet contract"
version.workspace = true
authors = { workspace = true }
description = "Common library for the Nym mixnet contract"
rust-version = "1.85"
edition = { workspace = true }
authors = { workspace = true }
license = { workspace = true }
repository = { workspace = true }
homepage.workspace = true
documentation.workspace = true
rust-version = "1.85"
readme.workspace = true
publish = true
[dependencies]
bs58 = { workspace = true }
@@ -1,16 +1,10 @@
[package]
name = "nym-multisig-contract-common"
description = "Common code for the Nym multisig CosmWasm smart contract"
version.workspace = true
authors.workspace = true
edition = "2021"
license.workspace = true
repository.workspace = true
description = "Common code for the Nym multisig CosmWasm smart contract"
homepage.workspace = true
documentation.workspace = true
rust-version = "1.85"
readme.workspace = true
publish = true
[dependencies]
cosmwasm-schema = { workspace = true }
@@ -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>,
}
@@ -1,16 +1,15 @@
[package]
name = "nym-performance-contract-common"
description = "Common crate for Nym's group performance contract"
version.workspace = true
authors.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
homepage.workspace = true
documentation.workspace = true
rust-version = "1.85"
edition.workspace = true
license.workspace = true
rust-version.workspace = true
readme.workspace = true
publish = true
description = "Common crate for Nym's group performance contract"
[dependencies]
thiserror = { workspace = true }
@@ -1,16 +1,15 @@
[package]
name = "nym-pool-contract-common"
description = "Common library for the Nym Pool contract"
version.workspace = true
description = "Common library for the Nym Pool contract"
authors.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
homepage.workspace = true
documentation.workspace = true
rust-version = "1.85"
edition.workspace = true
license.workspace = true
rust-version.workspace = true
readme.workspace = true
publish = true
[dependencies]
thiserror = { workspace = true }
@@ -1,16 +1,11 @@
[package]
name = "nym-vesting-contract-common"
description = "Common library for the Nym vesting contract"
version.workspace = true
authors = { workspace = true }
description = "Common library for the Nym vesting contract"
edition = { workspace = true }
authors = { workspace = true }
license = { workspace = true }
repository = { workspace = true }
homepage.workspace = true
documentation.workspace = true
rust-version = "1.85"
readme.workspace = true
publish = true
[dependencies]
cosmwasm-std = { workspace = true }
+3 -4
View File
@@ -1,16 +1,15 @@
[package]
name = "nym-credential-proxy-lib"
description = "Build script and core functionality of the Nym Credential Proxy"
version.workspace = true
authors.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
homepage.workspace = true
documentation.workspace = true
edition.workspace = true
license.workspace = true
rust-version.workspace = true
readme.workspace = true
publish = true
description = "Build script and core functionality of the Nym Credential Proxy"
[dependencies]
anyhow = { workspace = true }
@@ -13,9 +13,8 @@ use std::sync::Arc;
use std::time::Duration;
use time::OffsetDateTime;
use tokio::sync::Mutex as AsyncMutex;
use tokio::time::Instant;
use tokio_util::sync::CancellationToken;
use tracing::{debug, error, info, instrument, warn};
use tracing::{debug, info, instrument, warn};
use uuid::Uuid;
pub use helpers::{BufferedDeposit, PerformedDeposits, make_deposits_request, split_deposits};
@@ -147,14 +146,9 @@ impl DepositsBuffer {
// if we're here, we know we're below the threshold
fn maybe_refill_deposits(&self) {
if let Some((mut guard, completion_guard)) =
self.inner.deposits_refill_task.try_get_new_task_guard()
{
if let Some(mut guard) = self.inner.deposits_refill_task.try_get_new_task_guard() {
let this = self.clone();
*guard = Some(tokio::spawn(async move {
let _completion_guard = completion_guard;
this.refill_deposits().await
}));
*guard = Some(tokio::spawn(async move { this.refill_deposits().await }));
}
}
@@ -185,8 +179,6 @@ impl DepositsBuffer {
requested_on: OffsetDateTime,
client_pubkey: PublicKeyUser,
) -> Result<BufferedDeposit, CredentialProxyError> {
let wait_start = Instant::now();
let mut i = 0;
loop {
tokio::time::sleep(Duration::from_millis(500)).await;
if let Some(buffered_deposit) = self.inner.unused_deposits.lock().await.pop() {
@@ -203,15 +195,6 @@ impl DepositsBuffer {
// make sure there's always a task working in the background in case deposits get used up too quickly
self.maybe_refill_deposits()
}
i += 1;
let elapsed = wait_start.elapsed();
if elapsed > Duration::from_secs(5) && i % 10 == 0 {
warn!("we've been waiting for over 5s to make a deposit - something is wrong!")
} else if elapsed > Duration::from_secs(10) && i % 5 == 0 {
error!(
"we've been waiting for over 10s to make a deposit - something is SERIOUSLY wrong!"
)
}
}
}
@@ -3,22 +3,12 @@
use crate::error::CredentialProxyError;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex as StdMutex, MutexGuard};
use std::sync::{Mutex as StdMutex, MutexGuard};
use tokio::task::JoinHandle;
use tracing::{debug, error};
pub(super) type RefillTaskResult = Result<(), CredentialProxyError>;
pub(super) struct InProgressGuard {
in_progress: Arc<AtomicBool>,
}
impl Drop for InProgressGuard {
fn drop(&mut self) {
self.in_progress.store(false, Ordering::SeqCst);
}
}
#[derive(Default)]
pub(super) struct RefillTask {
// note that we can only have a single transaction in progress (or it'd mess up with our sequence numbers)
@@ -26,7 +16,7 @@ pub(super) struct RefillTask {
// we'll have to increase the number of deposits per transaction
join_handle: StdMutex<Option<JoinHandle<RefillTaskResult>>>,
in_progress: Arc<AtomicBool>,
in_progress: AtomicBool,
}
impl RefillTask {
@@ -38,15 +28,9 @@ impl RefillTask {
.is_ok()
}
/// Returns `None` if a refill is already in progress. On success, returns the
/// join-handle guard (to store the new `JoinHandle` into) and an [`InProgressGuard`]
/// that **must be moved into the spawned task** — it resets the flag when dropped.
pub(super) fn try_get_new_task_guard(
&self,
) -> Option<(
MutexGuard<'_, Option<JoinHandle<RefillTaskResult>>>,
InProgressGuard,
)> {
) -> Option<MutexGuard<'_, Option<JoinHandle<RefillTaskResult>>>> {
// sanity check for concurrent request
if !self.try_set_in_progress() {
debug!("another task has already started deposit refill request");
@@ -64,11 +48,7 @@ impl RefillTask {
}
}
let completion_guard = InProgressGuard {
in_progress: Arc::clone(&self.in_progress),
};
Some((guard, completion_guard))
Some(guard)
}
pub(super) fn take_task_join_handle(&self) -> Option<JoinHandle<RefillTaskResult>> {
@@ -76,34 +56,3 @@ impl RefillTask {
self.join_handle.lock().expect("mutex got poisoned").take()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn in_progress_resets_after_guard_drop() {
let task = RefillTask::default();
let (guard, completion_guard) = task.try_get_new_task_guard().unwrap();
drop(guard);
assert!(task.try_get_new_task_guard().is_none());
drop(completion_guard);
assert!(task.try_get_new_task_guard().is_some());
}
#[test]
fn in_progress_resets_on_panic() {
let task = RefillTask::default();
let (guard, completion_guard) = task.try_get_new_task_guard().unwrap();
drop(guard);
let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let _g = completion_guard;
panic!("simulated refill task panic");
}));
assert!(task.try_get_new_task_guard().is_some());
}
}
+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}"),
}
}
@@ -6,7 +6,7 @@ use crate::helpers::LockTimer;
use nym_ecash_contract_common::msg::ExecuteMsg;
use nym_validator_client::nyxd::contract_traits::NymContractsProvider;
use nym_validator_client::nyxd::cosmwasm_client::types::ExecuteResult;
use nym_validator_client::nyxd::{AccountId, Coin, Config, CosmWasmClient, NyxdClient};
use nym_validator_client::nyxd::{Coin, Config, CosmWasmClient, NyxdClient};
use nym_validator_client::{DirectSigningHttpRpcNyxdClient, nyxd};
use std::ops::Deref;
use std::sync::Arc;
@@ -50,10 +50,6 @@ impl ChainClient {
Ok(ChainClient(Arc::new(RwLock::new(client))))
}
pub async fn address(&self) -> AccountId {
self.0.read().await.address()
}
pub async fn query_chain(&self) -> ChainReadPermit<'_> {
let _acquire_timer = LockTimer::new("acquire chain query permit");
self.0.read().await
@@ -8,7 +8,6 @@ use nym_validator_client::nyxd::contract_traits::EcashQueryClient;
use std::sync::Arc;
use time::OffsetDateTime;
use tokio::sync::RwLock;
use tracing::{info, warn};
pub struct CachedDeposit {
valid_until: OffsetDateTime,
@@ -57,29 +56,13 @@ impl RequiredDepositCache {
// update cache
drop(read_guard);
let address = chain_client.address().await;
info!("checking deposit required by {address}");
let mut write_guard = self.inner.write().await;
let read_permit = chain_client.query_chain().await;
let reduced = read_permit
.get_reduced_deposit_amount(address.to_string())
let deposit_amount = chain_client
.query_chain()
.await
.get_required_deposit_amount()
.await?;
let deposit_amount = match reduced {
Some(reduced) => {
info!("we're permitted to use reduced price");
reduced
}
None => {
warn!(
"using default deposit value {address} is not whitelisted for price reduction"
);
read_permit.get_default_deposit_amount().await?
}
};
let nym_coin: Coin = deposit_amount.into();
write_guard.update(nym_coin.clone());
+2 -5
View File
@@ -1,16 +1,13 @@
[package]
name = "nym-credential-storage"
description = "Crate for handling and storing spent and unspent zknym ticketbooks"
version.workspace = true
authors.workspace = true
edition = "2021"
license.workspace = true
rust-version.workspace = true
description = "Crate for handling and storing spent and unspent zknym ticketbooks"
repository.workspace = true
homepage.workspace = true
documentation.workspace = true
rust-version.workspace = true
readme.workspace = true
publish = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+1 -5
View File
@@ -1,16 +1,12 @@
[package]
name = "nym-credential-utils"
description = "Utils crate for dealing with zknym credentials"
version.workspace = true
authors.workspace = true
edition = "2021"
license.workspace = true
description = "Utils crate for dealing with zknym credentials"
repository.workspace = true
homepage.workspace = true
documentation.workspace = true
rust-version.workspace = true
readme.workspace = true
publish = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
+3 -4
View File
@@ -1,16 +1,15 @@
[package]
name = "nym-credential-verification"
description = "Store and verify zknym credentials"
version.workspace = true
authors.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
homepage.workspace = true
documentation.workspace = true
edition.workspace = true
license.workspace = true
rust-version.workspace = true
readme.workspace = true
publish = true
description = "Store and verify zknym credentials"
[dependencies]
async-trait = { workspace = true }
@@ -3,19 +3,25 @@
use crate::Error;
use crate::ecash::error::EcashTicketError;
use crate::ecash::helpers::for_each_api_concurrent;
use crate::ecash::state::SharedState;
use cosmwasm_std::Fraction;
use cw_utils::ThresholdResponse;
use futures::channel::mpsc::UnboundedReceiver;
use futures::{Stream, StreamExt};
use nym_api_requests::ecash::models::VerifyEcashTicketBody;
use nym_api_requests::constants::MIN_BATCH_REDEMPTION_DELAY;
use nym_api_requests::ecash::models::{BatchRedeemTicketsBody, VerifyEcashTicketBody};
use nym_credentials_interface::Bandwidth;
use nym_credentials_interface::{ClientTicket, TicketType};
use nym_validator_client::EcashApiClient;
use nym_validator_client::coconut::EcashApiError;
use nym_validator_client::nym_api::NymApiClientExt;
use nym_validator_client::nym_api::{EpochId, NymApiClientExt};
use nym_validator_client::nyxd::AccountId;
use nym_validator_client::nyxd::contract_traits::MultisigQueryClient;
use nym_validator_client::nyxd::contract_traits::{
EcashSigningClient, MultisigQueryClient, MultisigSigningClient, PagedMultisigQueryClient,
};
use nym_validator_client::nyxd::cosmwasm_client::ContractResponseData;
use nym_validator_client::nyxd::cw3::Status;
use si_scale::helpers::bibytes2;
use std::collections::{HashMap, HashSet};
use std::ops::Deref;
@@ -25,6 +31,22 @@ use tokio::sync::{Mutex, RwLockReadGuard};
use tokio::time::{Duration, Instant, interval_at};
use tracing::{debug, error, info, instrument, trace, warn};
enum ProposalResult {
Executed,
Rejected,
Pending,
}
impl ProposalResult {
fn is_pending(&self) -> bool {
matches!(self, ProposalResult::Pending)
}
fn is_rejected(&self) -> bool {
matches!(self, ProposalResult::Rejected)
}
}
struct PendingVerification {
ticket: ClientTicket,
@@ -46,6 +68,43 @@ impl PendingVerification {
}
}
struct PendingRedemptionVote {
proposal_id: u64,
digest: Vec<u8>,
included_serial_numbers: Vec<Vec<u8>>,
epoch_id: EpochId,
// vec of node ids of apis that haven't sent a valid response
pending: Vec<u64>,
}
impl PendingRedemptionVote {
fn new(
proposal_id: u64,
digest: Vec<u8>,
included_serial_numbers: Vec<Vec<u8>>,
epoch_id: EpochId,
pending: Vec<u64>,
) -> Self {
PendingRedemptionVote {
proposal_id,
digest,
included_serial_numbers,
epoch_id,
pending,
}
}
fn to_request_body(&self, gateway_cosmos_addr: AccountId) -> BatchRedeemTicketsBody {
BatchRedeemTicketsBody::new(
self.digest.clone(),
self.proposal_id,
self.included_serial_numbers.clone(),
gateway_cosmos_addr,
)
}
}
pub struct CredentialHandlerConfig {
/// Specifies the multiplier for revoking a malformed/double-spent ticket
/// (if it has to go all the way to the nym-api for verification)
@@ -73,6 +132,7 @@ pub struct CredentialHandler {
ticket_receiver: UnboundedReceiver<ClientTicket>,
shared_state: SharedState,
pending_tickets: Vec<PendingVerification>,
pending_redemptions: Vec<PendingRedemptionVote>,
}
impl CredentialHandler {
@@ -124,6 +184,75 @@ impl CredentialHandler {
Ok(pending)
}
async fn rebuild_pending_votes(
shared_state: &SharedState,
) -> Result<Vec<PendingRedemptionVote>, EcashTicketError> {
// 1. get all tickets that were not fully verified
let unverified = shared_state.storage.get_all_unresolved_proposals().await?;
let mut pending = Vec::with_capacity(unverified.len());
let epoch_id = shared_state.current_epoch_id().await?;
let apis = shared_state
.api_clients(epoch_id)
.await?
.iter()
.map(|s| (s.cosmos_address.to_string(), s.node_id))
.collect::<Vec<_>>();
for proposal_id in unverified {
// get all of the votes
let votes = shared_state
.start_query()
.await
.get_all_votes(proposal_id as u64)
.await
.map_err(EcashTicketError::chain_query_failure)?
.into_iter()
.map(|v| v.voter)
.collect::<HashSet<_>>();
let mut missing_votes = Vec::new();
// see who hasn't voted
for (api_address, api_id) in &apis {
// for each signer, check if they have actually voted; if not, that's the missing guy
if !votes.contains(api_address) {
missing_votes.push(*api_id)
}
}
// attempt to rebuild SN and digest from the proposal info + storage data
let proposal_info = shared_state
.start_query()
.await
.query_proposal(proposal_id as u64)
.await
.map_err(EcashTicketError::chain_query_failure)?;
let tickets = shared_state
.storage
.get_all_proposed_tickets_with_sn(proposal_id as u32)
.await?;
let digest =
BatchRedeemTicketsBody::make_digest(tickets.iter().map(|t| &t.serial_number));
let encoded_digest = bs58::encode(&digest).into_string();
if encoded_digest != proposal_info.description {
error!("the lost proposal {proposal_id} does not have a matching digest!");
continue;
}
pending.push(PendingRedemptionVote {
proposal_id: proposal_id as u64,
digest,
included_serial_numbers: tickets.into_iter().map(|t| t.serial_number).collect(),
epoch_id,
pending: missing_votes,
})
}
Ok(pending)
}
pub(crate) async fn new(
config: CredentialHandlerConfig,
ticket_receiver: UnboundedReceiver<ClientTicket>,
@@ -147,15 +276,51 @@ impl CredentialHandler {
// on startup read pending credentials and api responses from the storage
let pending_tickets = Self::rebuild_pending_tickets(&shared_state).await?;
// on startup read pending proposals from the storage
// then reconstruct the votes by querying the multisig contract for votes on those proposals
// digest from the description and count from the message
let pending_redemptions = Self::rebuild_pending_votes(&shared_state).await?;
Ok(CredentialHandler {
config,
multisig_threshold,
ticket_receiver,
shared_state,
pending_tickets,
pending_redemptions,
})
}
// the argument is temporary as we'll be reading from the storage
async fn create_redemption_proposal(
&self,
commitment: &[u8],
number_of_tickets: u16,
) -> Result<u64, EcashTicketError> {
let res = self
.shared_state
.start_tx()
.await
.request_ticket_redemption(
bs58::encode(commitment).into_string(),
number_of_tickets,
None,
)
.await
.map_err(|source| EcashTicketError::RedemptionProposalCreationFailure { source })?;
// that one is quite tricky because proposal exists on chain, but we didn't get the id...
// but it should be quite impossible to ever reach this unless we make breaking changes
let proposal_id = res
.parse_singleton_u64_contract_data()
.inspect_err(|err| error!("reached seemingly impossible error! could not recover the redemption proposal id: {err}"))
.map_err(|source| EcashTicketError::ProposalIdParsingFailure { source })?;
info!("created redemption proposal {proposal_id} to redeem {number_of_tickets} tickets");
Ok(proposal_id)
}
/// Attempt to send ticket verification request to the provided ecash verifier.
async fn verify_ticket(
&self,
@@ -357,7 +522,42 @@ impl CredentialHandler {
async fn resolve_pending(&mut self) -> Result<(), EcashTicketError> {
let mut still_failing = Vec::new();
// 1. attempt to verify the remaining tickets
// 1. attempt to resolve all pending proposals
while let Some(mut pending) = self.pending_redemptions.pop() {
match self.try_resolve_pending_proposal(&mut pending, None).await {
Ok(resolution) => {
if resolution.is_pending() {
warn!(
"still failed to reach quorum for proposal {}. apis: {:?} haven't responded. we'll retry later",
pending.proposal_id, pending.pending
);
still_failing.push(pending);
} else {
self.shared_state
.storage
.clear_post_proposal_data(
pending.proposal_id as u32,
OffsetDateTime::now_utc(),
resolution.is_rejected(),
)
.await?;
}
}
Err(err) => {
error!(
"experienced internal error when attempting to resolve pending proposal: {err}"
);
// make sure to update internal state to not lose any data
self.pending_redemptions.push(pending);
self.pending_redemptions.append(&mut still_failing);
return Err(err);
}
}
}
let mut still_failing = Vec::new();
// 2. attempt to verify the remaining tickets
while let Some(mut pending) = self.pending_tickets.pop() {
// possible optimisation: if there's a lot of pending tickets, pre-emptively grab locks for api_clients
match self
@@ -395,14 +595,362 @@ impl CredentialHandler {
Ok(())
}
async fn periodic_operations(&mut self) -> Result<(), EcashTicketError> {
trace!(
"attempting to resolve all pending operations -> tickets that are waiting for verification"
/// Attempt to send batch redemption request to the provided ecash verifier.
async fn redeem_tickets(
&self,
proposal_id: u64,
request: &BatchRedeemTicketsBody,
client: &EcashApiClient,
) -> Result<bool, EcashTicketError> {
match client.api_client.batch_redeem_ecash_tickets(request).await {
Ok(res) => {
let accepted = if res.proposal_accepted {
trace!("{client} has accepted proposal {proposal_id}");
true
} else {
warn!("{client} has rejected proposal {proposal_id}");
false
};
Ok(accepted)
}
Err(err) => {
error!(
"failed to send proposal {proposal_id} for redemption vote to ecash signer '{client}': {err}. if we don't reach quorum, we'll retry later"
);
Ok(false)
}
}
}
async fn try_execute_proposal(&self, proposal_id: u64) -> Result<(), EcashTicketError> {
self.shared_state
.start_tx()
.await
.execute_proposal(proposal_id, None)
.await
.map_err(
|source| EcashTicketError::RedemptionProposalExecutionFailure {
proposal_id,
source,
},
)?;
Ok(())
}
async fn get_proposal_status(&self, proposal_id: u64) -> Result<Status, EcashTicketError> {
Ok(self
.shared_state
.start_query()
.await
.query_proposal(proposal_id)
.await
.map_err(EcashTicketError::chain_query_failure)?
.status)
}
async fn try_finalize_proposal(
&self,
proposal_id: u64,
) -> Result<ProposalResult, EcashTicketError> {
match self.get_proposal_status(proposal_id).await? {
Status::Pending => {
// the voting hasn't even begun!
error!("impossible case! the proposal {proposal_id} is still pending");
Ok(ProposalResult::Pending)
}
Status::Open => {
debug!("proposal {proposal_id} is still open and needs more votes");
Ok(ProposalResult::Pending)
}
Status::Rejected => {
warn!("proposal {proposal_id} has been rejected");
Ok(ProposalResult::Rejected)
}
Status::Passed => {
info!(
"proposal {proposal_id} has already been passed - we just need to execute it"
);
self.try_execute_proposal(proposal_id).await?;
info!("executed proposal {proposal_id}");
Ok(ProposalResult::Executed)
}
Status::Executed => {
info!("proposal {proposal_id} has already been executed - nothing to do!");
Ok(ProposalResult::Executed)
}
}
}
async fn try_resolve_pending_proposal(
&self,
pending: &mut PendingRedemptionVote,
api_clients: Option<RwLockReadGuard<'_, Vec<EcashApiClient>>>,
) -> Result<ProposalResult, EcashTicketError> {
let proposal_id = pending.proposal_id;
info!(
"attempting to resolve pending redemption proposal {proposal_id} to redeem {} tickets",
pending.included_serial_numbers.len()
);
// retry the pending verification requests that have failed before
// check if the proposal still needs more votes from the apis
let result = self.try_finalize_proposal(proposal_id).await?;
if !result.is_pending() {
return Ok(result);
}
let api_clients = match api_clients {
Some(clients) => clients,
None => self.shared_state.api_clients(pending.epoch_id).await?,
};
let redemption_request = pending.to_request_body(self.shared_state.address.clone());
// TODO: optimisation: tell other apis they can purge our tickets even if they haven't voted
let total = api_clients.len();
let api_failures = Mutex::new(Vec::new());
let rejected = AtomicUsize::new(0);
for_each_api_concurrent(&api_clients, &pending.pending, |ecash_client| async {
// errors are only returned on hard, storage, failures
match self
.redeem_tickets(pending.proposal_id, &redemption_request, ecash_client)
.await
{
Err(err) => {
error!("internal failure. could not proceed with ticket redemption: {err}");
api_failures.lock().await.push(ecash_client.node_id);
}
Ok(false) => {
rejected.fetch_add(1, Ordering::SeqCst);
}
_ => {}
}
})
.await;
let api_failures = api_failures.into_inner();
let num_failures = api_failures.len();
pending.pending = api_failures;
let rejected = rejected.into_inner();
let rejected_ratio = rejected as f32 / total as f32;
let rejected_perc = rejected_ratio * 100.;
if rejected_ratio >= (1. - self.multisig_threshold) {
error!(
"{rejected_perc:.2}% of signers rejected proposal {proposal_id}. we won't be able to execute it"
);
// no need to query the chain as with so many rejections it's impossible it has passed.
return Ok(ProposalResult::Rejected);
}
let accepted_ratio = (total - rejected - num_failures) as f32 / total as f32;
let accepted_perc = accepted_ratio * 100.;
match accepted_ratio {
n if n < self.multisig_threshold => {
error!(
"less than 2/3 of signers ({accepted_perc:.2}%) accepted proposal {proposal_id}. we're not yet be able to execute it to get funds out"
);
return Ok(ProposalResult::Pending);
}
n if n < self.config.minimum_api_quorum => {
warn!(
"the system seems to be a bit unstable: less than 80%, but more than 67% of signers ({accepted_perc:.2}%) accepted proposal {proposal_id}"
);
}
_ => {
trace!("{accepted_perc:.2}% of signers accepted proposal {proposal_id}");
}
}
// attempt to execute the proposal if it reached the required threshold
self.try_finalize_proposal(proposal_id).await
}
async fn maybe_redeem_tickets(&mut self) -> Result<(), EcashTicketError> {
if !self.pending_tickets.is_empty() {
return Err(EcashTicketError::PendingTickets);
}
let latest_stored = self.shared_state.storage.latest_proposal().await?;
// check if we have already created the proposal but crashed before persisting it in the db
//
// if we have some persisted proposals in storage, try to see if there's anything more recent on chain
// (i.e. the missing proposal)
// if not (i.e. this would have been our first) check the latest page of proposals.
// while this is not ideal, realistically speaking we probably crashed few minutes ago
// and worst case scenario we'll just recreate the proposal instead
//
// LIMITATION: if MULTIPLE proposals got created in between, well. though luck.
let latest_on_chain = if let Some(latest_stored) = &latest_stored {
// those are sorted in ASCENDING way
self.shared_state
.proposals_since(latest_stored.proposal_id as u64)
.await?
.pop()
} else {
// but those are DESCENDING
self.shared_state
.last_proposal_page()
.await?
.first()
.cloned()
};
let now = OffsetDateTime::now_utc();
let prior_proposal = match (&latest_stored, latest_on_chain) {
(None, None) => {
// we haven't created any proposals before
trace!("this could be our first redemption proposal");
None
}
(Some(stored), None) => {
if stored.created_at + MIN_BATCH_REDEMPTION_DELAY > now {
trace!("too soon to create new redemption proposal");
return Ok(());
}
None
}
(_, Some(on_chain)) => {
warn!(
"we seem to have crashed after creating proposal, but before persisting it onto disk!"
);
Some(on_chain)
}
};
// technically we could have been just caching all of those serial numbers as we verify tickets,
// but given how infrequently we call this, there's no point in wasting this memory
let verified_tickets = self
.shared_state
.storage
.get_all_verified_tickets_with_sn()
.await?;
// TODO: somehow simplify that nasty nested if
if verified_tickets.len() < self.config.minimum_redemption_tickets {
// bypass the number of tickets check if we're about to lose our rewards due to expiration
if let Some(latest_stored) = latest_stored {
if latest_stored.created_at + self.config.maximum_time_between_redemption < now {
{}
} else {
debug!(
"we only have {} verified tickets. there's no point in creating a redemption request yet. (we need at least {} (configurable))",
verified_tickets.len(),
self.config.minimum_redemption_tickets
);
return Ok(());
}
} else {
// first proposal
debug!(
"we only have {} verified tickets. there's no point in creating a redemption request yet. (we need at least {} (configurable))",
verified_tickets.len(),
self.config.minimum_redemption_tickets
);
return Ok(());
}
}
// this should have been ensured when querying
assert!(verified_tickets.len() <= u16::MAX as usize);
let digest =
BatchRedeemTicketsBody::make_digest(verified_tickets.iter().map(|t| &t.serial_number));
let encoded_digest = bs58::encode(&digest).into_string();
let prior_proposal_id = if let Some(prior_proposal) = prior_proposal {
if prior_proposal.description == encoded_digest {
info!("we have already created proposal for those tickets");
Some(prior_proposal.id)
} else {
warn!(
"our missed proposal seem to have been for different tickets - abandoning it"
);
None
}
} else {
None
};
// if the proposal has already existed on chain, do use it. otherwise create a new one
let proposal_id = if let Some(prior) = prior_proposal_id {
prior
} else {
self.create_redemption_proposal(&digest, verified_tickets.len() as u16)
.await?
};
if proposal_id > u32::MAX as u64 {
// realistically will we ever reach it? no.
panic!(
"we have created more than {} proposals. we can't handle that.",
u32::MAX
)
}
self.shared_state
.storage
.insert_redemption_proposal(
&verified_tickets,
proposal_id as u32,
OffsetDateTime::now_utc(),
)
.await?;
let current_epoch = self.shared_state.current_epoch_id().await?;
let api_clients = self.shared_state.api_clients(current_epoch).await?;
let ids = api_clients.iter().map(|c| c.node_id).collect();
let mut pending = PendingRedemptionVote::new(
proposal_id,
digest,
verified_tickets
.into_iter()
.map(|t| t.serial_number)
.collect(),
current_epoch,
ids,
);
let resolution = self
.try_resolve_pending_proposal(&mut pending, Some(api_clients))
.await?;
if resolution.is_pending() {
warn!(
"failed to reach quorum for proposal {proposal_id}. apis: {:?} haven't responded. we'll retry later",
pending.pending
);
self.pending_redemptions.push(pending);
} else {
self.shared_state
.storage
.clear_post_proposal_data(
proposal_id as u32,
OffsetDateTime::now_utc(),
resolution.is_rejected(),
)
.await?;
}
Ok(())
}
async fn periodic_operations(&mut self) -> Result<(), EcashTicketError> {
trace!(
"attempting to resolve all pending operations -> tickets that are waiting for verification and possibly redemption"
);
// 1. retry all operations that have failed in the past: verification requests and pending redemption
self.resolve_pending().await?;
// 2. if applicable, attempt to redeem all newly verified tickets
self.maybe_redeem_tickets().await?;
Ok(())
}
@@ -7,7 +7,7 @@ use std::future::Future;
use std::ops::Deref;
use tokio::sync::RwLockReadGuard;
pub fn apis_stream<'a>(
pub(crate) fn apis_stream<'a>(
// if needed we could make this argument more generic to accept either locks or iterators, etc.
all_clients: &'a RwLockReadGuard<'a, Vec<EcashApiClient>>,
filter_by_id: &'a [u64],
@@ -22,7 +22,7 @@ pub fn apis_stream<'a>(
)
}
pub async fn for_each_api_concurrent<'a, F, Fut>(
pub(crate) async fn for_each_api_concurrent<'a, F, Fut>(
all_clients: &'a RwLockReadGuard<'a, Vec<EcashApiClient>>,
filter_by_id: &'a [u64],
f: F,
@@ -20,7 +20,7 @@ use tracing::error;
pub mod credential_sender;
pub mod error;
pub mod helpers;
mod helpers;
mod state;
pub mod traits;
@@ -3,12 +3,17 @@
use crate::Error;
use crate::ecash::error::EcashTicketError;
use cosmwasm_std::{CosmosMsg, WasmMsg, from_json};
use nym_credentials_interface::VerificationKeyAuth;
use nym_ecash_contract_common::msg::ExecuteMsg;
use nym_gateway_storage::traits::BandwidthGatewayStorage;
use nym_validator_client::coconut::all_ecash_api_clients;
use nym_validator_client::nym_api::EpochId;
use nym_validator_client::nyxd::AccountId;
use nym_validator_client::nyxd::contract_traits::{DkgQueryClient, NymContractsProvider};
use nym_validator_client::nyxd::contract_traits::{
DkgQueryClient, MultisigQueryClient, NymContractsProvider,
};
use nym_validator_client::nyxd::cw3::ProposalResponse;
use nym_validator_client::{DirectSigningHttpRpcNyxdClient, EcashApiClient};
use std::collections::BTreeMap;
use std::ops::Deref;
@@ -72,6 +77,53 @@ impl SharedState {
Ok(this)
}
fn created_redemption_proposal(&self, proposal: &ProposalResponse) -> bool {
let Some(msg) = proposal.msgs.first() else {
return false;
};
let CosmosMsg::Wasm(WasmMsg::Execute { msg, .. }) = msg else {
return false;
};
let Ok(ExecuteMsg::RedeemTickets { gw, .. }) = from_json(msg) else {
return false;
};
gw == self.address.as_ref()
}
/// retrieve all redemption proposals made by this gateway since, but excluding, the provided id
pub(crate) async fn proposals_since(
&self,
proposal_id: u64,
) -> Result<Vec<ProposalResponse>, EcashTicketError> {
Ok(self
.start_query()
.await
.list_proposals(Some(proposal_id), None)
.await
.map_err(EcashTicketError::chain_query_failure)?
.proposals
.into_iter()
.filter(|p| self.created_redemption_proposal(p))
.collect())
}
/// retrieve all redemption proposals made by this gateway that are available on the last page of the query
pub(crate) async fn last_proposal_page(
&self,
) -> Result<Vec<ProposalResponse>, EcashTicketError> {
Ok(self
.start_query()
.await
.reverse_proposals(None, None)
.await
.map_err(EcashTicketError::chain_query_failure)?
.proposals
.into_iter()
.filter(|p| self.created_redemption_proposal(p))
.collect())
}
async fn set_epoch_data(
&self,
epoch_id: EpochId,
@@ -188,6 +240,24 @@ impl SharedState {
data.get(&epoch_id).map(|d| &d.master_key).unwrap()
}))
}
pub(crate) async fn start_tx(&self) -> RwLockWriteGuard<'_, DirectSigningHttpRpcNyxdClient> {
self.nyxd_client.write().await
}
pub(crate) async fn start_query(&self) -> RwLockReadGuard<'_, DirectSigningHttpRpcNyxdClient> {
self.nyxd_client.read().await
}
pub(crate) async fn current_epoch_id(&self) -> Result<EpochId, EcashTicketError> {
Ok(self
.start_query()
.await
.get_current_epoch()
.await
.map_err(EcashTicketError::chain_query_failure)?
.epoch_id)
}
}
pub(crate) struct EpochState {
+3 -6
View File
@@ -1,16 +1,13 @@
[package]
name = "nym-credentials-interface"
description = "Interface for Nym's compact eacash / zknym credential scheme"
version.workspace = true
authors.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
homepage.workspace = true
documentation.workspace = true
rust-version.workspace = true
readme.workspace = true
publish = true
edition.workspace = true
license.workspace = true
description = "Interface for Nym's compact eacash / zknym credential scheme"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+1 -13
View File
@@ -1,16 +1,12 @@
[package]
name = "nym-credentials"
description = "Crate for using Nym's zknym credentials"
version.workspace = true
authors.workspace = true
edition = "2021"
license.workspace = true
description = "Crate for using Nym's zknym credentials"
repository.workspace = true
homepage.workspace = true
documentation.workspace = true
rust-version.workspace = true
readme.workspace = true
publish = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@@ -36,13 +32,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 {
+2 -7
View File
@@ -1,16 +1,11 @@
[package]
name = "nym-crypto"
description = "Crypto library for the nym mixnet"
version.workspace = true
authors = { workspace = true }
description = "Crypto library for the nym mixnet"
edition = { workspace = true }
authors = { workspace = true }
license = { workspace = true }
repository = { workspace = true }
homepage.workspace = true
documentation.workspace = true
rust-version.workspace = true
readme.workspace = true
publish = true
[dependencies]
aes-gcm-siv = { workspace = true, optional = true }
-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;

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