Compare commits
103 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b6eb391e85 | |||
| 931ec03b28 | |||
| 8a93bce32f | |||
| 56846fee77 | |||
| 52949f825a | |||
| 2705330595 | |||
| fa842ceb4f | |||
| 3730260cf0 | |||
| 6bf48de7ba | |||
| 799dfd59bb | |||
| f944348216 | |||
| 26955dd74b | |||
| 7c890ea0c5 | |||
| 1bc5169691 | |||
| a900656ec8 | |||
| 4fcec99cc2 | |||
| 8ce06dbc0e | |||
| b866be4fcf | |||
| 37bd66236a | |||
| 906a93719f | |||
| 54779175f1 | |||
| 6f5e831127 | |||
| 13d48b4bb6 | |||
| 7374ceae6f | |||
| 500200db45 | |||
| b9cd2aa12e | |||
| 002bb3b0f8 | |||
| fcab100ec7 | |||
| 34709e76a1 | |||
| f7fbf942f9 | |||
| d609d30f3a | |||
| 2c1b5f59a3 | |||
| c52fc0c9af | |||
| 153645dabf | |||
| 959a986e2c | |||
| 9ff2ec249d | |||
| c85fb161d4 | |||
| e27cf142f9 | |||
| a9bf1954bc | |||
| b6202b5a6b | |||
| e8410b2302 | |||
| cff370a943 | |||
| 08dc353e82 | |||
| 495f020730 | |||
| 8de781f750 | |||
| 225024d428 | |||
| 367716612f | |||
| e82b116230 | |||
| c7780d2d34 | |||
| 4ad00dba3d | |||
| 7324bb23b6 | |||
| de9fa97129 | |||
| 298b55280e | |||
| b41ea3628f | |||
| dad2d30773 | |||
| 8392f7da94 | |||
| cc405cc802 | |||
| 3dd6b907fe | |||
| 2fd26581eb | |||
| aa7b1e939a | |||
| 639c7f83a4 | |||
| 4ce136ccf0 | |||
| 14a85901b4 | |||
| 0796e9e0a6 | |||
| a98a65c16d | |||
| 11320e3f6a | |||
| a52a8c3e81 | |||
| 23e6169c02 | |||
| 17d3791b8e | |||
| c9a9940cb9 | |||
| ff0ecc95fb | |||
| d791e08fac | |||
| 1532c0c16e | |||
| d37b4226d0 | |||
| 43a1bd38e8 | |||
| f28b1e2077 | |||
| dd8c0a2521 | |||
| dc64fb622c | |||
| 86021937df | |||
| e7057f3932 | |||
| f6f01d3700 | |||
| 128f727376 | |||
| a85256c8c4 | |||
| 7310d63f1b | |||
| 25eba09b92 | |||
| a8cecb1200 | |||
| 343a48c297 | |||
| 4e52e9bf77 | |||
| cf55e2fe86 | |||
| 9782bae54b | |||
| 526cb9b8be | |||
| dc0835f1f3 | |||
| b5a8b9d283 | |||
| a395167139 | |||
| 6b98c168fc | |||
| 4645de3eb5 | |||
| e6dd670b16 | |||
| dc48750271 | |||
| 626d013547 | |||
| 46c67440bb | |||
| e5cd9fd69e | |||
| 21c14c0df0 | |||
| 87c236a927 |
@@ -0,0 +1,2 @@
|
||||
[target.wasm32-unknown-unknown]
|
||||
rustflags = ["--cfg=getrandom_backend=\"wasm_js\""]
|
||||
@@ -23,10 +23,10 @@ jobs:
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v5.0.0
|
||||
with:
|
||||
version: 9
|
||||
version: 11.1.2
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
node-version: 24
|
||||
- name: Install Rust toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
|
||||
@@ -17,13 +17,16 @@ jobs:
|
||||
run: sudo apt-get install rsync
|
||||
continue-on-error: true
|
||||
- uses: rlespinasse/github-slug-action@v3.x
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v5.0.0
|
||||
with:
|
||||
version: 11.1.2
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
- name: Setup yarn
|
||||
run: npm install -g yarn
|
||||
node-version: 24
|
||||
cache: pnpm
|
||||
- name: Build
|
||||
run: yarn && yarn build && yarn build:ci:storybook
|
||||
run: pnpm install && pnpm build && pnpm build:ci:storybook
|
||||
- name: Deploy branch to CI www (storybook)
|
||||
continue-on-error: true
|
||||
uses: easingthemes/ssh-deploy@main
|
||||
|
||||
@@ -23,7 +23,6 @@ on:
|
||||
- 'sdk/ffi/**'
|
||||
- 'sdk/rust/**'
|
||||
- 'service-providers/**'
|
||||
- 'nym-browser-extension/storage/**'
|
||||
- 'tools/**'
|
||||
- 'wasm/**'
|
||||
- 'Cargo.toml'
|
||||
|
||||
@@ -40,7 +40,7 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "20"
|
||||
node-version: 24
|
||||
|
||||
- name: Validate version format
|
||||
run: |
|
||||
|
||||
@@ -42,7 +42,7 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "20"
|
||||
node-version: 24
|
||||
|
||||
- name: Validate version format
|
||||
run: |
|
||||
|
||||
@@ -7,7 +7,10 @@ on:
|
||||
paths:
|
||||
- "documentation/docs/**"
|
||||
- "sdk/typescript/packages/sdk/src/**"
|
||||
- "sdk/typescript/packages/mix-tunnel/src/**"
|
||||
- "sdk/typescript/packages/mix-fetch/src/**"
|
||||
- "sdk/typescript/packages/mix-dns/src/**"
|
||||
- "sdk/typescript/packages/mix-websocket/src/**"
|
||||
- ".github/workflows/ci-docs.yml"
|
||||
|
||||
jobs:
|
||||
@@ -30,10 +33,10 @@ jobs:
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v5.0.0
|
||||
with:
|
||||
version: 9
|
||||
version: 11.1.2
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
node-version: 24
|
||||
- name: Install Rust toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
@@ -47,7 +50,7 @@ jobs:
|
||||
- name: Check if TypeScript SDK source changed
|
||||
id: check-ts-sdk
|
||||
run: |
|
||||
if git diff --name-only ${{ github.event.before }} ${{ github.sha }} | grep -qE '^sdk/typescript/packages/(sdk|mix-fetch)/src/'; then
|
||||
if git diff --name-only ${{ github.event.before }} ${{ github.sha }} | grep -qE '^sdk/typescript/packages/(sdk|mix-tunnel|mix-fetch|mix-dns|mix-websocket)/src/'; then
|
||||
echo "changed=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "changed=false" >> $GITHUB_OUTPUT
|
||||
@@ -58,8 +61,11 @@ jobs:
|
||||
if: steps.check-ts-sdk.outputs.changed == 'true'
|
||||
run: |
|
||||
npm install -g typedoc@0.25.13 typedoc-plugin-markdown@4.0.3
|
||||
cd ${{ github.workspace }}/sdk/typescript/packages/sdk && typedoc --skipErrorChecking
|
||||
cd ${{ github.workspace }}/sdk/typescript/packages/mix-fetch && typedoc --skipErrorChecking
|
||||
cd ${{ github.workspace }}/sdk/typescript/packages/sdk && typedoc --skipErrorChecking
|
||||
cd ${{ github.workspace }}/sdk/typescript/packages/mix-tunnel && typedoc --skipErrorChecking
|
||||
cd ${{ github.workspace }}/sdk/typescript/packages/mix-fetch && typedoc --skipErrorChecking
|
||||
cd ${{ github.workspace }}/sdk/typescript/packages/mix-dns && typedoc --skipErrorChecking
|
||||
cd ${{ github.workspace }}/sdk/typescript/packages/mix-websocket && typedoc --skipErrorChecking
|
||||
|
||||
- name: Verify doc versions
|
||||
run: ${{ github.workspace }}/documentation/scripts/verify-doc-versions.sh
|
||||
|
||||
@@ -20,12 +20,14 @@ jobs:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: rlespinasse/github-slug-action@v3.x
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v5.0.0
|
||||
with:
|
||||
version: 11.1.2
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Setup yarn
|
||||
run: npm install -g yarn
|
||||
node-version: 24
|
||||
cache: pnpm
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
@@ -38,22 +40,24 @@ jobs:
|
||||
- name: Install wasm-opt
|
||||
run: cargo install wasm-opt
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "1.24.6"
|
||||
# Produce wasm/smolmix/pkg/package.json before any pnpm step. The
|
||||
# `pnpm dev:on` in `prebuild:ci` adds wasm/smolmix/pkg to the dynamic
|
||||
# workspace; mix-tunnel's `workspace:*` lookup against @nymproject/
|
||||
# smolmix-wasm needs the package.json to be present.
|
||||
- name: Build smolmix wasm
|
||||
run: make -C wasm/smolmix
|
||||
|
||||
- name: Install
|
||||
run: yarn
|
||||
run: pnpm i
|
||||
|
||||
- name: Build packages
|
||||
run: yarn build:ci
|
||||
run: pnpm build:ci
|
||||
|
||||
- name: Install again
|
||||
run: yarn
|
||||
run: pnpm i
|
||||
|
||||
- name: Lint
|
||||
run: yarn lint
|
||||
run: pnpm lint
|
||||
|
||||
- name: Typecheck with tsc
|
||||
run: yarn tsc
|
||||
run: pnpm tsc
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
name: ci-nym-wallet-frontend
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'nym-wallet/**'
|
||||
@@ -12,30 +13,34 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v5.0.0
|
||||
with:
|
||||
version: 11.1.2
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: nym-wallet/.nvmrc
|
||||
cache: yarn
|
||||
cache-dependency-path: yarn.lock
|
||||
cache: pnpm
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install --network-timeout 100000
|
||||
run: pnpm install
|
||||
|
||||
- name: Build TypeScript packages (wallet depends on @nymproject/types, etc.)
|
||||
run: yarn build:types
|
||||
run: pnpm build:types
|
||||
|
||||
- name: Build @nymproject/mui-theme and @nymproject/react (wallet imports subpaths)
|
||||
run: yarn build:packages
|
||||
run: pnpm build:packages
|
||||
|
||||
- name: Typecheck nym-wallet
|
||||
run: yarn --cwd nym-wallet tsc
|
||||
run: pnpm --filter @nymproject/nym-wallet-app tsc
|
||||
|
||||
- name: Lint nym-wallet
|
||||
run: yarn --cwd nym-wallet lint
|
||||
run: pnpm --filter @nymproject/nym-wallet-app lint
|
||||
|
||||
- name: Yarn audit (workspace lockfile; informational)
|
||||
run: yarn audit --level critical
|
||||
- name: pnpm audit (workspace lockfile; informational)
|
||||
run: pnpm audit --audit-level critical
|
||||
continue-on-error: true
|
||||
|
||||
- name: Unit tests (nym-wallet)
|
||||
run: yarn --cwd nym-wallet test
|
||||
run: pnpm --filter @nymproject/nym-wallet-app test
|
||||
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
node-version: 24
|
||||
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
@@ -30,11 +30,6 @@ jobs:
|
||||
override: true
|
||||
components: rustfmt, clippy
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "1.24.6"
|
||||
|
||||
- name: Install wasm-pack
|
||||
run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
|
||||
|
||||
|
||||
@@ -23,10 +23,13 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Node
|
||||
uses: actions/setup-node@v4
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v5.0.0
|
||||
with:
|
||||
node-version: 22.13.0
|
||||
version: 11.1.2
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
@@ -68,17 +71,17 @@ jobs:
|
||||
fileName: '.env'
|
||||
encodedString: ${{ secrets.WALLET_ADMIN_ADDRESS }}
|
||||
|
||||
- name: Yarn cache clean
|
||||
- name: pnpm cache clean
|
||||
shell: bash
|
||||
run: cd .. && yarn cache clean
|
||||
run: cd .. && pnpm cache delete
|
||||
|
||||
- name: Install project dependencies
|
||||
shell: bash
|
||||
run: cd .. && yarn --network-timeout 100000
|
||||
run: cd .. && pnpm i
|
||||
|
||||
- name: Yarn build
|
||||
- name: Build
|
||||
shell: bash
|
||||
run: cd .. && yarn build
|
||||
run: cd .. && pnpm build
|
||||
|
||||
- name: Install dependencies and build it
|
||||
env:
|
||||
@@ -97,7 +100,7 @@ jobs:
|
||||
TAURI_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||
TAURI_NOTARIZATION_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
run: |
|
||||
yarn build-macx86
|
||||
pnpm build-macx86
|
||||
|
||||
- name: Create app tarball
|
||||
run: |
|
||||
|
||||
@@ -26,12 +26,17 @@ jobs:
|
||||
libwebkit2gtk-4.1-dev build-essential curl wget libssl-dev jq \
|
||||
libgtk-3-dev squashfs-tools libayatana-appindicator3-dev make libfuse2 unzip librsvg2-dev file \
|
||||
libsoup-3.0-dev libjavascriptcoregtk-4.1-dev
|
||||
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v5.0.0
|
||||
with:
|
||||
version: 11.1.2
|
||||
|
||||
- name: Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22.13.0
|
||||
cache: 'yarn'
|
||||
node-version: 24
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
@@ -40,10 +45,10 @@ jobs:
|
||||
|
||||
- name: Install project dependencies
|
||||
shell: bash
|
||||
run: cd .. && yarn --network-timeout 100000
|
||||
run: cd .. && pnpm i
|
||||
|
||||
- name: Install app dependencies
|
||||
run: yarn
|
||||
run: pnpm i
|
||||
|
||||
- name: Create env file
|
||||
uses: timheuer/base64-to-file@v1.2
|
||||
@@ -52,7 +57,7 @@ jobs:
|
||||
encodedString: ${{ secrets.WALLET_ADMIN_ADDRESS }}
|
||||
|
||||
- name: Build app
|
||||
run: yarn build
|
||||
run: pnpm build
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
|
||||
@@ -40,16 +40,13 @@ jobs:
|
||||
- name: Setup MSBuild.exe
|
||||
uses: microsoft/setup-msbuild@v3
|
||||
|
||||
# No cache:yarn here: setup-node needs yarn on PATH to populate the cache, but this runner
|
||||
# only gets yarn from the step below.
|
||||
- name: Node
|
||||
uses: actions/setup-node@v4
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v5.0.0
|
||||
with:
|
||||
node-version: 22.13.0
|
||||
|
||||
- name: Install Yarn (classic)
|
||||
shell: bash
|
||||
run: npm install -g yarn@1.22.22
|
||||
version: 11.1.2
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
|
||||
- name: Strip Authenticode thumbprint (avoid signtool on runner)
|
||||
working-directory: nym-wallet/src-tauri
|
||||
@@ -118,11 +115,11 @@ jobs:
|
||||
' tauri.conf.json
|
||||
- name: Install project dependencies
|
||||
shell: bash
|
||||
run: cd .. && yarn --network-timeout 100000
|
||||
run: cd .. && pnpm i
|
||||
|
||||
- name: Install app dependencies
|
||||
shell: bash
|
||||
run: yarn --network-timeout 100000
|
||||
run: pnpm i
|
||||
|
||||
- name: Build and sign it
|
||||
shell: bash
|
||||
@@ -136,7 +133,7 @@ jobs:
|
||||
SSL_COM_TOTP_SECRET: ${{ env.SIGN_WINDOWS == 'true' && secrets.SSL_COM_TOTP_SECRET }}
|
||||
run: |
|
||||
echo "Starting build process..."
|
||||
yarn build
|
||||
pnpm build
|
||||
|
||||
- name: Check bundle directory
|
||||
shell: bash
|
||||
|
||||
@@ -1,6 +1,19 @@
|
||||
name: publish-sdk-npm
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
dry_run:
|
||||
description: "Rehearse the publish (pnpm publish --dry-run, no tarballs uploaded). Untick to publish for real."
|
||||
type: boolean
|
||||
default: true
|
||||
dist_tag:
|
||||
description: "Tag mode. 'auto' picks per package: new packages and same-major releases -> latest; a breaking major (e.g. mix-fetch v2 over v1) -> next, promote later with `npm dist-tag add`. 'next'/'latest' force that tag on all four."
|
||||
type: choice
|
||||
options:
|
||||
- auto
|
||||
- next
|
||||
- latest
|
||||
default: auto
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
@@ -8,15 +21,17 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v5.0.0
|
||||
with:
|
||||
version: 11.1.2
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
node-version: 24
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
|
||||
- name: Setup yarn
|
||||
run: npm install -g yarn
|
||||
|
||||
- name: Install rust toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
@@ -31,21 +46,15 @@ jobs:
|
||||
- name: Install wasm-opt
|
||||
run: cargo install wasm-opt
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "1.24.6"
|
||||
|
||||
- name: Update root CA certificate bundle
|
||||
run: ./wasm/mix-fetch/go-mix-conn/scripts/update-root-certs.sh
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn
|
||||
run: pnpm i
|
||||
|
||||
- name: Build WASM and Typescript SDK
|
||||
run: yarn sdk:build
|
||||
run: pnpm sdk:build
|
||||
|
||||
- name: Publish to NPM
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NODE_AUTH_TOKEN }}
|
||||
DRY_RUN: ${{ inputs.dry_run && '1' || '0' }}
|
||||
NPM_DIST_TAG: ${{ inputs.dist_tag }}
|
||||
run: ./sdk/typescript/scripts/publish.sh
|
||||
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
uses: actions/checkout@v6
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
node-version: 24
|
||||
- uses: nymtech/nym/.github/actions/nym-hash-releases@develop
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -79,3 +79,10 @@ CLAUDE.md
|
||||
/notes
|
||||
/target-otel
|
||||
test-tutorials/
|
||||
|
||||
# pnpm
|
||||
.pnpm-store/
|
||||
|
||||
tmp/
|
||||
# operator tools
|
||||
scripts/nym-node-setup/auto-bond/nodes.csv
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
shamefully-hoist=false
|
||||
prefer-workspace-packages=true
|
||||
hoist-pattern[]=*eslint*
|
||||
hoist-pattern[]=*prettier*
|
||||
hoist-pattern[]=*typescript*
|
||||
hoist-pattern[]=*@types*
|
||||
|
||||
auto-install-peers=true
|
||||
strict-peer-dependencies=false
|
||||
+134
@@ -4,6 +4,140 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [2026.11-xynomizithra] (2026-06-08)
|
||||
|
||||
- bugfix: allow re-inviting expired members ([#6863])
|
||||
- feat: disable Nagle's algorithm for LP between nym-nodes ([#6857])
|
||||
- Keep peer in wg table when updating psk ([#6856])
|
||||
- chore: minor nym-node improvements ([#6850])
|
||||
- chore: LP registration adjustments ([#6845])
|
||||
- crates release: bump version to 1.21.1 ([#6844])
|
||||
- fix gateways being penalised for no stress testing ([#6843])
|
||||
- fix score inflation for throttled nodes ([#6842])
|
||||
- Bugfix/cherry pick/waterloo stres testing floats ([#6841])
|
||||
- bugfix: NMv3 race condition ([#6837])
|
||||
- feat: implement UpdateFamily for the node families contract ([#6834])
|
||||
- Bugfix/cherry pick/waterloo ns api ([#6833])
|
||||
- experiment: attempt to retroactively generate specs for node families and ecash contracts ([#6813])
|
||||
- moving lp packets in lp-data crate ([#6810])
|
||||
- upgrade axum to 0.8.9 (and side deps) ([#6808])
|
||||
- chore: expose admin method for migrating vesting delegations/mixnodes ([#6795])
|
||||
- [chore] fix clippy 1.95 lints for future version update ([#6794])
|
||||
- Handle Rate Limit Challenge Response ([#6786])
|
||||
- NYM-583: Avoid corrupted database on Windows. ([#6785])
|
||||
- Max/smolmix wasm ([#6784])
|
||||
- Chore/bugfixes ([#6783])
|
||||
- Switch from yarn to pnpm ([#6779])
|
||||
- feat: Node Families: expose stake information inside DVpnGateway ([#6778])
|
||||
- feat: Node Families: expose family information for NS API consumers ([#6777])
|
||||
- feat: Node Families: cache and expose family data within nym API ([#6774])
|
||||
- Re-order default API urls for network details ([#6767])
|
||||
- add ci for NM agent binary ([#6764])
|
||||
- feat/refactor: introduce shared contract caches within Nym API ([#6760])
|
||||
- chore: removed dead code for redundant mixnet-vesting integration tests ([#6759])
|
||||
- feat: Node Families: remove nodes upon unbonding ([#6752])
|
||||
- feat: Node Families: contract transactions ([#6750])
|
||||
- feat: Node Families: contract queries ([#6731])
|
||||
- feat: Node Families: initial contract storage ([#6717])
|
||||
- start node families topic branch ([#6715])
|
||||
- Bump rand from 0.8.5 to 0.8.6 in /contracts ([#6702])
|
||||
- Testing port checks in NS Agents ([#6694])
|
||||
- build(deps): bump microsoft/setup-msbuild from 2 to 3 ([#6602])
|
||||
- build(deps): bump tar from 0.4.44 to 0.4.45 ([#6595])
|
||||
- build(deps): bump quinn-proto from 0.11.12 to 0.11.14 ([#6549])
|
||||
- build(deps): bump docker/login-action from 3 to 4 ([#6518])
|
||||
- build(deps): bump actions/download-artifact from 7 to 8 ([#6497])
|
||||
- build(deps): bump actions/upload-artifact from 6 to 7 ([#6496])
|
||||
|
||||
[#6863]: https://github.com/nymtech/nym/pull/6863
|
||||
[#6857]: https://github.com/nymtech/nym/pull/6857
|
||||
[#6856]: https://github.com/nymtech/nym/pull/6856
|
||||
[#6850]: https://github.com/nymtech/nym/pull/6850
|
||||
[#6845]: https://github.com/nymtech/nym/pull/6845
|
||||
[#6844]: https://github.com/nymtech/nym/pull/6844
|
||||
[#6843]: https://github.com/nymtech/nym/pull/6843
|
||||
[#6842]: https://github.com/nymtech/nym/pull/6842
|
||||
[#6841]: https://github.com/nymtech/nym/pull/6841
|
||||
[#6837]: https://github.com/nymtech/nym/pull/6837
|
||||
[#6834]: https://github.com/nymtech/nym/pull/6834
|
||||
[#6833]: https://github.com/nymtech/nym/pull/6833
|
||||
[#6813]: https://github.com/nymtech/nym/pull/6813
|
||||
[#6810]: https://github.com/nymtech/nym/pull/6810
|
||||
[#6808]: https://github.com/nymtech/nym/pull/6808
|
||||
[#6795]: https://github.com/nymtech/nym/pull/6795
|
||||
[#6794]: https://github.com/nymtech/nym/pull/6794
|
||||
[#6786]: https://github.com/nymtech/nym/pull/6786
|
||||
[#6785]: https://github.com/nymtech/nym/pull/6785
|
||||
[#6784]: https://github.com/nymtech/nym/pull/6784
|
||||
[#6783]: https://github.com/nymtech/nym/pull/6783
|
||||
[#6779]: https://github.com/nymtech/nym/pull/6779
|
||||
[#6778]: https://github.com/nymtech/nym/pull/6778
|
||||
[#6777]: https://github.com/nymtech/nym/pull/6777
|
||||
[#6774]: https://github.com/nymtech/nym/pull/6774
|
||||
[#6767]: https://github.com/nymtech/nym/pull/6767
|
||||
[#6764]: https://github.com/nymtech/nym/pull/6764
|
||||
[#6760]: https://github.com/nymtech/nym/pull/6760
|
||||
[#6759]: https://github.com/nymtech/nym/pull/6759
|
||||
[#6752]: https://github.com/nymtech/nym/pull/6752
|
||||
[#6750]: https://github.com/nymtech/nym/pull/6750
|
||||
[#6731]: https://github.com/nymtech/nym/pull/6731
|
||||
[#6717]: https://github.com/nymtech/nym/pull/6717
|
||||
[#6715]: https://github.com/nymtech/nym/pull/6715
|
||||
[#6702]: https://github.com/nymtech/nym/pull/6702
|
||||
[#6694]: https://github.com/nymtech/nym/pull/6694
|
||||
[#6602]: https://github.com/nymtech/nym/pull/6602
|
||||
[#6595]: https://github.com/nymtech/nym/pull/6595
|
||||
[#6549]: https://github.com/nymtech/nym/pull/6549
|
||||
[#6518]: https://github.com/nymtech/nym/pull/6518
|
||||
[#6497]: https://github.com/nymtech/nym/pull/6497
|
||||
[#6496]: https://github.com/nymtech/nym/pull/6496
|
||||
|
||||
## [2026.10-waterloo] (2026-05-27)
|
||||
|
||||
- Re-order default API urls for network details - Waterloo release ([#6799])
|
||||
- [bugfix] IPR v8<->v9 mismatch on Waterloo ([#6772])
|
||||
- Migrate to hickory 0.26.1 ([#6751])
|
||||
- add workflows for NM3 ([#6729])
|
||||
- credential proxy pool ([#6726])
|
||||
- chore: made sphinx version threshold assertion a compile time check ([#6718])
|
||||
- Feat/nmv3 updated performance calculation ([#6714])
|
||||
- feat: NMv3: submission of stress testing result into nym-api ([#6709])
|
||||
- feat: NMv3: Prometheus metrics for network monitor ([#6693])
|
||||
- feat: NMv3: add read-only results API to orchestrator ([#6689])
|
||||
- feat: NMv3: Eviction of stale testrun data ([#6685])
|
||||
- feat: NMv3: Wire up testrun assignment and result submission flow ([#6680])
|
||||
- feat: NMv3: Support multiple network monitor agents per host ([#6679])
|
||||
- Feat/nmv3 agent announcement ([#6673])
|
||||
- add node refresher for periodic scraping of bonded nym-node details ([#6626])
|
||||
- Feat/nmv3 orchestrator queue ([#6597])
|
||||
- feat: network monitor agent - standalone node stress-testing ([#6582])
|
||||
- [feat] propagate NM agent noise keys to nym-node routing ([#6577])
|
||||
- start mix stress testing topic branch ([#6575])
|
||||
- Feat/nmv3 agents subscription ([#6567])
|
||||
- Feat/nmv3 agents contract ([#6555])
|
||||
|
||||
[#6799]: https://github.com/nymtech/nym/pull/6799
|
||||
[#6772]: https://github.com/nymtech/nym/pull/6772
|
||||
[#6751]: https://github.com/nymtech/nym/pull/6751
|
||||
[#6729]: https://github.com/nymtech/nym/pull/6729
|
||||
[#6726]: https://github.com/nymtech/nym/pull/6726
|
||||
[#6718]: https://github.com/nymtech/nym/pull/6718
|
||||
[#6714]: https://github.com/nymtech/nym/pull/6714
|
||||
[#6709]: https://github.com/nymtech/nym/pull/6709
|
||||
[#6693]: https://github.com/nymtech/nym/pull/6693
|
||||
[#6689]: https://github.com/nymtech/nym/pull/6689
|
||||
[#6685]: https://github.com/nymtech/nym/pull/6685
|
||||
[#6680]: https://github.com/nymtech/nym/pull/6680
|
||||
[#6679]: https://github.com/nymtech/nym/pull/6679
|
||||
[#6673]: https://github.com/nymtech/nym/pull/6673
|
||||
[#6626]: https://github.com/nymtech/nym/pull/6626
|
||||
[#6597]: https://github.com/nymtech/nym/pull/6597
|
||||
[#6582]: https://github.com/nymtech/nym/pull/6582
|
||||
[#6577]: https://github.com/nymtech/nym/pull/6577
|
||||
[#6575]: https://github.com/nymtech/nym/pull/6575
|
||||
[#6567]: https://github.com/nymtech/nym/pull/6567
|
||||
[#6555]: https://github.com/nymtech/nym/pull/6555
|
||||
|
||||
## [2026.9-venaco] (2026-05-06)
|
||||
|
||||
- Fix for v9 IPR ([#6710])
|
||||
|
||||
Generated
+1601
-1520
File diff suppressed because it is too large
Load Diff
+133
-131
@@ -44,6 +44,7 @@ members = [
|
||||
"common/cosmwasm-smart-contracts/nym-performance-contract",
|
||||
"common/cosmwasm-smart-contracts/nym-pool-contract",
|
||||
"common/cosmwasm-smart-contracts/vesting-contract",
|
||||
"common/cosmwasm-smart-contracts/network-monitors-contract",
|
||||
"common/credential-proxy",
|
||||
"common/credential-storage",
|
||||
"common/credential-utils",
|
||||
@@ -130,7 +131,6 @@ members = [
|
||||
"nym-api",
|
||||
"nym-api/nym-api-requests",
|
||||
"nym-authenticator-client",
|
||||
"nym-browser-extension/storage",
|
||||
"nym-credential-proxy/nym-credential-proxy",
|
||||
"nym-credential-proxy/nym-credential-proxy-requests",
|
||||
"nym-data-observatory",
|
||||
@@ -174,10 +174,11 @@ members = [
|
||||
"tools/nymvisor",
|
||||
"tools/ts-rs-cli",
|
||||
"wasm/client",
|
||||
# "wasm/full-nym-wasm", # If we uncomment this again, remember to also uncomment the profile settings below
|
||||
"wasm/mix-fetch",
|
||||
"wasm/node-tester",
|
||||
"wasm/smolmix",
|
||||
"wasm/zknym-lib",
|
||||
"nym-network-monitor-v3/nym-network-monitor-orchestrator",
|
||||
"nym-network-monitor-v3/nym-network-monitor-agent",
|
||||
"nym-network-monitor-v3/nym-network-monitor-orchestrator-requests",
|
||||
]
|
||||
|
||||
default-members = [
|
||||
@@ -195,6 +196,8 @@ default-members = [
|
||||
"service-providers/network-requester",
|
||||
"tools/internal/localnet-orchestrator",
|
||||
"tools/nymvisor",
|
||||
"nym-network-monitor-v3/nym-network-monitor-orchestrator",
|
||||
"nym-network-monitor-v3/nym-network-monitor-agent",
|
||||
]
|
||||
|
||||
exclude = ["contracts", "nym-wallet", "cpu-cycles"]
|
||||
@@ -208,7 +211,7 @@ edition = "2024"
|
||||
license = "Apache-2.0"
|
||||
rust-version = "1.87.0"
|
||||
readme = "README.md"
|
||||
version = "1.21.0"
|
||||
version = "1.21.1"
|
||||
|
||||
[workspace.dependencies]
|
||||
addr = "0.15.6"
|
||||
@@ -222,6 +225,7 @@ anyhow = "1.0.98"
|
||||
arc-swap = "1.7.1"
|
||||
argon2 = "0.5.0"
|
||||
async-trait = "0.1.88"
|
||||
async-tungstenite = { version = "0.24", default-features = false }
|
||||
axum = "0.8.9"
|
||||
axum-client-ip = "1.3.1"
|
||||
axum-extra = "0.12.6"
|
||||
@@ -275,24 +279,27 @@ eyre = "0.6.9"
|
||||
fastrand = "2.1.1"
|
||||
flate2 = "1.1.1"
|
||||
futures = "0.3.31"
|
||||
futures-rustls = { version = "0.26", default-features = false }
|
||||
futures-util = "0.3"
|
||||
generic-array = "0.14.7"
|
||||
getrandom = "0.2.10"
|
||||
getrandom03 = { package = "getrandom", version = "=0.3.3" }
|
||||
getrandom04 = { package = "getrandom", version = "0.4" }
|
||||
glob = "0.3"
|
||||
handlebars = "3.5.5"
|
||||
hex = "0.4.3"
|
||||
hickory-proto = "0.26.1"
|
||||
hickory-proto = { version = "0.26.1", default-features = false }
|
||||
hickory-resolver = "0.26.1"
|
||||
hkdf = "0.12.3"
|
||||
hmac = "0.12.1"
|
||||
http = "1"
|
||||
http-body-util = "0.1"
|
||||
httparse = "1.10"
|
||||
httpcodec = "0.2.3"
|
||||
human-repr = "1.1.0"
|
||||
humantime = "2.2.0"
|
||||
humantime-serde = "1.1.1"
|
||||
hyper = "1.6.0"
|
||||
hyper = { version = "1.6.0", default-features = false }
|
||||
hyper-util = "0.1"
|
||||
indicatif = "0.18.0"
|
||||
inquire = "0.6.2"
|
||||
@@ -338,12 +345,14 @@ regex = "1.10.6"
|
||||
reqwest = { version = "0.13.1", default-features = false }
|
||||
rs_merkle = "1.5.0"
|
||||
rustls = { version = "0.23.37", default-features = false }
|
||||
rustls-pki-types = "1"
|
||||
rustls-rustcrypto = "0.0.2-alpha"
|
||||
schemars = "0.8.22"
|
||||
semver = "1.0.26"
|
||||
serde = "1.0.219"
|
||||
serde_bytes = "0.11.17"
|
||||
serde_derive = "1.0"
|
||||
serde_json = "1.0.140"
|
||||
serde_json = { version = "1.0.140", features = ["float_roundtrip"] }
|
||||
serde_json_path = "0.7.2"
|
||||
serde_repr = "0.1"
|
||||
serde_with = "3.9.0"
|
||||
@@ -351,6 +360,7 @@ serde_yaml = "0.9.25"
|
||||
serde_plain = "1.0.2"
|
||||
sha2 = "0.10.3"
|
||||
si-scale = "0.2.3"
|
||||
simple-dns = "0.7"
|
||||
smoltcp = "0.12"
|
||||
snow = "0.9.6"
|
||||
sphinx-packet = "=0.6.0"
|
||||
@@ -359,7 +369,7 @@ strum = "0.28.0"
|
||||
strum_macros = "0.28.0"
|
||||
subtle-encoding = "0.5"
|
||||
syn = "2"
|
||||
sysinfo = "0.37.0"
|
||||
sysinfo = "0.38.4"
|
||||
tap = "1.0.1"
|
||||
tar = "0.4.45"
|
||||
test-with = { version = "0.15.4", default-features = false }
|
||||
@@ -374,7 +384,7 @@ tokio-test = "0.4.4"
|
||||
tokio-tun = "0.11.5"
|
||||
tokio-rustls = "0.26"
|
||||
tokio-smoltcp = "0.5"
|
||||
tokio-tungstenite = { version = "0.20.1" }
|
||||
tokio-tungstenite = "0.20.1"
|
||||
tokio-util = "0.7.15"
|
||||
toml = "0.8.22"
|
||||
tower = "0.5.2"
|
||||
@@ -396,13 +406,16 @@ utoipa-swagger-ui = "9.0.2"
|
||||
utoipauto = "0.2"
|
||||
uuid = "1.19.0"
|
||||
vergen = { version = "=8.3.1", default-features = false }
|
||||
vergen-gitcl = { version = "1.0.8", default-features = false }
|
||||
walkdir = "2"
|
||||
x25519-dalek = "2.0.0"
|
||||
zeroize = "1.7.0"
|
||||
|
||||
prometheus = { version = "0.14.0" }
|
||||
|
||||
# recreating lioness
|
||||
# we don't care about particular versions - just pull whatever is used by sphinx
|
||||
lioness = "*"
|
||||
arrayref = "*"
|
||||
|
||||
# libcrux
|
||||
libcrux-kem = "0.0.7"
|
||||
@@ -415,114 +428,115 @@ libcrux-sha3 = "0.0.8"
|
||||
libcrux-traits = "0.0.6"
|
||||
|
||||
# Workspace dep definitions required by crates.io publication - we need a workspace version since `cargo workspaces` doesn't work with path imports from crate manifests
|
||||
nym-api-requests = { version = "1.21.0", path = "nym-api/nym-api-requests" }
|
||||
nym-authenticator-requests = { version = "1.21.0", path = "common/authenticator-requests" }
|
||||
nym-async-file-watcher = { version = "1.21.0", path = "common/async-file-watcher" }
|
||||
nym-authenticator-client = { version = "1.21.0", path = "nym-authenticator-client" }
|
||||
nym-bandwidth-controller = { version = "1.21.0", path = "common/bandwidth-controller" }
|
||||
nym-bin-common = { version = "1.21.0", path = "common/bin-common" }
|
||||
nym-cache = { version = "1.21.0", path = "common/nym-cache" }
|
||||
nym-client-core = { version = "1.21.0", path = "common/client-core", default-features = false }
|
||||
nym-client-core-config-types = { version = "1.21.0", path = "common/client-core/config-types" }
|
||||
nym-client-core-gateways-storage = { version = "1.21.0", path = "common/client-core/gateways-storage" }
|
||||
nym-client-core-surb-storage = { version = "1.21.0", path = "common/client-core/surb-storage" }
|
||||
nym-client-websocket-requests = { version = "1.21.0", path = "clients/native/websocket-requests" }
|
||||
nym-common = { version = "1.21.0", path = "common/nym-common" }
|
||||
nym-compact-ecash = { version = "1.21.0", path = "common/nym_offline_compact_ecash" }
|
||||
nym-config = { version = "1.21.0", path = "common/config" }
|
||||
nym-contracts-common = { version = "1.21.0", path = "common/cosmwasm-smart-contracts/contracts-common" }
|
||||
nym-coconut-dkg-common = { version = "1.21.0", path = "common/cosmwasm-smart-contracts/coconut-dkg" }
|
||||
nym-credential-storage = { version = "1.21.0", path = "common/credential-storage" }
|
||||
nym-credential-utils = { version = "1.21.0", path = "common/credential-utils" }
|
||||
nym-credential-proxy-lib = { version = "1.21.0", path = "common/credential-proxy" }
|
||||
nym-credentials = { version = "1.21.0", path = "common/credentials", default-features = false }
|
||||
nym-credentials-interface = { version = "1.21.0", path = "common/credentials-interface" }
|
||||
nym-credential-proxy-requests = { version = "1.21.0", path = "nym-credential-proxy/nym-credential-proxy-requests", default-features = false }
|
||||
nym-credential-verification = { version = "1.21.0", path = "common/credential-verification" }
|
||||
nym-crypto = { version = "1.21.0", path = "common/crypto", default-features = false }
|
||||
nym-dkg = { version = "1.21.0", path = "common/dkg" }
|
||||
nym-ecash-contract-common = { version = "1.21.0", path = "common/cosmwasm-smart-contracts/ecash-contract" }
|
||||
nym-ecash-signer-check = { version = "1.21.0", path = "common/ecash-signer-check" }
|
||||
nym-ecash-signer-check-types = { version = "1.21.0", path = "common/ecash-signer-check-types" }
|
||||
nym-ecash-time = { version = "1.21.0", path = "common/ecash-time" }
|
||||
nym-exit-policy = { version = "1.21.0", path = "common/exit-policy" }
|
||||
nym-ffi-shared = { version = "1.21.0", path = "sdk/ffi/shared" }
|
||||
nym-gateway-client = { version = "1.21.0", path = "common/client-libs/gateway-client", default-features = false }
|
||||
nym-gateway-probe = { version = "1.18.0", path = "nym-gateway-probe" }
|
||||
nym-gateway-requests = { version = "1.21.0", path = "common/gateway-requests" }
|
||||
nym-gateway-storage = { version = "1.21.0", path = "common/gateway-storage" }
|
||||
nym-gateway-stats-storage = { version = "1.21.0", path = "common/gateway-stats-storage" }
|
||||
nym-group-contract-common = { version = "1.21.0", path = "common/cosmwasm-smart-contracts/group-contract" }
|
||||
nym-http-api-client = { version = "1.21.0", path = "common/http-api-client" }
|
||||
nym-http-api-client-macro = { version = "1.21.0", path = "common/http-api-client-macro" }
|
||||
nym-http-api-common = { version = "1.21.0", path = "common/http-api-common", default-features = false }
|
||||
nym-id = { version = "1.21.0", path = "common/nym-id" }
|
||||
nym-ip-packet-client = { version = "1.21.0", path = "nym-ip-packet-client" }
|
||||
nym-ip-packet-requests = { version = "1.21.0", path = "common/ip-packet-requests" }
|
||||
nym-lp = { version = "1.21.0", path = "common/nym-lp" }
|
||||
nym-lp-data = { version = "1.21.0", path = "common/nym-lp-data" }
|
||||
nym-kkt = { version = "1.21.0", path = "common/nym-kkt" }
|
||||
nym-kkt-ciphersuite = { version = "1.21.0", path = "common/nym-kkt-ciphersuite" }
|
||||
nym-kkt-context = { version = "1.21.0", path = "common/nym-kkt-context" }
|
||||
nym-metrics = { version = "1.21.0", path = "common/nym-metrics" }
|
||||
nym-mixnet-client = { version = "1.21.0", path = "common/client-libs/mixnet-client" }
|
||||
nym-mixnet-contract-common = { version = "1.21.0", path = "common/cosmwasm-smart-contracts/mixnet-contract" }
|
||||
nym-multisig-contract-common = { version = "1.21.0", path = "common/cosmwasm-smart-contracts/multisig-contract" }
|
||||
nym-network-defaults = { version = "1.21.0", path = "common/network-defaults" }
|
||||
nym-node-tester-utils = { version = "1.21.0", path = "common/node-tester-utils" }
|
||||
nym-noise = { version = "1.21.0", path = "common/nymnoise" }
|
||||
nym-noise-keys = { version = "1.21.0", path = "common/nymnoise/keys" }
|
||||
nym-nonexhaustive-delayqueue = { version = "1.21.0", path = "common/nonexhaustive-delayqueue" }
|
||||
nym-node-requests = { version = "1.21.0", path = "nym-node/nym-node-requests", default-features = false }
|
||||
nym-node-metrics = { version = "1.21.0", path = "nym-node/nym-node-metrics" }
|
||||
nym-node-families-contract-common = { version = "1.21.0", path = "common/cosmwasm-smart-contracts/node-families-contract" }
|
||||
nym-ordered-buffer = { version = "1.21.0", path = "common/socks5/ordered-buffer" }
|
||||
nym-outfox = { version = "1.21.0", path = "nym-outfox" }
|
||||
nym-registration-common = { version = "1.21.0", path = "common/registration" }
|
||||
nym-pemstore = { version = "1.21.0", path = "common/pemstore" }
|
||||
nym-performance-contract-common = { version = "1.21.0", path = "common/cosmwasm-smart-contracts/nym-performance-contract" }
|
||||
nym-sdk = { version = "1.21.0", path = "sdk/rust/nym-sdk" }
|
||||
nym-serde-helpers = { version = "1.21.0", path = "common/serde-helpers" }
|
||||
nym-service-providers-common = { version = "1.21.0", path = "service-providers/common" }
|
||||
nym-service-provider-requests-common = { version = "1.21.0", path = "common/service-provider-requests-common" }
|
||||
nym-socks5-client-core = { version = "1.21.0", path = "common/socks5-client-core" }
|
||||
nym-socks5-proxy-helpers = { version = "1.21.0", path = "common/socks5/proxy-helpers" }
|
||||
nym-socks5-requests = { version = "1.21.0", path = "common/socks5/requests" }
|
||||
nym-sphinx = { version = "1.21.0", path = "common/nymsphinx" }
|
||||
nym-sphinx-acknowledgements = { version = "1.21.0", path = "common/nymsphinx/acknowledgements" }
|
||||
nym-sphinx-addressing = { version = "1.21.0", path = "common/nymsphinx/addressing" }
|
||||
nym-sphinx-anonymous-replies = { version = "1.21.0", path = "common/nymsphinx/anonymous-replies" }
|
||||
nym-sphinx-chunking = { version = "1.21.0", path = "common/nymsphinx/chunking" }
|
||||
nym-sphinx-cover = { version = "1.21.0", path = "common/nymsphinx/cover" }
|
||||
nym-sphinx-forwarding = { version = "1.21.0", path = "common/nymsphinx/forwarding" }
|
||||
nym-sphinx-framing = { version = "1.21.0", path = "common/nymsphinx/framing" }
|
||||
nym-sphinx-params = { version = "1.21.0", path = "common/nymsphinx/params" }
|
||||
nym-sphinx-routing = { version = "1.21.0", path = "common/nymsphinx/routing" }
|
||||
nym-sphinx-types = { version = "1.21.0", path = "common/nymsphinx/types" }
|
||||
nym-statistics-common = { version = "1.21.0", path = "common/statistics" }
|
||||
nym-store-cipher = { version = "1.21.0", path = "common/store-cipher" }
|
||||
nym-task = { version = "1.21.0", path = "common/task" }
|
||||
nym-tun = { version = "1.21.0", path = "common/tun" }
|
||||
nym-test-utils = { version = "1.21.0", path = "common/test-utils" }
|
||||
nym-ticketbooks-merkle = { version = "1.21.0", path = "common/ticketbooks-merkle" }
|
||||
nym-topology = { version = "1.21.0", path = "common/topology" }
|
||||
nym-types = { version = "1.21.0", path = "common/types" }
|
||||
nym-upgrade-mode-check = { version = "1.21.0", path = "common/upgrade-mode-check" }
|
||||
nym-validator-client = { version = "1.21.0", path = "common/client-libs/validator-client", default-features = false }
|
||||
nym-vesting-contract-common = { version = "1.21.0", path = "common/cosmwasm-smart-contracts/vesting-contract" }
|
||||
nym-verloc = { version = "1.21.0", path = "common/verloc" }
|
||||
nym-wireguard = { version = "1.21.0", path = "common/wireguard" }
|
||||
nym-wireguard-types = { version = "1.21.0", path = "common/wireguard-types" }
|
||||
nym-wireguard-private-metadata-shared = { version = "1.21.0", path = "common/wireguard-private-metadata/shared" }
|
||||
nym-wireguard-private-metadata-client = { version = "1.21.0", path = "common/wireguard-private-metadata/client" }
|
||||
nym-wireguard-private-metadata-server = { version = "1.21.0", path = "common/wireguard-private-metadata/server" }
|
||||
nym-sqlx-pool-guard = { version = "1.2.0", path = "nym-sqlx-pool-guard" }
|
||||
nym-wasm-client-core = { version = "1.21.0", path = "common/wasm/client-core" }
|
||||
nym-wasm-storage = { version = "1.21.0", path = "common/wasm/storage" }
|
||||
nym-wasm-utils = { version = "1.21.0", path = "common/wasm/utils", default-features = false }
|
||||
nyxd-scraper-shared = { version = "1.21.0", path = "common/nyxd-scraper-shared" }
|
||||
nym-api-requests = { version = "1.21.1", path = "nym-api/nym-api-requests" }
|
||||
nym-authenticator-requests = { version = "1.21.1", path = "common/authenticator-requests" }
|
||||
nym-async-file-watcher = { version = "1.21.1", path = "common/async-file-watcher" }
|
||||
nym-authenticator-client = { version = "1.21.1", path = "nym-authenticator-client" }
|
||||
nym-bandwidth-controller = { version = "1.21.1", path = "common/bandwidth-controller" }
|
||||
nym-bin-common = { version = "1.21.1", path = "common/bin-common" }
|
||||
nym-cache = { version = "1.21.1", path = "common/nym-cache" }
|
||||
nym-client-core = { version = "1.21.1", path = "common/client-core", default-features = false }
|
||||
nym-client-core-config-types = { version = "1.21.1", path = "common/client-core/config-types" }
|
||||
nym-client-core-gateways-storage = { version = "1.21.1", path = "common/client-core/gateways-storage" }
|
||||
nym-client-core-surb-storage = { version = "1.21.1", path = "common/client-core/surb-storage" }
|
||||
nym-client-websocket-requests = { version = "1.21.1", path = "clients/native/websocket-requests" }
|
||||
nym-common = { version = "1.21.1", path = "common/nym-common" }
|
||||
nym-compact-ecash = { version = "1.21.1", path = "common/nym_offline_compact_ecash" }
|
||||
nym-config = { version = "1.21.1", path = "common/config" }
|
||||
nym-contracts-common = { version = "1.21.1", path = "common/cosmwasm-smart-contracts/contracts-common" }
|
||||
nym-coconut-dkg-common = { version = "1.21.1", path = "common/cosmwasm-smart-contracts/coconut-dkg" }
|
||||
nym-credential-storage = { version = "1.21.1", path = "common/credential-storage" }
|
||||
nym-credential-utils = { version = "1.21.1", path = "common/credential-utils" }
|
||||
nym-credential-proxy-lib = { version = "1.21.1", path = "common/credential-proxy" }
|
||||
nym-credentials = { version = "1.21.1", path = "common/credentials", default-features = false }
|
||||
nym-credentials-interface = { version = "1.21.1", path = "common/credentials-interface" }
|
||||
nym-credential-proxy-requests = { version = "1.21.1", path = "nym-credential-proxy/nym-credential-proxy-requests", default-features = false }
|
||||
nym-credential-verification = { version = "1.21.1", path = "common/credential-verification" }
|
||||
nym-crypto = { version = "1.21.1", path = "common/crypto", default-features = false }
|
||||
nym-dkg = { version = "1.21.1", path = "common/dkg" }
|
||||
nym-ecash-contract-common = { version = "1.21.1", path = "common/cosmwasm-smart-contracts/ecash-contract" }
|
||||
nym-ecash-signer-check = { version = "1.21.1", path = "common/ecash-signer-check" }
|
||||
nym-ecash-signer-check-types = { version = "1.21.1", path = "common/ecash-signer-check-types" }
|
||||
nym-ecash-time = { version = "1.21.1", path = "common/ecash-time" }
|
||||
nym-exit-policy = { version = "1.21.1", path = "common/exit-policy" }
|
||||
nym-ffi-shared = { version = "1.21.1", path = "sdk/ffi/shared" }
|
||||
nym-gateway-client = { version = "1.21.1", path = "common/client-libs/gateway-client", default-features = false }
|
||||
nym-gateway-probe = { version = "1.21.1", path = "nym-gateway-probe" }
|
||||
nym-gateway-requests = { version = "1.21.1", path = "common/gateway-requests" }
|
||||
nym-gateway-storage = { version = "1.21.1", path = "common/gateway-storage" }
|
||||
nym-gateway-stats-storage = { version = "1.21.1", path = "common/gateway-stats-storage" }
|
||||
nym-group-contract-common = { version = "1.21.1", path = "common/cosmwasm-smart-contracts/group-contract" }
|
||||
nym-http-api-client = { version = "1.21.1", path = "common/http-api-client" }
|
||||
nym-http-api-client-macro = { version = "1.21.1", path = "common/http-api-client-macro" }
|
||||
nym-http-api-common = { version = "1.21.1", path = "common/http-api-common", default-features = false }
|
||||
nym-id = { version = "1.21.1", path = "common/nym-id" }
|
||||
nym-ip-packet-client = { version = "1.21.1", path = "nym-ip-packet-client" }
|
||||
nym-ip-packet-requests = { version = "1.21.1", path = "common/ip-packet-requests" }
|
||||
nym-lp = { version = "1.21.1", path = "common/nym-lp" }
|
||||
nym-lp-data = { version = "1.21.1", path = "common/nym-lp-data" }
|
||||
nym-kkt = { version = "1.21.1", path = "common/nym-kkt" }
|
||||
nym-kkt-ciphersuite = { version = "1.21.1", path = "common/nym-kkt-ciphersuite" }
|
||||
nym-kkt-context = { version = "1.21.1", path = "common/nym-kkt-context" }
|
||||
nym-metrics = { version = "1.21.1", path = "common/nym-metrics" }
|
||||
nym-mixnet-client = { version = "1.21.1", path = "common/client-libs/mixnet-client" }
|
||||
nym-mixnet-contract-common = { version = "1.21.1", path = "common/cosmwasm-smart-contracts/mixnet-contract" }
|
||||
nym-multisig-contract-common = { version = "1.21.1", path = "common/cosmwasm-smart-contracts/multisig-contract" }
|
||||
nym-network-defaults = { version = "1.21.1", path = "common/network-defaults" }
|
||||
nym-node-tester-utils = { version = "1.21.1", path = "common/node-tester-utils" }
|
||||
nym-noise = { version = "1.21.1", path = "common/nymnoise" }
|
||||
nym-noise-keys = { version = "1.21.1", path = "common/nymnoise/keys" }
|
||||
nym-nonexhaustive-delayqueue = { version = "1.21.1", path = "common/nonexhaustive-delayqueue" }
|
||||
nym-node-requests = { version = "1.21.1", path = "nym-node/nym-node-requests", default-features = false }
|
||||
nym-node-metrics = { version = "1.21.1", path = "nym-node/nym-node-metrics" }
|
||||
nym-node-families-contract-common = { version = "1.21.1", path = "common/cosmwasm-smart-contracts/node-families-contract" }
|
||||
nym-ordered-buffer = { version = "1.21.1", path = "common/socks5/ordered-buffer" }
|
||||
nym-outfox = { version = "1.21.1", path = "nym-outfox" }
|
||||
nym-registration-common = { version = "1.21.1", path = "common/registration" }
|
||||
nym-pemstore = { version = "1.21.1", path = "common/pemstore" }
|
||||
nym-performance-contract-common = { version = "1.21.1", path = "common/cosmwasm-smart-contracts/nym-performance-contract" }
|
||||
nym-sdk = { version = "1.21.1", path = "sdk/rust/nym-sdk" }
|
||||
nym-serde-helpers = { version = "1.21.1", path = "common/serde-helpers" }
|
||||
nym-service-providers-common = { version = "1.21.1", path = "service-providers/common" }
|
||||
nym-service-provider-requests-common = { version = "1.21.1", path = "common/service-provider-requests-common" }
|
||||
nym-socks5-client-core = { version = "1.21.1", path = "common/socks5-client-core" }
|
||||
nym-socks5-proxy-helpers = { version = "1.21.1", path = "common/socks5/proxy-helpers" }
|
||||
nym-socks5-requests = { version = "1.21.1", path = "common/socks5/requests" }
|
||||
nym-sphinx = { version = "1.21.1", path = "common/nymsphinx" }
|
||||
nym-sphinx-acknowledgements = { version = "1.21.1", path = "common/nymsphinx/acknowledgements" }
|
||||
nym-sphinx-addressing = { version = "1.21.1", path = "common/nymsphinx/addressing" }
|
||||
nym-sphinx-anonymous-replies = { version = "1.21.1", path = "common/nymsphinx/anonymous-replies" }
|
||||
nym-sphinx-chunking = { version = "1.21.1", path = "common/nymsphinx/chunking" }
|
||||
nym-sphinx-cover = { version = "1.21.1", path = "common/nymsphinx/cover" }
|
||||
nym-sphinx-forwarding = { version = "1.21.1", path = "common/nymsphinx/forwarding" }
|
||||
nym-sphinx-framing = { version = "1.21.1", path = "common/nymsphinx/framing" }
|
||||
nym-sphinx-params = { version = "1.21.1", path = "common/nymsphinx/params" }
|
||||
nym-sphinx-routing = { version = "1.21.1", path = "common/nymsphinx/routing" }
|
||||
nym-sphinx-types = { version = "1.21.1", path = "common/nymsphinx/types" }
|
||||
nym-statistics-common = { version = "1.21.1", path = "common/statistics" }
|
||||
nym-store-cipher = { version = "1.21.1", path = "common/store-cipher" }
|
||||
nym-task = { version = "1.21.1", path = "common/task" }
|
||||
nym-tun = { version = "1.21.1", path = "common/tun" }
|
||||
nym-test-utils = { version = "1.21.1", path = "common/test-utils" }
|
||||
nym-ticketbooks-merkle = { version = "1.21.1", path = "common/ticketbooks-merkle" }
|
||||
nym-topology = { version = "1.21.1", path = "common/topology" }
|
||||
nym-types = { version = "1.21.1", path = "common/types" }
|
||||
nym-upgrade-mode-check = { version = "1.21.1", path = "common/upgrade-mode-check" }
|
||||
nym-validator-client = { version = "1.21.1", path = "common/client-libs/validator-client", default-features = false }
|
||||
nym-vesting-contract-common = { version = "1.21.1", path = "common/cosmwasm-smart-contracts/vesting-contract" }
|
||||
nym-network-monitors-contract-common = { version = "1.21.1", path = "common/cosmwasm-smart-contracts/network-monitors-contract" }
|
||||
nym-verloc = { version = "1.21.1", path = "common/verloc" }
|
||||
nym-wireguard = { version = "1.21.1", path = "common/wireguard" }
|
||||
nym-wireguard-types = { version = "1.21.1", path = "common/wireguard-types" }
|
||||
nym-wireguard-private-metadata-shared = { version = "1.21.1", path = "common/wireguard-private-metadata/shared" }
|
||||
nym-wireguard-private-metadata-client = { version = "1.21.1", path = "common/wireguard-private-metadata/client" }
|
||||
nym-wireguard-private-metadata-server = { version = "1.21.1", path = "common/wireguard-private-metadata/server" }
|
||||
nym-sqlx-pool-guard = { version = "1.21.1", path = "nym-sqlx-pool-guard" }
|
||||
nym-wasm-client-core = { version = "1.21.1", path = "common/wasm/client-core" }
|
||||
nym-wasm-storage = { version = "1.21.1", path = "common/wasm/storage" }
|
||||
nym-wasm-utils = { version = "1.21.1", path = "common/wasm/utils", default-features = false }
|
||||
nyxd-scraper-shared = { version = "1.21.1", path = "common/nyxd-scraper-shared" }
|
||||
|
||||
smolmix = { version = "1.21.0", path = "smolmix/core" }
|
||||
smolmix = { version = "1.21.1", path = "smolmix/core" }
|
||||
|
||||
# coconut/DKG related
|
||||
# unfortunately until https://github.com/zkcrypto/nym-bls12_381-fork/issues/10 is resolved, we have to rely on the fork
|
||||
@@ -589,18 +603,7 @@ opt-level = 3
|
||||
# lto = true
|
||||
opt-level = 'z'
|
||||
|
||||
[profile.release.package.nym-node-tester-wasm]
|
||||
# lto = true
|
||||
opt-level = 'z'
|
||||
|
||||
# Commented out since the crate is also commented out from the inclusion in the
|
||||
# workspace above. We should uncomment this if we re-include it in the
|
||||
# workspace
|
||||
#[profile.release.package.nym-wasm-sdk]
|
||||
## lto = true
|
||||
#opt-level = 'z'
|
||||
|
||||
[profile.release.package.mix-fetch-wasm]
|
||||
[profile.release.package.smolmix-wasm]
|
||||
# lto = true
|
||||
opt-level = 'z'
|
||||
|
||||
@@ -621,4 +624,3 @@ exit = "deny"
|
||||
panic = "deny"
|
||||
unimplemented = "deny"
|
||||
unreachable = "deny"
|
||||
|
||||
|
||||
@@ -104,30 +104,30 @@ $(eval $(call add_cargo_workspace,wallet,nym-wallet))
|
||||
sdk-wasm: sdk-wasm-build sdk-wasm-test sdk-wasm-lint
|
||||
|
||||
sdk-wasm-build:
|
||||
# $(MAKE) -C nym-browser-extension/storage wasm-pack
|
||||
$(MAKE) -C wasm/client
|
||||
$(MAKE) -C wasm/node-tester
|
||||
$(MAKE) -C wasm/mix-fetch
|
||||
$(MAKE) -C wasm/smolmix
|
||||
# $(MAKE) -C wasm/zknym-lib
|
||||
# $(MAKE) -C wasm/full-nym-wasm
|
||||
|
||||
# run this from npm/yarn to ensure tools are in the path, e.g. yarn build:sdk from root of repo
|
||||
#
|
||||
# `mix-tunnel` must build before the three feature packages — they import it
|
||||
# via `workspace:*` and the lerna topological sort will respect that as long
|
||||
# as we keep them in the same `--scope` invocation.
|
||||
sdk-typescript-build:
|
||||
npx lerna run --scope @nymproject/sdk build --stream
|
||||
npx lerna run --scope @nymproject/mix-fetch build --stream
|
||||
npx lerna run --scope @nymproject/node-tester build --stream
|
||||
yarn --cwd sdk/typescript/codegen/contract-clients build
|
||||
npx lerna run --scope '{@nymproject/mix-tunnel,@nymproject/mix-fetch,@nymproject/mix-dns,@nymproject/mix-websocket}' build --stream
|
||||
pnpm --pwd sdk/typescript/codegen/contract-clients build
|
||||
|
||||
# NOTE: These targets are part of the main workspace (but not as wasm32-unknown-unknown)
|
||||
# WASM_CRATES = extension-storage nym-client-wasm nym-node-tester-wasm zknym-lib
|
||||
WASM_CRATES = nym-client-wasm nym-node-tester-wasm
|
||||
|
||||
WASM_CRATES = nym-client-wasm
|
||||
|
||||
sdk-wasm-test:
|
||||
#cargo test $(addprefix -p , $(WASM_CRATES)) --target wasm32-unknown-unknown -- -Dwarnings
|
||||
|
||||
sdk-wasm-lint:
|
||||
RUSTFLAGS='--cfg getrandom_backend="wasm_js"' cargo clippy $(addprefix -p , $(WASM_CRATES)) --target wasm32-unknown-unknown -- -Dwarnings
|
||||
$(MAKE) -C wasm/mix-fetch check-fmt
|
||||
$(MAKE) -C wasm/smolmix check-fmt
|
||||
|
||||
# Add to top-level targets
|
||||
build: sdk-wasm-build
|
||||
@@ -223,7 +223,7 @@ build-nym-cli:
|
||||
|
||||
generate-typescript:
|
||||
cd tools/ts-rs-cli && cargo run && cd ../..
|
||||
yarn types:lint:fix
|
||||
pnpm types:lint:fix
|
||||
|
||||
# Run the integration tests for public nym-api endpoints
|
||||
run-api-tests:
|
||||
|
||||
@@ -74,9 +74,9 @@ Nym Node Operators and Validators Terms and Conditions can be found [here](https
|
||||
## Getting Started
|
||||
|
||||
```bash
|
||||
yarn install
|
||||
pnpm install
|
||||
```
|
||||
|
||||
```bash
|
||||
yarn build
|
||||
pnpm build
|
||||
```
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
- name: Nym node auto-bonding
|
||||
hosts: all
|
||||
gather_facts: false
|
||||
serial: 1
|
||||
|
||||
roles:
|
||||
- role: postinstall-auto
|
||||
@@ -1,21 +1,4 @@
|
||||
---
|
||||
ansible_ssh_private_key_file: ~/.ssh/<SSH_KEY>
|
||||
|
||||
cli_url: "https://github.com/nymtech/nym/releases/download/nym-binaries-{{ nym_version }}/nym-cli"
|
||||
tunnel_manager_url: "https://github.com/nymtech/nym/raw/refs/heads/develop/scripts/nym-node-setup/network-tunnel-manager.sh"
|
||||
quic_bridge_deployment_url: "https://raw.githubusercontent.com/nymtech/nym/refs/heads/develop/scripts/nym-node-setup/quic_bridge_deployment.sh"
|
||||
|
||||
###############################################################################
|
||||
## GLOBAL VARS
|
||||
## These values will be used globally unless overwritten per node in inventory/all
|
||||
###############################################################################
|
||||
|
||||
ansible_user: root # used for ssh, like `ssh root@nym-exit.ch-1.mynodes.net`
|
||||
email: "<EMAIL>" # used in certbot, description.toml and landing page
|
||||
website: "<WEBSITE>" # it is used in the description.toml
|
||||
description: "<NODE_PUBLIC_DESCRIPTION>" # or define per node in inventory/all
|
||||
# operator_name: "<OPERATOR_NAME>" # used in landing page if provided
|
||||
|
||||
###############################################################################
|
||||
## GLOBAL VARS
|
||||
## These values will be used globally unless overwritten per node in inventory/all
|
||||
@@ -23,16 +6,41 @@ description: "<NODE_PUBLIC_DESCRIPTION>" # or define per node in inventory/all
|
||||
## Per node changes in inventory/all will overwrite these global vars
|
||||
###############################################################################
|
||||
|
||||
# moniker: "<MONIKER>" # if not setup here not in inventory/all it get's derived from the hostname
|
||||
# mode: <MODE> # entry-gateway/exit-gateway/mixnode
|
||||
# wireguard_enabled: <WIREGUARD_ENABLED> # true/false
|
||||
hostname: "" # this is a fallback, keep it and setup hostname per node in inventory/all
|
||||
## MANDATORY - uncomment & define
|
||||
|
||||
## --SSH--
|
||||
#ansible_user: root # used for ssh, like `ssh root@nym-exit.ch-1.mynodes.net`
|
||||
# ansible_ssh_private_key_file: ~/.ssh/<SSH_KEY>
|
||||
|
||||
## --Operator info--
|
||||
# email: "<EMAIL>" # used in certbot, description.toml and landing page
|
||||
# website: "<WEBSITE>" # it is used in the description.toml
|
||||
# description: "<NODE_PUBLIC_DESCRIPTION>" # or define per node in inventory/all
|
||||
# moniker: "<MONIKER>"
|
||||
|
||||
## --Node defaults (can override per node in inventory/all)--
|
||||
# accept_operator_terms: true # controls --accept-operator-terms-and-conditions, read: https://nym.com/docs/operators/nodes/nym-node/setup#terms--conditions
|
||||
# mode: exit-gateway # entry-gateway/exit-gateway/mixnode
|
||||
# wireguard_enabled: true # true/false
|
||||
hostname: "" # keep this fallback, keep it and setup hostname per node in inventory/all
|
||||
|
||||
## OPTIONAL - uncomment & define
|
||||
|
||||
# operator_name: "<OPERATOR_NAME>" # used in landing page if provided
|
||||
# nym_version: "nym-binaries-v2026.7-tola" # to use particular version instead of Latest, provide in such form:
|
||||
|
||||
## alternative SSH key var setting, instead of a hardcoded path
|
||||
## useful if the playbook is shared in a repo by more admins with each having own local key
|
||||
# ansible_ssh_private_key_file: "{{ lookup('env', '<YOUR_ANSIBLE SSH_KEY_ENV_VAR>') }}"
|
||||
|
||||
###############################################################################
|
||||
## GLOBAL PACKAGES
|
||||
## GLOBAL PACKAGES & URLs
|
||||
## These will be installed during deployment
|
||||
###############################################################################
|
||||
|
||||
nym_cli_url: "https://github.com/nymtech/nym/releases/download/{{ nym_version }}/nym-cli"
|
||||
tunnel_manager_url: "https://github.com/nymtech/nym/raw/refs/heads/develop/scripts/nym-node-setup/network-tunnel-manager.sh"
|
||||
quic_bridge_deployment_url: "https://raw.githubusercontent.com/nymtech/nym/refs/heads/develop/scripts/nym-node-setup/quic_bridge_deployment.sh"
|
||||
|
||||
packages:
|
||||
- tmux
|
||||
@@ -50,24 +58,6 @@ packages:
|
||||
- ufw
|
||||
|
||||
|
||||
###############################################################################
|
||||
## OPTIONAL OVERRIDES
|
||||
## All values below already have defaults in the playbook/roles
|
||||
## Uncomment only if you want to override them
|
||||
###############################################################################
|
||||
|
||||
###############################################################################
|
||||
## SYSTEM MAINTENANCE PLAYBOOK KNOBS
|
||||
###############################################################################
|
||||
|
||||
# To use particular version instead of Latest, provide in such form:
|
||||
# nym_version: "nym-binaries-v2026.7-tola"
|
||||
|
||||
## NOTE:
|
||||
## if you want to pin Nym to a specific version instead of using the
|
||||
## latest release from GitHub in /tasks/main.yml then
|
||||
## uncomment the line above and set the tag
|
||||
|
||||
###############################################################################
|
||||
## SYSTEM MAINTENANCE PLAYBOOK KNOBS
|
||||
###############################################################################
|
||||
|
||||
@@ -1,34 +1,39 @@
|
||||
[nym_nodes]
|
||||
# READ CONFIGURATION GUIDE:
|
||||
# https://nym.com//docs/operators/orchestration/ansible#configuration
|
||||
## READ CONFIGURATION GUIDE:
|
||||
## https://nym.com/docs/operators/orchestration/ansible#configuration
|
||||
|
||||
# VARIABLES INFO
|
||||
# required vars to set values per node:
|
||||
# `ansible_host`, `hostname`, `location`
|
||||
##############
|
||||
## TEMPLATE ##
|
||||
##############
|
||||
## uncomment and exchange the <VARIABLES> with your real values for each node without the <> brackets
|
||||
|
||||
# global vars can be set in the group_vars/all.yml, for example:
|
||||
# `email`, `ansible_user`, `moniker`, `description`, `mode`, `wireguard_enabled`
|
||||
# othersise they must be set per node!
|
||||
|
||||
############
|
||||
# TEMPLATE #
|
||||
############
|
||||
# node1 ansible_host=<YOUR_SERVER_IP> ansible_user=<USER> hostname=<HOSTNAME> location=<LOCATION> email=<EMAIL> mode=<MODE> wireguard_enabled=<true/false> moniker=<MONIKER> description=<DESCRIPTION>
|
||||
|
||||
# remove all comments and exchange the <VARIABLES> with your real values for each node
|
||||
# without <> brackets
|
||||
####################
|
||||
## VARIABLES INFO ##
|
||||
####################
|
||||
|
||||
# PRIORITY ORDER
|
||||
# anything setup globaly can be overwritten in this file per node
|
||||
# if provided here, it takes priority over the global setting
|
||||
## --REQUIRED VARS--
|
||||
## required per node:
|
||||
## ansible_host, hostname, location
|
||||
|
||||
# EXAMPLES
|
||||
# exit + wireguard gateway:
|
||||
## --OPTIONAL VARS--
|
||||
## can be set in the group_vars/all.yml or per node here:
|
||||
## email, ansible_user, moniker, description, mode, wireguard_enabled
|
||||
|
||||
## --PRIORITY ORDER--
|
||||
## anything setup globaly can be overwritten in this file per node
|
||||
## if provided here, it takes priority over the global setting
|
||||
|
||||
## --EXAMPLES--
|
||||
## exit + wireguard gateway:
|
||||
# node2 ansible_host=11.12.13.14 hostname=nym-exit.ch-1.mydomain.net mode=exit-gateway location=CH wireguard_enabled=true
|
||||
|
||||
# entry gateway, no wireguard:
|
||||
## entry gateway, no wireguard:
|
||||
# node3 ansible_host=12.13.14.15 hostname=nym-entry.ch-2.mydomain.net mode=entry-gateway location=CH wireguard_enabled=false
|
||||
|
||||
# NOTE:
|
||||
# all examples above don't have defined user, email nor description as we use the definition from group_vars/main.yml without an attempt of overwriting it
|
||||
# all examples above don't have moniker defined as there is a function in /templates/description.toml.j2 deriving it from the hostname
|
||||
## mixnode (comment out tunnel+quic roles in deploy.yml for these)
|
||||
# mix-de-1 ansible_host=13.14.15.16 hostname=nym-mix.de-1.example.net location=DE mode=mixnode wireguard_enabled=false
|
||||
|
||||
## NOTE:
|
||||
## all examples above don't have defined user, email nor description as we use global vars from playbooks/group_vars/all.yml
|
||||
|
||||
@@ -89,7 +89,6 @@
|
||||
loop:
|
||||
- "/etc/nginx/sites-enabled/{{ hostname }}-ssl"
|
||||
- "/etc/nginx/sites-enabled/nym-wss-config"
|
||||
when: not le_cert.stat.exists
|
||||
notify: Restart nginx
|
||||
|
||||
- name: Ensure nginx is enabled and running (needed for ACME http-01)
|
||||
@@ -111,18 +110,13 @@
|
||||
- name: Obtain/renew certificate
|
||||
command:
|
||||
cmd: >-
|
||||
{% if le_cert.stat.exists %}
|
||||
certbot certonly --webroot
|
||||
-w /var/www/{{ hostname }}
|
||||
certbot certonly --nginx
|
||||
--non-interactive --agree-tos --keep-until-expiring
|
||||
-m {{ email }} -d {{ hostname }}
|
||||
{% else %}
|
||||
certbot --nginx
|
||||
--non-interactive --agree-tos --redirect
|
||||
-m {{ email }} -d {{ hostname }}
|
||||
{% endif %}
|
||||
register: certbot_result
|
||||
failed_when: false
|
||||
failed_when: false
|
||||
|
||||
|
||||
|
||||
# re-check cert after certbot attempt
|
||||
- name: Re-check whether certificate exists after certbot
|
||||
@@ -170,4 +164,4 @@
|
||||
changed_when: false
|
||||
|
||||
- name: Flush handlers (apply restart after successful tests)
|
||||
meta: flush_handlers
|
||||
meta: flush_handlers
|
||||
@@ -10,7 +10,7 @@ mixnet_bind_address: "0.0.0.0:1789" # maps to --mixnet-bind-address
|
||||
landing_page_assets_base_dir: "/var/www"
|
||||
|
||||
# Flag toggles
|
||||
# accept_operator_terms: true # controls --accept-operator-terms-and-conditions
|
||||
accept_operator_terms: false # override in group_vars or inventory
|
||||
nym_write_flag: true # controls -w
|
||||
nym_init_only_flag: true # controls --init-only
|
||||
wss_port: 9001 # controlls --announce-wss-port
|
||||
@@ -18,7 +18,7 @@ wss_port: 9001 # controlls --announce-wss-port
|
||||
# Optional: extra flags if you want to append more later
|
||||
nym_extra_flags: ""
|
||||
|
||||
# CLI URL (nym_version can be set elsewhere / via GitHub API)
|
||||
# CLI URL
|
||||
nym_cli_url: "https://github.com/nymtech/nym/releases/download/{{ nym_version }}/nym-cli"
|
||||
|
||||
# UFW
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
- name: Show which node is being bonded
|
||||
tags: bonding
|
||||
debug:
|
||||
msg: "Bonding Nym node: {{ hostname }}"
|
||||
|
||||
- name: Get bonding details
|
||||
tags: bonding
|
||||
command: "/root/nym-binaries/nym-node bonding-information"
|
||||
register: bondinfo
|
||||
changed_when: false
|
||||
|
||||
- name: Display bonding info
|
||||
tags: bonding
|
||||
debug:
|
||||
msg: "{{ item }}"
|
||||
loop: "{{ bondinfo.stdout_lines }}"
|
||||
|
||||
- name: Sign bonding contract message on the node
|
||||
tags: bonding
|
||||
command:
|
||||
argv:
|
||||
- /root/nym-binaries/nym-node
|
||||
- sign
|
||||
- --contract-msg
|
||||
- "{{ contract_msg }}"
|
||||
- --output
|
||||
- json
|
||||
register: sign_output
|
||||
|
||||
- name: Display full signed message exactly as returned
|
||||
tags: bonding
|
||||
debug:
|
||||
msg: "{{ sign_output.stdout }}"
|
||||
|
||||
- name: Display encoded signature
|
||||
tags: bonding
|
||||
debug:
|
||||
msg: "ENCODED_SIGNATURE={{ (sign_output.stdout | from_json).encoded_signature }}"
|
||||
@@ -1,16 +1,20 @@
|
||||
- name: Download quic_bridge_deployment.sh
|
||||
tags: quic bridge deployment
|
||||
get_url:
|
||||
url: "{{ quic_bridge_deployment_url }}"
|
||||
dest: "/root/nym-binaries/quic_bridge_deployment.sh"
|
||||
command:
|
||||
cmd: "curl -fsSL {{ quic_bridge_deployment_url }} -o /root/nym-binaries/quic_bridge_deployment.sh"
|
||||
tags: quic
|
||||
|
||||
- name: Set quic_bridge_deployment permissions
|
||||
file:
|
||||
path: /root/nym-binaries/quic_bridge_deployment.sh
|
||||
mode: "0755"
|
||||
tags: quic
|
||||
|
||||
- name: Configure tunnel manager
|
||||
tags: quic bridge deployment
|
||||
become: true
|
||||
command:
|
||||
cmd: "/root/nym-binaries/quic_bridge_deployment.sh {{ item }}"
|
||||
environment:
|
||||
NONINTERACTIVE: "1"
|
||||
loop:
|
||||
- full_bridge_setup
|
||||
- full_bridge_setup
|
||||
tags: quic
|
||||
@@ -10,11 +10,17 @@
|
||||
- ntm
|
||||
|
||||
- name: Download network tunnel manager
|
||||
get_url:
|
||||
url: "{{ tunnel_manager_url }}"
|
||||
dest: /root/nym-binaries/network-tunnel-manager.sh
|
||||
command:
|
||||
cmd: "curl -fsSL {{ tunnel_manager_url }} -o /root/nym-binaries/network-tunnel-manager.sh"
|
||||
tags:
|
||||
- tunnel
|
||||
- network_tunnel_manager
|
||||
- ntm
|
||||
|
||||
- name: Set network tunnel manager permissions
|
||||
file:
|
||||
path: /root/nym-binaries/network-tunnel-manager.sh
|
||||
mode: "0755"
|
||||
force: yes
|
||||
tags:
|
||||
- tunnel
|
||||
- network_tunnel_manager
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "nym-client"
|
||||
description = "Implementation of the Nym Client"
|
||||
version = "1.1.76"
|
||||
version = "1.1.78"
|
||||
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>", "Jędrzej Stuczyński <andrew@nymtech.net>"]
|
||||
edition = "2021"
|
||||
license.workspace = true
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "nym-socks5-client"
|
||||
description = "A SOCKS5 localhost proxy that converts incoming messages to Sphinx and sends them to a Nym address"
|
||||
version = "1.1.76"
|
||||
version = "1.1.78"
|
||||
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>"]
|
||||
edition = "2021"
|
||||
license.workspace = true
|
||||
|
||||
@@ -240,7 +240,7 @@ mod nonwasm_sealed {
|
||||
impl GatewaySender for LocalGateway {
|
||||
async fn send_mix_packet(&mut self, packet: MixPacket) -> Result<(), ErasedGatewayError> {
|
||||
self.packet_forwarder
|
||||
.forward_packet(packet)
|
||||
.forward_client_packet_without_delay(packet)
|
||||
.map_err(erase_err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ publish = true
|
||||
[dependencies]
|
||||
dashmap = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
strum = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
tokio = { workspace = true, features = ["time", "sync"] }
|
||||
tokio-util = { workspace = true, features = ["codec"], optional = true }
|
||||
@@ -26,11 +27,13 @@ tokio-stream = { workspace = true }
|
||||
nym-noise = { workspace = true }
|
||||
nym-sphinx = { workspace = true }
|
||||
nym-task = { workspace = true, optional = true }
|
||||
nym-metrics = { workspace = true, optional = true }
|
||||
|
||||
[features]
|
||||
default = ["client"]
|
||||
client = ["tokio-util", "nym-task", "tokio/net", "tokio/rt"]
|
||||
client = ["tokio-util", "nym-task", "nym-metrics", "tokio/net", "tokio/rt"]
|
||||
|
||||
[dev-dependencies]
|
||||
nym-crypto = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
tokio = { workspace = true, features = ["macros", "io-util", "rt", "rt-multi-thread", "test-util"] }
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
// Copyright 2021-2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::metrics::{MixnetMetric, Traced};
|
||||
use dashmap::DashMap;
|
||||
use futures::StreamExt;
|
||||
use futures::{Sink, SinkExt, StreamExt};
|
||||
use nym_noise::config::NoiseConfig;
|
||||
use nym_noise::upgrade_noise_initiator;
|
||||
use nym_sphinx::forwarding::packet::MixPacket;
|
||||
@@ -10,14 +11,15 @@ use nym_sphinx::framing::codec::NymCodec;
|
||||
use nym_sphinx::framing::packet::FramedNymPacket;
|
||||
use std::io;
|
||||
use std::net::SocketAddr;
|
||||
use std::ops::Deref;
|
||||
use std::ops::{ControlFlow, Deref};
|
||||
use std::sync::atomic::{AtomicU32, AtomicUsize, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tokio::io::{AsyncRead, AsyncWrite};
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::sync::mpsc::error::TrySendError;
|
||||
use tokio::time::sleep;
|
||||
use tokio::time::{sleep, Instant};
|
||||
use tokio_stream::wrappers::ReceiverStream;
|
||||
use tokio_util::codec::Framed;
|
||||
use tracing::*;
|
||||
@@ -29,6 +31,13 @@ pub struct Config {
|
||||
pub initial_connection_timeout: Duration,
|
||||
pub maximum_connection_buffer_size: usize,
|
||||
pub use_legacy_packet_encoding: bool,
|
||||
/// Close an egress connection after this long with no packets sent (0 disables). The cache
|
||||
/// entry is evicted on close and the next packet to that peer transparently reconnects.
|
||||
pub connection_idle_timeout: Duration,
|
||||
/// Max time a single batch flush may block on the peer socket before we give up on it
|
||||
/// (0 disables). One timeout is treated as transient congestion - the batch is abandoned but
|
||||
/// the connection is retained (no re-handshake); only a few *consecutive* timeouts tear it down.
|
||||
pub connection_write_timeout: Duration,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
@@ -38,6 +47,8 @@ impl Config {
|
||||
initial_connection_timeout: Duration,
|
||||
maximum_connection_buffer_size: usize,
|
||||
use_legacy_packet_encoding: bool,
|
||||
connection_idle_timeout: Duration,
|
||||
connection_write_timeout: Duration,
|
||||
) -> Self {
|
||||
Config {
|
||||
initial_reconnection_backoff,
|
||||
@@ -45,14 +56,18 @@ impl Config {
|
||||
initial_connection_timeout,
|
||||
maximum_connection_buffer_size,
|
||||
use_legacy_packet_encoding,
|
||||
connection_idle_timeout,
|
||||
connection_write_timeout,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait SendWithoutResponse {
|
||||
// Without response in this context means we will not listen for anything we might get back (not
|
||||
// that we should get anything), including any possible io errors
|
||||
fn send_without_response(&self, packet: MixPacket) -> io::Result<()>;
|
||||
// that we should get anything), including any possible io errors.
|
||||
// The packet carries the latency trace started upstream (at receive); the egress stages are
|
||||
// stamped here and are a no-op for unsampled packets.
|
||||
fn send_without_response(&self, packet: Traced<MixPacket>) -> io::Result<()>;
|
||||
}
|
||||
|
||||
pub struct Client {
|
||||
@@ -88,15 +103,19 @@ impl Deref for ActiveConnections {
|
||||
}
|
||||
|
||||
pub struct ConnectionSender {
|
||||
channel: mpsc::Sender<FramedNymPacket>,
|
||||
channel: mpsc::Sender<Traced<FramedNymPacket>>,
|
||||
current_reconnection_attempt: Arc<AtomicU32>,
|
||||
// Identifies the `ManagedConnection` task currently owning this entry; used
|
||||
// to ensure drop-time eviction only fires on the still-owning task.
|
||||
handle_token: Arc<()>,
|
||||
}
|
||||
|
||||
impl ConnectionSender {
|
||||
fn new(channel: mpsc::Sender<FramedNymPacket>) -> Self {
|
||||
fn new(channel: mpsc::Sender<Traced<FramedNymPacket>>, handle_token: Arc<()>) -> Self {
|
||||
ConnectionSender {
|
||||
channel,
|
||||
current_reconnection_attempt: Arc::new(AtomicU32::new(0)),
|
||||
handle_token,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -104,91 +123,85 @@ impl ConnectionSender {
|
||||
struct ManagedConnection {
|
||||
address: SocketAddr,
|
||||
noise_config: NoiseConfig,
|
||||
message_receiver: ReceiverStream<FramedNymPacket>,
|
||||
message_receiver: ReceiverStream<Traced<FramedNymPacket>>,
|
||||
connection_timeout: Duration,
|
||||
idle_timeout: Duration,
|
||||
write_timeout: Duration,
|
||||
current_reconnection: Arc<AtomicU32>,
|
||||
active_connections: ActiveConnections,
|
||||
handle_token: Arc<()>,
|
||||
}
|
||||
|
||||
// Evicts the cache entry on task exit (only if still owned by this task).
|
||||
// Without this, a stale `ConnectionSender` survives after the peer disconnects
|
||||
// and the next outbound packet is silently swallowed by the dead TCP.
|
||||
struct EvictOnDrop {
|
||||
active_connections: ActiveConnections,
|
||||
address: SocketAddr,
|
||||
handle_token: Arc<()>,
|
||||
}
|
||||
|
||||
impl Drop for EvictOnDrop {
|
||||
fn drop(&mut self) {
|
||||
let address = self.address;
|
||||
let handle_token = &self.handle_token;
|
||||
self.active_connections.remove_if(&address, |_, sender| {
|
||||
Arc::ptr_eq(&sender.handle_token, handle_token)
|
||||
});
|
||||
trace!(
|
||||
peer = %address,
|
||||
"managed connection task exited; evicted owning cache entry"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
impl ManagedConnection {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn new(
|
||||
address: SocketAddr,
|
||||
noise_config: NoiseConfig,
|
||||
message_receiver: mpsc::Receiver<FramedNymPacket>,
|
||||
message_receiver: mpsc::Receiver<Traced<FramedNymPacket>>,
|
||||
connection_timeout: Duration,
|
||||
idle_timeout: Duration,
|
||||
write_timeout: Duration,
|
||||
current_reconnection: Arc<AtomicU32>,
|
||||
active_connections: ActiveConnections,
|
||||
handle_token: Arc<()>,
|
||||
) -> Self {
|
||||
ManagedConnection {
|
||||
address,
|
||||
noise_config,
|
||||
message_receiver: ReceiverStream::new(message_receiver),
|
||||
connection_timeout,
|
||||
idle_timeout,
|
||||
write_timeout,
|
||||
current_reconnection,
|
||||
active_connections,
|
||||
handle_token,
|
||||
}
|
||||
}
|
||||
|
||||
async fn run(self) {
|
||||
let address = self.address;
|
||||
let idle_timeout = self.idle_timeout;
|
||||
let write_timeout = self.write_timeout;
|
||||
let _evict_guard = EvictOnDrop {
|
||||
active_connections: self.active_connections,
|
||||
address,
|
||||
handle_token: self.handle_token,
|
||||
};
|
||||
|
||||
let reconnection_attempt = self.current_reconnection.load(Ordering::Acquire);
|
||||
let connect_start = tokio::time::Instant::now();
|
||||
let connection_fut = TcpStream::connect(address);
|
||||
|
||||
let conn = match tokio::time::timeout(self.connection_timeout, connection_fut).await {
|
||||
Ok(stream_res) => match stream_res {
|
||||
Ok(stream) => {
|
||||
let connect_ms = connect_start.elapsed().as_millis() as u64;
|
||||
debug!(
|
||||
peer = %address,
|
||||
connect_ms,
|
||||
"Managed to establish connection to {}", self.address
|
||||
);
|
||||
|
||||
let noise_start = tokio::time::Instant::now();
|
||||
let noise_stream =
|
||||
match upgrade_noise_initiator(stream, &self.noise_config).await {
|
||||
Ok(noise_stream) => noise_stream,
|
||||
Err(err) => {
|
||||
let noise_handshake_ms = noise_start.elapsed().as_millis() as u64;
|
||||
warn!(
|
||||
event = "connection.failed.noise",
|
||||
peer = %address,
|
||||
error = %err,
|
||||
connect_ms,
|
||||
noise_handshake_ms,
|
||||
reconnection_attempt,
|
||||
exit_reason = "noise_error",
|
||||
"Failed to perform Noise initiator handshake with {address}"
|
||||
);
|
||||
self.current_reconnection.fetch_add(1, Ordering::SeqCst);
|
||||
return;
|
||||
}
|
||||
};
|
||||
let noise_handshake_ms = noise_start.elapsed().as_millis() as u64;
|
||||
self.current_reconnection.store(0, Ordering::Release);
|
||||
debug!(
|
||||
peer = %address,
|
||||
connect_ms,
|
||||
noise_handshake_ms,
|
||||
"Noise initiator handshake completed for {:?}", address
|
||||
);
|
||||
Framed::new(noise_stream, NymCodec)
|
||||
}
|
||||
Err(err) => {
|
||||
let connect_ms = connect_start.elapsed().as_millis() as u64;
|
||||
warn!(
|
||||
event = "connection.failed.connect",
|
||||
peer = %address,
|
||||
error = %err,
|
||||
connect_ms,
|
||||
reconnection_attempt,
|
||||
exit_reason = "connect_error",
|
||||
"failed to establish connection to {address}"
|
||||
);
|
||||
return;
|
||||
}
|
||||
},
|
||||
// 1. attempt to establish the connection with timeout
|
||||
let maybe_stream = match tokio::time::timeout(self.connection_timeout, connection_fut).await
|
||||
{
|
||||
Ok(stream) => stream,
|
||||
Err(_) => {
|
||||
let connect_ms = connect_start.elapsed().as_millis() as u64;
|
||||
warn!(
|
||||
debug!(
|
||||
event = "connection.failed.timeout",
|
||||
peer = %address,
|
||||
timeout_ms = self.connection_timeout.as_millis() as u64,
|
||||
@@ -203,21 +216,295 @@ impl ManagedConnection {
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(err) = self.message_receiver.map(Ok).forward(conn).await {
|
||||
warn!(
|
||||
// 2. check if it actually succeeded
|
||||
let stream = match maybe_stream {
|
||||
Ok(stream) => stream,
|
||||
Err(err) => {
|
||||
let connect_ms = connect_start.elapsed().as_millis() as u64;
|
||||
debug!(
|
||||
event = "connection.failed.connect",
|
||||
peer = %address,
|
||||
error = %err,
|
||||
connect_ms,
|
||||
reconnection_attempt,
|
||||
exit_reason = "connect_error",
|
||||
"failed to establish connection to {address}"
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let connect_ms = connect_start.elapsed().as_millis() as u64;
|
||||
debug!(
|
||||
peer = %address,
|
||||
connect_ms,
|
||||
"Managed to establish connection to {}", self.address
|
||||
);
|
||||
|
||||
// disable Nagle: mix packets are latency-sensitive and flushed one at a time.
|
||||
if let Err(err) = stream.set_nodelay(true) {
|
||||
warn!(peer = %address, error = %err, "failed to set TCP_NODELAY on outbound mixnet connection");
|
||||
}
|
||||
|
||||
// 3. perform noise handshake (if applicable)
|
||||
let noise_start = tokio::time::Instant::now();
|
||||
let noise_stream = match upgrade_noise_initiator(stream, &self.noise_config).await {
|
||||
Ok(noise_stream) => noise_stream,
|
||||
Err(err) => {
|
||||
let noise_handshake_ms = noise_start.elapsed().as_millis() as u64;
|
||||
debug!(
|
||||
event = "connection.failed.noise",
|
||||
peer = %address,
|
||||
error = %err,
|
||||
connect_ms,
|
||||
noise_handshake_ms,
|
||||
reconnection_attempt,
|
||||
exit_reason = "noise_error",
|
||||
"Failed to perform Noise initiator handshake with {address}"
|
||||
);
|
||||
self.current_reconnection.fetch_add(1, Ordering::SeqCst);
|
||||
return;
|
||||
}
|
||||
};
|
||||
let noise_handshake_ms = noise_start.elapsed().as_millis() as u64;
|
||||
self.current_reconnection.store(0, Ordering::Release);
|
||||
debug!(
|
||||
peer = %address,
|
||||
connect_ms,
|
||||
noise_handshake_ms,
|
||||
"Noise initiator handshake completed for {:?}", address
|
||||
);
|
||||
let mut conn = Framed::new(noise_stream, NymCodec);
|
||||
// let the write buffer accumulate several packets before flushing (see run_io_loop)
|
||||
conn.set_backpressure_boundary(OUTBOUND_WRITE_BUFFER);
|
||||
|
||||
// 4. start handling the framed stream
|
||||
run_io_loop(
|
||||
conn,
|
||||
self.message_receiver,
|
||||
address,
|
||||
idle_timeout,
|
||||
write_timeout,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
/// Upper bound on how many already-queued packets we drain into a single flush.
|
||||
/// Bounds the per-batch allocation and how often we re-check the read side; the actual
|
||||
/// write coalescing is governed by the Framed backpressure boundary below.
|
||||
const OUTBOUND_FLUSH_BATCH: usize = 1024;
|
||||
|
||||
/// Write-buffer high-water mark for the egress `Framed`: packets are coalesced up to
|
||||
/// roughly this many bytes before a flush, trading a larger write burst for far fewer
|
||||
/// syscalls (and noise frames) under load. Kept under the ~64KiB noise frame ceiling so
|
||||
/// a flush is usually a single frame.
|
||||
const OUTBOUND_WRITE_BUFFER: usize = 32 * 1024;
|
||||
|
||||
/// Drive the read half solely to notice peer FIN/RST (the connection is send-only). Returns
|
||||
/// `Break` when the peer closed the connection or the read errored, `Continue` otherwise.
|
||||
fn handle_peer_read<P, E: std::fmt::Display>(
|
||||
msg: Option<Result<P, E>>,
|
||||
address: SocketAddr,
|
||||
) -> ControlFlow<()> {
|
||||
match msg {
|
||||
None => {
|
||||
debug!(
|
||||
peer = %address,
|
||||
exit_reason = "peer_closed",
|
||||
"peer closed mixnet connection to {address}"
|
||||
);
|
||||
ControlFlow::Break(())
|
||||
}
|
||||
Some(Err(err)) => {
|
||||
debug!(
|
||||
event = "connection.read_error",
|
||||
peer = %address,
|
||||
error = %err,
|
||||
exit_reason = "read_error",
|
||||
"read error on mixnet connection to {address}: {err}"
|
||||
);
|
||||
ControlFlow::Break(())
|
||||
}
|
||||
Some(Ok(_)) => {
|
||||
trace!(
|
||||
peer = %address,
|
||||
"unexpected inbound packet on mixnet connection to {address}; discarding"
|
||||
);
|
||||
ControlFlow::Continue(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Number of consecutive flush timeouts to the same peer we tolerate before dropping the
|
||||
/// connection. A single timeout is transient congestion (batch abandoned, connection retained to
|
||||
/// avoid a re-handshake); this many in a row means the peer is persistently unable to keep up, so
|
||||
/// we tear the connection down (it reconnects on the next packet).
|
||||
const MAX_CONSECUTIVE_WRITE_TIMEOUTS: u32 = 3;
|
||||
|
||||
/// Outcome of attempting to flush one batch to the peer.
|
||||
enum BatchOutcome {
|
||||
/// the batch was flushed to the socket
|
||||
Sent,
|
||||
/// the flush exceeded the write timeout (peer congested): the un-fed tail of the batch is
|
||||
/// dropped, but the already-encoded frames stay buffered for a later flush and the connection
|
||||
/// is left intact - the noise transport stays nonce-consistent across the cancelled flush, so
|
||||
/// resuming the write is sound
|
||||
WriteTimedOut,
|
||||
/// the sink errored: the connection is dead
|
||||
Failed,
|
||||
}
|
||||
|
||||
/// Feed a ready batch into the sink and flush it once (far fewer syscalls than per-packet), then
|
||||
/// stamp the egress latency stages: `EgressQueue` before each feed, then `SocketWrite` + the
|
||||
/// end-to-end total once the batch has hit the wire. The flush is bounded by `write_timeout`
|
||||
/// (0 disables) so a congested peer can't block this connection's egress queue into the
|
||||
/// multi-second range. The caller decides what a timeout means (see [`MAX_CONSECUTIVE_WRITE_TIMEOUTS`]).
|
||||
async fn forward_batch<S>(
|
||||
sink: &mut S,
|
||||
batch: Vec<Traced<FramedNymPacket>>,
|
||||
address: SocketAddr,
|
||||
write_timeout: Duration,
|
||||
) -> BatchOutcome
|
||||
where
|
||||
S: Sink<FramedNymPacket> + Unpin,
|
||||
S::Error: std::fmt::Display,
|
||||
{
|
||||
let mut traces = Vec::with_capacity(batch.len());
|
||||
let write = async {
|
||||
for mut traced in batch {
|
||||
// time spent waiting in this connection's egress buffer
|
||||
traced.record(MixnetMetric::EgressQueue);
|
||||
sink.feed(traced.inner).await?;
|
||||
traces.push(traced.trace);
|
||||
}
|
||||
sink.flush().await
|
||||
};
|
||||
|
||||
// bound how long we block on a slow/congested peer socket. On timeout the `write` future is
|
||||
// cancelled, which is safe: every already-encoded frame is buffered (nonce-consistent), so a
|
||||
// later flush resumes the byte stream in order.
|
||||
let write_result = if write_timeout.is_zero() {
|
||||
Ok(write.await)
|
||||
} else {
|
||||
tokio::time::timeout(write_timeout, write).await
|
||||
};
|
||||
|
||||
// socket-write time + end-to-end total for whatever was fed (on a timeout, those frames are
|
||||
// buffered and will hit the wire on a subsequent flush)
|
||||
for mut trace in traces {
|
||||
trace.record(MixnetMetric::SocketWrite);
|
||||
trace.record_total();
|
||||
}
|
||||
|
||||
match write_result {
|
||||
Ok(Ok(())) => BatchOutcome::Sent,
|
||||
Ok(Err(err)) => {
|
||||
debug!(
|
||||
event = "connection.forward_error",
|
||||
peer = %address,
|
||||
error = %err,
|
||||
exit_reason = "forward_error",
|
||||
"Failed to forward packets to {address}: {err}"
|
||||
"failed to forward packet batch to {address}: {err}"
|
||||
);
|
||||
BatchOutcome::Failed
|
||||
}
|
||||
Err(_elapsed) => BatchOutcome::WriteTimedOut,
|
||||
}
|
||||
}
|
||||
|
||||
debug!(
|
||||
peer = %address,
|
||||
exit_reason = "sender_dropped",
|
||||
"connection manager to {address} finished"
|
||||
);
|
||||
/// Instant at which a connection idle since `last_activity` should be closed, or `None` if idle
|
||||
/// reaping is disabled (`timeout` is zero).
|
||||
fn idle_deadline(last_activity: Instant, timeout: Duration) -> Option<Instant> {
|
||||
(!timeout.is_zero()).then(|| last_activity + timeout)
|
||||
}
|
||||
|
||||
// The connection is unidirectional (send-only); we read from it solely to
|
||||
// notice peer FIN/RST while idle so we can evict the cache entry before the
|
||||
// next outbound send finds it stale.
|
||||
async fn run_io_loop<T>(
|
||||
conn: Framed<T, NymCodec>,
|
||||
receiver: ReceiverStream<Traced<FramedNymPacket>>,
|
||||
address: SocketAddr,
|
||||
idle_timeout: Duration,
|
||||
write_timeout: Duration,
|
||||
) where
|
||||
T: AsyncRead + AsyncWrite + Unpin,
|
||||
{
|
||||
let (mut sink, mut stream) = conn.split();
|
||||
|
||||
// drain all currently-queued packets into one flush rather than flushing per packet,
|
||||
// which otherwise caps egress throughput and backs up the per-connection queue under load
|
||||
let mut receiver = receiver.ready_chunks(OUTBOUND_FLUSH_BATCH);
|
||||
|
||||
// reset by every batch we send; drives the idle-connection reaping below
|
||||
let mut last_send = tokio::time::Instant::now();
|
||||
// consecutive flush timeouts; a run of them (a persistently congested peer) drops the connection
|
||||
let mut consecutive_write_timeouts = 0u32;
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
msg = stream.next() => {
|
||||
if handle_peer_read(msg, address).is_break() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
outgoing = receiver.next() => {
|
||||
let Some(batch) = outgoing else {
|
||||
debug!(
|
||||
peer = %address,
|
||||
exit_reason = "sender_dropped",
|
||||
"connection manager to {address} finished"
|
||||
);
|
||||
break;
|
||||
};
|
||||
match forward_batch(&mut sink, batch, address, write_timeout).await {
|
||||
BatchOutcome::Sent => {
|
||||
consecutive_write_timeouts = 0;
|
||||
last_send = Instant::now();
|
||||
}
|
||||
BatchOutcome::WriteTimedOut => {
|
||||
consecutive_write_timeouts += 1;
|
||||
warn!(
|
||||
event = "connection.write_congested",
|
||||
peer = %address,
|
||||
write_ms = write_timeout.as_millis() as u64,
|
||||
attempt = consecutive_write_timeouts,
|
||||
max_attempts = MAX_CONSECUTIVE_WRITE_TIMEOUTS,
|
||||
"egress flush to {address} timed out (peer congested); abandoned batch, retaining connection"
|
||||
);
|
||||
if consecutive_write_timeouts >= MAX_CONSECUTIVE_WRITE_TIMEOUTS {
|
||||
debug!(
|
||||
peer = %address,
|
||||
exit_reason = "write_timeout",
|
||||
"egress connection to {address} congested for {MAX_CONSECUTIVE_WRITE_TIMEOUTS} consecutive flushes; dropping it"
|
||||
);
|
||||
break;
|
||||
}
|
||||
// keep the connection: a single congestion spike shouldn't cost a
|
||||
// re-handshake. `last_send` is deliberately not bumped, so a peer that goes
|
||||
// congested-then-silent still idle-reaps on schedule.
|
||||
}
|
||||
BatchOutcome::Failed => break,
|
||||
}
|
||||
}
|
||||
// close the connection (freeing the task/socket) if we haven't sent anything for too
|
||||
// long; EvictOnDrop then clears the cache entry and the next packet reconnects
|
||||
_ = async {
|
||||
match idle_deadline(last_send, idle_timeout) {
|
||||
Some(d) => tokio::time::sleep_until(d).await,
|
||||
None => std::future::pending::<()>().await,
|
||||
}
|
||||
} => {
|
||||
debug!(
|
||||
peer = %address,
|
||||
exit_reason = "idle_timeout",
|
||||
idle_secs = idle_timeout.as_secs(),
|
||||
"closing idle egress mixnet connection to {address}"
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -256,7 +543,7 @@ impl Client {
|
||||
}
|
||||
}
|
||||
|
||||
fn make_connection(&self, address: SocketAddr, pending_packet: FramedNymPacket) {
|
||||
fn make_connection(&self, address: SocketAddr, pending_packet: Traced<FramedNymPacket>) {
|
||||
let (sender, receiver) = mpsc::channel(self.config.maximum_connection_buffer_size);
|
||||
|
||||
// this CAN'T fail because we just created the channel which has a non-zero capacity
|
||||
@@ -264,13 +551,18 @@ impl Client {
|
||||
sender.try_send(pending_packet).unwrap();
|
||||
}
|
||||
|
||||
// Ownership token for the task we're about to spawn; lets it tell
|
||||
// on exit whether the cache entry still names it.
|
||||
let handle_token = Arc::new(());
|
||||
|
||||
// if we already tried to connect to `address` before, grab the current attempt count
|
||||
let current_reconnection_attempt =
|
||||
if let Some(mut existing) = self.active_connections.get_mut(&address) {
|
||||
existing.channel = sender;
|
||||
existing.handle_token = Arc::clone(&handle_token);
|
||||
Arc::clone(&existing.current_reconnection_attempt)
|
||||
} else {
|
||||
let new_entry = ConnectionSender::new(sender);
|
||||
let new_entry = ConnectionSender::new(sender, Arc::clone(&handle_token));
|
||||
let current_attempt = Arc::clone(&new_entry.current_reconnection_attempt);
|
||||
self.active_connections.insert(address, new_entry);
|
||||
current_attempt
|
||||
@@ -280,11 +572,14 @@ impl Client {
|
||||
let reconnection_attempt = current_reconnection_attempt.load(Ordering::Acquire);
|
||||
let backoff = self.determine_backoff(reconnection_attempt);
|
||||
|
||||
// copy the value before moving into another task
|
||||
// copy the values before moving into another task
|
||||
let initial_connection_timeout = self.config.initial_connection_timeout;
|
||||
let connection_idle_timeout = self.config.connection_idle_timeout;
|
||||
let connection_write_timeout = self.config.connection_write_timeout;
|
||||
|
||||
let connections_count = self.connections_count.clone();
|
||||
let noise_config = self.noise_config.clone();
|
||||
let active_connections = self.active_connections.clone();
|
||||
tokio::spawn(async move {
|
||||
// before executing the manager, wait for what was specified, if anything
|
||||
if let Some(backoff) = backoff {
|
||||
@@ -298,7 +593,11 @@ impl Client {
|
||||
noise_config,
|
||||
receiver,
|
||||
initial_connection_timeout,
|
||||
connection_idle_timeout,
|
||||
connection_write_timeout,
|
||||
current_reconnection_attempt,
|
||||
active_connections,
|
||||
handle_token,
|
||||
)
|
||||
.run()
|
||||
.await;
|
||||
@@ -308,14 +607,17 @@ impl Client {
|
||||
}
|
||||
|
||||
impl SendWithoutResponse for Client {
|
||||
fn send_without_response(&self, packet: MixPacket) -> io::Result<()> {
|
||||
let address = packet.next_hop_address();
|
||||
fn send_without_response(&self, packet: Traced<MixPacket>) -> io::Result<()> {
|
||||
let address = packet.inner.next_hop_address();
|
||||
trace!("Sending packet to {address}");
|
||||
|
||||
// capture the sample state before the trace is moved into `queued`
|
||||
let sampled = packet.trace.is_sampled();
|
||||
|
||||
// TODO: optimisation for the future: rather than constantly using legacy encoding,
|
||||
// use the mix packet type / flags to pick encoding per packet
|
||||
let framed_packet =
|
||||
FramedNymPacket::from_mix_packet(packet, self.config.use_legacy_packet_encoding);
|
||||
let legacy = self.config.use_legacy_packet_encoding;
|
||||
let queued = packet.map(|p| FramedNymPacket::from_mix_packet(p, legacy));
|
||||
|
||||
let Some(sender) = self.active_connections.get_mut(&address) else {
|
||||
// there was never a connection to begin with
|
||||
@@ -325,7 +627,7 @@ impl SendWithoutResponse for Client {
|
||||
result = "not_connected",
|
||||
"establishing initial connection to {address}"
|
||||
);
|
||||
self.make_connection(address, framed_packet);
|
||||
self.make_connection(address, queued);
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::NotConnected,
|
||||
"connection is in progress",
|
||||
@@ -336,7 +638,12 @@ impl SendWithoutResponse for Client {
|
||||
let channel_available = sender.channel.capacity();
|
||||
let channel_used = channel_capacity - channel_available;
|
||||
|
||||
let sending_res = sender.channel.try_send(framed_packet);
|
||||
// record how full this peer's egress buffer was (sampled packets only, to bound cost)
|
||||
if sampled {
|
||||
crate::metrics::observe_egress_buffer_fill(channel_used, channel_capacity);
|
||||
}
|
||||
|
||||
let sending_res = sender.channel.try_send(queued);
|
||||
drop(sender);
|
||||
|
||||
sending_res.map_err(|err| {
|
||||
@@ -391,6 +698,8 @@ mod tests {
|
||||
initial_connection_timeout: Duration::from_millis(1_500),
|
||||
maximum_connection_buffer_size: 128,
|
||||
use_legacy_packet_encoding: false,
|
||||
connection_idle_timeout: Duration::from_secs(300),
|
||||
connection_write_timeout: Duration::from_millis(500),
|
||||
},
|
||||
NoiseConfig::new(
|
||||
Arc::new(x25519::KeyPair::new(&mut rng)),
|
||||
@@ -428,4 +737,143 @@ mod tests {
|
||||
client.config.maximum_reconnection_backoff
|
||||
);
|
||||
}
|
||||
|
||||
fn test_addr() -> SocketAddr {
|
||||
"127.0.0.1:1".parse().unwrap()
|
||||
}
|
||||
|
||||
fn insert_with_token(
|
||||
active: &ActiveConnections,
|
||||
addr: SocketAddr,
|
||||
token: Arc<()>,
|
||||
) -> mpsc::Receiver<Traced<FramedNymPacket>> {
|
||||
let (tx, rx) = mpsc::channel(1);
|
||||
active.insert(addr, ConnectionSender::new(tx, token));
|
||||
rx
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn evict_on_drop_removes_entry_when_token_still_matches() {
|
||||
let active = ActiveConnections::default();
|
||||
let addr = test_addr();
|
||||
let token = Arc::new(());
|
||||
let _rx = insert_with_token(&active, addr, Arc::clone(&token));
|
||||
|
||||
assert!(active.get(&addr).is_some());
|
||||
|
||||
{
|
||||
let _guard = EvictOnDrop {
|
||||
active_connections: active.clone(),
|
||||
address: addr,
|
||||
handle_token: token,
|
||||
};
|
||||
}
|
||||
|
||||
assert!(
|
||||
active.get(&addr).is_none(),
|
||||
"owning task's drop should evict the entry"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn evict_on_drop_preserves_entry_replaced_by_newer_make_connection() {
|
||||
// Simulates the race: old task's run() has returned, but before its
|
||||
// drop guard fires, a concurrent `make_connection` replaced the
|
||||
// entry's channel + handle_token with a fresh task's token.
|
||||
let active = ActiveConnections::default();
|
||||
let addr = test_addr();
|
||||
let old_token = Arc::new(());
|
||||
let new_token = Arc::new(());
|
||||
let _rx_new = insert_with_token(&active, addr, Arc::clone(&new_token));
|
||||
|
||||
{
|
||||
let _guard = EvictOnDrop {
|
||||
active_connections: active.clone(),
|
||||
address: addr,
|
||||
handle_token: old_token,
|
||||
};
|
||||
}
|
||||
|
||||
assert!(
|
||||
active.get(&addr).is_some(),
|
||||
"old task's drop must not clobber the newer entry"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn io_loop_exits_when_peer_closes_idle_connection() {
|
||||
// The fix's second half: while no packets are flowing, peer FIN/RST
|
||||
// must still be observed so the cache entry can be evicted before the
|
||||
// next send finds it stale.
|
||||
let (a, b) = tokio::io::duplex(64);
|
||||
let conn = Framed::new(a, NymCodec);
|
||||
let (_tx, rx) = mpsc::channel(1);
|
||||
|
||||
// idle reaping disabled so only the peer-close path is exercised
|
||||
let task = tokio::spawn(run_io_loop(
|
||||
conn,
|
||||
ReceiverStream::new(rx),
|
||||
test_addr(),
|
||||
Duration::ZERO,
|
||||
Duration::ZERO,
|
||||
));
|
||||
|
||||
// Simulate peer closing both directions of the connection.
|
||||
drop(b);
|
||||
|
||||
tokio::time::timeout(Duration::from_secs(1), task)
|
||||
.await
|
||||
.expect("io_loop must notice peer close while idle")
|
||||
.expect("io_loop task must not panic");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn io_loop_exits_when_sender_dropped() {
|
||||
let (a, _b) = tokio::io::duplex(64);
|
||||
let conn = Framed::new(a, NymCodec);
|
||||
let (tx, rx) = mpsc::channel(1);
|
||||
|
||||
let task = tokio::spawn(run_io_loop(
|
||||
conn,
|
||||
ReceiverStream::new(rx),
|
||||
test_addr(),
|
||||
Duration::ZERO,
|
||||
Duration::ZERO,
|
||||
));
|
||||
|
||||
drop(tx);
|
||||
|
||||
tokio::time::timeout(Duration::from_secs(1), task)
|
||||
.await
|
||||
.expect("io_loop must exit when the upstream sender is dropped")
|
||||
.expect("io_loop task must not panic");
|
||||
}
|
||||
|
||||
#[tokio::test(start_paused = true)]
|
||||
async fn io_loop_closes_idle_connection() {
|
||||
// With no packets sent and the peer still connected, the idle timeout must eventually
|
||||
// close the connection so the task/socket don't linger forever. The paused clock is
|
||||
// virtual - it auto-advances to the next timer, so this completes instantly despite the
|
||||
// durations below (no real waiting).
|
||||
let (a, _b) = tokio::io::duplex(64);
|
||||
let conn = Framed::new(a, NymCodec);
|
||||
// keep the sender alive so the sender-dropped path can't fire instead
|
||||
let (_tx, rx) = mpsc::channel(1);
|
||||
|
||||
let idle_timeout = Duration::from_millis(50);
|
||||
let task = tokio::spawn(run_io_loop(
|
||||
conn,
|
||||
ReceiverStream::new(rx),
|
||||
test_addr(),
|
||||
idle_timeout,
|
||||
Duration::ZERO,
|
||||
));
|
||||
|
||||
// auto-advance fires the nearest timer (the 50ms idle deadline, sooner than this 500ms
|
||||
// guard) once the task is otherwise idle, reaping the connection
|
||||
tokio::time::timeout(Duration::from_millis(500), task)
|
||||
.await
|
||||
.expect("io_loop must close the connection after the idle timeout")
|
||||
.expect("io_loop task must not panic");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::metrics::PacketTrace;
|
||||
use futures::channel::mpsc;
|
||||
use futures::channel::mpsc::SendError;
|
||||
use nym_sphinx::forwarding::packet::MixPacket;
|
||||
@@ -21,12 +22,16 @@ impl From<mpsc::UnboundedSender<PacketToForward>> for MixForwardingSender {
|
||||
}
|
||||
|
||||
impl MixForwardingSender {
|
||||
pub fn forward_packet(&self, packet: impl Into<PacketToForward>) -> Result<(), SendError> {
|
||||
pub fn forward_packet(&self, packet: PacketToForward) -> Result<(), SendError> {
|
||||
self.0
|
||||
.unbounded_send(packet.into())
|
||||
.unbounded_send(packet)
|
||||
.map_err(|err| err.into_send_error())
|
||||
}
|
||||
|
||||
pub fn forward_client_packet_without_delay(&self, packet: MixPacket) -> Result<(), SendError> {
|
||||
self.forward_packet(PacketToForward::client_packet_without_delay(packet))
|
||||
}
|
||||
|
||||
#[allow(clippy::len_without_is_empty)]
|
||||
pub fn len(&self) -> usize {
|
||||
self.0.len()
|
||||
@@ -38,35 +43,28 @@ pub type MixForwardingReceiver = mpsc::UnboundedReceiver<PacketToForward>;
|
||||
pub struct PacketToForward {
|
||||
pub packet: MixPacket,
|
||||
pub forward_delay_target: Option<Instant>,
|
||||
}
|
||||
|
||||
impl From<MixPacket> for PacketToForward {
|
||||
fn from(packet: MixPacket) -> Self {
|
||||
PacketToForward::new_no_delay(packet)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(MixPacket, Option<Instant>)> for PacketToForward {
|
||||
fn from((packet, delay_until): (MixPacket, Option<Instant>)) -> Self {
|
||||
PacketToForward::new(packet, delay_until)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(MixPacket, Instant)> for PacketToForward {
|
||||
fn from((packet, delay_until): (MixPacket, Instant)) -> Self {
|
||||
PacketToForward::new(packet, Some(delay_until))
|
||||
}
|
||||
pub network_monitor_packet: bool,
|
||||
/// Latency breadcrumb started at packet receive; stamped as the packet moves through the
|
||||
/// forwarder and egress stages. `PacketTrace::Off` for untraced packets (e.g. acks).
|
||||
pub trace: PacketTrace,
|
||||
}
|
||||
|
||||
impl PacketToForward {
|
||||
pub fn new(packet: MixPacket, forward_delay_target: Option<Instant>) -> Self {
|
||||
pub fn new(
|
||||
packet: MixPacket,
|
||||
forward_delay_target: Option<Instant>,
|
||||
network_monitor_packet: bool,
|
||||
trace: PacketTrace,
|
||||
) -> Self {
|
||||
PacketToForward {
|
||||
packet,
|
||||
forward_delay_target,
|
||||
network_monitor_packet,
|
||||
trace,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_no_delay(packet: MixPacket) -> Self {
|
||||
Self::new(packet, None)
|
||||
pub fn client_packet_without_delay(packet: MixPacket) -> Self {
|
||||
Self::new(packet, None, false, PacketTrace::Off)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#[cfg(feature = "client")]
|
||||
pub mod client;
|
||||
pub mod forwarder;
|
||||
pub mod metrics;
|
||||
|
||||
#[cfg(feature = "client")]
|
||||
pub use client::{Client, Config, SendWithoutResponse};
|
||||
|
||||
@@ -0,0 +1,311 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use strum::{AsRefStr, EnumIter, EnumProperty, IntoEnumIterator};
|
||||
use tokio::time::Instant;
|
||||
|
||||
/// Histogram buckets (seconds) for per-stage and total packet latency: exponential, ~100us .. ~6.5s.
|
||||
/// Shared by every latency stage so the waterfall is directly comparable; the top finite bucket is
|
||||
/// intentionally high so a rare multi-second processing spike is measured with magnitude rather than
|
||||
/// being clipped into the `+Inf` overflow.
|
||||
const STAGE_LATENCY_BUCKETS: [f64; 17] = [
|
||||
0.0001, 0.0002, 0.0004, 0.0008, 0.0016, 0.0032, 0.0064, 0.0128, 0.0256, 0.0512, 0.1024, 0.2048,
|
||||
0.4096, 0.8192, 1.6384, 3.2768, 6.5536,
|
||||
];
|
||||
|
||||
/// Count buckets (1 .. MAX_DRAIN_BATCH) for the forwarder drain-batch-size histogram.
|
||||
const DRAIN_BATCH_BUCKETS: [f64; 9] = [1.0, 2.0, 4.0, 8.0, 16.0, 32.0, 64.0, 128.0, 256.0];
|
||||
|
||||
/// Fill-ratio buckets (used/capacity) for the per-connection egress buffer. A ratio near 1.0 means
|
||||
/// the buffer is close to full and packets to that peer are about to be dropped.
|
||||
const EGRESS_FILL_BUCKETS: [f64; 9] = [0.05, 0.1, 0.25, 0.5, 0.75, 0.9, 0.95, 0.99, 1.0];
|
||||
|
||||
/// Every histogram this crate emits, defined in one place. `AsRefStr` (`#[strum(to_string=...)]`)
|
||||
/// gives the prometheus metric name - the bare `mixnet_packet_*` family, with no per-crate prefix
|
||||
/// since this is a shared library writing straight to the process-global registry. The `help` prop
|
||||
/// gives the description and [`MixnetMetric::buckets`] gives the bucket layout.
|
||||
///
|
||||
/// Register the whole family at boot with [`register_all`]. Latency-stage variants are observed via
|
||||
/// the [`PacketTrace`] stopwatch; the auxiliary variants via the `observe_*` helpers. (Passing an
|
||||
/// auxiliary variant to `PacketTrace::record` is meaningless but harmless.)
|
||||
#[derive(Clone, Copy, EnumIter, AsRefStr, EnumProperty)]
|
||||
pub enum MixnetMetric {
|
||||
// ----- latency stages: the per-packet waterfall, recorded via `PacketTrace` -----
|
||||
/// receive -> sphinx unwrap (partial: shared secret + header MAC)
|
||||
#[strum(to_string = "mixnet_packet_stage_unwrap_seconds")]
|
||||
#[strum(props(help = "Seconds spent unwrapping a received sphinx packet"))]
|
||||
Unwrap,
|
||||
/// unwrap -> replay-check + finalise (includes the deferral wait)
|
||||
#[strum(to_string = "mixnet_packet_stage_replay_check_seconds")]
|
||||
#[strum(props(
|
||||
help = "Seconds from partial-unwrap to replay-check + finalise (includes the deferral wait)"
|
||||
))]
|
||||
ReplayCheck,
|
||||
/// wait in the ingress -> forwarder channel
|
||||
#[strum(to_string = "mixnet_packet_stage_forwarder_queue_seconds")]
|
||||
#[strum(props(
|
||||
help = "Seconds a forwarded packet waited in the ingress-to-forwarder channel"
|
||||
))]
|
||||
ForwarderQueue,
|
||||
/// the (intended) mix delay
|
||||
#[strum(to_string = "mixnet_packet_stage_delay_queue_seconds")]
|
||||
#[strum(props(help = "Seconds a forwarded packet spent in the (intended) mix delay queue"))]
|
||||
DelayQueue,
|
||||
/// diagnostic overlay on `DelayQueue`: how late beyond the target release the packet was
|
||||
/// actually forwarded (delay-queue scheduling/retrieval overhead, measured vs the deadline)
|
||||
#[strum(to_string = "mixnet_packet_stage_delay_queue_overrun_seconds")]
|
||||
#[strum(props(
|
||||
help = "Seconds a delayed packet was forwarded beyond its target release time (delay-queue scheduling/retrieval overhead)"
|
||||
))]
|
||||
DelayQueueOverrun,
|
||||
/// wait in the per-connection egress buffer
|
||||
#[strum(to_string = "mixnet_packet_stage_egress_queue_seconds")]
|
||||
#[strum(props(
|
||||
help = "Seconds a forwarded packet waited in the per-connection egress buffer"
|
||||
))]
|
||||
EgressQueue,
|
||||
/// flushing the packet batch to the socket
|
||||
#[strum(to_string = "mixnet_packet_stage_socket_write_seconds")]
|
||||
#[strum(props(help = "Seconds spent flushing a forwarded packet batch to the socket"))]
|
||||
SocketWrite,
|
||||
/// end-to-end: receive -> socket write
|
||||
#[strum(to_string = "mixnet_packet_total_latency_seconds")]
|
||||
#[strum(props(help = "Total in-node latency of a forwarded packet, receive to socket write"))]
|
||||
Total,
|
||||
|
||||
// ----- auxiliary histograms: observed directly, not part of the latency waterfall -----
|
||||
/// number of packets the forwarder drained from the ingress channel per wakeup
|
||||
#[strum(to_string = "mixnet_packet_forwarder_drain_batch_size")]
|
||||
#[strum(props(
|
||||
help = "Number of ingress packets the forwarder drained per select! wakeup (batch size)"
|
||||
))]
|
||||
ForwarderDrainBatchSize,
|
||||
/// number of expired packets the forwarder drained from the delay queue per wakeup
|
||||
#[strum(to_string = "mixnet_packet_forwarder_delay_drain_batch_size")]
|
||||
#[strum(props(
|
||||
help = "Number of expired delay-queue packets the forwarder drained per select! wakeup (batch size)"
|
||||
))]
|
||||
ForwarderDelayDrainBatchSize,
|
||||
/// per-connection egress buffer occupancy (used/capacity) at send time
|
||||
#[strum(to_string = "mixnet_packet_egress_buffer_fill_ratio")]
|
||||
#[strum(props(
|
||||
help = "Per-connection egress buffer fill ratio (used/capacity) sampled at packet send time"
|
||||
))]
|
||||
EgressBufferFillRatio,
|
||||
}
|
||||
|
||||
impl MixnetMetric {
|
||||
/// Histogram bucket layout for this metric.
|
||||
fn buckets(&self) -> &'static [f64] {
|
||||
match self {
|
||||
MixnetMetric::ForwarderDrainBatchSize | MixnetMetric::ForwarderDelayDrainBatchSize => {
|
||||
&DRAIN_BATCH_BUCKETS
|
||||
}
|
||||
MixnetMetric::EgressBufferFillRatio => &EGRESS_FILL_BUCKETS,
|
||||
// every latency stage shares the seconds buckets
|
||||
_ => &STAGE_LATENCY_BUCKETS,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Pre-register every histogram (at zero) into the global metrics registry so the whole
|
||||
/// `mixnet_packet_*` family is present on the prometheus endpoint from boot, before anything has
|
||||
/// been observed. Idempotent.
|
||||
pub fn register_all() {
|
||||
let registry = nym_metrics::metrics_registry();
|
||||
for metric in MixnetMetric::iter() {
|
||||
registry.register_histogram(
|
||||
metric.as_ref(),
|
||||
metric.get_str("help"),
|
||||
Some(metric.buckets()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Observe a value into a metric's histogram in the process-global registry.
|
||||
fn observe(metric: MixnetMetric, value: f64) {
|
||||
nym_metrics::metrics_registry().maybe_register_and_add_to_histogram(
|
||||
metric.as_ref(),
|
||||
value,
|
||||
Some(metric.buckets()),
|
||||
metric.get_str("help"),
|
||||
);
|
||||
}
|
||||
|
||||
/// Observe how many ingress-channel packets the forwarder drained in a single wakeup.
|
||||
pub fn observe_drain_batch_size(batch_size: usize) {
|
||||
observe(MixnetMetric::ForwarderDrainBatchSize, batch_size as f64);
|
||||
}
|
||||
|
||||
/// Observe how many expired delay-queue packets the forwarder drained in a single wakeup.
|
||||
pub fn observe_delay_drain_batch_size(batch_size: usize) {
|
||||
observe(
|
||||
MixnetMetric::ForwarderDelayDrainBatchSize,
|
||||
batch_size as f64,
|
||||
);
|
||||
}
|
||||
|
||||
/// Observe how full a per-connection egress buffer was when a packet was queued for it.
|
||||
pub fn observe_egress_buffer_fill(used: usize, capacity: usize) {
|
||||
if capacity == 0 {
|
||||
return;
|
||||
}
|
||||
observe(
|
||||
MixnetMetric::EgressBufferFillRatio,
|
||||
used as f64 / capacity as f64,
|
||||
);
|
||||
}
|
||||
|
||||
/// A lightweight per-packet stopwatch for attributing forwarding latency to pipeline
|
||||
/// stages. Unsampled packets carry the `Off` variant and do zero clock reads, so the only
|
||||
/// cost on the hot path is moving a small `Copy` value and a branch.
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum PacketTrace {
|
||||
Off,
|
||||
On {
|
||||
received_at: Instant,
|
||||
stage_at: Instant,
|
||||
},
|
||||
}
|
||||
|
||||
impl PacketTrace {
|
||||
/// Begin tracing. Reads the clock only for sampled packets.
|
||||
pub fn start(sampled: bool) -> Self {
|
||||
if sampled {
|
||||
let now = Instant::now();
|
||||
PacketTrace::On {
|
||||
received_at: now,
|
||||
stage_at: now,
|
||||
}
|
||||
} else {
|
||||
PacketTrace::Off
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether this packet is being traced (sampled).
|
||||
pub fn is_sampled(&self) -> bool {
|
||||
matches!(self, PacketTrace::On { .. })
|
||||
}
|
||||
|
||||
/// Seconds spent in the stage just completed, advancing the cursor to now.
|
||||
/// Returns `None` for unsampled packets.
|
||||
fn lap(&mut self) -> Option<f64> {
|
||||
match self {
|
||||
PacketTrace::Off => None,
|
||||
PacketTrace::On { stage_at, .. } => {
|
||||
let now = Instant::now();
|
||||
let secs = now.duration_since(*stage_at).as_secs_f64();
|
||||
*stage_at = now;
|
||||
Some(secs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Seconds since tracing began (i.e. since the packet was received), or `None` if unsampled.
|
||||
fn total(&self) -> Option<f64> {
|
||||
match self {
|
||||
PacketTrace::Off => None,
|
||||
PacketTrace::On { received_at, .. } => {
|
||||
Some(Instant::now().duration_since(*received_at).as_secs_f64())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Close out the stage just completed: lap the timer and, only if the packet is sampled,
|
||||
/// observe `stage`'s latency histogram.
|
||||
pub fn record(&mut self, stage: MixnetMetric) {
|
||||
if let Some(secs) = self.lap() {
|
||||
observe(stage, secs);
|
||||
}
|
||||
}
|
||||
|
||||
/// Observe the end-to-end [`MixnetMetric::Total`] latency (since receive) if sampled. Unlike
|
||||
/// [`PacketTrace::record`] this does not lap, so it can be called at the very end.
|
||||
pub fn record_total(&self) {
|
||||
if let Some(secs) = self.total() {
|
||||
observe(MixnetMetric::Total, secs);
|
||||
}
|
||||
}
|
||||
|
||||
/// Observe an explicit `secs` value for `stage` if the packet is sampled, without lapping the
|
||||
/// stage cursor. For diagnostics that don't fit the sequential waterfall (e.g. delay-queue
|
||||
/// overrun, measured against the target deadline rather than the previous stage).
|
||||
pub fn record_value(&self, stage: MixnetMetric, secs: f64) {
|
||||
if matches!(self, PacketTrace::On { .. }) {
|
||||
observe(stage, secs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A value paired with its in-flight latency trace, so the trace rides along as the value is
|
||||
/// moved between pipeline stages (and transformed via [`Traced::map`]). Used wherever a packet
|
||||
/// crosses a queue/channel: replay batch, delay queue, egress channel.
|
||||
pub struct Traced<T> {
|
||||
pub inner: T,
|
||||
pub trace: PacketTrace,
|
||||
}
|
||||
|
||||
impl<T> Traced<T> {
|
||||
pub fn new(inner: T, trace: PacketTrace) -> Self {
|
||||
Traced { inner, trace }
|
||||
}
|
||||
|
||||
/// Transform the carried value, keeping the same trace.
|
||||
pub fn map<U>(self, f: impl FnOnce(T) -> U) -> Traced<U> {
|
||||
Traced {
|
||||
inner: f(self.inner),
|
||||
trace: self.trace,
|
||||
}
|
||||
}
|
||||
|
||||
/// Record the stage just completed for the carried trace (see [`PacketTrace::record`]).
|
||||
pub fn record(&mut self, stage: MixnetMetric) {
|
||||
self.trace.record(stage)
|
||||
}
|
||||
|
||||
/// Observe an explicit value for the carried trace (see [`PacketTrace::record_value`]).
|
||||
pub fn record_value(&self, stage: MixnetMetric, secs: f64) {
|
||||
self.trace.record_value(stage, secs)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
// guards that AsRefStr honours `#[strum(to_string = ...)]` (rather than falling back to the
|
||||
// variant name), that every metric is in the `mixnet_packet_*` family, and carries a help
|
||||
// string, and that each metric resolves to a bucket layout.
|
||||
#[test]
|
||||
fn every_metric_has_a_mixnet_packet_name_help_and_buckets() {
|
||||
for metric in MixnetMetric::iter() {
|
||||
assert!(
|
||||
metric.as_ref().starts_with("mixnet_packet_"),
|
||||
"unexpected metric name: {}",
|
||||
metric.as_ref()
|
||||
);
|
||||
assert!(
|
||||
metric.get_str("help").is_some(),
|
||||
"missing help for {}",
|
||||
metric.as_ref()
|
||||
);
|
||||
assert!(
|
||||
!metric.buckets().is_empty(),
|
||||
"missing buckets for {}",
|
||||
metric.as_ref()
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
MixnetMetric::Unwrap.as_ref(),
|
||||
"mixnet_packet_stage_unwrap_seconds"
|
||||
);
|
||||
assert_eq!(
|
||||
MixnetMetric::Total.as_ref(),
|
||||
"mixnet_packet_total_latency_seconds"
|
||||
);
|
||||
assert_eq!(
|
||||
MixnetMetric::ForwarderDrainBatchSize.as_ref(),
|
||||
"mixnet_packet_forwarder_drain_batch_size"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,7 @@ nym-ecash-contract-common = { workspace = true }
|
||||
nym-multisig-contract-common = { workspace = true }
|
||||
nym-group-contract-common = { workspace = true }
|
||||
nym-performance-contract-common = { workspace = true }
|
||||
nym-network-monitors-contract-common = { workspace = true }
|
||||
nym-node-families-contract-common = { workspace = true }
|
||||
nym-serde-helpers = { workspace = true, features = ["hex", "base64"] }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
|
||||
@@ -104,6 +104,14 @@ impl TryFrom<NymNetworkDetails> for Config {
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn new(nyxd_url: Url, api_url: Url, nyxd_config: nyxd::Config) -> Self {
|
||||
Config {
|
||||
api_url,
|
||||
nyxd_url,
|
||||
nyxd_config,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_from_nym_network_details(
|
||||
details: &NymNetworkDetails,
|
||||
) -> Result<Self, ValidatorClientError> {
|
||||
@@ -114,6 +122,15 @@ impl Config {
|
||||
.map(|url| Url::parse(url))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
if let Some(nym_api_urls) = details.nym_api_urls.as_ref() {
|
||||
api_url.extend(
|
||||
nym_api_urls
|
||||
.iter()
|
||||
.map(|url| url.url.parse())
|
||||
.collect::<Result<Vec<_>, _>>()?,
|
||||
);
|
||||
}
|
||||
|
||||
if api_url.is_empty() {
|
||||
return Err(ValidatorClientError::NoAPIUrlAvailable);
|
||||
}
|
||||
|
||||
@@ -15,12 +15,16 @@ use nym_api_requests::ecash::models::{
|
||||
VerifyEcashTicketBody,
|
||||
};
|
||||
use nym_api_requests::ecash::VerificationKeyResponse;
|
||||
use nym_api_requests::models::network_monitor::{
|
||||
KnownNetworkMonitorResponse, StressTestBatchSubmission,
|
||||
};
|
||||
use nym_api_requests::models::node_families::NodeFamily;
|
||||
use nym_api_requests::models::{
|
||||
AnnotationResponse, ApiHealthResponse, BinaryBuildInformationOwned, ChainBlocksStatusResponse,
|
||||
ChainStatusResponse, KeyRotationInfoResponse, NodePerformanceResponse, NodeRefreshBody,
|
||||
NymNodeDescriptionV1, NymNodeDescriptionV2, PerformanceHistoryResponse, RewardedSetResponse,
|
||||
SignerInformationResponse,
|
||||
AnnotationResponseV1, AnnotationResponseV2, ApiHealthResponse, BinaryBuildInformationOwned,
|
||||
ChainBlocksStatusResponse, ChainStatusResponse, KeyRotationInfoResponse,
|
||||
NodePerformanceResponse, NodeRefreshBody, NymNodeDescriptionV1, NymNodeDescriptionV2,
|
||||
PerformanceHistoryResponse, RewardedSetResponse, SignerInformationResponse,
|
||||
StressTestBatchSubmissionResponse,
|
||||
};
|
||||
use nym_api_requests::pagination::PaginatedResponse;
|
||||
use nym_http_api_client::{ApiClient, NO_PARAMS};
|
||||
@@ -1016,7 +1020,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
async fn get_node_annotation(
|
||||
&self,
|
||||
node_id: NodeId,
|
||||
) -> Result<AnnotationResponse, NymAPIError> {
|
||||
) -> Result<AnnotationResponseV1, NymAPIError> {
|
||||
self.get_json(
|
||||
&[
|
||||
routes::V1_API_VERSION,
|
||||
@@ -1029,6 +1033,22 @@ pub trait NymApiClientExt: ApiClient {
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_node_annotation_v2(
|
||||
&self,
|
||||
node_id: NodeId,
|
||||
) -> Result<AnnotationResponseV2, NymAPIError> {
|
||||
self.get_json(
|
||||
&[
|
||||
routes::V2_API_VERSION,
|
||||
routes::NYM_NODES_ROUTES,
|
||||
routes::NYM_NODES_ANNOTATION,
|
||||
&node_id.to_string(),
|
||||
],
|
||||
NO_PARAMS,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[deprecated]
|
||||
async fn get_mixnode_avg_uptime(&self, mix_id: NodeId) -> Result<UptimeResponse, NymAPIError> {
|
||||
self.get_json(
|
||||
@@ -1399,6 +1419,53 @@ pub trait NymApiClientExt: ApiClient {
|
||||
|
||||
Ok(SemiSkimmedNodesWithMetadata::new(nodes, metadata))
|
||||
}
|
||||
|
||||
/// Queries the nym-api for whether a particular ed25519 identity key is currently recognised
|
||||
/// as an authorised network monitor permitted to submit stress testing results.
|
||||
///
|
||||
/// `identity_key` is expected to be the base58-encoded form of the ed25519 public key.
|
||||
#[instrument(level = "debug", skip(self))]
|
||||
async fn get_known_network_monitor(
|
||||
&self,
|
||||
identity_key: IdentityKeyRef<'_>,
|
||||
) -> Result<KnownNetworkMonitorResponse, NymAPIError> {
|
||||
self.get_json(
|
||||
&[
|
||||
routes::V3_API_VERSION,
|
||||
routes::NYM_NODES_ROUTES,
|
||||
routes::STRESS_TESTING,
|
||||
routes::STRESS_TESTING_KNOWN_MONITORS,
|
||||
identity_key,
|
||||
],
|
||||
NO_PARAMS,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Submit a signed batch of stress-testing results to nym-api on behalf of a network monitor
|
||||
/// orchestrator.
|
||||
///
|
||||
/// The caller is expected to have produced `request` via
|
||||
/// `StressTestBatchSubmissionContent::new(...)` and signed it with the orchestrator's ed25519
|
||||
/// key; nym-api will reject submissions that are stale, replayed, unauthorised, or whose
|
||||
/// signature fails to verify.
|
||||
#[instrument(level = "debug", skip(self, request))]
|
||||
async fn submit_stress_testing_results(
|
||||
&self,
|
||||
request: &StressTestBatchSubmission,
|
||||
) -> Result<StressTestBatchSubmissionResponse, NymAPIError> {
|
||||
self.post_json(
|
||||
&[
|
||||
routes::V3_API_VERSION,
|
||||
routes::NYM_NODES_ROUTES,
|
||||
routes::STRESS_TESTING,
|
||||
routes::STRESS_TESTING_BATCH_SUBMIT,
|
||||
],
|
||||
NO_PARAMS,
|
||||
request,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
// Client is already nym_http_api_client::Client (re-exported above), so just one impl needed
|
||||
|
||||
@@ -50,6 +50,9 @@ pub mod nym_nodes {
|
||||
pub const NYM_NODES_REWARDED_SET: &str = "rewarded-set";
|
||||
pub const NYM_NODES_REFRESH_DESCRIBED: &str = "refresh-described";
|
||||
pub const BY_ADDRESSES: &str = "by-addresses";
|
||||
pub const STRESS_TESTING: &str = "stress-testing";
|
||||
pub const STRESS_TESTING_KNOWN_MONITORS: &str = "known-monitors";
|
||||
pub const STRESS_TESTING_BATCH_SUBMIT: &str = "batch-submit";
|
||||
}
|
||||
|
||||
pub const STATUS_ROUTES: &str = "status";
|
||||
|
||||
@@ -13,6 +13,7 @@ pub mod ecash_query_client;
|
||||
pub mod group_query_client;
|
||||
pub mod mixnet_query_client;
|
||||
pub mod multisig_query_client;
|
||||
pub mod network_monitors_query_client;
|
||||
pub mod node_families_query_client;
|
||||
pub mod performance_query_client;
|
||||
pub mod vesting_query_client;
|
||||
@@ -23,6 +24,7 @@ pub mod ecash_signing_client;
|
||||
pub mod group_signing_client;
|
||||
pub mod mixnet_signing_client;
|
||||
pub mod multisig_signing_client;
|
||||
pub mod network_monitors_signing_client;
|
||||
pub mod node_families_signing_client;
|
||||
pub mod performance_signing_client;
|
||||
pub mod vesting_signing_client;
|
||||
@@ -33,6 +35,9 @@ pub use ecash_query_client::{EcashQueryClient, PagedEcashQueryClient};
|
||||
pub use group_query_client::{GroupQueryClient, PagedGroupQueryClient};
|
||||
pub use mixnet_query_client::{MixnetQueryClient, PagedMixnetQueryClient};
|
||||
pub use multisig_query_client::{MultisigQueryClient, PagedMultisigQueryClient};
|
||||
pub use network_monitors_query_client::{
|
||||
NetworkMonitorsQueryClient, PagedNetworkMonitorsQueryClient,
|
||||
};
|
||||
pub use node_families_query_client::{NodeFamiliesQueryClient, PagedNodeFamiliesQueryClient};
|
||||
pub use performance_query_client::{PagedPerformanceQueryClient, PerformanceQueryClient};
|
||||
pub use vesting_query_client::{PagedVestingQueryClient, VestingQueryClient};
|
||||
@@ -43,6 +48,7 @@ pub use ecash_signing_client::EcashSigningClient;
|
||||
pub use group_signing_client::GroupSigningClient;
|
||||
pub use mixnet_signing_client::MixnetSigningClient;
|
||||
pub use multisig_signing_client::MultisigSigningClient;
|
||||
pub use network_monitors_signing_client::NetworkMonitorsSigningClient;
|
||||
pub use node_families_signing_client::NodeFamiliesSigningClient;
|
||||
pub use performance_signing_client::PerformanceSigningClient;
|
||||
pub use vesting_signing_client::VestingSigningClient;
|
||||
@@ -53,6 +59,7 @@ pub trait NymContractsProvider {
|
||||
fn mixnet_contract_address(&self) -> Option<&AccountId>;
|
||||
fn vesting_contract_address(&self) -> Option<&AccountId>;
|
||||
fn performance_contract_address(&self) -> Option<&AccountId>;
|
||||
fn network_monitors_contract_address(&self) -> Option<&AccountId>;
|
||||
fn node_families_contract_address(&self) -> Option<&AccountId>;
|
||||
|
||||
// coconut-related
|
||||
@@ -67,6 +74,7 @@ pub struct TypedNymContracts {
|
||||
pub mixnet_contract_address: Option<AccountId>,
|
||||
pub vesting_contract_address: Option<AccountId>,
|
||||
pub performance_contract_address: Option<AccountId>,
|
||||
pub network_monitors_contract_address: Option<AccountId>,
|
||||
pub node_families_contract_address: Option<AccountId>,
|
||||
|
||||
pub ecash_contract_address: Option<AccountId>,
|
||||
@@ -92,6 +100,10 @@ impl TryFrom<NymContracts> for TypedNymContracts {
|
||||
.performance_contract_address
|
||||
.map(|addr| addr.parse())
|
||||
.transpose()?,
|
||||
network_monitors_contract_address: value
|
||||
.network_monitors_contract_address
|
||||
.map(|addr| addr.parse())
|
||||
.transpose()?,
|
||||
node_families_contract_address: value
|
||||
.node_families_contract_address
|
||||
.map(|addr| addr.parse())
|
||||
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::collect_paged;
|
||||
use crate::nyxd::contract_traits::NymContractsProvider;
|
||||
use crate::nyxd::error::NyxdError;
|
||||
use crate::nyxd::CosmWasmClient;
|
||||
use async_trait::async_trait;
|
||||
use nym_network_monitors_contract_common::{
|
||||
AuthorisedNetworkMonitor, AuthorisedNetworkMonitorOrchestratorsResponse,
|
||||
AuthorisedNetworkMonitorsPagedResponse, QueryMsg as NetworkMonitorsQueryMsg,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use std::net::SocketAddr;
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
pub trait NetworkMonitorsQueryClient {
|
||||
async fn query_network_monitors_contract<T>(
|
||||
&self,
|
||||
query: NetworkMonitorsQueryMsg,
|
||||
) -> Result<T, NyxdError>
|
||||
where
|
||||
for<'a> T: Deserialize<'a>;
|
||||
|
||||
async fn get_admin(&self) -> Result<cw_controllers::AdminResponse, NyxdError> {
|
||||
self.query_network_monitors_contract(NetworkMonitorsQueryMsg::Admin {})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_network_monitor_orchestrators(
|
||||
&self,
|
||||
) -> Result<AuthorisedNetworkMonitorOrchestratorsResponse, NyxdError> {
|
||||
self.query_network_monitors_contract(
|
||||
NetworkMonitorsQueryMsg::NetworkMonitorOrchestrators {},
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_network_monitor_agents_paged(
|
||||
&self,
|
||||
start_next_after: Option<SocketAddr>,
|
||||
limit: Option<u32>,
|
||||
) -> Result<AuthorisedNetworkMonitorsPagedResponse, NyxdError> {
|
||||
self.query_network_monitors_contract(NetworkMonitorsQueryMsg::NetworkMonitorAgents {
|
||||
start_next_after,
|
||||
limit,
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
pub trait PagedNetworkMonitorsQueryClient: NetworkMonitorsQueryClient {
|
||||
async fn get_all_network_monitor_agents(
|
||||
&self,
|
||||
) -> Result<Vec<AuthorisedNetworkMonitor>, NyxdError> {
|
||||
collect_paged!(self, get_network_monitor_agents_paged, authorised)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<T> PagedNetworkMonitorsQueryClient for T where T: NetworkMonitorsQueryClient {}
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
impl<C> NetworkMonitorsQueryClient for C
|
||||
where
|
||||
C: CosmWasmClient + NymContractsProvider + Send + Sync,
|
||||
{
|
||||
async fn query_network_monitors_contract<T>(
|
||||
&self,
|
||||
query: NetworkMonitorsQueryMsg,
|
||||
) -> Result<T, NyxdError>
|
||||
where
|
||||
for<'a> T: Deserialize<'a>,
|
||||
{
|
||||
let contract_address = &self
|
||||
.network_monitors_contract_address()
|
||||
.ok_or_else(|| NyxdError::unavailable_contract_address("network monitors contract"))?;
|
||||
self.query_contract_smart(contract_address, &query).await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::nyxd::contract_traits::tests::IgnoreValue;
|
||||
|
||||
// it's enough that this compiles and clippy is happy about it
|
||||
#[allow(dead_code)]
|
||||
fn all_query_variants_are_covered<C: NetworkMonitorsQueryClient + Send + Sync>(
|
||||
client: C,
|
||||
msg: NetworkMonitorsQueryMsg,
|
||||
) {
|
||||
match msg {
|
||||
NetworkMonitorsQueryMsg::Admin {} => client.get_admin().ignore(),
|
||||
NetworkMonitorsQueryMsg::NetworkMonitorOrchestrators {} => {
|
||||
client.get_network_monitor_orchestrators().ignore()
|
||||
}
|
||||
NetworkMonitorsQueryMsg::NetworkMonitorAgents { .. } => {
|
||||
client.get_network_monitor_agents_paged(None, None).ignore()
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
+205
@@ -0,0 +1,205 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::nyxd::contract_traits::NymContractsProvider;
|
||||
use crate::nyxd::cosmwasm_client::types::ExecuteResult;
|
||||
use crate::nyxd::error::NyxdError;
|
||||
use crate::nyxd::{Coin, Fee, SigningCosmWasmClient};
|
||||
use crate::signing::signer::OfflineSigner;
|
||||
use async_trait::async_trait;
|
||||
use nym_network_monitors_contract_common::ExecuteMsg as NetworkMonitorsExecuteMsg;
|
||||
use std::net::SocketAddr;
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
pub trait NetworkMonitorsSigningClient {
|
||||
async fn execute_network_monitors_contract(
|
||||
&self,
|
||||
fee: Option<Fee>,
|
||||
msg: NetworkMonitorsExecuteMsg,
|
||||
memo: String,
|
||||
funds: Vec<Coin>,
|
||||
) -> Result<ExecuteResult, NyxdError>;
|
||||
|
||||
async fn update_admin(
|
||||
&self,
|
||||
admin: String,
|
||||
fee: Option<Fee>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
let msg = NetworkMonitorsExecuteMsg::UpdateAdmin { admin };
|
||||
self.execute_network_monitors_contract(
|
||||
fee,
|
||||
msg,
|
||||
"NetworkMonitorsExecuteMsg::UpdateAdmin".into(),
|
||||
vec![],
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn authorise_network_monitor_orchestrator(
|
||||
&self,
|
||||
address: String,
|
||||
fee: Option<Fee>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
let msg = NetworkMonitorsExecuteMsg::AuthoriseNetworkMonitorOrchestrator { address };
|
||||
self.execute_network_monitors_contract(
|
||||
fee,
|
||||
msg,
|
||||
"NetworkMonitorsExecuteMsg::AuthoriseNetworkMonitorOrchestrator".into(),
|
||||
vec![],
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Announce (or rotate) the ed25519 identity key of the calling network monitor orchestrator.
|
||||
///
|
||||
/// The caller must already be an authorised orchestrator; the contract validates that
|
||||
/// `identity_key` is a well-formed base-58 encoding of a 32-byte ed25519 public key.
|
||||
async fn update_orchestrator_identity_key(
|
||||
&self,
|
||||
identity_key: String,
|
||||
fee: Option<Fee>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
let msg = NetworkMonitorsExecuteMsg::UpdateOrchestratorIdentityKey { key: identity_key };
|
||||
self.execute_network_monitors_contract(
|
||||
fee,
|
||||
msg,
|
||||
"NetworkMonitorsExecuteMsg::UpdateOrchestratorIdentityKey".into(),
|
||||
vec![],
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn revoke_network_monitor_orchestrator(
|
||||
&self,
|
||||
address: String,
|
||||
fee: Option<Fee>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
let msg = NetworkMonitorsExecuteMsg::RevokeNetworkMonitorOrchestrator { address };
|
||||
self.execute_network_monitors_contract(
|
||||
fee,
|
||||
msg,
|
||||
"NetworkMonitorsExecuteMsg::RevokeNetworkMonitorOrchestrator".into(),
|
||||
vec![],
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn authorise_network_monitor(
|
||||
&self,
|
||||
mixnet_address: SocketAddr,
|
||||
bs58_x25519_noise: String,
|
||||
noise_version: u8,
|
||||
fee: Option<Fee>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
let msg = NetworkMonitorsExecuteMsg::AuthoriseNetworkMonitor {
|
||||
mixnet_address,
|
||||
bs58_x25519_noise,
|
||||
noise_version,
|
||||
};
|
||||
self.execute_network_monitors_contract(
|
||||
fee,
|
||||
msg,
|
||||
"NetworkMonitorsExecuteMsg::AuthoriseNetworkMonitor".into(),
|
||||
vec![],
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn revoke_network_monitor(
|
||||
&self,
|
||||
address: SocketAddr,
|
||||
fee: Option<Fee>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
let msg = NetworkMonitorsExecuteMsg::RevokeNetworkMonitor { address };
|
||||
self.execute_network_monitors_contract(
|
||||
fee,
|
||||
msg,
|
||||
"NetworkMonitorsExecuteMsg::RevokeNetworkMonitor".into(),
|
||||
vec![],
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn revoke_all_network_monitors(
|
||||
&self,
|
||||
fee: Option<Fee>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
let msg = NetworkMonitorsExecuteMsg::RevokeAllNetworkMonitors;
|
||||
self.execute_network_monitors_contract(
|
||||
fee,
|
||||
msg,
|
||||
"NetworkMonitorsExecuteMsg::RevokeAllNetworkMonitors".into(),
|
||||
vec![],
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
impl<C> NetworkMonitorsSigningClient for C
|
||||
where
|
||||
C: SigningCosmWasmClient + NymContractsProvider + Sync,
|
||||
NyxdError: From<<Self as OfflineSigner>::Error>,
|
||||
{
|
||||
async fn execute_network_monitors_contract(
|
||||
&self,
|
||||
fee: Option<Fee>,
|
||||
msg: NetworkMonitorsExecuteMsg,
|
||||
memo: String,
|
||||
funds: Vec<Coin>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
let contract_address = &self
|
||||
.network_monitors_contract_address()
|
||||
.ok_or_else(|| NyxdError::unavailable_contract_address("network monitors contract"))?;
|
||||
|
||||
let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier())));
|
||||
|
||||
let signer_address = &self.signer_addresses()[0];
|
||||
self.execute(signer_address, contract_address, &msg, fee, memo, funds)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::nyxd::contract_traits::tests::IgnoreValue;
|
||||
use nym_network_monitors_contract_common::ExecuteMsg;
|
||||
|
||||
// it's enough that this compiles and clippy is happy about it
|
||||
#[allow(dead_code)]
|
||||
fn all_execute_variants_are_covered<C: NetworkMonitorsSigningClient + Send + Sync>(
|
||||
client: C,
|
||||
msg: NetworkMonitorsExecuteMsg,
|
||||
) {
|
||||
match msg {
|
||||
NetworkMonitorsExecuteMsg::UpdateAdmin { admin } => {
|
||||
client.update_admin(admin, None).ignore()
|
||||
}
|
||||
ExecuteMsg::AuthoriseNetworkMonitorOrchestrator { address } => client
|
||||
.authorise_network_monitor_orchestrator(address, None)
|
||||
.ignore(),
|
||||
ExecuteMsg::UpdateOrchestratorIdentityKey { key } => {
|
||||
client.update_orchestrator_identity_key(key, None).ignore()
|
||||
}
|
||||
ExecuteMsg::RevokeNetworkMonitorOrchestrator { address } => client
|
||||
.revoke_network_monitor_orchestrator(address, None)
|
||||
.ignore(),
|
||||
ExecuteMsg::AuthoriseNetworkMonitor {
|
||||
mixnet_address: address,
|
||||
bs58_x25519_noise,
|
||||
noise_version,
|
||||
} => client
|
||||
.authorise_network_monitor(address, bs58_x25519_noise, noise_version, None)
|
||||
.ignore(),
|
||||
ExecuteMsg::RevokeNetworkMonitor { address } => {
|
||||
client.revoke_network_monitor(address, None).ignore()
|
||||
}
|
||||
ExecuteMsg::RevokeAllNetworkMonitors => {
|
||||
client.revoke_all_network_monitors(None).ignore()
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
+8
-2
@@ -7,12 +7,12 @@ use crate::nyxd::error::NyxdError;
|
||||
use crate::nyxd::CosmWasmClient;
|
||||
use async_trait::async_trait;
|
||||
use cosmrs::AccountId;
|
||||
use nym_mixnet_contract_common::NodeId;
|
||||
use serde::Deserialize;
|
||||
|
||||
use nym_mixnet_contract_common::NodeId;
|
||||
pub use nym_node_families_contract_common::{
|
||||
msg::QueryMsg as NodeFamiliesQueryMsg, AllFamilyMembersPagedResponse,
|
||||
AllPastFamilyInvitationsPagedResponse, FamiliesPagedResponse, FamilyMemberRecord,
|
||||
AllPastFamilyInvitationsPagedResponse, Config, FamiliesPagedResponse, FamilyMemberRecord,
|
||||
FamilyMembersPagedResponse, GlobalPastFamilyInvitationCursor, NodeFamily,
|
||||
NodeFamilyByNameResponse, NodeFamilyByOwnerResponse, NodeFamilyId,
|
||||
NodeFamilyMembershipResponse, NodeFamilyResponse, PastFamilyInvitation,
|
||||
@@ -35,6 +35,11 @@ pub trait NodeFamiliesQueryClient {
|
||||
where
|
||||
for<'a> T: Deserialize<'a>;
|
||||
|
||||
async fn get_config(&self) -> Result<Config, NyxdError> {
|
||||
self.query_node_families_contract(NodeFamiliesQueryMsg::GetConfig {})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_family_by_id(
|
||||
&self,
|
||||
family_id: NodeFamilyId,
|
||||
@@ -360,6 +365,7 @@ mod tests {
|
||||
msg: NodeFamiliesQueryMsg,
|
||||
) {
|
||||
match msg {
|
||||
NodeFamiliesQueryMsg::GetConfig {} => client.get_config().ignore(),
|
||||
NodeFamiliesQueryMsg::GetFamilyById { family_id } => {
|
||||
client.get_family_by_id(family_id).ignore()
|
||||
}
|
||||
|
||||
+27
@@ -54,6 +54,27 @@ pub trait NodeFamiliesSigningClient {
|
||||
.await
|
||||
}
|
||||
|
||||
/// Update the name and/or description of the caller's family. Each
|
||||
/// argument follows `None = keep` / `Some(_) = replace` semantics; a
|
||||
/// call with both `None` is a server-side no-op.
|
||||
async fn update_family(
|
||||
&self,
|
||||
updated_name: Option<String>,
|
||||
updated_description: Option<String>,
|
||||
fee: Option<Fee>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
self.execute_node_families_contract(
|
||||
fee,
|
||||
NodeFamiliesExecuteMsg::UpdateFamily {
|
||||
updated_name,
|
||||
updated_description,
|
||||
},
|
||||
"NodeFamiliesContract::UpdateFamily".to_string(),
|
||||
vec![],
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn disband_family(&self, fee: Option<Fee>) -> Result<ExecuteResult, NyxdError> {
|
||||
self.execute_node_families_contract(
|
||||
fee,
|
||||
@@ -224,6 +245,12 @@ mod tests {
|
||||
NodeFamiliesExecuteMsg::CreateFamily { name, description } => client
|
||||
.create_family(name, description, None, vec![])
|
||||
.ignore(),
|
||||
NodeFamiliesExecuteMsg::UpdateFamily {
|
||||
updated_name,
|
||||
updated_description,
|
||||
} => client
|
||||
.update_family(updated_name, updated_description, None)
|
||||
.ignore(),
|
||||
NodeFamiliesExecuteMsg::DisbandFamily {} => client.disband_family(None).ignore(),
|
||||
NodeFamiliesExecuteMsg::InviteToFamily {
|
||||
node_id,
|
||||
|
||||
@@ -36,7 +36,7 @@ pub mod logs;
|
||||
pub mod module_traits;
|
||||
pub mod types;
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct SigningClientOptions {
|
||||
gas_price: GasPrice,
|
||||
simulated_gas_multiplier: f32,
|
||||
@@ -80,6 +80,17 @@ impl<C, S> MaybeSigningClient<C, S> {
|
||||
opts,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn clone_query_client(&self) -> MaybeSigningClient<C, NoSigner>
|
||||
where
|
||||
C: Clone,
|
||||
{
|
||||
MaybeSigningClient {
|
||||
client: self.client.clone(),
|
||||
signer: Default::default(),
|
||||
opts: self.opts.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "http-client")]
|
||||
|
||||
@@ -24,6 +24,8 @@ use async_trait::async_trait;
|
||||
use cosmrs::tendermint::{abci, evidence::Evidence, Genesis};
|
||||
use cosmrs::tx::{Raw, SignDoc};
|
||||
use cosmwasm_std::Addr;
|
||||
use nym_contracts_common::build_information::CONTRACT_BUILD_INFO_STORAGE_KEY;
|
||||
use nym_contracts_common::ContractBuildInformation;
|
||||
use nym_network_defaults::{ChainDetails, NymNetworkDetails};
|
||||
use serde::{de::DeserializeOwned, Serialize};
|
||||
use std::fmt::Debug;
|
||||
@@ -40,6 +42,7 @@ pub use crate::nyxd::{
|
||||
fee::Fee,
|
||||
};
|
||||
pub use crate::rpc::TendermintRpcClient;
|
||||
pub use bip39;
|
||||
pub use coin::Coin;
|
||||
pub use cosmrs::{
|
||||
bank::MsgSend,
|
||||
@@ -70,14 +73,19 @@ pub use tendermint_rpc::{
|
||||
Paging, Request, Response, SimpleRequest,
|
||||
};
|
||||
|
||||
pub use nym_ecash_contract_common;
|
||||
pub use nym_mixnet_contract_common;
|
||||
pub use nym_multisig_contract_common;
|
||||
pub use nym_network_monitors_contract_common;
|
||||
pub use nym_performance_contract_common;
|
||||
pub use nym_vesting_contract_common;
|
||||
|
||||
#[cfg(feature = "http-client")]
|
||||
use crate::http_client;
|
||||
#[cfg(feature = "http-client")]
|
||||
use crate::{DirectSigningHttpRpcNyxdClient, QueryHttpRpcNyxdClient};
|
||||
#[cfg(feature = "http-client")]
|
||||
use cosmrs::rpc::{HttpClient, HttpClientUrl};
|
||||
use nym_contracts_common::build_information::CONTRACT_BUILD_INFO_STORAGE_KEY;
|
||||
use nym_contracts_common::ContractBuildInformation;
|
||||
|
||||
pub mod coin;
|
||||
pub mod contract_traits;
|
||||
@@ -262,6 +270,16 @@ impl<C, S> NyxdClient<C, S> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clone_query_client(&self) -> NyxdClient<C>
|
||||
where
|
||||
C: Clone,
|
||||
{
|
||||
NyxdClient {
|
||||
client: self.client.clone_query_client(),
|
||||
config: self.config.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn current_config(&self) -> &Config {
|
||||
&self.config
|
||||
}
|
||||
@@ -293,6 +311,10 @@ impl<C, S> NyxdClient<C, S> {
|
||||
pub fn set_simulated_gas_multiplier(&mut self, multiplier: f32) {
|
||||
self.config.simulated_gas_multiplier = multiplier;
|
||||
}
|
||||
|
||||
pub fn get_nym_contracts(&self) -> TypedNymContracts {
|
||||
self.config.contracts.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl<C, S> NymContractsProvider for NyxdClient<C, S> {
|
||||
@@ -307,6 +329,12 @@ impl<C, S> NymContractsProvider for NyxdClient<C, S> {
|
||||
fn performance_contract_address(&self) -> Option<&AccountId> {
|
||||
self.config.contracts.performance_contract_address.as_ref()
|
||||
}
|
||||
fn network_monitors_contract_address(&self) -> Option<&AccountId> {
|
||||
self.config
|
||||
.contracts
|
||||
.network_monitors_contract_address
|
||||
.as_ref()
|
||||
}
|
||||
|
||||
fn node_families_contract_address(&self) -> Option<&AccountId> {
|
||||
self.config
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
[package]
|
||||
name = "nym-network-monitors-contract-common"
|
||||
description = "Common library for the Nym Network Monitors contract"
|
||||
authors.workspace = true
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
documentation.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
readme.workspace = true
|
||||
version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
thiserror = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
schemars = { workspace = true }
|
||||
|
||||
cosmwasm-std = { workspace = true }
|
||||
cosmwasm-schema = { workspace = true }
|
||||
cw-controllers = { workspace = true }
|
||||
|
||||
[features]
|
||||
schema = []
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -0,0 +1,8 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
pub mod storage_keys {
|
||||
pub const CONTRACT_ADMIN: &str = "contract-admin";
|
||||
pub const AUTHORISED_ORCHESTRATORS: &str = "authorised-orchestrators";
|
||||
pub const AUTHORISED_NETWORK_MONITORS: &str = "authorised-network-monitors";
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use cosmwasm_std::Addr;
|
||||
use cw_controllers::AdminError;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug, PartialEq)]
|
||||
pub enum NetworkMonitorsContractError {
|
||||
#[error("could not perform contract migration: {comment}")]
|
||||
FailedMigration { comment: String },
|
||||
|
||||
#[error(transparent)]
|
||||
Admin(#[from] AdminError),
|
||||
|
||||
#[error("unauthorised")]
|
||||
Unauthorized,
|
||||
|
||||
#[error("address {addr} is not an authorised orchestrator")]
|
||||
NotAnOrchestrator { addr: Addr },
|
||||
|
||||
#[error("Failed to recover x25519 public key from its base58 representation: {0}")]
|
||||
MalformedX25519AgentNoiseKey(String),
|
||||
|
||||
#[error("Failed to recover ed25519 public key from its base58 representation: {0}")]
|
||||
MalformedEd25519OrchestratorIdentityKey(String),
|
||||
|
||||
#[error(transparent)]
|
||||
StdErr(#[from] cosmwasm_std::StdError),
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
pub mod constants;
|
||||
pub mod error;
|
||||
pub mod msg;
|
||||
pub mod types;
|
||||
|
||||
pub use error::*;
|
||||
pub use msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg};
|
||||
pub use types::*;
|
||||
@@ -0,0 +1,78 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use cosmwasm_schema::cw_serde;
|
||||
use std::net::SocketAddr;
|
||||
|
||||
#[cfg(feature = "schema")]
|
||||
use crate::{
|
||||
AuthorisedNetworkMonitorOrchestratorsResponse, AuthorisedNetworkMonitorsPagedResponse,
|
||||
};
|
||||
|
||||
#[cw_serde]
|
||||
pub struct InstantiateMsg {
|
||||
/// Address of the initial network monitor orchestrator.
|
||||
pub orchestrator_address: String,
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
pub enum ExecuteMsg {
|
||||
/// Change the admin
|
||||
UpdateAdmin { admin: String },
|
||||
|
||||
/// Authorise new network monitor orchestrator
|
||||
AuthoriseNetworkMonitorOrchestrator { address: String },
|
||||
|
||||
/// Attempt to update the announced identity key of this orchestrator
|
||||
UpdateOrchestratorIdentityKey { key: String },
|
||||
|
||||
/// Revoke network monitor orchestrator authorisation.
|
||||
RevokeNetworkMonitorOrchestrator { address: String },
|
||||
|
||||
/// Authorise new network monitor (or renew authorisation)
|
||||
/// granting additional privileges when sending mixnet packets to Nym nodes.
|
||||
AuthoriseNetworkMonitor {
|
||||
/// Mixnet address of the agent.
|
||||
/// The underlying ip address is going to be used as ingress to the nodes,
|
||||
/// and the full socket address announces the egress and the association with the noise key
|
||||
mixnet_address: SocketAddr,
|
||||
|
||||
/// Base-58 encoded noise key of the agent.
|
||||
bs58_x25519_noise: String,
|
||||
|
||||
/// Version of the noise protocol used by the agent.
|
||||
noise_version: u8,
|
||||
},
|
||||
|
||||
/// Revoke network monitor authorisation.
|
||||
RevokeNetworkMonitor { address: SocketAddr },
|
||||
|
||||
/// Revoke all network monitor authorisations.
|
||||
RevokeAllNetworkMonitors,
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
#[cfg_attr(feature = "schema", derive(cosmwasm_schema::QueryResponses))]
|
||||
pub enum QueryMsg {
|
||||
#[cfg_attr(feature = "schema", returns(cw_controllers::AdminResponse))]
|
||||
Admin {},
|
||||
|
||||
// no need for pagination as we don't expect even a double digit of those
|
||||
#[cfg_attr(
|
||||
feature = "schema",
|
||||
returns(AuthorisedNetworkMonitorOrchestratorsResponse)
|
||||
)]
|
||||
NetworkMonitorOrchestrators {},
|
||||
|
||||
#[cfg_attr(feature = "schema", returns(AuthorisedNetworkMonitorsPagedResponse))]
|
||||
NetworkMonitorAgents {
|
||||
/// Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response.
|
||||
start_next_after: Option<SocketAddr>,
|
||||
|
||||
/// Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default.
|
||||
limit: Option<u32>,
|
||||
},
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
pub struct MigrateMsg {}
|
||||
@@ -0,0 +1,53 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use cosmwasm_schema::cw_serde;
|
||||
use cosmwasm_std::{Addr, Timestamp};
|
||||
use std::net::SocketAddr;
|
||||
|
||||
pub type OrchestratorAddress = Addr;
|
||||
|
||||
#[cw_serde]
|
||||
pub struct AuthorisedNetworkMonitorOrchestrator {
|
||||
/// The address associated with the network monitor orchestrator.
|
||||
pub address: Addr,
|
||||
|
||||
/// Base-58 encoded identity key of the orchestrator, announced by the orchestrator itself
|
||||
/// on startup.
|
||||
pub identity_key: Option<String>,
|
||||
|
||||
/// Timestamp of when the network monitor was authorised.
|
||||
pub authorised_at: Timestamp,
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
pub struct AuthorisedNetworkMonitor {
|
||||
/// Mixnet address of the agent.
|
||||
/// The underlying ip address is going to be used as ingress to the nodes,
|
||||
/// and the full socket address announces the egress and the association with the noise key
|
||||
pub mixnet_address: SocketAddr,
|
||||
|
||||
/// The address of the orchestrator that authorised the network monitor agent.
|
||||
pub authorised_by: OrchestratorAddress,
|
||||
|
||||
/// Timestamp of when the network monitor was authorised.
|
||||
pub authorised_at: Timestamp,
|
||||
|
||||
/// Base-58 encoded noise key of the agent.
|
||||
pub bs58_x25519_noise: String,
|
||||
|
||||
/// Version of the noise protocol used by the agent.
|
||||
pub noise_version: u8,
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
pub struct AuthorisedNetworkMonitorOrchestratorsResponse {
|
||||
pub authorised: Vec<AuthorisedNetworkMonitorOrchestrator>,
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
pub struct AuthorisedNetworkMonitorsPagedResponse {
|
||||
pub authorised: Vec<AuthorisedNetworkMonitor>,
|
||||
|
||||
pub start_next_after: Option<SocketAddr>,
|
||||
}
|
||||
@@ -101,4 +101,14 @@ pub mod events {
|
||||
|
||||
pub const NODE_UNBOND_CLEANUP_EVENT_NAME: &str = "family_node_unbond_cleanup";
|
||||
pub const NODE_UNBOND_CLEANUP_EVENT_NODE_ID: &str = "node_id";
|
||||
|
||||
pub const FAMILY_UPDATE_EVENT_NAME: &str = "family_update";
|
||||
pub const FAMILY_UPDATE_EVENT_FAMILY_ID: &str = "family_id";
|
||||
pub const FAMILY_UPDATE_EVENT_OWNER_ADDRESS: &str = "owner_address";
|
||||
/// Attribute carrying the new family name. Only emitted when the
|
||||
/// `UpdateFamily` message carried `updated_name = Some(_)`.
|
||||
pub const FAMILY_UPDATE_EVENT_UPDATED_NAME: &str = "updated_name";
|
||||
/// Attribute carrying the new family description. Only emitted when the
|
||||
/// `UpdateFamily` message carried `updated_description = Some(_)`.
|
||||
pub const FAMILY_UPDATE_EVENT_UPDATED_DESCRIPTION: &str = "updated_description";
|
||||
}
|
||||
|
||||
@@ -38,6 +38,16 @@ pub enum ExecuteMsg {
|
||||
/// `create_family_fee` must be attached as funds.
|
||||
CreateFamily { name: String, description: String },
|
||||
|
||||
/// Update the name and/or description of the family owned by the message
|
||||
/// sender. Each field is independently optional: `None` leaves the
|
||||
/// existing value unchanged, `Some(_)` replaces it. Updated values are
|
||||
/// validated against the same length / normalisation / global-uniqueness
|
||||
/// rules as [`Self::CreateFamily`].
|
||||
UpdateFamily {
|
||||
updated_name: Option<String>,
|
||||
updated_description: Option<String>,
|
||||
},
|
||||
|
||||
/// Disband the family owned by the message sender. The family must have
|
||||
/// no current members; any still-pending invitations are revoked.
|
||||
DisbandFamily {},
|
||||
@@ -86,6 +96,10 @@ pub enum ExecuteMsg {
|
||||
#[cw_serde]
|
||||
#[cfg_attr(feature = "schema", derive(cosmwasm_schema::QueryResponses))]
|
||||
pub enum QueryMsg {
|
||||
/// Retrieve current contract configuration values
|
||||
#[cfg_attr(feature = "schema", returns(Config))]
|
||||
GetConfig {},
|
||||
|
||||
/// Look up a single family by its id.
|
||||
#[cfg_attr(feature = "schema", returns(NodeFamilyResponse))]
|
||||
GetFamilyById { family_id: NodeFamilyId },
|
||||
|
||||
@@ -61,9 +61,12 @@ pub struct NodeFamily {
|
||||
|
||||
/// A pending invitation for a node to join a particular family.
|
||||
///
|
||||
/// Invitations are stored until they are accepted, rejected, revoked, or until the
|
||||
/// chain advances past `expires_at` (in which case they remain in storage but are
|
||||
/// treated as inert — there is no background process clearing expired invitations).
|
||||
/// Invitations are stored until they are accepted, rejected, or revoked. Once the
|
||||
/// chain advances past `expires_at` an invitation becomes inert but stays in storage
|
||||
/// — there is no background process clearing expired invitations. A timed-out
|
||||
/// invitation is cleared either when explicitly revoked/rejected, or when the family
|
||||
/// issues a fresh invitation for the same node, which archives the stale one as
|
||||
/// `Expired` and supersedes it.
|
||||
#[cw_serde]
|
||||
pub struct FamilyInvitation {
|
||||
/// The family that issued the invitation.
|
||||
@@ -107,8 +110,10 @@ pub struct PastFamilyMember {
|
||||
|
||||
/// Terminal status for an invitation that has been moved out of the pending set.
|
||||
///
|
||||
/// Note: timed-out invitations are not represented here — they are simply left in
|
||||
/// the pending set (see `FamilyInvitation::expires_at`).
|
||||
/// Note: an invitation that merely times out is **not** archived here on its own —
|
||||
/// it is left inert in the pending set (see `FamilyInvitation::expires_at`). It only
|
||||
/// reaches `Expired` if the family issues a fresh invitation for the same node, which
|
||||
/// supersedes and archives the stale one.
|
||||
#[cw_serde]
|
||||
pub enum FamilyInvitationStatus {
|
||||
/// Still awaiting a response. Recorded with a timestamp for completeness even
|
||||
@@ -121,11 +126,16 @@ pub enum FamilyInvitationStatus {
|
||||
/// The family revoked the invitation at the given timestamp before it could
|
||||
/// be accepted or rejected.
|
||||
Revoked { at: u64 },
|
||||
/// The invitation had already expired and was superseded by a fresh invitation
|
||||
/// for the same node from the same family, issued at the given timestamp. This is
|
||||
/// the only path that archives a timed-out invitation.
|
||||
Expired { at: u64 },
|
||||
}
|
||||
|
||||
/// Historical record of an invitation that has reached a terminal state
|
||||
/// (`Accepted`, `Rejected`, or `Revoked`). Timed-out invitations are **not**
|
||||
/// archived here — they remain in the pending map until explicitly cleared.
|
||||
/// (`Accepted`, `Rejected`, `Revoked`, or `Expired`). A timed-out invitation is
|
||||
/// archived here only when a fresh invitation for the same node supersedes it
|
||||
/// (status `Expired`); otherwise it stays in the pending map until explicitly cleared.
|
||||
#[cw_serde]
|
||||
pub struct PastFamilyInvitation {
|
||||
/// The original invitation as it was issued.
|
||||
|
||||
@@ -27,6 +27,9 @@ pub struct QuorumStateChecker {
|
||||
cancellation_token: CancellationToken,
|
||||
check_interval: Duration,
|
||||
quorum_state: QuorumState,
|
||||
|
||||
/// indicates whether the last check has been a failure
|
||||
last_failed: bool,
|
||||
}
|
||||
|
||||
impl QuorumStateChecker {
|
||||
@@ -42,6 +45,7 @@ impl QuorumStateChecker {
|
||||
quorum_state: QuorumState {
|
||||
available: Arc::new(Default::default()),
|
||||
},
|
||||
last_failed: false,
|
||||
};
|
||||
|
||||
// first check MUST succeed, otherwise we shouldn't start
|
||||
@@ -57,6 +61,7 @@ impl QuorumStateChecker {
|
||||
}
|
||||
|
||||
async fn check_quorum_state(&self) -> Result<bool, CredentialProxyError> {
|
||||
info!("checking the current quorum state");
|
||||
let client_guard = self.client.query_chain().await;
|
||||
|
||||
// split the operation as we only need to hold the reference to chain client for the first part
|
||||
@@ -64,7 +69,8 @@ impl QuorumStateChecker {
|
||||
let dkg_details = dkg_details_with_client(client_guard.deref()).await?;
|
||||
drop(client_guard);
|
||||
|
||||
let res = check_known_dealers(dkg_details).await?;
|
||||
let res = check_known_dealers(dkg_details, 4).await?;
|
||||
info!("there are {} known DKG dealers", res.results.len());
|
||||
|
||||
let Some(signing_threshold) = res.threshold else {
|
||||
warn!(
|
||||
@@ -76,15 +82,36 @@ impl QuorumStateChecker {
|
||||
let mut working_issuer = 0;
|
||||
|
||||
for result in res.results {
|
||||
let dealer = &result.information;
|
||||
let info = format!("[id: {}] @ {}", dealer.node_index, dealer.announce_address);
|
||||
if result.chain_available() && result.signing_available() {
|
||||
info!("✅ {info} is fully available");
|
||||
working_issuer += 1;
|
||||
} else if !result.chain_available() && !result.signing_available() {
|
||||
warn!("❌ {info} is not available for both chain and signing");
|
||||
} else if !result.chain_available() {
|
||||
warn!("❌ {info} is not available for chain");
|
||||
} else {
|
||||
warn!("❌ {info} is not available for signing");
|
||||
}
|
||||
}
|
||||
|
||||
Ok((working_issuer as u64) >= signing_threshold)
|
||||
let available = (working_issuer as u64) >= signing_threshold;
|
||||
|
||||
if available {
|
||||
info!(
|
||||
"✅ Quorum state is available with {working_issuer} out of {signing_threshold} issuers"
|
||||
)
|
||||
} else {
|
||||
error!(
|
||||
"❌ Quorum state is not available with {working_issuer} out of {signing_threshold} issuers"
|
||||
)
|
||||
}
|
||||
|
||||
Ok(available)
|
||||
}
|
||||
|
||||
pub async fn run_forever(self) {
|
||||
pub async fn run_forever(mut self) {
|
||||
info!("starting quorum state checker");
|
||||
loop {
|
||||
tokio::select! {
|
||||
@@ -94,7 +121,23 @@ impl QuorumStateChecker {
|
||||
}
|
||||
_ = tokio::time::sleep(self.check_interval) => {
|
||||
match self.check_quorum_state().await {
|
||||
Ok(available) => self.quorum_state.available.store(available, Ordering::SeqCst),
|
||||
Ok(available) => {
|
||||
let previous = self.quorum_state.available.load(Ordering::SeqCst);
|
||||
// only update the quorum state to a failed state if we've had two consecutive failures
|
||||
if available {
|
||||
if !previous {
|
||||
info!("quorum recovered");
|
||||
}
|
||||
self.quorum_state.available.store(true, Ordering::SeqCst);
|
||||
} else if self.last_failed {
|
||||
if previous {
|
||||
warn!("quorum became unavailable after 2 consecutive failed checks");
|
||||
}
|
||||
self.quorum_state.available.store(false, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
self.last_failed = !available;
|
||||
},
|
||||
Err(err) => error!("failed to check current quorum state: {err}"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,5 +36,13 @@ nym-ecash-contract-common = { workspace = true }
|
||||
nym-network-defaults = { workspace = true }
|
||||
nym-serde-helpers = { workspace = true, features = ["date"] }
|
||||
|
||||
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.tokio]
|
||||
workspace = true
|
||||
features = ["time"]
|
||||
|
||||
[target."cfg(target_arch = \"wasm32\")".dependencies.wasmtimer]
|
||||
workspace = true
|
||||
features = ["tokio"]
|
||||
|
||||
[dev-dependencies]
|
||||
rand = { workspace = true }
|
||||
|
||||
@@ -6,6 +6,7 @@ use crate::ecash::bandwidth::serialiser::VersionedSerialise;
|
||||
use crate::ecash::bandwidth::CredentialSigningData;
|
||||
use crate::ecash::utils::cred_exp_date;
|
||||
use crate::error::Error;
|
||||
use log::{debug, warn};
|
||||
use nym_api_requests::ecash::BlindSignRequestBody;
|
||||
use nym_credentials_interface::{
|
||||
aggregate_wallets, generate_keypair_user_from_seed, issue_verify, withdrawal_request,
|
||||
@@ -17,8 +18,15 @@ use nym_ecash_contract_common::deposit::DepositId;
|
||||
use nym_ecash_time::{ecash_default_expiration_date, ecash_today, EcashTime};
|
||||
use nym_validator_client::nym_api::{EpochId, NymApiClientExt};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::time::Duration;
|
||||
use time::Date;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use tokio::time::sleep;
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use wasmtimer::tokio::sleep;
|
||||
|
||||
pub use nym_validator_client::nyxd::{Coin, Hash};
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
@@ -192,6 +200,49 @@ impl IssuanceTicketBook {
|
||||
Ok(unblinded_signature)
|
||||
}
|
||||
|
||||
// ideally this would have been generic over credential type, but we really don't need secp256k1 keys for bandwidth vouchers
|
||||
pub async fn obtain_partial_ticketbook_credential_with_retries(
|
||||
&self,
|
||||
client: &nym_http_api_client::Client,
|
||||
signer_index: u64,
|
||||
validator_vk: &VerificationKeyAuth,
|
||||
signing_data: CredentialSigningData,
|
||||
max_attempts: usize,
|
||||
) -> Result<PartialWallet, Error> {
|
||||
let Some(client_url) = client.base_urls().first() else {
|
||||
return Err(Error::CredentialShareObtainFailed);
|
||||
};
|
||||
let mut last_err = None;
|
||||
for attempt in 0..max_attempts {
|
||||
if attempt > 0 {
|
||||
sleep(Duration::from_millis(500 * attempt as u64)).await;
|
||||
}
|
||||
debug!(
|
||||
"attempt {} / {max_attempts} to obtain partial ticketbook credential from {client_url}",
|
||||
attempt + 1,
|
||||
);
|
||||
match self
|
||||
.obtain_partial_ticketbook_credential(
|
||||
client,
|
||||
signer_index,
|
||||
validator_vk,
|
||||
signing_data.clone(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(partial_wallet) => return Ok(partial_wallet),
|
||||
Err(err) => {
|
||||
warn!(
|
||||
"attempt {} / {max_attempts} to obtain partial ticketbook credential from {client_url} failed: {err}",
|
||||
attempt + 1,
|
||||
);
|
||||
last_err = Some(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(last_err.unwrap_or(Error::CredentialShareObtainFailed))
|
||||
}
|
||||
|
||||
// ideally this would have been generic over credential type, but we really don't need secp256k1 keys for bandwidth vouchers
|
||||
pub async fn obtain_partial_ticketbook_credential(
|
||||
&self,
|
||||
|
||||
@@ -137,6 +137,8 @@ pub async fn obtain_aggregate_wallet(
|
||||
ecash_api_clients: &[EcashApiClient],
|
||||
threshold: u64,
|
||||
) -> Result<WalletSignatures, Error> {
|
||||
const MAX_ATTEMPTS: usize = 2;
|
||||
|
||||
if ecash_api_clients.len() < threshold as usize {
|
||||
return Err(Error::NoValidatorsAvailable);
|
||||
}
|
||||
@@ -154,11 +156,12 @@ pub async fn obtain_aggregate_wallet(
|
||||
);
|
||||
|
||||
match voucher
|
||||
.obtain_partial_ticketbook_credential(
|
||||
.obtain_partial_ticketbook_credential_with_retries(
|
||||
&ecash_api_client.api_client,
|
||||
ecash_api_client.node_id,
|
||||
&ecash_api_client.verification_key,
|
||||
request.clone(),
|
||||
MAX_ATTEMPTS,
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -167,6 +170,11 @@ pub async fn obtain_aggregate_wallet(
|
||||
warn!("failed to obtain partial credential from API {ecash_api_client}: {err}",);
|
||||
}
|
||||
};
|
||||
|
||||
// we got sufficient number of shares
|
||||
if wallets.len() >= threshold as usize {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if wallets.len() < threshold as usize {
|
||||
return Err(Error::NotEnoughShares);
|
||||
|
||||
@@ -63,6 +63,9 @@ pub enum Error {
|
||||
|
||||
#[error("failed to create a secp256k1 signature")]
|
||||
Secp256k1SignFailure,
|
||||
|
||||
#[error("failed to obtain a valid credential share")]
|
||||
CredentialShareObtainFailed,
|
||||
}
|
||||
|
||||
impl From<NymAPIError> for Error {
|
||||
|
||||
@@ -31,3 +31,5 @@ pub use aes_gcm_siv::{Aes128GcmSiv, Aes256GcmSiv};
|
||||
pub use blake3;
|
||||
#[cfg(feature = "stream_cipher")]
|
||||
pub use ctr;
|
||||
#[cfg(feature = "hashing")]
|
||||
pub use sha2;
|
||||
|
||||
@@ -6,7 +6,7 @@ use nym_coconut_dkg_common::verification_key::VerificationKeyShare;
|
||||
use nym_crypto::asymmetric::ed25519;
|
||||
use std::time::Duration;
|
||||
use time::OffsetDateTime;
|
||||
use tracing::{debug, warn};
|
||||
use tracing::warn;
|
||||
|
||||
pub trait Verifiable {
|
||||
fn verify_signature(&self, pub_key: &ed25519::PublicKey) -> bool;
|
||||
@@ -36,6 +36,7 @@ pub trait ChainResponse: Verifiable + TimestampedResponse {
|
||||
|
||||
// we rely on information provided from the api itself AS LONG AS it's not too outdated
|
||||
if self.timestamp() + stale_response_threshold < now {
|
||||
warn!("chain status response is stale");
|
||||
return false;
|
||||
}
|
||||
self.chain_synced()
|
||||
@@ -96,26 +97,27 @@ pub trait SignerResponse: Verifiable + TimestampedResponse {
|
||||
|
||||
// we rely on information provided from the api itself AS LONG AS it's not too outdated
|
||||
if self.timestamp() + stale_response_threshold < now {
|
||||
warn!("stale signer response");
|
||||
return false;
|
||||
}
|
||||
|
||||
if !self.has_signing_keys() {
|
||||
debug!("missing signing keys");
|
||||
warn!("missing signing keys");
|
||||
return false;
|
||||
}
|
||||
|
||||
if self.signer_disabled() {
|
||||
debug!("signer functionalities explicitly disabled");
|
||||
warn!("signer functionalities are explicitly disabled");
|
||||
return false;
|
||||
}
|
||||
|
||||
if !self.is_ecash_signer() {
|
||||
debug!("signer doesn't recognise it's a signer for this epoch");
|
||||
warn!("signer doesn't recognise it's a signer for this epoch");
|
||||
return false;
|
||||
}
|
||||
|
||||
if dkg_epoch_id != self.dkg_ecash_epoch_id() {
|
||||
debug!(
|
||||
warn!(
|
||||
"mismatched dkg epoch id. current: {dkg_epoch_id}, signer's: {}",
|
||||
self.dkg_ecash_epoch_id()
|
||||
);
|
||||
|
||||
@@ -11,10 +11,11 @@ use nym_crypto::asymmetric::ed25519;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::time::Duration;
|
||||
use time::OffsetDateTime;
|
||||
use tracing::warn;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
pub(crate) const CHAIN_STALL_THRESHOLD: Duration = Duration::from_secs(5 * 60);
|
||||
pub(crate) const STALE_RESPONSE_THRESHOLD: Duration = Duration::from_secs(5 * 60);
|
||||
pub(crate) const CHAIN_STALL_THRESHOLD: Duration = Duration::from_secs(10 * 60);
|
||||
pub(crate) const STALE_RESPONSE_THRESHOLD: Duration = Duration::from_secs(10 * 60);
|
||||
|
||||
// the reason for generics is not to remove duplication of code,
|
||||
// but because without them, we'd be having problems with circular dependencies,
|
||||
@@ -188,6 +189,7 @@ where
|
||||
};
|
||||
|
||||
let SignerStatus::Tested { result } = &self.status else {
|
||||
warn!("no valid chain response");
|
||||
return false;
|
||||
};
|
||||
result
|
||||
@@ -239,6 +241,7 @@ where
|
||||
};
|
||||
|
||||
let SignerStatus::Tested { result } = &self.status else {
|
||||
warn!("no valid signer response");
|
||||
return false;
|
||||
};
|
||||
result.signing_status.signing_available(
|
||||
|
||||
@@ -195,9 +195,9 @@ impl ClientUnderTest {
|
||||
pub(crate) async fn check_client(
|
||||
dealer_details: DealerDetails,
|
||||
dkg_epoch: u64,
|
||||
contract_share: Option<&ContractVKShare>,
|
||||
contract_share: Option<ContractVKShare>,
|
||||
) -> TypedSignerResult {
|
||||
let dealer_information = RawDealerInformation::new(&dealer_details, contract_share);
|
||||
let dealer_information = RawDealerInformation::new(&dealer_details, contract_share.as_ref());
|
||||
|
||||
// 7. attempt to construct client instances out of them
|
||||
let Ok(parsed_information) = dealer_information.parse() else {
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::client_check::check_client;
|
||||
use futures::stream::{FuturesUnordered, StreamExt};
|
||||
use futures::stream;
|
||||
use futures::stream::StreamExt;
|
||||
use nym_ecash_signer_check_types::status::{SignerResult, Status};
|
||||
use nym_network_defaults::NymNetworkDetails;
|
||||
use nym_validator_client::QueryHttpRpcNyxdClient;
|
||||
@@ -65,7 +66,7 @@ where
|
||||
C: DkgQueryClient + Sync,
|
||||
{
|
||||
let dkg_details = dkg_details_with_client(client).await?;
|
||||
check_known_dealers(dkg_details).await
|
||||
check_known_dealers(dkg_details, None).await
|
||||
}
|
||||
|
||||
pub async fn dkg_details_with_client<C>(client: &C) -> Result<DkgDetails, SignerCheckError>
|
||||
@@ -109,18 +110,21 @@ where
|
||||
|
||||
pub async fn check_known_dealers(
|
||||
dkg_details: DkgDetails,
|
||||
concurrency: impl Into<Option<usize>>,
|
||||
) -> Result<SignersTestResult, SignerCheckError> {
|
||||
// 6. for each dealer attempt to perform the checks
|
||||
let results = dkg_details
|
||||
.network_dealers
|
||||
.into_iter()
|
||||
.map(|d| {
|
||||
let share = dkg_details.submitted_shared.get(&d.assigned_index);
|
||||
check_client(d, dkg_details.dkg_epoch.epoch_id, share)
|
||||
})
|
||||
.collect::<FuturesUnordered<_>>()
|
||||
.collect::<Vec<_>>()
|
||||
.await;
|
||||
let epoch_id = dkg_details.dkg_epoch.epoch_id;
|
||||
let submitted = dkg_details.submitted_shared;
|
||||
let dealers = dkg_details.network_dealers.len();
|
||||
|
||||
let tasks = dkg_details.network_dealers.into_iter().map(move |d| {
|
||||
let share = submitted.get(&d.assigned_index).cloned();
|
||||
check_client(d, epoch_id, share)
|
||||
});
|
||||
|
||||
let limit = concurrency.into().filter(|&n| n > 0).unwrap_or(dealers);
|
||||
|
||||
let results = stream::iter(tasks).buffer_unordered(limit).collect().await;
|
||||
|
||||
Ok(SignersTestResult {
|
||||
threshold: dkg_details.threshold,
|
||||
|
||||
@@ -324,15 +324,12 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
// NOTE THIS TEST IS DISABLED BECAUSE IT INTERACTS WITH LIVE NETWORK AND EXPECTS
|
||||
// SPECIFIC BEHAVIOR. Keeping in case logic changes and needs tested locally.
|
||||
#[cfg(any())] // #[ignore] we run --ignore in CI/CD assuming it just means slow -_-
|
||||
async fn fallback_on_failure() {
|
||||
let url1 = Url::new(
|
||||
"https://fake-domain.nymtech.net",
|
||||
Some(vec![
|
||||
"https://wrong.host.badssl.com/",
|
||||
"https://untrusted-root.badssl.com/",
|
||||
"https://fake-front-1.nymtech.net",
|
||||
"https://fake-front-2.nymtech.net",
|
||||
]),
|
||||
)
|
||||
.unwrap();
|
||||
@@ -342,12 +339,8 @@ mod tests {
|
||||
)
|
||||
.unwrap(); // fastly
|
||||
|
||||
// use non-shared client and no-hickory so that we aren't reliant on a shared http client
|
||||
// that could be impacted by independent interleaved tests.
|
||||
let client = ClientBuilder::new_with_urls(vec![url1, url2])
|
||||
.expect("bad url")
|
||||
.non_shared()
|
||||
.no_hickory_dns()
|
||||
.with_fronting(Some(FrontPolicy::Always))
|
||||
.build()
|
||||
.expect("failed to build client");
|
||||
@@ -359,7 +352,7 @@ mod tests {
|
||||
);
|
||||
assert_eq!(
|
||||
client.current_url().front_str(),
|
||||
Some("wrong.host.badssl.com"),
|
||||
Some("fake-front-1.nymtech.net"),
|
||||
);
|
||||
|
||||
let result = client
|
||||
@@ -372,14 +365,14 @@ mod tests {
|
||||
.await;
|
||||
assert!(result.is_err());
|
||||
|
||||
// Check that the host configuration updated the front on error (tls error wring host)
|
||||
// Check that the host configuration updated the front on error.
|
||||
assert_eq!(
|
||||
client.current_url().as_str(),
|
||||
"https://fake-domain.nymtech.net/",
|
||||
);
|
||||
assert_eq!(
|
||||
client.current_url().front_str(),
|
||||
Some("untrusted-root.badssl.com"),
|
||||
Some("fake-front-2.nymtech.net"),
|
||||
);
|
||||
|
||||
let result = client
|
||||
@@ -392,8 +385,7 @@ mod tests {
|
||||
.await;
|
||||
assert!(result.is_err());
|
||||
|
||||
// Check that the host configuration updated the domain and front on error (tls error
|
||||
// untrusted root)
|
||||
// Check that the host configuration updated the domain and front on error.
|
||||
assert_eq!(
|
||||
client.current_url().as_str(),
|
||||
"https://validator.global.ssl.fastly.net/",
|
||||
|
||||
@@ -10,7 +10,9 @@ fn sanitize_fragment(segment: &str) -> &str {
|
||||
segment.trim_matches(|c: char| c.is_whitespace() || c == '/')
|
||||
}
|
||||
|
||||
/// Defines a path that can be used to make a request to an API.
|
||||
pub trait RequestPath: Debug {
|
||||
/// Sanitise the request path by removing empty segments and trimming whitespace and slashes
|
||||
fn to_sanitized_segments(&self) -> Vec<&str>;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@ publish = true
|
||||
|
||||
|
||||
[features]
|
||||
default = ["codec"]
|
||||
codec = ["dep:tokio-util"]
|
||||
test-utils = ["pnet_packet"]
|
||||
|
||||
[dependencies]
|
||||
@@ -29,6 +31,5 @@ semver = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
thiserror = { workspace = true }
|
||||
time = { workspace = true }
|
||||
tokio = { workspace = true, features = ["time"] }
|
||||
tokio-util = { workspace = true, features = ["codec"] }
|
||||
tokio-util = { workspace = true, features = ["codec"], optional = true }
|
||||
tracing = { workspace = true }
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use bytes::{Buf, Bytes, BytesMut};
|
||||
#[cfg(feature = "codec")]
|
||||
use tokio_util::codec::{Decoder, Encoder};
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
@@ -38,6 +39,23 @@ impl MultiIpPacketCodec {
|
||||
bundled_packets.extend_from_slice(&packet);
|
||||
bundled_packets.freeze()
|
||||
}
|
||||
|
||||
/// Decode one length-prefixed packet from `src`, advancing past it.
|
||||
///
|
||||
/// Same logic as the `Decoder` impl but available without the `codec`
|
||||
/// feature (i.e. without depending on `tokio-util`).
|
||||
pub fn decode_one(&mut self, src: &mut BytesMut) -> Result<Option<IprPacket>, Error> {
|
||||
if src.len() < LENGTH_PREFIX_SIZE {
|
||||
return Ok(None);
|
||||
}
|
||||
let packet_size = u16::from_be_bytes([src[0], src[1]]) as usize;
|
||||
if src.len() < packet_size + LENGTH_PREFIX_SIZE {
|
||||
return Ok(None);
|
||||
}
|
||||
src.advance(LENGTH_PREFIX_SIZE);
|
||||
let packet = src.split_to(packet_size);
|
||||
Ok(Some(IprPacket::Data(packet.freeze())))
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for MultiIpPacketCodec {
|
||||
@@ -82,6 +100,7 @@ impl From<Vec<u8>> for IprPacket {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "codec")]
|
||||
impl Encoder<IprPacket> for MultiIpPacketCodec {
|
||||
type Error = Error;
|
||||
|
||||
@@ -125,6 +144,7 @@ impl Encoder<IprPacket> for MultiIpPacketCodec {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "codec")]
|
||||
impl Decoder for MultiIpPacketCodec {
|
||||
type Item = IprPacket;
|
||||
type Error = Error;
|
||||
@@ -152,7 +172,7 @@ impl Decoder for MultiIpPacketCodec {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(all(test, feature = "codec"))]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use bytes::{Bytes, BytesMut};
|
||||
use tokio_util::codec::Decoder;
|
||||
use tracing::{error, info, warn};
|
||||
|
||||
use crate::{
|
||||
@@ -84,7 +83,7 @@ pub fn handle_ipr_response(data: &[u8]) -> Option<MixnetMessageOutcome> {
|
||||
let mut buf = BytesMut::from(data_response.ip_packet.as_ref());
|
||||
let mut packets = Vec::new();
|
||||
loop {
|
||||
match codec.decode(&mut buf) {
|
||||
match codec.decode_one(&mut buf) {
|
||||
Ok(Some(packet)) => packets.push(packet.into_bytes()),
|
||||
Ok(None) => break,
|
||||
Err(e) => {
|
||||
|
||||
@@ -19,11 +19,10 @@ exclude = ["build.rs"]
|
||||
|
||||
[dependencies]
|
||||
dotenvy = { workspace = true, optional = true }
|
||||
log = { workspace = true, optional = true }
|
||||
schemars = { workspace = true, features = ["preserve_order"], optional = true }
|
||||
serde = { workspace = true, features = ["derive"], optional = true }
|
||||
serde_json = {workspace = true, optional = true }
|
||||
tracing = {workspace = true, optional = true }
|
||||
serde_json = { workspace = true, optional = true }
|
||||
tracing = { workspace = true, optional = true }
|
||||
url = { workspace = true, optional = true }
|
||||
utoipa = { workspace = true, optional = true }
|
||||
|
||||
@@ -32,9 +31,9 @@ utoipa = { workspace = true, optional = true }
|
||||
|
||||
[features]
|
||||
default = ["env", "network"]
|
||||
env = ["dotenvy", "log", "serde_json", "tracing"]
|
||||
env = ["dotenvy", "serde_json", "tracing"]
|
||||
network = ["schemars", "serde", "url"]
|
||||
utoipa = [ "dep:utoipa" ]
|
||||
utoipa = ["dep:utoipa"]
|
||||
|
||||
[build-dependencies]
|
||||
regex = { workspace = true }
|
||||
|
||||
@@ -27,16 +27,20 @@ fn print_env_vars_with_keys_in_file<P: AsRef<Path> + Copy>(config_env_file: P) {
|
||||
.expect("Invalid path to environment configuration file");
|
||||
for item in items {
|
||||
let (key, val) = item.expect("Invalid item in environment configuration file");
|
||||
log::debug!("{key}: {val}");
|
||||
tracing::debug!("{key}: {val}");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn env_configured() -> bool {
|
||||
std::env::var(var_names::CONFIGURED).is_ok()
|
||||
}
|
||||
|
||||
pub fn setup_env<P: AsRef<Path>>(config_env_file: Option<P>) {
|
||||
match std::env::var(var_names::CONFIGURED) {
|
||||
// if the configuration is not already set in the env vars
|
||||
Err(std::env::VarError::NotPresent) => {
|
||||
if let Some(config_env_file) = &config_env_file {
|
||||
log::debug!(
|
||||
tracing::debug!(
|
||||
"Loading environment variables from {:?}",
|
||||
config_env_file.as_ref()
|
||||
);
|
||||
@@ -47,12 +51,12 @@ pub fn setup_env<P: AsRef<Path>>(config_env_file: Option<P>) {
|
||||
// if nothing is set, the use mainnet defaults
|
||||
// if the user has not set `CONFIGURED`, then even if they set any of the env variables,
|
||||
// overwrite them
|
||||
log::debug!("Loading mainnet defaults");
|
||||
tracing::debug!("Loading mainnet defaults");
|
||||
crate::mainnet::export_to_env();
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
log::debug!("Environment variables already set. Using them");
|
||||
tracing::debug!("Environment variables already set. Using them");
|
||||
crate::mainnet::export_to_env()
|
||||
}
|
||||
_ => {
|
||||
|
||||
@@ -22,10 +22,10 @@ pub const VESTING_CONTRACT_ADDRESS: &str =
|
||||
pub const PERFORMANCE_CONTRACT_ADDRESS: &str = "";
|
||||
// /\ TODO: this has to be updated once the contract is deployed
|
||||
|
||||
// \/ TODO: this has to be updated once the contract is deployed
|
||||
pub const NODE_FAMILIES_CONTRACT_ADDRESS: &str = "";
|
||||
// /\ TODO: this has to be updated once the contract is deployed
|
||||
|
||||
pub const NETWORK_MONITORS_CONTRACT_ADDRESS: &str =
|
||||
"n1m3a2ltkjqud8mkmrpqvgllrtv2p4r6js6qwl7p8cqkzrq8jg6e2qwqgl8z";
|
||||
pub const NODE_FAMILIES_CONTRACT_ADDRESS: &str =
|
||||
"n1na0vys0z077hq3zrz6pfea85zgv8ks3t5zysdt6y38c87q045hnsyf2g5x";
|
||||
pub const ECASH_CONTRACT_ADDRESS: &str =
|
||||
"n1r7s6aksyc6pqardx88k3rkgfagwvj4z4zum9mmz2sfk3zm2mha0sd4dnun";
|
||||
pub const GROUP_CONTRACT_ADDRESS: &str =
|
||||
@@ -40,6 +40,10 @@ pub const REWARDING_VALIDATOR_ADDRESS: &str = "n10yyd98e2tuwu0f7ypz9dy3hhjw7v772
|
||||
pub const NYXD_URL: &str = "https://rpc.nymtech.net";
|
||||
pub const NYXD_WS: &str = "wss://rpc.nymtech.net/websocket";
|
||||
|
||||
// cluster of lite rpc nodes (not part of consensus, aggressive pruning, no archival state)
|
||||
pub const NYXD_QUERY_LITE: &str = "https://blockstream.nymtech.net";
|
||||
pub const NYXD_WS_LITE: &str = "wss://blockstream.nymtech.net/websocket";
|
||||
|
||||
pub const NYM_API: &str = "https://validator.nymtech.net/api/";
|
||||
#[cfg(feature = "network")]
|
||||
pub const NYM_APIS: &[ApiUrlConst] = &[
|
||||
@@ -137,6 +141,11 @@ pub fn read_parsed_var_if_not_default<T: std::str::FromStr>(
|
||||
.map(std::str::FromStr::from_str)
|
||||
}
|
||||
|
||||
#[cfg(feature = "env")]
|
||||
pub fn read_parsed_var<T: std::str::FromStr>(var: &str) -> Result<T, T::Err> {
|
||||
std::env::var(var).unwrap_or_default().parse()
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "env", feature = "network"))]
|
||||
pub fn export_to_env() {
|
||||
use crate::var_names;
|
||||
@@ -167,6 +176,14 @@ pub fn export_to_env() {
|
||||
var_names::COCONUT_DKG_CONTRACT_ADDRESS,
|
||||
COCONUT_DKG_CONTRACT_ADDRESS,
|
||||
);
|
||||
set_var_to_default(
|
||||
var_names::PERFORMANCE_CONTRACT_ADDRESS,
|
||||
PERFORMANCE_CONTRACT_ADDRESS,
|
||||
);
|
||||
set_var_to_default(
|
||||
var_names::NETWORK_MONITORS_CONTRACT_ADDRESS,
|
||||
NETWORK_MONITORS_CONTRACT_ADDRESS,
|
||||
);
|
||||
set_var_to_default(
|
||||
var_names::REWARDING_VALIDATOR_ADDRESS,
|
||||
REWARDING_VALIDATOR_ADDRESS,
|
||||
@@ -186,6 +203,8 @@ pub fn export_to_env() {
|
||||
var_names::UPGRADE_MODE_ATTESTER_ED25519_BS58_PUBKEY,
|
||||
UPGRADE_MODE_ATTESTER_ED25519_BS58_PUBKEY,
|
||||
);
|
||||
set_var_to_default(var_names::NYXD_QUERY_LITE, NYXD_QUERY_LITE);
|
||||
set_var_to_default(var_names::NYXD_WS_LITE, NYXD_WS_LITE);
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "env", feature = "network"))]
|
||||
@@ -237,4 +256,6 @@ pub fn export_to_env_if_not_set() {
|
||||
var_names::UPGRADE_MODE_ATTESTER_ED25519_BS58_PUBKEY,
|
||||
UPGRADE_MODE_ATTESTER_ED25519_BS58_PUBKEY,
|
||||
);
|
||||
set_var_conditionally_to_default(var_names::NYXD_QUERY_LITE, NYXD_QUERY_LITE);
|
||||
set_var_conditionally_to_default(var_names::NYXD_WS_LITE, NYXD_WS_LITE);
|
||||
}
|
||||
|
||||
@@ -40,6 +40,8 @@ pub struct NymContracts {
|
||||
#[serde(default)]
|
||||
pub performance_contract_address: Option<String>,
|
||||
#[serde(default)]
|
||||
pub network_monitors_contract_address: Option<String>,
|
||||
#[serde(default)]
|
||||
pub node_families_contract_address: Option<String>,
|
||||
pub ecash_contract_address: Option<String>,
|
||||
pub group_contract_address: Option<String>,
|
||||
@@ -74,6 +76,15 @@ pub struct ApiUrl {
|
||||
pub front_hosts: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
impl From<Url> for ApiUrl {
|
||||
fn from(value: Url) -> Self {
|
||||
ApiUrl {
|
||||
url: value.to_string(),
|
||||
front_hosts: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize)]
|
||||
pub struct ApiUrlConst<'a> {
|
||||
pub url: &'a str,
|
||||
@@ -183,6 +194,10 @@ impl NymNetworkDetails {
|
||||
.with_group_contract(get_optional_env(var_names::GROUP_CONTRACT_ADDRESS))
|
||||
.with_multisig_contract(get_optional_env(var_names::MULTISIG_CONTRACT_ADDRESS))
|
||||
.with_coconut_dkg_contract(get_optional_env(var_names::COCONUT_DKG_CONTRACT_ADDRESS))
|
||||
.with_performance_contract(get_optional_env(var_names::PERFORMANCE_CONTRACT_ADDRESS))
|
||||
.with_network_monitors_contract(get_optional_env(
|
||||
var_names::NETWORK_MONITORS_CONTRACT_ADDRESS,
|
||||
))
|
||||
.with_nym_vpn_api_url(get_optional_env(var_names::NYM_VPN_API))
|
||||
.with_nym_vpn_api_urls(nym_vpn_api_urls)
|
||||
.with_nym_api_urls(nym_api_urls)
|
||||
@@ -204,6 +219,9 @@ impl NymNetworkDetails {
|
||||
performance_contract_address: parse_optional_str(
|
||||
mainnet::PERFORMANCE_CONTRACT_ADDRESS,
|
||||
),
|
||||
network_monitors_contract_address: parse_optional_str(
|
||||
mainnet::NETWORK_MONITORS_CONTRACT_ADDRESS,
|
||||
),
|
||||
node_families_contract_address: parse_optional_str(
|
||||
mainnet::NODE_FAMILIES_CONTRACT_ADDRESS,
|
||||
),
|
||||
@@ -234,7 +252,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 {
|
||||
@@ -260,6 +278,7 @@ impl NymNetworkDetails {
|
||||
|
||||
set_optional_var(var_names::MIXNET_CONTRACT_ADDRESS, self.contracts.mixnet_contract_address);
|
||||
set_optional_var(var_names::VESTING_CONTRACT_ADDRESS, self.contracts.vesting_contract_address);
|
||||
set_optional_var(var_names::NETWORK_MONITORS_CONTRACT_ADDRESS, self.contracts.network_monitors_contract_address);
|
||||
set_optional_var(var_names::NODE_FAMILIES_CONTRACT_ADDRESS, self.contracts.node_families_contract_address);
|
||||
set_optional_var(var_names::ECASH_CONTRACT_ADDRESS, self.contracts.ecash_contract_address);
|
||||
set_optional_var(var_names::GROUP_CONTRACT_ADDRESS, self.contracts.group_contract_address);
|
||||
@@ -379,15 +398,31 @@ impl NymNetworkDetails {
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_performance_contract<S: Into<String>>(mut self, contract: Option<S>) -> Self {
|
||||
self.contracts.performance_contract_address = contract.map(Into::into);
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_network_monitors_contract<S: Into<String>>(mut self, contract: Option<S>) -> Self {
|
||||
self.contracts.network_monitors_contract_address = contract.map(Into::into);
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_nym_vpn_api_url<S: Into<String>>(mut self, endpoint: Option<S>) -> Self {
|
||||
self.nym_vpn_api_url = endpoint.map(Into::into);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_nym_api_urls<U: Into<ApiUrl>>(&mut self, urls: Vec<U>) {
|
||||
self.nym_api_urls = Some(urls.into_iter().map(Into::into).collect());
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_nym_api_urls(mut self, urls: Vec<ApiUrl>) -> Self {
|
||||
self.nym_api_urls = Some(urls);
|
||||
pub fn with_nym_api_urls<U: Into<ApiUrl>>(mut self, urls: Vec<U>) -> Self {
|
||||
self.set_nym_api_urls(urls);
|
||||
self
|
||||
}
|
||||
|
||||
|
||||
@@ -19,11 +19,15 @@ pub const GROUP_CONTRACT_ADDRESS: &str = "GROUP_CONTRACT_ADDRESS";
|
||||
pub const MULTISIG_CONTRACT_ADDRESS: &str = "MULTISIG_CONTRACT_ADDRESS";
|
||||
pub const NODE_FAMILIES_CONTRACT_ADDRESS: &str = "NODE_FAMILIES_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";
|
||||
|
||||
@@ -73,6 +73,27 @@ impl<T> NonExhaustiveDelayQueue<T> {
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.inner.is_empty()
|
||||
}
|
||||
|
||||
/// Pop the next *already-expired* item without awaiting, or `None` if nothing is ready right
|
||||
/// now (the queue is empty, or its earliest item has not reached its deadline yet). Lets a
|
||||
/// caller drain a burst of simultaneously-expired items in a tight loop without yielding.
|
||||
///
|
||||
/// It polls the inner queue with a **no-op waker**, so a not-yet-due (`None`) result registers
|
||||
/// no real wakeup. This is therefore sound ONLY when the caller subsequently polls the
|
||||
/// [`Stream`] impl (`.next().await`) before parking the task - that re-arms the timer against
|
||||
/// the task's real waker, superseding the no-op one. The intended use is "drain the extra ready
|
||||
/// items right after `.next()` yielded one, in a loop that returns to `.next().await`". Calling
|
||||
/// it as the last thing before suspending would drop the wakeup (same caveat as
|
||||
/// `futures::FutureExt::now_or_never`).
|
||||
pub fn try_next_expired(&mut self) -> Option<Expired<T>> {
|
||||
let mut cx = Context::from_waker(Waker::noop());
|
||||
match Pin::new(&mut self.inner).poll_expired(&mut cx) {
|
||||
// a ready-expired item, or `None` because the queue is empty
|
||||
Poll::Ready(maybe_item) => maybe_item,
|
||||
// queue is non-empty but nothing is due yet
|
||||
Poll::Pending => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Default for NonExhaustiveDelayQueue<T> {
|
||||
|
||||
@@ -18,7 +18,7 @@ bytes.workspace = true
|
||||
futures.workspace = true
|
||||
nym-config = { workspace = true }
|
||||
nym-common = { workspace = true }
|
||||
nym-ip-packet-requests = { workspace = true }
|
||||
nym-ip-packet-requests = { workspace = true, default-features = true }
|
||||
nym-sdk = { workspace = true }
|
||||
pnet_packet.workspace = true
|
||||
thiserror.workspace = true
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "nym-kkt"
|
||||
description = "Key transport protocol for the Nym network"
|
||||
version = "1.21.0"
|
||||
version = "1.21.1"
|
||||
authors = ["Georgio Nicolas <georgio@nymtech.net>"]
|
||||
edition = { workspace = true }
|
||||
license.workspace = true
|
||||
|
||||
@@ -4,11 +4,13 @@ description = "Lewes Protocol data structure for the Nym network"
|
||||
version.workspace = true
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
documentation.workspace = true
|
||||
rust-version.workspace = true
|
||||
readme.workspace = true
|
||||
publish = true
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
|
||||
+385
-44
@@ -1,19 +1,19 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use arc_swap::ArcSwap;
|
||||
use nym_crypto::asymmetric::x25519;
|
||||
use snow::params::NoiseParams;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
net::{IpAddr, SocketAddr},
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use arc_swap::ArcSwap;
|
||||
use nym_crypto::asymmetric::x25519;
|
||||
use nym_noise_keys::{NoiseVersion, VersionedNoiseKeyV1};
|
||||
use snow::params::NoiseParams;
|
||||
|
||||
use strum_macros::{EnumIter, FromRepr};
|
||||
use tokio::sync::{Mutex, MutexGuard};
|
||||
|
||||
pub use nym_noise_keys::{NoiseVersion, VersionedNoiseKeyV1};
|
||||
|
||||
#[derive(Default, Debug, Clone, Copy, EnumIter, FromRepr, Eq, PartialEq)]
|
||||
#[repr(u8)]
|
||||
@@ -53,38 +53,125 @@ impl NoisePattern {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct SocketAddrToKey {
|
||||
inner: ArcSwap<HashMap<SocketAddr, VersionedNoiseKeyV1>>,
|
||||
}
|
||||
|
||||
// SW NOTE : Only for phased upgrade. To remove once we decide all nodes have to support Noise
|
||||
#[derive(Debug, Default)]
|
||||
struct IpAddrToVersion {
|
||||
inner: ArcSwap<HashMap<IpAddr, NoiseVersion>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct NoiseNetworkView {
|
||||
keys: Arc<SocketAddrToKey>,
|
||||
support: Arc<IpAddrToVersion>,
|
||||
inner: Arc<NoiseNetworkViewInner>,
|
||||
}
|
||||
|
||||
impl NoiseNetworkView {
|
||||
pub fn new_empty() -> Self {
|
||||
NoiseNetworkView {
|
||||
keys: Default::default(),
|
||||
support: Default::default(),
|
||||
/// Inner state of [`NoiseNetworkView`], shared behind an `Arc`.
|
||||
///
|
||||
/// # Concurrency model
|
||||
///
|
||||
/// Reads (on the packet-processing hot path) use `ArcSwap` and are fully lock-free.
|
||||
/// Writers must first acquire `update_lock` to serialise concurrent updates, then call
|
||||
/// `swap_view` to atomically publish the new map. The lock is intentionally *not* wrapping
|
||||
/// the map itself so that readers are never blocked.
|
||||
#[derive(Debug)]
|
||||
struct NoiseNetworkViewInner {
|
||||
update_lock: Mutex<()>,
|
||||
nodes: ArcSwap<HashMap<IpAddr, NoiseNode>>,
|
||||
}
|
||||
|
||||
/// A node in the noise network map, keyed by IP address.
|
||||
///
|
||||
/// A single IP can correspond to either one nym-node (which has a single noise key)
|
||||
/// or one-or-more network monitor agents (each with its own port and noise key).
|
||||
/// The two variants have independent lifecycles: nym-node entries come from the
|
||||
/// nym-api topology refresher, while agent entries come from blockchain events.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum NoiseNode {
|
||||
NymNode { key: VersionedNoiseKeyV1 },
|
||||
// due to the structure of network monitor agents,
|
||||
// it is possible to have multiple destinations with the same host ip address,
|
||||
// but a different noise key.
|
||||
// however, we are also guaranteed all of those are going to have a unique port.
|
||||
// note: we're not storing it in a map, since at maximum we might have maybe 20 or so
|
||||
// entries under a single ip address and linear look-up of a vec is faster than the overhead of a hashmap
|
||||
NetworkMonitorAgent { nodes: Vec<NetworkMonitorAgentNode> },
|
||||
}
|
||||
|
||||
impl NoiseNode {
|
||||
pub fn new_nym_node(key: VersionedNoiseKeyV1) -> Self {
|
||||
NoiseNode::NymNode { key }
|
||||
}
|
||||
|
||||
pub fn new_agent(socket_addr: SocketAddr, key: VersionedNoiseKeyV1) -> Self {
|
||||
NoiseNode::NetworkMonitorAgent {
|
||||
nodes: vec![NetworkMonitorAgentNode {
|
||||
port: socket_addr.port(),
|
||||
key,
|
||||
}],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn swap_view(&self, new: HashMap<SocketAddr, VersionedNoiseKeyV1>) {
|
||||
let noise_support = new
|
||||
.iter()
|
||||
.map(|(s_addr, key)| (s_addr.ip(), key.supported_version))
|
||||
.collect::<HashMap<_, _>>();
|
||||
self.keys.inner.store(Arc::new(new));
|
||||
self.support.inner.store(Arc::new(noise_support));
|
||||
pub fn is_nym_node(&self) -> bool {
|
||||
matches!(self, NoiseNode::NymNode { .. })
|
||||
}
|
||||
}
|
||||
|
||||
/// A single network monitor agent identified by its port on a shared host.
|
||||
///
|
||||
/// Multiple agents may share an IP address but are guaranteed to have unique ports.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct NetworkMonitorAgentNode {
|
||||
pub port: u16,
|
||||
pub key: VersionedNoiseKeyV1,
|
||||
}
|
||||
|
||||
impl NoiseNetworkView {
|
||||
pub fn new(nodes: HashMap<IpAddr, NoiseNode>) -> Self {
|
||||
// ensure we're always storing canonical IPs
|
||||
NoiseNetworkView {
|
||||
inner: Arc::new(NoiseNetworkViewInner {
|
||||
update_lock: Mutex::new(()),
|
||||
nodes: ArcSwap::from_pointee(
|
||||
nodes
|
||||
.into_iter()
|
||||
.map(|(k, v)| (k.to_canonical(), v))
|
||||
.collect(),
|
||||
),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_empty() -> Self {
|
||||
Self::new(Default::default())
|
||||
}
|
||||
|
||||
/// Build a noise view pre-populated with network monitor agents (used at startup).
|
||||
pub fn new_with_agents(agents: HashMap<IpAddr, Vec<NetworkMonitorAgentNode>>) -> Self {
|
||||
let mut nodes = HashMap::new();
|
||||
for (ip, agent_nodes) in agents {
|
||||
nodes.insert(ip, NoiseNode::NetworkMonitorAgent { nodes: agent_nodes });
|
||||
}
|
||||
Self::new(nodes)
|
||||
}
|
||||
|
||||
pub async fn get_update_permit(&self) -> MutexGuard<'_, ()> {
|
||||
self.inner.update_lock.lock().await
|
||||
}
|
||||
|
||||
/// Atomically replace the noise key map.
|
||||
///
|
||||
/// # Precondition
|
||||
///
|
||||
/// The caller **must** hold the permit returned by [`NoiseNetworkView::get_update_permit`].
|
||||
/// Passing the `MutexGuard` by value enforces this at the type level — the guard is dropped
|
||||
/// (releasing the lock) only after the swap completes, preventing torn writes from concurrent
|
||||
/// update calls.
|
||||
pub fn swap_view(&self, _permit: MutexGuard<'_, ()>, new: HashMap<IpAddr, NoiseNode>) {
|
||||
// defensive: ensure stored keys are always canonical so lookups (which canonicalise)
|
||||
// always match. callers should still canonicalise before assembling `new` to keep
|
||||
// collision resolution deterministic.
|
||||
let canonical = new
|
||||
.into_iter()
|
||||
.map(|(k, v)| (k.to_canonical(), v))
|
||||
.collect();
|
||||
self.inner.nodes.store(Arc::new(canonical));
|
||||
}
|
||||
|
||||
pub fn all_nodes(&self) -> HashMap<IpAddr, NoiseNode> {
|
||||
self.inner.nodes.load().as_ref().clone()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,20 +213,38 @@ impl NoiseConfig {
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn get_noise_key(&self, s_address: &SocketAddr) -> Option<VersionedNoiseKeyV1> {
|
||||
self.network.keys.inner.load().get(s_address).copied()
|
||||
/// Look up the noise key for a specific remote socket address.
|
||||
///
|
||||
/// Used on the **initiator** path where we need the responder's public key
|
||||
/// to start the handshake. For nym-nodes the port is ignored (one key per IP);
|
||||
/// for network monitor agents, the port disambiguates which agent's key to use.
|
||||
pub(crate) fn get_noise_key(&self, address: SocketAddr) -> Option<VersionedNoiseKeyV1> {
|
||||
let ip_to_check = address.ip().to_canonical();
|
||||
let nodes = self.network.inner.nodes.load();
|
||||
|
||||
// Resolve the noise key for `address` from a loaded snapshot of the node map.
|
||||
// For [`NoiseNode::NymNode`] entries the port is irrelevant — only the IP is matched.
|
||||
// For [`NoiseNode::NetworkMonitorAgent`] entries the port selects the specific agent.
|
||||
match nodes.get(&ip_to_check)? {
|
||||
NoiseNode::NymNode { key } => Some(*key),
|
||||
NoiseNode::NetworkMonitorAgent { nodes } => {
|
||||
let port = address.port();
|
||||
nodes.iter().find(|n| n.port == port).map(|n| n.key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Only for phased update
|
||||
//SW This can lead to some troubles if two nodes share the same IP and one support Noise but not the other.
|
||||
// This in only for the progressive update though and there is no workaround
|
||||
pub(crate) fn get_noise_support(&self, ip_addr: IpAddr) -> Option<NoiseVersion> {
|
||||
let plain_ip_support = self.network.support.inner.load().get(&ip_addr).copied();
|
||||
|
||||
// SW default bind address being [::]:1789, it can happen that a responder sees the ipv6-mapped address of the initiator, this check for that
|
||||
let canonical_ip = &ip_addr.to_canonical();
|
||||
let canonical_ip_support = self.network.support.inner.load().get(canonical_ip).copied();
|
||||
plain_ip_support.or(canonical_ip_support)
|
||||
/// Check whether a remote IP is known to support noise.
|
||||
/// Used on the responder path where we don't need the remote's key
|
||||
/// (the initiator sends it during the handshake).
|
||||
// note: in the case of network monitor agents, it must hold
|
||||
// that ALL agents on given host support it (or don't support it)
|
||||
pub(crate) fn supports_noise(&self, ip_addr: IpAddr) -> bool {
|
||||
self.network
|
||||
.inner
|
||||
.nodes
|
||||
.load()
|
||||
.contains_key(&ip_addr.to_canonical())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,4 +274,240 @@ mod tests {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod noise_key_lookup {
|
||||
use super::super::*;
|
||||
use nym_crypto::asymmetric::x25519;
|
||||
use nym_noise_keys::NoiseVersion;
|
||||
use nym_test_utils::helpers::deterministic_rng;
|
||||
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
fn dummy_key(seed: u8) -> VersionedNoiseKeyV1 {
|
||||
VersionedNoiseKeyV1 {
|
||||
supported_version: NoiseVersion::V1,
|
||||
x25519_pubkey: x25519::PublicKey::from([seed; 32]),
|
||||
}
|
||||
}
|
||||
|
||||
fn make_config(nodes: HashMap<IpAddr, NoiseNode>) -> NoiseConfig {
|
||||
NoiseConfig::new(
|
||||
Arc::new(x25519::KeyPair::new(&mut deterministic_rng())),
|
||||
NoiseNetworkView::new(nodes),
|
||||
Duration::from_secs(5),
|
||||
)
|
||||
}
|
||||
|
||||
// -- get_noise_key tests --
|
||||
|
||||
#[test]
|
||||
fn nym_node_key_returned_regardless_of_port() {
|
||||
let ip = IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1));
|
||||
let key = dummy_key(1);
|
||||
let config = make_config(HashMap::from([(ip, NoiseNode::new_nym_node(key))]));
|
||||
|
||||
// any port should resolve to the same key
|
||||
assert_eq!(config.get_noise_key(SocketAddr::new(ip, 1000)), Some(key));
|
||||
assert_eq!(config.get_noise_key(SocketAddr::new(ip, 9999)), Some(key));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn agent_key_resolved_by_port() {
|
||||
let ip = IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1));
|
||||
let key_a = dummy_key(1);
|
||||
let key_b = dummy_key(2);
|
||||
|
||||
let node = NoiseNode::NetworkMonitorAgent {
|
||||
nodes: vec![
|
||||
NetworkMonitorAgentNode {
|
||||
port: 1000,
|
||||
key: key_a,
|
||||
},
|
||||
NetworkMonitorAgentNode {
|
||||
port: 2000,
|
||||
key: key_b,
|
||||
},
|
||||
],
|
||||
};
|
||||
let config = make_config(HashMap::from([(ip, node)]));
|
||||
|
||||
assert_eq!(config.get_noise_key(SocketAddr::new(ip, 1000)), Some(key_a));
|
||||
assert_eq!(config.get_noise_key(SocketAddr::new(ip, 2000)), Some(key_b));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn agent_unknown_port_returns_none() {
|
||||
let ip = IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1));
|
||||
let node = NoiseNode::NetworkMonitorAgent {
|
||||
nodes: vec![NetworkMonitorAgentNode {
|
||||
port: 1000,
|
||||
key: dummy_key(1),
|
||||
}],
|
||||
};
|
||||
let config = make_config(HashMap::from([(ip, node)]));
|
||||
|
||||
assert!(config.get_noise_key(SocketAddr::new(ip, 9999)).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn completely_unknown_address_returns_none() {
|
||||
let config = make_config(HashMap::new());
|
||||
|
||||
assert!(config
|
||||
.get_noise_key(SocketAddr::new(IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4)), 80))
|
||||
.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn canonical_ipv6_fallback_for_nym_node() {
|
||||
// register under the plain IPv4 address
|
||||
let v4 = IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4));
|
||||
let key = dummy_key(1);
|
||||
let config = make_config(HashMap::from([(v4, NoiseNode::new_nym_node(key))]));
|
||||
|
||||
// query with the IPv4-mapped IPv6 form (::ffff:1.2.3.4)
|
||||
let v6_mapped = IpAddr::V6(Ipv4Addr::new(1, 2, 3, 4).to_ipv6_mapped());
|
||||
assert_eq!(
|
||||
config.get_noise_key(SocketAddr::new(v6_mapped, 1789)),
|
||||
Some(key)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn canonical_ipv6_fallback_for_agent() {
|
||||
let v4 = IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4));
|
||||
let key = dummy_key(1);
|
||||
let node = NoiseNode::NetworkMonitorAgent {
|
||||
nodes: vec![NetworkMonitorAgentNode { port: 1000, key }],
|
||||
};
|
||||
let config = make_config(HashMap::from([(v4, node)]));
|
||||
|
||||
let v6_mapped = IpAddr::V6(Ipv4Addr::new(1, 2, 3, 4).to_ipv6_mapped());
|
||||
assert_eq!(
|
||||
config.get_noise_key(SocketAddr::new(v6_mapped, 1000)),
|
||||
Some(key)
|
||||
);
|
||||
// wrong port still returns None even with the fallback
|
||||
assert!(config
|
||||
.get_noise_key(SocketAddr::new(v6_mapped, 9999))
|
||||
.is_none());
|
||||
}
|
||||
|
||||
// -- supports_noise tests --
|
||||
|
||||
#[test]
|
||||
fn supports_noise_true_for_nym_node() {
|
||||
let ip = IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1));
|
||||
let config = make_config(HashMap::from([(ip, NoiseNode::new_nym_node(dummy_key(1)))]));
|
||||
|
||||
assert!(config.supports_noise(ip));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn supports_noise_true_for_agent_ip() {
|
||||
let ip = IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1));
|
||||
let node = NoiseNode::NetworkMonitorAgent {
|
||||
nodes: vec![NetworkMonitorAgentNode {
|
||||
port: 1000,
|
||||
key: dummy_key(1),
|
||||
}],
|
||||
};
|
||||
let config = make_config(HashMap::from([(ip, node)]));
|
||||
|
||||
assert!(config.supports_noise(ip));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn supports_noise_false_for_unknown_ip() {
|
||||
let config = make_config(HashMap::new());
|
||||
|
||||
assert!(!config.supports_noise(IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn supports_noise_canonical_ipv6_fallback() {
|
||||
let v4 = IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4));
|
||||
let config = make_config(HashMap::from([(v4, NoiseNode::new_nym_node(dummy_key(1)))]));
|
||||
|
||||
let v6_mapped = IpAddr::V6(Ipv4Addr::new(1, 2, 3, 4).to_ipv6_mapped());
|
||||
assert!(config.supports_noise(v6_mapped));
|
||||
}
|
||||
|
||||
// -- new_with_agents test --
|
||||
|
||||
#[test]
|
||||
fn new_with_agents_builds_correct_view() {
|
||||
let ip = IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1));
|
||||
let key_a = dummy_key(1);
|
||||
let key_b = dummy_key(2);
|
||||
|
||||
let agents = HashMap::from([(
|
||||
ip,
|
||||
vec![
|
||||
NetworkMonitorAgentNode {
|
||||
port: 1000,
|
||||
key: key_a,
|
||||
},
|
||||
NetworkMonitorAgentNode {
|
||||
port: 2000,
|
||||
key: key_b,
|
||||
},
|
||||
],
|
||||
)]);
|
||||
|
||||
let config = NoiseConfig::new(
|
||||
Arc::new(x25519::KeyPair::new(&mut deterministic_rng())),
|
||||
NoiseNetworkView::new_with_agents(agents),
|
||||
Duration::from_secs(5),
|
||||
);
|
||||
|
||||
assert_eq!(config.get_noise_key(SocketAddr::new(ip, 1000)), Some(key_a));
|
||||
assert_eq!(config.get_noise_key(SocketAddr::new(ip, 2000)), Some(key_b));
|
||||
assert!(config.supports_noise(ip));
|
||||
}
|
||||
|
||||
// -- swap_view canonicalisation test --
|
||||
|
||||
// Regression: an agent registered via blockchain events flows through `swap_view` (called
|
||||
// from `NetworkMonitorAgentsModule::new_agent` and from the periodic network refresher).
|
||||
// If a non-canonical (IPv4-mapped IPv6) key reaches `swap_view`, lookups via
|
||||
// `supports_noise` (which canonicalises) used to miss, producing the
|
||||
// "can't speak Noise yet, falling back to TCP" warning despite the agent being correctly
|
||||
// authorised in the routing filter.
|
||||
#[tokio::test]
|
||||
async fn swap_view_canonicalises_non_canonical_keys() {
|
||||
let view = NoiseNetworkView::new_empty();
|
||||
let v4 = IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4));
|
||||
let v6_mapped = IpAddr::V6(Ipv4Addr::new(1, 2, 3, 4).to_ipv6_mapped());
|
||||
|
||||
let mut nodes = HashMap::new();
|
||||
// intentionally insert under the IPv4-mapped form — what a buggy caller might do
|
||||
nodes.insert(
|
||||
v6_mapped,
|
||||
NoiseNode::NetworkMonitorAgent {
|
||||
nodes: vec![NetworkMonitorAgentNode {
|
||||
port: 1000,
|
||||
key: dummy_key(1),
|
||||
}],
|
||||
},
|
||||
);
|
||||
|
||||
let permit = view.get_update_permit().await;
|
||||
view.swap_view(permit, nodes);
|
||||
|
||||
let config = NoiseConfig::new(
|
||||
Arc::new(x25519::KeyPair::new(&mut deterministic_rng())),
|
||||
view,
|
||||
Duration::from_secs(5),
|
||||
);
|
||||
|
||||
// lookup via either form must succeed
|
||||
assert!(config.supports_noise(v4));
|
||||
assert!(config.supports_noise(v6_mapped));
|
||||
assert!(config
|
||||
.get_noise_key(SocketAddr::new(v6_mapped, 1000))
|
||||
.is_some());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,12 @@ pub enum Connection<C> {
|
||||
Noise(#[pin] Box<NoiseStream<C>>),
|
||||
}
|
||||
|
||||
impl<C> Connection<C> {
|
||||
pub fn is_noise(&self) -> bool {
|
||||
matches!(self, Connection::Noise(_))
|
||||
}
|
||||
}
|
||||
|
||||
impl<C> AsyncRead for Connection<C>
|
||||
where
|
||||
C: AsyncRead + AsyncWrite + Unpin,
|
||||
|
||||
@@ -66,7 +66,7 @@ pub async fn upgrade_noise_initiator(
|
||||
Error::Prereq(Prerequisite::RemotePublicKey)
|
||||
})?;
|
||||
|
||||
let Some(key) = config.get_noise_key(&responder_addr) else {
|
||||
let Some(key) = config.get_noise_key(responder_addr) else {
|
||||
warn!("{responder_addr} can't speak Noise yet, falling back to TCP");
|
||||
return Ok(Connection::Raw(conn));
|
||||
};
|
||||
@@ -106,7 +106,7 @@ pub async fn upgrade_noise_responder(
|
||||
};
|
||||
|
||||
// if responder doesn't announce noise support, we fallback to tcp
|
||||
if config.get_noise_support(initiator_addr.ip()).is_none() {
|
||||
if !config.supports_noise(initiator_addr.ip()) {
|
||||
warn!("{initiator_addr} can't speak Noise yet, falling back to TCP",);
|
||||
return Ok(Connection::Raw(conn));
|
||||
};
|
||||
|
||||
@@ -110,6 +110,12 @@ pub enum PacketProcessingError {
|
||||
PacketReplay,
|
||||
}
|
||||
|
||||
impl PacketProcessingError {
|
||||
pub fn is_replay(&self) -> bool {
|
||||
matches!(self, PacketProcessingError::PacketReplay)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PartialyUnwrappedPacketWithKeyRotation {
|
||||
pub packet: PartiallyUnwrappedPacket,
|
||||
pub used_key_rotation: u32,
|
||||
|
||||
@@ -29,7 +29,7 @@ pub use sphinx_packet::{
|
||||
packet::builder::DEFAULT_PAYLOAD_SIZE,
|
||||
payload::{
|
||||
PAYLOAD_OVERHEAD_SIZE, Payload,
|
||||
key::{PayloadKey, PayloadKeySeed},
|
||||
key::{PayloadKey, PayloadKeySeed, derive_payload_key},
|
||||
},
|
||||
route::{Destination, DestinationAddressBytes, Node, NodeAddressBytes, SURBIdentifier},
|
||||
surb::{SURB, SURBMaterial},
|
||||
|
||||
@@ -7,8 +7,9 @@ use nyxd_scraper_shared::NyxdScraper;
|
||||
pub use nyxd_scraper_shared::constants;
|
||||
pub use nyxd_scraper_shared::error::ScraperError;
|
||||
pub use nyxd_scraper_shared::{
|
||||
BlockModule, MsgModule, NyxdScraperTransaction, ParsedTransactionResponse, PruningOptions,
|
||||
PruningStrategy, StartingBlockOpts, TxModule,
|
||||
BlockModule, DecodedMessage, MsgModule, NyxdScraperTransaction, ParsedTransactionDetails,
|
||||
ParsedTransactionResponse, PruningOptions, PruningStrategy, StartingBlockOpts, TxModule,
|
||||
parse_msg,
|
||||
};
|
||||
pub use storage::models;
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ use std::str::FromStr;
|
||||
// replicate behaviour of `CosmosMessageAddressesParser` from juno
|
||||
pub(crate) fn parse_addresses_from_events(tx: &ParsedTransactionResponse) -> Vec<String> {
|
||||
let mut addresses: Vec<String> = Vec::new();
|
||||
for event in &tx.tx_result.events {
|
||||
for event in &tx.tx_details.tx_result.events {
|
||||
for attribute in &event.attributes {
|
||||
let Ok(value) = attribute.value_str() else {
|
||||
continue;
|
||||
|
||||
@@ -147,6 +147,7 @@ impl PostgresStorageTransaction {
|
||||
for chain_tx in txs {
|
||||
// bdjuno style, base64 encode them
|
||||
let signatures = chain_tx
|
||||
.tx_details
|
||||
.tx
|
||||
.signatures
|
||||
.iter()
|
||||
@@ -154,12 +155,14 @@ impl PostgresStorageTransaction {
|
||||
.collect();
|
||||
|
||||
let messages = chain_tx
|
||||
.parsed_messages
|
||||
.decoded_messages
|
||||
.values()
|
||||
.map(|msg| &msg.decoded_content)
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let signer_infos = chain_tx
|
||||
.tx_details
|
||||
.tx
|
||||
.auth_info
|
||||
.signer_infos
|
||||
@@ -167,28 +170,28 @@ impl PostgresStorageTransaction {
|
||||
.map(|info| proto::cosmos::tx::v1beta1::SignerInfo::from(info.clone()))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let hash = chain_tx.hash.to_string();
|
||||
let height = chain_tx.height.into();
|
||||
let index = chain_tx.index as i32;
|
||||
let hash = chain_tx.tx_details.hash.to_string();
|
||||
let height = chain_tx.tx_details.height().into();
|
||||
let index = chain_tx.tx_details.index as i32;
|
||||
|
||||
let log = serde_json::to_value(chain_tx.tx_result.log.clone())
|
||||
let log = serde_json::to_value(chain_tx.tx_details.tx_result.log.clone())
|
||||
.inspect_err(|e| error!(hash, height, index, "Failed to parse logs: {e}"))
|
||||
.unwrap_or_default();
|
||||
let events = &chain_tx.tx_result.events;
|
||||
let events = &chain_tx.tx_details.tx_result.events;
|
||||
|
||||
insert_transaction(
|
||||
hash,
|
||||
height,
|
||||
index,
|
||||
chain_tx.tx_result.code.is_ok(),
|
||||
chain_tx.tx_details.tx_result.code.is_ok(),
|
||||
serde_json::Value::Array(messages),
|
||||
chain_tx.tx.body.memo.clone(),
|
||||
chain_tx.tx_details.tx.body.memo.clone(),
|
||||
signatures,
|
||||
serde_json::to_value(signer_infos)?,
|
||||
serde_json::to_value(&chain_tx.tx.auth_info.fee)?,
|
||||
chain_tx.tx_result.gas_wanted,
|
||||
chain_tx.tx_result.gas_used,
|
||||
chain_tx.tx_result.log.clone(),
|
||||
serde_json::to_value(&chain_tx.tx_details.tx.auth_info.fee)?,
|
||||
chain_tx.tx_details.tx_result.gas_wanted,
|
||||
chain_tx.tx_details.tx_result.gas_used,
|
||||
chain_tx.tx_details.tx_result.log.clone(),
|
||||
json!(log),
|
||||
json!(events),
|
||||
self.inner.as_mut(),
|
||||
@@ -207,17 +210,20 @@ impl PostgresStorageTransaction {
|
||||
|
||||
for chain_tx in txs {
|
||||
let involved_addresses = parse_addresses_from_events(chain_tx);
|
||||
for (index, msg) in chain_tx.tx.body.messages.iter().enumerate() {
|
||||
let parsed_message = chain_tx.parsed_messages.get(&index);
|
||||
for (index, msg) in chain_tx.tx_details.tx.body.messages.iter().enumerate() {
|
||||
let parsed_message = chain_tx
|
||||
.decoded_messages
|
||||
.get(&index)
|
||||
.map(|msg| &msg.decoded_content);
|
||||
let value = serde_json::to_value(parsed_message)?;
|
||||
|
||||
insert_message(
|
||||
chain_tx.hash.to_string(),
|
||||
chain_tx.tx_details.hash.to_string(),
|
||||
index as i64,
|
||||
msg.type_url.clone(),
|
||||
value,
|
||||
involved_addresses.clone(),
|
||||
chain_tx.height.into(),
|
||||
chain_tx.tx_details.height().into(),
|
||||
self.inner.as_mut(),
|
||||
)
|
||||
.await?
|
||||
|
||||
@@ -33,9 +33,9 @@ impl TxModule for FancyTxModule {
|
||||
async fn handle_tx(&mut self, tx: &ParsedTransactionResponse) -> Result<(), ScraperError> {
|
||||
println!(
|
||||
"✨ got new tx for height {}: {} ({} msgs)",
|
||||
tx.block.header.height,
|
||||
tx.hash,
|
||||
tx.parsed_messages.len()
|
||||
tx.tx_details.height(),
|
||||
tx.tx_details.hash,
|
||||
tx.tx_details.tx.body.messages.len()
|
||||
);
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -281,7 +281,7 @@ where
|
||||
&mut self,
|
||||
block: BlockToProcess,
|
||||
) -> Result<(), ScraperError> {
|
||||
info!("processing block at height {}", block.height);
|
||||
debug!("processing block at height {}", block.height);
|
||||
|
||||
let full_info = self
|
||||
.rpc_client
|
||||
@@ -291,8 +291,13 @@ where
|
||||
if let Some(tx_info) = &full_info.transactions {
|
||||
debug!("this block has {} transaction(s)", tx_info.len());
|
||||
for tx in tx_info {
|
||||
debug!("{} has {} message(s)", tx.hash, tx.tx.body.messages.len());
|
||||
for (index, msg) in tx.tx.body.messages.iter().enumerate() {
|
||||
let details = &tx.tx_details;
|
||||
debug!(
|
||||
"{} has {} message(s)",
|
||||
details.hash,
|
||||
details.tx.body.messages.len()
|
||||
);
|
||||
for (index, msg) in details.tx.body.messages.iter().enumerate() {
|
||||
debug!("{index}: {:?}", msg.type_url)
|
||||
}
|
||||
}
|
||||
@@ -315,11 +320,24 @@ where
|
||||
for tx_module in &mut self.tx_modules {
|
||||
tx_module.handle_tx(block_tx).await?;
|
||||
}
|
||||
let tx_details = &block_tx.tx_details;
|
||||
|
||||
// the ones concerned with individual messages
|
||||
for (index, msg) in block_tx.tx.body.messages.iter().enumerate() {
|
||||
for (index, msg) in tx_details.tx.body.messages.iter().enumerate() {
|
||||
let Some(decoded) = block_tx.decoded_messages.get(&index) else {
|
||||
warn!(
|
||||
"height: {} tx: {} tx_index: {}, msg_index: {index}: message failed to get decoded",
|
||||
tx_details.height(),
|
||||
tx_details.hash,
|
||||
tx_details.index,
|
||||
);
|
||||
continue;
|
||||
};
|
||||
for msg_module in &mut self.msg_modules {
|
||||
if msg.type_url == msg_module.type_url() {
|
||||
msg_module.handle_msg(index, msg, block_tx).await?
|
||||
msg_module
|
||||
.handle_msg(index, msg, decoded, tx_details)
|
||||
.await?
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,9 +7,16 @@ use tendermint::{Block, Hash, abci, block, tx};
|
||||
use tendermint_rpc::endpoint::{block as block_endpoint, block_results, validators};
|
||||
use tendermint_rpc::event::{Event, EventData};
|
||||
|
||||
// just get all everything out of tx::Response, but parse raw `tx` bytes
|
||||
/// Message decoded from the raw transaction and converted into json.
|
||||
/// Note that it might have gone through additional processing as set by the `MessageRegistry`
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ParsedTransactionResponse {
|
||||
pub struct DecodedMessage {
|
||||
pub type_url: String,
|
||||
pub decoded_content: serde_json::Value,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ParsedTransactionDetails {
|
||||
/// The hash of the transaction.
|
||||
///
|
||||
/// Deserialized from a hex-encoded string (there is a discrepancy between
|
||||
@@ -17,8 +24,6 @@ pub struct ParsedTransactionResponse {
|
||||
/// the Tendermint RPC).
|
||||
pub hash: Hash,
|
||||
|
||||
pub height: block::Height,
|
||||
|
||||
pub index: u32,
|
||||
|
||||
pub tx_result: abci::types::ExecTxResult,
|
||||
@@ -27,13 +32,23 @@ pub struct ParsedTransactionResponse {
|
||||
|
||||
pub proof: Option<tx::Proof>,
|
||||
|
||||
pub parsed_messages: BTreeMap<usize, serde_json::Value>,
|
||||
|
||||
pub parsed_message_urls: BTreeMap<usize, String>,
|
||||
|
||||
pub block: Block,
|
||||
}
|
||||
|
||||
impl ParsedTransactionDetails {
|
||||
pub fn height(&self) -> block::Height {
|
||||
self.block.header.height
|
||||
}
|
||||
}
|
||||
|
||||
// just get all everything out of tx::Response, but parse raw `tx` bytes
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ParsedTransactionResponse {
|
||||
pub tx_details: ParsedTransactionDetails,
|
||||
|
||||
pub decoded_messages: BTreeMap<usize, DecodedMessage>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct FullBlockInformation {
|
||||
/// Basic block information, including its signers.
|
||||
|
||||
@@ -82,10 +82,8 @@ pub enum ScraperError {
|
||||
source: cosmrs::ErrorReport,
|
||||
},
|
||||
|
||||
#[error("could not parse msg in tx {hash} at index {index} into {type_url}: {source}")]
|
||||
#[error("could not parse msg of type {type_url}: {source}")]
|
||||
MsgParseFailure {
|
||||
hash: Hash,
|
||||
index: usize,
|
||||
type_url: String,
|
||||
#[source]
|
||||
source: cosmrs::ErrorReport,
|
||||
|
||||
@@ -47,7 +47,7 @@ pub fn validator_consensus_address(id: account::Id) -> Result<AccountId, Malform
|
||||
}
|
||||
|
||||
pub fn tx_gas_sum(txs: &[ParsedTransactionResponse]) -> i64 {
|
||||
txs.iter().map(|tx| tx.tx_result.gas_used).sum()
|
||||
txs.iter().map(|tx| tx.tx_details.tx_result.gas_used).sum()
|
||||
}
|
||||
|
||||
pub fn validator_info(
|
||||
|
||||
@@ -15,12 +15,14 @@ pub(crate) mod subscriber;
|
||||
pub mod watcher;
|
||||
|
||||
pub use block_processor::pruning::{PruningOptions, PruningStrategy};
|
||||
pub use block_processor::types::ParsedTransactionResponse;
|
||||
pub use block_processor::types::{
|
||||
DecodedMessage, ParsedTransactionDetails, ParsedTransactionResponse,
|
||||
};
|
||||
pub use cosmos_module::{
|
||||
CosmosModule,
|
||||
message_registry::{MessageRegistry, default_message_registry},
|
||||
};
|
||||
pub use cosmrs::Any;
|
||||
pub use modules::{BlockModule, MsgModule, TxModule};
|
||||
pub use modules::{BlockModule, MsgModule, TxModule, parse_msg};
|
||||
pub use scraper::{Config, NyxdScraper, StartingBlockOpts};
|
||||
pub use storage::{NyxdScraperStorage, NyxdScraperTransaction};
|
||||
|
||||
@@ -6,5 +6,5 @@ mod msg_module;
|
||||
mod tx_module;
|
||||
|
||||
pub use block_module::BlockModule;
|
||||
pub use msg_module::MsgModule;
|
||||
pub use msg_module::{MsgModule, parse_msg};
|
||||
pub use tx_module::TxModule;
|
||||
|
||||
@@ -1,11 +1,47 @@
|
||||
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::block_processor::types::ParsedTransactionResponse;
|
||||
use crate::block_processor::types::{DecodedMessage, ParsedTransactionDetails};
|
||||
use crate::error::ScraperError;
|
||||
use async_trait::async_trait;
|
||||
use cosmrs::Any;
|
||||
use cosmrs::tx::Msg;
|
||||
|
||||
/// Parse a protobuf `Any` message into a strongly typed Cosmos message.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// let execute_msg: MsgExecuteContract = parse_msg(msg)?;
|
||||
/// ```
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns `ScraperError::MsgParseFailure` if:
|
||||
/// - The type URL doesn't match the expected type
|
||||
/// - The protobuf bytes are malformed
|
||||
/// - The message schema is incompatible with this version of the code
|
||||
pub fn parse_msg<T: Msg>(msg: &Any) -> Result<T, ScraperError> {
|
||||
T::from_any(msg).map_err(|source| ScraperError::MsgParseFailure {
|
||||
type_url: msg.type_url.clone(),
|
||||
source,
|
||||
})
|
||||
}
|
||||
|
||||
/// Trait for modules that process specific message types from blockchain transactions.
|
||||
///
|
||||
/// # Parameters
|
||||
///
|
||||
/// - `index`: Position of this message within the transaction (0-based)
|
||||
/// - `msg`: Raw protobuf message (use `parse_msg()` to decode)
|
||||
/// - `decoded_msg`: Pre-decoded JSON representation (may be None for unsupported types)
|
||||
/// - `tx`: Transaction details including block height, hash, and execution result
|
||||
///
|
||||
/// # Error Handling
|
||||
///
|
||||
/// - Return `Err` for critical failures that should stop block processing
|
||||
/// - Return `Ok(())` for non-critical errors (e.g., unexpected contract schema)
|
||||
/// - Log warnings for debugging without propagating errors
|
||||
#[async_trait]
|
||||
pub trait MsgModule {
|
||||
fn type_url(&self) -> String;
|
||||
@@ -14,6 +50,7 @@ pub trait MsgModule {
|
||||
&mut self,
|
||||
index: usize,
|
||||
msg: &Any,
|
||||
tx: &ParsedTransactionResponse,
|
||||
decoded_msg: &DecodedMessage,
|
||||
tx: &ParsedTransactionDetails,
|
||||
) -> Result<(), ScraperError>;
|
||||
}
|
||||
|
||||
@@ -2,14 +2,15 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::block_processor::types::{
|
||||
BlockToProcess, FullBlockInformation, ParsedTransactionResponse,
|
||||
BlockToProcess, DecodedMessage, FullBlockInformation, ParsedTransactionResponse,
|
||||
};
|
||||
use crate::error::ScraperError;
|
||||
use crate::helpers::tx_hash;
|
||||
use crate::{Any, MessageRegistry, default_message_registry};
|
||||
use crate::{Any, MessageRegistry, ParsedTransactionDetails, default_message_registry};
|
||||
use futures::StreamExt;
|
||||
use futures::future::join3;
|
||||
use std::collections::BTreeMap;
|
||||
use std::future::Future;
|
||||
use std::sync::Arc;
|
||||
use tendermint::{Block, Hash};
|
||||
use tendermint_rpc::endpoint::{block, block_results, tx, validators};
|
||||
@@ -18,6 +19,38 @@ use tokio::sync::Mutex;
|
||||
use tracing::{debug, instrument, warn};
|
||||
use url::Url;
|
||||
|
||||
const MAX_QUERY_ATTEMPTS: usize = 3;
|
||||
|
||||
/// Runs `op` up to `max_attempts` times (at least once), returning the first success or, on full
|
||||
/// exhaustion, the last error encountered.
|
||||
async fn query_with_retries<F, Fut, T>(mut max_attempts: usize, op: F) -> Result<T, ScraperError>
|
||||
where
|
||||
F: Fn() -> Fut,
|
||||
Fut: Future<Output = Result<T, ScraperError>>,
|
||||
{
|
||||
if max_attempts == 0 {
|
||||
max_attempts = 1;
|
||||
}
|
||||
|
||||
let mut last_err = None;
|
||||
|
||||
for i in 0..max_attempts {
|
||||
match op().await {
|
||||
Ok(result) => return Ok(result),
|
||||
Err(err) => {
|
||||
debug!("query failed, retrying {}/{max_attempts} - {err}", i + 1);
|
||||
last_err = Some(err);
|
||||
}
|
||||
}
|
||||
|
||||
tokio::time::sleep(std::time::Duration::from_millis(300 * (i as u64 + 1))).await;
|
||||
}
|
||||
|
||||
// SAFETY: max_attempts >= 1, so we only reach here after at least one recorded failure
|
||||
#[allow(clippy::unwrap_used)]
|
||||
Err(last_err.unwrap())
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct RetrievalConfig {
|
||||
pub get_validators: bool,
|
||||
@@ -77,8 +110,7 @@ impl RpcClient {
|
||||
) -> Result<Vec<ParsedTransactionResponse>, ScraperError> {
|
||||
let mut transactions = Vec::with_capacity(raw_transactions.len());
|
||||
for raw_tx in raw_transactions {
|
||||
let mut parsed_messages = BTreeMap::new();
|
||||
let mut parsed_message_urls = BTreeMap::new();
|
||||
let mut decoded_messages = BTreeMap::new();
|
||||
let tx = cosmrs::Tx::from_bytes(&raw_tx.tx).map_err(|source| {
|
||||
ScraperError::TxParseFailure {
|
||||
hash: raw_tx.hash,
|
||||
@@ -87,22 +119,27 @@ impl RpcClient {
|
||||
})?;
|
||||
|
||||
for (index, msg) in tx.body.messages.iter().enumerate() {
|
||||
if let Some(value) = self.decode_or_skip(msg) {
|
||||
parsed_messages.insert(index, value);
|
||||
parsed_message_urls.insert(index, msg.type_url.clone());
|
||||
if let Some(decoded_content) = self.decode_or_skip(msg) {
|
||||
decoded_messages.insert(
|
||||
index,
|
||||
DecodedMessage {
|
||||
type_url: msg.type_url.clone(),
|
||||
decoded_content,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
transactions.push(ParsedTransactionResponse {
|
||||
hash: raw_tx.hash,
|
||||
height: raw_tx.height,
|
||||
index: raw_tx.index,
|
||||
tx_result: raw_tx.tx_result,
|
||||
tx,
|
||||
proof: raw_tx.proof,
|
||||
parsed_messages,
|
||||
parsed_message_urls,
|
||||
block: block.clone(),
|
||||
tx_details: ParsedTransactionDetails {
|
||||
hash: raw_tx.hash,
|
||||
index: raw_tx.index,
|
||||
tx_result: raw_tx.tx_result,
|
||||
tx,
|
||||
proof: raw_tx.proof,
|
||||
block: block.clone(),
|
||||
},
|
||||
decoded_messages,
|
||||
})
|
||||
}
|
||||
Ok(transactions)
|
||||
@@ -169,13 +206,24 @@ impl RpcClient {
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument(skip(self), err(Display))]
|
||||
async fn get_block_results_with_retries(
|
||||
&self,
|
||||
height: u32,
|
||||
max_attempts: usize,
|
||||
) -> Result<block_results::Response, ScraperError> {
|
||||
query_with_retries(max_attempts, || self.get_block_results(height)).await
|
||||
}
|
||||
|
||||
async fn maybe_get_block_results(
|
||||
&self,
|
||||
height: u32,
|
||||
retrieve: bool,
|
||||
) -> Result<Option<block_results::Response>, ScraperError> {
|
||||
if retrieve {
|
||||
self.get_block_results(height).await.map(Some)
|
||||
self.get_block_results_with_retries(height, MAX_QUERY_ATTEMPTS)
|
||||
.await
|
||||
.map(Some)
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
@@ -215,8 +263,6 @@ impl RpcClient {
|
||||
|
||||
// "Data is just a wrapper for a list of transactions, where transactions are arbitrary byte arrays"
|
||||
// source: https://github.com/tendermint/spec/blob/d46cd7f573a2c6a2399fcab2cde981330aa63f37/spec/core/data_structures.md#data
|
||||
//
|
||||
// I hate that zip as much as you, dear reader, but for some reason the compiler didn't let me remove the `move`
|
||||
futures::stream::iter(
|
||||
raw.iter()
|
||||
.map(tx_hash)
|
||||
@@ -224,12 +270,14 @@ impl RpcClient {
|
||||
.zip(std::iter::repeat(ordered_results.clone())),
|
||||
)
|
||||
.for_each_concurrent(4, |((id, tx_hash), ordered_results)| async move {
|
||||
let res = self.get_transaction_result(tx_hash).await;
|
||||
let res = self
|
||||
.get_transaction_result_with_retries(tx_hash, MAX_QUERY_ATTEMPTS)
|
||||
.await;
|
||||
ordered_results.lock().await.insert(id, res);
|
||||
})
|
||||
.await;
|
||||
|
||||
// safety the futures have completed so we MUST have the only arc reference
|
||||
// safety: the futures have completed so we MUST have the only arc reference
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let inner = Arc::into_inner(ordered_results).unwrap().into_inner();
|
||||
|
||||
@@ -262,6 +310,15 @@ impl RpcClient {
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument(skip(self, tx_hash), fields(tx_hash = %tx_hash), err(Display))]
|
||||
async fn get_transaction_result_with_retries(
|
||||
&self,
|
||||
tx_hash: Hash,
|
||||
max_attempts: usize,
|
||||
) -> Result<tx::Response, ScraperError> {
|
||||
query_with_retries(max_attempts, || self.get_transaction_result(tx_hash)).await
|
||||
}
|
||||
|
||||
#[instrument(skip(self))]
|
||||
pub async fn get_validators_details(
|
||||
&self,
|
||||
@@ -278,13 +335,24 @@ impl RpcClient {
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument(skip(self), err(Display))]
|
||||
async fn get_validators_details_with_retries(
|
||||
&self,
|
||||
height: u32,
|
||||
max_attempts: usize,
|
||||
) -> Result<validators::Response, ScraperError> {
|
||||
query_with_retries(max_attempts, || self.get_validators_details(height)).await
|
||||
}
|
||||
|
||||
async fn maybe_get_validators_details(
|
||||
&self,
|
||||
height: u32,
|
||||
retrieve: bool,
|
||||
) -> Result<Option<validators::Response>, ScraperError> {
|
||||
if retrieve {
|
||||
self.get_validators_details(height).await.map(Some)
|
||||
self.get_validators_details_with_retries(height, MAX_QUERY_ATTEMPTS)
|
||||
.await
|
||||
.map(Some)
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user