Merge pull request #6829 from nymtech/release/2026.10-waterloo

Merge release/2026.10-waterloo
This commit is contained in:
benedetta davico
2026-05-27 16:06:25 +02:00
committed by GitHub
501 changed files with 30030 additions and 12138 deletions
@@ -0,0 +1,41 @@
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,6 +41,9 @@ 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:
@@ -71,3 +74,16 @@ 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
@@ -1,53 +0,0 @@
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.0-dev build-essential curl wget libssl-dev libgtk-3-dev squashfs-tools
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
if: matrix.os == 'ubuntu-22.04'
- name: Install rust toolchain
@@ -26,7 +26,7 @@ jobs:
- name: Node
uses: actions/setup-node@v4
with:
node-version: 21
node-version: 22.13.0
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
@@ -30,7 +30,7 @@ jobs:
- name: Node
uses: actions/setup-node@v4
with:
node-version: 21
node-version: 22.13.0
cache: 'yarn'
- name: Install Rust toolchain
@@ -72,6 +72,41 @@ 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: |
+63 -26
View File
@@ -26,6 +26,9 @@ jobs:
outputs:
release_tag: ${{ github.ref_name }}
env:
SIGN_WINDOWS: ${{ github.event_name == 'release' || inputs.sign }}
steps:
- uses: actions/checkout@v6
@@ -37,48 +40,82 @@ 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: 21
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
- name: Download EV CodeSignTool from ssl.com
working-directory: nym-wallet/src-tauri
if: ${{ inputs.sign }}
if: env.SIGN_WINDOWS == 'true'
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: ${{ inputs.sign }}
if: env.SIGN_WINDOWS == 'true'
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: ${{ inputs.sign }}
if: env.SIGN_WINDOWS == 'true'
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: |
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"
]
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"
]
}
}
}' tauri.conf.json
' tauri.conf.json
- name: Install project dependencies
shell: bash
run: cd .. && yarn --network-timeout 100000
@@ -93,10 +130,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: ${{ 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 }}
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 }}
run: |
echo "Starting build process..."
yarn build
@@ -167,4 +204,4 @@ jobs:
needs: publish-tauri
with:
release_tag: ${{ needs.publish-tauri.outputs.release_tag || github.ref_name }}
secrets: inherit
secrets: inherit
@@ -0,0 +1,61 @@
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
@@ -0,0 +1,57 @@
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
+1
View File
@@ -27,6 +27,7 @@ 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
+46
View File
@@ -4,6 +4,52 @@ 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])
Generated
+304 -51
View File
@@ -57,7 +57,7 @@ checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
dependencies = [
"cfg-if",
"cipher",
"cpufeatures",
"cpufeatures 0.2.17",
]
[[package]]
@@ -253,7 +253,7 @@ checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072"
dependencies = [
"base64ct",
"blake2 0.10.6",
"cpufeatures",
"cpufeatures 0.2.17",
"password-hash",
]
@@ -1309,7 +1309,18 @@ checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818"
dependencies = [
"cfg-if",
"cipher",
"cpufeatures",
"cpufeatures 0.2.17",
]
[[package]]
name = "chacha20"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601"
dependencies = [
"cfg-if",
"cpufeatures 0.3.0",
"rand_core 0.10.1",
]
[[package]]
@@ -1319,7 +1330,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35"
dependencies = [
"aead",
"chacha20",
"chacha20 0.9.1",
"cipher",
"poly1305",
"zeroize",
@@ -1831,6 +1842,15 @@ dependencies = [
"libc",
]
[[package]]
name = "cpufeatures"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
dependencies = [
"libc",
]
[[package]]
name = "crc"
version = "3.3.0"
@@ -2095,7 +2115,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be"
dependencies = [
"cfg-if",
"cpufeatures",
"cpufeatures 0.2.17",
"curve25519-dalek-derive",
"digest 0.10.7",
"fiat-crypto",
@@ -2799,18 +2819,6 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "enum-as-inner"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc"
dependencies = [
"heck 0.5.0",
"proc-macro2",
"quote",
"syn 2.0.106",
]
[[package]]
name = "env_filter"
version = "0.1.3"
@@ -3271,6 +3279,7 @@ dependencies = [
"cfg-if",
"libc",
"r-efi",
"rand_core 0.10.1",
"wasip2",
"wasip3",
]
@@ -3630,26 +3639,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ebdb29d2ea9ed0083cd8cece49bbd968021bd99b0849edb4a9a7ee0fdf6a4e0"
[[package]]
name = "hickory-proto"
version = "0.25.2"
name = "hickory-net"
version = "0.26.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8a6fe56c0038198998a6f217ca4e7ef3a5e51f46163bd6dd60b5c71ca6c6502"
checksum = "e2295ed2f9c31e471e1428a8f88a3f0e1f4b27c15049592138d1eebe9c35b183"
dependencies = [
"async-trait",
"bytes",
"cfg-if",
"data-encoding",
"enum-as-inner",
"futures-channel",
"futures-io",
"futures-util",
"h2 0.4.11",
"hickory-proto",
"http 1.3.1",
"idna",
"ipnet",
"once_cell",
"rand 0.9.2",
"ring",
"jni 0.22.4",
"rand 0.10.1",
"rustls 0.23.37",
"thiserror 2.0.12",
"tinyvec",
@@ -3657,31 +3665,56 @@ dependencies = [
"tokio-rustls 0.26.2",
"tracing",
"url",
"webpki-roots 0.26.11",
"webpki-roots 1.0.2",
]
[[package]]
name = "hickory-proto"
version = "0.26.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bab31817bfb44672a252e97fe81cd0c18d1b2cf892108922f6818820df8c643"
dependencies = [
"data-encoding",
"idna",
"ipnet",
"jni 0.22.4",
"once_cell",
"prefix-trie",
"rand 0.10.1",
"ring",
"thiserror 2.0.12",
"tinyvec",
"tracing",
"url",
]
[[package]]
name = "hickory-resolver"
version = "0.25.2"
version = "0.26.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc62a9a99b0bfb44d2ab95a7208ac952d31060efc16241c87eaf36406fecf87a"
checksum = "f0d58d28879ceecde6607729660c2667a081ccdc082e082675042793960f178c"
dependencies = [
"cfg-if",
"futures-util",
"hickory-net",
"hickory-proto",
"ipconfig",
"ipnet",
"jni 0.22.4",
"moka",
"ndk-context",
"once_cell",
"parking_lot",
"rand 0.9.2",
"rand 0.10.1",
"resolv-conf",
"rustls 0.23.37",
"smallvec",
"system-configuration 0.7.0",
"thiserror 2.0.12",
"tokio",
"tokio-rustls 0.26.2",
"tracing",
"webpki-roots 0.26.11",
"webpki-roots 1.0.2",
]
[[package]]
@@ -4409,6 +4442,9 @@ name = "ipnet"
version = "2.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130"
dependencies = [
"serde",
]
[[package]]
name = "ipnetwork"
@@ -4503,19 +4539,68 @@ dependencies = [
"cesu8",
"cfg-if",
"combine",
"jni-sys",
"jni-sys 0.3.0",
"log",
"thiserror 1.0.69",
"walkdir",
"windows-sys 0.45.0",
]
[[package]]
name = "jni"
version = "0.22.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498"
dependencies = [
"cfg-if",
"combine",
"jni-macros",
"jni-sys 0.4.1",
"log",
"simd_cesu8",
"thiserror 2.0.12",
"walkdir",
"windows-link 0.2.1",
]
[[package]]
name = "jni-macros"
version = "0.22.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3"
dependencies = [
"proc-macro2",
"quote",
"rustc_version 0.4.1",
"simd_cesu8",
"syn 2.0.106",
]
[[package]]
name = "jni-sys"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
[[package]]
name = "jni-sys"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2"
dependencies = [
"jni-sys-macros",
]
[[package]]
name = "jni-sys-macros"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264"
dependencies = [
"quote",
"syn 2.0.106",
]
[[package]]
name = "jobserver"
version = "0.1.33"
@@ -4582,7 +4667,7 @@ version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653"
dependencies = [
"cpufeatures",
"cpufeatures 0.2.17",
]
[[package]]
@@ -5359,6 +5444,12 @@ dependencies = [
"version_check",
]
[[package]]
name = "ndk-context"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b"
[[package]]
name = "netlink-packet-core"
version = "0.8.1"
@@ -5635,7 +5726,7 @@ dependencies = [
[[package]]
name = "nym-api"
version = "1.1.79"
version = "1.1.80"
dependencies = [
"anyhow",
"async-trait",
@@ -5880,7 +5971,7 @@ dependencies = [
[[package]]
name = "nym-cli"
version = "1.1.76"
version = "1.1.77"
dependencies = [
"anyhow",
"base64 0.22.1",
@@ -5963,7 +6054,7 @@ dependencies = [
[[package]]
name = "nym-client"
version = "1.1.76"
version = "1.1.77"
dependencies = [
"bs58",
"clap",
@@ -6273,7 +6364,7 @@ dependencies = [
[[package]]
name = "nym-credential-proxy"
version = "0.3.0"
version = "0.3.2-rc"
dependencies = [
"anyhow",
"axum 0.7.9",
@@ -6926,6 +7017,7 @@ dependencies = [
"bytes",
"cfg-if",
"encoding_rs",
"fastrand",
"hickory-resolver",
"http 1.3.1",
"inventory",
@@ -7326,7 +7418,6 @@ version = "1.20.4"
dependencies = [
"cargo_metadata 0.19.2",
"dotenvy",
"log",
"regex",
"schemars 0.8.22",
"serde",
@@ -7371,9 +7462,103 @@ dependencies = [
"utoipa-swagger-ui",
]
[[package]]
name = "nym-network-monitor-agent"
version = "1.0.2"
dependencies = [
"anyhow",
"arrayref",
"clap",
"futures",
"hkdf",
"humantime",
"lioness",
"nym-bin-common",
"nym-crypto",
"nym-network-monitor-orchestrator-requests",
"nym-noise",
"nym-pemstore",
"nym-sphinx-addressing",
"nym-sphinx-framing",
"nym-sphinx-params",
"nym-sphinx-types",
"nym-task",
"nym-test-utils",
"rand 0.8.5",
"sha2 0.10.9",
"time",
"tokio",
"tokio-util",
"tracing",
"url",
"x25519-dalek",
"zeroize",
]
[[package]]
name = "nym-network-monitor-orchestrator"
version = "1.0.2"
dependencies = [
"anyhow",
"axum 0.7.9",
"clap",
"futures",
"humantime",
"nym-api-requests",
"nym-bin-common",
"nym-crypto",
"nym-http-api-common",
"nym-metrics",
"nym-network-defaults",
"nym-network-monitor-orchestrator-requests",
"nym-node-requests",
"nym-task",
"nym-test-utils",
"nym-validator-client",
"rand 0.8.5",
"sqlx",
"strum 0.28.0",
"thiserror 2.0.12",
"time",
"tokio",
"tracing",
"url",
"utoipa",
"utoipa-swagger-ui",
"utoipauto",
"zeroize",
]
[[package]]
name = "nym-network-monitor-orchestrator-requests"
version = "1.20.4"
dependencies = [
"anyhow",
"humantime-serde",
"nym-crypto",
"nym-http-api-client",
"serde",
"time",
"tracing",
"utoipa",
"zeroize",
]
[[package]]
name = "nym-network-monitors-contract-common"
version = "1.20.4"
dependencies = [
"cosmwasm-schema",
"cosmwasm-std",
"cw-controllers",
"schemars 0.8.22",
"serde",
"thiserror 2.0.12",
]
[[package]]
name = "nym-network-requester"
version = "1.1.77"
version = "1.1.78"
dependencies = [
"addr",
"anyhow",
@@ -7423,7 +7608,7 @@ dependencies = [
[[package]]
name = "nym-node"
version = "1.31.0"
version = "1.32.0"
dependencies = [
"anyhow",
"arc-swap",
@@ -7492,6 +7677,7 @@ dependencies = [
"nym-verloc",
"nym-wireguard",
"nym-wireguard-types",
"nyxd-scraper-shared",
"opentelemetry",
"opentelemetry_sdk",
"rand 0.8.5",
@@ -7542,6 +7728,7 @@ dependencies = [
"nym-exit-policy",
"nym-http-api-client",
"nym-kkt-ciphersuite",
"nym-network-defaults",
"nym-noise-keys",
"nym-test-utils",
"nym-upgrade-mode-check",
@@ -7560,7 +7747,7 @@ dependencies = [
[[package]]
name = "nym-node-status-agent"
version = "2.0.0"
version = "2.0.1-rc3"
dependencies = [
"anyhow",
"clap",
@@ -7581,7 +7768,7 @@ dependencies = [
[[package]]
name = "nym-node-status-api"
version = "4.6.1"
version = "4.6.2-rc7"
dependencies = [
"ammonia",
"anyhow",
@@ -7768,7 +7955,7 @@ name = "nym-outfox"
version = "1.20.4"
dependencies = [
"blake3",
"chacha20",
"chacha20 0.9.1",
"chacha20poly1305",
"fastrand",
"sphinx-packet",
@@ -7975,7 +8162,7 @@ dependencies = [
[[package]]
name = "nym-socks5-client"
version = "1.1.76"
version = "1.1.77"
dependencies = [
"bs58",
"clap",
@@ -8480,6 +8667,7 @@ dependencies = [
"nym-mixnet-contract-common",
"nym-multisig-contract-common",
"nym-network-defaults",
"nym-network-monitors-contract-common",
"nym-performance-contract-common",
"nym-serde-helpers",
"nym-vesting-contract-common",
@@ -8773,7 +8961,7 @@ dependencies = [
[[package]]
name = "nymvisor"
version = "0.1.41"
version = "0.1.42"
dependencies = [
"anyhow",
"bytes",
@@ -9450,7 +9638,7 @@ version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf"
dependencies = [
"cpufeatures",
"cpufeatures 0.2.17",
"opaque-debug 0.3.1",
"universal-hash",
]
@@ -9462,7 +9650,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25"
dependencies = [
"cfg-if",
"cpufeatures",
"cpufeatures 0.2.17",
"opaque-debug 0.3.1",
"universal-hash",
]
@@ -9541,6 +9729,17 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
[[package]]
name = "prefix-trie"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4cf6e3177f0684016a5c209b00882e15f8bdd3f3bb48f0491df10cd102d0c6e7"
dependencies = [
"either",
"ipnet",
"num-traits",
]
[[package]]
name = "pretty_assertions"
version = "1.4.1"
@@ -9838,6 +10037,17 @@ dependencies = [
"rand_core 0.9.3",
]
[[package]]
name = "rand"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207"
dependencies = [
"chacha20 0.10.0",
"getrandom 0.4.1",
"rand_core 0.10.1",
]
[[package]]
name = "rand_chacha"
version = "0.3.1"
@@ -9876,6 +10086,12 @@ dependencies = [
"getrandom 0.3.3",
]
[[package]]
name = "rand_core"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
[[package]]
name = "rand_distr"
version = "0.4.3"
@@ -10005,7 +10221,7 @@ dependencies = [
"serde_json",
"serde_urlencoded",
"sync_wrapper 0.1.2",
"system-configuration",
"system-configuration 0.5.1",
"tokio",
"tokio-rustls 0.24.1",
"tower-service",
@@ -10428,7 +10644,7 @@ checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784"
dependencies = [
"core-foundation 0.10.1",
"core-foundation-sys",
"jni",
"jni 0.21.1",
"log",
"once_cell",
"rustls 0.23.37",
@@ -11015,7 +11231,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
dependencies = [
"cfg-if",
"cpufeatures",
"cpufeatures 0.2.17",
"digest 0.10.7",
]
@@ -11027,7 +11243,7 @@ checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800"
dependencies = [
"block-buffer 0.9.0",
"cfg-if",
"cpufeatures",
"cpufeatures 0.2.17",
"digest 0.9.0",
"opaque-debug 0.3.1",
]
@@ -11039,7 +11255,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
dependencies = [
"cfg-if",
"cpufeatures",
"cpufeatures 0.2.17",
"digest 0.10.7",
]
@@ -11120,6 +11336,22 @@ version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"
[[package]]
name = "simd_cesu8"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33"
dependencies = [
"rustc_version 0.4.1",
"simdutf8",
]
[[package]]
name = "simdutf8"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
[[package]]
name = "siphasher"
version = "0.3.11"
@@ -11721,7 +11953,18 @@ checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7"
dependencies = [
"bitflags 1.3.2",
"core-foundation 0.9.4",
"system-configuration-sys",
"system-configuration-sys 0.5.0",
]
[[package]]
name = "system-configuration"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b"
dependencies = [
"bitflags 2.9.1",
"core-foundation 0.9.4",
"system-configuration-sys 0.6.0",
]
[[package]]
@@ -11734,6 +11977,16 @@ dependencies = [
"libc",
]
[[package]]
name = "system-configuration-sys"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "tagptr"
version = "0.2.0"
+13 -3
View File
@@ -44,6 +44,7 @@ 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",
@@ -176,6 +177,8 @@ 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 = [
@@ -192,6 +195,8 @@ 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"
]
@@ -280,8 +285,8 @@ getrandom03 = { package = "getrandom", version = "=0.3.3" }
glob = "0.3"
handlebars = "3.5.5"
hex = "0.4.3"
hickory-proto = "0.25.2"
hickory-resolver = "0.25.2"
hickory-proto = "0.26.1"
hickory-resolver = "0.26.1"
hkdf = "0.12.3"
hmac = "0.12.1"
http = "1"
@@ -402,6 +407,10 @@ 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"
@@ -411,7 +420,7 @@ 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.8"
libcrux-traits = "0.0.6"
# Workspace dep definitions required by crates.io publication - we need a workspace version since `cargo workspaces` doesn't work with path imports from crate manifests
nym-api-requests = { version = "1.20.4", path = "nym-api/nym-api-requests" }
@@ -507,6 +516,7 @@ 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" }
+1 -1
View File
@@ -1,7 +1,7 @@
[package]
name = "nym-client"
description = "Implementation of the Nym Client"
version = "1.1.76"
version = "1.1.77"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>", "Jędrzej Stuczyński <andrew@nymtech.net>"]
edition = "2021"
license.workspace = true
+1 -1
View File
@@ -1,7 +1,7 @@
[package]
name = "nym-socks5-client"
description = "A SOCKS5 localhost proxy that converts incoming messages to Sphinx and sends them to a Nym address"
version = "1.1.76"
version = "1.1.77"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>"]
edition = "2021"
license.workspace = true
@@ -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_packet(packet)
.forward_client_packet_without_delay(packet)
.map_err(erase_err)
}
}
@@ -34,3 +34,4 @@ 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"] }
+277 -69
View File
@@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
use dashmap::DashMap;
use futures::StreamExt;
use futures::{SinkExt, StreamExt};
use nym_noise::config::NoiseConfig;
use nym_noise::upgrade_noise_initiator;
use nym_sphinx::forwarding::packet::MixPacket;
@@ -14,6 +14,7 @@ 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;
@@ -90,13 +91,17 @@ 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>) -> Self {
fn new(channel: mpsc::Sender<FramedNymPacket>, handle_token: Arc<()>) -> Self {
ConnectionSender {
channel,
current_reconnection_attempt: Arc::new(AtomicU32::new(0)),
handle_token,
}
}
}
@@ -107,6 +112,31 @@ 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 {
@@ -116,6 +146,8 @@ impl ManagedConnection {
message_receiver: mpsc::Receiver<FramedNymPacket>,
connection_timeout: Duration,
current_reconnection: Arc<AtomicU32>,
active_connections: ActiveConnections,
handle_token: Arc<()>,
) -> Self {
ManagedConnection {
address,
@@ -123,72 +155,30 @@ 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);
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;
}
},
// 1. attempt to establish the connection with timeout
let maybe_stream = match tokio::time::timeout(self.connection_timeout, connection_fut).await
{
Ok(stream) => stream,
Err(_) => {
let connect_ms = connect_start.elapsed().as_millis() as u64;
warn!(
debug!(
event = "connection.failed.timeout",
peer = %address,
timeout_ms = self.connection_timeout.as_millis() as u64,
@@ -203,21 +193,133 @@ impl ManagedConnection {
}
};
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}"
);
}
// 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,
exit_reason = "sender_dropped",
"connection manager to {address} finished"
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;
}
}
}
}
}
}
}
@@ -264,13 +366,18 @@ 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);
let new_entry = ConnectionSender::new(sender, Arc::clone(&handle_token));
let current_attempt = Arc::clone(&new_entry.current_reconnection_attempt);
self.active_connections.insert(address, new_entry);
current_attempt
@@ -285,6 +392,7 @@ 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 {
@@ -299,6 +407,8 @@ impl Client {
receiver,
initial_connection_timeout,
current_reconnection_attempt,
active_connections,
handle_token,
)
.run()
.await;
@@ -428,4 +538,102 @@ 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,12 +21,16 @@ impl From<mpsc::UnboundedSender<PacketToForward>> for MixForwardingSender {
}
impl MixForwardingSender {
pub fn forward_packet(&self, packet: impl Into<PacketToForward>) -> Result<(), SendError> {
pub fn forward_packet(&self, packet: PacketToForward) -> Result<(), SendError> {
self.0
.unbounded_send(packet.into())
.unbounded_send(packet)
.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()
@@ -38,35 +42,23 @@ pub type MixForwardingReceiver = mpsc::UnboundedReceiver<PacketToForward>;
pub struct PacketToForward {
pub packet: MixPacket,
pub forward_delay_target: Option<Instant>,
}
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))
}
pub network_monitor_packet: bool,
}
impl PacketToForward {
pub fn new(packet: MixPacket, forward_delay_target: Option<Instant>) -> Self {
pub fn new(
packet: MixPacket,
forward_delay_target: Option<Instant>,
network_monitor_packet: bool,
) -> Self {
PacketToForward {
packet,
forward_delay_target,
network_monitor_packet,
}
}
pub fn new_no_delay(packet: MixPacket) -> Self {
Self::new(packet, None)
pub fn client_packet_without_delay(packet: MixPacket) -> Self {
Self::new(packet, None, false)
}
}
@@ -26,6 +26,7 @@ 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,6 +104,14 @@ 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> {
@@ -114,6 +122,15 @@ 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,11 +15,15 @@ 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::{
AnnotationResponse, ApiHealthResponse, BinaryBuildInformationOwned, ChainBlocksStatusResponse,
ChainStatusResponse, KeyRotationInfoResponse, NodePerformanceResponse, NodeRefreshBody,
NymNodeDescriptionV1, NymNodeDescriptionV2, PerformanceHistoryResponse, RewardedSetResponse,
SignerInformationResponse,
AnnotationResponseV1, ApiHealthResponse, BinaryBuildInformationOwned,
ChainBlocksStatusResponse, ChainStatusResponse, KeyRotationInfoResponse,
NodePerformanceResponse, NodeRefreshBody, NymNodeDescriptionV1, NymNodeDescriptionV2,
PerformanceHistoryResponse, RewardedSetResponse, SignerInformationResponse,
StressTestBatchSubmissionResponse,
};
use nym_api_requests::pagination::PaginatedResponse;
use nym_http_api_client::{ApiClient, NO_PARAMS};
@@ -976,7 +980,7 @@ pub trait NymApiClientExt: ApiClient {
async fn get_node_annotation(
&self,
node_id: NodeId,
) -> Result<AnnotationResponse, NymAPIError> {
) -> Result<AnnotationResponseV1, NymAPIError> {
self.get_json(
&[
routes::V1_API_VERSION,
@@ -1359,6 +1363,53 @@ 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,6 +49,9 @@ pub mod nym_nodes {
pub const NYM_NODES_REWARDED_SET: &str = "rewarded-set";
pub const NYM_NODES_REFRESH_DESCRIBED: &str = "refresh-described";
pub const BY_ADDRESSES: &str = "by-addresses";
pub const STRESS_TESTING: &str = "stress-testing";
pub const STRESS_TESTING_KNOWN_MONITORS: &str = "known-monitors";
pub const STRESS_TESTING_BATCH_SUBMIT: &str = "batch-submit";
}
pub const STATUS_ROUTES: &str = "status";
@@ -13,6 +13,7 @@ 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;
@@ -22,6 +23,7 @@ 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;
@@ -31,6 +33,9 @@ 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};
@@ -40,6 +45,7 @@ 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;
@@ -49,6 +55,7 @@ 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>;
@@ -62,6 +69,7 @@ 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>,
@@ -86,6 +94,10 @@ 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())
@@ -0,0 +1,107 @@
// 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()
}
};
}
}
@@ -0,0 +1,205 @@
// 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)]
#[derive(Debug, Clone)]
pub(crate) struct SigningClientOptions {
gas_price: GasPrice,
simulated_gas_multiplier: f32,
@@ -80,6 +80,17 @@ 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,6 +24,8 @@ 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;
@@ -40,6 +42,7 @@ pub use crate::nyxd::{
fee::Fee,
};
pub use crate::rpc::TendermintRpcClient;
pub use bip39;
pub use coin::Coin;
pub use cosmrs::{
bank::MsgSend,
@@ -70,14 +73,19 @@ 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;
@@ -262,6 +270,16 @@ 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
}
@@ -289,6 +307,10 @@ 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> {
@@ -303,6 +325,12 @@ 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()
@@ -0,0 +1,26 @@
[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
@@ -0,0 +1,8 @@
// 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";
}
@@ -0,0 +1,30 @@
// 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),
}
@@ -0,0 +1,11 @@
// 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::*;
@@ -0,0 +1,78 @@
// 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 {}
@@ -0,0 +1,53 @@
// 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>,
}
@@ -13,8 +13,9 @@ 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, info, instrument, warn};
use tracing::{debug, error, info, instrument, warn};
use uuid::Uuid;
pub use helpers::{BufferedDeposit, PerformedDeposits, make_deposits_request, split_deposits};
@@ -146,9 +147,14 @@ impl DepositsBuffer {
// if we're here, we know we're below the threshold
fn maybe_refill_deposits(&self) {
if let Some(mut guard) = self.inner.deposits_refill_task.try_get_new_task_guard() {
if let Some((mut guard, completion_guard)) =
self.inner.deposits_refill_task.try_get_new_task_guard()
{
let this = self.clone();
*guard = Some(tokio::spawn(async move { this.refill_deposits().await }));
*guard = Some(tokio::spawn(async move {
let _completion_guard = completion_guard;
this.refill_deposits().await
}));
}
}
@@ -179,6 +185,8 @@ 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() {
@@ -195,6 +203,15 @@ 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,12 +3,22 @@
use crate::error::CredentialProxyError;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Mutex as StdMutex, MutexGuard};
use std::sync::{Arc, 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)
@@ -16,7 +26,7 @@ pub(super) struct RefillTask {
// we'll have to increase the number of deposits per transaction
join_handle: StdMutex<Option<JoinHandle<RefillTaskResult>>>,
in_progress: AtomicBool,
in_progress: Arc<AtomicBool>,
}
impl RefillTask {
@@ -28,9 +38,15 @@ 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>>>> {
) -> Option<(
MutexGuard<'_, Option<JoinHandle<RefillTaskResult>>>,
InProgressGuard,
)> {
// sanity check for concurrent request
if !self.try_set_in_progress() {
debug!("another task has already started deposit refill request");
@@ -48,7 +64,11 @@ impl RefillTask {
}
}
Some(guard)
let completion_guard = InProgressGuard {
in_progress: Arc::clone(&self.in_progress),
};
Some((guard, completion_guard))
}
pub(super) fn take_task_join_handle(&self) -> Option<JoinHandle<RefillTaskResult>> {
@@ -56,3 +76,34 @@ 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());
}
}
+24 -1
View File
@@ -57,6 +57,7 @@ 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
@@ -65,6 +66,7 @@ impl QuorumStateChecker {
drop(client_guard);
let res = check_known_dealers(dkg_details).await?;
info!("there are {} known DKG dealers", res.results.len());
let Some(signing_threshold) = res.threshold else {
warn!(
@@ -76,12 +78,33 @@ 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");
}
}
Ok((working_issuer as u64) >= signing_threshold)
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)
}
pub async fn run_forever(self) {
+2
View File
@@ -31,3 +31,5 @@ pub use aes_gcm_siv::{Aes128GcmSiv, Aes256GcmSiv};
pub use blake3;
#[cfg(feature = "stream_cipher")]
pub use ctr;
#[cfg(feature = "hashing")]
pub use sha2;
@@ -6,7 +6,7 @@ use nym_coconut_dkg_common::verification_key::VerificationKeyShare;
use nym_crypto::asymmetric::ed25519;
use std::time::Duration;
use time::OffsetDateTime;
use tracing::{debug, warn};
use tracing::warn;
pub trait Verifiable {
fn verify_signature(&self, pub_key: &ed25519::PublicKey) -> bool;
@@ -36,6 +36,7 @@ pub trait ChainResponse: Verifiable + TimestampedResponse {
// we rely on information provided from the api itself AS LONG AS it's not too outdated
if self.timestamp() + stale_response_threshold < now {
warn!("chain status response is stale");
return false;
}
self.chain_synced()
@@ -96,26 +97,27 @@ pub trait SignerResponse: Verifiable + TimestampedResponse {
// we rely on information provided from the api itself AS LONG AS it's not too outdated
if self.timestamp() + stale_response_threshold < now {
warn!("stale signer response");
return false;
}
if !self.has_signing_keys() {
debug!("missing signing keys");
warn!("missing signing keys");
return false;
}
if self.signer_disabled() {
debug!("signer functionalities explicitly disabled");
warn!("signer functionalities are explicitly disabled");
return false;
}
if !self.is_ecash_signer() {
debug!("signer doesn't recognise it's a signer for this epoch");
warn!("signer doesn't recognise it's a signer for this epoch");
return false;
}
if dkg_epoch_id != self.dkg_ecash_epoch_id() {
debug!(
warn!(
"mismatched dkg epoch id. current: {dkg_epoch_id}, signer's: {}",
self.dkg_ecash_epoch_id()
);
@@ -11,10 +11,11 @@ use nym_crypto::asymmetric::ed25519;
use serde::{Deserialize, Serialize};
use std::time::Duration;
use time::OffsetDateTime;
use tracing::warn;
use utoipa::ToSchema;
pub(crate) const CHAIN_STALL_THRESHOLD: Duration = Duration::from_secs(5 * 60);
pub(crate) const STALE_RESPONSE_THRESHOLD: Duration = Duration::from_secs(5 * 60);
pub(crate) const CHAIN_STALL_THRESHOLD: Duration = Duration::from_secs(10 * 60);
pub(crate) const STALE_RESPONSE_THRESHOLD: Duration = Duration::from_secs(10 * 60);
// the reason for generics is not to remove duplication of code,
// but because without them, we'd be having problems with circular dependencies,
@@ -188,6 +189,7 @@ where
};
let SignerStatus::Tested { result } = &self.status else {
warn!("no valid chain response");
return false;
};
result
@@ -239,6 +241,7 @@ where
};
let SignerStatus::Tested { result } = &self.status else {
warn!("no valid signer response");
return false;
};
result.signing_status.signing_available(
+1
View File
@@ -36,6 +36,7 @@ thiserror = { workspace = true }
tracing = { workspace = true }
itertools = { workspace = true }
inventory = { workspace = true }
fastrand = { workspace = true }
tokio = { workspace = true, features = ["rt", "macros", "time"] }
rustls = { workspace=true }
# used for decoding text responses (they were already implicitly included)
+78 -128
View File
@@ -55,9 +55,8 @@ use std::{
use hickory_resolver::{
TokioResolver,
config::{NameServerConfig, NameServerConfigGroup, ResolverConfig, ResolverOpts},
lookup_ip::LookupIpIntoIter,
name_server::TokioConnectionProvider,
config::{CLOUDFLARE, NameServerConfig, QUAD9, ResolverConfig, ResolverOpts},
net::{NetError, runtime::TokioRuntimeProvider},
};
use once_cell::sync::OnceCell;
use reqwest::dns::{Addrs, Name, Resolve, Resolving};
@@ -113,7 +112,7 @@ pub enum ResolveError {
#[error("invalid name: {0}")]
InvalidNameError(String),
#[error("hickory-dns resolver error: {0}")]
ResolveError(#[from] hickory_resolver::ResolveError),
ResolveError(#[from] NetError),
#[error("high level lookup timed out")]
Timeout,
#[error("hostname not found in static lookup table")]
@@ -123,7 +122,10 @@ pub enum ResolveError {
impl ResolveError {
/// Returns true if the error is a timeout.
pub fn is_timeout(&self) -> bool {
matches!(self, ResolveError::Timeout)
matches!(
self,
ResolveError::Timeout | ResolveError::ResolveError(NetError::Timeout)
)
}
}
@@ -167,18 +169,17 @@ impl Resolve for HickoryDnsResolver {
fn resolve(&self, name: Name) -> Resolving {
let use_system = self.use_system.load(std::sync::atomic::Ordering::Relaxed);
let use_shared = self.use_shared;
let resolver = if use_system {
match self
.system_resolver
let result = if use_system {
self.system_resolver
.get_or_try_init(|| HickoryDnsResolver::new_resolver_system(use_shared))
{
Ok(r) => r.clone(),
Err(e) => return Box::pin(return_err(e)),
}
} else {
self.state
.get_or_init(|| HickoryDnsResolver::new_resolver(use_shared))
.clone()
.get_or_try_init(|| HickoryDnsResolver::new_resolver(use_shared))
};
let resolver = match result {
Ok(r) => r.clone(),
Err(err) => return Box::pin(return_err(err)),
};
let maybe_static = self.static_base.clone();
@@ -227,9 +228,11 @@ async fn resolve(
let primary_err = match resolve_fut.await {
Err(_) => ResolveError::Timeout,
Ok(Ok(lookup)) => {
let addrs: Addrs = Box::new(SocketAddrs {
iter: lookup.into_iter(),
});
// Shuffle so that successive connection attempts cycle through all
// returned IPs rather than always hitting the same first address.
let mut ips = Vec::from_iter(lookup.iter());
fastrand::shuffle(&mut ips);
let addrs: Addrs = Box::new(ips.into_iter().map(|ip| SocketAddr::new(ip, 0)));
return Ok(addrs);
}
Ok(Err(e)) => {
@@ -256,18 +259,6 @@ async fn resolve(
Err(primary_err)
}
struct SocketAddrs {
iter: LookupIpIntoIter,
}
impl Iterator for SocketAddrs {
type Item = SocketAddr;
fn next(&mut self) -> Option<Self::Item> {
self.iter.next().map(|ip_addr| SocketAddr::new(ip_addr, 0))
}
}
impl HickoryDnsResolver {
/// Returns an instance of the shared resolver.
pub fn shared() -> Self {
@@ -288,7 +279,7 @@ impl HickoryDnsResolver {
.clone()
} else {
self.state
.get_or_init(|| HickoryDnsResolver::new_resolver(self.use_shared))
.get_or_try_init(|| HickoryDnsResolver::new_resolver(self.use_shared))?
.clone()
};
@@ -311,11 +302,11 @@ impl HickoryDnsResolver {
}
}
fn new_resolver(use_shared: bool) -> TokioResolver {
fn new_resolver(use_shared: bool) -> Result<TokioResolver, ResolveError> {
// using a closure here is slightly gross, but this makes sure that if the
// lazy-init returns an error it can be handled by the client
if use_shared {
SHARED_RESOLVER.state.get_or_init(new_resolver).clone()
SHARED_RESOLVER.state.get_or_try_init(new_resolver).cloned()
} else {
new_resolver()
}
@@ -367,7 +358,7 @@ impl HickoryDnsResolver {
/// Clear entries from the static table that would return entries during the pre-resolve stage.
/// This means that all lookups will attempt to use the network resolver again before the static
/// table is consulted.
///
///
/// Entries elevated to pre-resolve from fallback (added from default or using
/// [`set_fallback`]`) will have their cache timeout cleared. Entries added directly to
/// pre-resolve (using [`Self::set_static_preresolve`]) will be removed.
@@ -438,20 +429,7 @@ impl HickoryDnsResolver {
/// Get the list of currently available nameserver configs.
pub fn all_configured_name_servers(&self) -> Vec<NameServerConfig> {
default_nameserver_group().to_vec()
}
/// Get the list of currently used nameserver configs.
pub fn active_name_servers(&self) -> Vec<NameServerConfig> {
if !self.use_shared {
return self
.state
.get()
.map(|r| r.config().name_servers().to_vec())
.unwrap_or(self.all_configured_name_servers());
}
SHARED_RESOLVER.active_name_servers()
default_nameserver_group()
}
/// Do a trial resolution using each nameserver individually to test which are working and which
@@ -477,65 +455,60 @@ impl HickoryDnsResolver {
///
/// Caches successfully resolved addresses for 30 minutes to prevent continual use of remote lookup.
/// This resolver is intended to be used for OUR API endpoints that do not rapidly rotate IPs.
fn new_resolver() -> TokioResolver {
fn new_resolver() -> Result<TokioResolver, ResolveError> {
let name_servers = default_nameserver_group_ipv4_only();
configure_and_build_resolver(name_servers)
}
fn configure_and_build_resolver<G>(name_servers: G) -> TokioResolver
where
G: Into<NameServerConfigGroup>,
{
fn configure_and_build_resolver(
name_servers: Vec<NameServerConfig>,
) -> Result<TokioResolver, ResolveError> {
let options = HickoryDnsResolver::default_options();
let name_servers: NameServerConfigGroup = name_servers.into();
info!("building new configured resolver");
debug!("configuring resolver with {options:?}, {name_servers:?}");
let config = ResolverConfig::from_parts(None, Vec::new(), name_servers);
let mut resolver_builder =
TokioResolver::builder_with_config(config, TokioConnectionProvider::default());
TokioResolver::builder_with_config(config, TokioRuntimeProvider::default());
resolver_builder = resolver_builder.with_options(options);
resolver_builder.build()
Ok(resolver_builder.build()?)
}
fn filter_ipv4(nameservers: impl AsRef<[NameServerConfig]>) -> Vec<NameServerConfig> {
fn filter_ipv4(nameservers: impl IntoIterator<Item = NameServerConfig>) -> Vec<NameServerConfig> {
nameservers
.as_ref()
.iter()
.filter(|ns| ns.socket_addr.is_ipv4())
.cloned()
.into_iter()
.filter(|ns| ns.ip.is_ipv4())
.collect()
}
#[allow(unused)]
fn filter_ipv6(nameservers: impl AsRef<[NameServerConfig]>) -> Vec<NameServerConfig> {
fn filter_ipv6(nameservers: impl IntoIterator<Item = NameServerConfig>) -> Vec<NameServerConfig> {
nameservers
.as_ref()
.iter()
.filter(|ns| ns.socket_addr.is_ipv6())
.cloned()
.into_iter()
.filter(|ns| ns.ip.is_ipv6())
.collect()
}
#[allow(unused)]
fn default_nameserver_group() -> NameServerConfigGroup {
let mut name_servers = NameServerConfigGroup::quad9_tls();
name_servers.merge(NameServerConfigGroup::quad9_https());
name_servers.merge(NameServerConfigGroup::cloudflare_tls());
name_servers.merge(NameServerConfigGroup::cloudflare_https());
name_servers
fn default_nameserver_group() -> Vec<NameServerConfig> {
QUAD9
.tls()
.chain(QUAD9.https())
.chain(CLOUDFLARE.tls())
.chain(CLOUDFLARE.https())
.collect()
}
fn default_nameserver_group_ipv4_only() -> NameServerConfigGroup {
filter_ipv4(&default_nameserver_group() as &[NameServerConfig]).into()
fn default_nameserver_group_ipv4_only() -> Vec<NameServerConfig> {
filter_ipv4(default_nameserver_group())
}
#[allow(unused)]
fn default_nameserver_group_ipv6_only() -> NameServerConfigGroup {
filter_ipv6(&default_nameserver_group() as &[NameServerConfig]).into()
fn default_nameserver_group_ipv6_only() -> Vec<NameServerConfig> {
filter_ipv6(default_nameserver_group())
}
/// Create a new resolver with the default configuration, which reads from the system DNS config
@@ -550,7 +523,7 @@ fn new_resolver_system() -> Result<TokioResolver, ResolveError> {
resolver_builder = resolver_builder.with_options(options);
Ok(resolver_builder.build())
Ok(resolver_builder.build()?)
}
fn new_default_static_fallback() -> StaticResolver {
@@ -577,7 +550,7 @@ async fn trial_nameservers_inner(
async fn trial_lookup(name_server: NameServerConfig, query: &str) -> Result<(), ResolveError> {
debug!("running ns trial {name_server:?} query={query}");
let resolver = configure_and_build_resolver(vec![name_server]);
let resolver = configure_and_build_resolver(vec![name_server])?;
match tokio::time::timeout(DEFAULT_OVERALL_LOOKUP_TIMEOUT, resolver.ipv4_lookup(query)).await {
Ok(Ok(_)) => Ok(()),
@@ -590,8 +563,10 @@ async fn trial_lookup(name_server: NameServerConfig, query: &str) -> Result<(),
mod test {
use super::*;
use itertools::Itertools;
use std::collections::HashMap;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
use std::{
collections::HashMap,
net::{IpAddr, Ipv4Addr, Ipv6Addr},
};
/// IP addresses guaranteed to fail attempts to resolve
///
@@ -670,26 +645,16 @@ mod test {
let mut ns_ips = GUARANTEED_BROKEN_IPS_1.to_vec();
ns_ips.push(good_cf_ip);
let broken_ns_https = NameServerConfigGroup::from_ips_https(
&ns_ips,
443,
"cloudflare-dns.com".to_string(),
true,
);
let domain = Arc::<str>::from("cloudflare-dns.com");
let path = Arc::<str>::from("/dns-query");
let broken_ns_https = GUARANTEED_BROKEN_IPS_1
.iter()
.chain([&good_cf_ip])
.map(|ip| NameServerConfig::https(*ip, domain.clone(), Some(path.clone())))
.collect::<Vec<_>>();
let inner = configure_and_build_resolver(broken_ns_https);
// create a new resolver that won't mess with the shared resolver used by other tests
let resolver = HickoryDnsResolver {
use_shared: false,
state: Arc::new(OnceCell::with_value(inner)),
static_base: Some(Default::default()),
..Default::default()
};
let name_servers = resolver.state.get().unwrap().config().name_servers();
for (ns, result) in trial_nameservers_inner(name_servers).await {
if ns.socket_addr.ip() == good_cf_ip {
for (ns, result) in trial_nameservers_inner(&broken_ns_https).await {
if ns.ip == good_cf_ip {
assert!(result.is_ok())
} else {
assert!(result.is_err())
@@ -705,21 +670,20 @@ mod test {
fn build_broken_resolver() -> Result<TokioResolver, ResolveError> {
info!("building new faulty resolver");
let mut broken_ns_group = NameServerConfigGroup::from_ips_tls(
GUARANTEED_BROKEN_IPS_1,
853,
"cloudflare-dns.com".to_string(),
true,
);
let broken_ns_https = NameServerConfigGroup::from_ips_https(
GUARANTEED_BROKEN_IPS_1,
443,
"cloudflare-dns.com".to_string(),
true,
);
broken_ns_group.merge(broken_ns_https);
let domain = Arc::<str>::from("cloudflare-dns.com");
let path = Arc::<str>::from("/dns-query");
let broken_ns_group = GUARANTEED_BROKEN_IPS_1
.iter()
.map(|ip| NameServerConfig::tls(*ip, domain.clone()))
.chain(
GUARANTEED_BROKEN_IPS_1
.iter()
.map(|ip| NameServerConfig::https(*ip, domain.clone(), Some(path.clone())))
.collect::<Vec<_>>(),
)
.collect::<Vec<_>>();
Ok(configure_and_build_resolver(broken_ns_group))
configure_and_build_resolver(broken_ns_group)
}
#[tokio::test]
@@ -740,7 +704,7 @@ mod test {
build_broken_resolver()?;
let domain = "ifconfig.me";
let result = resolver.resolve_str(domain).await;
assert!(result.is_err_and(|e| matches!(e, ResolveError::Timeout)));
assert!(result.is_err_and(|e| e.is_timeout()));
let duration = time_start.elapsed();
assert!(duration < resolver.overall_dns_timeout + Duration::from_secs(1));
@@ -774,25 +738,11 @@ mod test {
// unsuccessful lookup - primary times out, and not in static table
let domain = "non-existent.nymtech.net";
let result = resolver.resolve_str(domain).await;
assert!(result.is_err_and(|e| matches!(e, ResolveError::Timeout)));
assert!(result.is_err_and(|e| e.is_timeout()));
Ok(())
}
#[test]
fn default_resolver_uses_ipv4_only_nameservers() {
let resolver = HickoryDnsResolver::thread_resolver();
resolver
.active_name_servers()
.iter()
.all(|cfg| cfg.socket_addr.is_ipv4());
SHARED_RESOLVER
.active_name_servers()
.iter()
.all(|cfg| cfg.socket_addr.is_ipv4());
}
#[tokio::test]
#[cfg(any())] // #[ignore] we run --ignore in CI/CD assuming it just means slow -_-
// This test impacts the state of the shared resolver and as such is disabled to avoid
+13 -13
View File
@@ -141,9 +141,7 @@
use http::header::USER_AGENT;
pub use inventory;
pub use reqwest;
pub use reqwest::ClientBuilder as ReqwestClientBuilder;
pub use reqwest::StatusCode;
pub use reqwest::{self, ClientBuilder as ReqwestClientBuilder, StatusCode};
use std::error::Error;
pub mod registry;
@@ -152,19 +150,21 @@ use crate::path::RequestPath;
use async_trait::async_trait;
use bytes::Bytes;
use cfg_if::cfg_if;
use http::HeaderMap;
use http::header::{ACCEPT, CONTENT_TYPE};
use http::{
HeaderMap,
header::{ACCEPT, CONTENT_TYPE},
};
use itertools::Itertools;
use mime::Mime;
use reqwest::header::HeaderValue;
use reqwest::{RequestBuilder, Response};
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use std::fmt::Display;
use reqwest::{RequestBuilder, Response, header::HeaderValue};
use serde::{Deserialize, Serialize, de::DeserializeOwned};
#[cfg(not(target_arch = "wasm32"))]
use std::io::ErrorKind;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::Duration;
use std::{
fmt::Display,
sync::atomic::{AtomicUsize, Ordering},
time::Duration,
};
use thiserror::Error;
use tracing::{debug, instrument, warn};
@@ -1274,7 +1274,7 @@ pub(crate) fn might_be_network_interference(err: &reqwest::Error) -> bool {
} else if let Some(_tls_err) = e.downcast_ref::<rustls::Error>() {
// try downcast to TLS error
return true;
} else if let Some(resolve_err) = e.downcast_ref::<hickory_resolver::ResolveError>() {
} else if let Some(resolve_err) = e.downcast_ref::<hickory_resolver::net::NetError>() {
// try downcast to DNS error
return resolve_err.is_nx_domain();
} else {
+2
View File
@@ -10,7 +10,9 @@ fn sanitize_fragment(segment: &str) -> &str {
segment.trim_matches(|c: char| c.is_whitespace() || c == '/')
}
/// Defines a path that can be used to make a request to an API.
pub trait RequestPath: Debug {
/// Sanitise the request path by removing empty segments and trimming whitespace and slashes
fn to_sanitized_segments(&self) -> Vec<&str>;
}
-12
View File
@@ -20,18 +20,6 @@ pub const MAX_NON_STREAM_VERSION: u8 = v8::VERSION;
/// mixnet sends, matching the node-side enforcement in `ip-packet-router`.
pub const SPHINX_STREAM_VERSION_THRESHOLD: u8 = v9::VERSION;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn stream_transport_threshold_is_consistent() {
assert_eq!(MAX_NON_STREAM_VERSION, 8);
assert_eq!(SPHINX_STREAM_VERSION_THRESHOLD, 9);
assert!(SPHINX_STREAM_VERSION_THRESHOLD > MAX_NON_STREAM_VERSION);
}
}
// version 3: initial version
// version 4: IPv6 support
// version 5: Add severity level to info response
+4 -5
View File
@@ -17,11 +17,10 @@ exclude = ["build.rs"]
[dependencies]
dotenvy = { workspace = true, optional = true }
log = { workspace = true, optional = true }
schemars = { workspace = true, features = ["preserve_order"], optional = true }
serde = { workspace = true, features = ["derive"], optional = true }
serde_json = {workspace = true, optional = true }
tracing = {workspace = true, optional = true }
serde_json = { workspace = true, optional = true }
tracing = { workspace = true, optional = true }
url = { workspace = true, optional = true }
utoipa = { workspace = true, optional = true }
@@ -30,9 +29,9 @@ utoipa = { workspace = true, optional = true }
[features]
default = ["env", "network"]
env = ["dotenvy", "log", "serde_json", "tracing"]
env = ["dotenvy", "serde_json", "tracing"]
network = ["schemars", "serde", "url"]
utoipa = [ "dep:utoipa" ]
utoipa = ["dep:utoipa"]
[build-dependencies]
regex = { workspace = true }
+8 -4
View File
@@ -27,16 +27,20 @@ fn print_env_vars_with_keys_in_file<P: AsRef<Path> + Copy>(config_env_file: P) {
.expect("Invalid path to environment configuration file");
for item in items {
let (key, val) = item.expect("Invalid item in environment configuration file");
log::debug!("{key}: {val}");
tracing::debug!("{key}: {val}");
}
}
pub fn env_configured() -> bool {
std::env::var(var_names::CONFIGURED).is_ok()
}
pub fn setup_env<P: AsRef<Path>>(config_env_file: Option<P>) {
match std::env::var(var_names::CONFIGURED) {
// if the configuration is not already set in the env vars
Err(std::env::VarError::NotPresent) => {
if let Some(config_env_file) = &config_env_file {
log::debug!(
tracing::debug!(
"Loading environment variables from {:?}",
config_env_file.as_ref()
);
@@ -47,12 +51,12 @@ pub fn setup_env<P: AsRef<Path>>(config_env_file: Option<P>) {
// if nothing is set, the use mainnet defaults
// if the user has not set `CONFIGURED`, then even if they set any of the env variables,
// overwrite them
log::debug!("Loading mainnet defaults");
tracing::debug!("Loading mainnet defaults");
crate::mainnet::export_to_env();
}
}
Err(_) => {
log::debug!("Environment variables already set. Using them");
tracing::debug!("Environment variables already set. Using them");
crate::mainnet::export_to_env()
}
_ => {
+24 -5
View File
@@ -22,6 +22,8 @@ pub const VESTING_CONTRACT_ADDRESS: &str =
pub const PERFORMANCE_CONTRACT_ADDRESS: &str = "";
// /\ TODO: this has to be updated once the contract is deployed
pub const NETWORK_MONITORS_CONTRACT_ADDRESS: &str =
"n1m3a2ltkjqud8mkmrpqvgllrtv2p4r6js6qwl7p8cqkzrq8jg6e2qwqgl8z";
pub const ECASH_CONTRACT_ADDRESS: &str =
"n1r7s6aksyc6pqardx88k3rkgfagwvj4z4zum9mmz2sfk3zm2mha0sd4dnun";
pub const GROUP_CONTRACT_ADDRESS: &str =
@@ -36,6 +38,10 @@ pub const REWARDING_VALIDATOR_ADDRESS: &str = "n10yyd98e2tuwu0f7ypz9dy3hhjw7v772
pub const NYXD_URL: &str = "https://rpc.nymtech.net";
pub const NYXD_WS: &str = "wss://rpc.nymtech.net/websocket";
// cluster of lite rpc nodes (not part of consensus, aggressive pruning, no archival state)
pub const NYXD_QUERY_LITE: &str = "https://blockstream.nymtech.net";
pub const NYXD_WS_LITE: &str = "wss://blockstream.nymtech.net/websocket";
pub const NYM_API: &str = "https://validator.nymtech.net/api/";
#[cfg(feature = "network")]
pub const NYM_APIS: &[ApiUrlConst] = &[
@@ -43,10 +49,6 @@ pub const NYM_APIS: &[ApiUrlConst] = &[
url: NYM_API,
front_hosts: None,
},
ApiUrlConst {
url: "https://nym-frontdoor.vercel.app/api/",
front_hosts: Some(&["vercel.app", "vercel.com"]),
},
ApiUrlConst {
url: "https://nym-frontdoor.global.ssl.fastly.net/api/",
front_hosts: Some(&["yelp.global.ssl.fastly.net"]),
@@ -68,7 +70,7 @@ pub const UPGRADE_MODE_ATTESTER_ED25519_BS58_PUBKEY: &str =
pub const NYM_VPN_APIS: &[ApiUrlConst] = &[
ApiUrlConst {
url: NYM_VPN_API,
front_hosts: Some(&["vercel.app", "vercel.com"]),
front_hosts: None,
},
ApiUrlConst {
url: "https://nymvpn-frontdoor.global.ssl.fastly.net/api/",
@@ -137,6 +139,11 @@ pub fn read_parsed_var_if_not_default<T: std::str::FromStr>(
.map(std::str::FromStr::from_str)
}
#[cfg(feature = "env")]
pub fn read_parsed_var<T: std::str::FromStr>(var: &str) -> Result<T, T::Err> {
std::env::var(var).unwrap_or_default().parse()
}
#[cfg(all(feature = "env", feature = "network"))]
pub fn export_to_env() {
use crate::var_names;
@@ -167,6 +174,14 @@ pub fn export_to_env() {
var_names::COCONUT_DKG_CONTRACT_ADDRESS,
COCONUT_DKG_CONTRACT_ADDRESS,
);
set_var_to_default(
var_names::PERFORMANCE_CONTRACT_ADDRESS,
PERFORMANCE_CONTRACT_ADDRESS,
);
set_var_to_default(
var_names::NETWORK_MONITORS_CONTRACT_ADDRESS,
NETWORK_MONITORS_CONTRACT_ADDRESS,
);
set_var_to_default(
var_names::REWARDING_VALIDATOR_ADDRESS,
REWARDING_VALIDATOR_ADDRESS,
@@ -186,6 +201,8 @@ pub fn export_to_env() {
var_names::UPGRADE_MODE_ATTESTER_ED25519_BS58_PUBKEY,
UPGRADE_MODE_ATTESTER_ED25519_BS58_PUBKEY,
);
set_var_to_default(var_names::NYXD_QUERY_LITE, NYXD_QUERY_LITE);
set_var_to_default(var_names::NYXD_WS_LITE, NYXD_WS_LITE);
}
#[cfg(all(feature = "env", feature = "network"))]
@@ -237,4 +254,6 @@ pub fn export_to_env_if_not_set() {
var_names::UPGRADE_MODE_ATTESTER_ED25519_BS58_PUBKEY,
UPGRADE_MODE_ATTESTER_ED25519_BS58_PUBKEY,
);
set_var_conditionally_to_default(var_names::NYXD_QUERY_LITE, NYXD_QUERY_LITE);
set_var_conditionally_to_default(var_names::NYXD_WS_LITE, NYXD_WS_LITE);
}
+37 -3
View File
@@ -39,6 +39,8 @@ pub struct NymContracts {
pub vesting_contract_address: Option<String>,
#[serde(default)]
pub performance_contract_address: Option<String>,
#[serde(default)]
pub network_monitors_contract_address: Option<String>,
pub ecash_contract_address: Option<String>,
pub group_contract_address: Option<String>,
pub multisig_contract_address: Option<String>,
@@ -72,6 +74,15 @@ pub struct ApiUrl {
pub front_hosts: Option<Vec<String>>,
}
impl From<Url> for ApiUrl {
fn from(value: Url) -> Self {
ApiUrl {
url: value.to_string(),
front_hosts: None,
}
}
}
#[derive(Copy, Clone, Debug, Serialize)]
pub struct ApiUrlConst<'a> {
pub url: &'a str,
@@ -178,6 +189,10 @@ impl NymNetworkDetails {
.with_group_contract(get_optional_env(var_names::GROUP_CONTRACT_ADDRESS))
.with_multisig_contract(get_optional_env(var_names::MULTISIG_CONTRACT_ADDRESS))
.with_coconut_dkg_contract(get_optional_env(var_names::COCONUT_DKG_CONTRACT_ADDRESS))
.with_performance_contract(get_optional_env(var_names::PERFORMANCE_CONTRACT_ADDRESS))
.with_network_monitors_contract(get_optional_env(
var_names::NETWORK_MONITORS_CONTRACT_ADDRESS,
))
.with_nym_vpn_api_url(get_optional_env(var_names::NYM_VPN_API))
.with_nym_vpn_api_urls(nym_vpn_api_urls)
.with_nym_api_urls(nym_api_urls)
@@ -199,6 +214,9 @@ impl NymNetworkDetails {
performance_contract_address: parse_optional_str(
mainnet::PERFORMANCE_CONTRACT_ADDRESS,
),
network_monitors_contract_address: parse_optional_str(
mainnet::NETWORK_MONITORS_CONTRACT_ADDRESS,
),
ecash_contract_address: parse_optional_str(mainnet::ECASH_CONTRACT_ADDRESS),
group_contract_address: parse_optional_str(mainnet::GROUP_CONTRACT_ADDRESS),
multisig_contract_address: parse_optional_str(mainnet::MULTISIG_CONTRACT_ADDRESS),
@@ -226,7 +244,7 @@ impl NymNetworkDetails {
fn set_optional_var(var_name: &str, value: Option<String>) {
if let Some(value) = value {
unsafe {set_var(var_name, value)}
unsafe { set_var(var_name, value) }
}
}
unsafe {
@@ -364,15 +382,31 @@ impl NymNetworkDetails {
self
}
#[must_use]
pub fn with_performance_contract<S: Into<String>>(mut self, contract: Option<S>) -> Self {
self.contracts.performance_contract_address = contract.map(Into::into);
self
}
#[must_use]
pub fn with_network_monitors_contract<S: Into<String>>(mut self, contract: Option<S>) -> Self {
self.contracts.network_monitors_contract_address = contract.map(Into::into);
self
}
#[must_use]
pub fn with_nym_vpn_api_url<S: Into<String>>(mut self, endpoint: Option<S>) -> Self {
self.nym_vpn_api_url = endpoint.map(Into::into);
self
}
pub fn set_nym_api_urls<U: Into<ApiUrl>>(&mut self, urls: Vec<U>) {
self.nym_api_urls = Some(urls.into_iter().map(Into::into).collect());
}
#[must_use]
pub fn with_nym_api_urls(mut self, urls: Vec<ApiUrl>) -> Self {
self.nym_api_urls = Some(urls);
pub fn with_nym_api_urls<U: Into<ApiUrl>>(mut self, urls: Vec<U>) -> Self {
self.set_nym_api_urls(urls);
self
}
+4
View File
@@ -18,11 +18,15 @@ pub const ECASH_CONTRACT_ADDRESS: &str = "ECASH_CONTRACT_ADDRESS";
pub const GROUP_CONTRACT_ADDRESS: &str = "GROUP_CONTRACT_ADDRESS";
pub const MULTISIG_CONTRACT_ADDRESS: &str = "MULTISIG_CONTRACT_ADDRESS";
pub const COCONUT_DKG_CONTRACT_ADDRESS: &str = "COCONUT_DKG_CONTRACT_ADDRESS";
pub const PERFORMANCE_CONTRACT_ADDRESS: &str = "PERFORMANCE_CONTRACT_ADDRESS";
pub const NETWORK_MONITORS_CONTRACT_ADDRESS: &str = "NETWORK_MONITORS_CONTRACT_ADDRESS";
pub const REWARDING_VALIDATOR_ADDRESS: &str = "REWARDING_VALIDATOR_ADDRESS";
pub const NYXD: &str = "NYXD";
pub const NYM_API: &str = "NYM_API";
pub const NYM_APIS: &str = "NYM_APIS";
pub const NYXD_WEBSOCKET: &str = "NYXD_WS";
pub const NYXD_QUERY_LITE: &str = "NYXD_LITE";
pub const NYXD_WS_LITE: &str = "NYXD_WS_LITE";
pub const EXIT_POLICY_URL: &str = "EXIT_POLICY";
pub const NYM_VPN_API: &str = "NYM_VPN_API";
pub const NYM_VPN_APIS: &str = "NYM_VPN_APIS";
+385 -44
View File
@@ -1,19 +1,19 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use arc_swap::ArcSwap;
use nym_crypto::asymmetric::x25519;
use snow::params::NoiseParams;
use std::{
collections::HashMap,
net::{IpAddr, SocketAddr},
sync::Arc,
time::Duration,
};
use arc_swap::ArcSwap;
use nym_crypto::asymmetric::x25519;
use nym_noise_keys::{NoiseVersion, VersionedNoiseKeyV1};
use snow::params::NoiseParams;
use strum_macros::{EnumIter, FromRepr};
use tokio::sync::{Mutex, MutexGuard};
pub use nym_noise_keys::{NoiseVersion, VersionedNoiseKeyV1};
#[derive(Default, Debug, Clone, Copy, EnumIter, FromRepr, Eq, PartialEq)]
#[repr(u8)]
@@ -53,38 +53,125 @@ impl NoisePattern {
}
}
#[derive(Debug, Default)]
struct SocketAddrToKey {
inner: ArcSwap<HashMap<SocketAddr, VersionedNoiseKeyV1>>,
}
// SW NOTE : Only for phased upgrade. To remove once we decide all nodes have to support Noise
#[derive(Debug, Default)]
struct IpAddrToVersion {
inner: ArcSwap<HashMap<IpAddr, NoiseVersion>>,
}
#[derive(Debug, Clone, Default)]
#[derive(Debug, Clone)]
pub struct NoiseNetworkView {
keys: Arc<SocketAddrToKey>,
support: Arc<IpAddrToVersion>,
inner: Arc<NoiseNetworkViewInner>,
}
impl NoiseNetworkView {
pub fn new_empty() -> Self {
NoiseNetworkView {
keys: Default::default(),
support: Default::default(),
/// Inner state of [`NoiseNetworkView`], shared behind an `Arc`.
///
/// # Concurrency model
///
/// Reads (on the packet-processing hot path) use `ArcSwap` and are fully lock-free.
/// Writers must first acquire `update_lock` to serialise concurrent updates, then call
/// `swap_view` to atomically publish the new map. The lock is intentionally *not* wrapping
/// the map itself so that readers are never blocked.
#[derive(Debug)]
struct NoiseNetworkViewInner {
update_lock: Mutex<()>,
nodes: ArcSwap<HashMap<IpAddr, NoiseNode>>,
}
/// A node in the noise network map, keyed by IP address.
///
/// A single IP can correspond to either one nym-node (which has a single noise key)
/// or one-or-more network monitor agents (each with its own port and noise key).
/// The two variants have independent lifecycles: nym-node entries come from the
/// nym-api topology refresher, while agent entries come from blockchain events.
#[derive(Debug, Clone)]
pub enum NoiseNode {
NymNode { key: VersionedNoiseKeyV1 },
// due to the structure of network monitor agents,
// it is possible to have multiple destinations with the same host ip address,
// but a different noise key.
// however, we are also guaranteed all of those are going to have a unique port.
// note: we're not storing it in a map, since at maximum we might have maybe 20 or so
// entries under a single ip address and linear look-up of a vec is faster than the overhead of a hashmap
NetworkMonitorAgent { nodes: Vec<NetworkMonitorAgentNode> },
}
impl NoiseNode {
pub fn new_nym_node(key: VersionedNoiseKeyV1) -> Self {
NoiseNode::NymNode { key }
}
pub fn new_agent(socket_addr: SocketAddr, key: VersionedNoiseKeyV1) -> Self {
NoiseNode::NetworkMonitorAgent {
nodes: vec![NetworkMonitorAgentNode {
port: socket_addr.port(),
key,
}],
}
}
pub fn swap_view(&self, new: HashMap<SocketAddr, VersionedNoiseKeyV1>) {
let noise_support = new
.iter()
.map(|(s_addr, key)| (s_addr.ip(), key.supported_version))
.collect::<HashMap<_, _>>();
self.keys.inner.store(Arc::new(new));
self.support.inner.store(Arc::new(noise_support));
pub fn is_nym_node(&self) -> bool {
matches!(self, NoiseNode::NymNode { .. })
}
}
/// A single network monitor agent identified by its port on a shared host.
///
/// Multiple agents may share an IP address but are guaranteed to have unique ports.
#[derive(Debug, Clone)]
pub struct NetworkMonitorAgentNode {
pub port: u16,
pub key: VersionedNoiseKeyV1,
}
impl NoiseNetworkView {
pub fn new(nodes: HashMap<IpAddr, NoiseNode>) -> Self {
// ensure we're always storing canonical IPs
NoiseNetworkView {
inner: Arc::new(NoiseNetworkViewInner {
update_lock: Mutex::new(()),
nodes: ArcSwap::from_pointee(
nodes
.into_iter()
.map(|(k, v)| (k.to_canonical(), v))
.collect(),
),
}),
}
}
pub fn new_empty() -> Self {
Self::new(Default::default())
}
/// Build a noise view pre-populated with network monitor agents (used at startup).
pub fn new_with_agents(agents: HashMap<IpAddr, Vec<NetworkMonitorAgentNode>>) -> Self {
let mut nodes = HashMap::new();
for (ip, agent_nodes) in agents {
nodes.insert(ip, NoiseNode::NetworkMonitorAgent { nodes: agent_nodes });
}
Self::new(nodes)
}
pub async fn get_update_permit(&self) -> MutexGuard<'_, ()> {
self.inner.update_lock.lock().await
}
/// Atomically replace the noise key map.
///
/// # Precondition
///
/// The caller **must** hold the permit returned by [`NoiseNetworkView::get_update_permit`].
/// Passing the `MutexGuard` by value enforces this at the type level — the guard is dropped
/// (releasing the lock) only after the swap completes, preventing torn writes from concurrent
/// update calls.
pub fn swap_view(&self, _permit: MutexGuard<'_, ()>, new: HashMap<IpAddr, NoiseNode>) {
// defensive: ensure stored keys are always canonical so lookups (which canonicalise)
// always match. callers should still canonicalise before assembling `new` to keep
// collision resolution deterministic.
let canonical = new
.into_iter()
.map(|(k, v)| (k.to_canonical(), v))
.collect();
self.inner.nodes.store(Arc::new(canonical));
}
pub fn all_nodes(&self) -> HashMap<IpAddr, NoiseNode> {
self.inner.nodes.load().as_ref().clone()
}
}
@@ -126,20 +213,38 @@ impl NoiseConfig {
self
}
pub(crate) fn get_noise_key(&self, s_address: &SocketAddr) -> Option<VersionedNoiseKeyV1> {
self.network.keys.inner.load().get(s_address).copied()
/// Look up the noise key for a specific remote socket address.
///
/// Used on the **initiator** path where we need the responder's public key
/// to start the handshake. For nym-nodes the port is ignored (one key per IP);
/// for network monitor agents, the port disambiguates which agent's key to use.
pub(crate) fn get_noise_key(&self, address: SocketAddr) -> Option<VersionedNoiseKeyV1> {
let ip_to_check = address.ip().to_canonical();
let nodes = self.network.inner.nodes.load();
// Resolve the noise key for `address` from a loaded snapshot of the node map.
// For [`NoiseNode::NymNode`] entries the port is irrelevant — only the IP is matched.
// For [`NoiseNode::NetworkMonitorAgent`] entries the port selects the specific agent.
match nodes.get(&ip_to_check)? {
NoiseNode::NymNode { key } => Some(*key),
NoiseNode::NetworkMonitorAgent { nodes } => {
let port = address.port();
nodes.iter().find(|n| n.port == port).map(|n| n.key)
}
}
}
// Only for phased update
//SW This can lead to some troubles if two nodes share the same IP and one support Noise but not the other.
// This in only for the progressive update though and there is no workaround
pub(crate) fn get_noise_support(&self, ip_addr: IpAddr) -> Option<NoiseVersion> {
let plain_ip_support = self.network.support.inner.load().get(&ip_addr).copied();
// SW default bind address being [::]:1789, it can happen that a responder sees the ipv6-mapped address of the initiator, this check for that
let canonical_ip = &ip_addr.to_canonical();
let canonical_ip_support = self.network.support.inner.load().get(canonical_ip).copied();
plain_ip_support.or(canonical_ip_support)
/// Check whether a remote IP is known to support noise.
/// Used on the responder path where we don't need the remote's key
/// (the initiator sends it during the handshake).
// note: in the case of network monitor agents, it must hold
// that ALL agents on given host support it (or don't support it)
pub(crate) fn supports_noise(&self, ip_addr: IpAddr) -> bool {
self.network
.inner
.nodes
.load()
.contains_key(&ip_addr.to_canonical())
}
}
@@ -169,4 +274,240 @@ mod tests {
}
}
}
mod noise_key_lookup {
use super::super::*;
use nym_crypto::asymmetric::x25519;
use nym_noise_keys::NoiseVersion;
use nym_test_utils::helpers::deterministic_rng;
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::sync::Arc;
use std::time::Duration;
fn dummy_key(seed: u8) -> VersionedNoiseKeyV1 {
VersionedNoiseKeyV1 {
supported_version: NoiseVersion::V1,
x25519_pubkey: x25519::PublicKey::from([seed; 32]),
}
}
fn make_config(nodes: HashMap<IpAddr, NoiseNode>) -> NoiseConfig {
NoiseConfig::new(
Arc::new(x25519::KeyPair::new(&mut deterministic_rng())),
NoiseNetworkView::new(nodes),
Duration::from_secs(5),
)
}
// -- get_noise_key tests --
#[test]
fn nym_node_key_returned_regardless_of_port() {
let ip = IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1));
let key = dummy_key(1);
let config = make_config(HashMap::from([(ip, NoiseNode::new_nym_node(key))]));
// any port should resolve to the same key
assert_eq!(config.get_noise_key(SocketAddr::new(ip, 1000)), Some(key));
assert_eq!(config.get_noise_key(SocketAddr::new(ip, 9999)), Some(key));
}
#[test]
fn agent_key_resolved_by_port() {
let ip = IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1));
let key_a = dummy_key(1);
let key_b = dummy_key(2);
let node = NoiseNode::NetworkMonitorAgent {
nodes: vec![
NetworkMonitorAgentNode {
port: 1000,
key: key_a,
},
NetworkMonitorAgentNode {
port: 2000,
key: key_b,
},
],
};
let config = make_config(HashMap::from([(ip, node)]));
assert_eq!(config.get_noise_key(SocketAddr::new(ip, 1000)), Some(key_a));
assert_eq!(config.get_noise_key(SocketAddr::new(ip, 2000)), Some(key_b));
}
#[test]
fn agent_unknown_port_returns_none() {
let ip = IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1));
let node = NoiseNode::NetworkMonitorAgent {
nodes: vec![NetworkMonitorAgentNode {
port: 1000,
key: dummy_key(1),
}],
};
let config = make_config(HashMap::from([(ip, node)]));
assert!(config.get_noise_key(SocketAddr::new(ip, 9999)).is_none());
}
#[test]
fn completely_unknown_address_returns_none() {
let config = make_config(HashMap::new());
assert!(config
.get_noise_key(SocketAddr::new(IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4)), 80))
.is_none());
}
#[test]
fn canonical_ipv6_fallback_for_nym_node() {
// register under the plain IPv4 address
let v4 = IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4));
let key = dummy_key(1);
let config = make_config(HashMap::from([(v4, NoiseNode::new_nym_node(key))]));
// query with the IPv4-mapped IPv6 form (::ffff:1.2.3.4)
let v6_mapped = IpAddr::V6(Ipv4Addr::new(1, 2, 3, 4).to_ipv6_mapped());
assert_eq!(
config.get_noise_key(SocketAddr::new(v6_mapped, 1789)),
Some(key)
);
}
#[test]
fn canonical_ipv6_fallback_for_agent() {
let v4 = IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4));
let key = dummy_key(1);
let node = NoiseNode::NetworkMonitorAgent {
nodes: vec![NetworkMonitorAgentNode { port: 1000, key }],
};
let config = make_config(HashMap::from([(v4, node)]));
let v6_mapped = IpAddr::V6(Ipv4Addr::new(1, 2, 3, 4).to_ipv6_mapped());
assert_eq!(
config.get_noise_key(SocketAddr::new(v6_mapped, 1000)),
Some(key)
);
// wrong port still returns None even with the fallback
assert!(config
.get_noise_key(SocketAddr::new(v6_mapped, 9999))
.is_none());
}
// -- supports_noise tests --
#[test]
fn supports_noise_true_for_nym_node() {
let ip = IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1));
let config = make_config(HashMap::from([(ip, NoiseNode::new_nym_node(dummy_key(1)))]));
assert!(config.supports_noise(ip));
}
#[test]
fn supports_noise_true_for_agent_ip() {
let ip = IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1));
let node = NoiseNode::NetworkMonitorAgent {
nodes: vec![NetworkMonitorAgentNode {
port: 1000,
key: dummy_key(1),
}],
};
let config = make_config(HashMap::from([(ip, node)]));
assert!(config.supports_noise(ip));
}
#[test]
fn supports_noise_false_for_unknown_ip() {
let config = make_config(HashMap::new());
assert!(!config.supports_noise(IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4))));
}
#[test]
fn supports_noise_canonical_ipv6_fallback() {
let v4 = IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4));
let config = make_config(HashMap::from([(v4, NoiseNode::new_nym_node(dummy_key(1)))]));
let v6_mapped = IpAddr::V6(Ipv4Addr::new(1, 2, 3, 4).to_ipv6_mapped());
assert!(config.supports_noise(v6_mapped));
}
// -- new_with_agents test --
#[test]
fn new_with_agents_builds_correct_view() {
let ip = IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1));
let key_a = dummy_key(1);
let key_b = dummy_key(2);
let agents = HashMap::from([(
ip,
vec![
NetworkMonitorAgentNode {
port: 1000,
key: key_a,
},
NetworkMonitorAgentNode {
port: 2000,
key: key_b,
},
],
)]);
let config = NoiseConfig::new(
Arc::new(x25519::KeyPair::new(&mut deterministic_rng())),
NoiseNetworkView::new_with_agents(agents),
Duration::from_secs(5),
);
assert_eq!(config.get_noise_key(SocketAddr::new(ip, 1000)), Some(key_a));
assert_eq!(config.get_noise_key(SocketAddr::new(ip, 2000)), Some(key_b));
assert!(config.supports_noise(ip));
}
// -- swap_view canonicalisation test --
// Regression: an agent registered via blockchain events flows through `swap_view` (called
// from `NetworkMonitorAgentsModule::new_agent` and from the periodic network refresher).
// If a non-canonical (IPv4-mapped IPv6) key reaches `swap_view`, lookups via
// `supports_noise` (which canonicalises) used to miss, producing the
// "can't speak Noise yet, falling back to TCP" warning despite the agent being correctly
// authorised in the routing filter.
#[tokio::test]
async fn swap_view_canonicalises_non_canonical_keys() {
let view = NoiseNetworkView::new_empty();
let v4 = IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4));
let v6_mapped = IpAddr::V6(Ipv4Addr::new(1, 2, 3, 4).to_ipv6_mapped());
let mut nodes = HashMap::new();
// intentionally insert under the IPv4-mapped form — what a buggy caller might do
nodes.insert(
v6_mapped,
NoiseNode::NetworkMonitorAgent {
nodes: vec![NetworkMonitorAgentNode {
port: 1000,
key: dummy_key(1),
}],
},
);
let permit = view.get_update_permit().await;
view.swap_view(permit, nodes);
let config = NoiseConfig::new(
Arc::new(x25519::KeyPair::new(&mut deterministic_rng())),
view,
Duration::from_secs(5),
);
// lookup via either form must succeed
assert!(config.supports_noise(v4));
assert!(config.supports_noise(v6_mapped));
assert!(config
.get_noise_key(SocketAddr::new(v6_mapped, 1000))
.is_some());
}
}
}
+6
View File
@@ -15,6 +15,12 @@ pub enum Connection<C> {
Noise(#[pin] Box<NoiseStream<C>>),
}
impl<C> Connection<C> {
pub fn is_noise(&self) -> bool {
matches!(self, Connection::Noise(_))
}
}
impl<C> AsyncRead for Connection<C>
where
C: AsyncRead + AsyncWrite + Unpin,
+2 -2
View File
@@ -66,7 +66,7 @@ pub async fn upgrade_noise_initiator(
Error::Prereq(Prerequisite::RemotePublicKey)
})?;
let Some(key) = config.get_noise_key(&responder_addr) else {
let Some(key) = config.get_noise_key(responder_addr) else {
warn!("{responder_addr} can't speak Noise yet, falling back to TCP");
return Ok(Connection::Raw(conn));
};
@@ -106,7 +106,7 @@ pub async fn upgrade_noise_responder(
};
// if responder doesn't announce noise support, we fallback to tcp
if config.get_noise_support(initiator_addr.ip()).is_none() {
if !config.supports_noise(initiator_addr.ip()) {
warn!("{initiator_addr} can't speak Noise yet, falling back to TCP",);
return Ok(Connection::Raw(conn));
};
@@ -110,6 +110,12 @@ pub enum PacketProcessingError {
PacketReplay,
}
impl PacketProcessingError {
pub fn is_replay(&self) -> bool {
matches!(self, PacketProcessingError::PacketReplay)
}
}
pub struct PartialyUnwrappedPacketWithKeyRotation {
pub packet: PartiallyUnwrappedPacket,
pub used_key_rotation: u32,
+1 -1
View File
@@ -29,7 +29,7 @@ pub use sphinx_packet::{
packet::builder::DEFAULT_PAYLOAD_SIZE,
payload::{
PAYLOAD_OVERHEAD_SIZE, Payload,
key::{PayloadKey, PayloadKeySeed},
key::{PayloadKey, PayloadKeySeed, derive_payload_key},
},
route::{Destination, DestinationAddressBytes, Node, NodeAddressBytes, SURBIdentifier},
surb::{SURB, SURBMaterial},
+3 -2
View File
@@ -7,8 +7,9 @@ use nyxd_scraper_shared::NyxdScraper;
pub use nyxd_scraper_shared::constants;
pub use nyxd_scraper_shared::error::ScraperError;
pub use nyxd_scraper_shared::{
BlockModule, MsgModule, NyxdScraperTransaction, ParsedTransactionResponse, PruningOptions,
PruningStrategy, StartingBlockOpts, TxModule,
BlockModule, DecodedMessage, MsgModule, NyxdScraperTransaction, ParsedTransactionDetails,
ParsedTransactionResponse, PruningOptions, PruningStrategy, StartingBlockOpts, TxModule,
parse_msg,
};
pub use storage::models;
@@ -9,7 +9,7 @@ use std::str::FromStr;
// replicate behaviour of `CosmosMessageAddressesParser` from juno
pub(crate) fn parse_addresses_from_events(tx: &ParsedTransactionResponse) -> Vec<String> {
let mut addresses: Vec<String> = Vec::new();
for event in &tx.tx_result.events {
for event in &tx.tx_details.tx_result.events {
for attribute in &event.attributes {
let Ok(value) = attribute.value_str() else {
continue;
@@ -147,6 +147,7 @@ impl PostgresStorageTransaction {
for chain_tx in txs {
// bdjuno style, base64 encode them
let signatures = chain_tx
.tx_details
.tx
.signatures
.iter()
@@ -154,12 +155,14 @@ impl PostgresStorageTransaction {
.collect();
let messages = chain_tx
.parsed_messages
.decoded_messages
.values()
.map(|msg| &msg.decoded_content)
.cloned()
.collect::<Vec<_>>();
let signer_infos = chain_tx
.tx_details
.tx
.auth_info
.signer_infos
@@ -167,28 +170,28 @@ impl PostgresStorageTransaction {
.map(|info| proto::cosmos::tx::v1beta1::SignerInfo::from(info.clone()))
.collect::<Vec<_>>();
let hash = chain_tx.hash.to_string();
let height = chain_tx.height.into();
let index = chain_tx.index as i32;
let hash = chain_tx.tx_details.hash.to_string();
let height = chain_tx.tx_details.height().into();
let index = chain_tx.tx_details.index as i32;
let log = serde_json::to_value(chain_tx.tx_result.log.clone())
let log = serde_json::to_value(chain_tx.tx_details.tx_result.log.clone())
.inspect_err(|e| error!(hash, height, index, "Failed to parse logs: {e}"))
.unwrap_or_default();
let events = &chain_tx.tx_result.events;
let events = &chain_tx.tx_details.tx_result.events;
insert_transaction(
hash,
height,
index,
chain_tx.tx_result.code.is_ok(),
chain_tx.tx_details.tx_result.code.is_ok(),
serde_json::Value::Array(messages),
chain_tx.tx.body.memo.clone(),
chain_tx.tx_details.tx.body.memo.clone(),
signatures,
serde_json::to_value(signer_infos)?,
serde_json::to_value(&chain_tx.tx.auth_info.fee)?,
chain_tx.tx_result.gas_wanted,
chain_tx.tx_result.gas_used,
chain_tx.tx_result.log.clone(),
serde_json::to_value(&chain_tx.tx_details.tx.auth_info.fee)?,
chain_tx.tx_details.tx_result.gas_wanted,
chain_tx.tx_details.tx_result.gas_used,
chain_tx.tx_details.tx_result.log.clone(),
json!(log),
json!(events),
self.inner.as_mut(),
@@ -207,17 +210,20 @@ impl PostgresStorageTransaction {
for chain_tx in txs {
let involved_addresses = parse_addresses_from_events(chain_tx);
for (index, msg) in chain_tx.tx.body.messages.iter().enumerate() {
let parsed_message = chain_tx.parsed_messages.get(&index);
for (index, msg) in chain_tx.tx_details.tx.body.messages.iter().enumerate() {
let parsed_message = chain_tx
.decoded_messages
.get(&index)
.map(|msg| &msg.decoded_content);
let value = serde_json::to_value(parsed_message)?;
insert_message(
chain_tx.hash.to_string(),
chain_tx.tx_details.hash.to_string(),
index as i64,
msg.type_url.clone(),
value,
involved_addresses.clone(),
chain_tx.height.into(),
chain_tx.tx_details.height().into(),
self.inner.as_mut(),
)
.await?
@@ -33,9 +33,9 @@ impl TxModule for FancyTxModule {
async fn handle_tx(&mut self, tx: &ParsedTransactionResponse) -> Result<(), ScraperError> {
println!(
"✨ got new tx for height {}: {} ({} msgs)",
tx.block.header.height,
tx.hash,
tx.parsed_messages.len()
tx.tx_details.height(),
tx.tx_details.hash,
tx.tx_details.tx.body.messages.len()
);
Ok(())
@@ -281,7 +281,7 @@ where
&mut self,
block: BlockToProcess,
) -> Result<(), ScraperError> {
info!("processing block at height {}", block.height);
debug!("processing block at height {}", block.height);
let full_info = self
.rpc_client
@@ -291,8 +291,13 @@ where
if let Some(tx_info) = &full_info.transactions {
debug!("this block has {} transaction(s)", tx_info.len());
for tx in tx_info {
debug!("{} has {} message(s)", tx.hash, tx.tx.body.messages.len());
for (index, msg) in tx.tx.body.messages.iter().enumerate() {
let details = &tx.tx_details;
debug!(
"{} has {} message(s)",
details.hash,
details.tx.body.messages.len()
);
for (index, msg) in details.tx.body.messages.iter().enumerate() {
debug!("{index}: {:?}", msg.type_url)
}
}
@@ -315,11 +320,24 @@ where
for tx_module in &mut self.tx_modules {
tx_module.handle_tx(block_tx).await?;
}
let tx_details = &block_tx.tx_details;
// the ones concerned with individual messages
for (index, msg) in block_tx.tx.body.messages.iter().enumerate() {
for (index, msg) in tx_details.tx.body.messages.iter().enumerate() {
let Some(decoded) = block_tx.decoded_messages.get(&index) else {
warn!(
"height: {} tx: {} tx_index: {}, msg_index: {index}: message failed to get decoded",
tx_details.height(),
tx_details.hash,
tx_details.index,
);
continue;
};
for msg_module in &mut self.msg_modules {
if msg.type_url == msg_module.type_url() {
msg_module.handle_msg(index, msg, block_tx).await?
msg_module
.handle_msg(index, msg, decoded, tx_details)
.await?
}
}
}
@@ -7,9 +7,16 @@ use tendermint::{Block, Hash, abci, block, tx};
use tendermint_rpc::endpoint::{block as block_endpoint, block_results, validators};
use tendermint_rpc::event::{Event, EventData};
// just get all everything out of tx::Response, but parse raw `tx` bytes
/// Message decoded from the raw transaction and converted into json.
/// Note that it might have gone through additional processing as set by the `MessageRegistry`
#[derive(Clone, Debug)]
pub struct ParsedTransactionResponse {
pub struct DecodedMessage {
pub type_url: String,
pub decoded_content: serde_json::Value,
}
#[derive(Clone, Debug)]
pub struct ParsedTransactionDetails {
/// The hash of the transaction.
///
/// Deserialized from a hex-encoded string (there is a discrepancy between
@@ -17,8 +24,6 @@ pub struct ParsedTransactionResponse {
/// the Tendermint RPC).
pub hash: Hash,
pub height: block::Height,
pub index: u32,
pub tx_result: abci::types::ExecTxResult,
@@ -27,13 +32,23 @@ pub struct ParsedTransactionResponse {
pub proof: Option<tx::Proof>,
pub parsed_messages: BTreeMap<usize, serde_json::Value>,
pub parsed_message_urls: BTreeMap<usize, String>,
pub block: Block,
}
impl ParsedTransactionDetails {
pub fn height(&self) -> block::Height {
self.block.header.height
}
}
// just get all everything out of tx::Response, but parse raw `tx` bytes
#[derive(Clone, Debug)]
pub struct ParsedTransactionResponse {
pub tx_details: ParsedTransactionDetails,
pub decoded_messages: BTreeMap<usize, DecodedMessage>,
}
#[derive(Debug)]
pub struct FullBlockInformation {
/// Basic block information, including its signers.
+1 -3
View File
@@ -82,10 +82,8 @@ pub enum ScraperError {
source: cosmrs::ErrorReport,
},
#[error("could not parse msg in tx {hash} at index {index} into {type_url}: {source}")]
#[error("could not parse msg of type {type_url}: {source}")]
MsgParseFailure {
hash: Hash,
index: usize,
type_url: String,
#[source]
source: cosmrs::ErrorReport,
+1 -1
View File
@@ -47,7 +47,7 @@ pub fn validator_consensus_address(id: account::Id) -> Result<AccountId, Malform
}
pub fn tx_gas_sum(txs: &[ParsedTransactionResponse]) -> i64 {
txs.iter().map(|tx| tx.tx_result.gas_used).sum()
txs.iter().map(|tx| tx.tx_details.tx_result.gas_used).sum()
}
pub fn validator_info(
+4 -2
View File
@@ -15,12 +15,14 @@ pub(crate) mod subscriber;
pub mod watcher;
pub use block_processor::pruning::{PruningOptions, PruningStrategy};
pub use block_processor::types::ParsedTransactionResponse;
pub use block_processor::types::{
DecodedMessage, ParsedTransactionDetails, ParsedTransactionResponse,
};
pub use cosmos_module::{
CosmosModule,
message_registry::{MessageRegistry, default_message_registry},
};
pub use cosmrs::Any;
pub use modules::{BlockModule, MsgModule, TxModule};
pub use modules::{BlockModule, MsgModule, TxModule, parse_msg};
pub use scraper::{Config, NyxdScraper, StartingBlockOpts};
pub use storage::{NyxdScraperStorage, NyxdScraperTransaction};
@@ -6,5 +6,5 @@ mod msg_module;
mod tx_module;
pub use block_module::BlockModule;
pub use msg_module::MsgModule;
pub use msg_module::{MsgModule, parse_msg};
pub use tx_module::TxModule;
@@ -1,11 +1,47 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::block_processor::types::ParsedTransactionResponse;
use crate::block_processor::types::{DecodedMessage, ParsedTransactionDetails};
use crate::error::ScraperError;
use async_trait::async_trait;
use cosmrs::Any;
use cosmrs::tx::Msg;
/// Parse a protobuf `Any` message into a strongly typed Cosmos message.
///
/// # Example
///
/// ```rust,ignore
/// let execute_msg: MsgExecuteContract = parse_msg(msg)?;
/// ```
///
/// # Errors
///
/// Returns `ScraperError::MsgParseFailure` if:
/// - The type URL doesn't match the expected type
/// - The protobuf bytes are malformed
/// - The message schema is incompatible with this version of the code
pub fn parse_msg<T: Msg>(msg: &Any) -> Result<T, ScraperError> {
T::from_any(msg).map_err(|source| ScraperError::MsgParseFailure {
type_url: msg.type_url.clone(),
source,
})
}
/// Trait for modules that process specific message types from blockchain transactions.
///
/// # Parameters
///
/// - `index`: Position of this message within the transaction (0-based)
/// - `msg`: Raw protobuf message (use `parse_msg()` to decode)
/// - `decoded_msg`: Pre-decoded JSON representation (may be None for unsupported types)
/// - `tx`: Transaction details including block height, hash, and execution result
///
/// # Error Handling
///
/// - Return `Err` for critical failures that should stop block processing
/// - Return `Ok(())` for non-critical errors (e.g., unexpected contract schema)
/// - Log warnings for debugging without propagating errors
#[async_trait]
pub trait MsgModule {
fn type_url(&self) -> String;
@@ -14,6 +50,7 @@ pub trait MsgModule {
&mut self,
index: usize,
msg: &Any,
tx: &ParsedTransactionResponse,
decoded_msg: &DecodedMessage,
tx: &ParsedTransactionDetails,
) -> Result<(), ScraperError>;
}
+20 -16
View File
@@ -2,11 +2,11 @@
// SPDX-License-Identifier: Apache-2.0
use crate::block_processor::types::{
BlockToProcess, FullBlockInformation, ParsedTransactionResponse,
BlockToProcess, DecodedMessage, FullBlockInformation, ParsedTransactionResponse,
};
use crate::error::ScraperError;
use crate::helpers::tx_hash;
use crate::{Any, MessageRegistry, default_message_registry};
use crate::{Any, MessageRegistry, ParsedTransactionDetails, default_message_registry};
use futures::StreamExt;
use futures::future::join3;
use std::collections::BTreeMap;
@@ -77,8 +77,7 @@ impl RpcClient {
) -> Result<Vec<ParsedTransactionResponse>, ScraperError> {
let mut transactions = Vec::with_capacity(raw_transactions.len());
for raw_tx in raw_transactions {
let mut parsed_messages = BTreeMap::new();
let mut parsed_message_urls = BTreeMap::new();
let mut decoded_messages = BTreeMap::new();
let tx = cosmrs::Tx::from_bytes(&raw_tx.tx).map_err(|source| {
ScraperError::TxParseFailure {
hash: raw_tx.hash,
@@ -87,22 +86,27 @@ impl RpcClient {
})?;
for (index, msg) in tx.body.messages.iter().enumerate() {
if let Some(value) = self.decode_or_skip(msg) {
parsed_messages.insert(index, value);
parsed_message_urls.insert(index, msg.type_url.clone());
if let Some(decoded_content) = self.decode_or_skip(msg) {
decoded_messages.insert(
index,
DecodedMessage {
type_url: msg.type_url.clone(),
decoded_content,
},
);
}
}
transactions.push(ParsedTransactionResponse {
hash: raw_tx.hash,
height: raw_tx.height,
index: raw_tx.index,
tx_result: raw_tx.tx_result,
tx,
proof: raw_tx.proof,
parsed_messages,
parsed_message_urls,
block: block.clone(),
tx_details: ParsedTransactionDetails {
hash: raw_tx.hash,
index: raw_tx.index,
tx_result: raw_tx.tx_result,
tx,
proof: raw_tx.proof,
block: block.clone(),
},
decoded_messages,
})
}
Ok(transactions)
+10 -2
View File
@@ -14,15 +14,16 @@ use tracing::info;
use url::Url;
pub struct WatcherConfig {
/// Url to the websocket endpoint of a validator, for example `wss://rpc.nymtech.net/websocket`
/// Url to the websocket endpoint of a validator, for example, `wss://rpc.nymtech.net/websocket`
pub websocket_url: Url,
/// Url to the rpc endpoint of a validator, for example `https://rpc.nymtech.net/`
/// Url to the rpc endpoint of a validator, for example, `https://rpc.nymtech.net/`
pub rpc_url: Url,
}
pub struct NyxdWatcherBuilder {
config: WatcherConfig,
custom_shutdown: CancellationToken,
block_modules: Vec<Box<dyn BlockModule + Send>>,
tx_modules: Vec<Box<dyn TxModule + Send>>,
@@ -33,12 +34,19 @@ impl NyxdWatcherBuilder {
pub fn new(config: WatcherConfig) -> Self {
NyxdWatcherBuilder {
config,
custom_shutdown: CancellationToken::new(),
block_modules: vec![],
tx_modules: vec![],
msg_modules: vec![],
}
}
#[must_use]
pub fn with_custom_shutdown(mut self, token: CancellationToken) -> Self {
self.custom_shutdown = token;
self
}
#[must_use]
pub fn with_block_module<M: BlockModule + Send + 'static>(mut self, module: M) -> Self {
self.block_modules.push(Box::new(module));
+3 -2
View File
@@ -7,8 +7,9 @@ use nyxd_scraper_shared::NyxdScraper;
pub use nyxd_scraper_shared::constants;
pub use nyxd_scraper_shared::error::ScraperError;
pub use nyxd_scraper_shared::{
BlockModule, MsgModule, NyxdScraperTransaction, ParsedTransactionResponse, PruningOptions,
PruningStrategy, StartingBlockOpts, TxModule,
BlockModule, DecodedMessage, MsgModule, NyxdScraperTransaction, ParsedTransactionDetails,
ParsedTransactionResponse, PruningOptions, PruningStrategy, StartingBlockOpts, TxModule,
parse_msg,
};
pub use storage::models;
@@ -132,15 +132,15 @@ impl SqliteStorageTransaction {
for chain_tx in txs {
insert_transaction(
chain_tx.hash.to_string(),
chain_tx.height.into(),
chain_tx.index as i64,
chain_tx.tx_result.code.is_ok(),
chain_tx.tx.body.messages.len() as i64,
chain_tx.tx.body.memo.clone(),
chain_tx.tx_result.gas_wanted,
chain_tx.tx_result.gas_used,
chain_tx.tx_result.log.clone(),
chain_tx.tx_details.hash.to_string(),
chain_tx.tx_details.height().into(),
chain_tx.tx_details.index as i64,
chain_tx.tx_details.tx_result.code.is_ok(),
chain_tx.tx_details.tx.body.messages.len() as i64,
chain_tx.tx_details.tx.body.memo.clone(),
chain_tx.tx_details.tx_result.gas_wanted,
chain_tx.tx_details.tx_result.gas_used,
chain_tx.tx_details.tx_result.log.clone(),
self.0.as_mut(),
)
.await?;
@@ -156,12 +156,12 @@ impl SqliteStorageTransaction {
debug!("persisting messages");
for chain_tx in txs {
for (index, msg) in chain_tx.tx.body.messages.iter().enumerate() {
for (index, msg) in chain_tx.tx_details.tx.body.messages.iter().enumerate() {
insert_message(
chain_tx.hash.to_string(),
chain_tx.tx_details.hash.to_string(),
index as i64,
msg.type_url.clone(),
chain_tx.height.into(),
chain_tx.tx_details.height().into(),
self.0.as_mut(),
)
.await?
+28
View File
@@ -1180,6 +1180,22 @@ dependencies = [
"rand_chacha",
]
[[package]]
name = "network-monitors"
version = "1.0.0"
dependencies = [
"anyhow",
"bs58",
"cosmwasm-schema",
"cosmwasm-std",
"cw-controllers",
"cw-storage-plus",
"cw2",
"nym-contracts-common",
"nym-contracts-common-testing",
"nym-network-monitors-contract-common",
]
[[package]]
name = "num-bigint"
version = "0.4.6"
@@ -1456,6 +1472,18 @@ dependencies = [
"regex",
]
[[package]]
name = "nym-network-monitors-contract-common"
version = "1.20.4"
dependencies = [
"cosmwasm-schema",
"cosmwasm-std",
"cw-controllers",
"schemars",
"serde",
"thiserror 2.0.12",
]
[[package]]
name = "nym-pemstore"
version = "1.20.4"
+5 -3
View File
@@ -10,6 +10,7 @@ members = [
"multisig/cw4-group",
"vesting",
"performance",
"network-monitors",
]
[workspace.package]
@@ -19,6 +20,8 @@ homepage = "https://nymtech.net"
documentation = "https://nymtech.net"
edition = "2021"
license = "Apache-2.0"
rust-version = "1.86.0"
readme = "../README.md"
[profile.release]
opt-level = 3
@@ -67,6 +70,7 @@ nym-ecash-contract-common = "1.20.4"
nym-group-contract-common = "1.20.4"
nym-mixnet-contract-common = "1.20.4"
nym-multisig-contract-common = "1.20.4"
nym-network-monitors-contract-common = "1.20.4"
nym-network-defaults = { version = "1.20.4", default-features = false }
nym-performance-contract-common = "1.20.4"
nym-pool-contract-common = "1.20.4"
@@ -94,8 +98,6 @@ unimplemented = "deny"
unreachable = "deny"
# For local development, import via path instead of crates.io, e.g.
# [patch.crates-io]
# nym-coconut-dkg-common = { path = "../common/cosmwasm-smart-contracts/coconut-dkg" }
[patch.crates-io]
nym-network-monitors-contract-common = { path = "../common/cosmwasm-smart-contracts/network-monitors-contract" }
nym-ecash-contract-common = { path = "../common/cosmwasm-smart-contracts/ecash-contract" }
+4
View File
@@ -0,0 +1,4 @@
[alias]
wasm = "build --release --lib --target wasm32-unknown-unknown"
unit-test = "test --lib"
schema = "run --bin schema --features=schema-gen"
+43
View File
@@ -0,0 +1,43 @@
[package]
name = "network-monitors"
description = "CosmWasm smart contract storing information on Nym network monitors"
version = "1.0.0"
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 = false
[[bin]]
name = "schema"
required-features = ["schema-gen"]
[lib]
name = "network_monitors_contract"
crate-type = ["cdylib", "rlib"]
[dependencies]
bs58 = { workspace = true }
cosmwasm-std = { workspace = true }
cw2 = { workspace = true }
cw-storage-plus = { workspace = true }
cw-controllers = { workspace = true }
cosmwasm-schema = { workspace = true, optional = true }
nym-network-monitors-contract-common = { workspace = true }
nym-contracts-common = { workspace = true }
[dev-dependencies]
anyhow = { workspace = true }
nym-contracts-common-testing = { workspace = true }
[features]
schema-gen = ["nym-network-monitors-contract-common/schema", "cosmwasm-schema"]
[lints]
workspace = true
+5
View File
@@ -0,0 +1,5 @@
wasm:
RUSTFLAGS='-C link-arg=-s' cargo build --release --target wasm32-unknown-unknown
generate-schema:
cargo schema
@@ -0,0 +1,368 @@
{
"contract_name": "network-monitors",
"contract_version": "0.1.0",
"idl_version": "1.0.0",
"instantiate": {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "InstantiateMsg",
"type": "object",
"required": [
"orchestrator_address"
],
"properties": {
"orchestrator_address": {
"description": "Address of the initial network monitor orchestrator.",
"type": "string"
}
},
"additionalProperties": false
},
"execute": {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "ExecuteMsg",
"oneOf": [
{
"description": "Change the admin",
"type": "object",
"required": [
"update_admin"
],
"properties": {
"update_admin": {
"type": "object",
"required": [
"admin"
],
"properties": {
"admin": {
"type": "string"
}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
{
"description": "Authorise new network monitor orchestrator",
"type": "object",
"required": [
"authorise_network_monitor_orchestrator"
],
"properties": {
"authorise_network_monitor_orchestrator": {
"type": "object",
"required": [
"address"
],
"properties": {
"address": {
"type": "string"
}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
{
"description": "Revoke network monitor orchestrator authorisation.",
"type": "object",
"required": [
"revoke_network_monitor_orchestrator"
],
"properties": {
"revoke_network_monitor_orchestrator": {
"type": "object",
"required": [
"address"
],
"properties": {
"address": {
"type": "string"
}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
{
"description": "Authorise new network monitor (or renew authorisation) granting additional privileges when sending mixnet packets to Nym nodes.",
"type": "object",
"required": [
"authorise_network_monitor"
],
"properties": {
"authorise_network_monitor": {
"type": "object",
"required": [
"address"
],
"properties": {
"address": {
"type": "string",
"format": "ip"
}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
{
"description": "Revoke network monitor authorisation.",
"type": "object",
"required": [
"revoke_network_monitor"
],
"properties": {
"revoke_network_monitor": {
"type": "object",
"required": [
"address"
],
"properties": {
"address": {
"type": "string",
"format": "ip"
}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
{
"description": "Revoke all network monitor authorisations.",
"type": "string",
"enum": [
"revoke_all_network_monitors"
]
}
]
},
"query": {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "QueryMsg",
"oneOf": [
{
"type": "object",
"required": [
"admin"
],
"properties": {
"admin": {
"type": "object",
"additionalProperties": false
}
},
"additionalProperties": false
},
{
"type": "object",
"required": [
"network_monitor_orchestrators"
],
"properties": {
"network_monitor_orchestrators": {
"type": "object",
"additionalProperties": false
}
},
"additionalProperties": false
},
{
"type": "object",
"required": [
"network_monitor_agents"
],
"properties": {
"network_monitor_agents": {
"type": "object",
"properties": {
"limit": {
"description": "Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default.",
"type": [
"integer",
"null"
],
"format": "uint32",
"minimum": 0.0
},
"start_next_after": {
"description": "Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response.",
"type": [
"string",
"null"
],
"format": "ip"
}
},
"additionalProperties": false
}
},
"additionalProperties": false
}
]
},
"migrate": {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "MigrateMsg",
"type": "object",
"additionalProperties": false
},
"sudo": null,
"responses": {
"admin": {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "AdminResponse",
"description": "Returned from Admin.query_admin()",
"type": "object",
"properties": {
"admin": {
"type": [
"string",
"null"
]
}
},
"additionalProperties": false
},
"network_monitor_agents": {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "AuthorisedNetworkMonitorsPagedResponse",
"type": "object",
"required": [
"authorised"
],
"properties": {
"authorised": {
"type": "array",
"items": {
"$ref": "#/definitions/AuthorisedNetworkMonitor"
}
},
"start_next_after": {
"type": [
"string",
"null"
],
"format": "ip"
}
},
"additionalProperties": false,
"definitions": {
"Addr": {
"description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.",
"type": "string"
},
"AuthorisedNetworkMonitor": {
"type": "object",
"required": [
"address",
"authorised_at",
"authorised_by"
],
"properties": {
"address": {
"description": "The Ip address associated with the network monitor agent.",
"type": "string",
"format": "ip"
},
"authorised_at": {
"description": "Timestamp of when the network monitor was authorised.",
"allOf": [
{
"$ref": "#/definitions/Timestamp"
}
]
},
"authorised_by": {
"description": "The address of the orchestrator that authorised the network monitor agent.",
"allOf": [
{
"$ref": "#/definitions/Addr"
}
]
}
},
"additionalProperties": false
},
"Timestamp": {
"description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```",
"allOf": [
{
"$ref": "#/definitions/Uint64"
}
]
},
"Uint64": {
"description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```",
"type": "string"
}
}
},
"network_monitor_orchestrators": {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "AuthorisedNetworkMonitorOrchestratorsResponse",
"type": "object",
"required": [
"authorised"
],
"properties": {
"authorised": {
"type": "array",
"items": {
"$ref": "#/definitions/AuthorisedNetworkMonitorOrchestrator"
}
}
},
"additionalProperties": false,
"definitions": {
"Addr": {
"description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.",
"type": "string"
},
"AuthorisedNetworkMonitorOrchestrator": {
"type": "object",
"required": [
"address",
"authorised_at"
],
"properties": {
"address": {
"description": "The address associated with the network monitor orchestrator.",
"allOf": [
{
"$ref": "#/definitions/Addr"
}
]
},
"authorised_at": {
"description": "Timestamp of when the network monitor was authorised.",
"allOf": [
{
"$ref": "#/definitions/Timestamp"
}
]
}
},
"additionalProperties": false
},
"Timestamp": {
"description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```",
"allOf": [
{
"$ref": "#/definitions/Uint64"
}
]
},
"Uint64": {
"description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```",
"type": "string"
}
}
}
}
}
@@ -0,0 +1,125 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "ExecuteMsg",
"oneOf": [
{
"description": "Change the admin",
"type": "object",
"required": [
"update_admin"
],
"properties": {
"update_admin": {
"type": "object",
"required": [
"admin"
],
"properties": {
"admin": {
"type": "string"
}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
{
"description": "Authorise new network monitor orchestrator",
"type": "object",
"required": [
"authorise_network_monitor_orchestrator"
],
"properties": {
"authorise_network_monitor_orchestrator": {
"type": "object",
"required": [
"address"
],
"properties": {
"address": {
"type": "string"
}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
{
"description": "Revoke network monitor orchestrator authorisation.",
"type": "object",
"required": [
"revoke_network_monitor_orchestrator"
],
"properties": {
"revoke_network_monitor_orchestrator": {
"type": "object",
"required": [
"address"
],
"properties": {
"address": {
"type": "string"
}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
{
"description": "Authorise new network monitor (or renew authorisation) granting additional privileges when sending mixnet packets to Nym nodes.",
"type": "object",
"required": [
"authorise_network_monitor"
],
"properties": {
"authorise_network_monitor": {
"type": "object",
"required": [
"address"
],
"properties": {
"address": {
"type": "string",
"format": "ip"
}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
{
"description": "Revoke network monitor authorisation.",
"type": "object",
"required": [
"revoke_network_monitor"
],
"properties": {
"revoke_network_monitor": {
"type": "object",
"required": [
"address"
],
"properties": {
"address": {
"type": "string",
"format": "ip"
}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
{
"description": "Revoke all network monitor authorisations.",
"type": "string",
"enum": [
"revoke_all_network_monitors"
]
}
]
}
@@ -0,0 +1,15 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "InstantiateMsg",
"type": "object",
"required": [
"orchestrator_address"
],
"properties": {
"orchestrator_address": {
"description": "Address of the initial network monitor orchestrator.",
"type": "string"
}
},
"additionalProperties": false
}
@@ -0,0 +1,6 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "MigrateMsg",
"type": "object",
"additionalProperties": false
}
@@ -0,0 +1,64 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "QueryMsg",
"oneOf": [
{
"type": "object",
"required": [
"admin"
],
"properties": {
"admin": {
"type": "object",
"additionalProperties": false
}
},
"additionalProperties": false
},
{
"type": "object",
"required": [
"network_monitor_orchestrators"
],
"properties": {
"network_monitor_orchestrators": {
"type": "object",
"additionalProperties": false
}
},
"additionalProperties": false
},
{
"type": "object",
"required": [
"network_monitor_agents"
],
"properties": {
"network_monitor_agents": {
"type": "object",
"properties": {
"limit": {
"description": "Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default.",
"type": [
"integer",
"null"
],
"format": "uint32",
"minimum": 0.0
},
"start_next_after": {
"description": "Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response.",
"type": [
"string",
"null"
],
"format": "ip"
}
},
"additionalProperties": false
}
},
"additionalProperties": false
}
]
}
@@ -0,0 +1,15 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "AdminResponse",
"description": "Returned from Admin.query_admin()",
"type": "object",
"properties": {
"admin": {
"type": [
"string",
"null"
]
}
},
"additionalProperties": false
}
@@ -0,0 +1,74 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "AuthorisedNetworkMonitorsPagedResponse",
"type": "object",
"required": [
"authorised"
],
"properties": {
"authorised": {
"type": "array",
"items": {
"$ref": "#/definitions/AuthorisedNetworkMonitor"
}
},
"start_next_after": {
"type": [
"string",
"null"
],
"format": "ip"
}
},
"additionalProperties": false,
"definitions": {
"Addr": {
"description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.",
"type": "string"
},
"AuthorisedNetworkMonitor": {
"type": "object",
"required": [
"address",
"authorised_at",
"authorised_by"
],
"properties": {
"address": {
"description": "The Ip address associated with the network monitor agent.",
"type": "string",
"format": "ip"
},
"authorised_at": {
"description": "Timestamp of when the network monitor was authorised.",
"allOf": [
{
"$ref": "#/definitions/Timestamp"
}
]
},
"authorised_by": {
"description": "The address of the orchestrator that authorised the network monitor agent.",
"allOf": [
{
"$ref": "#/definitions/Addr"
}
]
}
},
"additionalProperties": false
},
"Timestamp": {
"description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```",
"allOf": [
{
"$ref": "#/definitions/Uint64"
}
]
},
"Uint64": {
"description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```",
"type": "string"
}
}
}
@@ -0,0 +1,61 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "AuthorisedNetworkMonitorOrchestratorsResponse",
"type": "object",
"required": [
"authorised"
],
"properties": {
"authorised": {
"type": "array",
"items": {
"$ref": "#/definitions/AuthorisedNetworkMonitorOrchestrator"
}
}
},
"additionalProperties": false,
"definitions": {
"Addr": {
"description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.",
"type": "string"
},
"AuthorisedNetworkMonitorOrchestrator": {
"type": "object",
"required": [
"address",
"authorised_at"
],
"properties": {
"address": {
"description": "The address associated with the network monitor orchestrator.",
"allOf": [
{
"$ref": "#/definitions/Addr"
}
]
},
"authorised_at": {
"description": "Timestamp of when the network monitor was authorised.",
"allOf": [
{
"$ref": "#/definitions/Timestamp"
}
]
}
},
"additionalProperties": false
},
"Timestamp": {
"description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```",
"allOf": [
{
"$ref": "#/definitions/Uint64"
}
]
},
"Uint64": {
"description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```",
"type": "string"
}
}
}
@@ -0,0 +1,14 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use cosmwasm_schema::write_api;
use nym_network_monitors_contract_common::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg};
fn main() {
write_api! {
instantiate: InstantiateMsg,
query: QueryMsg,
execute: ExecuteMsg,
migrate: MigrateMsg,
}
}
+179
View File
@@ -0,0 +1,179 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::queries::{
query_admin, query_network_monitor_agents, query_network_monitor_orchestrators,
};
use crate::storage::NETWORK_MONITORS_CONTRACT_STORAGE;
use crate::transactions::{
try_authorise_network_monitor, try_authorise_network_monitor_orchestrator,
try_revoke_all_network_monitors, try_revoke_network_monitor,
try_revoke_network_monitor_orchestrator, try_update_contract_admin,
try_update_orchestrator_identity_key,
};
use cosmwasm_std::{
entry_point, to_json_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response,
};
use nym_contracts_common::set_build_information;
use nym_network_monitors_contract_common::{
ExecuteMsg, InstantiateMsg, MigrateMsg, NetworkMonitorsContractError, QueryMsg,
};
const CONTRACT_NAME: &str = "crate:nym-network-monitors-contract";
const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");
#[entry_point]
pub fn instantiate(
deps: DepsMut,
env: Env,
info: MessageInfo,
msg: InstantiateMsg,
) -> Result<Response, NetworkMonitorsContractError> {
cw2::set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
set_build_information!(deps.storage)?;
let orchestrator = deps.api.addr_validate(&msg.orchestrator_address)?;
NETWORK_MONITORS_CONTRACT_STORAGE.initialise(deps, env, info.sender, orchestrator)?;
Ok(Response::default())
}
#[entry_point]
pub fn execute(
deps: DepsMut,
env: Env,
info: MessageInfo,
msg: ExecuteMsg,
) -> Result<Response, NetworkMonitorsContractError> {
match msg {
ExecuteMsg::UpdateAdmin { admin } => try_update_contract_admin(deps, info, admin),
ExecuteMsg::AuthoriseNetworkMonitorOrchestrator { address } => {
try_authorise_network_monitor_orchestrator(deps, env, info, address)
}
ExecuteMsg::UpdateOrchestratorIdentityKey { key } => {
try_update_orchestrator_identity_key(deps, info, key)
}
ExecuteMsg::RevokeNetworkMonitorOrchestrator { address } => {
try_revoke_network_monitor_orchestrator(deps, info, address)
}
ExecuteMsg::AuthoriseNetworkMonitor {
mixnet_address: address,
bs58_x25519_noise,
noise_version,
} => try_authorise_network_monitor(
deps,
env,
info,
address,
bs58_x25519_noise,
noise_version,
),
ExecuteMsg::RevokeNetworkMonitor { address } => {
try_revoke_network_monitor(deps, info, address)
}
ExecuteMsg::RevokeAllNetworkMonitors => try_revoke_all_network_monitors(deps, info),
}
}
#[entry_point]
pub fn query(deps: Deps, _: Env, msg: QueryMsg) -> Result<Binary, NetworkMonitorsContractError> {
match msg {
QueryMsg::Admin {} => Ok(to_json_binary(&query_admin(deps)?)?),
QueryMsg::NetworkMonitorOrchestrators {} => {
Ok(to_json_binary(&query_network_monitor_orchestrators(deps)?)?)
}
QueryMsg::NetworkMonitorAgents {
start_next_after,
limit,
} => Ok(to_json_binary(&query_network_monitor_agents(
deps,
start_next_after,
limit,
)?)?),
}
}
#[entry_point]
pub fn migrate(
deps: DepsMut,
_env: Env,
_msg: MigrateMsg,
) -> Result<Response, NetworkMonitorsContractError> {
set_build_information!(deps.storage)?;
cw2::ensure_from_older_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
Ok(Default::default())
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(test)]
mod contract_instantiation {
use super::*;
use cosmwasm_std::testing::{message_info, mock_dependencies, mock_env};
use cosmwasm_std::Addr;
#[test]
fn sets_contract_admin_to_the_message_sender() -> anyhow::Result<()> {
let mut deps = mock_dependencies();
let env = mock_env();
let init_msg = InstantiateMsg {
orchestrator_address: deps.api.addr_make("foo").to_string(),
};
let some_sender = deps.api.addr_make("some_sender");
instantiate(
deps.as_mut(),
env,
message_info(&some_sender, &[]),
init_msg,
)?;
NETWORK_MONITORS_CONTRACT_STORAGE
.contract_admin
.assert_admin(deps.as_ref(), &some_sender)?;
Ok(())
}
#[test]
fn sets_the_initial_orchestrator() -> anyhow::Result<()> {
let mut deps = mock_dependencies();
let env = mock_env();
let admin = deps.api.addr_make("some_sender");
let bad_addr = "foo".to_string();
let good_addr = deps.api.addr_make("foo").to_string();
let bad_init_msg = InstantiateMsg {
orchestrator_address: bad_addr.clone(),
};
let good_init_msg = InstantiateMsg {
orchestrator_address: good_addr.clone(),
};
let res = instantiate(
deps.as_mut(),
env.clone(),
message_info(&admin, &[]),
bad_init_msg,
);
assert!(res.is_err());
let is_orchestrator = NETWORK_MONITORS_CONTRACT_STORAGE
.is_orchestrator(deps.as_ref(), &Addr::unchecked(&good_addr))?;
assert!(!is_orchestrator);
instantiate(deps.as_mut(), env, message_info(&admin, &[]), good_init_msg)?;
let is_orchestrator = NETWORK_MONITORS_CONTRACT_STORAGE
.is_orchestrator(deps.as_ref(), &Addr::unchecked(&good_addr))?;
assert!(is_orchestrator);
Ok(())
}
}
}
+11
View File
@@ -0,0 +1,11 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
pub mod contract;
pub mod queries;
pub mod queued_migrations;
pub mod storage;
pub mod transactions;
#[cfg(test)]
pub mod testing;
+345
View File
@@ -0,0 +1,345 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::storage::{retrieval_limits, AgentStorageKey, NETWORK_MONITORS_CONTRACT_STORAGE};
use cosmwasm_std::{Deps, StdResult};
use cw_controllers::AdminResponse;
use cw_storage_plus::Bound;
use nym_network_monitors_contract_common::{
AuthorisedNetworkMonitorOrchestratorsResponse, AuthorisedNetworkMonitorsPagedResponse,
NetworkMonitorsContractError,
};
use std::net::SocketAddr;
pub fn query_admin(deps: Deps) -> Result<AdminResponse, NetworkMonitorsContractError> {
NETWORK_MONITORS_CONTRACT_STORAGE
.contract_admin
.query_admin(deps)
.map_err(Into::into)
}
// no need for pagination as we don't expect even a double digit of those
pub fn query_network_monitor_orchestrators(
deps: Deps,
) -> Result<AuthorisedNetworkMonitorOrchestratorsResponse, NetworkMonitorsContractError> {
let authorised = NETWORK_MONITORS_CONTRACT_STORAGE
.authorised_orchestrators
.range(deps.storage, None, None, cosmwasm_std::Order::Ascending)
.map(|record| record.map(|(_, details)| details))
.collect::<StdResult<Vec<_>>>()?;
Ok(AuthorisedNetworkMonitorOrchestratorsResponse { authorised })
}
pub fn query_network_monitor_agents(
deps: Deps,
start_after: Option<SocketAddr>,
limit: Option<u32>,
) -> Result<AuthorisedNetworkMonitorsPagedResponse, NetworkMonitorsContractError> {
let limit = limit
.unwrap_or(retrieval_limits::AGENTS_DEFAULT_LIMIT)
.min(retrieval_limits::AGENTS_MAX_LIMIT) as usize;
let start = start_after.map(|addr| Bound::exclusive(AgentStorageKey::from(addr)));
let authorised = NETWORK_MONITORS_CONTRACT_STORAGE
.authorised_agents
.range(deps.storage, start, None, cosmwasm_std::Order::Ascending)
.take(limit)
.map(|record| record.map(|(_, details)| details))
.collect::<StdResult<Vec<_>>>()?;
let start_next_after = authorised.last().map(|last| last.mixnet_address);
Ok(AuthorisedNetworkMonitorsPagedResponse {
authorised,
start_next_after,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(test)]
mod admin_query {
use crate::queries::query_admin;
use crate::testing::init_contract_tester;
use nym_contracts_common_testing::{AdminExt, ChainOpts, ContractOpts, RandExt};
use nym_network_monitors_contract_common::ExecuteMsg;
#[test]
fn returns_current_admin() -> anyhow::Result<()> {
let mut test = init_contract_tester();
let initial_admin = test.admin_unchecked();
// initial
let res = query_admin(test.deps())?;
assert_eq!(res.admin, Some(initial_admin.to_string()));
let new_admin = test.generate_account();
// sanity check
assert_ne!(initial_admin, new_admin);
// after update
test.execute_msg(
initial_admin.clone(),
&ExecuteMsg::UpdateAdmin {
admin: new_admin.to_string(),
},
)?;
let updated_admin = query_admin(test.deps())?;
assert_eq!(updated_admin.admin, Some(new_admin.to_string()));
Ok(())
}
}
#[cfg(test)]
mod network_monitor_orchestrators_query {
use super::*;
use crate::testing::{init_contract_tester, NetworkMonitorsContractTesterExt};
use nym_contracts_common_testing::{AdminExt, ContractOpts};
use nym_network_monitors_contract_common::ExecuteMsg;
#[test]
fn returns_empty_list_when_there_are_no_extra_orchestrators() -> anyhow::Result<()> {
// make sure to start with an empty state
let mut test = init_contract_tester();
test.remove_all_orchestrators();
let res = query_network_monitor_orchestrators(test.deps())?;
assert!(res.authorised.is_empty());
Ok(())
}
#[test]
fn returns_all_authorised_orchestrators() -> anyhow::Result<()> {
// make sure to start with an empty state
let mut test = init_contract_tester();
test.remove_all_orchestrators();
let orchestrator1 = test.add_orchestrator()?;
let orchestrator2 = test.add_orchestrator()?;
let orchestrator3 = test.add_orchestrator()?;
let res = query_network_monitor_orchestrators(test.deps())?;
assert_eq!(res.authorised.len(), 3);
assert!(res.authorised.iter().any(|o| o.address == orchestrator1));
assert!(res.authorised.iter().any(|o| o.address == orchestrator2));
assert!(res.authorised.iter().any(|o| o.address == orchestrator3));
Ok(())
}
#[test]
fn does_not_return_revoked_orchestrators() -> anyhow::Result<()> {
// make sure to start with an empty state
let mut test = init_contract_tester();
test.remove_all_orchestrators();
let orchestrator1 = test.add_orchestrator()?;
let orchestrator2 = test.add_orchestrator()?;
test.execute_raw(
test.admin_unchecked(),
ExecuteMsg::RevokeNetworkMonitorOrchestrator {
address: orchestrator1.to_string(),
},
)?;
let res = query_network_monitor_orchestrators(test.deps())?;
assert!(!res.authorised.iter().any(|o| o.address == orchestrator1));
assert!(res.authorised.iter().any(|o| o.address == orchestrator2));
Ok(())
}
#[test]
fn returns_entries_in_ascending_order() -> anyhow::Result<()> {
// make sure to start with an empty state
let mut test = init_contract_tester();
test.remove_all_orchestrators();
test.add_orchestrator()?;
test.add_orchestrator()?;
test.add_orchestrator()?;
let res = query_network_monitor_orchestrators(test.deps())?;
assert!(res
.authorised
.windows(2)
.all(|window| window[0].address <= window[1].address));
Ok(())
}
}
#[cfg(test)]
mod network_monitor_agents_query {
use super::*;
use crate::testing::{
init_contract_tester, storage_socket_comp, NetworkMonitorsContract,
NetworkMonitorsContractTesterExt,
};
use nym_contracts_common_testing::{ContractOpts, ContractTester};
use std::net::SocketAddr;
fn storage_sorted_addresses(
test: &mut ContractTester<NetworkMonitorsContract>,
n: usize,
) -> Vec<SocketAddr> {
let mut ips = Vec::new();
for _ in 0..n {
ips.push(test.random_socket());
}
ips.sort_by(|a, b| storage_socket_comp(*a, *b));
ips
}
#[test]
fn returns_empty_response_when_no_agents_are_authorised() -> anyhow::Result<()> {
let test = init_contract_tester();
let res = query_network_monitor_agents(test.deps(), None, None)?;
assert!(res.authorised.is_empty());
assert_eq!(res.start_next_after, None);
Ok(())
}
#[test]
fn returns_all_authorised_agents_below_default_limit() -> anyhow::Result<()> {
let mut test = init_contract_tester();
let agents = storage_sorted_addresses(&mut test, 5);
for agent in &agents {
test.add_dummy_agent(*agent)
}
let res = query_network_monitor_agents(test.deps(), None, None)?;
assert_eq!(res.authorised.len(), agents.len());
assert_eq!(res.start_next_after, agents.last().copied());
for agent in &agents {
assert!(res.authorised.iter().any(|a| a.mixnet_address == *agent));
}
Ok(())
}
#[test]
fn respects_explicit_limit() -> anyhow::Result<()> {
let mut test = init_contract_tester();
let agents = storage_sorted_addresses(&mut test, 5);
for agent in &agents {
test.add_dummy_agent(*agent)
}
let res = query_network_monitor_agents(test.deps(), None, Some(2))?;
assert_eq!(res.authorised.len(), 2);
assert_eq!(res.authorised[0].mixnet_address, agents[0]);
assert_eq!(res.authorised[1].mixnet_address, agents[1]);
assert_eq!(res.start_next_after, Some(agents[1]));
Ok(())
}
#[test]
fn respects_start_after_for_pagination() -> anyhow::Result<()> {
let mut test = init_contract_tester();
let agents = storage_sorted_addresses(&mut test, 5);
for agent in &agents {
test.add_dummy_agent(*agent)
}
let res = query_network_monitor_agents(test.deps(), Some(agents[1]), Some(2))?;
assert_eq!(res.authorised.len(), 2);
assert_eq!(res.authorised[0].mixnet_address, agents[2]);
assert_eq!(res.authorised[1].mixnet_address, agents[3]);
assert_eq!(res.start_next_after, Some(agents[3]));
Ok(())
}
#[test]
fn caps_limit_at_maximum() -> anyhow::Result<()> {
let mut test = init_contract_tester();
let total = retrieval_limits::AGENTS_MAX_LIMIT as usize + 20;
let agents = storage_sorted_addresses(&mut test, total);
for agent in &agents {
test.add_dummy_agent(*agent)
}
let res = query_network_monitor_agents(
test.deps(),
None,
Some(retrieval_limits::AGENTS_MAX_LIMIT + 1),
)?;
assert_eq!(
res.authorised.len(),
retrieval_limits::AGENTS_MAX_LIMIT as usize
);
assert_eq!(
res.start_next_after,
Some(agents[retrieval_limits::AGENTS_MAX_LIMIT as usize - 1])
);
Ok(())
}
#[test]
fn start_next_after_is_none_for_empty_page() -> anyhow::Result<()> {
let mut test = init_contract_tester();
let agents = storage_sorted_addresses(&mut test, 3);
for agent in &agents {
test.add_dummy_agent(*agent)
}
let res = query_network_monitor_agents(test.deps(), Some(agents[2]), Some(10))?;
assert!(res.authorised.is_empty());
assert_eq!(res.start_next_after, None);
Ok(())
}
#[test]
fn returns_entries_in_ascending_order() -> anyhow::Result<()> {
let mut test = init_contract_tester();
let agents = storage_sorted_addresses(&mut test, 6);
for agent in &agents {
test.add_dummy_agent(*agent)
}
let res = query_network_monitor_agents(test.deps(), None, None)?;
assert!(res.authorised.windows(2).all(|window| storage_socket_comp(
window[0].mixnet_address,
window[1].mixnet_address
)
.is_le()));
Ok(())
}
}
}
@@ -0,0 +1,2 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,188 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::contract::{execute, instantiate, migrate, query};
use cosmwasm_std::{Addr, Order};
use nym_contracts_common_testing::{
mock_dependencies, AdminExt, ChainOpts, CommonStorageKeys, ContractFn, ContractOpts,
ContractTester, DenomExt, PermissionedFn, QueryFn, RandExt, Rng, RngCore, TestableNymContract,
};
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
use crate::storage::NetworkMonitorsStorage;
use nym_network_monitors_contract_common::constants::storage_keys;
use nym_network_monitors_contract_common::{
ExecuteMsg, InstantiateMsg, MigrateMsg, NetworkMonitorsContractError, QueryMsg,
};
pub struct NetworkMonitorsContract;
impl TestableNymContract for NetworkMonitorsContract {
const NAME: &'static str = "nym-network-monitors-contract";
type InitMsg = InstantiateMsg;
type ExecuteMsg = ExecuteMsg;
type QueryMsg = QueryMsg;
type MigrateMsg = MigrateMsg;
type ContractError = NetworkMonitorsContractError;
fn instantiate() -> ContractFn<Self::InitMsg, Self::ContractError> {
instantiate
}
fn execute() -> ContractFn<Self::ExecuteMsg, Self::ContractError> {
execute
}
fn query() -> QueryFn<Self::QueryMsg, Self::ContractError> {
query
}
fn migrate() -> PermissionedFn<Self::MigrateMsg, Self::ContractError> {
migrate
}
fn base_init_msg() -> Self::InitMsg {
let deps = mock_dependencies();
InstantiateMsg {
orchestrator_address: deps.api.addr_make("initial-dummy-orchestrator").to_string(),
}
}
}
pub fn init_contract_tester() -> ContractTester<NetworkMonitorsContract> {
NetworkMonitorsContract::init()
.with_common_storage_key(CommonStorageKeys::Admin, storage_keys::CONTRACT_ADMIN)
}
pub trait NetworkMonitorsContractTesterExt:
ContractOpts<
ExecuteMsg = ExecuteMsg,
QueryMsg = QueryMsg,
ContractError = NetworkMonitorsContractError,
> + ChainOpts
+ AdminExt
+ DenomExt
+ RandExt
{
fn add_orchestrator(&mut self) -> Result<Addr, NetworkMonitorsContractError> {
let admin = self.admin_unchecked();
let addr = self.generate_account();
self.execute_raw(
admin,
ExecuteMsg::AuthoriseNetworkMonitorOrchestrator {
address: addr.to_string(),
},
)?;
Ok(addr)
}
fn remove_all_orchestrators(&mut self) {
let orchestrators = self.all_orchestrators();
for orchestrator in orchestrators {
self.execute_raw(
self.admin_unchecked(),
ExecuteMsg::RevokeNetworkMonitorOrchestrator {
address: orchestrator.to_string(),
},
)
.unwrap();
}
}
fn add_dummy_agent(&mut self, agent: SocketAddr) {
let orchestrators = self.all_orchestrators();
let orchestrator = match orchestrators.first() {
Some(orchestrator) => orchestrator.clone(),
None => self.add_orchestrator().unwrap().clone(),
};
self.execute_raw(
orchestrator,
ExecuteMsg::AuthoriseNetworkMonitor {
mixnet_address: agent,
bs58_x25519_noise: "11111111111111111111111111111111".to_string(),
noise_version: 1,
},
)
.unwrap();
}
fn random_ipv4(&mut self) -> IpAddr {
let rng = self.raw_rng();
IpAddr::V4(Ipv4Addr::new(rng.gen(), rng.gen(), rng.gen(), rng.gen()))
}
fn random_ipv6(&mut self) -> IpAddr {
let rng = self.raw_rng();
IpAddr::V6(Ipv6Addr::new(
rng.gen(),
rng.gen(),
rng.gen(),
rng.gen(),
rng.gen(),
rng.gen(),
rng.gen(),
rng.gen(),
))
}
fn random_ip(&mut self) -> IpAddr {
let rng = self.raw_rng();
// toss a coin, if even => ipv4, if odd => ipv6
if rng.next_u32() % 2 == 0 {
self.random_ipv4()
} else {
self.random_ipv6()
}
}
fn random_socket_ipv4(&mut self) -> SocketAddr {
let port = self.raw_rng().gen();
SocketAddr::new(self.random_ipv4(), port)
}
fn random_socket_ipv6(&mut self) -> SocketAddr {
let port = self.raw_rng().gen();
SocketAddr::new(self.random_ipv6(), port)
}
fn random_socket(&mut self) -> SocketAddr {
let port = self.raw_rng().gen();
SocketAddr::new(self.random_ip(), port)
}
fn all_agents(&self) -> Vec<SocketAddr> {
NetworkMonitorsStorage::new()
.authorised_agents
.range(self.storage(), None, None, Order::Ascending)
.map(|record| record.unwrap().1.mixnet_address)
.collect()
}
fn all_orchestrators(&self) -> Vec<Addr> {
NetworkMonitorsStorage::new()
.authorised_orchestrators
.range(self.storage(), None, None, Order::Ascending)
.map(|record| record.unwrap().0)
.collect()
}
}
impl NetworkMonitorsContractTesterExt for ContractTester<NetworkMonitorsContract> {}
/// Compare SocketAddrs in the same order as the storage key encoding.
///
/// Storage keys are: `[0, ip_len] [ip_octets...] [port_be_bytes]`
/// This means IPv4 (len=4) always sorts before IPv6 (len=16),
/// within the same type keys sort by IP octets then by port.
pub(crate) fn storage_socket_comp(a: SocketAddr, b: SocketAddr) -> std::cmp::Ordering {
let ip_ord = match (a.ip(), b.ip()) {
(IpAddr::V4(a), IpAddr::V4(b)) => a.octets().cmp(&b.octets()),
(IpAddr::V6(a), IpAddr::V6(b)) => a.octets().cmp(&b.octets()),
// length prefix [0, 4] < [0, 16] so all IPv4 sorts before all IPv6
(IpAddr::V4(_), IpAddr::V6(_)) => std::cmp::Ordering::Less,
(IpAddr::V6(_), IpAddr::V4(_)) => std::cmp::Ordering::Greater,
};
ip_ord.then(a.port().cmp(&b.port()))
}
File diff suppressed because it is too large Load Diff
@@ -5,7 +5,7 @@
},
"mixmining_reserve": {
"denom": "unym",
"amount": "168575020719057"
"amount": "166613455567357"
},
"vesting_tokens": {
"denom": "unym",
@@ -13,6 +13,6 @@
},
"circulating_supply": {
"denom": "unym",
"amount": "831424979280943"
"amount": "833386544432643"
}
}
@@ -1,6 +1,6 @@
{
"nodes": 752,
"locations": 76,
"mixnodes": 273,
"exit_gateways": 470
"nodes": 745,
"locations": 77,
"mixnodes": 264,
"exit_gateways": 474
}
@@ -1 +1 @@
831_424_979
833_386_544
@@ -1 +1 @@
61_050_638
61_194_673

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