Compare commits

..

8 Commits

Author SHA1 Message Date
Tommy Verrall c4046a256f Merge remote-tracking branch 'origin/feature/wallet-investigation-next' into feature/wallet-investigation-next 2026-05-06 14:57:35 +02:00
Tommy Verrall f54a6a3a05 Fixing the logging to ouput debug window
- Route-scoped refresh
- Mock TDelegationContext
- notistack for log copy errors
- main-thread Tauri emit for log viewer events.
2026-05-06 14:57:18 +02:00
Tommy Verrall a5fc879c4b Delegation query cache, log UX, log webview telemetry
- TanStack Query for delegation summary (5m stale, fetch on /delegation only)
- Context exposes isFetching, isError, lastUpdatedAtMs; list summaries use isFetching
- Delegation page: last updated label, error alert, stable 5m poll via ref
- LogViewer: Copy latest/all, Clear list (clipboard plugin)
- Log window: on_page_load Started/Finished logs; NYM_WALLET_LOG_WEBVIEW_DEVTOOLS=1 opens devtools
- Comment linking programmatic use_https_scheme to tauri.conf
2026-05-06 14:57:18 +02:00
Tommy Verrall 303ec36f54 Use HTTPS asset scheme for programmatic webviews
Tauri defaults WebviewWindowBuilder to use_https_scheme=false while
tauri.conf uses useHttpsScheme=true for the main window. Secondary
windows (logs, auth/main swap) then load the asset protocol on a
different origin; scripts can fail to run, leaving a blank webview and
broken UI. Align with app config.
2026-05-06 14:57:04 +02:00
Tommy Verrall 330a300710 Fixing the logging to ouput debug window
- Route-scoped refresh
- Mock TDelegationContext
- notistack for log copy errors
- main-thread Tauri emit for log viewer events.
2026-05-06 14:55:56 +02:00
Tommy Verrall f3f927bdbb feat(nym-wallet): delegation query cache, log UX, log webview telemetry
- TanStack Query for delegation summary (5m stale, fetch on /delegation only)
- Context exposes isFetching, isError, lastUpdatedAtMs; list summaries use isFetching
- Delegation page: last updated label, error alert, stable 5m poll via ref
- LogViewer: Copy latest/all, Clear list (clipboard plugin)
- Log window: on_page_load Started/Finished logs; NYM_WALLET_LOG_WEBVIEW_DEVTOOLS=1 opens devtools
- Comment linking programmatic use_https_scheme to tauri.conf
2026-05-06 14:32:12 +02:00
Tommy Verrall 69ed018ffc fix(nym-wallet): use HTTPS asset scheme for programmatic webviews
Tauri defaults WebviewWindowBuilder to use_https_scheme=false while
tauri.conf uses useHttpsScheme=true for the main window. Secondary
windows (logs, auth/main swap) then load the asset protocol on a
different origin; scripts can fail to run, leaving a blank webview and
broken UI. Align with app config.
2026-05-06 14:23:13 +02:00
Tommy Verrall e8fe6a29c0 Log window UX and delegation route cache
- Log viewer: empty-state copy, Close button, Advanced settings note
- Keep delegation/rewards context at app route level; refetch on route
  with background refresh when cached; reset on logout address change
- Avoid duplicate refresh on load vs confirmation modal (refresh on modal close)
2026-05-06 14:19:05 +02:00
293 changed files with 2886 additions and 20772 deletions
@@ -1,63 +0,0 @@
name: ci-build-upload-network-monitor-agent
on:
workflow_dispatch:
jobs:
build-and-upload:
strategy:
fail-fast: false
matrix:
platform: [arc-ubuntu-22.04]
runs-on: ${{ matrix.platform }}
env:
CARGO_TERM_COLOR: always
RUSTUP_PERMIT_COPY_RENAME: 1
steps:
- uses: actions/checkout@v6
- name: Prepare build output directory
shell: bash
env:
OUTPUT_DIR: ci-builds/${{ github.ref_name }}
run: |
rm -rf ci-builds || true
mkdir -p "$OUTPUT_DIR"
- name: Install Dependencies (Linux)
run: sudo apt-get update && sudo apt-get -y install libudev-dev
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@master
with:
toolchain: ${{ vars.REQUIRED_RUSTC_VERSION }}
- name: Build nym-network-monitor-agent
shell: bash
run: cargo build -p nym-network-monitor-agent --release
- name: Upload artifact
uses: actions/upload-artifact@v6
with:
name: nym-network-monitor-agent
path: target/release/nym-network-monitor-agent
retention-days: 30
- name: Prepare build output
shell: bash
env:
OUTPUT_DIR: ci-builds/${{ github.ref_name }}
run: cp target/release/nym-network-monitor-agent "$OUTPUT_DIR"
- name: Deploy to CI www
uses: easingthemes/ssh-deploy@main
env:
SSH_PRIVATE_KEY: ${{ secrets.CI_WWW_SSH_PRIVATE_KEY }}
ARGS: "-avzr"
SOURCE: "ci-builds/"
REMOTE_HOST: ${{ secrets.CI_WWW_REMOTE_HOST }}
REMOTE_USER: ${{ secrets.CI_WWW_REMOTE_USER }}
TARGET: ${{ secrets.CI_WWW_REMOTE_TARGET }}/builds/
EXCLUDE: "/dist/, /node_modules/"
+6 -16
View File
@@ -19,7 +19,6 @@ jobs:
RUSTUP_PERMIT_COPY_RENAME: 1
permissions:
contents: write
pull-requests: write
steps:
- name: Checkout repo
uses: actions/checkout@v6
@@ -67,20 +66,11 @@ jobs:
--no-git-commit \
--yes
- name: Create pull request
uses: peter-evans/create-pull-request@v7
with:
token: ${{ secrets.GITHUB_TOKEN }}
branch: "chore/bump-version-${{ inputs.version }}"
base: ${{ github.ref_name }}
commit-message: "crates release: bump version to ${{ inputs.version }}"
title: "chore: bump crate versions to ${{ inputs.version }}"
body: |
Automated version bump from `${{ steps.current_version.outputs.version }}` → `${{ inputs.version }}`.
Triggered by @${{ github.actor }} via workflow dispatch.
labels: "automated, crates-version-bump"
delete-branch: true
- name: Commit and push version bump
run: |
git add -A
git commit -m "crates release: bump version to ${{ inputs.version }}"
git push
- name: Show package versions
run: cargo workspaces list --long
run: cargo workspaces list --long
+8 -27
View File
@@ -26,9 +26,6 @@ jobs:
outputs:
release_tag: ${{ github.ref_name }}
env:
SIGN_WINDOWS: ${{ github.event_name == 'release' || inputs.sign }}
steps:
- uses: actions/checkout@v6
@@ -51,39 +48,23 @@ jobs:
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: env.SIGN_WINDOWS == 'true'
if: ${{ inputs.sign }}
shell: bash
run: |
curl -L0 https://www.ssl.com/download/codesigntool-for-linux-and-macos/ -o codesigntool.zip
unzip codesigntool.zip
- name: Get EV certificate credential id
working-directory: nym-wallet/src-tauri
if: env.SIGN_WINDOWS == 'true'
if: ${{ inputs.sign }}
id: get_credential_ids
shell: bash
run: |
echo "SSL_COM_CREDENTIAL_ID=$(./CodeSignTool.sh get_credential_ids -username=${{ secrets.SSL_COM_USERNAME }} -password=${{ secrets.SSL_COM_PASSWORD }} | sed -n '1!p' | sed 's/- //')" >> "$GITHUB_OUTPUT"
- name: Add custom sign command to tauri.conf.json
working-directory: nym-wallet/src-tauri
if: env.SIGN_WINDOWS == 'true'
if: ${{ inputs.sign }}
shell: bash
env:
SSL_SIGN_USER: ${{ secrets.SSL_COM_USERNAME }}
@@ -130,10 +111,10 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
SSL_COM_USERNAME: ${{ env.SIGN_WINDOWS == 'true' && secrets.SSL_COM_USERNAME }}
SSL_COM_PASSWORD: ${{ env.SIGN_WINDOWS == 'true' && secrets.SSL_COM_PASSWORD }}
SSL_COM_CREDENTIAL_ID: ${{ env.SIGN_WINDOWS == 'true' && steps.get_credential_ids.outputs.SSL_COM_CREDENTIAL_ID }}
SSL_COM_TOTP_SECRET: ${{ env.SIGN_WINDOWS == 'true' && secrets.SSL_COM_TOTP_SECRET }}
SSL_COM_USERNAME: ${{ inputs.sign && secrets.SSL_COM_USERNAME }}
SSL_COM_PASSWORD: ${{ inputs.sign && secrets.SSL_COM_PASSWORD }}
SSL_COM_CREDENTIAL_ID: ${{ inputs.sign && steps.get_credential_ids.outputs.SSL_COM_CREDENTIAL_ID }}
SSL_COM_TOTP_SECRET: ${{ inputs.sign && secrets.SSL_COM_TOTP_SECRET }}
run: |
echo "Starting build process..."
yarn build
@@ -204,4 +185,4 @@ jobs:
needs: publish-tauri
with:
release_tag: ${{ needs.publish-tauri.outputs.release_tag || github.ref_name }}
secrets: inherit
secrets: inherit
-46
View File
@@ -4,52 +4,6 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
## [Unreleased]
## [2026.10-waterloo] (2026-05-27)
- Re-order default API urls for network details - Waterloo release ([#6799])
- [bugfix] IPR v8<->v9 mismatch on Waterloo ([#6772])
- Migrate to hickory 0.26.1 ([#6751])
- add workflows for NM3 ([#6729])
- credential proxy pool ([#6726])
- chore: made sphinx version threshold assertion a compile time check ([#6718])
- Feat/nmv3 updated performance calculation ([#6714])
- feat: NMv3: submission of stress testing result into nym-api ([#6709])
- feat: NMv3: Prometheus metrics for network monitor ([#6693])
- feat: NMv3: add read-only results API to orchestrator ([#6689])
- feat: NMv3: Eviction of stale testrun data ([#6685])
- feat: NMv3: Wire up testrun assignment and result submission flow ([#6680])
- feat: NMv3: Support multiple network monitor agents per host ([#6679])
- Feat/nmv3 agent announcement ([#6673])
- add node refresher for periodic scraping of bonded nym-node details ([#6626])
- Feat/nmv3 orchestrator queue ([#6597])
- feat: network monitor agent - standalone node stress-testing ([#6582])
- [feat] propagate NM agent noise keys to nym-node routing ([#6577])
- start mix stress testing topic branch ([#6575])
- Feat/nmv3 agents subscription ([#6567])
- Feat/nmv3 agents contract ([#6555])
[#6799]: https://github.com/nymtech/nym/pull/6799
[#6772]: https://github.com/nymtech/nym/pull/6772
[#6751]: https://github.com/nymtech/nym/pull/6751
[#6729]: https://github.com/nymtech/nym/pull/6729
[#6726]: https://github.com/nymtech/nym/pull/6726
[#6718]: https://github.com/nymtech/nym/pull/6718
[#6714]: https://github.com/nymtech/nym/pull/6714
[#6709]: https://github.com/nymtech/nym/pull/6709
[#6693]: https://github.com/nymtech/nym/pull/6693
[#6689]: https://github.com/nymtech/nym/pull/6689
[#6685]: https://github.com/nymtech/nym/pull/6685
[#6680]: https://github.com/nymtech/nym/pull/6680
[#6679]: https://github.com/nymtech/nym/pull/6679
[#6673]: https://github.com/nymtech/nym/pull/6673
[#6626]: https://github.com/nymtech/nym/pull/6626
[#6597]: https://github.com/nymtech/nym/pull/6597
[#6582]: https://github.com/nymtech/nym/pull/6582
[#6577]: https://github.com/nymtech/nym/pull/6577
[#6575]: https://github.com/nymtech/nym/pull/6575
[#6567]: https://github.com/nymtech/nym/pull/6567
[#6555]: https://github.com/nymtech/nym/pull/6555
## [2026.9-venaco] (2026-05-06)
- Fix for v9 IPR ([#6710])
Generated
+50 -302
View File
@@ -57,7 +57,7 @@ checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
dependencies = [
"cfg-if",
"cipher",
"cpufeatures 0.2.17",
"cpufeatures",
]
[[package]]
@@ -253,7 +253,7 @@ checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072"
dependencies = [
"base64ct",
"blake2 0.10.6",
"cpufeatures 0.2.17",
"cpufeatures",
"password-hash",
]
@@ -1309,18 +1309,7 @@ checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818"
dependencies = [
"cfg-if",
"cipher",
"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",
"cpufeatures",
]
[[package]]
@@ -1330,7 +1319,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35"
dependencies = [
"aead",
"chacha20 0.9.1",
"chacha20",
"cipher",
"poly1305",
"zeroize",
@@ -1842,15 +1831,6 @@ 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"
@@ -2115,7 +2095,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be"
dependencies = [
"cfg-if",
"cpufeatures 0.2.17",
"cpufeatures",
"curve25519-dalek-derive",
"digest 0.10.7",
"fiat-crypto",
@@ -2819,6 +2799,18 @@ 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"
@@ -3279,7 +3271,6 @@ dependencies = [
"cfg-if",
"libc",
"r-efi",
"rand_core 0.10.1",
"wasip2",
"wasip3",
]
@@ -3639,25 +3630,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ebdb29d2ea9ed0083cd8cece49bbd968021bd99b0849edb4a9a7ee0fdf6a4e0"
[[package]]
name = "hickory-net"
version = "0.26.1"
name = "hickory-proto"
version = "0.25.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2295ed2f9c31e471e1428a8f88a3f0e1f4b27c15049592138d1eebe9c35b183"
checksum = "f8a6fe56c0038198998a6f217ca4e7ef3a5e51f46163bd6dd60b5c71ca6c6502"
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",
"jni 0.22.4",
"rand 0.10.1",
"once_cell",
"rand 0.9.2",
"ring",
"rustls 0.23.37",
"thiserror 2.0.12",
"tinyvec",
@@ -3665,56 +3657,31 @@ dependencies = [
"tokio-rustls 0.26.2",
"tracing",
"url",
"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",
"webpki-roots 0.26.11",
]
[[package]]
name = "hickory-resolver"
version = "0.26.1"
version = "0.25.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0d58d28879ceecde6607729660c2667a081ccdc082e082675042793960f178c"
checksum = "dc62a9a99b0bfb44d2ab95a7208ac952d31060efc16241c87eaf36406fecf87a"
dependencies = [
"cfg-if",
"futures-util",
"hickory-net",
"hickory-proto",
"ipconfig",
"ipnet",
"jni 0.22.4",
"moka",
"ndk-context",
"once_cell",
"parking_lot",
"rand 0.10.1",
"rand 0.9.2",
"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 1.0.2",
"webpki-roots 0.26.11",
]
[[package]]
@@ -4442,9 +4409,6 @@ name = "ipnet"
version = "2.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130"
dependencies = [
"serde",
]
[[package]]
name = "ipnetwork"
@@ -4539,68 +4503,19 @@ dependencies = [
"cesu8",
"cfg-if",
"combine",
"jni-sys 0.3.0",
"jni-sys",
"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"
@@ -4667,7 +4582,7 @@ version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653"
dependencies = [
"cpufeatures 0.2.17",
"cpufeatures",
]
[[package]]
@@ -5444,12 +5359,6 @@ 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"
@@ -5726,7 +5635,7 @@ dependencies = [
[[package]]
name = "nym-api"
version = "1.1.80"
version = "1.1.79"
dependencies = [
"anyhow",
"async-trait",
@@ -5971,7 +5880,7 @@ dependencies = [
[[package]]
name = "nym-cli"
version = "1.1.77"
version = "1.1.76"
dependencies = [
"anyhow",
"base64 0.22.1",
@@ -6054,7 +5963,7 @@ dependencies = [
[[package]]
name = "nym-client"
version = "1.1.77"
version = "1.1.76"
dependencies = [
"bs58",
"clap",
@@ -7418,6 +7327,7 @@ version = "1.20.4"
dependencies = [
"cargo_metadata 0.19.2",
"dotenvy",
"log",
"regex",
"schemars 0.8.22",
"serde",
@@ -7462,103 +7372,9 @@ 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.78"
version = "1.1.77"
dependencies = [
"addr",
"anyhow",
@@ -7608,7 +7424,7 @@ dependencies = [
[[package]]
name = "nym-node"
version = "1.32.1"
version = "1.31.0"
dependencies = [
"anyhow",
"arc-swap",
@@ -7677,7 +7493,6 @@ dependencies = [
"nym-verloc",
"nym-wireguard",
"nym-wireguard-types",
"nyxd-scraper-shared",
"opentelemetry",
"opentelemetry_sdk",
"rand 0.8.5",
@@ -7728,7 +7543,6 @@ dependencies = [
"nym-exit-policy",
"nym-http-api-client",
"nym-kkt-ciphersuite",
"nym-network-defaults",
"nym-noise-keys",
"nym-test-utils",
"nym-upgrade-mode-check",
@@ -7747,7 +7561,7 @@ dependencies = [
[[package]]
name = "nym-node-status-agent"
version = "2.0.1-rc3"
version = "2.0.0"
dependencies = [
"anyhow",
"clap",
@@ -7768,7 +7582,7 @@ dependencies = [
[[package]]
name = "nym-node-status-api"
version = "4.6.2-rc7"
version = "4.6.1"
dependencies = [
"ammonia",
"anyhow",
@@ -7955,7 +7769,7 @@ name = "nym-outfox"
version = "1.20.4"
dependencies = [
"blake3",
"chacha20 0.9.1",
"chacha20",
"chacha20poly1305",
"fastrand",
"sphinx-packet",
@@ -8162,7 +7976,7 @@ dependencies = [
[[package]]
name = "nym-socks5-client"
version = "1.1.77"
version = "1.1.76"
dependencies = [
"bs58",
"clap",
@@ -8667,7 +8481,6 @@ 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",
@@ -8961,7 +8774,7 @@ dependencies = [
[[package]]
name = "nymvisor"
version = "0.1.42"
version = "0.1.41"
dependencies = [
"anyhow",
"bytes",
@@ -9638,7 +9451,7 @@ version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf"
dependencies = [
"cpufeatures 0.2.17",
"cpufeatures",
"opaque-debug 0.3.1",
"universal-hash",
]
@@ -9650,7 +9463,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25"
dependencies = [
"cfg-if",
"cpufeatures 0.2.17",
"cpufeatures",
"opaque-debug 0.3.1",
"universal-hash",
]
@@ -9729,17 +9542,6 @@ 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"
@@ -10037,17 +9839,6 @@ 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"
@@ -10086,12 +9877,6 @@ 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"
@@ -10221,7 +10006,7 @@ dependencies = [
"serde_json",
"serde_urlencoded",
"sync_wrapper 0.1.2",
"system-configuration 0.5.1",
"system-configuration",
"tokio",
"tokio-rustls 0.24.1",
"tower-service",
@@ -10644,7 +10429,7 @@ checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784"
dependencies = [
"core-foundation 0.10.1",
"core-foundation-sys",
"jni 0.21.1",
"jni",
"log",
"once_cell",
"rustls 0.23.37",
@@ -11231,7 +11016,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
dependencies = [
"cfg-if",
"cpufeatures 0.2.17",
"cpufeatures",
"digest 0.10.7",
]
@@ -11243,7 +11028,7 @@ checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800"
dependencies = [
"block-buffer 0.9.0",
"cfg-if",
"cpufeatures 0.2.17",
"cpufeatures",
"digest 0.9.0",
"opaque-debug 0.3.1",
]
@@ -11255,7 +11040,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
dependencies = [
"cfg-if",
"cpufeatures 0.2.17",
"cpufeatures",
"digest 0.10.7",
]
@@ -11336,22 +11121,6 @@ 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"
@@ -11953,18 +11722,7 @@ checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7"
dependencies = [
"bitflags 1.3.2",
"core-foundation 0.9.4",
"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",
"system-configuration-sys",
]
[[package]]
@@ -11977,16 +11735,6 @@ 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"
+3 -13
View File
@@ -44,7 +44,6 @@ members = [
"common/cosmwasm-smart-contracts/nym-performance-contract",
"common/cosmwasm-smart-contracts/nym-pool-contract",
"common/cosmwasm-smart-contracts/vesting-contract",
"common/cosmwasm-smart-contracts/network-monitors-contract",
"common/credential-proxy",
"common/credential-storage",
"common/credential-utils",
@@ -177,8 +176,6 @@ members = [
"integration-tests",
"common/nym-kkt-ciphersuite",
"common/nym-kkt-context",
"nym-network-monitor-v3/nym-network-monitor-orchestrator",
"nym-network-monitor-v3/nym-network-monitor-agent", "nym-network-monitor-v3/nym-network-monitor-orchestrator-requests",
]
default-members = [
@@ -195,8 +192,6 @@ default-members = [
"service-providers/network-requester",
"tools/nymvisor",
"nym-registration-client",
"nym-network-monitor-v3/nym-network-monitor-orchestrator",
"nym-network-monitor-v3/nym-network-monitor-agent",
"tools/internal/localnet-orchestrator"
]
@@ -285,8 +280,8 @@ getrandom03 = { package = "getrandom", version = "=0.3.3" }
glob = "0.3"
handlebars = "3.5.5"
hex = "0.4.3"
hickory-proto = "0.26.1"
hickory-resolver = "0.26.1"
hickory-proto = "0.25.2"
hickory-resolver = "0.25.2"
hkdf = "0.12.3"
hmac = "0.12.1"
http = "1"
@@ -407,10 +402,6 @@ 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"
@@ -420,7 +411,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.6"
libcrux-traits = "0.0.8"
# Workspace dep definitions required by crates.io publication - we need a workspace version since `cargo workspaces` doesn't work with path imports from crate manifests
nym-api-requests = { version = "1.20.4", path = "nym-api/nym-api-requests" }
@@ -516,7 +507,6 @@ nym-types = { version = "1.20.4", path = "common/types" }
nym-upgrade-mode-check = { version = "1.20.4", path = "common/upgrade-mode-check" }
nym-validator-client = { version = "1.20.4", path = "common/client-libs/validator-client", default-features = false }
nym-vesting-contract-common = { version = "1.20.4", path = "common/cosmwasm-smart-contracts/vesting-contract" }
nym-network-monitors-contract-common = { version = "1.20.4", path = "common/cosmwasm-smart-contracts/network-monitors-contract" }
nym-verloc = { version = "1.20.4", path = "common/verloc" }
nym-wireguard = { version = "1.20.4", path = "common/wireguard" }
nym-wireguard-types = { version = "1.20.4", path = "common/wireguard-types" }
+1 -1
View File
@@ -1,7 +1,7 @@
[package]
name = "nym-client"
description = "Implementation of the Nym Client"
version = "1.1.77"
version = "1.1.76"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>", "Jędrzej Stuczyński <andrew@nymtech.net>"]
edition = "2021"
license.workspace = true
+1 -1
View File
@@ -1,7 +1,7 @@
[package]
name = "nym-socks5-client"
description = "A SOCKS5 localhost proxy that converts incoming messages to Sphinx and sends them to a Nym address"
version = "1.1.77"
version = "1.1.76"
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_client_packet_without_delay(packet)
.forward_packet(packet)
.map_err(erase_err)
}
}
@@ -34,4 +34,3 @@ client = ["tokio-util", "nym-task", "tokio/net", "tokio/rt"]
[dev-dependencies]
nym-crypto = { workspace = true }
rand = { workspace = true }
tokio = { workspace = true, features = ["macros", "io-util", "rt", "rt-multi-thread"] }
+72 -280
View File
@@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
use dashmap::DashMap;
use futures::{SinkExt, StreamExt};
use futures::StreamExt;
use nym_noise::config::NoiseConfig;
use nym_noise::upgrade_noise_initiator;
use nym_sphinx::forwarding::packet::MixPacket;
@@ -14,7 +14,6 @@ use std::ops::Deref;
use std::sync::atomic::{AtomicU32, AtomicUsize, Ordering};
use std::sync::Arc;
use std::time::Duration;
use tokio::io::{AsyncRead, AsyncWrite};
use tokio::net::TcpStream;
use tokio::sync::mpsc;
use tokio::sync::mpsc::error::TrySendError;
@@ -91,17 +90,13 @@ impl Deref for ActiveConnections {
pub struct ConnectionSender {
channel: mpsc::Sender<FramedNymPacket>,
current_reconnection_attempt: Arc<AtomicU32>,
// Identifies the `ManagedConnection` task currently owning this entry; used
// to ensure drop-time eviction only fires on the still-owning task.
handle_token: Arc<()>,
}
impl ConnectionSender {
fn new(channel: mpsc::Sender<FramedNymPacket>, handle_token: Arc<()>) -> Self {
fn new(channel: mpsc::Sender<FramedNymPacket>) -> Self {
ConnectionSender {
channel,
current_reconnection_attempt: Arc::new(AtomicU32::new(0)),
handle_token,
}
}
}
@@ -112,31 +107,6 @@ struct ManagedConnection {
message_receiver: ReceiverStream<FramedNymPacket>,
connection_timeout: Duration,
current_reconnection: Arc<AtomicU32>,
active_connections: ActiveConnections,
handle_token: Arc<()>,
}
// Evicts the cache entry on task exit (only if still owned by this task).
// Without this, a stale `ConnectionSender` survives after the peer disconnects
// and the next outbound packet is silently swallowed by the dead TCP.
struct EvictOnDrop {
active_connections: ActiveConnections,
address: SocketAddr,
handle_token: Arc<()>,
}
impl Drop for EvictOnDrop {
fn drop(&mut self) {
let address = self.address;
let handle_token = &self.handle_token;
self.active_connections.remove_if(&address, |_, sender| {
Arc::ptr_eq(&sender.handle_token, handle_token)
});
trace!(
peer = %address,
"managed connection task exited; evicted owning cache entry"
);
}
}
impl ManagedConnection {
@@ -146,8 +116,6 @@ impl ManagedConnection {
message_receiver: mpsc::Receiver<FramedNymPacket>,
connection_timeout: Duration,
current_reconnection: Arc<AtomicU32>,
active_connections: ActiveConnections,
handle_token: Arc<()>,
) -> Self {
ManagedConnection {
address,
@@ -155,30 +123,72 @@ impl ManagedConnection {
message_receiver: ReceiverStream::new(message_receiver),
connection_timeout,
current_reconnection,
active_connections,
handle_token,
}
}
async fn run(self) {
let address = self.address;
let _evict_guard = EvictOnDrop {
active_connections: self.active_connections,
address,
handle_token: self.handle_token,
};
let reconnection_attempt = self.current_reconnection.load(Ordering::Acquire);
let connect_start = tokio::time::Instant::now();
let connection_fut = TcpStream::connect(address);
// 1. attempt to establish the connection with timeout
let maybe_stream = match tokio::time::timeout(self.connection_timeout, connection_fut).await
{
Ok(stream) => stream,
let conn = match tokio::time::timeout(self.connection_timeout, connection_fut).await {
Ok(stream_res) => match stream_res {
Ok(stream) => {
let connect_ms = connect_start.elapsed().as_millis() as u64;
debug!(
peer = %address,
connect_ms,
"Managed to establish connection to {}", self.address
);
let noise_start = tokio::time::Instant::now();
let noise_stream =
match upgrade_noise_initiator(stream, &self.noise_config).await {
Ok(noise_stream) => noise_stream,
Err(err) => {
let noise_handshake_ms = noise_start.elapsed().as_millis() as u64;
warn!(
event = "connection.failed.noise",
peer = %address,
error = %err,
connect_ms,
noise_handshake_ms,
reconnection_attempt,
exit_reason = "noise_error",
"Failed to perform Noise initiator handshake with {address}"
);
self.current_reconnection.fetch_add(1, Ordering::SeqCst);
return;
}
};
let noise_handshake_ms = noise_start.elapsed().as_millis() as u64;
self.current_reconnection.store(0, Ordering::Release);
debug!(
peer = %address,
connect_ms,
noise_handshake_ms,
"Noise initiator handshake completed for {:?}", address
);
Framed::new(noise_stream, NymCodec)
}
Err(err) => {
let connect_ms = connect_start.elapsed().as_millis() as u64;
warn!(
event = "connection.failed.connect",
peer = %address,
error = %err,
connect_ms,
reconnection_attempt,
exit_reason = "connect_error",
"failed to establish connection to {address}"
);
return;
}
},
Err(_) => {
let connect_ms = connect_start.elapsed().as_millis() as u64;
debug!(
warn!(
event = "connection.failed.timeout",
peer = %address,
timeout_ms = self.connection_timeout.as_millis() as u64,
@@ -193,133 +203,21 @@ impl ManagedConnection {
}
};
// 2. check if it actually succeeded
let stream = match maybe_stream {
Ok(stream) => stream,
Err(err) => {
let connect_ms = connect_start.elapsed().as_millis() as u64;
debug!(
event = "connection.failed.connect",
peer = %address,
error = %err,
connect_ms,
reconnection_attempt,
exit_reason = "connect_error",
"failed to establish connection to {address}"
);
return;
}
};
let connect_ms = connect_start.elapsed().as_millis() as u64;
debug!(
peer = %address,
connect_ms,
"Managed to establish connection to {}", self.address
);
// 3. perform noise handshake (if applicable)
let noise_start = tokio::time::Instant::now();
let noise_stream = match upgrade_noise_initiator(stream, &self.noise_config).await {
Ok(noise_stream) => noise_stream,
Err(err) => {
let noise_handshake_ms = noise_start.elapsed().as_millis() as u64;
debug!(
event = "connection.failed.noise",
peer = %address,
error = %err,
connect_ms,
noise_handshake_ms,
reconnection_attempt,
exit_reason = "noise_error",
"Failed to perform Noise initiator handshake with {address}"
);
self.current_reconnection.fetch_add(1, Ordering::SeqCst);
return;
}
};
let noise_handshake_ms = noise_start.elapsed().as_millis() as u64;
self.current_reconnection.store(0, Ordering::Release);
debug!(
peer = %address,
connect_ms,
noise_handshake_ms,
"Noise initiator handshake completed for {:?}", address
);
let conn = Framed::new(noise_stream, NymCodec);
// 4. start handling the framed stream
run_io_loop(conn, self.message_receiver, address).await;
}
}
// The connection is unidirectional (send-only); we read from it solely to
// notice peer FIN/RST while idle so we can evict the cache entry before the
// next outbound send finds it stale.
async fn run_io_loop<T>(
conn: Framed<T, NymCodec>,
mut receiver: ReceiverStream<FramedNymPacket>,
address: SocketAddr,
) where
T: AsyncRead + AsyncWrite + Unpin,
{
let (mut sink, mut stream) = conn.split();
loop {
tokio::select! {
msg = stream.next() => {
match msg {
None => {
debug!(
peer = %address,
exit_reason = "peer_closed",
"peer closed mixnet connection to {address}"
);
break;
}
Some(Err(err)) => {
debug!(
event = "connection.read_error",
peer = %address,
error = %err,
exit_reason = "read_error",
"read error on mixnet connection to {address}: {err}"
);
break;
}
Some(Ok(_)) => {
trace!(
peer = %address,
"unexpected inbound packet on mixnet connection to {address}; discarding"
);
}
}
}
outgoing = receiver.next() => {
match outgoing {
None => {
debug!(
peer = %address,
exit_reason = "sender_dropped",
"connection manager to {address} finished"
);
break;
}
Some(packet) => {
if let Err(err) = sink.send(packet).await {
debug!(
event = "connection.forward_error",
peer = %address,
error = %err,
exit_reason = "forward_error",
"Failed to forward packet to {address}: {err}"
);
break;
}
}
}
}
if let Err(err) = self.message_receiver.map(Ok).forward(conn).await {
warn!(
event = "connection.forward_error",
peer = %address,
error = %err,
exit_reason = "forward_error",
"Failed to forward packets to {address}: {err}"
);
}
debug!(
peer = %address,
exit_reason = "sender_dropped",
"connection manager to {address} finished"
);
}
}
@@ -366,18 +264,13 @@ impl Client {
sender.try_send(pending_packet).unwrap();
}
// Ownership token for the task we're about to spawn; lets it tell
// on exit whether the cache entry still names it.
let handle_token = Arc::new(());
// if we already tried to connect to `address` before, grab the current attempt count
let current_reconnection_attempt =
if let Some(mut existing) = self.active_connections.get_mut(&address) {
existing.channel = sender;
existing.handle_token = Arc::clone(&handle_token);
Arc::clone(&existing.current_reconnection_attempt)
} else {
let new_entry = ConnectionSender::new(sender, Arc::clone(&handle_token));
let new_entry = ConnectionSender::new(sender);
let current_attempt = Arc::clone(&new_entry.current_reconnection_attempt);
self.active_connections.insert(address, new_entry);
current_attempt
@@ -392,7 +285,6 @@ impl Client {
let connections_count = self.connections_count.clone();
let noise_config = self.noise_config.clone();
let active_connections = self.active_connections.clone();
tokio::spawn(async move {
// before executing the manager, wait for what was specified, if anything
if let Some(backoff) = backoff {
@@ -407,8 +299,6 @@ impl Client {
receiver,
initial_connection_timeout,
current_reconnection_attempt,
active_connections,
handle_token,
)
.run()
.await;
@@ -538,102 +428,4 @@ mod tests {
client.config.maximum_reconnection_backoff
);
}
fn test_addr() -> SocketAddr {
"127.0.0.1:1".parse().unwrap()
}
fn insert_with_token(
active: &ActiveConnections,
addr: SocketAddr,
token: Arc<()>,
) -> mpsc::Receiver<FramedNymPacket> {
let (tx, rx) = mpsc::channel(1);
active.insert(addr, ConnectionSender::new(tx, token));
rx
}
#[test]
fn evict_on_drop_removes_entry_when_token_still_matches() {
let active = ActiveConnections::default();
let addr = test_addr();
let token = Arc::new(());
let _rx = insert_with_token(&active, addr, Arc::clone(&token));
assert!(active.get(&addr).is_some());
{
let _guard = EvictOnDrop {
active_connections: active.clone(),
address: addr,
handle_token: token,
};
}
assert!(
active.get(&addr).is_none(),
"owning task's drop should evict the entry"
);
}
#[test]
fn evict_on_drop_preserves_entry_replaced_by_newer_make_connection() {
// Simulates the race: old task's run() has returned, but before its
// drop guard fires, a concurrent `make_connection` replaced the
// entry's channel + handle_token with a fresh task's token.
let active = ActiveConnections::default();
let addr = test_addr();
let old_token = Arc::new(());
let new_token = Arc::new(());
let _rx_new = insert_with_token(&active, addr, Arc::clone(&new_token));
{
let _guard = EvictOnDrop {
active_connections: active.clone(),
address: addr,
handle_token: old_token,
};
}
assert!(
active.get(&addr).is_some(),
"old task's drop must not clobber the newer entry"
);
}
#[tokio::test]
async fn io_loop_exits_when_peer_closes_idle_connection() {
// The fix's second half: while no packets are flowing, peer FIN/RST
// must still be observed so the cache entry can be evicted before the
// next send finds it stale.
let (a, b) = tokio::io::duplex(64);
let conn = Framed::new(a, NymCodec);
let (_tx, rx) = mpsc::channel(1);
let task = tokio::spawn(run_io_loop(conn, ReceiverStream::new(rx), test_addr()));
// Simulate peer closing both directions of the connection.
drop(b);
tokio::time::timeout(Duration::from_secs(1), task)
.await
.expect("io_loop must notice peer close while idle")
.expect("io_loop task must not panic");
}
#[tokio::test]
async fn io_loop_exits_when_sender_dropped() {
let (a, _b) = tokio::io::duplex(64);
let conn = Framed::new(a, NymCodec);
let (tx, rx) = mpsc::channel(1);
let task = tokio::spawn(run_io_loop(conn, ReceiverStream::new(rx), test_addr()));
drop(tx);
tokio::time::timeout(Duration::from_secs(1), task)
.await
.expect("io_loop must exit when the upstream sender is dropped")
.expect("io_loop task must not panic");
}
}
@@ -21,16 +21,12 @@ impl From<mpsc::UnboundedSender<PacketToForward>> for MixForwardingSender {
}
impl MixForwardingSender {
pub fn forward_packet(&self, packet: PacketToForward) -> Result<(), SendError> {
pub fn forward_packet(&self, packet: impl Into<PacketToForward>) -> Result<(), SendError> {
self.0
.unbounded_send(packet)
.unbounded_send(packet.into())
.map_err(|err| err.into_send_error())
}
pub fn forward_client_packet_without_delay(&self, packet: MixPacket) -> Result<(), SendError> {
self.forward_packet(PacketToForward::client_packet_without_delay(packet))
}
#[allow(clippy::len_without_is_empty)]
pub fn len(&self) -> usize {
self.0.len()
@@ -42,23 +38,35 @@ pub type MixForwardingReceiver = mpsc::UnboundedReceiver<PacketToForward>;
pub struct PacketToForward {
pub packet: MixPacket,
pub forward_delay_target: Option<Instant>,
pub network_monitor_packet: bool,
}
impl From<MixPacket> for PacketToForward {
fn from(packet: MixPacket) -> Self {
PacketToForward::new_no_delay(packet)
}
}
impl From<(MixPacket, Option<Instant>)> for PacketToForward {
fn from((packet, delay_until): (MixPacket, Option<Instant>)) -> Self {
PacketToForward::new(packet, delay_until)
}
}
impl From<(MixPacket, Instant)> for PacketToForward {
fn from((packet, delay_until): (MixPacket, Instant)) -> Self {
PacketToForward::new(packet, Some(delay_until))
}
}
impl PacketToForward {
pub fn new(
packet: MixPacket,
forward_delay_target: Option<Instant>,
network_monitor_packet: bool,
) -> Self {
pub fn new(packet: MixPacket, forward_delay_target: Option<Instant>) -> Self {
PacketToForward {
packet,
forward_delay_target,
network_monitor_packet,
}
}
pub fn client_packet_without_delay(packet: MixPacket) -> Self {
Self::new(packet, None, false)
pub fn new_no_delay(packet: MixPacket) -> Self {
Self::new(packet, None)
}
}
@@ -26,7 +26,6 @@ nym-ecash-contract-common = { workspace = true }
nym-multisig-contract-common = { workspace = true }
nym-group-contract-common = { workspace = true }
nym-performance-contract-common = { workspace = true }
nym-network-monitors-contract-common = { workspace = true }
nym-serde-helpers = { workspace = true, features = ["hex", "base64"] }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
@@ -104,14 +104,6 @@ impl TryFrom<NymNetworkDetails> for Config {
}
impl Config {
pub fn new(nyxd_url: Url, api_url: Url, nyxd_config: nyxd::Config) -> Self {
Config {
api_url,
nyxd_url,
nyxd_config,
}
}
pub fn try_from_nym_network_details(
details: &NymNetworkDetails,
) -> Result<Self, ValidatorClientError> {
@@ -122,15 +114,6 @@ impl Config {
.map(|url| Url::parse(url))
.collect::<Result<Vec<_>, _>>()?;
if let Some(nym_api_urls) = details.nym_api_urls.as_ref() {
api_url.extend(
nym_api_urls
.iter()
.map(|url| url.url.parse())
.collect::<Result<Vec<_>, _>>()?,
);
}
if api_url.is_empty() {
return Err(ValidatorClientError::NoAPIUrlAvailable);
}
@@ -15,15 +15,11 @@ use nym_api_requests::ecash::models::{
VerifyEcashTicketBody,
};
use nym_api_requests::ecash::VerificationKeyResponse;
use nym_api_requests::models::network_monitor::{
KnownNetworkMonitorResponse, StressTestBatchSubmission,
};
use nym_api_requests::models::{
AnnotationResponseV1, ApiHealthResponse, BinaryBuildInformationOwned,
ChainBlocksStatusResponse, ChainStatusResponse, KeyRotationInfoResponse,
NodePerformanceResponse, NodeRefreshBody, NymNodeDescriptionV1, NymNodeDescriptionV2,
PerformanceHistoryResponse, RewardedSetResponse, SignerInformationResponse,
StressTestBatchSubmissionResponse,
AnnotationResponse, ApiHealthResponse, BinaryBuildInformationOwned, ChainBlocksStatusResponse,
ChainStatusResponse, KeyRotationInfoResponse, NodePerformanceResponse, NodeRefreshBody,
NymNodeDescriptionV1, NymNodeDescriptionV2, PerformanceHistoryResponse, RewardedSetResponse,
SignerInformationResponse,
};
use nym_api_requests::pagination::PaginatedResponse;
use nym_http_api_client::{ApiClient, NO_PARAMS};
@@ -980,7 +976,7 @@ pub trait NymApiClientExt: ApiClient {
async fn get_node_annotation(
&self,
node_id: NodeId,
) -> Result<AnnotationResponseV1, NymAPIError> {
) -> Result<AnnotationResponse, NymAPIError> {
self.get_json(
&[
routes::V1_API_VERSION,
@@ -1363,53 +1359,6 @@ pub trait NymApiClientExt: ApiClient {
Ok(SemiSkimmedNodesWithMetadata::new(nodes, metadata))
}
/// Queries the nym-api for whether a particular ed25519 identity key is currently recognised
/// as an authorised network monitor permitted to submit stress testing results.
///
/// `identity_key` is expected to be the base58-encoded form of the ed25519 public key.
#[instrument(level = "debug", skip(self))]
async fn get_known_network_monitor(
&self,
identity_key: IdentityKeyRef<'_>,
) -> Result<KnownNetworkMonitorResponse, NymAPIError> {
self.get_json(
&[
routes::V3_API_VERSION,
routes::NYM_NODES_ROUTES,
routes::STRESS_TESTING,
routes::STRESS_TESTING_KNOWN_MONITORS,
identity_key,
],
NO_PARAMS,
)
.await
}
/// Submit a signed batch of stress-testing results to nym-api on behalf of a network monitor
/// orchestrator.
///
/// The caller is expected to have produced `request` via
/// `StressTestBatchSubmissionContent::new(...)` and signed it with the orchestrator's ed25519
/// key; nym-api will reject submissions that are stale, replayed, unauthorised, or whose
/// signature fails to verify.
#[instrument(level = "debug", skip(self, request))]
async fn submit_stress_testing_results(
&self,
request: &StressTestBatchSubmission,
) -> Result<StressTestBatchSubmissionResponse, NymAPIError> {
self.post_json(
&[
routes::V3_API_VERSION,
routes::NYM_NODES_ROUTES,
routes::STRESS_TESTING,
routes::STRESS_TESTING_BATCH_SUBMIT,
],
NO_PARAMS,
request,
)
.await
}
}
// Client is already nym_http_api_client::Client (re-exported above), so just one impl needed
@@ -49,9 +49,6 @@ pub mod nym_nodes {
pub const NYM_NODES_REWARDED_SET: &str = "rewarded-set";
pub const NYM_NODES_REFRESH_DESCRIBED: &str = "refresh-described";
pub const BY_ADDRESSES: &str = "by-addresses";
pub const STRESS_TESTING: &str = "stress-testing";
pub const STRESS_TESTING_KNOWN_MONITORS: &str = "known-monitors";
pub const STRESS_TESTING_BATCH_SUBMIT: &str = "batch-submit";
}
pub const STATUS_ROUTES: &str = "status";
@@ -13,7 +13,6 @@ pub mod ecash_query_client;
pub mod group_query_client;
pub mod mixnet_query_client;
pub mod multisig_query_client;
pub mod network_monitors_query_client;
pub mod performance_query_client;
pub mod vesting_query_client;
@@ -23,7 +22,6 @@ pub mod ecash_signing_client;
pub mod group_signing_client;
pub mod mixnet_signing_client;
pub mod multisig_signing_client;
pub mod network_monitors_signing_client;
pub mod performance_signing_client;
pub mod vesting_signing_client;
@@ -33,9 +31,6 @@ pub use ecash_query_client::{EcashQueryClient, PagedEcashQueryClient};
pub use group_query_client::{GroupQueryClient, PagedGroupQueryClient};
pub use mixnet_query_client::{MixnetQueryClient, PagedMixnetQueryClient};
pub use multisig_query_client::{MultisigQueryClient, PagedMultisigQueryClient};
pub use network_monitors_query_client::{
NetworkMonitorsQueryClient, PagedNetworkMonitorsQueryClient,
};
pub use performance_query_client::{PagedPerformanceQueryClient, PerformanceQueryClient};
pub use vesting_query_client::{PagedVestingQueryClient, VestingQueryClient};
@@ -45,7 +40,6 @@ pub use ecash_signing_client::EcashSigningClient;
pub use group_signing_client::GroupSigningClient;
pub use mixnet_signing_client::MixnetSigningClient;
pub use multisig_signing_client::MultisigSigningClient;
pub use network_monitors_signing_client::NetworkMonitorsSigningClient;
pub use performance_signing_client::PerformanceSigningClient;
pub use vesting_signing_client::VestingSigningClient;
@@ -55,7 +49,6 @@ pub trait NymContractsProvider {
fn mixnet_contract_address(&self) -> Option<&AccountId>;
fn vesting_contract_address(&self) -> Option<&AccountId>;
fn performance_contract_address(&self) -> Option<&AccountId>;
fn network_monitors_contract_address(&self) -> Option<&AccountId>;
// coconut-related
fn ecash_contract_address(&self) -> Option<&AccountId>;
@@ -69,7 +62,6 @@ pub struct TypedNymContracts {
pub mixnet_contract_address: Option<AccountId>,
pub vesting_contract_address: Option<AccountId>,
pub performance_contract_address: Option<AccountId>,
pub network_monitors_contract_address: Option<AccountId>,
pub ecash_contract_address: Option<AccountId>,
pub group_contract_address: Option<AccountId>,
@@ -94,10 +86,6 @@ impl TryFrom<NymContracts> for TypedNymContracts {
.performance_contract_address
.map(|addr| addr.parse())
.transpose()?,
network_monitors_contract_address: value
.network_monitors_contract_address
.map(|addr| addr.parse())
.transpose()?,
ecash_contract_address: value
.ecash_contract_address
.map(|addr| addr.parse())
@@ -1,107 +0,0 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::collect_paged;
use crate::nyxd::contract_traits::NymContractsProvider;
use crate::nyxd::error::NyxdError;
use crate::nyxd::CosmWasmClient;
use async_trait::async_trait;
use nym_network_monitors_contract_common::{
AuthorisedNetworkMonitor, AuthorisedNetworkMonitorOrchestratorsResponse,
AuthorisedNetworkMonitorsPagedResponse, QueryMsg as NetworkMonitorsQueryMsg,
};
use serde::Deserialize;
use std::net::SocketAddr;
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait NetworkMonitorsQueryClient {
async fn query_network_monitors_contract<T>(
&self,
query: NetworkMonitorsQueryMsg,
) -> Result<T, NyxdError>
where
for<'a> T: Deserialize<'a>;
async fn get_admin(&self) -> Result<cw_controllers::AdminResponse, NyxdError> {
self.query_network_monitors_contract(NetworkMonitorsQueryMsg::Admin {})
.await
}
async fn get_network_monitor_orchestrators(
&self,
) -> Result<AuthorisedNetworkMonitorOrchestratorsResponse, NyxdError> {
self.query_network_monitors_contract(
NetworkMonitorsQueryMsg::NetworkMonitorOrchestrators {},
)
.await
}
async fn get_network_monitor_agents_paged(
&self,
start_next_after: Option<SocketAddr>,
limit: Option<u32>,
) -> Result<AuthorisedNetworkMonitorsPagedResponse, NyxdError> {
self.query_network_monitors_contract(NetworkMonitorsQueryMsg::NetworkMonitorAgents {
start_next_after,
limit,
})
.await
}
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait PagedNetworkMonitorsQueryClient: NetworkMonitorsQueryClient {
async fn get_all_network_monitor_agents(
&self,
) -> Result<Vec<AuthorisedNetworkMonitor>, NyxdError> {
collect_paged!(self, get_network_monitor_agents_paged, authorised)
}
}
#[async_trait]
impl<T> PagedNetworkMonitorsQueryClient for T where T: NetworkMonitorsQueryClient {}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl<C> NetworkMonitorsQueryClient for C
where
C: CosmWasmClient + NymContractsProvider + Send + Sync,
{
async fn query_network_monitors_contract<T>(
&self,
query: NetworkMonitorsQueryMsg,
) -> Result<T, NyxdError>
where
for<'a> T: Deserialize<'a>,
{
let contract_address = &self
.network_monitors_contract_address()
.ok_or_else(|| NyxdError::unavailable_contract_address("network monitors contract"))?;
self.query_contract_smart(contract_address, &query).await
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::nyxd::contract_traits::tests::IgnoreValue;
// it's enough that this compiles and clippy is happy about it
#[allow(dead_code)]
fn all_query_variants_are_covered<C: NetworkMonitorsQueryClient + Send + Sync>(
client: C,
msg: NetworkMonitorsQueryMsg,
) {
match msg {
NetworkMonitorsQueryMsg::Admin {} => client.get_admin().ignore(),
NetworkMonitorsQueryMsg::NetworkMonitorOrchestrators {} => {
client.get_network_monitor_orchestrators().ignore()
}
NetworkMonitorsQueryMsg::NetworkMonitorAgents { .. } => {
client.get_network_monitor_agents_paged(None, None).ignore()
}
};
}
}
@@ -1,205 +0,0 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::nyxd::contract_traits::NymContractsProvider;
use crate::nyxd::cosmwasm_client::types::ExecuteResult;
use crate::nyxd::error::NyxdError;
use crate::nyxd::{Coin, Fee, SigningCosmWasmClient};
use crate::signing::signer::OfflineSigner;
use async_trait::async_trait;
use nym_network_monitors_contract_common::ExecuteMsg as NetworkMonitorsExecuteMsg;
use std::net::SocketAddr;
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait NetworkMonitorsSigningClient {
async fn execute_network_monitors_contract(
&self,
fee: Option<Fee>,
msg: NetworkMonitorsExecuteMsg,
memo: String,
funds: Vec<Coin>,
) -> Result<ExecuteResult, NyxdError>;
async fn update_admin(
&self,
admin: String,
fee: Option<Fee>,
) -> Result<ExecuteResult, NyxdError> {
let msg = NetworkMonitorsExecuteMsg::UpdateAdmin { admin };
self.execute_network_monitors_contract(
fee,
msg,
"NetworkMonitorsExecuteMsg::UpdateAdmin".into(),
vec![],
)
.await
}
async fn authorise_network_monitor_orchestrator(
&self,
address: String,
fee: Option<Fee>,
) -> Result<ExecuteResult, NyxdError> {
let msg = NetworkMonitorsExecuteMsg::AuthoriseNetworkMonitorOrchestrator { address };
self.execute_network_monitors_contract(
fee,
msg,
"NetworkMonitorsExecuteMsg::AuthoriseNetworkMonitorOrchestrator".into(),
vec![],
)
.await
}
/// Announce (or rotate) the ed25519 identity key of the calling network monitor orchestrator.
///
/// The caller must already be an authorised orchestrator; the contract validates that
/// `identity_key` is a well-formed base-58 encoding of a 32-byte ed25519 public key.
async fn update_orchestrator_identity_key(
&self,
identity_key: String,
fee: Option<Fee>,
) -> Result<ExecuteResult, NyxdError> {
let msg = NetworkMonitorsExecuteMsg::UpdateOrchestratorIdentityKey { key: identity_key };
self.execute_network_monitors_contract(
fee,
msg,
"NetworkMonitorsExecuteMsg::UpdateOrchestratorIdentityKey".into(),
vec![],
)
.await
}
async fn revoke_network_monitor_orchestrator(
&self,
address: String,
fee: Option<Fee>,
) -> Result<ExecuteResult, NyxdError> {
let msg = NetworkMonitorsExecuteMsg::RevokeNetworkMonitorOrchestrator { address };
self.execute_network_monitors_contract(
fee,
msg,
"NetworkMonitorsExecuteMsg::RevokeNetworkMonitorOrchestrator".into(),
vec![],
)
.await
}
async fn authorise_network_monitor(
&self,
mixnet_address: SocketAddr,
bs58_x25519_noise: String,
noise_version: u8,
fee: Option<Fee>,
) -> Result<ExecuteResult, NyxdError> {
let msg = NetworkMonitorsExecuteMsg::AuthoriseNetworkMonitor {
mixnet_address,
bs58_x25519_noise,
noise_version,
};
self.execute_network_monitors_contract(
fee,
msg,
"NetworkMonitorsExecuteMsg::AuthoriseNetworkMonitor".into(),
vec![],
)
.await
}
async fn revoke_network_monitor(
&self,
address: SocketAddr,
fee: Option<Fee>,
) -> Result<ExecuteResult, NyxdError> {
let msg = NetworkMonitorsExecuteMsg::RevokeNetworkMonitor { address };
self.execute_network_monitors_contract(
fee,
msg,
"NetworkMonitorsExecuteMsg::RevokeNetworkMonitor".into(),
vec![],
)
.await
}
async fn revoke_all_network_monitors(
&self,
fee: Option<Fee>,
) -> Result<ExecuteResult, NyxdError> {
let msg = NetworkMonitorsExecuteMsg::RevokeAllNetworkMonitors;
self.execute_network_monitors_contract(
fee,
msg,
"NetworkMonitorsExecuteMsg::RevokeAllNetworkMonitors".into(),
vec![],
)
.await
}
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl<C> NetworkMonitorsSigningClient for C
where
C: SigningCosmWasmClient + NymContractsProvider + Sync,
NyxdError: From<<Self as OfflineSigner>::Error>,
{
async fn execute_network_monitors_contract(
&self,
fee: Option<Fee>,
msg: NetworkMonitorsExecuteMsg,
memo: String,
funds: Vec<Coin>,
) -> Result<ExecuteResult, NyxdError> {
let contract_address = &self
.network_monitors_contract_address()
.ok_or_else(|| NyxdError::unavailable_contract_address("network monitors contract"))?;
let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier())));
let signer_address = &self.signer_addresses()[0];
self.execute(signer_address, contract_address, &msg, fee, memo, funds)
.await
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::nyxd::contract_traits::tests::IgnoreValue;
use nym_network_monitors_contract_common::ExecuteMsg;
// it's enough that this compiles and clippy is happy about it
#[allow(dead_code)]
fn all_execute_variants_are_covered<C: NetworkMonitorsSigningClient + Send + Sync>(
client: C,
msg: NetworkMonitorsExecuteMsg,
) {
match msg {
NetworkMonitorsExecuteMsg::UpdateAdmin { admin } => {
client.update_admin(admin, None).ignore()
}
ExecuteMsg::AuthoriseNetworkMonitorOrchestrator { address } => client
.authorise_network_monitor_orchestrator(address, None)
.ignore(),
ExecuteMsg::UpdateOrchestratorIdentityKey { key } => {
client.update_orchestrator_identity_key(key, None).ignore()
}
ExecuteMsg::RevokeNetworkMonitorOrchestrator { address } => client
.revoke_network_monitor_orchestrator(address, None)
.ignore(),
ExecuteMsg::AuthoriseNetworkMonitor {
mixnet_address: address,
bs58_x25519_noise,
noise_version,
} => client
.authorise_network_monitor(address, bs58_x25519_noise, noise_version, None)
.ignore(),
ExecuteMsg::RevokeNetworkMonitor { address } => {
client.revoke_network_monitor(address, None).ignore()
}
ExecuteMsg::RevokeAllNetworkMonitors => {
client.revoke_all_network_monitors(None).ignore()
}
};
}
}
@@ -36,7 +36,7 @@ pub mod logs;
pub mod module_traits;
pub mod types;
#[derive(Debug, Clone)]
#[derive(Debug)]
pub(crate) struct SigningClientOptions {
gas_price: GasPrice,
simulated_gas_multiplier: f32,
@@ -80,17 +80,6 @@ impl<C, S> MaybeSigningClient<C, S> {
opts,
}
}
pub(crate) fn clone_query_client(&self) -> MaybeSigningClient<C, NoSigner>
where
C: Clone,
{
MaybeSigningClient {
client: self.client.clone(),
signer: Default::default(),
opts: self.opts.clone(),
}
}
}
#[cfg(feature = "http-client")]
@@ -24,8 +24,6 @@ use async_trait::async_trait;
use cosmrs::tendermint::{abci, evidence::Evidence, Genesis};
use cosmrs::tx::{Raw, SignDoc};
use cosmwasm_std::Addr;
use nym_contracts_common::build_information::CONTRACT_BUILD_INFO_STORAGE_KEY;
use nym_contracts_common::ContractBuildInformation;
use nym_network_defaults::{ChainDetails, NymNetworkDetails};
use serde::{de::DeserializeOwned, Serialize};
use std::fmt::Debug;
@@ -42,7 +40,6 @@ pub use crate::nyxd::{
fee::Fee,
};
pub use crate::rpc::TendermintRpcClient;
pub use bip39;
pub use coin::Coin;
pub use cosmrs::{
bank::MsgSend,
@@ -73,19 +70,14 @@ pub use tendermint_rpc::{
Paging, Request, Response, SimpleRequest,
};
pub use nym_ecash_contract_common;
pub use nym_mixnet_contract_common;
pub use nym_multisig_contract_common;
pub use nym_network_monitors_contract_common;
pub use nym_performance_contract_common;
pub use nym_vesting_contract_common;
#[cfg(feature = "http-client")]
use crate::http_client;
#[cfg(feature = "http-client")]
use crate::{DirectSigningHttpRpcNyxdClient, QueryHttpRpcNyxdClient};
#[cfg(feature = "http-client")]
use cosmrs::rpc::{HttpClient, HttpClientUrl};
use nym_contracts_common::build_information::CONTRACT_BUILD_INFO_STORAGE_KEY;
use nym_contracts_common::ContractBuildInformation;
pub mod coin;
pub mod contract_traits;
@@ -270,16 +262,6 @@ impl<C, S> NyxdClient<C, S> {
}
}
pub fn clone_query_client(&self) -> NyxdClient<C>
where
C: Clone,
{
NyxdClient {
client: self.client.clone_query_client(),
config: self.config.clone(),
}
}
pub fn current_config(&self) -> &Config {
&self.config
}
@@ -307,10 +289,6 @@ impl<C, S> NyxdClient<C, S> {
pub fn set_simulated_gas_multiplier(&mut self, multiplier: f32) {
self.config.simulated_gas_multiplier = multiplier;
}
pub fn get_nym_contracts(&self) -> TypedNymContracts {
self.config.contracts.clone()
}
}
impl<C, S> NymContractsProvider for NyxdClient<C, S> {
@@ -325,12 +303,6 @@ impl<C, S> NymContractsProvider for NyxdClient<C, S> {
fn performance_contract_address(&self) -> Option<&AccountId> {
self.config.contracts.performance_contract_address.as_ref()
}
fn network_monitors_contract_address(&self) -> Option<&AccountId> {
self.config
.contracts
.network_monitors_contract_address
.as_ref()
}
fn ecash_contract_address(&self) -> Option<&AccountId> {
self.config.contracts.ecash_contract_address.as_ref()
@@ -1,26 +0,0 @@
[package]
name = "nym-network-monitors-contract-common"
description = "Common library for the Nym Network Monitors contract"
authors.workspace = true
repository.workspace = true
homepage.workspace = true
documentation.workspace = true
edition.workspace = true
license.workspace = true
readme.workspace = true
version.workspace = true
[dependencies]
thiserror = { workspace = true }
serde = { workspace = true }
schemars = { workspace = true }
cosmwasm-std = { workspace = true }
cosmwasm-schema = { workspace = true }
cw-controllers = { workspace = true }
[features]
schema = []
[lints]
workspace = true
@@ -1,8 +0,0 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
pub mod storage_keys {
pub const CONTRACT_ADMIN: &str = "contract-admin";
pub const AUTHORISED_ORCHESTRATORS: &str = "authorised-orchestrators";
pub const AUTHORISED_NETWORK_MONITORS: &str = "authorised-network-monitors";
}
@@ -1,30 +0,0 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use cosmwasm_std::Addr;
use cw_controllers::AdminError;
use thiserror::Error;
#[derive(Error, Debug, PartialEq)]
pub enum NetworkMonitorsContractError {
#[error("could not perform contract migration: {comment}")]
FailedMigration { comment: String },
#[error(transparent)]
Admin(#[from] AdminError),
#[error("unauthorised")]
Unauthorized,
#[error("address {addr} is not an authorised orchestrator")]
NotAnOrchestrator { addr: Addr },
#[error("Failed to recover x25519 public key from its base58 representation: {0}")]
MalformedX25519AgentNoiseKey(String),
#[error("Failed to recover ed25519 public key from its base58 representation: {0}")]
MalformedEd25519OrchestratorIdentityKey(String),
#[error(transparent)]
StdErr(#[from] cosmwasm_std::StdError),
}
@@ -1,11 +0,0 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
pub mod constants;
pub mod error;
pub mod msg;
pub mod types;
pub use error::*;
pub use msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg};
pub use types::*;
@@ -1,78 +0,0 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use cosmwasm_schema::cw_serde;
use std::net::SocketAddr;
#[cfg(feature = "schema")]
use crate::{
AuthorisedNetworkMonitorOrchestratorsResponse, AuthorisedNetworkMonitorsPagedResponse,
};
#[cw_serde]
pub struct InstantiateMsg {
/// Address of the initial network monitor orchestrator.
pub orchestrator_address: String,
}
#[cw_serde]
pub enum ExecuteMsg {
/// Change the admin
UpdateAdmin { admin: String },
/// Authorise new network monitor orchestrator
AuthoriseNetworkMonitorOrchestrator { address: String },
/// Attempt to update the announced identity key of this orchestrator
UpdateOrchestratorIdentityKey { key: String },
/// Revoke network monitor orchestrator authorisation.
RevokeNetworkMonitorOrchestrator { address: String },
/// Authorise new network monitor (or renew authorisation)
/// granting additional privileges when sending mixnet packets to Nym nodes.
AuthoriseNetworkMonitor {
/// Mixnet address of the agent.
/// The underlying ip address is going to be used as ingress to the nodes,
/// and the full socket address announces the egress and the association with the noise key
mixnet_address: SocketAddr,
/// Base-58 encoded noise key of the agent.
bs58_x25519_noise: String,
/// Version of the noise protocol used by the agent.
noise_version: u8,
},
/// Revoke network monitor authorisation.
RevokeNetworkMonitor { address: SocketAddr },
/// Revoke all network monitor authorisations.
RevokeAllNetworkMonitors,
}
#[cw_serde]
#[cfg_attr(feature = "schema", derive(cosmwasm_schema::QueryResponses))]
pub enum QueryMsg {
#[cfg_attr(feature = "schema", returns(cw_controllers::AdminResponse))]
Admin {},
// no need for pagination as we don't expect even a double digit of those
#[cfg_attr(
feature = "schema",
returns(AuthorisedNetworkMonitorOrchestratorsResponse)
)]
NetworkMonitorOrchestrators {},
#[cfg_attr(feature = "schema", returns(AuthorisedNetworkMonitorsPagedResponse))]
NetworkMonitorAgents {
/// Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response.
start_next_after: Option<SocketAddr>,
/// Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default.
limit: Option<u32>,
},
}
#[cw_serde]
pub struct MigrateMsg {}
@@ -1,53 +0,0 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use cosmwasm_schema::cw_serde;
use cosmwasm_std::{Addr, Timestamp};
use std::net::SocketAddr;
pub type OrchestratorAddress = Addr;
#[cw_serde]
pub struct AuthorisedNetworkMonitorOrchestrator {
/// The address associated with the network monitor orchestrator.
pub address: Addr,
/// Base-58 encoded identity key of the orchestrator, announced by the orchestrator itself
/// on startup.
pub identity_key: Option<String>,
/// Timestamp of when the network monitor was authorised.
pub authorised_at: Timestamp,
}
#[cw_serde]
pub struct AuthorisedNetworkMonitor {
/// Mixnet address of the agent.
/// The underlying ip address is going to be used as ingress to the nodes,
/// and the full socket address announces the egress and the association with the noise key
pub mixnet_address: SocketAddr,
/// The address of the orchestrator that authorised the network monitor agent.
pub authorised_by: OrchestratorAddress,
/// Timestamp of when the network monitor was authorised.
pub authorised_at: Timestamp,
/// Base-58 encoded noise key of the agent.
pub bs58_x25519_noise: String,
/// Version of the noise protocol used by the agent.
pub noise_version: u8,
}
#[cw_serde]
pub struct AuthorisedNetworkMonitorOrchestratorsResponse {
pub authorised: Vec<AuthorisedNetworkMonitorOrchestrator>,
}
#[cw_serde]
pub struct AuthorisedNetworkMonitorsPagedResponse {
pub authorised: Vec<AuthorisedNetworkMonitor>,
pub start_next_after: Option<SocketAddr>,
}
+1 -24
View File
@@ -57,7 +57,6 @@ impl QuorumStateChecker {
}
async fn check_quorum_state(&self) -> Result<bool, CredentialProxyError> {
info!("checking the current quorum state");
let client_guard = self.client.query_chain().await;
// split the operation as we only need to hold the reference to chain client for the first part
@@ -66,7 +65,6 @@ 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!(
@@ -78,33 +76,12 @@ impl QuorumStateChecker {
let mut working_issuer = 0;
for result in res.results {
let dealer = &result.information;
let info = format!("[id: {}] @ {}", dealer.node_index, dealer.announce_address);
if result.chain_available() && result.signing_available() {
info!("✅ {info} is fully available");
working_issuer += 1;
} else if !result.chain_available() && !result.signing_available() {
warn!("❌ {info} is not available for both chain and signing");
} else if !result.chain_available() {
warn!("❌ {info} is not available for chain");
} else {
warn!("❌ {info} is not available for signing");
}
}
let available = (working_issuer as u64) >= signing_threshold;
if available {
info!(
"✅ Quorum state is available with {working_issuer} out of {signing_threshold} issuers"
)
} else {
error!(
"❌ Quorum state is not available with {working_issuer} out of {signing_threshold} issuers"
)
}
Ok(available)
Ok((working_issuer as u64) >= signing_threshold)
}
pub async fn run_forever(self) {
-2
View File
@@ -31,5 +31,3 @@ pub use aes_gcm_siv::{Aes128GcmSiv, Aes256GcmSiv};
pub use blake3;
#[cfg(feature = "stream_cipher")]
pub use ctr;
#[cfg(feature = "hashing")]
pub use sha2;
@@ -6,7 +6,7 @@ use nym_coconut_dkg_common::verification_key::VerificationKeyShare;
use nym_crypto::asymmetric::ed25519;
use std::time::Duration;
use time::OffsetDateTime;
use tracing::warn;
use tracing::{debug, warn};
pub trait Verifiable {
fn verify_signature(&self, pub_key: &ed25519::PublicKey) -> bool;
@@ -36,7 +36,6 @@ pub trait ChainResponse: Verifiable + TimestampedResponse {
// we rely on information provided from the api itself AS LONG AS it's not too outdated
if self.timestamp() + stale_response_threshold < now {
warn!("chain status response is stale");
return false;
}
self.chain_synced()
@@ -97,27 +96,26 @@ pub trait SignerResponse: Verifiable + TimestampedResponse {
// we rely on information provided from the api itself AS LONG AS it's not too outdated
if self.timestamp() + stale_response_threshold < now {
warn!("stale signer response");
return false;
}
if !self.has_signing_keys() {
warn!("missing signing keys");
debug!("missing signing keys");
return false;
}
if self.signer_disabled() {
warn!("signer functionalities are explicitly disabled");
debug!("signer functionalities explicitly disabled");
return false;
}
if !self.is_ecash_signer() {
warn!("signer doesn't recognise it's a signer for this epoch");
debug!("signer doesn't recognise it's a signer for this epoch");
return false;
}
if dkg_epoch_id != self.dkg_ecash_epoch_id() {
warn!(
debug!(
"mismatched dkg epoch id. current: {dkg_epoch_id}, signer's: {}",
self.dkg_ecash_epoch_id()
);
@@ -11,11 +11,10 @@ use nym_crypto::asymmetric::ed25519;
use serde::{Deserialize, Serialize};
use std::time::Duration;
use time::OffsetDateTime;
use tracing::warn;
use utoipa::ToSchema;
pub(crate) const CHAIN_STALL_THRESHOLD: Duration = Duration::from_secs(10 * 60);
pub(crate) const STALE_RESPONSE_THRESHOLD: Duration = Duration::from_secs(10 * 60);
pub(crate) const CHAIN_STALL_THRESHOLD: Duration = Duration::from_secs(5 * 60);
pub(crate) const STALE_RESPONSE_THRESHOLD: Duration = Duration::from_secs(5 * 60);
// the reason for generics is not to remove duplication of code,
// but because without them, we'd be having problems with circular dependencies,
@@ -189,7 +188,6 @@ where
};
let SignerStatus::Tested { result } = &self.status else {
warn!("no valid chain response");
return false;
};
result
@@ -241,7 +239,6 @@ where
};
let SignerStatus::Tested { result } = &self.status else {
warn!("no valid signer response");
return false;
};
result.signing_status.signing_available(
+113 -74
View File
@@ -55,8 +55,8 @@ use std::{
use hickory_resolver::{
TokioResolver,
config::{CLOUDFLARE, NameServerConfig, QUAD9, ResolverConfig, ResolverOpts},
net::{NetError, runtime::TokioRuntimeProvider},
config::{NameServerConfig, NameServerConfigGroup, ResolverConfig, ResolverOpts},
name_server::TokioConnectionProvider,
};
use once_cell::sync::OnceCell;
use reqwest::dns::{Addrs, Name, Resolve, Resolving};
@@ -112,7 +112,7 @@ pub enum ResolveError {
#[error("invalid name: {0}")]
InvalidNameError(String),
#[error("hickory-dns resolver error: {0}")]
ResolveError(#[from] NetError),
ResolveError(#[from] hickory_resolver::ResolveError),
#[error("high level lookup timed out")]
Timeout,
#[error("hostname not found in static lookup table")]
@@ -122,10 +122,7 @@ pub enum ResolveError {
impl ResolveError {
/// Returns true if the error is a timeout.
pub fn is_timeout(&self) -> bool {
matches!(
self,
ResolveError::Timeout | ResolveError::ResolveError(NetError::Timeout)
)
matches!(self, ResolveError::Timeout)
}
}
@@ -169,17 +166,18 @@ 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 result = if use_system {
self.system_resolver
let resolver = if use_system {
match 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_try_init(|| HickoryDnsResolver::new_resolver(use_shared))
};
let resolver = match result {
Ok(r) => r.clone(),
Err(err) => return Box::pin(return_err(err)),
.get_or_init(|| HickoryDnsResolver::new_resolver(use_shared))
.clone()
};
let maybe_static = self.static_base.clone();
@@ -230,7 +228,7 @@ async fn resolve(
Ok(Ok(lookup)) => {
// 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());
let mut ips: Vec<IpAddr> = lookup.into_iter().collect();
fastrand::shuffle(&mut ips);
let addrs: Addrs = Box::new(ips.into_iter().map(|ip| SocketAddr::new(ip, 0)));
return Ok(addrs);
@@ -279,7 +277,7 @@ impl HickoryDnsResolver {
.clone()
} else {
self.state
.get_or_try_init(|| HickoryDnsResolver::new_resolver(self.use_shared))?
.get_or_init(|| HickoryDnsResolver::new_resolver(self.use_shared))
.clone()
};
@@ -302,11 +300,11 @@ impl HickoryDnsResolver {
}
}
fn new_resolver(use_shared: bool) -> Result<TokioResolver, ResolveError> {
fn new_resolver(use_shared: bool) -> TokioResolver {
// 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_try_init(new_resolver).cloned()
SHARED_RESOLVER.state.get_or_init(new_resolver).clone()
} else {
new_resolver()
}
@@ -358,7 +356,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.
@@ -429,7 +427,20 @@ impl HickoryDnsResolver {
/// Get the list of currently available nameserver configs.
pub fn all_configured_name_servers(&self) -> Vec<NameServerConfig> {
default_nameserver_group()
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()
}
/// Do a trial resolution using each nameserver individually to test which are working and which
@@ -455,60 +466,65 @@ 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() -> Result<TokioResolver, ResolveError> {
fn new_resolver() -> TokioResolver {
let name_servers = default_nameserver_group_ipv4_only();
configure_and_build_resolver(name_servers)
}
fn configure_and_build_resolver(
name_servers: Vec<NameServerConfig>,
) -> Result<TokioResolver, ResolveError> {
fn configure_and_build_resolver<G>(name_servers: G) -> TokioResolver
where
G: Into<NameServerConfigGroup>,
{
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, TokioRuntimeProvider::default());
TokioResolver::builder_with_config(config, TokioConnectionProvider::default());
resolver_builder = resolver_builder.with_options(options);
Ok(resolver_builder.build()?)
resolver_builder.build()
}
fn filter_ipv4(nameservers: impl IntoIterator<Item = NameServerConfig>) -> Vec<NameServerConfig> {
fn filter_ipv4(nameservers: impl AsRef<[NameServerConfig]>) -> Vec<NameServerConfig> {
nameservers
.into_iter()
.filter(|ns| ns.ip.is_ipv4())
.as_ref()
.iter()
.filter(|ns| ns.socket_addr.is_ipv4())
.cloned()
.collect()
}
#[allow(unused)]
fn filter_ipv6(nameservers: impl IntoIterator<Item = NameServerConfig>) -> Vec<NameServerConfig> {
fn filter_ipv6(nameservers: impl AsRef<[NameServerConfig]>) -> Vec<NameServerConfig> {
nameservers
.into_iter()
.filter(|ns| ns.ip.is_ipv6())
.as_ref()
.iter()
.filter(|ns| ns.socket_addr.is_ipv6())
.cloned()
.collect()
}
#[allow(unused)]
fn default_nameserver_group() -> Vec<NameServerConfig> {
QUAD9
.tls()
.chain(QUAD9.https())
.chain(CLOUDFLARE.tls())
.chain(CLOUDFLARE.https())
.collect()
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_ipv4_only() -> Vec<NameServerConfig> {
filter_ipv4(default_nameserver_group())
fn default_nameserver_group_ipv4_only() -> NameServerConfigGroup {
filter_ipv4(&default_nameserver_group() as &[NameServerConfig]).into()
}
#[allow(unused)]
fn default_nameserver_group_ipv6_only() -> Vec<NameServerConfig> {
filter_ipv6(default_nameserver_group())
fn default_nameserver_group_ipv6_only() -> NameServerConfigGroup {
filter_ipv6(&default_nameserver_group() as &[NameServerConfig]).into()
}
/// Create a new resolver with the default configuration, which reads from the system DNS config
@@ -523,7 +539,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 {
@@ -550,7 +566,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(()),
@@ -563,10 +579,8 @@ async fn trial_lookup(name_server: NameServerConfig, query: &str) -> Result<(),
mod test {
use super::*;
use itertools::Itertools;
use std::{
collections::HashMap,
net::{IpAddr, Ipv4Addr, Ipv6Addr},
};
use std::collections::HashMap;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
/// IP addresses guaranteed to fail attempts to resolve
///
@@ -645,16 +659,26 @@ mod test {
let mut ns_ips = GUARANTEED_BROKEN_IPS_1.to_vec();
ns_ips.push(good_cf_ip);
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 broken_ns_https = NameServerConfigGroup::from_ips_https(
&ns_ips,
443,
"cloudflare-dns.com".to_string(),
true,
);
for (ns, result) in trial_nameservers_inner(&broken_ns_https).await {
if ns.ip == good_cf_ip {
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 {
assert!(result.is_ok())
} else {
assert!(result.is_err())
@@ -670,20 +694,21 @@ mod test {
fn build_broken_resolver() -> Result<TokioResolver, ResolveError> {
info!("building new faulty resolver");
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<_>>();
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);
configure_and_build_resolver(broken_ns_group)
Ok(configure_and_build_resolver(broken_ns_group))
}
#[tokio::test]
@@ -704,7 +729,7 @@ mod test {
build_broken_resolver()?;
let domain = "ifconfig.me";
let result = resolver.resolve_str(domain).await;
assert!(result.is_err_and(|e| e.is_timeout()));
assert!(result.is_err_and(|e| matches!(e, ResolveError::Timeout)));
let duration = time_start.elapsed();
assert!(duration < resolver.overall_dns_timeout + Duration::from_secs(1));
@@ -738,11 +763,25 @@ 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| e.is_timeout()));
assert!(result.is_err_and(|e| matches!(e, ResolveError::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,7 +141,9 @@
use http::header::USER_AGENT;
pub use inventory;
pub use reqwest::{self, ClientBuilder as ReqwestClientBuilder, StatusCode};
pub use reqwest;
pub use reqwest::ClientBuilder as ReqwestClientBuilder;
pub use reqwest::StatusCode;
use std::error::Error;
pub mod registry;
@@ -150,21 +152,19 @@ use crate::path::RequestPath;
use async_trait::async_trait;
use bytes::Bytes;
use cfg_if::cfg_if;
use http::{
HeaderMap,
header::{ACCEPT, CONTENT_TYPE},
};
use http::HeaderMap;
use http::header::{ACCEPT, CONTENT_TYPE};
use itertools::Itertools;
use mime::Mime;
use reqwest::{RequestBuilder, Response, header::HeaderValue};
use serde::{Deserialize, Serialize, de::DeserializeOwned};
use reqwest::header::HeaderValue;
use reqwest::{RequestBuilder, Response};
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use std::fmt::Display;
#[cfg(not(target_arch = "wasm32"))]
use std::io::ErrorKind;
use std::{
fmt::Display,
sync::atomic::{AtomicUsize, Ordering},
time::Duration,
};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::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::net::NetError>() {
} else if let Some(resolve_err) = e.downcast_ref::<hickory_resolver::ResolveError>() {
// try downcast to DNS error
return resolve_err.is_nx_domain();
} else {
-2
View File
@@ -10,9 +10,7 @@ fn sanitize_fragment(segment: &str) -> &str {
segment.trim_matches(|c: char| c.is_whitespace() || c == '/')
}
/// Defines a path that can be used to make a request to an API.
pub trait RequestPath: Debug {
/// Sanitise the request path by removing empty segments and trimming whitespace and slashes
fn to_sanitized_segments(&self) -> Vec<&str>;
}
+15
View File
@@ -20,6 +20,21 @@ 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::*;
const _: () = {
assert!(SPHINX_STREAM_VERSION_THRESHOLD > MAX_NON_STREAM_VERSION);
};
#[test]
fn stream_transport_threshold_is_consistent() {
assert_eq!(MAX_NON_STREAM_VERSION, 8);
assert_eq!(SPHINX_STREAM_VERSION_THRESHOLD, 9);
}
}
// version 3: initial version
// version 4: IPv6 support
// version 5: Add severity level to info response
+5 -4
View File
@@ -17,10 +17,11 @@ 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 }
@@ -29,9 +30,9 @@ utoipa = { workspace = true, optional = true }
[features]
default = ["env", "network"]
env = ["dotenvy", "serde_json", "tracing"]
env = ["dotenvy", "log", "serde_json", "tracing"]
network = ["schemars", "serde", "url"]
utoipa = ["dep:utoipa"]
utoipa = [ "dep:utoipa" ]
[build-dependencies]
regex = { workspace = true }
+4 -8
View File
@@ -27,20 +27,16 @@ 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");
tracing::debug!("{key}: {val}");
log::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 {
tracing::debug!(
log::debug!(
"Loading environment variables from {:?}",
config_env_file.as_ref()
);
@@ -51,12 +47,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
tracing::debug!("Loading mainnet defaults");
log::debug!("Loading mainnet defaults");
crate::mainnet::export_to_env();
}
}
Err(_) => {
tracing::debug!("Environment variables already set. Using them");
log::debug!("Environment variables already set. Using them");
crate::mainnet::export_to_env()
}
_ => {
+5 -24
View File
@@ -22,8 +22,6 @@ 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 =
@@ -38,10 +36,6 @@ 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] = &[
@@ -49,6 +43,10 @@ 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"]),
@@ -70,7 +68,7 @@ pub const UPGRADE_MODE_ATTESTER_ED25519_BS58_PUBKEY: &str =
pub const NYM_VPN_APIS: &[ApiUrlConst] = &[
ApiUrlConst {
url: NYM_VPN_API,
front_hosts: None,
front_hosts: Some(&["vercel.app", "vercel.com"]),
},
ApiUrlConst {
url: "https://nymvpn-frontdoor.global.ssl.fastly.net/api/",
@@ -139,11 +137,6 @@ 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;
@@ -174,14 +167,6 @@ 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,
@@ -201,8 +186,6 @@ 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"))]
@@ -254,6 +237,4 @@ 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);
}
+3 -37
View File
@@ -39,8 +39,6 @@ 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>,
@@ -74,15 +72,6 @@ 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,
@@ -189,10 +178,6 @@ 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)
@@ -214,9 +199,6 @@ 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),
@@ -244,7 +226,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 {
@@ -382,31 +364,15 @@ 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<U: Into<ApiUrl>>(mut self, urls: Vec<U>) -> Self {
self.set_nym_api_urls(urls);
pub fn with_nym_api_urls(mut self, urls: Vec<ApiUrl>) -> Self {
self.nym_api_urls = Some(urls);
self
}
-4
View File
@@ -18,15 +18,11 @@ 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";
+42 -383
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 strum_macros::{EnumIter, FromRepr};
use tokio::sync::{Mutex, MutexGuard};
pub use nym_noise_keys::{NoiseVersion, VersionedNoiseKeyV1};
use arc_swap::ArcSwap;
use nym_crypto::asymmetric::x25519;
use nym_noise_keys::{NoiseVersion, VersionedNoiseKeyV1};
use snow::params::NoiseParams;
use strum_macros::{EnumIter, FromRepr};
#[derive(Default, Debug, Clone, Copy, EnumIter, FromRepr, Eq, PartialEq)]
#[repr(u8)]
@@ -53,125 +53,38 @@ impl NoisePattern {
}
}
#[derive(Debug, Clone)]
#[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)]
pub struct NoiseNetworkView {
inner: Arc<NoiseNetworkViewInner>,
}
/// 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 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,
keys: Arc<SocketAddrToKey>,
support: Arc<IpAddrToVersion>,
}
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 });
NoiseNetworkView {
keys: Default::default(),
support: Default::default(),
}
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()
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));
}
}
@@ -213,38 +126,20 @@ impl NoiseConfig {
self
}
/// 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)
}
}
pub(crate) fn get_noise_key(&self, s_address: &SocketAddr) -> Option<VersionedNoiseKeyV1> {
self.network.keys.inner.load().get(s_address).copied()
}
/// 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())
// 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)
}
}
@@ -274,240 +169,4 @@ 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,12 +15,6 @@ 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.supports_noise(initiator_addr.ip()) {
if config.get_noise_support(initiator_addr.ip()).is_none() {
warn!("{initiator_addr} can't speak Noise yet, falling back to TCP",);
return Ok(Connection::Raw(conn));
};
@@ -110,12 +110,6 @@ 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, derive_payload_key},
key::{PayloadKey, PayloadKeySeed},
},
route::{Destination, DestinationAddressBytes, Node, NodeAddressBytes, SURBIdentifier},
surb::{SURB, SURBMaterial},
+2 -3
View File
@@ -7,9 +7,8 @@ use nyxd_scraper_shared::NyxdScraper;
pub use nyxd_scraper_shared::constants;
pub use nyxd_scraper_shared::error::ScraperError;
pub use nyxd_scraper_shared::{
BlockModule, DecodedMessage, MsgModule, NyxdScraperTransaction, ParsedTransactionDetails,
ParsedTransactionResponse, PruningOptions, PruningStrategy, StartingBlockOpts, TxModule,
parse_msg,
BlockModule, MsgModule, NyxdScraperTransaction, ParsedTransactionResponse, PruningOptions,
PruningStrategy, StartingBlockOpts, TxModule,
};
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_details.tx_result.events {
for event in &tx.tx_result.events {
for attribute in &event.attributes {
let Ok(value) = attribute.value_str() else {
continue;
@@ -147,7 +147,6 @@ impl PostgresStorageTransaction {
for chain_tx in txs {
// bdjuno style, base64 encode them
let signatures = chain_tx
.tx_details
.tx
.signatures
.iter()
@@ -155,14 +154,12 @@ impl PostgresStorageTransaction {
.collect();
let messages = chain_tx
.decoded_messages
.parsed_messages
.values()
.map(|msg| &msg.decoded_content)
.cloned()
.collect::<Vec<_>>();
let signer_infos = chain_tx
.tx_details
.tx
.auth_info
.signer_infos
@@ -170,28 +167,28 @@ impl PostgresStorageTransaction {
.map(|info| proto::cosmos::tx::v1beta1::SignerInfo::from(info.clone()))
.collect::<Vec<_>>();
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 hash = chain_tx.hash.to_string();
let height = chain_tx.height.into();
let index = chain_tx.index as i32;
let log = serde_json::to_value(chain_tx.tx_details.tx_result.log.clone())
let log = serde_json::to_value(chain_tx.tx_result.log.clone())
.inspect_err(|e| error!(hash, height, index, "Failed to parse logs: {e}"))
.unwrap_or_default();
let events = &chain_tx.tx_details.tx_result.events;
let events = &chain_tx.tx_result.events;
insert_transaction(
hash,
height,
index,
chain_tx.tx_details.tx_result.code.is_ok(),
chain_tx.tx_result.code.is_ok(),
serde_json::Value::Array(messages),
chain_tx.tx_details.tx.body.memo.clone(),
chain_tx.tx.body.memo.clone(),
signatures,
serde_json::to_value(signer_infos)?,
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(),
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(),
json!(log),
json!(events),
self.inner.as_mut(),
@@ -210,20 +207,17 @@ impl PostgresStorageTransaction {
for chain_tx in txs {
let involved_addresses = parse_addresses_from_events(chain_tx);
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);
for (index, msg) in chain_tx.tx.body.messages.iter().enumerate() {
let parsed_message = chain_tx.parsed_messages.get(&index);
let value = serde_json::to_value(parsed_message)?;
insert_message(
chain_tx.tx_details.hash.to_string(),
chain_tx.hash.to_string(),
index as i64,
msg.type_url.clone(),
value,
involved_addresses.clone(),
chain_tx.tx_details.height().into(),
chain_tx.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.tx_details.height(),
tx.tx_details.hash,
tx.tx_details.tx.body.messages.len()
tx.block.header.height,
tx.hash,
tx.parsed_messages.len()
);
Ok(())
@@ -281,7 +281,7 @@ where
&mut self,
block: BlockToProcess,
) -> Result<(), ScraperError> {
debug!("processing block at height {}", block.height);
info!("processing block at height {}", block.height);
let full_info = self
.rpc_client
@@ -291,13 +291,8 @@ where
if let Some(tx_info) = &full_info.transactions {
debug!("this block has {} transaction(s)", tx_info.len());
for tx in tx_info {
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!("{} has {} message(s)", tx.hash, tx.tx.body.messages.len());
for (index, msg) in tx.tx.body.messages.iter().enumerate() {
debug!("{index}: {:?}", msg.type_url)
}
}
@@ -320,24 +315,11 @@ 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 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 (index, msg) in block_tx.tx.body.messages.iter().enumerate() {
for msg_module in &mut self.msg_modules {
if msg.type_url == msg_module.type_url() {
msg_module
.handle_msg(index, msg, decoded, tx_details)
.await?
msg_module.handle_msg(index, msg, block_tx).await?
}
}
}
@@ -7,16 +7,9 @@ use tendermint::{Block, Hash, abci, block, tx};
use tendermint_rpc::endpoint::{block as block_endpoint, block_results, validators};
use tendermint_rpc::event::{Event, EventData};
/// Message decoded from the raw transaction and converted into json.
/// Note that it might have gone through additional processing as set by the `MessageRegistry`
// just get all everything out of tx::Response, but parse raw `tx` bytes
#[derive(Clone, Debug)]
pub struct DecodedMessage {
pub type_url: String,
pub decoded_content: serde_json::Value,
}
#[derive(Clone, Debug)]
pub struct ParsedTransactionDetails {
pub struct ParsedTransactionResponse {
/// The hash of the transaction.
///
/// Deserialized from a hex-encoded string (there is a discrepancy between
@@ -24,6 +17,8 @@ pub struct ParsedTransactionDetails {
/// the Tendermint RPC).
pub hash: Hash,
pub height: block::Height,
pub index: u32,
pub tx_result: abci::types::ExecTxResult,
@@ -32,23 +27,13 @@ pub struct ParsedTransactionDetails {
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.
+3 -1
View File
@@ -82,8 +82,10 @@ pub enum ScraperError {
source: cosmrs::ErrorReport,
},
#[error("could not parse msg of type {type_url}: {source}")]
#[error("could not parse msg in tx {hash} at index {index} into {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_details.tx_result.gas_used).sum()
txs.iter().map(|tx| tx.tx_result.gas_used).sum()
}
pub fn validator_info(
+2 -4
View File
@@ -15,14 +15,12 @@ pub(crate) mod subscriber;
pub mod watcher;
pub use block_processor::pruning::{PruningOptions, PruningStrategy};
pub use block_processor::types::{
DecodedMessage, ParsedTransactionDetails, ParsedTransactionResponse,
};
pub use block_processor::types::ParsedTransactionResponse;
pub use cosmos_module::{
CosmosModule,
message_registry::{MessageRegistry, default_message_registry},
};
pub use cosmrs::Any;
pub use modules::{BlockModule, MsgModule, TxModule, parse_msg};
pub use modules::{BlockModule, MsgModule, TxModule};
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, parse_msg};
pub use msg_module::MsgModule;
pub use tx_module::TxModule;
@@ -1,47 +1,11 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::block_processor::types::{DecodedMessage, ParsedTransactionDetails};
use crate::block_processor::types::ParsedTransactionResponse;
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;
@@ -50,7 +14,6 @@ pub trait MsgModule {
&mut self,
index: usize,
msg: &Any,
decoded_msg: &DecodedMessage,
tx: &ParsedTransactionDetails,
tx: &ParsedTransactionResponse,
) -> Result<(), ScraperError>;
}
+16 -20
View File
@@ -2,11 +2,11 @@
// SPDX-License-Identifier: Apache-2.0
use crate::block_processor::types::{
BlockToProcess, DecodedMessage, FullBlockInformation, ParsedTransactionResponse,
BlockToProcess, FullBlockInformation, ParsedTransactionResponse,
};
use crate::error::ScraperError;
use crate::helpers::tx_hash;
use crate::{Any, MessageRegistry, ParsedTransactionDetails, default_message_registry};
use crate::{Any, MessageRegistry, default_message_registry};
use futures::StreamExt;
use futures::future::join3;
use std::collections::BTreeMap;
@@ -77,7 +77,8 @@ impl RpcClient {
) -> Result<Vec<ParsedTransactionResponse>, ScraperError> {
let mut transactions = Vec::with_capacity(raw_transactions.len());
for raw_tx in raw_transactions {
let mut decoded_messages = BTreeMap::new();
let mut parsed_messages = BTreeMap::new();
let mut parsed_message_urls = BTreeMap::new();
let tx = cosmrs::Tx::from_bytes(&raw_tx.tx).map_err(|source| {
ScraperError::TxParseFailure {
hash: raw_tx.hash,
@@ -86,27 +87,22 @@ impl RpcClient {
})?;
for (index, msg) in tx.body.messages.iter().enumerate() {
if let Some(decoded_content) = self.decode_or_skip(msg) {
decoded_messages.insert(
index,
DecodedMessage {
type_url: msg.type_url.clone(),
decoded_content,
},
);
if let Some(value) = self.decode_or_skip(msg) {
parsed_messages.insert(index, value);
parsed_message_urls.insert(index, msg.type_url.clone());
}
}
transactions.push(ParsedTransactionResponse {
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,
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(),
})
}
Ok(transactions)
+2 -10
View File
@@ -14,16 +14,15 @@ 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>>,
@@ -34,19 +33,12 @@ 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));
+2 -3
View File
@@ -7,9 +7,8 @@ use nyxd_scraper_shared::NyxdScraper;
pub use nyxd_scraper_shared::constants;
pub use nyxd_scraper_shared::error::ScraperError;
pub use nyxd_scraper_shared::{
BlockModule, DecodedMessage, MsgModule, NyxdScraperTransaction, ParsedTransactionDetails,
ParsedTransactionResponse, PruningOptions, PruningStrategy, StartingBlockOpts, TxModule,
parse_msg,
BlockModule, MsgModule, NyxdScraperTransaction, ParsedTransactionResponse, PruningOptions,
PruningStrategy, StartingBlockOpts, TxModule,
};
pub use storage::models;
@@ -132,15 +132,15 @@ impl SqliteStorageTransaction {
for chain_tx in txs {
insert_transaction(
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(),
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(),
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_details.tx.body.messages.iter().enumerate() {
for (index, msg) in chain_tx.tx.body.messages.iter().enumerate() {
insert_message(
chain_tx.tx_details.hash.to_string(),
chain_tx.hash.to_string(),
index as i64,
msg.type_url.clone(),
chain_tx.tx_details.height().into(),
chain_tx.height.into(),
self.0.as_mut(),
)
.await?
@@ -14,7 +14,6 @@ use tokio::sync::mpsc::Receiver;
#[derive(Hash, PartialOrd, PartialEq, Clone, Debug, Eq, Copy)]
pub enum PeerControlRequestTypeV2 {
AddPeer,
UpdatePeerPsk,
RemovePeer,
QueryPeer,
GetClientBandwidthByKey,
@@ -27,7 +26,6 @@ impl From<&PeerControlRequest> for PeerControlRequestTypeV2 {
fn from(req: &PeerControlRequest) -> Self {
match req {
PeerControlRequest::AddPeer { .. } => PeerControlRequestTypeV2::AddPeer,
PeerControlRequest::UpdatePeerPsk { .. } => PeerControlRequestTypeV2::UpdatePeerPsk,
PeerControlRequest::PreAllocateIpPair { .. } => PeerControlRequestTypeV2::AddPeer,
PeerControlRequest::RemovePeer { .. } => PeerControlRequestTypeV2::RemovePeer,
PeerControlRequest::QueryPeer { .. } => PeerControlRequestTypeV2::QueryPeer,
@@ -117,15 +115,6 @@ impl MockPeerControllerV2 {
)
.unwrap();
}
PeerControlRequest::UpdatePeerPsk { response_tx, .. } => {
response_tx
.send(
*response
.downcast()
.expect("registered response has mismatched type"),
)
.unwrap();
}
PeerControlRequest::PreAllocateIpPair { response_tx, .. } => {
response_tx
.send(
@@ -71,7 +71,6 @@ impl From<&Key> for KeyWrapper {
#[derive(Hash, PartialOrd, PartialEq, Clone, Debug, Eq)]
pub enum PeerControlRequestType {
AddPeer { public_key: KeyWrapper },
UpdatePeerPsk { peer_key: KeyWrapper },
AllocatePeerIpPair {},
ReleaseIpPair { ip_pair: IpPair },
RemovePeer { key: KeyWrapper },
@@ -87,7 +86,6 @@ impl PeerControlRequestType {
pub fn peer_key(&self) -> Option<KeyWrapper> {
match self {
PeerControlRequestType::AddPeer { public_key } => Some(public_key.clone()),
PeerControlRequestType::UpdatePeerPsk { peer_key } => Some(peer_key.clone()),
PeerControlRequestType::AllocatePeerIpPair {} => None,
PeerControlRequestType::ReleaseIpPair { .. } => None,
PeerControlRequestType::RemovePeer { key } => Some(key.clone()),
@@ -111,11 +109,6 @@ impl From<&PeerControlRequest> for PeerControlRequestType {
PeerControlRequest::AddPeer { peer, .. } => PeerControlRequestType::AddPeer {
public_key: (&peer.public_key).into(),
},
PeerControlRequest::UpdatePeerPsk { peer_key, .. } => {
PeerControlRequestType::UpdatePeerPsk {
peer_key: peer_key.into(),
}
}
PeerControlRequest::PreAllocateIpPair { .. } => {
PeerControlRequestType::AllocatePeerIpPair {}
}
@@ -278,9 +271,6 @@ impl MockPeerController {
}
response_tx.send_downcasted(response.content)
}
PeerControlRequest::UpdatePeerPsk { response_tx, .. } => {
response_tx.send_downcasted(response.content)
}
PeerControlRequest::PreAllocateIpPair { response_tx, .. } => {
response_tx.send_downcasted(response.content)
}
@@ -76,12 +76,6 @@ pub enum PeerControlRequest {
peer: Peer,
response_tx: oneshot::Sender<AddPeerControlResponse>,
},
/// Update PSK for an existing peer, without changing its IP allocation
UpdatePeerPsk {
peer_key: Key,
psk: Key,
response_tx: oneshot::Sender<UpdatePeerPskControlResponse>,
},
/// Attempt to allocate an IP pair from the pool
PreAllocateIpPair {
response_tx: oneshot::Sender<AllocatePeerControlResponse>,
@@ -124,7 +118,6 @@ pub enum PeerControlRequest {
}
pub type AddPeerControlResponse = Result<()>;
pub type UpdatePeerPskControlResponse = Result<()>;
pub type AllocatePeerControlResponse = Result<IpPair>;
pub type ReleaseIpPairControlResponse = Result<()>;
pub type RemovePeerControlResponse = Result<()>;
@@ -324,50 +317,6 @@ impl PeerController {
Ok(())
}
async fn handle_update_peer_psk_request(&mut self, peer_key: &Key, psk: Key) -> Result<()> {
// observation will get automatically added once dropped
let _metric_timer =
PROMETHEUS_METRICS.start_timer(PrometheusMetric::WireguardDefguardPeerPskUpdate);
nym_metrics::inc!("wg_peer_update_psk_attempts");
let Ok(Some(mut peer)) = self.handle_query_peer_by_key(peer_key).await else {
return Ok(());
};
let encoded_psk = psk.to_lower_hex();
peer.preshared_key = Some(psk);
// Account for bandwidth used so far *before* reconfiguring: `configure_peer`
// isn't guaranteed to preserve the kernel rx/tx counters, so fold the
// accrued bytes into the metrics first to avoid losing them on a reset.
if let Ok(host) = self.wg_api.read_interface_data() {
self.update_metrics(&host).await;
*self.host_information.write().await = host;
}
// Try to update WireGuard peer
if let Err(e) = self.wg_api.configure_peer(&peer) {
nym_metrics::inc!("wg_peer_update_psk_failed");
nym_metrics::inc!("wg_config_errors_total");
return Err(e.into());
};
// Persist the new PSK to disk so it survives a restart. Kernel-first: a
// failure here leaves the live session working, only risking drift on restart.
self.ecash_verifier
.storage()
.update_peer_psk(&peer_key.to_string(), Some(&encoded_psk))
.await?;
// Refresh again so the cached host information reflects the post-update state
if let Ok(host) = self.wg_api.read_interface_data() {
*self.host_information.write().await = host;
}
nym_metrics::inc!("wg_peer_update_psk_success");
Ok(())
}
/// Allocate IP pair from pool for a new peer registration
///
/// This only allocates IPs - the caller must handle database storage and
@@ -564,15 +513,6 @@ impl PeerController {
PeerControlRequest::AddPeer { peer, response_tx } => {
response_tx.send(self.handle_add_request(&peer).await).ok();
}
PeerControlRequest::UpdatePeerPsk {
peer_key,
psk,
response_tx,
} => {
response_tx
.send(self.handle_update_peer_psk_request(&peer_key, psk).await)
.ok();
}
PeerControlRequest::PreAllocateIpPair { response_tx } => {
response_tx.send(self.handle_ip_allocation_request()).ok();
}
-28
View File
@@ -1180,22 +1180,6 @@ 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"
@@ -1472,18 +1456,6 @@ 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"
+3 -5
View File
@@ -10,7 +10,6 @@ members = [
"multisig/cw4-group",
"vesting",
"performance",
"network-monitors",
]
[workspace.package]
@@ -20,8 +19,6 @@ 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
@@ -70,7 +67,6 @@ 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"
@@ -98,6 +94,8 @@ 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
@@ -1,4 +0,0 @@
[alias]
wasm = "build --release --lib --target wasm32-unknown-unknown"
unit-test = "test --lib"
schema = "run --bin schema --features=schema-gen"
-43
View File
@@ -1,43 +0,0 @@
[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
@@ -1,5 +0,0 @@
wasm:
RUSTFLAGS='-C link-arg=-s' cargo build --release --target wasm32-unknown-unknown
generate-schema:
cargo schema
@@ -1,368 +0,0 @@
{
"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"
}
}
}
}
}
@@ -1,125 +0,0 @@
{
"$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"
]
}
]
}
@@ -1,15 +0,0 @@
{
"$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
}
@@ -1,6 +0,0 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "MigrateMsg",
"type": "object",
"additionalProperties": false
}
@@ -1,64 +0,0 @@
{
"$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
}
]
}
@@ -1,15 +0,0 @@
{
"$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
}
@@ -1,74 +0,0 @@
{
"$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"
}
}
}
@@ -1,61 +0,0 @@
{
"$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"
}
}
}
@@ -1,14 +0,0 @@
// 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
@@ -1,179 +0,0 @@
// 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
@@ -1,11 +0,0 @@
// 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
@@ -1,345 +0,0 @@
// 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(())
}
}
}
@@ -1,2 +0,0 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
File diff suppressed because it is too large Load Diff
@@ -1,188 +0,0 @@
// 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
@@ -1,6 +1,6 @@
{
"nodes": 745,
"nodes": 744,
"locations": 77,
"mixnodes": 264,
"mixnodes": 263,
"exit_gateways": 474
}
@@ -1 +1 @@
Wednesday, May 6th 2026, 11:11:33 UTC
Thursday, April 30th 2026, 12:03:44 UTC
@@ -11,7 +11,7 @@ options:
--no_routing_history Display node stats without routing history
--no_verloc_metrics Display node stats without verloc metrics
-m, --markdown Display results in markdown format
-o [OUTPUT], --output [OUTPUT]
-o, --output [OUTPUT]
Save results to file (in current dir or supply with
path without filename)
```
@@ -13,8 +13,7 @@ usage: nym-node-cli install [-h] [-V] [-d BRANCH] [-v]
options:
-h, --help show this help message and exit
-V, --version show program's version number and exit
-d BRANCH, --dev BRANCH
Define github branch (default: develop)
-d, --dev BRANCH Define github branch (default: develop)
-v, --verbose Show full error tracebacks
--mode {mixnode,entry-gateway,exit-gateway}
Node mode: 'mixnode', 'entry-gateway', or 'exit-
@@ -6,79 +6,66 @@ Usage: nym-node run [OPTIONS]
Options:
--id <ID> Id of the nym-node to use [env: NYMNODE_ID=] [default: default-nym-node]
--config-file <CONFIG_FILE> Path to a configuration file of this node [env: NYMNODE_CONFIG=]
--accept-operator-terms-and-conditions Explicitly specify whether you agree with the terms and conditions of a nym node operator as defined at
<https://nymtech.net/terms-and-conditions/operators/v1.0.0> [env: NYMNODE_ACCEPT_OPERATOR_TERMS=]
--deny-init Forbid a new node from being initialised if configuration file for the provided specification doesn't already exist [env:
NYMNODE_DENY_INIT=]
--init-only If this is a brand new nym-node, specify whether it should only be initialised without actually running the subprocesses [env:
NYMNODE_INIT_ONLY=]
--accept-operator-terms-and-conditions Explicitly specify whether you agree with the terms and conditions of a nym node operator as defined at <https://nymtech.net/terms-and-conditions/operators/v1.0.0> [env:
NYMNODE_ACCEPT_OPERATOR_TERMS=]
--deny-init Forbid a new node from being initialised if configuration file for the provided specification doesn't already exist [env: NYMNODE_DENY_INIT=]
--init-only If this is a brand new nym-node, specify whether it should only be initialised without actually running the subprocesses [env: NYMNODE_INIT_ONLY=]
--local Flag specifying this node will be running in a local setting [env: NYMNODE_LOCAL=]
--mode [<MODE>...] Specifies the current mode(s) of this nym-node [env: NYMNODE_MODE=] [possible values: mixnode, entry-gateway, exit-gateway,
exit-providers-only]
--modes <MODES> Specifies the current mode(s) of this nym-node as a single flag [env: NYMNODE_MODES=] [possible values: mixnode, entry-gateway,
exit-gateway, exit-providers-only]
-w, --write-changes If this node has been initialised before, specify whether to write any new changes to the config file [env:
NYMNODE_WRITE_CONFIG_CHANGES=]
--bonding-information-output <BONDING_INFORMATION_OUTPUT> Specify output file for bonding information of this nym-node, i.e. its encoded keys. NOTE: the required bonding information is still a
subject to change and this argument should be treated only as a preview of future features [env: NYMNODE_BONDING_INFORMATION_OUTPUT=]
-o, --output <OUTPUT> Specify the output format of the bonding information (`text` or `json`) [env: NYMNODE_OUTPUT=] [default: text] [possible values: text,
json]
--public-ips <PUBLIC_IPS> Comma separated list of public ip addresses that will be announced to the nym-api and subsequently to the clients. In nearly all
circumstances, it's going to be identical to the address you're going to use for bonding [env: NYMNODE_PUBLIC_IPS=]
--hostname <HOSTNAME> Optional hostname associated with this gateway that will be announced to the nym-api and subsequently to the clients [env:
NYMNODE_HOSTNAME=]
--location <LOCATION> Optional **physical** location of this node's server. Either full country name (e.g. 'Poland'), two-letter alpha2 (e.g. 'PL'),
three-letter alpha3 (e.g. 'POL') or three-digit numeric-3 (e.g. '616') can be provided [env: NYMNODE_LOCATION=]
--mode [<MODE>...] Specifies the current mode(s) of this nym-node [env: NYMNODE_MODE=] [possible values: mixnode, entry-gateway, exit-gateway, exit-providers-only]
--modes <MODES> Specifies the current mode(s) of this nym-node as a single flag [env: NYMNODE_MODES=] [possible values: mixnode, entry-gateway, exit-gateway, exit-providers-only]
-w, --write-changes If this node has been initialised before, specify whether to write any new changes to the config file [env: NYMNODE_WRITE_CONFIG_CHANGES=]
--bonding-information-output <BONDING_INFORMATION_OUTPUT> Specify output file for bonding information of this nym-node, i.e. its encoded keys. NOTE: the required bonding information is still a subject to change and this argument
should be treated only as a preview of future features [env: NYMNODE_BONDING_INFORMATION_OUTPUT=]
-o, --output <OUTPUT> Specify the output format of the bonding information (`text` or `json`) [env: NYMNODE_OUTPUT=] [default: text] [possible values: text, json]
--public-ips <PUBLIC_IPS> Comma separated list of public ip addresses that will be announced to the nym-api and subsequently to the clients. In nearly all circumstances, it's going to be identical to
the address you're going to use for bonding [env: NYMNODE_PUBLIC_IPS=]
--hostname <HOSTNAME> Optional hostname associated with this gateway that will be announced to the nym-api and subsequently to the clients [env: NYMNODE_HOSTNAME=]
--location <LOCATION> Optional **physical** location of this node's server. Either full country name (e.g. 'Poland'), two-letter alpha2 (e.g. 'PL'), three-letter alpha3 (e.g. 'POL') or three-digit
numeric-3 (e.g. '616') can be provided [env: NYMNODE_LOCATION=]
--http-bind-address <HTTP_BIND_ADDRESS> Socket address this node will use for binding its http API. default: `[::]:8080` [env: NYMNODE_HTTP_BIND_ADDRESS=]
--landing-page-assets-path <LANDING_PAGE_ASSETS_PATH> Path to assets directory of custom landing page of this node [env: NYMNODE_HTTP_LANDING_ASSETS=]
--http-access-token <HTTP_ACCESS_TOKEN> An optional bearer token for accessing certain http endpoints. Currently only used for prometheus metrics [env:
NYMNODE_HTTP_ACCESS_TOKEN=]
--expose-system-info <EXPOSE_SYSTEM_INFO> Specify whether basic system information should be exposed. default: true [env: NYMNODE_HTTP_EXPOSE_SYSTEM_INFO=] [possible values:
true, false]
--expose-system-hardware <EXPOSE_SYSTEM_HARDWARE> Specify whether basic system hardware information should be exposed. default: true [env: NYMNODE_HTTP_EXPOSE_SYSTEM_HARDWARE=]
[possible values: true, false]
--expose-crypto-hardware <EXPOSE_CRYPTO_HARDWARE> Specify whether detailed system crypto hardware information should be exposed. default: true [env:
NYMNODE_HTTP_EXPOSE_CRYPTO_HARDWARE=] [possible values: true, false]
--http-access-token <HTTP_ACCESS_TOKEN> An optional bearer token for accessing certain http endpoints. Currently only used for prometheus metrics [env: NYMNODE_HTTP_ACCESS_TOKEN=]
--expose-system-info <EXPOSE_SYSTEM_INFO> Specify whether basic system information should be exposed. default: true [env: NYMNODE_HTTP_EXPOSE_SYSTEM_INFO=] [possible values: true, false]
--expose-system-hardware <EXPOSE_SYSTEM_HARDWARE> Specify whether basic system hardware information should be exposed. default: true [env: NYMNODE_HTTP_EXPOSE_SYSTEM_HARDWARE=] [possible values: true, false]
--expose-crypto-hardware <EXPOSE_CRYPTO_HARDWARE> Specify whether detailed system crypto hardware information should be exposed. default: true [env: NYMNODE_HTTP_EXPOSE_CRYPTO_HARDWARE=] [possible values: true, false]
--mixnet-bind-address <MIXNET_BIND_ADDRESS> Address this node will bind to for listening for mixnet packets default: `[::]:1789` [env: NYMNODE_MIXNET_BIND_ADDRESS=]
--mixnet-announce-port <MIXNET_ANNOUNCE_PORT> If applicable, custom port announced in the self-described API that other clients and nodes will use. Useful when the node is behind a
proxy [env: NYMNODE_MIXNET_ANNOUNCE_PORT=]
--mixnet-announce-port <MIXNET_ANNOUNCE_PORT> If applicable, custom port announced in the self-described API that other clients and nodes will use. Useful when the node is behind a proxy [env:
NYMNODE_MIXNET_ANNOUNCE_PORT=]
--nym-api-urls <NYM_API_URLS> Addresses to nym APIs from which the node gets the view of the network [env: NYMNODE_NYM_APIS=]
--nyxd-urls <NYXD_URLS> Addresses to nyxd chain endpoint which the node will use for chain interactions [env: NYMNODE_NYXD=]
--enable-console-logging <ENABLE_CONSOLE_LOGGING> Specify whether running statistics of this node should be logged to the console [env: NYMNODE_ENABLE_CONSOLE_LOGGING=] [possible
values: true, false]
--enable-console-logging <ENABLE_CONSOLE_LOGGING> Specify whether running statistics of this node should be logged to the console [env: NYMNODE_ENABLE_CONSOLE_LOGGING=] [possible values: true, false]
--wireguard-enabled <WIREGUARD_ENABLED> Specifies whether the wireguard service is enabled on this node [env: NYMNODE_WG_ENABLED=] [possible values: true, false]
--wireguard-bind-address <WIREGUARD_BIND_ADDRESS> Socket address this node will use for binding its wireguard interface. default: `[::]:51822` [env: NYMNODE_WG_BIND_ADDRESS=]
--wireguard-tunnel-announced-port <WIREGUARD_TUNNEL_ANNOUNCED_PORT> Tunnel port announced to external clients wishing to connect to the wireguard interface. Useful in the instances where the node is
behind a proxy [env: NYMNODE_WG_ANNOUNCED_PORT=]
--wireguard-private-network-prefix <WIREGUARD_PRIVATE_NETWORK_PREFIX> The prefix denoting the maximum number of the clients that can be connected via Wireguard. The maximum value for IPv4 is 32 and for
IPv6 is 128 [env: NYMNODE_WG_PRIVATE_NETWORK_PREFIX=]
--wireguard-userspace <WIREGUARD_USERSPACE> Use userspace implementation of WireGuard (wireguard-go) instead of kernel module. Useful in containerized environments without kernel
WireGuard support [env: NYMNODE_WG_USERSPACE=] [possible values: true, false]
--wireguard-tunnel-announced-port <WIREGUARD_TUNNEL_ANNOUNCED_PORT> Tunnel port announced to external clients wishing to connect to the wireguard interface. Useful in the instances where the node is behind a proxy [env:
NYMNODE_WG_ANNOUNCED_PORT=]
--wireguard-private-network-prefix <WIREGUARD_PRIVATE_NETWORK_PREFIX> The prefix denoting the maximum number of the clients that can be connected via Wireguard. The maximum value for IPv4 is 32 and for IPv6 is 128 [env:
NYMNODE_WG_PRIVATE_NETWORK_PREFIX=]
--wireguard-userspace <WIREGUARD_USERSPACE> Use userspace implementation of WireGuard (wireguard-go) instead of kernel module. Useful in containerized environments without kernel WireGuard support [env:
NYMNODE_WG_USERSPACE=] [possible values: true, false]
--verloc-bind-address <VERLOC_BIND_ADDRESS> Socket address this node will use for binding its verloc API. default: `[::]:1790` [env: NYMNODE_VERLOC_BIND_ADDRESS=]
--verloc-announce-port <VERLOC_ANNOUNCE_PORT> If applicable, custom port announced in the self-described API that other clients and nodes will use. Useful when the node is behind a
proxy [env: NYMNODE_VERLOC_ANNOUNCE_PORT=]
--verloc-announce-port <VERLOC_ANNOUNCE_PORT> If applicable, custom port announced in the self-described API that other clients and nodes will use. Useful when the node is behind a proxy [env:
NYMNODE_VERLOC_ANNOUNCE_PORT=]
--entry-bind-address <ENTRY_BIND_ADDRESS> Socket address this node will use for binding its client websocket API. default: `[::]:9000` [env: NYMNODE_ENTRY_BIND_ADDRESS=]
--announce-ws-port <ANNOUNCE_WS_PORT> Custom announced port for listening for websocket client traffic. If unspecified, the value from the `bind_address` will be used
instead [env: NYMNODE_ENTRY_ANNOUNCE_WS_PORT=]
--announce-ws-port <ANNOUNCE_WS_PORT> Custom announced port for listening for websocket client traffic. If unspecified, the value from the `bind_address` will be used instead [env:
NYMNODE_ENTRY_ANNOUNCE_WS_PORT=]
--announce-wss-port <ANNOUNCE_WSS_PORT> If applicable, announced port for listening for secure websocket client traffic [env: NYMNODE_ENTRY_ANNOUNCE_WSS_PORT=]
--enforce-zk-nyms <ENFORCE_ZK_NYMS> Indicates whether this gateway is accepting only coconut credentials for accessing the mixnet or if it also accepts non-paying clients
[env: NYMNODE_ENFORCE_ZK_NYMS=] [possible values: true, false]
--mnemonic <MNEMONIC> Custom cosmos wallet mnemonic used for zk-nym redemption. If no value is provided, a fresh mnemonic is going to be generated [env:
NYMNODE_MNEMONIC=]
--upgrade-mode-attestation-url <UPGRADE_MODE_ATTESTATION_URL> Endpoint to query to retrieve current upgrade mode attestation. This argument should never be set outside testnets and local networks
[env: NYMNODE_UPGRADE_MODE_ATTESTATION_URL=]
--upgrade-mode-attester-public-key <UPGRADE_MODE_ATTESTER_PUBLIC_KEY> Expected public key of the entity signing the published attestation. This argument should never be set outside testnets and local
networks [env: NYMNODE_UPGRADE_MODE_ATTESTER_PUBKEY=]
--enforce-zk-nyms <ENFORCE_ZK_NYMS> Indicates whether this gateway is accepting only coconut credentials for accessing the mixnet or if it also accepts non-paying clients [env: NYMNODE_ENFORCE_ZK_NYMS=]
[possible values: true, false]
--mnemonic <MNEMONIC> Custom cosmos wallet mnemonic used for zk-nym redemption. If no value is provided, a fresh mnemonic is going to be generated [env: NYMNODE_MNEMONIC=]
--upgrade-mode-attestation-url <UPGRADE_MODE_ATTESTATION_URL> Endpoint to query to retrieve current upgrade mode attestation. This argument should never be set outside testnets and local networks [env:
NYMNODE_UPGRADE_MODE_ATTESTATION_URL=]
--upgrade-mode-attester-public-key <UPGRADE_MODE_ATTESTER_PUBLIC_KEY> Expected public key of the entity signing the published attestation. This argument should never be set outside testnets and local networks [env:
NYMNODE_UPGRADE_MODE_ATTESTER_PUBKEY=]
--upstream-exit-policy-url <UPSTREAM_EXIT_POLICY_URL> Specifies the url for an upstream source of the exit policy used by this node [env: NYMNODE_UPSTREAM_EXIT_POLICY=]
--open-proxy <OPEN_PROXY> Specifies whether this exit node should run in 'open-proxy' mode and thus would attempt to resolve **ANY** request it receives [env:
NYMNODE_OPEN_PROXY=] [possible values: true, false]
--open-proxy <OPEN_PROXY> Specifies whether this exit node should run in 'open-proxy' mode and thus would attempt to resolve **ANY** request it receives [env: NYMNODE_OPEN_PROXY=] [possible values:
true, false]
--lp-control-bind-address <LP_CONTROL_BIND_ADDRESS> Bind address for the TCP LP control traffic. default: `[::]:41264` [env: NYMNODE_LP_CONTROL_BIND_ADDRESS=]
--lp-control-announce-port <LP_CONTROL_ANNOUNCE_PORT> Custom announced port for listening for the TCP LP control traffic. If unspecified, the value from the `lp_control_bind_address` will
be used instead [env: NYMNODE_LP_CONTROL_ANNOUNCE_PORT=]
--lp-control-announce-port <LP_CONTROL_ANNOUNCE_PORT> Custom announced port for listening for the TCP LP control traffic. If unspecified, the value from the `lp_control_bind_address` will be used instead [env:
NYMNODE_LP_CONTROL_ANNOUNCE_PORT=]
--lp-data-bind-address <LP_DATA_BIND_ADDRESS> Bind address for the UDP LP data traffic. default: `[::]:51264` [env: NYMNODE_LP_DATA_BIND_ADDRESS=]
--lp-data-announce-port <LP_DATA_ANNOUNCE_PORT> Custom announced port for listening for the UDP LP data traffic. If unspecified, the value from the `lp_data_bind_address` will be
used instead [env: NYMNODE_LP_DATA_ANNOUNCE_PORT=]
--lp-use-mock-ecash <LP_USE_MOCK_ECASH> Use mock ecash manager for LP testing. WARNING: Only use this for local testing! Never enable in production. When enabled, the LP
listener will accept any credential without blockchain verification [env: NYMNODE_LP_USE_MOCK_ECASH=] [possible values: true, false]
--lp-data-announce-port <LP_DATA_ANNOUNCE_PORT> Custom announced port for listening for the UDP LP data traffic. If unspecified, the value from the `lp_data_bind_address` will be used instead [env:
NYMNODE_LP_DATA_ANNOUNCE_PORT=]
--lp-use-mock-ecash <LP_USE_MOCK_ECASH> Use mock ecash manager for LP testing. WARNING: Only use this for local testing! Never enable in production. When enabled, the LP listener will accept any credential without
blockchain verification [env: NYMNODE_LP_USE_MOCK_ECASH=] [possible values: true, false]
-h, --help Print help
```
@@ -57,44 +57,6 @@ This page displays a full list of all the changes during our release cycle from
<VarInfo />
## `v2026.9-venaco`
- [Release Binaries](https://github.com/nymtech/nym/releases/tag/nym-binaries-v2026.9-venaco)
- [`nym-node`](nodes/nym-node.mdx) version `1.31.0`
```sh
nym-node
Binary Name: nym-node
Build Timestamp: 2026-05-06T05:19:51.973494120Z
Build Version: 1.31.0
Commit SHA: f84de25302e886d4bd97a898885c569724c002a7
Commit Date: 2026-05-06T07:16:42.000000000+02:00
Commit Branch: HEAD
rustc Version: 1.91.1
rustc Channel: stable
cargo Profile: release
```
### Operators Updates & Tools
- [**NIP-11 is out, please vote!**](https://governator.nym.com/proposal/prop-fe98cb2a-fa34-4e7d-82ac-4aa0e6e64aa2) Another upgrade of Nym exit policy opened for operators decision, this time expanding ports to support Gemini and enabling partial Telegram support for NymVPN users.
### Features
- [Only init `SHARED_CLIENT` if requested](https://github.com/nymtech/nym/pull/6708): This PR prevents shared reqwest client from being initialized even when `self.reqwest_client` is used instead
- [Return IPv6 addresses](https://github.com/nymtech/nym/pull/6684)
- [Block non-public IPR/NR checks](https://github.com/nymtech/nym/pull/6670)
### Bugfix
- [Fix for v9 IPR ](https://github.com/nymtech/nym/pull/6710)
- [Fixes to crates and CI](https://github.com/nymtech/nym/pull/6686)
- [Fix invalid ticket spend](https://github.com/nymtech/nym/pull/6683)
## `v2026.8-urda`
- [Release Binaries](https://github.com/nymtech/nym/releases/tag/nym-binaries-v2026.8-urda)
@@ -19,12 +19,11 @@ This documentation page provides a guide on how to set up and run a [NYM NODE](.
## Current version
```sh
nym-node
Binary Name: nym-node
Build Timestamp: 2026-05-06T05:19:51.973494120Z
Build Version: 1.31.0
Commit SHA: f84de25302e886d4bd97a898885c569724c002a7
Commit Date: 2026-05-06T07:16:42.000000000+02:00
Build Timestamp: 2026-04-21T10:11:28.164080002Z
Build Version: 1.30.0
Commit SHA: 0c83ae2408ea9efcc1abae613711c6e4e16148ad
Commit Date: 2026-04-21T12:06:23.000000000+02:00
Commit Branch: HEAD
rustc Version: 1.91.1
rustc Channel: stable
-1
View File
@@ -17,7 +17,6 @@ GROUP_CONTRACT_ADDRESS=n1ewmwz97xm0h8rdk8sw7h9mwn866qkx9hl9zlmagqfkhuzvwk5hhq844
MULTISIG_CONTRACT_ADDRESS=n1tz0setr8vkh9udp8xyxgpqc89ns27k4d0jx2h942hr0ax63yjhmqz6xct8
COCONUT_DKG_CONTRACT_ADDRESS=n1v3n2ly2dp3a9ng3ff6rh26yfkn0pc5hed7w2shc5u9ca5c865utqj5elvh
ECASH_CONTRACT_ADDRESS=n1v3vydvs2ued84yv3khqwtgldmgwn0elljsdh08dr5s2j9x4rc5fs9jlwz9
NETWORK_MONITORS_CONTRACT_ADDRESS=n1x5krtvyqklj360x38v62ze42g8s8trfsfqzlv8c9296chcpvqadssqnem5
STATISTICS_SERVICE_DOMAIN_ADDRESS="http://0.0.0.0"
NYXD=https://rpc.sandbox.nymtech.net
@@ -241,7 +241,7 @@ impl<R, S> AuthenticatedHandler<R, S> {
.inner
.shared_state
.outbound_mix_sender
.forward_client_packet_without_delay(mix_packet)
.forward_packet(mix_packet)
{
error!("We failed to forward requested mix packet - {err}. Presumably our mix forwarder has crashed. We cannot continue.");
process::exit(1);
+2 -5
View File
@@ -77,8 +77,6 @@ pub struct LocalAuthenticatorOpts {
pub struct GatewayTasksBuilder {
config: Config,
network: NymNetworkDetails,
network_requester_opts: Option<LocalNetworkRequesterOpts>,
ip_packet_router_opts: Option<LocalIpPacketRouterOpts>,
@@ -122,7 +120,6 @@ impl GatewayTasksBuilder {
#[allow(clippy::too_many_arguments)]
pub fn new(
config: Config,
network: NymNetworkDetails,
identity: Arc<ed25519::KeyPair>,
storage: GatewayStorage,
mix_packet_sender: MixForwardingSender,
@@ -136,7 +133,6 @@ impl GatewayTasksBuilder {
) -> GatewayTasksBuilder {
GatewayTasksBuilder {
config,
network,
network_requester_opts: None,
ip_packet_router_opts: None,
authenticator_opts: None,
@@ -188,7 +184,8 @@ impl GatewayTasksBuilder {
.choose(&mut thread_rng())
.ok_or(GatewayError::NoNyxdAvailable)?;
let client_config = nyxd::Config::try_from_nym_network_details(&self.network)?;
let network_details = NymNetworkDetails::new_from_env();
let client_config = nyxd::Config::try_from_nym_network_details(&network_details)?;
let nyxd_client = DirectSigningHttpRpcNyxdClient::connect_with_mnemonic(
client_config,
@@ -15,14 +15,25 @@ use std::time::Instant;
impl PeerRegistrator {
/// In the case of an already registered WG peer, update its PSK.
///
/// The peer controller keeps the active config and the on-disk PSK in sync.
pub(super) async fn update_peer_psk(
&self,
peer: PeerPublicKey,
psk: Key,
) -> Result<(), GatewayWireguardError> {
self.peer_manager.update_peer_psk(peer, psk).await
// 1. check if the peer is currently being handled
if self.peer_manager.check_active_peer(peer).await? {
// 2. if so, force disconnect it (as we're handling new request from the same peer)
self.peer_manager.remove_peer(peer).await?;
}
// 3. update the on-disk PSK
let encoded_psk = psk.to_lower_hex();
self.ecash_verifier
.storage()
.update_peer_psk(&peer.to_string(), Some(&encoded_psk))
.await?;
Ok(())
}
fn lp_peer_to_final_response(
@@ -125,44 +125,6 @@ impl PeerManager {
res
}
pub async fn update_peer_psk(
&self,
pub_key: PeerPublicKey,
psk: Key,
) -> Result<(), GatewayWireguardError> {
let controller_start = Instant::now();
let peer_key = Key::new(pub_key.to_bytes());
let (response_tx, response_rx) = oneshot::channel();
let msg = PeerControlRequest::UpdatePeerPsk {
peer_key,
psk,
response_tx,
};
self.wireguard_gateway_data
.peer_tx()
.send(msg)
.await
.map_err(|_| GatewayWireguardError::PeerInteractionStopped)?;
let res = response_rx
.await
.map_err(|_| GatewayWireguardError::internal("no response for update peer psk"))?
.map_err(|err| {
GatewayWireguardError::InternalError(format!(
"updating peer psk could not be performed: {err:?}"
))
});
let latency = controller_start.elapsed().as_secs_f64();
add_histogram_obs!(
"wg_peer_controller_channel_latency_seconds",
latency,
WG_CONTROLLER_LATENCY_BUCKETS
);
res
}
pub async fn remove_peer(&self, pub_key: PeerPublicKey) -> Result<(), GatewayWireguardError> {
let controller_start = Instant::now();
let key = Key::new(pub_key.to_bytes());
+1 -1
View File
@@ -3,7 +3,7 @@
[package]
name = "nym-api"
version = "1.1.80"
version = "1.1.79"
authors.workspace = true
edition = "2021"
license = "GPL-3.0"
@@ -1,29 +0,0 @@
/*
* Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
* SPDX-License-Identifier: GPL-3.0-only
*/
CREATE TABLE nym_node_stress_testing_result
(
-- Orchestrator-local testrun id that produced this result. Paired with `submitter_pubkey`
-- it uniquely identifies a measurement and lets us dedupe retried submissions (the
-- orchestrator uses at-least-once delivery and may re-POST the same row after a crash
-- between a successful POST and its watermark update).
testrun_id INTEGER NOT NULL,
-- Base58-encoded ed25519 identity key of the submitting orchestrator. Part of the primary
-- key so distinct orchestrators can coincidentally share a `testrun_id` without colliding.
submitter_pubkey TEXT NOT NULL,
-- unfortunately, due to legacy reasons we have separate tables for mixnodes and gateways
-- so that we can't put a reference constraint here
node_id INTEGER NOT NULL,
result REAL NOT NULL,
was_reachable BOOLEAN NOT NULL,
test_timestamp TIMESTAMP WITHOUT TIME ZONE NOT NULL,
PRIMARY KEY (testrun_id, submitter_pubkey)
);
@@ -2,8 +2,6 @@
// SPDX-License-Identifier: Apache-2.0
use crate::pagination::PaginatedResponse;
use nym_crypto::asymmetric::ed25519;
use nym_crypto::asymmetric::ed25519::serde_helpers::bs58_ed25519_pubkey;
use nym_mixnet_contract_common::NodeId;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
@@ -74,111 +72,3 @@ pub struct GatewayCoreStatusResponse {
pub identity: String,
pub count: i64,
}
pub use v3::*;
/// Request/response types for the v3 network-monitor flow, in which an orchestrator submits
/// stress testing results to nym-api via signed batches.
pub mod v3 {
use super::*;
use crate::signable::SignedMessage;
use std::time::Duration;
use time::OffsetDateTime;
/// Signed envelope posted by a network monitor orchestrator to
/// `POST /v3/nym-nodes/stress-testing/batch-submit`.
///
/// The signature is checked against the `signer` field of the inner
/// [`StressTestBatchSubmissionContent`], which must also match one of the orchestrators
/// registered in the network-monitors contract.
pub type StressTestBatchSubmission = SignedMessage<StressTestBatchSubmissionContent>;
/// Confirmation returned to an orchestrator after a successful submission.
/// Currently empty — exists to give the response an explicit type rather than
/// relying on `Json(())`.
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
pub struct StressTestBatchSubmissionResponse {}
/// Single stress-test measurement for one node, produced by a network monitor orchestrator.
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
pub struct StressTestResult {
/// Orchestrator-local id of the test run that produced this result. Combined with the
/// batch's `signer` it uniquely identifies the measurement, allowing nym-api to dedupe
/// retried submissions on the at-least-once delivery path.
pub testrun_id: i64,
/// Contract-assigned id of the node that was tested.
pub node_id: NodeId,
/// Whether the tested node was acting as a mixnode during the measurement.
///
/// Included explicitly (rather than inferred from on-chain role) so the API can reject or
/// route entries that don't match the expected role without re-querying the contract.
pub is_mixnode: bool,
#[schema(value_type = String)]
#[serde(with = "time::serde::rfc3339")]
pub test_timestamp: OffsetDateTime,
/// Measured performance score in the `[0.0, 1.0]` range.
pub test_performance: f64,
/// Whether the node responded at all during testing.
///
/// Recorded alongside `test_performance` so that a genuine 0.0 score (node responded but
/// dropped everything) can be distinguished from the node being offline entirely.
pub was_reachable: bool,
}
/// Body of a stress-test batch submission, signed by a network monitor orchestrator.
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
pub struct StressTestBatchSubmissionContent {
/// ed25519 identity key of the submitting orchestrator. Must match an entry in the
/// network-monitors contract for the batch to be accepted.
#[schema(value_type = String)]
#[serde(with = "ed25519::bs58_ed25519_pubkey")]
pub signer: ed25519::PublicKey,
/// Time at which this batch was produced. Also used as a monotonic nonce for replay
/// protection: the API rejects submissions whose timestamp is not strictly greater than
/// the orchestrator's previous accepted submission.
#[schema(value_type = String)]
#[serde(with = "time::serde::rfc3339")]
pub timestamp: OffsetDateTime,
pub results: Vec<StressTestResult>,
}
impl StressTestBatchSubmissionContent {
/// Build a batch submission body stamped with the current UTC time.
pub fn new(signer: ed25519::PublicKey, results: Vec<StressTestResult>) -> Self {
StressTestBatchSubmissionContent {
signer,
timestamp: OffsetDateTime::now_utc(),
results,
}
}
/// Whether this submission is older than `max_age` relative to the current UTC time.
///
/// Used server-side to reject submissions that have been sitting around too long, even if
/// they are otherwise well-formed and correctly signed.
pub fn is_stale(&self, max_age: Duration) -> bool {
self.timestamp + max_age < OffsetDateTime::now_utc()
}
}
/// Response body for `GET /v3/nym-nodes/stress-testing/known-monitors/{identity_key}`,
/// used by orchestrators to check whether this nym-api currently recognises their key.
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
pub struct KnownNetworkMonitorResponse {
/// The ed25519 identity key that was queried (base58-encoded on the wire).
#[serde(with = "bs58_ed25519_pubkey")]
#[schema(value_type = String)]
pub identity_key: ed25519::PublicKey,
/// Whether the queried identity key is currently recognised by this nym-api
/// as an authorised network monitor permitted to submit stress testing results.
pub authorised: bool,
}
}

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