Compare commits

..

3 Commits

Author SHA1 Message Date
Jędrzej Stuczyński 944fc27ef6 add additional leniency in ticketbook requests 2026-05-28 11:05:24 +01:00
Jędrzej Stuczyński 3853c0f0c9 reduce concurrency during quorum check tests 2026-05-27 16:36:13 +01:00
Jędrzej Stuczyński 97f79381b9 NS: don't return nodes with 0 performance 2026-05-27 14:36:02 +01:00
1272 changed files with 74614 additions and 91765 deletions
-2
View File
@@ -1,2 +0,0 @@
[target.wasm32-unknown-unknown]
rustflags = ["--cfg=getrandom_backend=\"wasm_js\""]
+2 -2
View File
@@ -25,14 +25,14 @@ jobs:
echo "file2=$(ls nym-vpn*.deb)" >> $GITHUB_ENV
- name: Upload nym-repo-setup
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@v6
with:
name: ${{ env.file1 }}
path: ppa/packages/nym-repo-setup*.deb
retention-days: 10
- name: Upload nym-vpn
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@v6
with:
name: ${{ env.file2 }}
path: ppa/packages/nym-vpn*.deb
+3 -6
View File
@@ -21,12 +21,12 @@ jobs:
run: sudo apt-get install -y rsync
- uses: rlespinasse/github-slug-action@v3.x
- name: Setup pnpm
uses: pnpm/action-setup@v5.0.0
uses: pnpm/action-setup@v4.2.0
with:
version: 11.1.2
version: 9
- uses: actions/setup-node@v4
with:
node-version: 24
node-version: 20
- name: Install Rust toolchain
uses: actions-rs/toolchain@v1
with:
@@ -37,9 +37,6 @@ jobs:
command: build
args: --workspace --release
- name: Verify doc versions
run: ${{ github.workspace }}/documentation/scripts/verify-doc-versions.sh
working-directory: ${{ github.workspace }}
- name: Install project dependencies
run: pnpm i
- name: Generate llms-full.txt
+4 -7
View File
@@ -17,16 +17,13 @@ jobs:
run: sudo apt-get install rsync
continue-on-error: true
- uses: rlespinasse/github-slug-action@v3.x
- name: Setup pnpm
uses: pnpm/action-setup@v5.0.0
with:
version: 11.1.2
- uses: actions/setup-node@v4
with:
node-version: 24
cache: pnpm
node-version: 20
- name: Setup yarn
run: npm install -g yarn
- name: Build
run: pnpm install && pnpm build && pnpm build:ci:storybook
run: yarn && yarn build && yarn build:ci:storybook
- name: Deploy branch to CI www (storybook)
continue-on-error: true
uses: easingthemes/ssh-deploy@main
@@ -110,7 +110,7 @@ jobs:
- name: Upload Artifact
if: github.event_name == 'workflow_dispatch'
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@v6
with:
name: nym-binaries-artifacts
path: |
@@ -1,63 +0,0 @@
name: ci-build-upload-network-monitor-agent
on:
workflow_dispatch:
jobs:
build-and-upload:
strategy:
fail-fast: false
matrix:
platform: [arc-ubuntu-22.04]
runs-on: ${{ matrix.platform }}
env:
CARGO_TERM_COLOR: always
RUSTUP_PERMIT_COPY_RENAME: 1
steps:
- uses: actions/checkout@v6
- name: Prepare build output directory
shell: bash
env:
OUTPUT_DIR: ci-builds/${{ github.ref_name }}
run: |
rm -rf ci-builds || true
mkdir -p "$OUTPUT_DIR"
- name: Install Dependencies (Linux)
run: sudo apt-get update && sudo apt-get -y install libudev-dev
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@master
with:
toolchain: ${{ vars.REQUIRED_RUSTC_VERSION }}
- name: Build nym-network-monitor-agent
shell: bash
run: cargo build -p nym-network-monitor-agent --release
- name: Upload artifact
uses: actions/upload-artifact@v6
with:
name: nym-network-monitor-agent
path: target/release/nym-network-monitor-agent
retention-days: 30
- name: Prepare build output
shell: bash
env:
OUTPUT_DIR: ci-builds/${{ github.ref_name }}
run: cp target/release/nym-network-monitor-agent "$OUTPUT_DIR"
- name: Deploy to CI www
uses: easingthemes/ssh-deploy@main
env:
SSH_PRIVATE_KEY: ${{ secrets.CI_WWW_SSH_PRIVATE_KEY }}
ARGS: "-avzr"
SOURCE: "ci-builds/"
REMOTE_HOST: ${{ secrets.CI_WWW_REMOTE_HOST }}
REMOTE_USER: ${{ secrets.CI_WWW_REMOTE_USER }}
TARGET: ${{ secrets.CI_WWW_REMOTE_TARGET }}/builds/
EXCLUDE: "/dist/, /node_modules/"
+1
View File
@@ -23,6 +23,7 @@ on:
- 'sdk/ffi/**'
- 'sdk/rust/**'
- 'service-providers/**'
- 'nym-browser-extension/storage/**'
- 'tools/**'
- 'wasm/**'
- 'Cargo.toml'
-19
View File
@@ -1,19 +0,0 @@
name: ci-crates-preflight
on:
workflow_dispatch:
pull_request:
paths:
- 'Cargo.toml'
- '**/Cargo.toml'
- 'tools/internal/check_publish_preflight.py'
- '.github/workflows/ci-crates-preflight.yml'
jobs:
preflight:
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v6
- name: Preflight publish checks
run: python3 tools/internal/check_publish_preflight.py
@@ -40,7 +40,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 24
node-version: "20"
- name: Validate version format
run: |
@@ -57,8 +57,7 @@ jobs:
- name: Update workspace dependencies
run: |
# Match any semver version on lines with `path = `, not just the current workspace version.
sed -i '/path = /s/version = "[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*"/version = "${{ inputs.version }}"/g' Cargo.toml
sed -i '/path = /s/version = "${{ steps.current_version.outputs.version }}"/version = "${{ inputs.version }}"/g' Cargo.toml
- name: Bump versions (local only)
run: |
+1 -5
View File
@@ -33,11 +33,7 @@ jobs:
- name: Install cargo-workspaces
run: cargo install cargo-workspaces
- name: Preflight publish checks
run: |
python3 tools/internal/check_publish_preflight.py
# --publish-as-is skips version bumping since that's done in a separate CI job.
# `--publish-as-is` skips version bumping since that's done in a separate CI job.
- name: Publish
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
+3 -30
View File
@@ -19,7 +19,6 @@ jobs:
RUSTUP_PERMIT_COPY_RENAME: 1
permissions:
contents: write
pull-requests: write
steps:
- name: Checkout repo
uses: actions/checkout@v6
@@ -42,7 +41,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 24
node-version: "20"
- name: Validate version format
run: |
@@ -59,9 +58,7 @@ jobs:
- name: Update workspace dependencies
run: |
# Match any semver version on lines with `path = `, not just the current workspace version.
# This catches entries whose version has drifted (e.g. nym-sqlx-pool-guard at 1.2.0).
sed -i '/path = /s/version = "[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*"/version = "${{ inputs.version }}"/g' Cargo.toml
sed -i '/path = /s/version = "${{ steps.current_version.outputs.version }}"/version = "${{ inputs.version }}"/g' Cargo.toml
- name: Bump versions
run: |
@@ -71,33 +68,9 @@ jobs:
- name: Commit and push version bump
run: |
set -euo pipefail
BASE_BRANCH="${GITHUB_REF_NAME}"
PR_BRANCH="ci/crates-version-bump-${{ inputs.version }}-${GITHUB_RUN_ID}"
git checkout -b "$PR_BRANCH"
git add -A
git commit -m "crates release: bump version to ${{ inputs.version }}"
git push -u origin "$PR_BRANCH"
cat > /tmp/crates-version-bump-pr-body.md <<'EOF'
This PR was created by CI because direct pushes to the release branch are blocked by branch protection rules.
## Summary
- Bump workspace crate versions to the requested release version.
- Update workspace dependency versions accordingly.
## Notes
- Merge this PR to proceed with crates.io publishing.
EOF
gh pr create \
--base "$BASE_BRANCH" \
--head "$PR_BRANCH" \
--title "crates release: bump version to ${{ inputs.version }}" \
--body-file /tmp/crates-version-bump-pr-body.md
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
git push
- name: Show package versions
run: cargo workspaces list --long
+6 -15
View File
@@ -7,10 +7,7 @@ on:
paths:
- "documentation/docs/**"
- "sdk/typescript/packages/sdk/src/**"
- "sdk/typescript/packages/mix-tunnel/src/**"
- "sdk/typescript/packages/mix-fetch/src/**"
- "sdk/typescript/packages/mix-dns/src/**"
- "sdk/typescript/packages/mix-websocket/src/**"
- ".github/workflows/ci-docs.yml"
jobs:
@@ -31,12 +28,12 @@ jobs:
run: sudo apt-get install -y rsync
- uses: rlespinasse/github-slug-action@v3.x
- name: Setup pnpm
uses: pnpm/action-setup@v5.0.0
uses: pnpm/action-setup@v4.2.0
with:
version: 11.1.2
version: 9
- uses: actions/setup-node@v4
with:
node-version: 24
node-version: 20
- name: Install Rust toolchain
uses: actions-rs/toolchain@v1
with:
@@ -50,7 +47,7 @@ jobs:
- name: Check if TypeScript SDK source changed
id: check-ts-sdk
run: |
if git diff --name-only ${{ github.event.before }} ${{ github.sha }} | grep -qE '^sdk/typescript/packages/(sdk|mix-tunnel|mix-fetch|mix-dns|mix-websocket)/src/'; then
if git diff --name-only ${{ github.event.before }} ${{ github.sha }} | grep -qE '^sdk/typescript/packages/(sdk|mix-fetch)/src/'; then
echo "changed=true" >> $GITHUB_OUTPUT
else
echo "changed=false" >> $GITHUB_OUTPUT
@@ -61,15 +58,9 @@ jobs:
if: steps.check-ts-sdk.outputs.changed == 'true'
run: |
npm install -g typedoc@0.25.13 typedoc-plugin-markdown@4.0.3
cd ${{ github.workspace }}/sdk/typescript/packages/sdk && typedoc --skipErrorChecking
cd ${{ github.workspace }}/sdk/typescript/packages/mix-tunnel && typedoc --skipErrorChecking
cd ${{ github.workspace }}/sdk/typescript/packages/mix-fetch && typedoc --skipErrorChecking
cd ${{ github.workspace }}/sdk/typescript/packages/mix-dns && typedoc --skipErrorChecking
cd ${{ github.workspace }}/sdk/typescript/packages/mix-websocket && typedoc --skipErrorChecking
cd ${{ github.workspace }}/sdk/typescript/packages/sdk && typedoc --skipErrorChecking
cd ${{ github.workspace }}/sdk/typescript/packages/mix-fetch && typedoc --skipErrorChecking
- name: Verify doc versions
run: ${{ github.workspace }}/documentation/scripts/verify-doc-versions.sh
working-directory: ${{ github.workspace }}
- name: Install project dependencies
run: pnpm i
- name: Generate llms-full.txt
+13 -17
View File
@@ -20,14 +20,12 @@ jobs:
- uses: actions/checkout@v6
- uses: rlespinasse/github-slug-action@v3.x
- name: Setup pnpm
uses: pnpm/action-setup@v5.0.0
with:
version: 11.1.2
- uses: actions/setup-node@v4
with:
node-version: 24
cache: pnpm
node-version: 20
- name: Setup yarn
run: npm install -g yarn
- name: Install Rust toolchain
uses: actions-rs/toolchain@v1
@@ -40,24 +38,22 @@ jobs:
- name: Install wasm-opt
run: cargo install wasm-opt
# Produce wasm/smolmix/pkg/package.json before any pnpm step. The
# `pnpm dev:on` in `prebuild:ci` adds wasm/smolmix/pkg to the dynamic
# workspace; mix-tunnel's `workspace:*` lookup against @nymproject/
# smolmix-wasm needs the package.json to be present.
- name: Build smolmix wasm
run: make -C wasm/smolmix
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version: "1.24.6"
- name: Install
run: pnpm i
run: yarn
- name: Build packages
run: pnpm build:ci
run: yarn build:ci
- name: Install again
run: pnpm i
run: yarn
- name: Lint
run: pnpm lint
run: yarn lint
- name: Typecheck with tsc
run: pnpm tsc
run: yarn tsc
+10 -15
View File
@@ -1,7 +1,6 @@
name: ci-nym-wallet-frontend
on:
workflow_dispatch:
pull_request:
paths:
- 'nym-wallet/**'
@@ -13,34 +12,30 @@ jobs:
steps:
- uses: actions/checkout@v6
- name: Setup pnpm
uses: pnpm/action-setup@v5.0.0
with:
version: 11.1.2
- uses: actions/setup-node@v4
with:
node-version-file: nym-wallet/.nvmrc
cache: pnpm
cache: yarn
cache-dependency-path: yarn.lock
- name: Install dependencies
run: pnpm install
run: yarn install --network-timeout 100000
- name: Build TypeScript packages (wallet depends on @nymproject/types, etc.)
run: pnpm build:types
run: yarn build:types
- name: Build @nymproject/mui-theme and @nymproject/react (wallet imports subpaths)
run: pnpm build:packages
run: yarn build:packages
- name: Typecheck nym-wallet
run: pnpm --filter @nymproject/nym-wallet-app tsc
run: yarn --cwd nym-wallet tsc
- name: Lint nym-wallet
run: pnpm --filter @nymproject/nym-wallet-app lint
run: yarn --cwd nym-wallet lint
- name: pnpm audit (workspace lockfile; informational)
run: pnpm audit --audit-level critical
- name: Yarn audit (workspace lockfile; informational)
run: yarn audit --level critical
continue-on-error: true
- name: Unit tests (nym-wallet)
run: pnpm --filter @nymproject/nym-wallet-app test
run: yarn --cwd nym-wallet test
+6 -1
View File
@@ -20,7 +20,7 @@ jobs:
- uses: actions/setup-node@v4
with:
node-version: 24
node-version: 20
- uses: actions-rs/toolchain@v1
with:
@@ -30,6 +30,11 @@ jobs:
override: true
components: rustfmt, clippy
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version: "1.24.6"
- name: Install wasm-pack
run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
+1 -1
View File
@@ -20,7 +20,7 @@ jobs:
find . -name Cargo.toml -exec cargo deny --manifest-path {} check \
advisories -A advisory-not-detected --hide-inclusion-graph \; &> \
>(uniq &> .github/workflows/support-files/notifications/deny.message )
- uses: actions/upload-artifact@v7
- uses: actions/upload-artifact@v6
with:
name: report
path: .github/workflows/support-files/notifications/deny.message
+1 -1
View File
@@ -66,7 +66,7 @@ jobs:
args: --workspace --release ${{ env.CARGO_FEATURES }}
- name: Upload Artifact
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@v6
with:
name: my-artifact
path: |
+2 -2
View File
@@ -27,14 +27,14 @@ jobs:
run: make contracts
- name: Upload Mixnet Contract Artifact
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@v6
with:
name: mixnet_contract.wasm
path: contracts/target/wasm32-unknown-unknown/release/mixnet_contract.wasm
retention-days: 5
- name: Upload Vesting Contract Artifact
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@v6
with:
name: vesting_contract.wasm
path: contracts/target/wasm32-unknown-unknown/release/vesting_contract.wasm
+10 -13
View File
@@ -23,13 +23,10 @@ jobs:
steps:
- uses: actions/checkout@v6
- name: Setup pnpm
uses: pnpm/action-setup@v5.0.0
- name: Node
uses: actions/setup-node@v4
with:
version: 11.1.2
- uses: actions/setup-node@v4
with:
node-version: 24
node-version: 22.13.0
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
@@ -71,17 +68,17 @@ jobs:
fileName: '.env'
encodedString: ${{ secrets.WALLET_ADMIN_ADDRESS }}
- name: pnpm cache clean
- name: Yarn cache clean
shell: bash
run: cd .. && pnpm cache delete
run: cd .. && yarn cache clean
- name: Install project dependencies
shell: bash
run: cd .. && pnpm i
run: cd .. && yarn --network-timeout 100000
- name: Build
- name: Yarn build
shell: bash
run: cd .. && pnpm build
run: cd .. && yarn build
- name: Install dependencies and build it
env:
@@ -100,7 +97,7 @@ jobs:
TAURI_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
TAURI_NOTARIZATION_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
run: |
pnpm build-macx86
yarn build-macx86
- name: Create app tarball
run: |
@@ -111,7 +108,7 @@ jobs:
cd -
- name: Upload Artifact
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@v6
with:
name: nym-wallet.app.tar.gz
path: nym-wallet/target/x86_64-apple-darwin/release/bundle/macos/nym-wallet.app.tar.gz
@@ -26,17 +26,12 @@ jobs:
libwebkit2gtk-4.1-dev build-essential curl wget libssl-dev jq \
libgtk-3-dev squashfs-tools libayatana-appindicator3-dev make libfuse2 unzip librsvg2-dev file \
libsoup-3.0-dev libjavascriptcoregtk-4.1-dev
- name: Setup pnpm
uses: pnpm/action-setup@v5.0.0
with:
version: 11.1.2
- name: Node
uses: actions/setup-node@v4
with:
node-version: 24
cache: 'pnpm'
node-version: 22.13.0
cache: 'yarn'
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
@@ -45,10 +40,10 @@ jobs:
- name: Install project dependencies
shell: bash
run: cd .. && pnpm i
run: cd .. && yarn --network-timeout 100000
- name: Install app dependencies
run: pnpm i
run: yarn
- name: Create env file
uses: timheuer/base64-to-file@v1.2
@@ -57,7 +52,7 @@ jobs:
encodedString: ${{ secrets.WALLET_ADMIN_ADDRESS }}
- name: Build app
run: pnpm build
run: yarn build
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
@@ -137,7 +132,7 @@ jobs:
fi
- name: Upload Artifact
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@v6
with:
name: nym-wallet-appimage.tar.gz
path: |
+14 -11
View File
@@ -38,15 +38,18 @@ jobs:
toolchain: ${{ vars.REQUIRED_RUSTC_VERSION }}
- name: Setup MSBuild.exe
uses: microsoft/setup-msbuild@v3
uses: microsoft/setup-msbuild@v2
- name: Setup pnpm
uses: pnpm/action-setup@v5.0.0
# No cache:yarn here: setup-node needs yarn on PATH to populate the cache, but this runner
# only gets yarn from the step below.
- name: Node
uses: actions/setup-node@v4
with:
version: 11.1.2
- uses: actions/setup-node@v4
with:
node-version: 24
node-version: 22.13.0
- name: Install Yarn (classic)
shell: bash
run: npm install -g yarn@1.22.22
- name: Strip Authenticode thumbprint (avoid signtool on runner)
working-directory: nym-wallet/src-tauri
@@ -115,11 +118,11 @@ jobs:
' tauri.conf.json
- name: Install project dependencies
shell: bash
run: cd .. && pnpm i
run: cd .. && yarn --network-timeout 100000
- name: Install app dependencies
shell: bash
run: pnpm i
run: yarn --network-timeout 100000
- name: Build and sign it
shell: bash
@@ -133,7 +136,7 @@ jobs:
SSL_COM_TOTP_SECRET: ${{ env.SIGN_WINDOWS == 'true' && secrets.SSL_COM_TOTP_SECRET }}
run: |
echo "Starting build process..."
pnpm build
yarn build
- name: Check bundle directory
shell: bash
@@ -162,7 +165,7 @@ jobs:
find . -name "*.msi" -type f
- name: Upload Artifact
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@v6
with:
name: nym-wallet.msi
path: |
@@ -76,7 +76,7 @@ jobs:
apk/nyms5-arch64-release.apk
- name: Upload APKs
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@v6
with:
name: nyms5-apk-arch64
path: |
@@ -91,7 +91,7 @@ jobs:
- name: Checkout
uses: actions/checkout@v6
- name: Download binary artifact
uses: actions/download-artifact@v8
uses: actions/download-artifact@v7
with:
name: nyms5-apk-arch64
path: apk
+14 -23
View File
@@ -1,19 +1,6 @@
name: publish-sdk-npm
on:
workflow_dispatch:
inputs:
dry_run:
description: "Rehearse the publish (pnpm publish --dry-run, no tarballs uploaded). Untick to publish for real."
type: boolean
default: true
dist_tag:
description: "Tag mode. 'auto' picks per package: new packages and same-major releases -> latest; a breaking major (e.g. mix-fetch v2 over v1) -> next, promote later with `npm dist-tag add`. 'next'/'latest' force that tag on all four."
type: choice
options:
- auto
- next
- latest
default: auto
jobs:
publish:
@@ -21,17 +8,15 @@ jobs:
steps:
- uses: actions/checkout@v6
- name: Setup pnpm
uses: pnpm/action-setup@v5.0.0
with:
version: 11.1.2
- name: Install Node
uses: actions/setup-node@v4
with:
node-version: 24
node-version: 20
registry-url: "https://registry.npmjs.org"
- name: Setup yarn
run: npm install -g yarn
- name: Install rust toolchain
uses: actions-rs/toolchain@v1
with:
@@ -46,15 +31,21 @@ jobs:
- name: Install wasm-opt
run: cargo install wasm-opt
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version: "1.24.6"
- name: Update root CA certificate bundle
run: ./wasm/mix-fetch/go-mix-conn/scripts/update-root-certs.sh
- name: Install dependencies
run: pnpm i
run: yarn
- name: Build WASM and Typescript SDK
run: pnpm sdk:build
run: yarn sdk:build
- name: Publish to NPM
env:
NODE_AUTH_TOKEN: ${{ secrets.NODE_AUTH_TOKEN }}
DRY_RUN: ${{ inputs.dry_run && '1' || '0' }}
NPM_DIST_TAG: ${{ inputs.dist_tag }}
run: ./sdk/typescript/scripts/publish.sh
+1 -1
View File
@@ -11,7 +11,7 @@ jobs:
runs-on: arc-linux-latest-dind
steps:
- name: Login to Harbor
uses: docker/login-action@v4
uses: docker/login-action@v3
with:
registry: harbor.nymte.ch
username: ${{ secrets.HARBOR_ROBOT_USERNAME }}
+1 -1
View File
@@ -11,7 +11,7 @@ jobs:
runs-on: arc-linux-latest-dind
steps:
- name: Login to Harbor
uses: docker/login-action@v4
uses: docker/login-action@v3
with:
registry: harbor.nymte.ch
username: ${{ secrets.HARBOR_ROBOT_USERNAME }}
+1 -1
View File
@@ -11,7 +11,7 @@ jobs:
runs-on: arc-ubuntu-22.04-dind
steps:
- name: Login to Harbor
uses: docker/login-action@v4
uses: docker/login-action@v3
with:
registry: harbor.nymte.ch
username: ${{ secrets.HARBOR_ROBOT_USERNAME }}
@@ -18,7 +18,7 @@ jobs:
runs-on: arc-linux-latest-dind
steps:
- name: Login to Harbor
uses: docker/login-action@v4
uses: docker/login-action@v3
with:
registry: harbor.nymte.ch
username: ${{ secrets.HARBOR_ROBOT_USERNAME }}
+1 -1
View File
@@ -17,7 +17,7 @@ jobs:
runs-on: arc-linux-latest-dind
steps:
- name: Login to Harbor
uses: docker/login-action@v4
uses: docker/login-action@v3
with:
registry: harbor.nymte.ch
username: ${{ secrets.HARBOR_ROBOT_USERNAME }}
+1 -1
View File
@@ -11,7 +11,7 @@ jobs:
runs-on: arc-ubuntu-22.04-dind
steps:
- name: Login to Harbor
uses: docker/login-action@v4
uses: docker/login-action@v3
with:
registry: harbor.nymte.ch
username: ${{ secrets.HARBOR_ROBOT_USERNAME }}
+1 -1
View File
@@ -11,7 +11,7 @@ jobs:
runs-on: arc-ubuntu-22.04-dind
steps:
- name: Login to Harbor
uses: docker/login-action@v4
uses: docker/login-action@v3
with:
registry: harbor.nymte.ch
username: ${{ secrets.HARBOR_ROBOT_USERNAME }}
@@ -11,7 +11,7 @@ jobs:
runs-on: arc-linux-latest-dind
steps:
- name: Login to Harbor
uses: docker/login-action@v4
uses: docker/login-action@v3
with:
registry: harbor.nymte.ch
username: ${{ secrets.HARBOR_ROBOT_USERNAME }}
@@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Login to Harbor
uses: docker/login-action@v4
uses: docker/login-action@v3
with:
registry: harbor.nymte.ch
username: ${{ secrets.HARBOR_ROBOT_USERNAME }}
@@ -11,7 +11,7 @@ jobs:
runs-on: arc-ubuntu-22.04-dind
steps:
- name: Login to Harbor
uses: docker/login-action@v4
uses: docker/login-action@v3
with:
registry: harbor.nymte.ch
username: ${{ secrets.HARBOR_ROBOT_USERNAME }}
+2 -2
View File
@@ -23,14 +23,14 @@ jobs:
uses: actions/checkout@v6
- uses: actions/setup-node@v4
with:
node-version: 24
node-version: 20
- uses: nymtech/nym/.github/actions/nym-hash-releases@develop
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
release-tag-or-name-or-id: ${{ inputs.release_tag }}
- uses: actions/upload-artifact@v7
- uses: actions/upload-artifact@v6
with:
name: Asset Hashes
path: hashes.json
-8
View File
@@ -78,11 +78,3 @@ CLAUDE.md
/notes
/target-otel
test-tutorials/
# pnpm
.pnpm-store/
tmp/
# operator tools
scripts/nym-node-setup/auto-bond/nodes.csv
-9
View File
@@ -1,9 +0,0 @@
shamefully-hoist=false
prefer-workspace-packages=true
hoist-pattern[]=*eslint*
hoist-pattern[]=*prettier*
hoist-pattern[]=*typescript*
hoist-pattern[]=*@types*
auto-install-peers=true
strict-peer-dependencies=false
-88
View File
@@ -4,94 +4,6 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
## [Unreleased]
## [2026.11-xynomizithra] (2026-06-08)
- bugfix: allow re-inviting expired members ([#6863])
- feat: disable Nagle's algorithm for LP between nym-nodes ([#6857])
- Keep peer in wg table when updating psk ([#6856])
- chore: minor nym-node improvements ([#6850])
- chore: LP registration adjustments ([#6845])
- crates release: bump version to 1.21.1 ([#6844])
- fix gateways being penalised for no stress testing ([#6843])
- fix score inflation for throttled nodes ([#6842])
- Bugfix/cherry pick/waterloo stres testing floats ([#6841])
- bugfix: NMv3 race condition ([#6837])
- feat: implement UpdateFamily for the node families contract ([#6834])
- Bugfix/cherry pick/waterloo ns api ([#6833])
- experiment: attempt to retroactively generate specs for node families and ecash contracts ([#6813])
- moving lp packets in lp-data crate ([#6810])
- upgrade axum to 0.8.9 (and side deps) ([#6808])
- chore: expose admin method for migrating vesting delegations/mixnodes ([#6795])
- [chore] fix clippy 1.95 lints for future version update ([#6794])
- Handle Rate Limit Challenge Response ([#6786])
- NYM-583: Avoid corrupted database on Windows. ([#6785])
- Max/smolmix wasm ([#6784])
- Chore/bugfixes ([#6783])
- Switch from yarn to pnpm ([#6779])
- feat: Node Families: expose stake information inside DVpnGateway ([#6778])
- feat: Node Families: expose family information for NS API consumers ([#6777])
- feat: Node Families: cache and expose family data within nym API ([#6774])
- Re-order default API urls for network details ([#6767])
- add ci for NM agent binary ([#6764])
- feat/refactor: introduce shared contract caches within Nym API ([#6760])
- chore: removed dead code for redundant mixnet-vesting integration tests ([#6759])
- feat: Node Families: remove nodes upon unbonding ([#6752])
- feat: Node Families: contract transactions ([#6750])
- feat: Node Families: contract queries ([#6731])
- feat: Node Families: initial contract storage ([#6717])
- start node families topic branch ([#6715])
- Bump rand from 0.8.5 to 0.8.6 in /contracts ([#6702])
- Testing port checks in NS Agents ([#6694])
- build(deps): bump microsoft/setup-msbuild from 2 to 3 ([#6602])
- build(deps): bump tar from 0.4.44 to 0.4.45 ([#6595])
- build(deps): bump quinn-proto from 0.11.12 to 0.11.14 ([#6549])
- build(deps): bump docker/login-action from 3 to 4 ([#6518])
- build(deps): bump actions/download-artifact from 7 to 8 ([#6497])
- build(deps): bump actions/upload-artifact from 6 to 7 ([#6496])
[#6863]: https://github.com/nymtech/nym/pull/6863
[#6857]: https://github.com/nymtech/nym/pull/6857
[#6856]: https://github.com/nymtech/nym/pull/6856
[#6850]: https://github.com/nymtech/nym/pull/6850
[#6845]: https://github.com/nymtech/nym/pull/6845
[#6844]: https://github.com/nymtech/nym/pull/6844
[#6843]: https://github.com/nymtech/nym/pull/6843
[#6842]: https://github.com/nymtech/nym/pull/6842
[#6841]: https://github.com/nymtech/nym/pull/6841
[#6837]: https://github.com/nymtech/nym/pull/6837
[#6834]: https://github.com/nymtech/nym/pull/6834
[#6833]: https://github.com/nymtech/nym/pull/6833
[#6813]: https://github.com/nymtech/nym/pull/6813
[#6810]: https://github.com/nymtech/nym/pull/6810
[#6808]: https://github.com/nymtech/nym/pull/6808
[#6795]: https://github.com/nymtech/nym/pull/6795
[#6794]: https://github.com/nymtech/nym/pull/6794
[#6786]: https://github.com/nymtech/nym/pull/6786
[#6785]: https://github.com/nymtech/nym/pull/6785
[#6784]: https://github.com/nymtech/nym/pull/6784
[#6783]: https://github.com/nymtech/nym/pull/6783
[#6779]: https://github.com/nymtech/nym/pull/6779
[#6778]: https://github.com/nymtech/nym/pull/6778
[#6777]: https://github.com/nymtech/nym/pull/6777
[#6774]: https://github.com/nymtech/nym/pull/6774
[#6767]: https://github.com/nymtech/nym/pull/6767
[#6764]: https://github.com/nymtech/nym/pull/6764
[#6760]: https://github.com/nymtech/nym/pull/6760
[#6759]: https://github.com/nymtech/nym/pull/6759
[#6752]: https://github.com/nymtech/nym/pull/6752
[#6750]: https://github.com/nymtech/nym/pull/6750
[#6731]: https://github.com/nymtech/nym/pull/6731
[#6717]: https://github.com/nymtech/nym/pull/6717
[#6715]: https://github.com/nymtech/nym/pull/6715
[#6702]: https://github.com/nymtech/nym/pull/6702
[#6694]: https://github.com/nymtech/nym/pull/6694
[#6602]: https://github.com/nymtech/nym/pull/6602
[#6595]: https://github.com/nymtech/nym/pull/6595
[#6549]: https://github.com/nymtech/nym/pull/6549
[#6518]: https://github.com/nymtech/nym/pull/6518
[#6497]: https://github.com/nymtech/nym/pull/6497
[#6496]: https://github.com/nymtech/nym/pull/6496
## [2026.10-waterloo] (2026-05-27)
- Re-order default API urls for network details - Waterloo release ([#6799])
Generated
+2171 -2294
View File
File diff suppressed because it is too large Load Diff
+153 -151
View File
@@ -31,6 +31,7 @@ members = [
"common/client-libs/mixnet-client",
"common/client-libs/validator-client",
"common/commands",
"common/nym-common",
"common/config",
"common/cosmwasm-smart-contracts/coconut-dkg",
"common/cosmwasm-smart-contracts/contracts-common",
@@ -40,7 +41,6 @@ members = [
"common/cosmwasm-smart-contracts/group-contract",
"common/cosmwasm-smart-contracts/mixnet-contract",
"common/cosmwasm-smart-contracts/multisig-contract",
"common/cosmwasm-smart-contracts/node-families-contract",
"common/cosmwasm-smart-contracts/nym-performance-contract",
"common/cosmwasm-smart-contracts/nym-pool-contract",
"common/cosmwasm-smart-contracts/vesting-contract",
@@ -71,15 +71,11 @@ members = [
"common/node-tester-utils",
"common/nonexhaustive-delayqueue",
"common/nym-cache",
"common/nym-common",
"common/nym-connection-monitor",
"common/nym-id",
"common/nym-kcp",
"common/nym-kkt",
"common/nym-kkt-ciphersuite",
"common/nym-kkt-context",
"common/nym-lp",
"common/nym-lp-data",
"common/nym-kkt",
"common/nym-metrics",
"common/nym_offline_compact_ecash",
"common/nymnoise",
@@ -95,9 +91,9 @@ members = [
"common/nymsphinx/params",
"common/nymsphinx/routing",
"common/nymsphinx/types",
"common/nyxd-scraper-sqlite",
"common/nyxd-scraper-psql",
"common/nyxd-scraper-shared",
"common/nyxd-scraper-sqlite",
"common/pemstore",
"common/registration",
"common/serde-helpers",
@@ -127,14 +123,13 @@ members = [
"common/zulip-client",
"documentation/autodoc",
"gateway",
"integration-tests",
"nym-api",
"nym-api/nym-api-requests",
"nym-authenticator-client",
"nym-browser-extension/storage",
"nym-credential-proxy/nym-credential-proxy",
"nym-credential-proxy/nym-credential-proxy-requests",
"nym-data-observatory",
"nym-gateway-probe",
"nym-ip-packet-client",
"nym-network-monitor",
"nym-node",
@@ -146,7 +141,6 @@ members = [
"nym-outfox",
"nym-registration-client",
"nym-signers-monitor",
"nym-sqlx-pool-guard",
"nym-statistics-api",
"nym-validator-rewarder",
"nyx-chain-watcher",
@@ -154,18 +148,19 @@ members = [
"sdk/ffi/go",
"sdk/ffi/shared",
"sdk/rust/nym-sdk",
"smolmix/core",
"service-providers/common",
"service-providers/ip-packet-router",
"service-providers/network-requester",
"smolmix/core",
"nym-sqlx-pool-guard",
"tools/echo-server",
"tools/internal/contract-state-importer/importer-cli",
"tools/internal/contract-state-importer/importer-contract",
"tools/internal/localnet-orchestrator",
"tools/internal/localnet-orchestrator/dkg-bypass-contract",
"tools/internal/mixnet-connectivity-check",
# "tools/internal/sdk-version-bump",
"tools/internal/ssl-inject",
"tools/internal/localnet-orchestrator",
"tools/internal/localnet-orchestrator/dkg-bypass-contract",
"tools/internal/validator-status-check",
"tools/nym-cli",
"tools/nym-id-cli",
@@ -174,30 +169,35 @@ members = [
"tools/nymvisor",
"tools/ts-rs-cli",
"wasm/client",
"wasm/smolmix",
# "wasm/full-nym-wasm", # If we uncomment this again, remember to also uncomment the profile settings below
"wasm/mix-fetch",
"wasm/node-tester",
"wasm/zknym-lib",
"nym-gateway-probe",
"integration-tests",
"common/nym-kkt-ciphersuite",
"common/nym-kkt-context",
"nym-network-monitor-v3/nym-network-monitor-orchestrator",
"nym-network-monitor-v3/nym-network-monitor-agent",
"nym-network-monitor-v3/nym-network-monitor-orchestrator-requests",
"nym-network-monitor-v3/nym-network-monitor-agent", "nym-network-monitor-v3/nym-network-monitor-orchestrator-requests",
]
default-members = [
"clients/native",
"clients/socks5",
"nym-api",
"nym-authenticator-client",
"nym-api",
"nym-credential-proxy/nym-credential-proxy",
"nym-node",
"nym-registration-client",
"nym-statistics-api",
"nym-validator-rewarder",
"nyx-chain-watcher",
"service-providers/ip-packet-router",
"service-providers/network-requester",
"tools/internal/localnet-orchestrator",
"tools/nymvisor",
"nym-registration-client",
"nym-network-monitor-v3/nym-network-monitor-orchestrator",
"nym-network-monitor-v3/nym-network-monitor-agent",
"tools/internal/localnet-orchestrator"
]
exclude = ["contracts", "nym-wallet", "cpu-cycles"]
@@ -211,7 +211,7 @@ edition = "2024"
license = "Apache-2.0"
rust-version = "1.87.0"
readme = "README.md"
version = "1.21.1"
version = "1.20.4"
[workspace.dependencies]
addr = "0.15.6"
@@ -225,17 +225,16 @@ anyhow = "1.0.98"
arc-swap = "1.7.1"
argon2 = "0.5.0"
async-trait = "0.1.88"
async-tungstenite = { version = "0.24", default-features = false }
axum = "0.8.9"
axum-client-ip = "1.3.1"
axum-extra = "0.12.6"
axum-test = "20.0.0"
axum = "0.7.5"
axum-client-ip = "0.6.1"
axum-extra = "0.9.4"
axum-test = "16.2.0"
base64 = "0.22.1"
base85rs = "0.1.3"
bincode = "1.3.3"
bip39 = { version = "2.0.0", features = ["zeroize"] }
bitvec = "1.0.0"
blake3 = ">=1.7, <1.8.4" # blake3 1.8.4+ requires digest 0.11; workspace is on 0.10
blake3 = "1.7.0"
bloomfilter = "3.0.1"
bs58 = "0.5.1"
bytecodec = "0.4.15"
@@ -254,7 +253,7 @@ clap_complete_fig = "4.5"
colored = "2.2"
comfy-table = "7.1.4"
console = "0.16.0"
console-subscriber = "0.5.0"
console-subscriber = "0.4.1"
console_error_panic_hook = "0.1"
const-str = "0.5.6"
const_format = "0.2.34"
@@ -279,27 +278,24 @@ eyre = "0.6.9"
fastrand = "2.1.1"
flate2 = "1.1.1"
futures = "0.3.31"
futures-rustls = { version = "0.26", default-features = false }
futures-util = "0.3"
generic-array = "0.14.7"
getrandom = "0.2.10"
getrandom03 = { package = "getrandom", version = "=0.3.3" }
getrandom04 = { package = "getrandom", version = "0.4" }
glob = "0.3"
handlebars = "3.5.5"
hex = "0.4.3"
hickory-proto = { version = "0.26.1", default-features = false }
hickory-proto = "0.26.1"
hickory-resolver = "0.26.1"
hkdf = "0.12.3"
hmac = "0.12.1"
http = "1"
http-body-util = "0.1"
httparse = "1.10"
httpcodec = "0.2.3"
human-repr = "1.1.0"
humantime = "2.2.0"
humantime-serde = "1.1.1"
hyper = { version = "1.6.0", default-features = false }
hyper = "1.6.0"
hyper-util = "0.1"
indicatif = "0.18.0"
inquire = "0.6.2"
@@ -334,7 +330,7 @@ pnet_packet = "0.35.0"
publicsuffix = "2.3.0"
proc_pidinfo = "0.1.3"
quote = "1"
rand = "0.8.6"
rand = "0.8.5"
rand09 = { package = "rand", version = "=0.9.2" }
rand_chacha = "0.3"
rand_chacha09 = { package = "rand_chacha", version = "=0.9.0" }
@@ -345,14 +341,12 @@ regex = "1.10.6"
reqwest = { version = "0.13.1", default-features = false }
rs_merkle = "1.5.0"
rustls = { version = "0.23.37", default-features = false }
rustls-pki-types = "1"
rustls-rustcrypto = "0.0.2-alpha"
schemars = "0.8.22"
semver = "1.0.26"
serde = "1.0.219"
serde_bytes = "0.11.17"
serde_derive = "1.0"
serde_json = { version = "1.0.140", features = ["float_roundtrip"] }
serde_json = "1.0.140"
serde_json_path = "0.7.2"
serde_repr = "0.1"
serde_with = "3.9.0"
@@ -360,7 +354,7 @@ serde_yaml = "0.9.25"
serde_plain = "1.0.2"
sha2 = "0.10.3"
si-scale = "0.2.3"
simple-dns = "0.7"
smolmix = { version = "0.0.1", path = "smolmix/core" }
smoltcp = "0.12"
snow = "0.9.6"
sphinx-packet = "=0.6.0"
@@ -369,9 +363,9 @@ strum = "0.28.0"
strum_macros = "0.28.0"
subtle-encoding = "0.5"
syn = "2"
sysinfo = "0.38.4"
sysinfo = "0.37.0"
tap = "1.0.1"
tar = "0.4.45"
tar = "0.4.44"
test-with = { version = "0.15.4", default-features = false }
tempfile = "3.20"
thiserror = "2.0"
@@ -384,7 +378,7 @@ tokio-test = "0.4.4"
tokio-tun = "0.11.5"
tokio-rustls = "0.26"
tokio-smoltcp = "0.5"
tokio-tungstenite = "0.20.1"
tokio-tungstenite = { version = "0.20.1" }
tokio-util = "0.7.15"
toml = "0.8.22"
tower = "0.5.2"
@@ -402,10 +396,11 @@ uniffi = "0.29.2"
uniffi_build = "0.29.0"
url = "2.5"
utoipa = "5.2"
utoipa-swagger-ui = "9.0.2"
utoipa-swagger-ui = "8.1"
utoipauto = "0.2"
uuid = "1.19.0"
vergen = { version = "=8.3.1", default-features = false }
vergen-gitcl = { version = "1.0.8", default-features = false }
walkdir = "2"
x25519-dalek = "2.0.0"
zeroize = "1.7.0"
@@ -428,115 +423,111 @@ libcrux-sha3 = "0.0.8"
libcrux-traits = "0.0.6"
# Workspace dep definitions required by crates.io publication - we need a workspace version since `cargo workspaces` doesn't work with path imports from crate manifests
nym-api-requests = { version = "1.21.1", path = "nym-api/nym-api-requests" }
nym-authenticator-requests = { version = "1.21.1", path = "common/authenticator-requests" }
nym-async-file-watcher = { version = "1.21.1", path = "common/async-file-watcher" }
nym-authenticator-client = { version = "1.21.1", path = "nym-authenticator-client" }
nym-bandwidth-controller = { version = "1.21.1", path = "common/bandwidth-controller" }
nym-bin-common = { version = "1.21.1", path = "common/bin-common" }
nym-cache = { version = "1.21.1", path = "common/nym-cache" }
nym-client-core = { version = "1.21.1", path = "common/client-core", default-features = false }
nym-client-core-config-types = { version = "1.21.1", path = "common/client-core/config-types" }
nym-client-core-gateways-storage = { version = "1.21.1", path = "common/client-core/gateways-storage" }
nym-client-core-surb-storage = { version = "1.21.1", path = "common/client-core/surb-storage" }
nym-client-websocket-requests = { version = "1.21.1", path = "clients/native/websocket-requests" }
nym-common = { version = "1.21.1", path = "common/nym-common" }
nym-compact-ecash = { version = "1.21.1", path = "common/nym_offline_compact_ecash" }
nym-config = { version = "1.21.1", path = "common/config" }
nym-contracts-common = { version = "1.21.1", path = "common/cosmwasm-smart-contracts/contracts-common" }
nym-coconut-dkg-common = { version = "1.21.1", path = "common/cosmwasm-smart-contracts/coconut-dkg" }
nym-credential-storage = { version = "1.21.1", path = "common/credential-storage" }
nym-credential-utils = { version = "1.21.1", path = "common/credential-utils" }
nym-credential-proxy-lib = { version = "1.21.1", path = "common/credential-proxy" }
nym-credentials = { version = "1.21.1", path = "common/credentials", default-features = false }
nym-credentials-interface = { version = "1.21.1", path = "common/credentials-interface" }
nym-credential-proxy-requests = { version = "1.21.1", path = "nym-credential-proxy/nym-credential-proxy-requests", default-features = false }
nym-credential-verification = { version = "1.21.1", path = "common/credential-verification" }
nym-crypto = { version = "1.21.1", path = "common/crypto", default-features = false }
nym-dkg = { version = "1.21.1", path = "common/dkg" }
nym-ecash-contract-common = { version = "1.21.1", path = "common/cosmwasm-smart-contracts/ecash-contract" }
nym-ecash-signer-check = { version = "1.21.1", path = "common/ecash-signer-check" }
nym-ecash-signer-check-types = { version = "1.21.1", path = "common/ecash-signer-check-types" }
nym-ecash-time = { version = "1.21.1", path = "common/ecash-time" }
nym-exit-policy = { version = "1.21.1", path = "common/exit-policy" }
nym-ffi-shared = { version = "1.21.1", path = "sdk/ffi/shared" }
nym-gateway-client = { version = "1.21.1", path = "common/client-libs/gateway-client", default-features = false }
nym-gateway-probe = { version = "1.21.1", path = "nym-gateway-probe" }
nym-gateway-requests = { version = "1.21.1", path = "common/gateway-requests" }
nym-gateway-storage = { version = "1.21.1", path = "common/gateway-storage" }
nym-gateway-stats-storage = { version = "1.21.1", path = "common/gateway-stats-storage" }
nym-group-contract-common = { version = "1.21.1", path = "common/cosmwasm-smart-contracts/group-contract" }
nym-http-api-client = { version = "1.21.1", path = "common/http-api-client" }
nym-http-api-client-macro = { version = "1.21.1", path = "common/http-api-client-macro" }
nym-http-api-common = { version = "1.21.1", path = "common/http-api-common", default-features = false }
nym-id = { version = "1.21.1", path = "common/nym-id" }
nym-ip-packet-client = { version = "1.21.1", path = "nym-ip-packet-client" }
nym-ip-packet-requests = { version = "1.21.1", path = "common/ip-packet-requests" }
nym-lp = { version = "1.21.1", path = "common/nym-lp" }
nym-lp-data = { version = "1.21.1", path = "common/nym-lp-data" }
nym-kkt = { version = "1.21.1", path = "common/nym-kkt" }
nym-kkt-ciphersuite = { version = "1.21.1", path = "common/nym-kkt-ciphersuite" }
nym-kkt-context = { version = "1.21.1", path = "common/nym-kkt-context" }
nym-metrics = { version = "1.21.1", path = "common/nym-metrics" }
nym-mixnet-client = { version = "1.21.1", path = "common/client-libs/mixnet-client" }
nym-mixnet-contract-common = { version = "1.21.1", path = "common/cosmwasm-smart-contracts/mixnet-contract" }
nym-multisig-contract-common = { version = "1.21.1", path = "common/cosmwasm-smart-contracts/multisig-contract" }
nym-network-defaults = { version = "1.21.1", path = "common/network-defaults" }
nym-node-tester-utils = { version = "1.21.1", path = "common/node-tester-utils" }
nym-noise = { version = "1.21.1", path = "common/nymnoise" }
nym-noise-keys = { version = "1.21.1", path = "common/nymnoise/keys" }
nym-nonexhaustive-delayqueue = { version = "1.21.1", path = "common/nonexhaustive-delayqueue" }
nym-node-requests = { version = "1.21.1", path = "nym-node/nym-node-requests", default-features = false }
nym-node-metrics = { version = "1.21.1", path = "nym-node/nym-node-metrics" }
nym-node-families-contract-common = { version = "1.21.1", path = "common/cosmwasm-smart-contracts/node-families-contract" }
nym-ordered-buffer = { version = "1.21.1", path = "common/socks5/ordered-buffer" }
nym-outfox = { version = "1.21.1", path = "nym-outfox" }
nym-registration-common = { version = "1.21.1", path = "common/registration" }
nym-pemstore = { version = "1.21.1", path = "common/pemstore" }
nym-performance-contract-common = { version = "1.21.1", path = "common/cosmwasm-smart-contracts/nym-performance-contract" }
nym-sdk = { version = "1.21.1", path = "sdk/rust/nym-sdk" }
nym-serde-helpers = { version = "1.21.1", path = "common/serde-helpers" }
nym-service-providers-common = { version = "1.21.1", path = "service-providers/common" }
nym-service-provider-requests-common = { version = "1.21.1", path = "common/service-provider-requests-common" }
nym-socks5-client-core = { version = "1.21.1", path = "common/socks5-client-core" }
nym-socks5-proxy-helpers = { version = "1.21.1", path = "common/socks5/proxy-helpers" }
nym-socks5-requests = { version = "1.21.1", path = "common/socks5/requests" }
nym-sphinx = { version = "1.21.1", path = "common/nymsphinx" }
nym-sphinx-acknowledgements = { version = "1.21.1", path = "common/nymsphinx/acknowledgements" }
nym-sphinx-addressing = { version = "1.21.1", path = "common/nymsphinx/addressing" }
nym-sphinx-anonymous-replies = { version = "1.21.1", path = "common/nymsphinx/anonymous-replies" }
nym-sphinx-chunking = { version = "1.21.1", path = "common/nymsphinx/chunking" }
nym-sphinx-cover = { version = "1.21.1", path = "common/nymsphinx/cover" }
nym-sphinx-forwarding = { version = "1.21.1", path = "common/nymsphinx/forwarding" }
nym-sphinx-framing = { version = "1.21.1", path = "common/nymsphinx/framing" }
nym-sphinx-params = { version = "1.21.1", path = "common/nymsphinx/params" }
nym-sphinx-routing = { version = "1.21.1", path = "common/nymsphinx/routing" }
nym-sphinx-types = { version = "1.21.1", path = "common/nymsphinx/types" }
nym-statistics-common = { version = "1.21.1", path = "common/statistics" }
nym-store-cipher = { version = "1.21.1", path = "common/store-cipher" }
nym-task = { version = "1.21.1", path = "common/task" }
nym-tun = { version = "1.21.1", path = "common/tun" }
nym-test-utils = { version = "1.21.1", path = "common/test-utils" }
nym-ticketbooks-merkle = { version = "1.21.1", path = "common/ticketbooks-merkle" }
nym-topology = { version = "1.21.1", path = "common/topology" }
nym-types = { version = "1.21.1", path = "common/types" }
nym-upgrade-mode-check = { version = "1.21.1", path = "common/upgrade-mode-check" }
nym-validator-client = { version = "1.21.1", path = "common/client-libs/validator-client", default-features = false }
nym-vesting-contract-common = { version = "1.21.1", path = "common/cosmwasm-smart-contracts/vesting-contract" }
nym-network-monitors-contract-common = { version = "1.21.1", path = "common/cosmwasm-smart-contracts/network-monitors-contract" }
nym-verloc = { version = "1.21.1", path = "common/verloc" }
nym-wireguard = { version = "1.21.1", path = "common/wireguard" }
nym-wireguard-types = { version = "1.21.1", path = "common/wireguard-types" }
nym-wireguard-private-metadata-shared = { version = "1.21.1", path = "common/wireguard-private-metadata/shared" }
nym-wireguard-private-metadata-client = { version = "1.21.1", path = "common/wireguard-private-metadata/client" }
nym-wireguard-private-metadata-server = { version = "1.21.1", path = "common/wireguard-private-metadata/server" }
nym-sqlx-pool-guard = { version = "1.21.1", path = "nym-sqlx-pool-guard" }
nym-wasm-client-core = { version = "1.21.1", path = "common/wasm/client-core" }
nym-wasm-storage = { version = "1.21.1", path = "common/wasm/storage" }
nym-wasm-utils = { version = "1.21.1", path = "common/wasm/utils", default-features = false }
nyxd-scraper-shared = { version = "1.21.1", path = "common/nyxd-scraper-shared" }
smolmix = { version = "1.21.1", path = "smolmix/core" }
nym-api-requests = { version = "1.20.4", path = "nym-api/nym-api-requests" }
nym-authenticator-requests = { version = "1.20.4", path = "common/authenticator-requests" }
nym-async-file-watcher = { version = "1.20.4", path = "common/async-file-watcher" }
nym-authenticator-client = { version = "1.20.4", path = "nym-authenticator-client" }
nym-bandwidth-controller = { version = "1.20.4", path = "common/bandwidth-controller" }
nym-bin-common = { version = "1.20.4", path = "common/bin-common" }
nym-cache = { version = "1.20.4", path = "common/nym-cache" }
nym-client-core = { version = "1.20.4", path = "common/client-core", default-features = false }
nym-client-core-config-types = { version = "1.20.4", path = "common/client-core/config-types" }
nym-client-core-gateways-storage = { version = "1.20.4", path = "common/client-core/gateways-storage" }
nym-client-core-surb-storage = { version = "1.20.4", path = "common/client-core/surb-storage" }
nym-client-websocket-requests = { version = "1.20.4", path = "clients/native/websocket-requests" }
nym-common = { version = "1.20.4", path = "common/nym-common" }
nym-compact-ecash = { version = "1.20.4", path = "common/nym_offline_compact_ecash" }
nym-config = { version = "1.20.4", path = "common/config" }
nym-contracts-common = { version = "1.20.4", path = "common/cosmwasm-smart-contracts/contracts-common" }
nym-coconut-dkg-common = { version = "1.20.4", path = "common/cosmwasm-smart-contracts/coconut-dkg" }
nym-credential-storage = { version = "1.20.4", path = "common/credential-storage" }
nym-credential-utils = { version = "1.20.4", path = "common/credential-utils" }
nym-credential-proxy-lib = { version = "1.20.4", path = "common/credential-proxy" }
nym-credentials = { version = "1.20.4", path = "common/credentials", default-features = false }
nym-credentials-interface = { version = "1.20.4", path = "common/credentials-interface" }
nym-credential-proxy-requests = { version = "1.20.4", path = "nym-credential-proxy/nym-credential-proxy-requests", default-features = false }
nym-credential-verification = { version = "1.20.4", path = "common/credential-verification" }
nym-crypto = { version = "1.20.4", path = "common/crypto", default-features = false }
nym-dkg = { version = "1.20.4", path = "common/dkg" }
nym-ecash-contract-common = { version = "1.20.4", path = "common/cosmwasm-smart-contracts/ecash-contract" }
nym-ecash-signer-check = { version = "1.20.4", path = "common/ecash-signer-check" }
nym-ecash-signer-check-types = { version = "1.20.4", path = "common/ecash-signer-check-types" }
nym-ecash-time = { version = "1.20.4", path = "common/ecash-time" }
nym-exit-policy = { version = "1.20.4", path = "common/exit-policy" }
nym-ffi-shared = { version = "1.20.4", path = "sdk/ffi/shared" }
nym-gateway-client = { version = "1.20.4", 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.20.4", path = "common/gateway-requests" }
nym-gateway-storage = { version = "1.20.4", path = "common/gateway-storage" }
nym-gateway-stats-storage = { version = "1.20.4", path = "common/gateway-stats-storage" }
nym-group-contract-common = { version = "1.20.4", path = "common/cosmwasm-smart-contracts/group-contract" }
nym-http-api-client = { version = "1.20.4", path = "common/http-api-client" }
nym-http-api-client-macro = { version = "1.20.4", path = "common/http-api-client-macro" }
nym-http-api-common = { version = "1.20.4", path = "common/http-api-common", default-features = false }
nym-id = { version = "1.20.4", path = "common/nym-id" }
nym-ip-packet-client = { version = "1.20.4", path = "nym-ip-packet-client" }
nym-ip-packet-requests = { version = "1.20.4", path = "common/ip-packet-requests" }
nym-lp = { version = "1.20.4", path = "common/nym-lp" }
nym-kkt = { version = "0.1.0", path = "common/nym-kkt" }
nym-kkt-ciphersuite = { version = "1.20.4", path = "common/nym-kkt-ciphersuite" }
nym-kkt-context = { version = "1.20.4", path = "common/nym-kkt-context" }
nym-metrics = { version = "1.20.4", path = "common/nym-metrics" }
nym-mixnet-client = { version = "1.20.4", path = "common/client-libs/mixnet-client" }
nym-mixnet-contract-common = { version = "1.20.4", path = "common/cosmwasm-smart-contracts/mixnet-contract" }
nym-multisig-contract-common = { version = "1.20.4", path = "common/cosmwasm-smart-contracts/multisig-contract" }
nym-network-defaults = { version = "1.20.4", path = "common/network-defaults" }
nym-node-tester-utils = { version = "1.20.4", path = "common/node-tester-utils" }
nym-noise = { version = "1.20.4", path = "common/nymnoise" }
nym-noise-keys = { version = "1.20.4", path = "common/nymnoise/keys" }
nym-nonexhaustive-delayqueue = { version = "1.20.4", path = "common/nonexhaustive-delayqueue" }
nym-node-requests = { version = "1.20.4", path = "nym-node/nym-node-requests", default-features = false }
nym-node-metrics = { version = "1.20.4", path = "nym-node/nym-node-metrics" }
nym-ordered-buffer = { version = "1.20.4", path = "common/socks5/ordered-buffer" }
nym-outfox = { version = "1.20.4", path = "nym-outfox" }
nym-registration-common = { version = "1.20.4", path = "common/registration" }
nym-pemstore = { version = "1.20.4", path = "common/pemstore" }
nym-performance-contract-common = { version = "1.20.4", path = "common/cosmwasm-smart-contracts/nym-performance-contract" }
nym-sdk = { version = "1.20.4", path = "sdk/rust/nym-sdk" }
nym-serde-helpers = { version = "1.20.4", path = "common/serde-helpers" }
nym-service-providers-common = { version = "1.20.4", path = "service-providers/common" }
nym-service-provider-requests-common = { version = "1.20.4", path = "common/service-provider-requests-common" }
nym-socks5-client-core = { version = "1.20.4", path = "common/socks5-client-core" }
nym-socks5-proxy-helpers = { version = "1.20.4", path = "common/socks5/proxy-helpers" }
nym-socks5-requests = { version = "1.20.4", path = "common/socks5/requests" }
nym-sphinx = { version = "1.20.4", path = "common/nymsphinx" }
nym-sphinx-acknowledgements = { version = "1.20.4", path = "common/nymsphinx/acknowledgements" }
nym-sphinx-addressing = { version = "1.20.4", path = "common/nymsphinx/addressing" }
nym-sphinx-anonymous-replies = { version = "1.20.4", path = "common/nymsphinx/anonymous-replies" }
nym-sphinx-chunking = { version = "1.20.4", path = "common/nymsphinx/chunking" }
nym-sphinx-cover = { version = "1.20.4", path = "common/nymsphinx/cover" }
nym-sphinx-forwarding = { version = "1.20.4", path = "common/nymsphinx/forwarding" }
nym-sphinx-framing = { version = "1.20.4", path = "common/nymsphinx/framing" }
nym-sphinx-params = { version = "1.20.4", path = "common/nymsphinx/params" }
nym-sphinx-routing = { version = "1.20.4", path = "common/nymsphinx/routing" }
nym-sphinx-types = { version = "1.20.4", path = "common/nymsphinx/types" }
nym-statistics-common = { version = "1.20.4", path = "common/statistics" }
nym-store-cipher = { version = "1.20.4", path = "common/store-cipher" }
nym-task = { version = "1.20.4", path = "common/task" }
nym-tun = { version = "1.20.4", path = "common/tun" }
nym-test-utils = { version = "1.20.4", path = "common/test-utils" }
nym-ticketbooks-merkle = { version = "1.20.4", path = "common/ticketbooks-merkle" }
nym-topology = { version = "1.20.4", path = "common/topology" }
nym-types = { version = "1.20.4", path = "common/types" }
nym-upgrade-mode-check = { version = "1.20.4", path = "common/upgrade-mode-check" }
nym-validator-client = { version = "1.20.4", path = "common/client-libs/validator-client", default-features = false }
nym-vesting-contract-common = { version = "1.20.4", path = "common/cosmwasm-smart-contracts/vesting-contract" }
nym-network-monitors-contract-common = { version = "1.20.4", path = "common/cosmwasm-smart-contracts/network-monitors-contract" }
nym-verloc = { version = "1.20.4", path = "common/verloc" }
nym-wireguard = { version = "1.20.4", path = "common/wireguard" }
nym-wireguard-types = { version = "1.20.4", path = "common/wireguard-types" }
nym-wireguard-private-metadata-shared = { version = "1.20.4", path = "common/wireguard-private-metadata/shared" }
nym-wireguard-private-metadata-client = { version = "1.20.4", path = "common/wireguard-private-metadata/client" }
nym-wireguard-private-metadata-server = { version = "1.20.4", 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.20.4", path = "common/wasm/client-core" }
nym-wasm-storage = { version = "1.20.4", path = "common/wasm/storage" }
nym-wasm-utils = { version = "1.20.4", path = "common/wasm/utils", default-features = false }
nyxd-scraper-shared = { version = "1.20.4", path = "common/nyxd-scraper-shared" }
# coconut/DKG related
# unfortunately until https://github.com/zkcrypto/nym-bls12_381-fork/issues/10 is resolved, we have to rely on the fork
@@ -603,7 +594,18 @@ opt-level = 3
# lto = true
opt-level = 'z'
[profile.release.package.smolmix-wasm]
[profile.release.package.nym-node-tester-wasm]
# lto = true
opt-level = 'z'
# Commented out since the crate is also commented out from the inclusion in the
# workspace above. We should uncomment this if we re-include it in the
# workspace
#[profile.release.package.nym-wasm-sdk]
## lto = true
#opt-level = 'z'
[profile.release.package.mix-fetch-wasm]
# lto = true
opt-level = 'z'
+11 -11
View File
@@ -104,30 +104,30 @@ $(eval $(call add_cargo_workspace,wallet,nym-wallet))
sdk-wasm: sdk-wasm-build sdk-wasm-test sdk-wasm-lint
sdk-wasm-build:
# $(MAKE) -C nym-browser-extension/storage wasm-pack
$(MAKE) -C wasm/client
$(MAKE) -C wasm/smolmix
$(MAKE) -C wasm/node-tester
$(MAKE) -C wasm/mix-fetch
# $(MAKE) -C wasm/zknym-lib
# $(MAKE) -C wasm/full-nym-wasm
# run this from npm/yarn to ensure tools are in the path, e.g. yarn build:sdk from root of repo
#
# `mix-tunnel` must build before the three feature packages — they import it
# via `workspace:*` and the lerna topological sort will respect that as long
# as we keep them in the same `--scope` invocation.
sdk-typescript-build:
npx lerna run --scope @nymproject/sdk build --stream
npx lerna run --scope '{@nymproject/mix-tunnel,@nymproject/mix-fetch,@nymproject/mix-dns,@nymproject/mix-websocket}' build --stream
pnpm --pwd sdk/typescript/codegen/contract-clients build
npx lerna run --scope @nymproject/mix-fetch build --stream
npx lerna run --scope @nymproject/node-tester build --stream
yarn --cwd sdk/typescript/codegen/contract-clients build
# NOTE: These targets are part of the main workspace (but not as wasm32-unknown-unknown)
WASM_CRATES = nym-client-wasm
# WASM_CRATES = extension-storage nym-client-wasm nym-node-tester-wasm zknym-lib
WASM_CRATES = nym-client-wasm nym-node-tester-wasm
sdk-wasm-test:
#cargo test $(addprefix -p , $(WASM_CRATES)) --target wasm32-unknown-unknown -- -Dwarnings
sdk-wasm-lint:
RUSTFLAGS='--cfg getrandom_backend="wasm_js"' cargo clippy $(addprefix -p , $(WASM_CRATES)) --target wasm32-unknown-unknown -- -Dwarnings
$(MAKE) -C wasm/smolmix check-fmt
$(MAKE) -C wasm/mix-fetch check-fmt
# Add to top-level targets
build: sdk-wasm-build
@@ -223,7 +223,7 @@ build-nym-cli:
generate-typescript:
cd tools/ts-rs-cli && cargo run && cd ../..
pnpm types:lint:fix
yarn types:lint:fix
# Run the integration tests for public nym-api endpoints
run-api-tests:
+2 -2
View File
@@ -74,9 +74,9 @@ Nym Node Operators and Validators Terms and Conditions can be found [here](https
## Getting Started
```bash
pnpm install
yarn install
```
```bash
pnpm build
yarn build
```
-8
View File
@@ -1,8 +0,0 @@
---
- name: Nym node auto-bonding
hosts: all
gather_facts: false
serial: 1
roles:
- role: postinstall-auto
+40 -30
View File
@@ -1,4 +1,21 @@
---
ansible_ssh_private_key_file: ~/.ssh/<SSH_KEY>
cli_url: "https://github.com/nymtech/nym/releases/download/nym-binaries-{{ nym_version }}/nym-cli"
tunnel_manager_url: "https://github.com/nymtech/nym/raw/refs/heads/develop/scripts/nym-node-setup/network-tunnel-manager.sh"
quic_bridge_deployment_url: "https://raw.githubusercontent.com/nymtech/nym/refs/heads/develop/scripts/nym-node-setup/quic_bridge_deployment.sh"
###############################################################################
## GLOBAL VARS
## These values will be used globally unless overwritten per node in inventory/all
###############################################################################
ansible_user: root # used for ssh, like `ssh root@nym-exit.ch-1.mynodes.net`
email: "<EMAIL>" # used in certbot, description.toml and landing page
website: "<WEBSITE>" # it is used in the description.toml
description: "<NODE_PUBLIC_DESCRIPTION>" # or define per node in inventory/all
# operator_name: "<OPERATOR_NAME>" # used in landing page if provided
###############################################################################
## GLOBAL VARS
## These values will be used globally unless overwritten per node in inventory/all
@@ -6,41 +23,16 @@
## Per node changes in inventory/all will overwrite these global vars
###############################################################################
## MANDATORY - uncomment & define
## --SSH--
#ansible_user: root # used for ssh, like `ssh root@nym-exit.ch-1.mynodes.net`
# ansible_ssh_private_key_file: ~/.ssh/<SSH_KEY>
## --Operator info--
# email: "<EMAIL>" # used in certbot, description.toml and landing page
# website: "<WEBSITE>" # it is used in the description.toml
# description: "<NODE_PUBLIC_DESCRIPTION>" # or define per node in inventory/all
# moniker: "<MONIKER>"
## --Node defaults (can override per node in inventory/all)--
# accept_operator_terms: true # controls --accept-operator-terms-and-conditions, read: https://nym.com/docs/operators/nodes/nym-node/setup#terms--conditions
# mode: exit-gateway # entry-gateway/exit-gateway/mixnode
# wireguard_enabled: true # true/false
hostname: "" # keep this fallback, keep it and setup hostname per node in inventory/all
## OPTIONAL - uncomment & define
# operator_name: "<OPERATOR_NAME>" # used in landing page if provided
# nym_version: "nym-binaries-v2026.7-tola" # to use particular version instead of Latest, provide in such form:
## alternative SSH key var setting, instead of a hardcoded path
## useful if the playbook is shared in a repo by more admins with each having own local key
# ansible_ssh_private_key_file: "{{ lookup('env', '<YOUR_ANSIBLE SSH_KEY_ENV_VAR>') }}"
# moniker: "<MONIKER>" # if not setup here not in inventory/all it get's derived from the hostname
# mode: <MODE> # entry-gateway/exit-gateway/mixnode
# wireguard_enabled: <WIREGUARD_ENABLED> # true/false
hostname: "" # this is a fallback, keep it and setup hostname per node in inventory/all
###############################################################################
## GLOBAL PACKAGES & URLs
## GLOBAL PACKAGES
## These will be installed during deployment
###############################################################################
nym_cli_url: "https://github.com/nymtech/nym/releases/download/{{ nym_version }}/nym-cli"
tunnel_manager_url: "https://github.com/nymtech/nym/raw/refs/heads/develop/scripts/nym-node-setup/network-tunnel-manager.sh"
quic_bridge_deployment_url: "https://raw.githubusercontent.com/nymtech/nym/refs/heads/develop/scripts/nym-node-setup/quic_bridge_deployment.sh"
packages:
- tmux
@@ -58,6 +50,24 @@ packages:
- ufw
###############################################################################
## OPTIONAL OVERRIDES
## All values below already have defaults in the playbook/roles
## Uncomment only if you want to override them
###############################################################################
###############################################################################
## SYSTEM MAINTENANCE PLAYBOOK KNOBS
###############################################################################
# To use particular version instead of Latest, provide in such form:
# nym_version: "nym-binaries-v2026.7-tola"
## NOTE:
## if you want to pin Nym to a specific version instead of using the
## latest release from GitHub in /tasks/main.yml then
## uncomment the line above and set the tag
###############################################################################
## SYSTEM MAINTENANCE PLAYBOOK KNOBS
###############################################################################
+23 -28
View File
@@ -1,39 +1,34 @@
[nym_nodes]
## READ CONFIGURATION GUIDE:
## https://nym.com/docs/operators/orchestration/ansible#configuration
# READ CONFIGURATION GUIDE:
# https://nym.com//docs/operators/orchestration/ansible#configuration
##############
## TEMPLATE ##
##############
## uncomment and exchange the <VARIABLES> with your real values for each node without the <> brackets
# VARIABLES INFO
# required vars to set values per node:
# `ansible_host`, `hostname`, `location`
# global vars can be set in the group_vars/all.yml, for example:
# `email`, `ansible_user`, `moniker`, `description`, `mode`, `wireguard_enabled`
# othersise they must be set per node!
############
# TEMPLATE #
############
# node1 ansible_host=<YOUR_SERVER_IP> ansible_user=<USER> hostname=<HOSTNAME> location=<LOCATION> email=<EMAIL> mode=<MODE> wireguard_enabled=<true/false> moniker=<MONIKER> description=<DESCRIPTION>
####################
## VARIABLES INFO ##
####################
# remove all comments and exchange the <VARIABLES> with your real values for each node
# without <> brackets
## --REQUIRED VARS--
## required per node:
## ansible_host, hostname, location
# PRIORITY ORDER
# anything setup globaly can be overwritten in this file per node
# if provided here, it takes priority over the global setting
## --OPTIONAL VARS--
## can be set in the group_vars/all.yml or per node here:
## email, ansible_user, moniker, description, mode, wireguard_enabled
## --PRIORITY ORDER--
## anything setup globaly can be overwritten in this file per node
## if provided here, it takes priority over the global setting
## --EXAMPLES--
## exit + wireguard gateway:
# EXAMPLES
# exit + wireguard gateway:
# node2 ansible_host=11.12.13.14 hostname=nym-exit.ch-1.mydomain.net mode=exit-gateway location=CH wireguard_enabled=true
## entry gateway, no wireguard:
# entry gateway, no wireguard:
# node3 ansible_host=12.13.14.15 hostname=nym-entry.ch-2.mydomain.net mode=entry-gateway location=CH wireguard_enabled=false
## mixnode (comment out tunnel+quic roles in deploy.yml for these)
# mix-de-1 ansible_host=13.14.15.16 hostname=nym-mix.de-1.example.net location=DE mode=mixnode wireguard_enabled=false
## NOTE:
## all examples above don't have defined user, email nor description as we use global vars from playbooks/group_vars/all.yml
# NOTE:
# all examples above don't have defined user, email nor description as we use the definition from group_vars/main.yml without an attempt of overwriting it
# all examples above don't have moniker defined as there is a function in /templates/description.toml.j2 deriving it from the hostname
@@ -1,42 +0,0 @@
# Mitigation playbook for CopyFail (CVE-2026-31431) and DirtyFrag (CVE-2026-43284 / CVE-2026-43500)
# This playbook applies interim module blacklists only
# Kernel patches are not yet available (May 2026)
# Once patched kernels ship, use remove_kernel_CVE_mitigations.yml to reverse everything
# This playbook is idempotent - safe to re-run if mitigations were already applied
- name: Mitigate Copy Fail + Dirty Frag
hosts: all
become: true
tasks:
- name: Blacklist algif_aead (Copy Fail)
copy:
dest: /etc/modprobe.d/disable-algif_aead.conf
content: "install algif_aead /bin/false\n"
owner: root
group: root
mode: "0644"
- name: Blacklist esp4, esp6, rxrpc (Dirty Frag)
copy:
dest: /etc/modprobe.d/dirtyfrag.conf
content: |
install esp4 /bin/false
install esp6 /bin/false
install rxrpc /bin/false
owner: root
group: root
mode: "0644"
- name: Unload all affected modules
modprobe:
name: "{{ item }}"
state: absent
loop:
- algif_aead
- esp4
- esp6
- rxrpc
ignore_errors: true
- name: Drop page cache to clear any contamination
shell: echo 3 > /proc/sys/vm/drop_caches
@@ -1,111 +0,0 @@
############################################################################################
############################################################################################
############################################################################################
#### THIS PLAYBOOK IS NOT MEANT TO BE RUN YET, IT IS NOT REFERRED IN ANY DOCUMENTATION! ####
############################################################################################
############################################################################################
############################################################################################
#
# Reversal playbook for mitigate_kernel_CVE.yml (CopyFail CVE-2026-31431 / DirtyFrag CVE-2026-43284 / CVE-2026-43500).
#
# Run this AFTER your distro has shipped the patched kernel.
# This playbook:
# 1. Updates the kernel via apt
# 2. Reboots and waits for reconnect
# 3. Verifies the running kernel is newer than the pre-patch version
# 4. Removes the interim module blacklists
# 5. Re-enables the affected modules live (no second reboot needed)
#
# Debian family only (Debian, Ubuntu). Tested on Debian 11, Debian 12, Ubuntu 20.04, 22.04, 24.04.
#
# For exit-gateway nodes with --wireguard-enabled true:
# After this playbook completes, run the networking restore step on each node via:
# ansible-playbook deploy.yml -t ntm
# See the CVE patch documentation for details.
- name: Remove CVE mitigations and apply patched kernel
hosts: all
become: true
tasks:
- name: Verify OS is Debian family
assert:
that:
- ansible_os_family == "Debian"
fail_msg: "This playbook supports Debian-family distros only (Debian, Ubuntu). For other distros, apply the kernel update and mitigation removal manually."
- name: Update apt cache
apt:
update_cache: true
cache_valid_time: 0
- name: Upgrade kernel packages
apt:
upgrade: full
only_upgrade: false
register: apt_upgrade_result
- name: Record pre-reboot kernel version
command: uname -r
register: kernel_before
changed_when: false
- name: Reboot to load patched kernel
reboot:
msg: "Rebooting to apply patched kernel (CVE-2026-31431 / CVE-2026-43284 / CVE-2026-43500)"
reboot_timeout: 300
pre_reboot_delay: 5
post_reboot_delay: 15
- name: Record post-reboot kernel version
command: uname -r
register: kernel_after
changed_when: false
- name: Show kernel versions before and after reboot
debug:
msg:
- "Kernel before reboot: {{ kernel_before.stdout }}"
- "Kernel after reboot: {{ kernel_after.stdout }}"
- name: Warn if kernel did not change after reboot
debug:
msg: >
WARNING: kernel version did not change after reboot ({{ kernel_after.stdout }}).
The patched kernel may not have been selected by GRUB, or no kernel update was available.
Do NOT remove the interim mitigations until you have confirmed the running kernel is patched.
Check: apt-cache policy linux-image-amd64 # Debian
Check: apt-cache policy linux-image-generic # Ubuntu
when: kernel_before.stdout == kernel_after.stdout
- name: Remove algif_aead blacklist
file:
path: /etc/modprobe.d/disable-algif_aead.conf
state: absent
- name: Remove DirtyFrag blacklist (esp4, esp6, rxrpc)
file:
path: /etc/modprobe.d/dirtyfrag.conf
state: absent
- name: Re-enable affected modules live
modprobe:
name: "{{ item }}"
state: present
loop:
- esp4
- esp6
- rxrpc
- algif_aead
ignore_errors: true
- name: Confirm nym-node service is still running
systemd:
name: nym-node
state: started
register: nym_node_status
failed_when: false
- name: Show nym-node status
debug:
msg: "nym-node service state: {{ nym_node_status.state | default('unknown - service may not exist on this node') }}"
+11 -5
View File
@@ -89,6 +89,7 @@
loop:
- "/etc/nginx/sites-enabled/{{ hostname }}-ssl"
- "/etc/nginx/sites-enabled/nym-wss-config"
when: not le_cert.stat.exists
notify: Restart nginx
- name: Ensure nginx is enabled and running (needed for ACME http-01)
@@ -110,13 +111,18 @@
- name: Obtain/renew certificate
command:
cmd: >-
certbot certonly --nginx
{% if le_cert.stat.exists %}
certbot certonly --webroot
-w /var/www/{{ hostname }}
--non-interactive --agree-tos --keep-until-expiring
-m {{ email }} -d {{ hostname }}
{% else %}
certbot --nginx
--non-interactive --agree-tos --redirect
-m {{ email }} -d {{ hostname }}
{% endif %}
register: certbot_result
failed_when: false
failed_when: false
# re-check cert after certbot attempt
- name: Re-check whether certificate exists after certbot
@@ -164,4 +170,4 @@
changed_when: false
- name: Flush handlers (apply restart after successful tests)
meta: flush_handlers
meta: flush_handlers
+2 -2
View File
@@ -10,7 +10,7 @@ mixnet_bind_address: "0.0.0.0:1789" # maps to --mixnet-bind-address
landing_page_assets_base_dir: "/var/www"
# Flag toggles
accept_operator_terms: false # override in group_vars or inventory
# accept_operator_terms: true # controls --accept-operator-terms-and-conditions
nym_write_flag: true # controls -w
nym_init_only_flag: true # controls --init-only
wss_port: 9001 # controlls --announce-wss-port
@@ -18,7 +18,7 @@ wss_port: 9001 # controlls --announce-wss-port
# Optional: extra flags if you want to append more later
nym_extra_flags: ""
# CLI URL
# CLI URL (nym_version can be set elsewhere / via GitHub API)
nym_cli_url: "https://github.com/nymtech/nym/releases/download/{{ nym_version }}/nym-cli"
# UFW
@@ -1,38 +0,0 @@
- name: Show which node is being bonded
tags: bonding
debug:
msg: "Bonding Nym node: {{ hostname }}"
- name: Get bonding details
tags: bonding
command: "/root/nym-binaries/nym-node bonding-information"
register: bondinfo
changed_when: false
- name: Display bonding info
tags: bonding
debug:
msg: "{{ item }}"
loop: "{{ bondinfo.stdout_lines }}"
- name: Sign bonding contract message on the node
tags: bonding
command:
argv:
- /root/nym-binaries/nym-node
- sign
- --contract-msg
- "{{ contract_msg }}"
- --output
- json
register: sign_output
- name: Display full signed message exactly as returned
tags: bonding
debug:
msg: "{{ sign_output.stdout }}"
- name: Display encoded signature
tags: bonding
debug:
msg: "ENCODED_SIGNATURE={{ (sign_output.stdout | from_json).encoded_signature }}"
+6 -10
View File
@@ -1,20 +1,16 @@
- name: Download quic_bridge_deployment.sh
command:
cmd: "curl -fsSL {{ quic_bridge_deployment_url }} -o /root/nym-binaries/quic_bridge_deployment.sh"
tags: quic
- name: Set quic_bridge_deployment permissions
file:
path: /root/nym-binaries/quic_bridge_deployment.sh
tags: quic bridge deployment
get_url:
url: "{{ quic_bridge_deployment_url }}"
dest: "/root/nym-binaries/quic_bridge_deployment.sh"
mode: "0755"
tags: quic
- name: Configure tunnel manager
tags: quic bridge deployment
become: true
command:
cmd: "/root/nym-binaries/quic_bridge_deployment.sh {{ item }}"
environment:
NONINTERACTIVE: "1"
loop:
- full_bridge_setup
tags: quic
- full_bridge_setup
+4 -10
View File
@@ -10,17 +10,11 @@
- ntm
- name: Download network tunnel manager
command:
cmd: "curl -fsSL {{ tunnel_manager_url }} -o /root/nym-binaries/network-tunnel-manager.sh"
tags:
- tunnel
- network_tunnel_manager
- ntm
- name: Set network tunnel manager permissions
file:
path: /root/nym-binaries/network-tunnel-manager.sh
get_url:
url: "{{ tunnel_manager_url }}"
dest: /root/nym-binaries/network-tunnel-manager.sh
mode: "0755"
force: yes
tags:
- tunnel
- network_tunnel_manager
+1 -1
View File
@@ -1,7 +1,7 @@
[package]
name = "nym-client"
description = "Implementation of the Nym Client"
version = "1.1.78"
version = "1.1.77"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>", "Jędrzej Stuczyński <andrew@nymtech.net>"]
edition = "2021"
license.workspace = true
-2
View File
@@ -472,7 +472,6 @@ impl Handler {
fn prepare_reconstructed_binary(
reconstructed_messages: Vec<ReconstructedMessage>,
) -> Vec<Result<WsMessage, WsError>> {
#[allow(clippy::result_large_err)] // TODO : remove this once tungstenite is updated
reconstructed_messages
.into_iter()
.map(ServerResponse::Received)
@@ -485,7 +484,6 @@ fn prepare_reconstructed_binary(
fn prepare_reconstructed_text(
reconstructed_messages: Vec<ReconstructedMessage>,
) -> Vec<Result<WsMessage, WsError>> {
#[allow(clippy::result_large_err)] // TODO : remove this once tungstenite is updated
reconstructed_messages
.into_iter()
.map(ServerResponse::Received)
+1 -1
View File
@@ -1,7 +1,7 @@
[package]
name = "nym-socks5-client"
description = "A SOCKS5 localhost proxy that converts incoming messages to Sphinx and sends them to a Nym address"
version = "1.1.78"
version = "1.1.77"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>"]
edition = "2021"
license.workspace = true
-10
View File
@@ -25,8 +25,6 @@ pub trait BandwidthTicketProvider: Send + Sync {
) -> Result<PreparedCredential, BandwidthControllerError>;
async fn get_upgrade_mode_token(&self) -> Result<Option<String>, BandwidthControllerError>;
async fn close(&self) {}
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
@@ -58,10 +56,6 @@ where
.map_err(|_| BandwidthControllerError::MalformedUpgradeModeToken)?;
Ok(Some(token))
}
async fn close(&self) {
self.storage.close().await;
}
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
@@ -81,8 +75,4 @@ impl<T: BandwidthTicketProvider + ?Sized + Send> BandwidthTicketProvider for Box
async fn get_upgrade_mode_token(&self) -> Result<Option<String>, BandwidthControllerError> {
(**self).get_upgrade_mode_token().await
}
async fn close(&self) {
(**self).close().await;
}
}
@@ -1023,16 +1023,6 @@ where
let encryption_keys = init_res.client_keys.encryption_keypair();
let identity_keys = init_res.client_keys.identity_keypair();
let credential_store_for_close = credential_store.clone();
let close_credential_token = shutdown_tracker.clone_shutdown_token();
shutdown_tracker.try_spawn_named(
async move {
close_credential_token.cancelled().await;
credential_store_for_close.close().await;
},
"CredentialStorage::close_on_shutdown",
);
// the components are started in very specific order. Unless you know what you are doing,
// do not change that.
let bandwidth_controller = self
@@ -11,17 +11,11 @@ use nym_bandwidth_controller::BandwidthController;
use nym_client_core_gateways_storage::OnDiskGatewaysDetails;
use nym_credential_storage::storage::Storage as CredentialStorage;
use nym_validator_client::{QueryHttpRpcNyxdClient, nyxd};
use std::{io, path::Path, time::Duration};
use std::{io, path::Path};
use time::OffsetDateTime;
use tracing::{error, info, trace};
use url::Url;
/// Maximum rename retry attempts when the database file is temporarily locked.
const ARCHIVE_MAX_RETRY_ATTEMPTS: u8 = 15;
/// Delay between archive rename retry attempts.
const ARCHIVE_RETRY_DELAY: Duration = Duration::from_millis(200);
async fn setup_fresh_backend<P: AsRef<Path>>(
db_path: P,
surb_config: &config::ReplySurbs,
@@ -80,58 +74,13 @@ async fn archive_corrupted_database<P: AsRef<Path>>(db_path: P) -> io::Result<()
};
let renamed = db_path.with_extension(new_extension);
// On Windows, sqlx may release its OS file handles asynchronously after
// pool.close() returns, briefly keeping the file locked
// (ERROR_SHARING_VIOLATION, os error 32). Retry with a short delay to
// give the OS time to flush the remaining handles.
for attempt in 0..ARCHIVE_MAX_RETRY_ATTEMPTS {
match tokio::fs::rename(db_path, &renamed).await {
Ok(()) => return Ok(()),
Err(e) if is_file_locked_error(&e) && (attempt + 1) < ARCHIVE_MAX_RETRY_ATTEMPTS => {
trace!(
"Database file is temporarily locked, retrying archive \
(attempt {}/{}): {e}",
attempt + 1,
ARCHIVE_MAX_RETRY_ATTEMPTS
);
tokio::time::sleep(ARCHIVE_RETRY_DELAY).await;
}
Err(e) => {
error!(
"Failed to rename corrupt database file: {} to {}",
db_path.display(),
renamed.display()
);
return Err(e);
}
}
}
// Reached only when every attempt was blocked by a file lock.
error!(
"Failed to rename corrupt database file after {} attempts: {} to {}",
ARCHIVE_MAX_RETRY_ATTEMPTS,
db_path.display(),
renamed.display()
);
Err(io::Error::other(
"corrupt database archive blocked by persistent file lock",
))
}
/// Returns `true` when the IO error indicates a temporary file lock held by another handle
/// within the same process. Only meaningful on Windows; always `false` elsewhere.
fn is_file_locked_error(e: &io::Error) -> bool {
#[cfg(windows)]
{
// ERROR_SHARING_VIOLATION = 32, ERROR_LOCK_VIOLATION = 33
matches!(e.raw_os_error(), Some(32) | Some(33))
}
#[cfg(not(windows))]
{
let _ = e;
false
}
tokio::fs::rename(db_path, &renamed).await.inspect_err(|_| {
error!(
"Failed to rename corrupt database file: {} to {}",
db_path.display(),
renamed.display()
);
})
}
pub async fn setup_fs_reply_surb_backend<P: AsRef<Path>>(
@@ -439,7 +439,7 @@ where
let mut pending_acks = Vec::with_capacity(fragments.len());
let mut to_forward: HashMap<_, Vec<_>> = HashMap::new();
for (raw, prepared) in fragments.into_iter().zip(prepared_fragments) {
for (raw, prepared) in fragments.into_iter().zip(prepared_fragments.into_iter()) {
let lane = raw.0;
let FragmentWithMaxRetransmissions {
fragment,
@@ -670,7 +670,7 @@ where
Ok(fragments
.into_iter()
.zip(reply_surbs)
.zip(reply_surbs.into_iter())
.map(|(fragment, reply_surb)| {
// unwrap here is fine as we know we have a valid topology
#[allow(clippy::unwrap_used)]
@@ -337,8 +337,6 @@ impl ReplyStorageBackend for Backend {
}
async fn stop_storage_session(self) -> Result<(), Self::StorageError> {
let result = self.stop_client_use().await;
self.shutdown().await;
result
self.stop_client_use().await
}
}
+3 -5
View File
@@ -48,7 +48,6 @@ where
debug!("Started PersistentReplyStorage");
if let Err(err) = self.backend.start_storage_session().await {
error!("failed to start the storage session - {err}");
self.backend.stop_storage_session().await.ok();
return;
}
@@ -56,11 +55,10 @@ where
info!("PersistentReplyStorage is flushing all reply-related data to underlying storage");
if let Err(err) = self.backend.flush_surb_storage(&mem_state).await {
error!("failed to flush our reply-related data to the persistent storage: {err}");
self.backend.stop_storage_session().await.ok();
return;
error!("failed to flush our reply-related data to the persistent storage: {err}")
} else {
info!("Data flush is complete")
}
info!("Data flush is complete");
if let Err(err) = self.backend.stop_storage_session().await {
error!("failed to properly stop the storage session - {err}. We might not be able to smoothly restore it")
+2 -4
View File
@@ -17,7 +17,6 @@ publish = true
[dependencies]
dashmap = { workspace = true }
futures = { workspace = true }
strum = { workspace = true }
tracing = { workspace = true }
tokio = { workspace = true, features = ["time", "sync"] }
tokio-util = { workspace = true, features = ["codec"], optional = true }
@@ -27,13 +26,12 @@ tokio-stream = { workspace = true }
nym-noise = { workspace = true }
nym-sphinx = { workspace = true }
nym-task = { workspace = true, optional = true }
nym-metrics = { workspace = true, optional = true }
[features]
default = ["client"]
client = ["tokio-util", "nym-task", "nym-metrics", "tokio/net", "tokio/rt"]
client = ["tokio-util", "nym-task", "tokio/net", "tokio/rt"]
[dev-dependencies]
nym-crypto = { workspace = true }
rand = { workspace = true }
tokio = { workspace = true, features = ["macros", "io-util", "rt", "rt-multi-thread", "test-util"] }
tokio = { workspace = true, features = ["macros", "io-util", "rt", "rt-multi-thread"] }
+61 -301
View File
@@ -1,9 +1,8 @@
// Copyright 2021-2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::metrics::{MixnetMetric, Traced};
use dashmap::DashMap;
use futures::{Sink, SinkExt, StreamExt};
use futures::{SinkExt, StreamExt};
use nym_noise::config::NoiseConfig;
use nym_noise::upgrade_noise_initiator;
use nym_sphinx::forwarding::packet::MixPacket;
@@ -11,7 +10,7 @@ use nym_sphinx::framing::codec::NymCodec;
use nym_sphinx::framing::packet::FramedNymPacket;
use std::io;
use std::net::SocketAddr;
use std::ops::{ControlFlow, Deref};
use std::ops::Deref;
use std::sync::atomic::{AtomicU32, AtomicUsize, Ordering};
use std::sync::Arc;
use std::time::Duration;
@@ -19,7 +18,7 @@ use tokio::io::{AsyncRead, AsyncWrite};
use tokio::net::TcpStream;
use tokio::sync::mpsc;
use tokio::sync::mpsc::error::TrySendError;
use tokio::time::{sleep, Instant};
use tokio::time::sleep;
use tokio_stream::wrappers::ReceiverStream;
use tokio_util::codec::Framed;
use tracing::*;
@@ -31,13 +30,6 @@ pub struct Config {
pub initial_connection_timeout: Duration,
pub maximum_connection_buffer_size: usize,
pub use_legacy_packet_encoding: bool,
/// Close an egress connection after this long with no packets sent (0 disables). The cache
/// entry is evicted on close and the next packet to that peer transparently reconnects.
pub connection_idle_timeout: Duration,
/// Max time a single batch flush may block on the peer socket before we give up on it
/// (0 disables). One timeout is treated as transient congestion - the batch is abandoned but
/// the connection is retained (no re-handshake); only a few *consecutive* timeouts tear it down.
pub connection_write_timeout: Duration,
}
impl Config {
@@ -47,8 +39,6 @@ impl Config {
initial_connection_timeout: Duration,
maximum_connection_buffer_size: usize,
use_legacy_packet_encoding: bool,
connection_idle_timeout: Duration,
connection_write_timeout: Duration,
) -> Self {
Config {
initial_reconnection_backoff,
@@ -56,18 +46,14 @@ impl Config {
initial_connection_timeout,
maximum_connection_buffer_size,
use_legacy_packet_encoding,
connection_idle_timeout,
connection_write_timeout,
}
}
}
pub trait SendWithoutResponse {
// Without response in this context means we will not listen for anything we might get back (not
// that we should get anything), including any possible io errors.
// The packet carries the latency trace started upstream (at receive); the egress stages are
// stamped here and are a no-op for unsampled packets.
fn send_without_response(&self, packet: Traced<MixPacket>) -> io::Result<()>;
// that we should get anything), including any possible io errors
fn send_without_response(&self, packet: MixPacket) -> io::Result<()>;
}
pub struct Client {
@@ -103,7 +89,7 @@ impl Deref for ActiveConnections {
}
pub struct ConnectionSender {
channel: mpsc::Sender<Traced<FramedNymPacket>>,
channel: mpsc::Sender<FramedNymPacket>,
current_reconnection_attempt: Arc<AtomicU32>,
// Identifies the `ManagedConnection` task currently owning this entry; used
// to ensure drop-time eviction only fires on the still-owning task.
@@ -111,7 +97,7 @@ pub struct ConnectionSender {
}
impl ConnectionSender {
fn new(channel: mpsc::Sender<Traced<FramedNymPacket>>, handle_token: Arc<()>) -> Self {
fn new(channel: mpsc::Sender<FramedNymPacket>, handle_token: Arc<()>) -> Self {
ConnectionSender {
channel,
current_reconnection_attempt: Arc::new(AtomicU32::new(0)),
@@ -123,10 +109,8 @@ impl ConnectionSender {
struct ManagedConnection {
address: SocketAddr,
noise_config: NoiseConfig,
message_receiver: ReceiverStream<Traced<FramedNymPacket>>,
message_receiver: ReceiverStream<FramedNymPacket>,
connection_timeout: Duration,
idle_timeout: Duration,
write_timeout: Duration,
current_reconnection: Arc<AtomicU32>,
active_connections: ActiveConnections,
handle_token: Arc<()>,
@@ -156,14 +140,11 @@ impl Drop for EvictOnDrop {
}
impl ManagedConnection {
#[allow(clippy::too_many_arguments)]
fn new(
address: SocketAddr,
noise_config: NoiseConfig,
message_receiver: mpsc::Receiver<Traced<FramedNymPacket>>,
message_receiver: mpsc::Receiver<FramedNymPacket>,
connection_timeout: Duration,
idle_timeout: Duration,
write_timeout: Duration,
current_reconnection: Arc<AtomicU32>,
active_connections: ActiveConnections,
handle_token: Arc<()>,
@@ -173,8 +154,6 @@ impl ManagedConnection {
noise_config,
message_receiver: ReceiverStream::new(message_receiver),
connection_timeout,
idle_timeout,
write_timeout,
current_reconnection,
active_connections,
handle_token,
@@ -183,8 +162,6 @@ impl ManagedConnection {
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,
@@ -241,11 +218,6 @@ impl ManagedConnection {
"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 {
@@ -274,236 +246,79 @@ impl ManagedConnection {
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);
let conn = Framed::new(noise_stream, NymCodec);
// 4. start handling the framed stream
run_io_loop(
conn,
self.message_receiver,
address,
idle_timeout,
write_timeout,
)
.await;
run_io_loop(conn, self.message_receiver, address).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 packet batch to {address}: {err}"
);
BatchOutcome::Failed
}
Err(_elapsed) => BatchOutcome::WriteTimedOut,
}
}
/// Instant at which a connection idle since `last_activity` should be closed, or `None` if idle
/// reaping is disabled (`timeout` is zero).
fn idle_deadline(last_activity: Instant, timeout: Duration) -> Option<Instant> {
(!timeout.is_zero()).then(|| last_activity + timeout)
}
// The connection is unidirectional (send-only); we read from it solely to
// notice peer FIN/RST while idle so we can evict the cache entry before the
// next outbound send finds it stale.
async fn run_io_loop<T>(
conn: Framed<T, NymCodec>,
receiver: ReceiverStream<Traced<FramedNymPacket>>,
mut receiver: ReceiverStream<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;
match msg {
None => {
debug!(
peer = %address,
exit_reason = "peer_closed",
"peer closed mixnet connection to {address}"
);
break;
}
Some(Err(err)) => {
debug!(
event = "connection.read_error",
peer = %address,
error = %err,
exit_reason = "read_error",
"read error on mixnet connection to {address}: {err}"
);
break;
}
Some(Ok(_)) => {
trace!(
peer = %address,
"unexpected inbound packet on mixnet connection to {address}; discarding"
);
}
}
}
outgoing = receiver.next() => {
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",
match outgoing {
None => {
debug!(
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"
exit_reason = "sender_dropped",
"connection manager to {address} finished"
);
if consecutive_write_timeouts >= MAX_CONSECUTIVE_WRITE_TIMEOUTS {
break;
}
Some(packet) => {
if let Err(err) = sink.send(packet).await {
debug!(
event = "connection.forward_error",
peer = %address,
exit_reason = "write_timeout",
"egress connection to {address} congested for {MAX_CONSECUTIVE_WRITE_TIMEOUTS} consecutive flushes; dropping it"
error = %err,
exit_reason = "forward_error",
"Failed to forward packet to {address}: {err}"
);
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;
}
}
}
}
@@ -543,7 +358,7 @@ impl Client {
}
}
fn make_connection(&self, address: SocketAddr, pending_packet: Traced<FramedNymPacket>) {
fn make_connection(&self, address: SocketAddr, pending_packet: FramedNymPacket) {
let (sender, receiver) = mpsc::channel(self.config.maximum_connection_buffer_size);
// this CAN'T fail because we just created the channel which has a non-zero capacity
@@ -572,10 +387,8 @@ impl Client {
let reconnection_attempt = current_reconnection_attempt.load(Ordering::Acquire);
let backoff = self.determine_backoff(reconnection_attempt);
// copy the values before moving into another task
// copy the value before moving into another task
let initial_connection_timeout = self.config.initial_connection_timeout;
let connection_idle_timeout = self.config.connection_idle_timeout;
let connection_write_timeout = self.config.connection_write_timeout;
let connections_count = self.connections_count.clone();
let noise_config = self.noise_config.clone();
@@ -593,8 +406,6 @@ impl Client {
noise_config,
receiver,
initial_connection_timeout,
connection_idle_timeout,
connection_write_timeout,
current_reconnection_attempt,
active_connections,
handle_token,
@@ -607,17 +418,14 @@ impl Client {
}
impl SendWithoutResponse for Client {
fn send_without_response(&self, packet: Traced<MixPacket>) -> io::Result<()> {
let address = packet.inner.next_hop_address();
fn send_without_response(&self, packet: MixPacket) -> io::Result<()> {
let address = packet.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 legacy = self.config.use_legacy_packet_encoding;
let queued = packet.map(|p| FramedNymPacket::from_mix_packet(p, legacy));
let framed_packet =
FramedNymPacket::from_mix_packet(packet, self.config.use_legacy_packet_encoding);
let Some(sender) = self.active_connections.get_mut(&address) else {
// there was never a connection to begin with
@@ -627,7 +435,7 @@ impl SendWithoutResponse for Client {
result = "not_connected",
"establishing initial connection to {address}"
);
self.make_connection(address, queued);
self.make_connection(address, framed_packet);
return Err(io::Error::new(
io::ErrorKind::NotConnected,
"connection is in progress",
@@ -638,12 +446,7 @@ impl SendWithoutResponse for Client {
let channel_available = sender.channel.capacity();
let channel_used = channel_capacity - channel_available;
// record how full this peer's egress buffer was (sampled packets only, to bound cost)
if sampled {
crate::metrics::observe_egress_buffer_fill(channel_used, channel_capacity);
}
let sending_res = sender.channel.try_send(queued);
let sending_res = sender.channel.try_send(framed_packet);
drop(sender);
sending_res.map_err(|err| {
@@ -698,8 +501,6 @@ mod tests {
initial_connection_timeout: Duration::from_millis(1_500),
maximum_connection_buffer_size: 128,
use_legacy_packet_encoding: false,
connection_idle_timeout: Duration::from_secs(300),
connection_write_timeout: Duration::from_millis(500),
},
NoiseConfig::new(
Arc::new(x25519::KeyPair::new(&mut rng)),
@@ -746,7 +547,7 @@ mod tests {
active: &ActiveConnections,
addr: SocketAddr,
token: Arc<()>,
) -> mpsc::Receiver<Traced<FramedNymPacket>> {
) -> mpsc::Receiver<FramedNymPacket> {
let (tx, rx) = mpsc::channel(1);
active.insert(addr, ConnectionSender::new(tx, token));
rx
@@ -809,14 +610,7 @@ mod tests {
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,
));
let task = tokio::spawn(run_io_loop(conn, ReceiverStream::new(rx), test_addr()));
// Simulate peer closing both directions of the connection.
drop(b);
@@ -833,13 +627,7 @@ mod tests {
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,
));
let task = tokio::spawn(run_io_loop(conn, ReceiverStream::new(rx), test_addr()));
drop(tx);
@@ -848,32 +636,4 @@ mod tests {
.expect("io_loop must exit when the upstream sender is dropped")
.expect("io_loop task must not panic");
}
#[tokio::test(start_paused = true)]
async fn io_loop_closes_idle_connection() {
// With no packets sent and the peer still connected, the idle timeout must eventually
// close the connection so the task/socket don't linger forever. The paused clock is
// virtual - it auto-advances to the next timer, so this completes instantly despite the
// durations below (no real waiting).
let (a, _b) = tokio::io::duplex(64);
let conn = Framed::new(a, NymCodec);
// keep the sender alive so the sender-dropped path can't fire instead
let (_tx, rx) = mpsc::channel(1);
let idle_timeout = Duration::from_millis(50);
let task = tokio::spawn(run_io_loop(
conn,
ReceiverStream::new(rx),
test_addr(),
idle_timeout,
Duration::ZERO,
));
// auto-advance fires the nearest timer (the 50ms idle deadline, sooner than this 500ms
// guard) once the task is otherwise idle, reaping the connection
tokio::time::timeout(Duration::from_millis(500), task)
.await
.expect("io_loop must close the connection after the idle timeout")
.expect("io_loop task must not panic");
}
}
@@ -1,7 +1,6 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::metrics::PacketTrace;
use futures::channel::mpsc;
use futures::channel::mpsc::SendError;
use nym_sphinx::forwarding::packet::MixPacket;
@@ -44,9 +43,6 @@ pub struct PacketToForward {
pub packet: MixPacket,
pub forward_delay_target: Option<Instant>,
pub network_monitor_packet: bool,
/// Latency breadcrumb started at packet receive; stamped as the packet moves through the
/// forwarder and egress stages. `PacketTrace::Off` for untraced packets (e.g. acks).
pub trace: PacketTrace,
}
impl PacketToForward {
@@ -54,17 +50,15 @@ impl PacketToForward {
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 client_packet_without_delay(packet: MixPacket) -> Self {
Self::new(packet, None, false, PacketTrace::Off)
Self::new(packet, None, false)
}
}
@@ -4,7 +4,6 @@
#[cfg(feature = "client")]
pub mod client;
pub mod forwarder;
pub mod metrics;
#[cfg(feature = "client")]
pub use client::{Client, Config, SendWithoutResponse};
@@ -1,311 +0,0 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use strum::{AsRefStr, EnumIter, EnumProperty, IntoEnumIterator};
use tokio::time::Instant;
/// Histogram buckets (seconds) for per-stage and total packet latency: exponential, ~100us .. ~6.5s.
/// Shared by every latency stage so the waterfall is directly comparable; the top finite bucket is
/// intentionally high so a rare multi-second processing spike is measured with magnitude rather than
/// being clipped into the `+Inf` overflow.
const STAGE_LATENCY_BUCKETS: [f64; 17] = [
0.0001, 0.0002, 0.0004, 0.0008, 0.0016, 0.0032, 0.0064, 0.0128, 0.0256, 0.0512, 0.1024, 0.2048,
0.4096, 0.8192, 1.6384, 3.2768, 6.5536,
];
/// Count buckets (1 .. MAX_DRAIN_BATCH) for the forwarder drain-batch-size histogram.
const DRAIN_BATCH_BUCKETS: [f64; 9] = [1.0, 2.0, 4.0, 8.0, 16.0, 32.0, 64.0, 128.0, 256.0];
/// Fill-ratio buckets (used/capacity) for the per-connection egress buffer. A ratio near 1.0 means
/// the buffer is close to full and packets to that peer are about to be dropped.
const EGRESS_FILL_BUCKETS: [f64; 9] = [0.05, 0.1, 0.25, 0.5, 0.75, 0.9, 0.95, 0.99, 1.0];
/// Every histogram this crate emits, defined in one place. `AsRefStr` (`#[strum(to_string=...)]`)
/// gives the prometheus metric name - the bare `mixnet_packet_*` family, with no per-crate prefix
/// since this is a shared library writing straight to the process-global registry. The `help` prop
/// gives the description and [`MixnetMetric::buckets`] gives the bucket layout.
///
/// Register the whole family at boot with [`register_all`]. Latency-stage variants are observed via
/// the [`PacketTrace`] stopwatch; the auxiliary variants via the `observe_*` helpers. (Passing an
/// auxiliary variant to `PacketTrace::record` is meaningless but harmless.)
#[derive(Clone, Copy, EnumIter, AsRefStr, EnumProperty)]
pub enum MixnetMetric {
// ----- latency stages: the per-packet waterfall, recorded via `PacketTrace` -----
/// receive -> sphinx unwrap (partial: shared secret + header MAC)
#[strum(to_string = "mixnet_packet_stage_unwrap_seconds")]
#[strum(props(help = "Seconds spent unwrapping a received sphinx packet"))]
Unwrap,
/// unwrap -> replay-check + finalise (includes the deferral wait)
#[strum(to_string = "mixnet_packet_stage_replay_check_seconds")]
#[strum(props(
help = "Seconds from partial-unwrap to replay-check + finalise (includes the deferral wait)"
))]
ReplayCheck,
/// wait in the ingress -> forwarder channel
#[strum(to_string = "mixnet_packet_stage_forwarder_queue_seconds")]
#[strum(props(
help = "Seconds a forwarded packet waited in the ingress-to-forwarder channel"
))]
ForwarderQueue,
/// the (intended) mix delay
#[strum(to_string = "mixnet_packet_stage_delay_queue_seconds")]
#[strum(props(help = "Seconds a forwarded packet spent in the (intended) mix delay queue"))]
DelayQueue,
/// diagnostic overlay on `DelayQueue`: how late beyond the target release the packet was
/// actually forwarded (delay-queue scheduling/retrieval overhead, measured vs the deadline)
#[strum(to_string = "mixnet_packet_stage_delay_queue_overrun_seconds")]
#[strum(props(
help = "Seconds a delayed packet was forwarded beyond its target release time (delay-queue scheduling/retrieval overhead)"
))]
DelayQueueOverrun,
/// wait in the per-connection egress buffer
#[strum(to_string = "mixnet_packet_stage_egress_queue_seconds")]
#[strum(props(
help = "Seconds a forwarded packet waited in the per-connection egress buffer"
))]
EgressQueue,
/// flushing the packet batch to the socket
#[strum(to_string = "mixnet_packet_stage_socket_write_seconds")]
#[strum(props(help = "Seconds spent flushing a forwarded packet batch to the socket"))]
SocketWrite,
/// end-to-end: receive -> socket write
#[strum(to_string = "mixnet_packet_total_latency_seconds")]
#[strum(props(help = "Total in-node latency of a forwarded packet, receive to socket write"))]
Total,
// ----- auxiliary histograms: observed directly, not part of the latency waterfall -----
/// number of packets the forwarder drained from the ingress channel per wakeup
#[strum(to_string = "mixnet_packet_forwarder_drain_batch_size")]
#[strum(props(
help = "Number of ingress packets the forwarder drained per select! wakeup (batch size)"
))]
ForwarderDrainBatchSize,
/// number of expired packets the forwarder drained from the delay queue per wakeup
#[strum(to_string = "mixnet_packet_forwarder_delay_drain_batch_size")]
#[strum(props(
help = "Number of expired delay-queue packets the forwarder drained per select! wakeup (batch size)"
))]
ForwarderDelayDrainBatchSize,
/// per-connection egress buffer occupancy (used/capacity) at send time
#[strum(to_string = "mixnet_packet_egress_buffer_fill_ratio")]
#[strum(props(
help = "Per-connection egress buffer fill ratio (used/capacity) sampled at packet send time"
))]
EgressBufferFillRatio,
}
impl MixnetMetric {
/// Histogram bucket layout for this metric.
fn buckets(&self) -> &'static [f64] {
match self {
MixnetMetric::ForwarderDrainBatchSize | MixnetMetric::ForwarderDelayDrainBatchSize => {
&DRAIN_BATCH_BUCKETS
}
MixnetMetric::EgressBufferFillRatio => &EGRESS_FILL_BUCKETS,
// every latency stage shares the seconds buckets
_ => &STAGE_LATENCY_BUCKETS,
}
}
}
/// Pre-register every histogram (at zero) into the global metrics registry so the whole
/// `mixnet_packet_*` family is present on the prometheus endpoint from boot, before anything has
/// been observed. Idempotent.
pub fn register_all() {
let registry = nym_metrics::metrics_registry();
for metric in MixnetMetric::iter() {
registry.register_histogram(
metric.as_ref(),
metric.get_str("help"),
Some(metric.buckets()),
);
}
}
/// Observe a value into a metric's histogram in the process-global registry.
fn observe(metric: MixnetMetric, value: f64) {
nym_metrics::metrics_registry().maybe_register_and_add_to_histogram(
metric.as_ref(),
value,
Some(metric.buckets()),
metric.get_str("help"),
);
}
/// Observe how many ingress-channel packets the forwarder drained in a single wakeup.
pub fn observe_drain_batch_size(batch_size: usize) {
observe(MixnetMetric::ForwarderDrainBatchSize, batch_size as f64);
}
/// Observe how many expired delay-queue packets the forwarder drained in a single wakeup.
pub fn observe_delay_drain_batch_size(batch_size: usize) {
observe(
MixnetMetric::ForwarderDelayDrainBatchSize,
batch_size as f64,
);
}
/// Observe how full a per-connection egress buffer was when a packet was queued for it.
pub fn observe_egress_buffer_fill(used: usize, capacity: usize) {
if capacity == 0 {
return;
}
observe(
MixnetMetric::EgressBufferFillRatio,
used as f64 / capacity as f64,
);
}
/// A lightweight per-packet stopwatch for attributing forwarding latency to pipeline
/// stages. Unsampled packets carry the `Off` variant and do zero clock reads, so the only
/// cost on the hot path is moving a small `Copy` value and a branch.
#[derive(Clone, Copy)]
pub enum PacketTrace {
Off,
On {
received_at: Instant,
stage_at: Instant,
},
}
impl PacketTrace {
/// Begin tracing. Reads the clock only for sampled packets.
pub fn start(sampled: bool) -> Self {
if sampled {
let now = Instant::now();
PacketTrace::On {
received_at: now,
stage_at: now,
}
} else {
PacketTrace::Off
}
}
/// Whether this packet is being traced (sampled).
pub fn is_sampled(&self) -> bool {
matches!(self, PacketTrace::On { .. })
}
/// Seconds spent in the stage just completed, advancing the cursor to now.
/// Returns `None` for unsampled packets.
fn lap(&mut self) -> Option<f64> {
match self {
PacketTrace::Off => None,
PacketTrace::On { stage_at, .. } => {
let now = Instant::now();
let secs = now.duration_since(*stage_at).as_secs_f64();
*stage_at = now;
Some(secs)
}
}
}
/// Seconds since tracing began (i.e. since the packet was received), or `None` if unsampled.
fn total(&self) -> Option<f64> {
match self {
PacketTrace::Off => None,
PacketTrace::On { received_at, .. } => {
Some(Instant::now().duration_since(*received_at).as_secs_f64())
}
}
}
/// Close out the stage just completed: lap the timer and, only if the packet is sampled,
/// observe `stage`'s latency histogram.
pub fn record(&mut self, stage: MixnetMetric) {
if let Some(secs) = self.lap() {
observe(stage, secs);
}
}
/// Observe the end-to-end [`MixnetMetric::Total`] latency (since receive) if sampled. Unlike
/// [`PacketTrace::record`] this does not lap, so it can be called at the very end.
pub fn record_total(&self) {
if let Some(secs) = self.total() {
observe(MixnetMetric::Total, secs);
}
}
/// Observe an explicit `secs` value for `stage` if the packet is sampled, without lapping the
/// stage cursor. For diagnostics that don't fit the sequential waterfall (e.g. delay-queue
/// overrun, measured against the target deadline rather than the previous stage).
pub fn record_value(&self, stage: MixnetMetric, secs: f64) {
if matches!(self, PacketTrace::On { .. }) {
observe(stage, secs);
}
}
}
/// A value paired with its in-flight latency trace, so the trace rides along as the value is
/// moved between pipeline stages (and transformed via [`Traced::map`]). Used wherever a packet
/// crosses a queue/channel: replay batch, delay queue, egress channel.
pub struct Traced<T> {
pub inner: T,
pub trace: PacketTrace,
}
impl<T> Traced<T> {
pub fn new(inner: T, trace: PacketTrace) -> Self {
Traced { inner, trace }
}
/// Transform the carried value, keeping the same trace.
pub fn map<U>(self, f: impl FnOnce(T) -> U) -> Traced<U> {
Traced {
inner: f(self.inner),
trace: self.trace,
}
}
/// Record the stage just completed for the carried trace (see [`PacketTrace::record`]).
pub fn record(&mut self, stage: MixnetMetric) {
self.trace.record(stage)
}
/// Observe an explicit value for the carried trace (see [`PacketTrace::record_value`]).
pub fn record_value(&self, stage: MixnetMetric, secs: f64) {
self.trace.record_value(stage, secs)
}
}
#[cfg(test)]
mod tests {
use super::*;
// guards that AsRefStr honours `#[strum(to_string = ...)]` (rather than falling back to the
// variant name), that every metric is in the `mixnet_packet_*` family, and carries a help
// string, and that each metric resolves to a bucket layout.
#[test]
fn every_metric_has_a_mixnet_packet_name_help_and_buckets() {
for metric in MixnetMetric::iter() {
assert!(
metric.as_ref().starts_with("mixnet_packet_"),
"unexpected metric name: {}",
metric.as_ref()
);
assert!(
metric.get_str("help").is_some(),
"missing help for {}",
metric.as_ref()
);
assert!(
!metric.buckets().is_empty(),
"missing buckets for {}",
metric.as_ref()
);
}
assert_eq!(
MixnetMetric::Unwrap.as_ref(),
"mixnet_packet_stage_unwrap_seconds"
);
assert_eq!(
MixnetMetric::Total.as_ref(),
"mixnet_packet_total_latency_seconds"
);
assert_eq!(
MixnetMetric::ForwarderDrainBatchSize.as_ref(),
"mixnet_packet_forwarder_drain_batch_size"
);
}
}
@@ -27,7 +27,6 @@ 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"] }
serde_json = { workspace = true }
@@ -18,9 +18,8 @@ use nym_api_requests::ecash::VerificationKeyResponse;
use nym_api_requests::models::network_monitor::{
KnownNetworkMonitorResponse, StressTestBatchSubmission,
};
use nym_api_requests::models::node_families::NodeFamily;
use nym_api_requests::models::{
AnnotationResponseV1, AnnotationResponseV2, ApiHealthResponse, BinaryBuildInformationOwned,
AnnotationResponseV1, ApiHealthResponse, BinaryBuildInformationOwned,
ChainBlocksStatusResponse, ChainStatusResponse, KeyRotationInfoResponse,
NodePerformanceResponse, NodeRefreshBody, NymNodeDescriptionV1, NymNodeDescriptionV2,
PerformanceHistoryResponse, RewardedSetResponse, SignerInformationResponse,
@@ -394,45 +393,6 @@ pub trait NymApiClientExt: ApiClient {
Ok(bonds)
}
#[tracing::instrument(level = "debug", skip_all)]
async fn get_node_families(
&self,
page: Option<u32>,
per_page: Option<u32>,
) -> Result<PaginatedResponse<NodeFamily>, NymAPIError> {
let mut params = Vec::new();
if let Some(page) = page {
params.push(("page", page.to_string()))
}
if let Some(per_page) = per_page {
params.push(("per_page", per_page.to_string()))
}
self.get_json(
&[routes::V1_API_VERSION, routes::NODE_FAMILIES_ROUTES],
&params,
)
.await
}
async fn get_all_node_families(&self) -> Result<Vec<NodeFamily>, NymAPIError> {
// TODO: deal with paging in macro or some helper function or something, because it's the same pattern everywhere
let mut page = 0;
let mut families = Vec::new();
loop {
let mut res = self.get_node_families(Some(page), None).await?;
families.append(&mut res.data);
if families.len() < res.pagination.total {
page += 1
} else {
break;
}
}
Ok(families)
}
#[deprecated]
#[tracing::instrument(level = "debug", skip_all)]
async fn get_basic_mixnodes(&self) -> Result<CachedNodesResponse<SkimmedNodeV1>, NymAPIError> {
@@ -1033,22 +993,6 @@ pub trait NymApiClientExt: ApiClient {
.await
}
async fn get_node_annotation_v2(
&self,
node_id: NodeId,
) -> Result<AnnotationResponseV2, NymAPIError> {
self.get_json(
&[
routes::V2_API_VERSION,
routes::NYM_NODES_ROUTES,
routes::NYM_NODES_ANNOTATION,
&node_id.to_string(),
],
NO_PARAMS,
)
.await
}
#[deprecated]
async fn get_mixnode_avg_uptime(&self, mix_id: NodeId) -> Result<UptimeResponse, NymAPIError> {
self.get_json(
@@ -38,7 +38,6 @@ pub mod ecash {
}
pub const NYM_NODES_ROUTES: &str = "nym-nodes";
pub const NODE_FAMILIES_ROUTES: &str = "node-families";
pub use nym_nodes::*;
pub mod nym_nodes {
@@ -867,10 +867,6 @@ mod tests {
MixnetExecuteMsg::TestingResolveAllPendingEvents { .. } => {
client.testing_resolve_all_pending_events(None).ignore()
}
// not expected to be exposed by the client
ExecuteMsg::AdminMigrateVestedMixNode { .. }
| ExecuteMsg::AdminMigrateVestedDelegation { .. }
| ExecuteMsg::AdminBatchMigrateVestedDelegations { .. } => ().ignore(),
};
}
}
@@ -14,7 +14,6 @@ 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;
@@ -25,7 +24,6 @@ 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;
@@ -38,7 +36,6 @@ 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};
@@ -49,7 +46,6 @@ 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;
@@ -60,7 +56,6 @@ pub trait NymContractsProvider {
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
fn ecash_contract_address(&self) -> Option<&AccountId>;
@@ -75,7 +70,6 @@ pub struct TypedNymContracts {
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>,
pub group_contract_address: Option<AccountId>,
@@ -104,10 +98,6 @@ impl TryFrom<NymContracts> for TypedNymContracts {
.network_monitors_contract_address
.map(|addr| addr.parse())
.transpose()?,
node_families_contract_address: value
.node_families_contract_address
.map(|addr| addr.parse())
.transpose()?,
ecash_contract_address: value
.ecash_contract_address
.map(|addr| addr.parse())
@@ -1,447 +0,0 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::collect_paged;
use crate::nyxd::contract_traits::NymContractsProvider;
use crate::nyxd::error::NyxdError;
use crate::nyxd::CosmWasmClient;
use async_trait::async_trait;
use cosmrs::AccountId;
use nym_mixnet_contract_common::NodeId;
use serde::Deserialize;
pub use nym_node_families_contract_common::{
msg::QueryMsg as NodeFamiliesQueryMsg, AllFamilyMembersPagedResponse,
AllPastFamilyInvitationsPagedResponse, Config, FamiliesPagedResponse, FamilyMemberRecord,
FamilyMembersPagedResponse, GlobalPastFamilyInvitationCursor, NodeFamily,
NodeFamilyByNameResponse, NodeFamilyByOwnerResponse, NodeFamilyId,
NodeFamilyMembershipResponse, NodeFamilyResponse, PastFamilyInvitation,
PastFamilyInvitationCursor, PastFamilyInvitationForNodeCursor,
PastFamilyInvitationsForNodePagedResponse, PastFamilyInvitationsPagedResponse,
PastFamilyMember, PastFamilyMemberCursor, PastFamilyMemberForNodeCursor,
PastFamilyMembersForNodePagedResponse, PastFamilyMembersPagedResponse,
PendingFamilyInvitationDetails, PendingFamilyInvitationResponse,
PendingFamilyInvitationsPagedResponse, PendingInvitationsForNodePagedResponse,
PendingInvitationsPagedResponse,
};
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait NodeFamiliesQueryClient {
async fn query_node_families_contract<T>(
&self,
query: NodeFamiliesQueryMsg,
) -> Result<T, NyxdError>
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,
) -> Result<NodeFamilyResponse, NyxdError> {
self.query_node_families_contract(NodeFamiliesQueryMsg::GetFamilyById { family_id })
.await
}
async fn get_family_by_owner(
&self,
owner: &AccountId,
) -> Result<NodeFamilyByOwnerResponse, NyxdError> {
self.query_node_families_contract(NodeFamiliesQueryMsg::GetFamilyByOwner {
owner: owner.to_string(),
})
.await
}
async fn get_family_by_name(
&self,
name: String,
) -> Result<NodeFamilyByNameResponse, NyxdError> {
self.query_node_families_contract(NodeFamiliesQueryMsg::GetFamilyByName { name })
.await
}
async fn get_families_paged(
&self,
start_after: Option<NodeFamilyId>,
limit: Option<u32>,
) -> Result<FamiliesPagedResponse, NyxdError> {
self.query_node_families_contract(NodeFamiliesQueryMsg::GetFamiliesPaged {
start_after,
limit,
})
.await
}
async fn get_family_membership(
&self,
node_id: NodeId,
) -> Result<NodeFamilyMembershipResponse, NyxdError> {
self.query_node_families_contract(NodeFamiliesQueryMsg::GetFamilyMembership { node_id })
.await
}
async fn get_family_members_paged(
&self,
family_id: NodeFamilyId,
start_after: Option<NodeId>,
limit: Option<u32>,
) -> Result<FamilyMembersPagedResponse, NyxdError> {
self.query_node_families_contract(NodeFamiliesQueryMsg::GetFamilyMembersPaged {
family_id,
start_after,
limit,
})
.await
}
async fn get_all_family_members_paged(
&self,
start_after: Option<NodeId>,
limit: Option<u32>,
) -> Result<AllFamilyMembersPagedResponse, NyxdError> {
self.query_node_families_contract(NodeFamiliesQueryMsg::GetAllFamilyMembersPaged {
start_after,
limit,
})
.await
}
async fn get_pending_invitation(
&self,
family_id: NodeFamilyId,
node_id: NodeId,
) -> Result<PendingFamilyInvitationResponse, NyxdError> {
self.query_node_families_contract(NodeFamiliesQueryMsg::GetPendingInvitation {
family_id,
node_id,
})
.await
}
async fn get_pending_invitations_for_family_paged(
&self,
family_id: NodeFamilyId,
start_after: Option<NodeId>,
limit: Option<u32>,
) -> Result<PendingFamilyInvitationsPagedResponse, NyxdError> {
self.query_node_families_contract(
NodeFamiliesQueryMsg::GetPendingInvitationsForFamilyPaged {
family_id,
start_after,
limit,
},
)
.await
}
async fn get_pending_invitations_for_node_paged(
&self,
node_id: NodeId,
start_after: Option<NodeFamilyId>,
limit: Option<u32>,
) -> Result<PendingInvitationsForNodePagedResponse, NyxdError> {
self.query_node_families_contract(NodeFamiliesQueryMsg::GetPendingInvitationsForNodePaged {
node_id,
start_after,
limit,
})
.await
}
async fn get_all_pending_invitations_paged(
&self,
start_after: Option<(NodeFamilyId, NodeId)>,
limit: Option<u32>,
) -> Result<PendingInvitationsPagedResponse, NyxdError> {
self.query_node_families_contract(NodeFamiliesQueryMsg::GetAllPendingInvitationsPaged {
start_after,
limit,
})
.await
}
async fn get_past_invitations_for_family_paged(
&self,
family_id: NodeFamilyId,
start_after: Option<PastFamilyInvitationCursor>,
limit: Option<u32>,
) -> Result<PastFamilyInvitationsPagedResponse, NyxdError> {
self.query_node_families_contract(NodeFamiliesQueryMsg::GetPastInvitationsForFamilyPaged {
family_id,
start_after,
limit,
})
.await
}
async fn get_past_invitations_for_node_paged(
&self,
node_id: NodeId,
start_after: Option<PastFamilyInvitationForNodeCursor>,
limit: Option<u32>,
) -> Result<PastFamilyInvitationsForNodePagedResponse, NyxdError> {
self.query_node_families_contract(NodeFamiliesQueryMsg::GetPastInvitationsForNodePaged {
node_id,
start_after,
limit,
})
.await
}
async fn get_all_past_invitations_paged(
&self,
start_after: Option<GlobalPastFamilyInvitationCursor>,
limit: Option<u32>,
) -> Result<AllPastFamilyInvitationsPagedResponse, NyxdError> {
self.query_node_families_contract(NodeFamiliesQueryMsg::GetAllPastInvitationsPaged {
start_after,
limit,
})
.await
}
async fn get_past_members_for_family_paged(
&self,
family_id: NodeFamilyId,
start_after: Option<PastFamilyMemberCursor>,
limit: Option<u32>,
) -> Result<PastFamilyMembersPagedResponse, NyxdError> {
self.query_node_families_contract(NodeFamiliesQueryMsg::GetPastMembersForFamilyPaged {
family_id,
start_after,
limit,
})
.await
}
async fn get_past_members_for_node_paged(
&self,
node_id: NodeId,
start_after: Option<PastFamilyMemberForNodeCursor>,
limit: Option<u32>,
) -> Result<PastFamilyMembersForNodePagedResponse, NyxdError> {
self.query_node_families_contract(NodeFamiliesQueryMsg::GetPastMembersForNodePaged {
node_id,
start_after,
limit,
})
.await
}
}
// extension trait to the query client to deal with the paged queries
// (it didn't feel appropriate to combine it with the existing trait)
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait PagedNodeFamiliesQueryClient: NodeFamiliesQueryClient {
async fn get_all_families(&self) -> Result<Vec<NodeFamily>, NyxdError> {
collect_paged!(self, get_families_paged, families)
}
async fn get_all_family_members_for_family(
&self,
family_id: NodeFamilyId,
) -> Result<Vec<FamilyMemberRecord>, NyxdError> {
collect_paged!(self, get_family_members_paged, members, family_id)
}
async fn get_all_family_members(&self) -> Result<Vec<FamilyMemberRecord>, NyxdError> {
collect_paged!(self, get_all_family_members_paged, members)
}
async fn get_all_pending_invitations_for_family(
&self,
family_id: NodeFamilyId,
) -> Result<Vec<PendingFamilyInvitationDetails>, NyxdError> {
collect_paged!(
self,
get_pending_invitations_for_family_paged,
invitations,
family_id
)
}
async fn get_all_pending_invitations_for_node(
&self,
node_id: NodeId,
) -> Result<Vec<PendingFamilyInvitationDetails>, NyxdError> {
collect_paged!(
self,
get_pending_invitations_for_node_paged,
invitations,
node_id
)
}
async fn get_all_pending_invitations(
&self,
) -> Result<Vec<PendingFamilyInvitationDetails>, NyxdError> {
collect_paged!(self, get_all_pending_invitations_paged, invitations)
}
async fn get_all_past_invitations_for_family(
&self,
family_id: NodeFamilyId,
) -> Result<Vec<PastFamilyInvitation>, NyxdError> {
collect_paged!(
self,
get_past_invitations_for_family_paged,
invitations,
family_id
)
}
async fn get_all_past_invitations_for_node(
&self,
node_id: NodeId,
) -> Result<Vec<PastFamilyInvitation>, NyxdError> {
collect_paged!(
self,
get_past_invitations_for_node_paged,
invitations,
node_id
)
}
async fn get_all_past_invitations(&self) -> Result<Vec<PastFamilyInvitation>, NyxdError> {
collect_paged!(self, get_all_past_invitations_paged, invitations)
}
async fn get_all_past_members_for_family(
&self,
family_id: NodeFamilyId,
) -> Result<Vec<PastFamilyMember>, NyxdError> {
collect_paged!(self, get_past_members_for_family_paged, members, family_id)
}
async fn get_all_past_members_for_node(
&self,
node_id: NodeId,
) -> Result<Vec<PastFamilyMember>, NyxdError> {
collect_paged!(self, get_past_members_for_node_paged, members, node_id)
}
}
#[async_trait]
impl<T> PagedNodeFamiliesQueryClient for T where T: NodeFamiliesQueryClient {}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl<C> NodeFamiliesQueryClient for C
where
C: CosmWasmClient + NymContractsProvider + Send + Sync,
{
async fn query_node_families_contract<T>(
&self,
query: NodeFamiliesQueryMsg,
) -> Result<T, NyxdError>
where
for<'a> T: Deserialize<'a>,
{
let node_families_contract_address = &self
.node_families_contract_address()
.ok_or_else(|| NyxdError::unavailable_contract_address("node families contract"))?;
self.query_contract_smart(node_families_contract_address, &query)
.await
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::nyxd::contract_traits::tests::IgnoreValue;
use nym_node_families_contract_common::QueryMsg;
// it's enough that this compiles and clippy is happy about it
#[allow(dead_code)]
fn all_query_variants_are_covered<C: NodeFamiliesQueryClient + Send + Sync>(
client: C,
msg: NodeFamiliesQueryMsg,
) {
match msg {
NodeFamiliesQueryMsg::GetConfig {} => client.get_config().ignore(),
NodeFamiliesQueryMsg::GetFamilyById { family_id } => {
client.get_family_by_id(family_id).ignore()
}
NodeFamiliesQueryMsg::GetFamilyByOwner { owner } => {
client.get_family_by_owner(&owner.parse().unwrap()).ignore()
}
NodeFamiliesQueryMsg::GetFamilyByName { name } => {
client.get_family_by_name(name).ignore()
}
NodeFamiliesQueryMsg::GetFamiliesPaged { start_after, limit } => {
client.get_families_paged(start_after, limit).ignore()
}
NodeFamiliesQueryMsg::GetFamilyMembership { node_id } => {
client.get_family_membership(node_id).ignore()
}
NodeFamiliesQueryMsg::GetFamilyMembersPaged {
family_id,
start_after,
limit,
} => client
.get_family_members_paged(family_id, start_after, limit)
.ignore(),
NodeFamiliesQueryMsg::GetAllFamilyMembersPaged { start_after, limit } => client
.get_all_family_members_paged(start_after, limit)
.ignore(),
NodeFamiliesQueryMsg::GetPendingInvitation { family_id, node_id } => {
client.get_pending_invitation(family_id, node_id).ignore()
}
NodeFamiliesQueryMsg::GetPendingInvitationsForFamilyPaged {
family_id,
start_after,
limit,
} => client
.get_pending_invitations_for_family_paged(family_id, start_after, limit)
.ignore(),
NodeFamiliesQueryMsg::GetPendingInvitationsForNodePaged {
node_id,
start_after,
limit,
} => client
.get_pending_invitations_for_node_paged(node_id, start_after, limit)
.ignore(),
NodeFamiliesQueryMsg::GetAllPendingInvitationsPaged { start_after, limit } => client
.get_all_pending_invitations_paged(start_after, limit)
.ignore(),
NodeFamiliesQueryMsg::GetPastInvitationsForFamilyPaged {
family_id,
start_after,
limit,
} => client
.get_past_invitations_for_family_paged(family_id, start_after, limit)
.ignore(),
NodeFamiliesQueryMsg::GetPastInvitationsForNodePaged {
node_id,
start_after,
limit,
} => client
.get_past_invitations_for_node_paged(node_id, start_after, limit)
.ignore(),
NodeFamiliesQueryMsg::GetAllPastInvitationsPaged { start_after, limit } => client
.get_all_past_invitations_paged(start_after, limit)
.ignore(),
NodeFamiliesQueryMsg::GetPastMembersForFamilyPaged {
family_id,
start_after,
limit,
} => client
.get_past_members_for_family_paged(family_id, start_after, limit)
.ignore(),
QueryMsg::GetPastMembersForNodePaged {
node_id,
start_after,
limit,
} => client
.get_past_members_for_node_paged(node_id, start_after, limit)
.ignore(),
};
}
}
@@ -1,281 +0,0 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::nyxd::coin::Coin;
use crate::nyxd::contract_traits::NymContractsProvider;
use crate::nyxd::cosmwasm_client::types::ExecuteResult;
use crate::nyxd::error::NyxdError;
use crate::nyxd::{Fee, SigningCosmWasmClient};
use crate::signing::signer::OfflineSigner;
use async_trait::async_trait;
use nym_mixnet_contract_common::NodeId;
use nym_node_families_contract_common::{
Config, ExecuteMsg as NodeFamiliesExecuteMsg, NodeFamilyId,
};
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait NodeFamiliesSigningClient {
async fn execute_node_families_contract(
&self,
fee: Option<Fee>,
msg: NodeFamiliesExecuteMsg,
memo: String,
funds: Vec<Coin>,
) -> Result<ExecuteResult, NyxdError>;
async fn update_node_families_config(
&self,
config: Config,
fee: Option<Fee>,
) -> Result<ExecuteResult, NyxdError> {
self.execute_node_families_contract(
fee,
NodeFamiliesExecuteMsg::UpdateConfig { config },
"NodeFamiliesContract::UpdateConfig".to_string(),
vec![],
)
.await
}
async fn create_family(
&self,
name: String,
description: String,
fee: Option<Fee>,
creation_fee: Vec<Coin>,
) -> Result<ExecuteResult, NyxdError> {
self.execute_node_families_contract(
fee,
NodeFamiliesExecuteMsg::CreateFamily { name, description },
"NodeFamiliesContract::CreateFamily".to_string(),
creation_fee,
)
.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,
NodeFamiliesExecuteMsg::DisbandFamily {},
"NodeFamiliesContract::DisbandFamily".to_string(),
vec![],
)
.await
}
async fn invite_to_family(
&self,
node_id: NodeId,
validity_secs: Option<u64>,
fee: Option<Fee>,
) -> Result<ExecuteResult, NyxdError> {
self.execute_node_families_contract(
fee,
NodeFamiliesExecuteMsg::InviteToFamily {
node_id,
validity_secs,
},
"NodeFamiliesContract::InviteToFamily".to_string(),
vec![],
)
.await
}
async fn revoke_family_invitation(
&self,
node_id: NodeId,
fee: Option<Fee>,
) -> Result<ExecuteResult, NyxdError> {
self.execute_node_families_contract(
fee,
NodeFamiliesExecuteMsg::RevokeFamilyInvitation { node_id },
"NodeFamiliesContract::RevokeFamilyInvitation".to_string(),
vec![],
)
.await
}
async fn accept_family_invitation(
&self,
family_id: NodeFamilyId,
node_id: NodeId,
fee: Option<Fee>,
) -> Result<ExecuteResult, NyxdError> {
self.execute_node_families_contract(
fee,
NodeFamiliesExecuteMsg::AcceptFamilyInvitation { family_id, node_id },
"NodeFamiliesContract::AcceptFamilyInvitation".to_string(),
vec![],
)
.await
}
async fn reject_family_invitation(
&self,
family_id: NodeFamilyId,
node_id: NodeId,
fee: Option<Fee>,
) -> Result<ExecuteResult, NyxdError> {
self.execute_node_families_contract(
fee,
NodeFamiliesExecuteMsg::RejectFamilyInvitation { family_id, node_id },
"NodeFamiliesContract::RejectFamilyInvitation".to_string(),
vec![],
)
.await
}
async fn leave_family(
&self,
node_id: NodeId,
fee: Option<Fee>,
) -> Result<ExecuteResult, NyxdError> {
self.execute_node_families_contract(
fee,
NodeFamiliesExecuteMsg::LeaveFamily { node_id },
"NodeFamiliesContract::LeaveFamily".to_string(),
vec![],
)
.await
}
async fn kick_from_family(
&self,
node_id: NodeId,
fee: Option<Fee>,
) -> Result<ExecuteResult, NyxdError> {
self.execute_node_families_contract(
fee,
NodeFamiliesExecuteMsg::KickFromFamily { node_id },
"NodeFamiliesContract::KickFromFamily".to_string(),
vec![],
)
.await
}
/// Cross-contract callback fired by the mixnet contract on node unbonding.
/// Exposed for completeness; the families contract rejects this call from
/// any sender other than the configured mixnet contract address.
async fn on_nym_node_unbond(
&self,
node_id: NodeId,
fee: Option<Fee>,
) -> Result<ExecuteResult, NyxdError> {
self.execute_node_families_contract(
fee,
NodeFamiliesExecuteMsg::OnNymNodeUnbond { node_id },
"NodeFamiliesContract::OnNymNodeUnbond".to_string(),
vec![],
)
.await
}
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl<C> NodeFamiliesSigningClient for C
where
C: SigningCosmWasmClient + NymContractsProvider + Sync,
NyxdError: From<<Self as OfflineSigner>::Error>,
{
async fn execute_node_families_contract(
&self,
fee: Option<Fee>,
msg: NodeFamiliesExecuteMsg,
memo: String,
funds: Vec<Coin>,
) -> Result<ExecuteResult, NyxdError> {
let node_families_contract_address = &self
.node_families_contract_address()
.ok_or_else(|| NyxdError::unavailable_contract_address("node families contract"))?;
let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier())));
let signer_address = &self.signer_addresses()[0];
self.execute(
signer_address,
node_families_contract_address,
&msg,
fee,
memo,
funds,
)
.await
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::nyxd::contract_traits::tests::IgnoreValue;
use nym_node_families_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: NodeFamiliesSigningClient + Send + Sync>(
client: C,
msg: NodeFamiliesExecuteMsg,
) {
match msg {
NodeFamiliesExecuteMsg::UpdateConfig { config } => {
client.update_node_families_config(config, None).ignore()
}
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,
validity_secs,
} => client
.invite_to_family(node_id, validity_secs, None)
.ignore(),
NodeFamiliesExecuteMsg::RevokeFamilyInvitation { node_id } => {
client.revoke_family_invitation(node_id, None).ignore()
}
NodeFamiliesExecuteMsg::AcceptFamilyInvitation { family_id, node_id } => client
.accept_family_invitation(family_id, node_id, None)
.ignore(),
NodeFamiliesExecuteMsg::RejectFamilyInvitation { family_id, node_id } => client
.reject_family_invitation(family_id, node_id, None)
.ignore(),
NodeFamiliesExecuteMsg::LeaveFamily { node_id } => {
client.leave_family(node_id, None).ignore()
}
NodeFamiliesExecuteMsg::KickFromFamily { node_id } => {
client.kick_from_family(node_id, None).ignore()
}
ExecuteMsg::OnNymNodeUnbond { node_id } => {
client.on_nym_node_unbond(node_id, None).ignore()
}
};
}
}
@@ -304,10 +304,6 @@ impl<C, S> NyxdClient<C, S> {
self.config.contracts.multisig_contract_address = Some(address);
}
pub fn set_node_families_contract_address(&mut self, address: AccountId) {
self.config.contracts.node_families_contract_address = Some(address);
}
pub fn set_simulated_gas_multiplier(&mut self, multiplier: f32) {
self.config.simulated_gas_multiplier = multiplier;
}
@@ -336,13 +332,6 @@ impl<C, S> NymContractsProvider for NyxdClient<C, S> {
.as_ref()
}
fn node_families_contract_address(&self) -> Option<&AccountId> {
self.config
.contracts
.node_families_contract_address
.as_ref()
}
fn ecash_contract_address(&self) -> Option<&AccountId> {
self.config.contracts.ecash_contract_address.as_ref()
}
@@ -30,9 +30,6 @@ pub struct Args {
#[clap(long)]
pub vesting_contract_address: Option<AccountId>,
#[clap(long)]
pub node_families_contract_address: Option<AccountId>,
#[clap(long)]
pub rewarding_denom: Option<String>,
@@ -133,14 +130,6 @@ pub async fn generate(args: Args) {
.expect("Failed converting vesting contract address to AccountId")
});
let node_families_contract_address = args.node_families_contract_address.unwrap_or_else(|| {
let address =
std::env::var(nym_network_defaults::var_names::NODE_FAMILIES_CONTRACT_ADDRESS)
.expect("node families contract address has to be set");
AccountId::from_str(address.as_str())
.expect("Failed converting node families contract address to AccountId")
});
let rewarding_denom = args.rewarding_denom.unwrap_or_else(|| {
std::env::var(nym_network_defaults::var_names::MIX_DENOM)
.expect("Rewarding (mix) denom has to be set")
@@ -153,7 +142,6 @@ pub async fn generate(args: Args) {
let instantiate_msg = InstantiateMsg {
rewarding_validator_address: rewarding_validator_address.to_string(),
vesting_contract_address: vesting_contract_address.to_string(),
node_families_contract_address: node_families_contract_address.to_string(),
rewarding_denom,
epochs_in_interval: args.epochs_in_interval,
epoch_duration: Duration::from_secs(args.epoch_duration),
@@ -26,14 +26,6 @@ pub trait ContractOpts {
fn addr_make(&self, input: &str) -> Addr;
fn make_sender_with_funds(&self, input: &str, funds: &[Coin]) -> MessageInfo {
message_info(&self.addr_make(input), funds)
}
fn make_sender(&self, input: &str) -> MessageInfo {
self.make_sender_with_funds(input, &[])
}
fn deps_mut_env(&mut self) -> (DepsMut<'_>, Env) {
let env = self.env().clone();
(self.deps_mut(), env)
@@ -1,14 +1,8 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
//! Blacklist types. **The blacklist surface is stubbed today** - the execute
//! handlers always return `UnimplementedBlacklisting` and the storage map is
//! never populated. These types are kept for the planned redesign.
use cosmwasm_schema::cw_serde;
/// Public-key + metadata pair surfaced by `GetBlacklistedAccount` /
/// `GetBlacklistPaged`. Always empty on a freshly deployed contract.
#[cw_serde]
pub struct BlacklistedAccount {
pub public_key: String,
@@ -21,8 +15,6 @@ impl From<(String, Blacklisting)> for BlacklistedAccount {
}
}
/// Per-key blacklist record: the multisig proposal that approved it and the
/// block height at which finalisation landed (None until finalised).
#[cw_serde]
pub struct Blacklisting {
pub proposal_id: u64,
@@ -44,8 +36,6 @@ impl BlacklistedAccount {
}
}
/// Page of blacklist entries returned by `GetBlacklistPaged`. Always empty on
/// a freshly deployed contract.
#[cw_serde]
pub struct PagedBlacklistedAccountResponse {
pub accounts: Vec<BlacklistedAccount>,
@@ -69,8 +59,6 @@ impl PagedBlacklistedAccountResponse {
}
}
/// Response shape for `GetBlacklistedAccount`. `account` is `None` for any
/// key not present in the (currently always-empty) blacklist.
#[cw_serde]
pub struct BlacklistedAccountResponse {
pub account: Option<Blacklisting>,
@@ -4,9 +4,6 @@
use cosmwasm_schema::cw_serde;
use cosmwasm_std::Coin;
/// Pool-level deposit accounting. Updated by every successful
/// `DepositTicketBookFunds` and (for the unredeemed-tickets counter) by every
/// successful legacy `RedeemTickets`.
#[cw_serde]
pub struct PoolCounters {
/// Represents the total amount of funds deposited into the contract.
@@ -5,13 +5,8 @@ use crate::error::EcashContractError;
use cosmwasm_schema::cw_serde;
use cosmwasm_std::{StdError, StdResult};
/// Sequential identifier assigned to every accepted deposit. Starts at 0 and
/// is never recycled.
pub type DepositId = u32;
/// Opaque on-chain record of a deposit: the depositor-claimed bs58-encoded
/// ed25519 identity public key. The contract does not verify control of the
/// corresponding private key.
#[cw_serde]
pub struct Deposit {
pub bs58_encoded_ed25519_pubkey: String,
@@ -24,8 +19,6 @@ impl Deposit {
}
}
/// Decode a bs58-encoded ed25519 public key to its 32-byte raw form.
/// Surfaces `MalformedEd25519Identity` on any bs58 / length failure.
pub fn get_ed25519_pubkey_bytes(raw: &str) -> Result<[u8; 32], EcashContractError> {
let mut ed25519_pubkey_bytes = [0u8; 32];
bs58::decode(raw)
@@ -39,13 +32,10 @@ impl Deposit {
bs58::encode(raw).into_string()
}
/// Decode this deposit's identity key to its 32-byte raw form for storage.
pub fn to_bytes(&self) -> Result<[u8; 32], EcashContractError> {
Self::get_ed25519_pubkey_bytes(&self.bs58_encoded_ed25519_pubkey)
}
/// Reconstruct a `Deposit` from a raw 32-byte ed25519 pubkey as stored
/// under the `"deposit"` namespace.
pub fn try_from_bytes(bytes: &[u8]) -> StdResult<Self> {
if bytes.len() != 32 {
return Err(StdError::generic_err("malformed deposit data"));
@@ -57,16 +47,12 @@ impl Deposit {
}
}
/// Response shape for `GetLatestDeposit`. `deposit` is `None` on a freshly
/// deployed contract.
#[cw_serde]
#[derive(Default)]
pub struct LatestDepositResponse {
pub deposit: Option<DepositData>,
}
/// Response shape for `GetDeposit { deposit_id }`. `deposit` is `None` when
/// the id has not yet been assigned (`id >= total_deposits_made`).
#[cw_serde]
pub struct DepositResponse {
pub id: DepositId,
@@ -74,8 +60,6 @@ pub struct DepositResponse {
pub deposit: Option<Deposit>,
}
/// `(deposit_id, deposit)` pair surfaced by the latest-deposit and paginated
/// deposit queries.
#[cw_serde]
pub struct DepositData {
pub id: DepositId,
@@ -89,8 +73,6 @@ impl From<(DepositId, Deposit)> for DepositData {
}
}
/// Page of deposits returned by `GetDepositsPaged`. `start_next_after` is the
/// id of the last returned entry; pass it as the next call's `start_after`.
#[cw_serde]
pub struct PagedDepositsResponse {
pub deposits: Vec<DepositData>,
@@ -6,108 +6,69 @@ use cw_controllers::AdminError;
use cw_utils::PaymentError;
use thiserror::Error;
/// Errors surfaced by the ecash contract. Each reachable variant is named in at
/// least one scenario of `openspec/specs/ecash-contract/spec.md`.
#[derive(Error, Debug, PartialEq)]
pub enum EcashContractError {
/// Wrapper for any underlying `cosmwasm_std::StdError` (storage faults,
/// address validation, etc.).
#[error(transparent)]
Std(#[from] StdError),
/// Raised by `cw_utils::must_pay` on `DepositTicketBookFunds` when funds
/// are missing, multi-denom, or in the wrong denom. Inner variants
/// `NoFunds`, `MultipleDenoms`, `MissingDenom` are all reachable.
#[error("Invalid deposit")]
InvalidDeposit(#[from] PaymentError),
/// `DepositTicketBookFunds` with the right denom but a non-matching amount.
/// `amount` is the reduced amount (if the sender is whitelisted) or the
/// default amount.
#[error("received wrong amount for deposit. got: {received}. required: {amount}")]
WrongAmount { received: Coin, amount: Coin },
/// **Unreachable** - preserved for forward compatibility (no current
/// execute path triggers this).
#[error("There aren't enough funds in the contract")]
NotEnoughFunds,
/// Wrapper for `cw_controllers::AdminError`. Raised by every admin-gated
/// and multisig-gated handler when the sender is wrong.
#[error(transparent)]
Admin(#[from] AdminError),
/// Redemption-proposal reply could not find a `proposal_id` attribute on
/// the multisig `wasm` event.
#[error("could not find proposal id inside the multisig reply SubMsg")]
MissingProposalId,
/// Redemption-proposal reply found a `proposal_id` attribute that could
/// not be parsed as `u64`. Realistically unreachable.
// realistically this should NEVER be thrown
#[error("the proposal id returned by the multisig contract could not be parsed into an u64")]
MalformedProposalId,
/// Instantiation given a `group_addr` that failed bech32 validation.
#[error("Group contract invalid address '{addr}'")]
InvalidGroup { addr: String },
/// **Unreachable** - no current execute path triggers this.
#[error("Unauthorized")]
Unauthorized,
/// **Unreachable** - preserved for future SemVer comparisons during migration.
#[error("Failed to parse {value} into a valid SemVer version: {error_message}")]
SemVerFailure {
value: String,
error_message: String,
},
/// Reply dispatcher saw an `id` that does not match
/// `BLACKLIST_PROPOSAL_REPLY_ID` or `REDEMPTION_PROPOSAL_REPLY_ID`.
#[error("received an invalid reply id: {id}. it does not correspond to any sent SubMsg")]
InvalidReplyId { id: u64 },
/// **Unreachable** - preserved for the (future) typed-deposit-info feature.
#[error("reached the maximum of 255 different deposit types")]
MaximumDepositTypesReached,
/// **Unreachable** - preserved for the (future) typed-deposit-info feature.
#[error("compressed deposit info {typ} does not corresponds to any known type")]
UnknownCompressedDepositInfoType { typ: u8 },
/// **Unreachable** - preserved for the (future) typed-deposit-info feature.
#[error("deposit info {typ} does not corresponds to any previously seen type")]
UnknownDepositInfoType { typ: String },
/// `DepositTicketBookFunds` with an `identity_key` that fails to bs58-decode
/// to exactly 32 bytes. Raised inside `Deposit::to_bytes` during
/// `save_deposit`.
#[error("the provided ed25519 identity was malformed")]
MalformedEd25519Identity,
/// `nym_network_defaults::TICKETBOOK_SIZE` has diverged from the value
/// snapshotted at instantiation in `Item<Invariants>`. Tripwire for
/// uncoordinated network-defaults bumps.
#[error("the ticket book size has changed since the contract was created! This was not expected! It used to be {at_init} but it's {current} now! Please let the developers know ASAP!")]
TicketBookSizeChanged { at_init: u64, current: u64 },
/// `RequestRedemption` with a `commitment_bs58` that does not decode to a
/// 32-byte sha256 digest.
#[error("the provided tickets redemption commitment is malformed")]
MalformedRedemptionCommitment,
/// Always thrown by `ProposeToBlacklist` and `AddToBlacklist` until the
/// blacklist redesign lands.
#[error("the account blacklisting hasn't been fully implemented yet")]
UnimplementedBlacklisting,
/// `SetReducedDepositPrice` (or migration whitelist seeding) given a coin
/// whose denom does not match `Config::deposit_amount.denom`.
#[error("reduced deposit must use the same denom as the default deposit (expected '{expected}', got '{got}')")]
InvalidReducedDepositDenom { expected: String, got: String },
/// `SetReducedDepositPrice` (or migration whitelist seeding) given a
/// reduced amount not strictly less than the current default.
#[error(
"reduced deposit amount ({reduced}) must be strictly less than the default ({default})"
)]
@@ -116,13 +77,9 @@ pub enum EcashContractError {
default: cosmwasm_std::Uint128,
},
/// `RemoveReducedDepositPrice` invoked for an address with no current
/// reduced-deposit entry.
#[error("address '{address}' does not have a custom reduced deposit price set")]
NoReducedDepositPrice { address: String },
/// `UpdateDefaultDepositValue` or `SetReducedDepositPrice` given an amount
/// below `nym_network_defaults::TICKETBOOK_SIZE`.
#[error(
"deposit amount ({amount}) must be at least the ticket book size ({ticket_book_size})"
)]
@@ -1,7 +1,4 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
/// Duplicate of `events::PROPOSAL_ID_ATTRIBUTE_NAME`. **Dead code**: not
/// referenced anywhere in the workspace today; preserved here pending a
/// follow-on cleanup. Use `events::PROPOSAL_ID_ATTRIBUTE_NAME` instead.
pub const BANDWIDTH_PROPOSAL_ID: &str = "proposal_id";
@@ -1,21 +1,9 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
//! Event names and attribute keys emitted by the ecash contract. Renaming any
//! of these is a breaking change for indexers and downstream tooling.
/// Event type emitted by every successful `DepositTicketBookFunds`. Carries a
/// single `deposit-id` attribute with the assigned id as a decimal string.
// event types
pub const DEPOSITED_FUNDS_EVENT_TYPE: &str = "deposited-funds";
/// Attribute key on the `deposited-funds` event: the newly assigned deposit id.
pub const DEPOSIT_ID: &str = "deposit-id";
/// Name of the cosmwasm-std auto-generated event that carries handler
/// attributes (`updated_deposit`, `action`, `address`, `deposit`,
/// `proposal_id`).
pub const WASM_EVENT_NAME: &str = "wasm";
/// Attribute key carrying the multisig-issued `proposal_id` on the `wasm`
/// event from the redemption-proposal reply handler.
pub const PROPOSAL_ID_ATTRIBUTE_NAME: &str = "proposal_id";
@@ -1,12 +1,6 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
//! Shared types, messages, events, and errors for the ecash contract.
//!
//! Consumed by both the contract crate (`contracts/ecash`) and any off-chain
//! client (gateways, nym-api signers, indexers, validator-client). See
//! `openspec/specs/ecash-contract/spec.md` for the normative interface.
pub mod blacklist;
pub mod counters;
pub mod deposit;
@@ -15,134 +15,100 @@ use crate::reduced_deposit::WhitelistedAccountsResponse;
#[cfg(feature = "schema")]
use cosmwasm_schema::QueryResponses;
/// Instantiation payload. The sender of the instantiate transaction becomes the
/// contract admin; the three addresses are bech32-validated and persisted as
/// immutable cross-contract pointers (see spec requirement "Contract instantiation").
#[cw_serde]
pub struct InstantiateMsg {
/// Cosmos SDK address reserved for the future pool-contract transition.
/// Stored in `Config` but never debited by the current contract.
pub holding_account: String,
/// cw3 multisig contract that gates `RedeemTickets` and (in the redesign)
/// blacklist proposals. Not updatable through any execute path.
pub multisig_addr: String,
/// cw4 group contract referenced by the (stubbed) blacklist proposal flow.
pub group_addr: String,
/// Default per-deposit price. The denom of this coin is the contract's
/// canonical denom for the rest of its lifetime.
pub deposit_amount: Coin,
}
#[cw_serde]
pub enum ExecuteMsg {
/// Submitted by clients to escrow funds and register a claimed ed25519
/// identity key. Mints a sequential `deposit_id`. The contract does not
/// verify control of the identity key - that proof is checked off-chain by
/// nym-api signers at blind-sign time.
DepositTicketBookFunds { identity_key: String },
/// Used by clients to request ticket books from the signers
DepositTicketBookFunds {
identity_key: String,
},
/// Submitted by gateways to request batch redemption of spent tickets.
/// Dispatches a `Propose` SubMsg to the multisig contract; the actual
/// transfer effect is gated behind multisig approval.
/// Used by gateways to batch redeem tokens from the spent tickets
RequestRedemption {
commitment_bs58: String,
number_of_tickets: u16,
},
/// **Legacy / dead code.** Only callable by the multisig; bumps the
/// unredeemed-tickets counter and emits a `ticket_redemption` event with
/// `moved_to_holding_account = "false"`. No known consumer depends on the
/// side effects; candidate for removal in a follow-on breaking-schema
/// change.
RedeemTickets { n: u16, gw: String },
/// The actual message that gets executed, after multisig votes, that transfers the ticket tokens into gateway's (and the holding) account
RedeemTickets {
n: u16,
gw: String,
},
/// Transfer the contract admin role. Only the current admin may sign.
/// Dispatches via the cw_controllers `execute_update_admin` handshake.
UpdateAdmin { admin: String },
UpdateAdmin {
admin: String,
},
/// Overwrite `Config::deposit_amount`. Only callable by the contract admin.
/// Rejects values below `nym_network_defaults::TICKETBOOK_SIZE` and trips
/// `TicketBookSizeChanged` if the snapshotted invariant has diverged from
/// the current crate constant.
#[serde(alias = "update_deposit_value")]
UpdateDefaultDepositValue { new_deposit: Coin },
UpdateDefaultDepositValue {
new_deposit: Coin,
},
/// Set (or overwrite) a reduced deposit price for a specific address.
/// Only callable by the contract admin.
SetReducedDepositPrice { address: String, deposit: Coin },
SetReducedDepositPrice {
address: String,
deposit: Coin,
},
/// Remove the reduced deposit price for a specific address, reverting them to
/// the default price. Returns an error if the address has no custom price set.
/// Only callable by the contract admin.
RemoveReducedDepositPrice { address: String },
RemoveReducedDepositPrice {
address: String,
},
/// **Stubbed**: always returns `EcashContractError::UnimplementedBlacklisting`.
/// Storage, reply handler, and helper paths exist but are unreachable from
/// the public ExecuteMsg surface. Preserved for the redesign.
ProposeToBlacklist { public_key: String },
/// **Stubbed**: always returns `EcashContractError::UnimplementedBlacklisting`.
AddToBlacklist { public_key: String },
// TODO: properly implement
ProposeToBlacklist {
public_key: String,
},
AddToBlacklist {
public_key: String,
},
}
#[cw_serde]
#[cfg_attr(feature = "schema", derive(QueryResponses))]
pub enum QueryMsg {
/// Look up a blacklist entry by its bs58-encoded ed25519 public key. Always
/// returns `None` on a freshly deployed contract because the blacklist
/// execute paths are stubbed.
#[cfg_attr(feature = "schema", returns(BlacklistedAccountResponse))]
GetBlacklistedAccount { public_key: String },
/// Paginated listing of blacklist entries. Always empty today (see stubbed
/// blacklist surface). Defaults: limit 50, max 75.
#[cfg_attr(feature = "schema", returns(PagedBlacklistedAccountResponse))]
GetBlacklistPaged {
limit: Option<u32>,
start_after: Option<String>,
},
/// Default per-deposit price (`Config::deposit_amount`). The
/// `GetRequiredDepositAmount` aliases are kept for backwards compatibility.
#[cfg_attr(feature = "schema", returns(Coin))]
#[serde(alias = "get_required_deposit_amount")]
#[serde(alias = "GetRequiredDepositAmount")]
GetDefaultDepositAmount {},
/// Per-address reduced deposit price override, if any. `None` for any
/// non-whitelisted address.
#[cfg_attr(feature = "schema", returns(Option<Coin>))]
GetReducedDepositAmount { address: String },
/// Enumerate every reduced-deposit whitelist entry in ascending address
/// order. Unpaginated by design (the whitelist is expected to stay small).
#[cfg_attr(feature = "schema", returns(WhitelistedAccountsResponse))]
GetAllWhitelistedAccounts {},
/// Look up a deposit by id. Returns `{ id, deposit: None }` when the id has
/// not yet been assigned.
#[cfg_attr(feature = "schema", returns(DepositResponse))]
GetDeposit { deposit_id: u32 },
/// Most recently assigned deposit (or `{ deposit: None }` on a fresh
/// contract). See `DepositStorage::latest_deposit`.
#[cfg_attr(feature = "schema", returns(LatestDepositResponse))]
GetLatestDeposit {},
/// Paginated listing of deposits in ascending id order. Defaults: limit 50,
/// max 100.
#[cfg_attr(feature = "schema", returns(PagedDepositsResponse))]
GetDepositsPaged {
limit: Option<u32>,
start_after: Option<u32>,
},
/// Aggregate statistics: global totals + per-account custom-price
/// breakdowns. Reassembled in a single read pass from `PoolCounters` and
/// `DepositStatsStorage`.
#[cfg_attr(feature = "schema", returns(DepositsStatistics))]
GetDepositsStatistics {},
}
@@ -1,8 +1,5 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
/// Title used for the cw3 `Propose` message dispatched by `RequestRedemption`.
/// nym-api signers cross-check this exact string when validating that an
/// in-flight proposal originated from the ecash contract.
// TODO: to be moved to multisig
pub const BATCH_REDEMPTION_PROPOSAL_TITLE: &str = "ecash-redemption";
@@ -4,16 +4,12 @@
use cosmwasm_schema::cw_serde;
use cosmwasm_std::{Addr, Coin};
/// Whitelist entry: an address and the reduced deposit price it may pay.
/// Persisted in the `"reduced_deposits"` storage map.
#[cw_serde]
pub struct WhitelistedAccount {
pub address: Addr,
pub deposit: Coin,
}
/// Response shape for `GetAllWhitelistedAccounts`. Unpaginated - the whitelist
/// is expected to stay small.
#[cw_serde]
pub struct WhitelistedAccountsResponse {
pub whitelisted_accounts: Vec<WhitelistedAccount>,
@@ -3,121 +3,12 @@
use crate::error::MixnetContractError;
use crate::mixnode::PendingMixNodeChanges;
use crate::nym_node::NodeOwnershipResponse;
use crate::{
EpochEventId, EpochId, Interval, IntervalEventId, MixNodeBond, MixNodeDetails, NodeId,
NodeRewarding, NymNodeBond, NymNodeDetails, PendingNodeChanges, QueryMsg,
EpochEventId, IntervalEventId, MixNodeBond, MixNodeDetails, NodeId, NodeRewarding, NymNodeBond,
NymNodeDetails, PendingNodeChanges,
};
use cosmwasm_std::{
Addr, Binary, Coin, CustomQuery, Decimal, QuerierWrapper, StdError, StdResult, Uint128,
from_json,
};
use cw_storage_plus::{Key, Namespace, Path, PrimaryKey};
use cosmwasm_std::{Coin, Decimal, StdError, StdResult, Uint128};
use nym_contracts_common::IdentityKeyRef;
use serde::de::DeserializeOwned;
use std::ops::Deref;
pub trait MixnetContractQuerier {
#[allow(dead_code)]
fn query_mixnet_contract<T: DeserializeOwned>(
&self,
address: impl Into<String>,
msg: &QueryMsg,
) -> StdResult<T>;
fn query_mixnet_contract_storage(
&self,
address: impl Into<String>,
key: impl Into<Binary>,
) -> StdResult<Option<Vec<u8>>>;
fn query_mixnet_contract_storage_value<T: DeserializeOwned>(
&self,
address: impl Into<String>,
key: impl Into<Binary>,
) -> StdResult<Option<T>> {
match self.query_mixnet_contract_storage(address, key)? {
None => Ok(None),
Some(value) => Ok(Some(from_json(&value)?)),
}
}
fn query_current_mixnet_interval(&self, address: impl Into<String>) -> StdResult<Interval> {
self.query_mixnet_contract_storage_value(address, b"ci")?
.ok_or(StdError::not_found(
"unable to retrieve interval information from the mixnet contract storage",
))
}
fn query_current_absolute_mixnet_epoch_id(
&self,
address: impl Into<String>,
) -> StdResult<EpochId> {
self.query_current_mixnet_interval(address)
.map(|interval| interval.current_epoch_absolute_id())
}
fn check_node_existence(&self, address: impl Into<String>, node_id: NodeId) -> StdResult<bool> {
let mixnet_contract_address = address.into();
if let Some(nym_node) = self.query_nymnode_bond(mixnet_contract_address.clone(), node_id)? {
return Ok(!nym_node.is_unbonding);
}
Ok(false)
}
fn query_nymnode_bond(
&self,
address: impl Into<String>,
node_id: NodeId,
) -> StdResult<Option<NymNodeBond>> {
// construct proper map key
let pk_namespace = "nn";
let path: Path<NymNodeBond> = Path::new(
Namespace::from_static_str(pk_namespace).as_slice(),
&node_id.key().iter().map(Key::as_ref).collect::<Vec<_>>(),
);
let storage_key = path.deref();
self.query_mixnet_contract_storage_value(address, storage_key)
}
fn query_nymnode_ownership(
&self,
address: impl Into<String>,
owner: &Addr,
) -> StdResult<Option<NymNodeBond>> {
let resp: NodeOwnershipResponse = self.query_mixnet_contract(
address,
&QueryMsg::GetOwnedNymNode {
address: owner.to_string(),
},
)?;
Ok(resp.details.map(|d| d.bond_information))
}
}
impl<C> MixnetContractQuerier for QuerierWrapper<'_, C>
where
C: CustomQuery,
{
fn query_mixnet_contract<T: DeserializeOwned>(
&self,
address: impl Into<String>,
msg: &QueryMsg,
) -> StdResult<T> {
self.query_wasm_smart(address, msg)
}
fn query_mixnet_contract_storage(
&self,
address: impl Into<String>,
key: impl Into<Binary>,
) -> StdResult<Option<Vec<u8>>> {
self.query_wasm_raw(address, key)
}
}
#[track_caller]
pub fn compare_decimals(a: Decimal, b: Decimal, epsilon: Option<Decimal>) {
@@ -30,7 +30,6 @@ pub use gateway::{
Gateway, GatewayBond, GatewayBondResponse, GatewayConfigUpdate, GatewayOwnershipResponse,
PagedGatewayResponse,
};
pub use helpers::MixnetContractQuerier;
pub use interval::{
CurrentIntervalResponse, EpochId, EpochState, EpochStatus, Interval, IntervalId,
};
@@ -190,10 +190,6 @@ impl NodeRewarding {
truncate_reward(self.operator, denom)
}
pub fn delegations_with_reward(&self, denom: impl Into<String>) -> Coin {
truncate_reward(self.delegates, denom)
}
pub fn pending_delegator_reward(&self, delegation: &Delegation) -> StdResult<Coin> {
let delegator_reward = self.determine_delegation_reward(delegation)?;
Ok(truncate_reward(delegator_reward, &delegation.amount.denom))
@@ -63,7 +63,6 @@ use nym_contracts_common::{ContractBuildInformation, signing::Nonce};
pub struct InstantiateMsg {
pub rewarding_validator_address: String,
pub vesting_contract_address: String,
pub node_families_contract_address: String,
pub rewarding_denom: String,
pub epochs_in_interval: u32,
@@ -306,22 +305,6 @@ pub enum ExecuteMsg {
MigrateVestedDelegation {
mix_id: NodeId,
},
/// Admin-only: forcibly migrate the vested mixnode owned by `owner`.
/// Used to drain the last vested entries so the mixnet contract can drop its dependency on the vesting contract.
AdminMigrateVestedMixNode {
owner: String,
},
/// Admin-only: forcibly migrate the vested delegation `(mix_id, owner)`.
/// Used to drain the last vested entries so the mixnet contract can drop its dependency on the vesting contract.
AdminMigrateVestedDelegation {
mix_id: NodeId,
owner: String,
},
/// Admin-only: batch variant of [`ExecuteMsg::AdminMigrateVestedDelegation`].
/// Reverts the entire batch on the first error, so callers should treat it as all-or-nothing.
AdminBatchMigrateVestedDelegations {
entries: Vec<VestedDelegationMigrationEntry>,
},
// testing-only
#[cfg(feature = "contract-testing")]
@@ -411,15 +394,6 @@ impl ExecuteMsg {
}
ExecuteMsg::MigrateVestedMixNode { .. } => "migrate vested mixnode".into(),
ExecuteMsg::MigrateVestedDelegation { .. } => "migrate vested delegation".to_string(),
ExecuteMsg::AdminMigrateVestedMixNode { owner } => {
format!("admin migrating vested mixnode of {owner}")
}
ExecuteMsg::AdminMigrateVestedDelegation { mix_id, owner } => {
format!("admin migrating vested delegation of {owner} on mixnode {mix_id}")
}
ExecuteMsg::AdminBatchMigrateVestedDelegations { entries } => {
format!("admin batch migrating {} vested delegations", entries.len())
}
ExecuteMsg::AssignRoles { .. } => "assigning epoch roles".into(),
ExecuteMsg::MigrateMixnode { .. } => "migrating legacy mixnode".into(),
ExecuteMsg::MigrateGateway { .. } => "migrating legacy gateway".into(),
@@ -907,15 +881,8 @@ pub enum QueryMsg {
GetKeyRotationId {},
}
#[cw_serde]
pub struct VestedDelegationMigrationEntry {
pub mix_id: NodeId,
pub owner: String,
}
#[cw_serde]
pub struct MigrateMsg {
pub unsafe_skip_state_updates: Option<bool>,
pub vesting_contract_address: Option<String>,
pub node_families_contract_address: String,
}
@@ -212,10 +212,6 @@ pub struct ContractState {
/// track-related messages.
pub vesting_contract_address: Addr,
/// Address of the node families contract. It is called whenever nym-node unbonds
/// so that it could be removed from any family it belongs to.
pub node_families_contract_address: Addr,
/// The expected denom used for rewarding (and realistically any other operation).
/// Default: `unym`
pub rewarding_denom: String,
@@ -1,32 +0,0 @@
[package]
name = "nym-node-families-contract-common"
description = "Common crate for Nym's node families contract"
version.workspace = true
authors.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
homepage.workspace = true
documentation.workspace = true
rust-version = "1.85"
readme.workspace = true
publish = true
[dependencies]
thiserror = { workspace = true }
serde = { workspace = true }
schemars = { workspace = true }
cosmwasm-std = { workspace = true }
cosmwasm-schema = { workspace = true }
cw-controllers = { workspace = true }
cw-utils = { workspace = true }
nym-contracts-common = { workspace = true }
nym-mixnet-contract-common = { workspace = true }
[features]
schema = []
[lints]
workspace = true
@@ -1,114 +0,0 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
/// Storage key constants used by the node families contract.
///
/// They are kept in the common crate so that off-chain tooling (indexers, migration
/// scripts) can reference them without depending on the contract crate itself.
/// Changing any of these values is a breaking change for already-deployed contracts.
pub mod storage_keys {
/// `Item<Addr>`: address of the mixnet contract used to validate node existence.
pub const MIXNET_CONTRACT_ADDRESS: &str = "mixnet-contract-address";
/// `Item<Config>`: runtime configuration (fees, length limits) set at instantiation.
pub const CONFIG: &str = "config";
/// `Admin` (cw-controllers): admin allowed to perform privileged operations.
pub const CONTRACT_ADMIN: &str = "contract-admin";
/// `Item<NodeFamilyId>`: monotonically increasing id counter for new families.
pub const NODE_FAMILY_ID_COUNTER: &str = "node-family-id-counter";
/// Primary namespace for the current family-members `IndexedMap`,
/// keyed by `NodeId` with value [`crate::FamilyMembership`].
pub const NODE_FAMILY_MEMBERS: &str = "node-family-members";
/// Multi-index over current family members keyed by family id —
/// enables paginated listing of all nodes in a given family.
pub const NODE_FAMILY_MEMBERS_FAMILY_IDX_NAMESPACE: &str = "node-family-members__family";
/// Primary namespace for the families `IndexedMap`.
pub const FAMILIES_NAMESPACE: &str = "families";
/// Secondary unique index keyed by `owner` (one family per owner).
pub const FAMILIES_OWNER_IDX_NAMESPACE: &str = "families__owner";
/// Secondary unique index keyed by `name` (family names are globally unique).
pub const FAMILIES_NAME_IDX_NAMESPACE: &str = "families__name";
/// Primary namespace for the pending invitations `IndexedMap`.
pub const INVITATIONS_NAMESPACE: &str = "invitations";
/// Multi-index over pending invitations keyed by family id.
pub const INVITATIONS_FAMILY_IDX_NAMESPACE: &str = "invitations__family";
/// Multi-index over pending invitations keyed by node id
/// (a node can be invited to multiple families simultaneously).
pub const INVITATIONS_NODE_IDX_NAMESPACE: &str = "invitations__node";
/// Primary namespace for the archived (accepted/rejected/revoked) invitations `IndexedMap`.
pub const PAST_INVITATIONS_NAMESPACE: &str = "past-invitations";
/// Multi-index over past invitations keyed by family id.
pub const PAST_INVITATIONS_FAMILY_IDX_NAMESPACE: &str = "past-invitations__family";
/// Multi-index over past invitations keyed by node id.
pub const PAST_INVITATIONS_NODE_IDX_NAMESPACE: &str = "past-invitations__node";
/// `Map<(NodeFamilyId, NodeId), u64>`: per-`(family, node)` counter used to
/// disambiguate repeat archive entries (a node can be invited and have the
/// invitation reach a terminal state more than once).
pub const PAST_INVITATIONS_COUNTER_NAMESPACE: &str = "past-invitations-counter";
/// Primary namespace for the past-members `IndexedMap`.
pub const PAST_FAMILY_MEMBER_NAMESPACE: &str = "past-family-member";
/// Multi-index over past members keyed by family id.
pub const PAST_FAMILY_MEMBER_FAMILY_IDX_NAMESPACE: &str = "past-family-member__family";
/// Multi-index over past members keyed by node id.
pub const PAST_FAMILY_MEMBER_NODE_IDX_NAMESPACE: &str = "past-family-member__node";
/// `Map<(NodeFamilyId, NodeId), u64>`: per-`(family, node)` counter used to
/// disambiguate repeat past-membership entries (a node can join and leave
/// the same family more than once).
pub const PAST_FAMILY_MEMBER_COUNTER_NAMESPACE: &str = "past-family-member-counter";
}
pub mod events {
pub const FAMILY_CREATION_EVENT_NAME: &str = "family_creation";
pub const FAMILY_CREATION_EVENT_FAMILY_NAME: &str = "family_name";
pub const FAMILY_CREATION_EVENT_OWNER_ADDRESS: &str = "owner_address";
pub const FAMILY_CREATION_EVENT_FAMILY_ID: &str = "family_id";
pub const FAMILY_CREATION_EVENT_PAID_FEE: &str = "paid_fee";
pub const FAMILY_DISBAND_EVENT_NAME: &str = "family_disband";
pub const FAMILY_DISBAND_EVENT_FAMILY_ID: &str = "family_id";
pub const FAMILY_DISBAND_EVENT_OWNER_ADDRESS: &str = "owner_address";
pub const FAMILY_DISBAND_EVENT_REFUNDED_FEE: &str = "refunded_fee";
pub const FAMILY_INVITATION_EVENT_NAME: &str = "family_invitation";
pub const FAMILY_INVITATION_EVENT_FAMILY_ID: &str = "family_id";
pub const FAMILY_INVITATION_EVENT_NODE_ID: &str = "node_id";
pub const FAMILY_INVITATION_EVENT_EXPIRES_AT: &str = "expires_at";
pub const FAMILY_INVITATION_REVOKED_EVENT_NAME: &str = "family_invitation_revoked";
pub const FAMILY_INVITATION_REVOKED_EVENT_FAMILY_ID: &str = "family_id";
pub const FAMILY_INVITATION_REVOKED_EVENT_NODE_ID: &str = "node_id";
pub const FAMILY_INVITATION_ACCEPTED_EVENT_NAME: &str = "family_invitation_accepted";
pub const FAMILY_INVITATION_ACCEPTED_EVENT_FAMILY_ID: &str = "family_id";
pub const FAMILY_INVITATION_ACCEPTED_EVENT_NODE_ID: &str = "node_id";
pub const FAMILY_INVITATION_REJECTED_EVENT_NAME: &str = "family_invitation_rejected";
pub const FAMILY_INVITATION_REJECTED_EVENT_FAMILY_ID: &str = "family_id";
pub const FAMILY_INVITATION_REJECTED_EVENT_NODE_ID: &str = "node_id";
pub const FAMILY_MEMBER_LEFT_EVENT_NAME: &str = "family_member_left";
pub const FAMILY_MEMBER_LEFT_EVENT_FAMILY_ID: &str = "family_id";
pub const FAMILY_MEMBER_LEFT_EVENT_NODE_ID: &str = "node_id";
pub const FAMILY_MEMBER_KICKED_EVENT_NAME: &str = "family_member_kicked";
pub const FAMILY_MEMBER_KICKED_EVENT_FAMILY_ID: &str = "family_id";
pub const FAMILY_MEMBER_KICKED_EVENT_NODE_ID: &str = "node_id";
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";
}
@@ -1,161 +0,0 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use crate::NodeFamilyId;
use cosmwasm_std::{Addr, Coin};
use cw_controllers::AdminError;
use cw_utils::PaymentError;
use nym_mixnet_contract_common::NodeId;
use thiserror::Error;
/// Errors returned from any entry point of the node families contract.
#[derive(Error, Debug, PartialEq)]
pub enum NodeFamiliesContractError {
/// Returned from `migrate` when the on-chain state cannot be brought forward
/// to the current contract version (e.g. unsupported source version, malformed
/// stored data).
#[error("could not perform contract migration: {comment}")]
FailedMigration { comment: String },
/// The referenced family does not exist (or no longer exists).
#[error("family with id {family_id} does not exist")]
FamilyNotFound { family_id: NodeFamilyId },
/// Disbanding was requested on a family that still has members.
#[error("family {family_id} cannot be disbanded: it still has {members} member(s)")]
FamilyNotEmpty {
family_id: NodeFamilyId,
members: u64,
},
/// The given node is not currently a member of any family.
#[error("node {node_id} is not currently a member of any family")]
NodeNotInFamily { node_id: NodeId },
/// The given node is a member of a different family than the one the
/// caller is acting on. Distinct from [`NodeNotInFamily`] (which means the
/// node has no membership at all) — surfaces when, e.g., a family owner
/// tries to kick a node that belongs to someone else's family.
#[error("node {node_id} is not a member of family {family_id}")]
NodeNotMemberOfFamily {
node_id: NodeId,
family_id: NodeFamilyId,
},
/// A cross-contract callback (e.g. `OnNymNodeUnbond`) was received from a
/// sender that is not the configured mixnet contract address.
#[error("address {sender} is not authorised to invoke the mixnet-contract callback")]
UnauthorisedMixnetCallback { sender: Addr },
/// No pending invitation exists for the given `(family, node)` pair.
#[error("no pending invitation for node {node_id} from family {family_id}")]
InvitationNotFound {
family_id: NodeFamilyId,
node_id: NodeId,
},
/// A pending invitation for the given `(family, node)` pair already exists;
/// issuing a new one would silently overwrite it.
#[error("a pending invitation for node {node_id} from family {family_id} already exists")]
PendingInvitationAlreadyExists {
family_id: NodeFamilyId,
node_id: NodeId,
},
/// The invitation exists but its `expires_at` is at or before the current
/// block time, so it can no longer be acted on.
#[error(
"invitation for node {node_id} from family {family_id} expired at {expires_at} (now: {now})"
)]
InvitationExpired {
family_id: NodeFamilyId,
node_id: NodeId,
expires_at: u64,
now: u64,
},
/// The funds attached to a paid execution failed `cw_utils` payment
/// validation (no funds, wrong/extra denom).
#[error("invalid fee provided: {0}")]
InvalidDeposit(#[from] PaymentError),
/// The funds attached to a `CreateFamily` execution don't match the
/// configured `create_family_fee`.
#[error("expected exactly {expected} as family creation fee; received {received:?}")]
InvalidFamilyCreationFee { expected: Coin, received: Vec<Coin> },
/// The submitted family name normalised to the empty string (i.e. it
/// contained no ASCII alphanumeric characters).
#[error("family name cannot be empty after normalisation")]
EmptyFamilyName,
/// The submitted family name exceeds the configured length limit.
#[error("family name length {length} exceeds the configured limit of {limit}")]
FamilyNameTooLong { length: usize, limit: usize },
/// The submitted family description exceeds the configured length limit.
#[error("family description length {length} exceeds the configured limit of {limit}")]
FamilyDescriptionTooLong { length: usize, limit: usize },
/// The transaction sender already owns a family.
#[error("address {address} already owns family {family_id}")]
SenderAlreadyOwnsAFamily {
address: Addr,
family_id: NodeFamilyId,
},
/// The transaction sender does not currently own any family - emitted by
/// owner-gated operations like `disband_family` when the sender has
/// nothing to act on.
#[error("address {address} does not currently own any family")]
SenderDoesntOwnAFamily { address: Addr },
/// The transaction sender is not the controller of the bonded node
/// referenced by the message. Covers all of: sender controls no bonded
/// node, sender controls a different node id, and sender's node has
/// entered the unbonding state.
#[error("address {address} is not the controller of bonded node {node_id}")]
SenderDoesntControlNode { address: Addr, node_id: NodeId },
/// A family with the requested (normalised) name already exists.
#[error("a family with name {name:?} already exists (id {family_id})")]
FamilyNameAlreadyTaken {
name: String,
family_id: NodeFamilyId,
},
/// A node controlled by the address is currently a member of a family,
/// so the address cannot also become a family owner or join another family.
#[error("address {address} controls node {node_id} which is currently in family {family_id}")]
AlreadyInFamily {
address: Addr,
node_id: NodeId,
family_id: NodeFamilyId,
},
/// The node referenced by an invitation does not exist as a bonded node
/// in the mixnet contract (or has already unbonded).
#[error("node {node_id} is not a bonded node in the mixnet contract")]
NodeDoesntExist { node_id: NodeId },
/// The node referenced by an invitation is already a member of a family,
/// so it cannot be invited to another one until it leaves / is removed.
#[error("node {node_id} is already a member of family {family_id}")]
NodeAlreadyInFamily {
node_id: NodeId,
family_id: NodeFamilyId,
},
/// The sender supplied a `validity_secs` of `0` for an invitation, which
/// would create one that is already expired at the moment it is stored.
#[error("invitation validity must be strictly positive")]
ZeroInvitationValidity,
/// Wraps errors raised by `cw-controllers::Admin` (e.g. caller is not admin).
#[error(transparent)]
Admin(#[from] AdminError),
/// Wraps any underlying `cosmwasm_std::StdError` (storage, serialization, etc.).
#[error(transparent)]
StdErr(#[from] cosmwasm_std::StdError),
}
@@ -1,22 +0,0 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
//! Common types, messages, errors and storage-key constants shared between the
//! node families contract and any off-chain client.
//!
//! Keeping these in a separate crate allows clients to depend on the contract's
//! public surface without pulling in `cw-storage-plus` and other on-chain-only
//! dependencies.
/// Storage-key string constants. See [`constants::storage_keys`].
pub mod constants;
/// Contract-level error type.
pub mod error;
/// `InstantiateMsg`, `ExecuteMsg`, `QueryMsg`, `MigrateMsg` definitions.
pub mod msg;
/// Domain types stored in / returned by the contract.
pub mod types;
pub use error::*;
pub use msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg};
pub use types::*;
@@ -1,225 +0,0 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use crate::{
Config, GlobalPastFamilyInvitationCursor, NodeFamilyId, PastFamilyInvitationCursor,
PastFamilyInvitationForNodeCursor, PastFamilyMemberCursor, PastFamilyMemberForNodeCursor,
};
use cosmwasm_schema::cw_serde;
use nym_mixnet_contract_common::NodeId;
#[cfg(feature = "schema")]
use crate::{
AllFamilyMembersPagedResponse, AllPastFamilyInvitationsPagedResponse, FamiliesPagedResponse,
FamilyMembersPagedResponse, NodeFamilyByNameResponse, NodeFamilyByOwnerResponse,
NodeFamilyMembershipResponse, NodeFamilyResponse, PastFamilyInvitationsForNodePagedResponse,
PastFamilyInvitationsPagedResponse, PastFamilyMembersForNodePagedResponse,
PastFamilyMembersPagedResponse, PendingFamilyInvitationResponse,
PendingFamilyInvitationsPagedResponse, PendingInvitationsForNodePagedResponse,
PendingInvitationsPagedResponse,
};
/// Message used to instantiate the node families contract.
#[cw_serde]
pub struct InstantiateMsg {
pub config: Config,
pub mixnet_contract_address: String,
}
/// Execute messages accepted by the contract.
#[cw_serde]
pub enum ExecuteMsg {
/// Replace the contract's runtime [`Config`]. Restricted to the contract
/// admin.
UpdateConfig { config: Config },
/// Create a new family owned by the message sender. The configured
/// `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 {},
/// Invite a node to the family owned by the message sender. If
/// `validity_secs` is omitted the invitation expires
/// `default_invitation_validity_secs` seconds (from [`Config`]) after the
/// current block time.
InviteToFamily {
node_id: NodeId,
validity_secs: Option<u64>,
},
/// Revoke a still-pending invitation previously issued by the sender's
/// family.
RevokeFamilyInvitation { node_id: NodeId },
/// Accept a pending invitation. The sender must control `node_id`.
AcceptFamilyInvitation {
family_id: NodeFamilyId,
node_id: NodeId,
},
/// Reject a pending invitation. The sender must control `node_id`.
RejectFamilyInvitation {
family_id: NodeFamilyId,
node_id: NodeId,
},
/// Leave the family `node_id` currently belongs to. The sender must
/// control `node_id`.
LeaveFamily { node_id: NodeId },
/// Remove `node_id` from the family owned by the message sender.
KickFromFamily { node_id: NodeId },
/// Cross-contract callback fired by the mixnet contract the moment
/// node with `node_id` initiates unbonding.
/// Removes the node from any family it currently
/// belongs to and rejects every pending invitation issued to it.
/// Sender must be the configured mixnet contract address.
OnNymNodeUnbond { node_id: NodeId },
}
/// Query messages accepted by the contract.
#[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 },
/// Look up the (at most one) family owned by a given address.
#[cfg_attr(feature = "schema", returns(NodeFamilyByOwnerResponse))]
GetFamilyByOwner { owner: String },
/// Look up a single family by its name. The lookup is normalised
/// contract-side (lowercased, non-alphanumerics stripped), so equivalent
/// inputs resolve to the same family.
#[cfg_attr(feature = "schema", returns(NodeFamilyByNameResponse))]
GetFamilyByName { name: String },
#[cfg_attr(feature = "schema", returns(FamiliesPagedResponse))]
GetFamiliesPaged {
start_after: Option<NodeFamilyId>,
limit: Option<u32>,
},
/// Look up which family — if any — a node currently belongs to.
#[cfg_attr(feature = "schema", returns(NodeFamilyMembershipResponse))]
GetFamilyMembership { node_id: NodeId },
/// Page through every node currently in a given family.
#[cfg_attr(feature = "schema", returns(FamilyMembersPagedResponse))]
GetFamilyMembersPaged {
family_id: NodeFamilyId,
start_after: Option<NodeId>,
limit: Option<u32>,
},
/// Page through every current family member across all families, in
/// ascending [`NodeId`] order. Each entry carries the membership record
/// (which in turn names the family the node belongs to).
#[cfg_attr(feature = "schema", returns(AllFamilyMembersPagedResponse))]
GetAllFamilyMembersPaged {
start_after: Option<NodeId>,
limit: Option<u32>,
},
/// Look up the pending invitation for a specific `(family_id, node_id)`
/// pair.
#[cfg_attr(feature = "schema", returns(PendingFamilyInvitationResponse))]
GetPendingInvitation {
family_id: NodeFamilyId,
node_id: NodeId,
},
/// Page through every pending invitation issued by a given family.
#[cfg_attr(feature = "schema", returns(PendingFamilyInvitationsPagedResponse))]
GetPendingInvitationsForFamilyPaged {
family_id: NodeFamilyId,
start_after: Option<NodeId>,
limit: Option<u32>,
},
/// Page through every pending invitation issued for a given node.
#[cfg_attr(feature = "schema", returns(PendingInvitationsForNodePagedResponse))]
GetPendingInvitationsForNodePaged {
node_id: NodeId,
start_after: Option<NodeFamilyId>,
limit: Option<u32>,
},
/// Page through every pending invitation across all families.
#[cfg_attr(feature = "schema", returns(PendingInvitationsPagedResponse))]
GetAllPendingInvitationsPaged {
start_after: Option<(NodeFamilyId, NodeId)>,
limit: Option<u32>,
},
/// Page through every archived (terminal-state) invitation issued by a
/// given family.
#[cfg_attr(feature = "schema", returns(PastFamilyInvitationsPagedResponse))]
GetPastInvitationsForFamilyPaged {
family_id: NodeFamilyId,
start_after: Option<PastFamilyInvitationCursor>,
limit: Option<u32>,
},
/// Page through every archived (terminal-state) invitation issued to a
/// given node.
#[cfg_attr(feature = "schema", returns(PastFamilyInvitationsForNodePagedResponse))]
GetPastInvitationsForNodePaged {
node_id: NodeId,
start_after: Option<PastFamilyInvitationForNodeCursor>,
limit: Option<u32>,
},
/// Page through every archived (terminal-state) invitation across all
/// families.
#[cfg_attr(feature = "schema", returns(AllPastFamilyInvitationsPagedResponse))]
GetAllPastInvitationsPaged {
start_after: Option<GlobalPastFamilyInvitationCursor>,
limit: Option<u32>,
},
/// Page through every archived membership record for a given family
/// (nodes that used to belong to it but have since been removed).
#[cfg_attr(feature = "schema", returns(PastFamilyMembersPagedResponse))]
GetPastMembersForFamilyPaged {
family_id: NodeFamilyId,
start_after: Option<PastFamilyMemberCursor>,
limit: Option<u32>,
},
/// Page through every archived membership record for a given node
/// (every family the node used to belong to but has since been removed
/// from), across all families.
#[cfg_attr(feature = "schema", returns(PastFamilyMembersForNodePagedResponse))]
GetPastMembersForNodePaged {
node_id: NodeId,
start_after: Option<PastFamilyMemberForNodeCursor>,
limit: Option<u32>,
},
}
/// Message passed to the contract's `migrate` entry point.
#[cw_serde]
pub struct MigrateMsg {
//
}
@@ -1,413 +0,0 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use cosmwasm_schema::cw_serde;
use cosmwasm_std::{Addr, Coin};
use nym_mixnet_contract_common::NodeId;
/// Identifier of a node family.
///
/// Issued sequentially by the contract on family creation; never reused even if the
/// family is later disbanded.
pub type NodeFamilyId = u32;
/// Runtime configuration of the node families contract.
#[cw_serde]
pub struct Config {
/// Fee charged on each successful `create_family` execution.
pub create_family_fee: Coin,
/// Maximum allowed length, in characters, of a family name.
pub family_name_length_limit: usize,
/// Maximum allowed length, in characters, of a family description.
pub family_description_length_limit: usize,
/// Default lifetime, in seconds, used by `invite_to_family` when the
/// sender doesn't supply an explicit value. Senders may override this
/// per-invitation via the optional `validity_secs` argument.
pub default_invitation_validity_secs: u64,
}
/// On-chain representation of a node family.
#[cw_serde]
pub struct NodeFamily {
/// The id of the node family
pub id: NodeFamilyId,
/// The name of the node family
pub name: String,
/// Normalised name of the node family used for uniqueness checks
pub normalised_name: String,
/// The optional description of the node family
pub description: String,
/// The owner of the node family
pub owner: Addr,
/// Records the fee paid when the family was created,
/// so that the appropriate amount could be returned upon it getting disbanded.
pub paid_fee: Coin,
/// Memoized value of the current number of members in the node family
/// Used to detect if the family is empty
pub members: u64,
/// Timestamp of the creation of the node family
pub created_at: u64,
}
/// A pending invitation for a node to join a particular family.
///
/// Invitations are stored until they are accepted, rejected, or revoked. Once the
/// chain advances past `expires_at` an invitation becomes inert but stays in storage
/// — there is no background process clearing expired invitations. A timed-out
/// invitation is cleared either when explicitly revoked/rejected, or when the family
/// issues a fresh invitation for the same node, which archives the stale one as
/// `Expired` and supersedes it.
#[cw_serde]
pub struct FamilyInvitation {
/// The family that issued the invitation.
pub family_id: NodeFamilyId,
/// The node being invited.
pub node_id: NodeId,
/// Block timestamp (unix seconds) after which the invitation is no longer valid.
pub expires_at: u64,
}
/// On-chain record of a node's current family membership.
///
/// A node belongs to at most one family at a time, so this is keyed by
/// `NodeId` alone — `family_id` is carried in the value to support reverse
/// lookups (all nodes in a given family) via a secondary index.
#[cw_serde]
pub struct FamilyMembership {
/// The family the node is currently a member of.
pub family_id: NodeFamilyId,
/// Block timestamp (unix seconds) at which the node accepted its
/// invitation and joined the family.
pub joined_at: u64,
}
/// Historical record of a node that used to be part of a family but has since been
/// removed (kicked, left voluntarily, or because the family was disbanded).
#[cw_serde]
pub struct PastFamilyMember {
/// The family the node used to belong to.
pub family_id: NodeFamilyId,
/// The node that was removed.
pub node_id: NodeId,
/// Block timestamp (unix seconds) at which the membership was terminated.
pub removed_at: u64,
}
/// Terminal status for an invitation that has been moved out of the pending set.
///
/// Note: an invitation that merely times out is **not** archived here on its own —
/// it is left inert in the pending set (see `FamilyInvitation::expires_at`). It only
/// reaches `Expired` if the family issues a fresh invitation for the same node, which
/// supersedes and archives the stale one.
#[cw_serde]
pub enum FamilyInvitationStatus {
/// Still awaiting a response. Recorded with a timestamp for completeness even
/// though pending invitations live in a separate map.
Pending { at: u64 },
/// The invitee accepted and joined the family at the given timestamp.
Accepted { at: u64 },
/// The invitee explicitly rejected the invitation at the given timestamp.
Rejected { at: u64 },
/// The family revoked the invitation at the given timestamp before it could
/// be accepted or rejected.
Revoked { at: u64 },
/// The invitation had already expired and was superseded by a fresh invitation
/// for the same node from the same family, issued at the given timestamp. This is
/// the only path that archives a timed-out invitation.
Expired { at: u64 },
}
/// Historical record of an invitation that has reached a terminal state
/// (`Accepted`, `Rejected`, `Revoked`, or `Expired`). A timed-out invitation is
/// archived here only when a fresh invitation for the same node supersedes it
/// (status `Expired`); otherwise it stays in the pending map until explicitly cleared.
#[cw_serde]
pub struct PastFamilyInvitation {
/// The original invitation as it was issued.
pub invitation: FamilyInvitation,
/// What ultimately happened to it.
pub status: FamilyInvitationStatus,
}
/// Response to [`QueryMsg::GetFamilyById`](crate::QueryMsg::GetFamilyById).
#[cw_serde]
pub struct NodeFamilyResponse {
/// The id that was queried, echoed back so paginated callers can correlate.
pub family_id: NodeFamilyId,
/// The matching family, or `None` if no family with `family_id` exists.
pub family: Option<NodeFamily>,
}
/// Response to [`QueryMsg::GetFamilyByOwner`](crate::QueryMsg::GetFamilyByOwner).
#[cw_serde]
pub struct NodeFamilyByOwnerResponse {
/// The (validated) owner address that was queried, echoed back so callers
/// can correlate.
pub owner: Addr,
/// The matching family, or `None` if `owner` does not currently own one.
pub family: Option<NodeFamily>,
}
/// Response to [`QueryMsg::GetFamilyByName`](crate::QueryMsg::GetFamilyByName).
#[cw_serde]
pub struct NodeFamilyByNameResponse {
/// The name that was queried, echoed back so callers can correlate.
pub name: String,
/// The matching family, or `None` if no family with that name exists.
pub family: Option<NodeFamily>,
}
/// Response to [`QueryMsg::GetFamilyMembership`](crate::QueryMsg::GetFamilyMembership).
#[cw_serde]
pub struct NodeFamilyMembershipResponse {
/// The node that was queried.
pub node_id: NodeId,
/// The id of the family the node currently belongs to, or `None` if the
/// node is not currently a member of any family.
pub family_id: Option<NodeFamilyId>,
}
/// A pending [`FamilyInvitation`] paired with whether it has already timed
/// out at the time the query was served.
#[cw_serde]
pub struct PendingFamilyInvitationDetails {
/// The stored invitation as it was issued.
pub invitation: FamilyInvitation,
/// `true` iff `now >= invitation.expires_at` at query time, i.e. the
/// invitation is still in the pending map but can no longer be acted on.
pub expired: bool,
}
/// Response to [`QueryMsg::GetPendingInvitation`](crate::QueryMsg::GetPendingInvitation).
#[cw_serde]
pub struct PendingFamilyInvitationResponse {
/// The family component of the queried `(family_id, node_id)` key.
pub family_id: NodeFamilyId,
/// The node component of the queried `(family_id, node_id)` key.
pub node_id: NodeId,
/// The matching pending invitation along with an explicit expiry flag,
/// or `None` if no such invitation exists.
pub invitation: Option<PendingFamilyInvitationDetails>,
}
/// One entry in a [`FamilyMembersPagedResponse`] page — pairs a node id with
/// its [`FamilyMembership`] record (notably its `joined_at` timestamp).
#[cw_serde]
pub struct FamilyMemberRecord {
/// The node currently in the family.
pub node_id: NodeId,
/// The membership record (carries `family_id` and `joined_at`).
pub membership: FamilyMembership,
}
/// Response to [`QueryMsg::GetFamilyMembersPaged`](crate::QueryMsg::GetFamilyMembersPaged).
#[cw_serde]
pub struct FamilyMembersPagedResponse {
/// The family whose members were queried, echoed back so paginated
/// callers can correlate.
pub family_id: NodeFamilyId,
/// The members on this page, in ascending [`NodeId`] order.
pub members: Vec<FamilyMemberRecord>,
/// Cursor to pass as `start_after` on the next call, or `None` if this
/// page is empty (which the caller should treat as end-of-list).
pub start_next_after: Option<NodeId>,
}
/// Response to [`QueryMsg::GetAllFamilyMembersPaged`](crate::QueryMsg::GetAllFamilyMembersPaged).
#[cw_serde]
pub struct AllFamilyMembersPagedResponse {
/// The members on this page, in ascending [`NodeId`] order across every
/// family.
pub members: Vec<FamilyMemberRecord>,
/// Cursor (last `node_id`) to pass as `start_after` on the next call,
/// or `None` if this page is empty (treat as end-of-list).
pub start_next_after: Option<NodeId>,
}
/// Response to [`QueryMsg::GetPendingInvitationsForFamilyPaged`](crate::QueryMsg::GetPendingInvitationsForFamilyPaged).
#[cw_serde]
pub struct PendingFamilyInvitationsPagedResponse {
/// The family whose pending invitations were queried, echoed back so
/// paginated callers can correlate.
pub family_id: NodeFamilyId,
/// The pending invitations on this page, in ascending invitee
/// [`NodeId`] order, each stamped with whether it had already timed out
/// at the time the query was served.
pub invitations: Vec<PendingFamilyInvitationDetails>,
/// Cursor (last invitee node id) to pass as `start_after` on the next
/// call, or `None` if this page is empty (treat as end-of-list).
pub start_next_after: Option<NodeId>,
}
/// Response to [`QueryMsg::GetPendingInvitationsForNodePaged`](crate::QueryMsg::GetPendingInvitationsForNodePaged).
#[cw_serde]
pub struct PendingInvitationsForNodePagedResponse {
/// The node whose pending invitations were queried, echoed back so
/// paginated callers can correlate.
pub node_id: NodeId,
/// The pending invitations addressed to this node on this page, in
/// ascending [`NodeFamilyId`] order, each stamped with whether it had
/// already timed out at the time the query was served.
pub invitations: Vec<PendingFamilyInvitationDetails>,
/// Cursor (last issuing family id) to pass as `start_after` on the
/// next call, or `None` if this page is empty (treat as end-of-list).
pub start_next_after: Option<NodeFamilyId>,
}
/// Response to [`QueryMsg::GetAllPendingInvitationsPaged`](crate::QueryMsg::GetAllPendingInvitationsPaged).
#[cw_serde]
pub struct PendingInvitationsPagedResponse {
/// The pending invitations on this page, in ascending
/// `(family_id, node_id)` order, each stamped with whether it had
/// already timed out at the time the query was served.
pub invitations: Vec<PendingFamilyInvitationDetails>,
/// Cursor (last `(family_id, node_id)` pair) to pass as `start_after`
/// on the next call, or `None` if this page is empty (treat as
/// end-of-list).
pub start_next_after: Option<(NodeFamilyId, NodeId)>,
}
/// Cursor for paginating per-family past-invitation listings: identifies a
/// single archive entry within a family by `(node_id, counter)`. The
/// `counter` is the per-`(family, node)` archive slot — multiple archived
/// invitations can exist for the same `(family, node)` pair (a node may be
/// invited and have the invitation reach a terminal state more than once).
pub type PastFamilyInvitationCursor = (NodeId, u64);
/// Cursor for paginating per-node past-invitation listings: identifies a
/// single archive entry addressed to a fixed node by `(family_id, counter)`.
pub type PastFamilyInvitationForNodeCursor = (NodeFamilyId, u64);
/// Cursor for paginating global past-invitation listings: identifies a
/// single archive entry across all families by `((family_id, node_id), counter)`.
pub type GlobalPastFamilyInvitationCursor = ((NodeFamilyId, NodeId), u64);
/// Response to [`QueryMsg::GetPastInvitationsForFamilyPaged`](crate::QueryMsg::GetPastInvitationsForFamilyPaged).
#[cw_serde]
pub struct PastFamilyInvitationsPagedResponse {
/// The family whose archived invitations were queried, echoed back so
/// paginated callers can correlate.
pub family_id: NodeFamilyId,
/// The archived invitations on this page, in ascending
/// `(node_id, counter)` order across all terminal statuses.
pub invitations: Vec<PastFamilyInvitation>,
/// Cursor to pass as `start_after` on the next call, or `None` if this
/// page is empty (treat as end-of-list).
pub start_next_after: Option<PastFamilyInvitationCursor>,
}
/// Response to [`QueryMsg::GetPastInvitationsForNodePaged`](crate::QueryMsg::GetPastInvitationsForNodePaged).
#[cw_serde]
pub struct PastFamilyInvitationsForNodePagedResponse {
/// The node whose past invitations were queried, echoed back so
/// paginated callers can correlate.
pub node_id: NodeId,
/// The archived invitations addressed to this node on this page, in
/// ascending `(family_id, counter)` order across all terminal statuses.
pub invitations: Vec<PastFamilyInvitation>,
/// Cursor to pass as `start_after` on the next call, or `None` if this
/// page is empty (treat as end-of-list).
pub start_next_after: Option<PastFamilyInvitationForNodeCursor>,
}
/// Response to [`QueryMsg::GetAllPastInvitationsPaged`](crate::QueryMsg::GetAllPastInvitationsPaged).
#[cw_serde]
pub struct AllPastFamilyInvitationsPagedResponse {
/// The archived invitations on this page, in ascending
/// `((family_id, node_id), counter)` order across all terminal statuses.
pub invitations: Vec<PastFamilyInvitation>,
/// Cursor to pass as `start_after` on the next call, or `None` if this
/// page is empty (treat as end-of-list).
pub start_next_after: Option<GlobalPastFamilyInvitationCursor>,
}
/// Cursor for paginating per-family past-member listings: identifies a single
/// archive entry within a family by `(node_id, counter)`. The `counter` is the
/// per-`(family, node)` archive slot — multiple archived membership entries
/// can exist for the same `(family, node)` pair (a node may join, leave, and
/// re-join the same family more than once).
pub type PastFamilyMemberCursor = (NodeId, u64);
/// Cursor for paginating per-node past-member listings: identifies a single
/// archive entry for a fixed node by `(family_id, counter)`.
pub type PastFamilyMemberForNodeCursor = (NodeFamilyId, u64);
/// Response to [`QueryMsg::GetPastMembersForFamilyPaged`](crate::QueryMsg::GetPastMembersForFamilyPaged).
#[cw_serde]
pub struct PastFamilyMembersPagedResponse {
/// The family whose archived memberships were queried, echoed back so
/// paginated callers can correlate.
pub family_id: NodeFamilyId,
/// The archived membership records on this page, in ascending
/// `(node_id, counter)` order.
pub members: Vec<PastFamilyMember>,
/// Cursor to pass as `start_after` on the next call, or `None` if this
/// page is empty (treat as end-of-list).
pub start_next_after: Option<PastFamilyMemberCursor>,
}
/// Response to [`QueryMsg::GetPastMembersForNodePaged`](crate::QueryMsg::GetPastMembersForNodePaged).
#[cw_serde]
pub struct PastFamilyMembersForNodePagedResponse {
/// The node whose archived memberships were queried, echoed back so
/// paginated callers can correlate.
pub node_id: NodeId,
/// The archived membership records for this node on this page, in
/// ascending `(family_id, counter)` order.
pub members: Vec<PastFamilyMember>,
/// Cursor to pass as `start_after` on the next call, or `None` if this
/// page is empty (treat as end-of-list).
pub start_next_after: Option<PastFamilyMemberForNodeCursor>,
}
/// Response to [`QueryMsg::GetFamiliesPaged`](crate::QueryMsg::GetFamiliesPaged).
#[cw_serde]
pub struct FamiliesPagedResponse {
/// The families on this page, in ascending [`NodeFamilyId`] order.
pub families: Vec<NodeFamily>,
/// Cursor to pass as `start_after` on the next call, or `None` if this
/// page is empty (which the caller should treat as end-of-list).
pub start_next_after: Option<NodeFamilyId>,
}
@@ -230,8 +230,8 @@ impl MemoryEcachTicketbookManager {
expiration_date: t.ticketbook.expiration_date(),
ticketbook_type: t.ticketbook.ticketbook_type().to_string(),
epoch_id: t.ticketbook.epoch_id() as u32,
total_tickets: t.total_tickets,
used_tickets: t.ticketbook.spent_tickets() as u32,
total_tickets: t.ticketbook.spent_tickets() as u32,
used_tickets: t.total_tickets,
})
.collect()
}
@@ -333,339 +333,3 @@ impl MemoryEcachTicketbookManager {
guard.emergency_credentials.remove(typ);
}
}
#[cfg(test)]
mod tests {
use super::*;
use nym_compact_ecash::tests::helpers::generate_expiration_date_signatures;
use nym_compact_ecash::{issue, ttp_keygen};
use nym_credentials_interface::TicketType;
use nym_crypto::asymmetric::ed25519;
use nym_ecash_time::EcashTime;
use nym_test_utils::helpers::deterministic_rng;
fn mock_issuance(deposit_id: u32) -> IssuanceTicketBook {
let identifier = "foomp";
let mut rng = deterministic_rng();
let key = ed25519::PrivateKey::new(&mut rng);
let typ = TicketType::V1MixnetEntry;
IssuanceTicketBook::new(deposit_id, identifier, key, typ)
}
fn mock_ticketbook() -> anyhow::Result<IssuedTicketBook> {
let signing_keys = ttp_keygen(1, 1)?.remove(0);
let issuance = mock_issuance(42);
let expiration_date = issuance.expiration_date();
let sig_req = issuance.prepare_for_signing();
let _exp_date_sigs = generate_expiration_date_signatures(
sig_req.expiration_date.ecash_unix_timestamp(),
&[signing_keys.secret_key()],
&[signing_keys.verification_key()],
&signing_keys.verification_key(),
&[1],
)?;
let blind_sig = issue(
signing_keys.secret_key(),
sig_req.ecash_pub_key,
&sig_req.withdrawal_request,
expiration_date.ecash_unix_timestamp(),
issuance.ticketbook_type().encode(),
)?;
let partial_wallet =
issuance.unblind_signature(&signing_keys.verification_key(), &sig_req, blind_sig, 1)?;
let wallet = issuance.aggregate_signature_shares(
&signing_keys.verification_key(),
&[partial_wallet],
sig_req,
)?;
Ok(issuance.into_issued_ticketbook(wallet, 1))
}
fn mock_verification_key() -> VerificationKeyAuth {
ttp_keygen(1, 1).unwrap().remove(0).verification_key()
}
#[tokio::test]
async fn get_ticketbooks_info_empty() {
let manager = MemoryEcachTicketbookManager::new();
let info = manager.get_ticketbooks_info().await;
assert!(info.is_empty());
}
#[tokio::test]
async fn get_ticketbooks_info_maps_inserted_ticketbook() -> anyhow::Result<()> {
let manager = MemoryEcachTicketbookManager::new();
let ticketbook = mock_ticketbook()?;
let total_tickets = 100;
let used_tickets = 25;
manager
.insert_new_ticketbook(&ticketbook, total_tickets, used_tickets)
.await;
let info = manager.get_ticketbooks_info().await;
assert_eq!(info.len(), 1);
let entry = &info[0];
assert_eq!(entry.id, 0);
assert_eq!(entry.expiration_date, ticketbook.expiration_date());
assert_eq!(
entry.ticketbook_type,
ticketbook.ticketbook_type().to_string()
);
assert_eq!(entry.epoch_id, ticketbook.epoch_id() as u32);
assert_eq!(entry.total_tickets, total_tickets);
assert_eq!(entry.used_tickets, used_tickets);
Ok(())
}
#[tokio::test]
async fn contains_ticketbook_reflects_insertion() -> anyhow::Result<()> {
let manager = MemoryEcachTicketbookManager::new();
let ticketbook = mock_ticketbook()?;
assert!(!manager.contains_ticketbook(&ticketbook).await);
manager.insert_new_ticketbook(&ticketbook, 100, 0).await;
assert!(manager.contains_ticketbook(&ticketbook).await);
Ok(())
}
#[tokio::test]
async fn insert_new_ticketbook_assigns_incrementing_ids() -> anyhow::Result<()> {
let manager = MemoryEcachTicketbookManager::new();
let ticketbook = mock_ticketbook()?;
manager.insert_new_ticketbook(&ticketbook, 100, 0).await;
manager.insert_new_ticketbook(&ticketbook, 100, 0).await;
let mut ids: Vec<i64> = manager
.get_ticketbooks_info()
.await
.into_iter()
.map(|i| i.id)
.collect();
ids.sort();
assert_eq!(ids, vec![0, 1]);
Ok(())
}
#[tokio::test]
async fn get_next_unspent_ticketbook_updates_spent_and_exhausts() -> anyhow::Result<()> {
let manager = MemoryEcachTicketbookManager::new();
let ticketbook = mock_ticketbook()?;
let typ = ticketbook.ticketbook_type().to_string();
// total = 3, used = 0 — leaves 3 tickets available
manager.insert_new_ticketbook(&ticketbook, 3, 0).await;
let first = manager
.get_next_unspent_ticketbook_and_update(typ.clone(), 2)
.await;
assert!(first.is_some());
let first = first.unwrap();
assert_eq!(first.total_tickets, 3);
// returned ticketbook reflects state *before* the update
assert_eq!(first.ticketbook.spent_tickets(), 0);
// next withdrawal of 2 should be rejected (only 1 left)
let second = manager
.get_next_unspent_ticketbook_and_update(typ.clone(), 2)
.await;
assert!(second.is_none());
// but a withdrawal of 1 succeeds
let third = manager
.get_next_unspent_ticketbook_and_update(typ.clone(), 1)
.await;
assert!(third.is_some());
// and now nothing left
let fourth = manager.get_next_unspent_ticketbook_and_update(typ, 1).await;
assert!(fourth.is_none());
Ok(())
}
#[tokio::test]
async fn get_next_unspent_ticketbook_filters_by_type() -> anyhow::Result<()> {
let manager = MemoryEcachTicketbookManager::new();
let ticketbook = mock_ticketbook()?;
manager.insert_new_ticketbook(&ticketbook, 5, 0).await;
let mismatched = manager
.get_next_unspent_ticketbook_and_update("nonexistent_type".to_string(), 1)
.await;
assert!(mismatched.is_none());
Ok(())
}
#[tokio::test]
async fn revert_ticketbook_withdrawal_resets_spent_only_when_expected_matches(
) -> anyhow::Result<()> {
let manager = MemoryEcachTicketbookManager::new();
let ticketbook = mock_ticketbook()?;
let typ = ticketbook.ticketbook_type().to_string();
manager.insert_new_ticketbook(&ticketbook, 10, 0).await;
manager
.get_next_unspent_ticketbook_and_update(typ.clone(), 4)
.await
.expect("should withdraw");
// stale expected_current_total_spent — should be rejected
assert!(!manager.revert_ticketbook_withdrawal(0, 4, 99).await);
// spent_tickets unchanged
let used_after_failed = manager.get_ticketbooks_info().await[0].used_tickets;
assert_eq!(used_after_failed, 4);
// matching expected — should succeed and restore
assert!(manager.revert_ticketbook_withdrawal(0, 4, 4).await);
let used_after_revert = manager.get_ticketbooks_info().await[0].used_tickets;
assert_eq!(used_after_revert, 0);
// unknown ticketbook_id is rejected
assert!(!manager.revert_ticketbook_withdrawal(999, 1, 0).await);
Ok(())
}
#[tokio::test]
async fn pending_ticketbook_round_trip() {
let manager = MemoryEcachTicketbookManager::new();
let issuance = mock_issuance(7);
let deposit_id = issuance.deposit_id() as i64;
assert!(manager.get_pending_ticketbooks().await.is_empty());
manager.insert_pending_ticketbook(&issuance).await;
let pending = manager.get_pending_ticketbooks().await;
assert_eq!(pending.len(), 1);
assert_eq!(pending[0].pending_id, deposit_id);
assert_eq!(
pending[0].pending_ticketbook.deposit_id(),
issuance.deposit_id()
);
manager.remove_pending_ticketbook(deposit_id).await;
assert!(manager.get_pending_ticketbooks().await.is_empty());
// removing a non-existent id is a no-op
manager.remove_pending_ticketbook(999).await;
}
#[tokio::test]
async fn emergency_credential_lifecycle() {
let manager = MemoryEcachTicketbookManager::new();
let cred_a = EmergencyCredentialContent {
typ: "type-a".to_string(),
content: vec![1, 2, 3],
expiration: None,
};
let cred_b = EmergencyCredentialContent {
typ: "type-a".to_string(),
content: vec![4, 5, 6],
expiration: None,
};
let cred_c = EmergencyCredentialContent {
typ: "type-b".to_string(),
content: vec![7, 8, 9],
expiration: None,
};
assert!(manager.get_emergency_credential("type-a").await.is_none());
manager.insert_emergency_credential(&cred_a).await;
manager.insert_emergency_credential(&cred_b).await;
manager.insert_emergency_credential(&cred_c).await;
// get returns the first inserted entry for the type
let first = manager.get_emergency_credential("type-a").await.unwrap();
assert_eq!(first.id, 0);
assert_eq!(first.data.content, vec![1, 2, 3]);
// remove by id drops only that entry; type-a now exposes cred_b
manager.remove_emergency_credential(0).await;
let after_remove = manager.get_emergency_credential("type-a").await.unwrap();
assert_eq!(after_remove.id, 1);
assert_eq!(after_remove.data.content, vec![4, 5, 6]);
// remove by type clears the bucket entirely
manager.remove_emergency_credentials_of_type("type-a").await;
assert!(manager.get_emergency_credential("type-a").await.is_none());
// unrelated type is untouched
assert!(manager.get_emergency_credential("type-b").await.is_some());
}
#[tokio::test]
async fn master_verification_key_round_trip() {
let manager = MemoryEcachTicketbookManager::new();
let key = mock_verification_key();
let epoch = EpochVerificationKey {
epoch_id: 7,
key: key.clone(),
};
assert!(manager.get_master_verification_key(7).await.is_none());
manager.insert_master_verification_key(&epoch).await;
assert_eq!(manager.get_master_verification_key(7).await, Some(key));
assert!(manager.get_master_verification_key(8).await.is_none());
}
#[tokio::test]
async fn coin_index_signatures_round_trip() {
let manager = MemoryEcachTicketbookManager::new();
let sigs = AggregatedCoinIndicesSignatures {
epoch_id: 3,
signatures: vec![],
};
assert!(manager.get_coin_index_signatures(3).await.is_none());
manager.insert_coin_index_signatures(&sigs).await;
let retrieved = manager.get_coin_index_signatures(3).await;
assert!(retrieved.is_some());
assert!(retrieved.unwrap().is_empty());
assert!(manager.get_coin_index_signatures(4).await.is_none());
}
#[tokio::test]
async fn expiration_date_signatures_round_trip() {
let manager = MemoryEcachTicketbookManager::new();
let date = nym_ecash_time::ecash_today().date();
let sigs = AggregatedExpirationDateSignatures {
epoch_id: 5,
expiration_date: date,
signatures: vec![],
};
assert!(manager
.get_expiration_date_signatures(date, 5)
.await
.is_none());
manager.insert_expiration_date_signatures(&sigs).await;
let retrieved = manager.get_expiration_date_signatures(date, 5).await;
assert!(retrieved.is_some());
assert!(retrieved.unwrap().is_empty());
// wrong epoch / wrong date → miss
assert!(manager
.get_expiration_date_signatures(date, 6)
.await
.is_none());
}
}
+1 -3
View File
@@ -8,9 +8,7 @@ license = { workspace = true }
repository = { workspace = true }
homepage.workspace = true
documentation.workspace = true
# pinned (not inherited from workspace) because this crate is imported by the ecash contract,
# and the contracts workspace cannot be built with rustc more recent than 1.86
rust-version = "1.86.0"
rust-version.workspace = true
readme.workspace = true
publish = true
@@ -6,7 +6,7 @@ use nym_coconut_dkg_common::verification_key::VerificationKeyShare;
use nym_crypto::asymmetric::ed25519;
use std::time::Duration;
use time::OffsetDateTime;
use tracing::warn;
use tracing::{debug, warn};
pub trait Verifiable {
fn verify_signature(&self, pub_key: &ed25519::PublicKey) -> bool;
+1 -4
View File
@@ -38,10 +38,7 @@ itertools = { workspace = true }
inventory = { workspace = true }
fastrand = { workspace = true }
tokio = { workspace = true, features = ["rt", "macros", "time"] }
rustls = { workspace = true, features = ["aws_lc_rs"] }
# Android: preconfigured webpki roots replace the JNI-bound platform verifier
# (see registry.rs); a standalone sidecar process can't init the platform store.
webpki-roots = { workspace = true }
rustls = { workspace=true }
# used for decoding text responses (they were already implicitly included)
bytes = { workspace = true }
encoding_rs = { workspace = true }

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