Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 944fc27ef6 | |||
| 3853c0f0c9 | |||
| 97f79381b9 | |||
| 25eba09b92 | |||
| a8cecb1200 | |||
| 4e52e9bf77 | |||
| cf55e2fe86 | |||
| dc0835f1f3 | |||
| b5a8b9d283 | |||
| a395167139 | |||
| 6b98c168fc | |||
| 4645de3eb5 | |||
| e6dd670b16 | |||
| dc48750271 | |||
| 46c67440bb | |||
| e5cd9fd69e | |||
| 21c14c0df0 | |||
| 87c236a927 |
@@ -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
|
||||
|
||||
@@ -21,7 +21,7 @@ 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: 9
|
||||
- uses: actions/setup-node@v4
|
||||
@@ -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
|
||||
|
||||
@@ -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,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
|
||||
@@ -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: |
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -19,7 +19,6 @@ jobs:
|
||||
RUSTUP_PERMIT_COPY_RENAME: 1
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v6
|
||||
@@ -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
|
||||
|
||||
@@ -28,7 +28,7 @@ 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: 9
|
||||
- uses: actions/setup-node@v4
|
||||
@@ -61,9 +61,6 @@ jobs:
|
||||
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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -108,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
|
||||
|
||||
@@ -132,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: |
|
||||
|
||||
@@ -38,7 +38,7 @@ jobs:
|
||||
toolchain: ${{ vars.REQUIRED_RUSTC_VERSION }}
|
||||
|
||||
- name: Setup MSBuild.exe
|
||||
uses: microsoft/setup-msbuild@v3
|
||||
uses: microsoft/setup-msbuild@v2
|
||||
|
||||
# No cache:yarn here: setup-node needs yarn on PATH to populate the cache, but this runner
|
||||
# only gets yarn from the step below.
|
||||
@@ -165,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
|
||||
|
||||
@@ -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: 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: 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 }}
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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-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 }}
|
||||
|
||||
@@ -30,7 +30,7 @@ jobs:
|
||||
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
|
||||
|
||||
@@ -78,4 +78,3 @@ CLAUDE.md
|
||||
|
||||
/notes
|
||||
/target-otel
|
||||
test-tutorials/
|
||||
|
||||
@@ -4,6 +4,52 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [2026.10-waterloo] (2026-05-27)
|
||||
|
||||
- Re-order default API urls for network details - Waterloo release ([#6799])
|
||||
- [bugfix] IPR v8<->v9 mismatch on Waterloo ([#6772])
|
||||
- Migrate to hickory 0.26.1 ([#6751])
|
||||
- add workflows for NM3 ([#6729])
|
||||
- credential proxy pool ([#6726])
|
||||
- chore: made sphinx version threshold assertion a compile time check ([#6718])
|
||||
- Feat/nmv3 updated performance calculation ([#6714])
|
||||
- feat: NMv3: submission of stress testing result into nym-api ([#6709])
|
||||
- feat: NMv3: Prometheus metrics for network monitor ([#6693])
|
||||
- feat: NMv3: add read-only results API to orchestrator ([#6689])
|
||||
- feat: NMv3: Eviction of stale testrun data ([#6685])
|
||||
- feat: NMv3: Wire up testrun assignment and result submission flow ([#6680])
|
||||
- feat: NMv3: Support multiple network monitor agents per host ([#6679])
|
||||
- Feat/nmv3 agent announcement ([#6673])
|
||||
- add node refresher for periodic scraping of bonded nym-node details ([#6626])
|
||||
- Feat/nmv3 orchestrator queue ([#6597])
|
||||
- feat: network monitor agent - standalone node stress-testing ([#6582])
|
||||
- [feat] propagate NM agent noise keys to nym-node routing ([#6577])
|
||||
- start mix stress testing topic branch ([#6575])
|
||||
- Feat/nmv3 agents subscription ([#6567])
|
||||
- Feat/nmv3 agents contract ([#6555])
|
||||
|
||||
[#6799]: https://github.com/nymtech/nym/pull/6799
|
||||
[#6772]: https://github.com/nymtech/nym/pull/6772
|
||||
[#6751]: https://github.com/nymtech/nym/pull/6751
|
||||
[#6729]: https://github.com/nymtech/nym/pull/6729
|
||||
[#6726]: https://github.com/nymtech/nym/pull/6726
|
||||
[#6718]: https://github.com/nymtech/nym/pull/6718
|
||||
[#6714]: https://github.com/nymtech/nym/pull/6714
|
||||
[#6709]: https://github.com/nymtech/nym/pull/6709
|
||||
[#6693]: https://github.com/nymtech/nym/pull/6693
|
||||
[#6689]: https://github.com/nymtech/nym/pull/6689
|
||||
[#6685]: https://github.com/nymtech/nym/pull/6685
|
||||
[#6680]: https://github.com/nymtech/nym/pull/6680
|
||||
[#6679]: https://github.com/nymtech/nym/pull/6679
|
||||
[#6673]: https://github.com/nymtech/nym/pull/6673
|
||||
[#6626]: https://github.com/nymtech/nym/pull/6626
|
||||
[#6597]: https://github.com/nymtech/nym/pull/6597
|
||||
[#6582]: https://github.com/nymtech/nym/pull/6582
|
||||
[#6577]: https://github.com/nymtech/nym/pull/6577
|
||||
[#6575]: https://github.com/nymtech/nym/pull/6575
|
||||
[#6567]: https://github.com/nymtech/nym/pull/6567
|
||||
[#6555]: https://github.com/nymtech/nym/pull/6555
|
||||
|
||||
## [2026.9-venaco] (2026-05-06)
|
||||
|
||||
- Fix for v9 IPR ([#6710])
|
||||
|
||||
Generated
+526
-705
File diff suppressed because it is too large
Load Diff
+131
-129
@@ -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,10 +41,10 @@ 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",
|
||||
"common/cosmwasm-smart-contracts/network-monitors-contract",
|
||||
"common/credential-proxy",
|
||||
"common/credential-storage",
|
||||
"common/credential-utils",
|
||||
@@ -70,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",
|
||||
@@ -94,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",
|
||||
@@ -126,7 +123,6 @@ members = [
|
||||
"common/zulip-client",
|
||||
"documentation/autodoc",
|
||||
"gateway",
|
||||
"integration-tests",
|
||||
"nym-api",
|
||||
"nym-api/nym-api-requests",
|
||||
"nym-authenticator-client",
|
||||
@@ -134,9 +130,7 @@ members = [
|
||||
"nym-credential-proxy/nym-credential-proxy",
|
||||
"nym-credential-proxy/nym-credential-proxy-requests",
|
||||
"nym-data-observatory",
|
||||
"nym-gateway-probe",
|
||||
"nym-ip-packet-client",
|
||||
"nym-mix-sim",
|
||||
"nym-network-monitor",
|
||||
"nym-node",
|
||||
"nym-node-status-api/nym-node-status-agent",
|
||||
@@ -147,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",
|
||||
@@ -155,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",
|
||||
@@ -179,24 +173,31 @@ members = [
|
||||
"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",
|
||||
]
|
||||
|
||||
default-members = [
|
||||
"clients/native",
|
||||
"clients/socks5",
|
||||
"nym-api",
|
||||
"nym-authenticator-client",
|
||||
"nym-api",
|
||||
"nym-credential-proxy/nym-credential-proxy",
|
||||
"nym-mix-sim",
|
||||
"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"]
|
||||
@@ -210,7 +211,7 @@ edition = "2024"
|
||||
license = "Apache-2.0"
|
||||
rust-version = "1.87.0"
|
||||
readme = "README.md"
|
||||
version = "1.21.0"
|
||||
version = "1.20.4"
|
||||
|
||||
[workspace.dependencies]
|
||||
addr = "0.15.6"
|
||||
@@ -233,7 +234,7 @@ 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"
|
||||
@@ -329,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" }
|
||||
@@ -353,6 +354,7 @@ serde_yaml = "0.9.25"
|
||||
serde_plain = "1.0.2"
|
||||
sha2 = "0.10.3"
|
||||
si-scale = "0.2.3"
|
||||
smolmix = { version = "0.0.1", path = "smolmix/core" }
|
||||
smoltcp = "0.12"
|
||||
snow = "0.9.6"
|
||||
sphinx-packet = "=0.6.0"
|
||||
@@ -363,7 +365,7 @@ subtle-encoding = "0.5"
|
||||
syn = "2"
|
||||
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"
|
||||
@@ -405,6 +407,10 @@ zeroize = "1.7.0"
|
||||
|
||||
prometheus = { version = "0.14.0" }
|
||||
|
||||
# recreating lioness
|
||||
# we don't care about particular versions - just pull whatever is used by sphinx
|
||||
lioness = "*"
|
||||
arrayref = "*"
|
||||
|
||||
# libcrux
|
||||
libcrux-kem = "0.0.7"
|
||||
@@ -417,114 +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.0", path = "nym-api/nym-api-requests" }
|
||||
nym-authenticator-requests = { version = "1.21.0", path = "common/authenticator-requests" }
|
||||
nym-async-file-watcher = { version = "1.21.0", path = "common/async-file-watcher" }
|
||||
nym-authenticator-client = { version = "1.21.0", path = "nym-authenticator-client" }
|
||||
nym-bandwidth-controller = { version = "1.21.0", path = "common/bandwidth-controller" }
|
||||
nym-bin-common = { version = "1.21.0", path = "common/bin-common" }
|
||||
nym-cache = { version = "1.21.0", path = "common/nym-cache" }
|
||||
nym-client-core = { version = "1.21.0", path = "common/client-core", default-features = false }
|
||||
nym-client-core-config-types = { version = "1.21.0", path = "common/client-core/config-types" }
|
||||
nym-client-core-gateways-storage = { version = "1.21.0", path = "common/client-core/gateways-storage" }
|
||||
nym-client-core-surb-storage = { version = "1.21.0", path = "common/client-core/surb-storage" }
|
||||
nym-client-websocket-requests = { version = "1.21.0", path = "clients/native/websocket-requests" }
|
||||
nym-common = { version = "1.21.0", path = "common/nym-common" }
|
||||
nym-compact-ecash = { version = "1.21.0", path = "common/nym_offline_compact_ecash" }
|
||||
nym-config = { version = "1.21.0", path = "common/config" }
|
||||
nym-contracts-common = { version = "1.21.0", path = "common/cosmwasm-smart-contracts/contracts-common" }
|
||||
nym-coconut-dkg-common = { version = "1.21.0", path = "common/cosmwasm-smart-contracts/coconut-dkg" }
|
||||
nym-credential-storage = { version = "1.21.0", path = "common/credential-storage" }
|
||||
nym-credential-utils = { version = "1.21.0", path = "common/credential-utils" }
|
||||
nym-credential-proxy-lib = { version = "1.21.0", path = "common/credential-proxy" }
|
||||
nym-credentials = { version = "1.21.0", path = "common/credentials", default-features = false }
|
||||
nym-credentials-interface = { version = "1.21.0", path = "common/credentials-interface" }
|
||||
nym-credential-proxy-requests = { version = "1.21.0", path = "nym-credential-proxy/nym-credential-proxy-requests", default-features = false }
|
||||
nym-credential-verification = { version = "1.21.0", path = "common/credential-verification" }
|
||||
nym-crypto = { version = "1.21.0", path = "common/crypto", default-features = false }
|
||||
nym-dkg = { version = "1.21.0", path = "common/dkg" }
|
||||
nym-ecash-contract-common = { version = "1.21.0", path = "common/cosmwasm-smart-contracts/ecash-contract" }
|
||||
nym-ecash-signer-check = { version = "1.21.0", path = "common/ecash-signer-check" }
|
||||
nym-ecash-signer-check-types = { version = "1.21.0", path = "common/ecash-signer-check-types" }
|
||||
nym-ecash-time = { version = "1.21.0", path = "common/ecash-time" }
|
||||
nym-exit-policy = { version = "1.21.0", path = "common/exit-policy" }
|
||||
nym-ffi-shared = { version = "1.21.0", path = "sdk/ffi/shared" }
|
||||
nym-gateway-client = { version = "1.21.0", path = "common/client-libs/gateway-client", default-features = false }
|
||||
nym-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.21.0", path = "common/gateway-requests" }
|
||||
nym-gateway-storage = { version = "1.21.0", path = "common/gateway-storage" }
|
||||
nym-gateway-stats-storage = { version = "1.21.0", path = "common/gateway-stats-storage" }
|
||||
nym-group-contract-common = { version = "1.21.0", path = "common/cosmwasm-smart-contracts/group-contract" }
|
||||
nym-http-api-client = { version = "1.21.0", path = "common/http-api-client" }
|
||||
nym-http-api-client-macro = { version = "1.21.0", path = "common/http-api-client-macro" }
|
||||
nym-http-api-common = { version = "1.21.0", path = "common/http-api-common", default-features = false }
|
||||
nym-id = { version = "1.21.0", path = "common/nym-id" }
|
||||
nym-ip-packet-client = { version = "1.21.0", path = "nym-ip-packet-client" }
|
||||
nym-ip-packet-requests = { version = "1.21.0", path = "common/ip-packet-requests" }
|
||||
nym-lp = { version = "1.21.0", path = "common/nym-lp" }
|
||||
nym-lp-data = { version = "1.21.0", path = "common/nym-lp-data" }
|
||||
nym-kkt = { version = "1.21.0", path = "common/nym-kkt" }
|
||||
nym-kkt-ciphersuite = { version = "1.21.0", path = "common/nym-kkt-ciphersuite" }
|
||||
nym-kkt-context = { version = "1.21.0", path = "common/nym-kkt-context" }
|
||||
nym-metrics = { version = "1.21.0", path = "common/nym-metrics" }
|
||||
nym-mixnet-client = { version = "1.21.0", path = "common/client-libs/mixnet-client" }
|
||||
nym-mixnet-contract-common = { version = "1.21.0", path = "common/cosmwasm-smart-contracts/mixnet-contract" }
|
||||
nym-multisig-contract-common = { version = "1.21.0", path = "common/cosmwasm-smart-contracts/multisig-contract" }
|
||||
nym-network-defaults = { version = "1.21.0", path = "common/network-defaults" }
|
||||
nym-node-tester-utils = { version = "1.21.0", path = "common/node-tester-utils" }
|
||||
nym-noise = { version = "1.21.0", path = "common/nymnoise" }
|
||||
nym-noise-keys = { version = "1.21.0", path = "common/nymnoise/keys" }
|
||||
nym-nonexhaustive-delayqueue = { version = "1.21.0", path = "common/nonexhaustive-delayqueue" }
|
||||
nym-node-requests = { version = "1.21.0", path = "nym-node/nym-node-requests", default-features = false }
|
||||
nym-node-metrics = { version = "1.21.0", path = "nym-node/nym-node-metrics" }
|
||||
nym-node-families-contract-common = { version = "1.21.0", path = "common/cosmwasm-smart-contracts/node-families-contract" }
|
||||
nym-ordered-buffer = { version = "1.21.0", path = "common/socks5/ordered-buffer" }
|
||||
nym-outfox = { version = "1.21.0", path = "nym-outfox" }
|
||||
nym-registration-common = { version = "1.21.0", path = "common/registration" }
|
||||
nym-pemstore = { version = "1.21.0", path = "common/pemstore" }
|
||||
nym-performance-contract-common = { version = "1.21.0", path = "common/cosmwasm-smart-contracts/nym-performance-contract" }
|
||||
nym-sdk = { version = "1.21.0", path = "sdk/rust/nym-sdk" }
|
||||
nym-serde-helpers = { version = "1.21.0", path = "common/serde-helpers" }
|
||||
nym-service-providers-common = { version = "1.21.0", path = "service-providers/common" }
|
||||
nym-service-provider-requests-common = { version = "1.21.0", path = "common/service-provider-requests-common" }
|
||||
nym-socks5-client-core = { version = "1.21.0", path = "common/socks5-client-core" }
|
||||
nym-socks5-proxy-helpers = { version = "1.21.0", path = "common/socks5/proxy-helpers" }
|
||||
nym-socks5-requests = { version = "1.21.0", path = "common/socks5/requests" }
|
||||
nym-sphinx = { version = "1.21.0", path = "common/nymsphinx" }
|
||||
nym-sphinx-acknowledgements = { version = "1.21.0", path = "common/nymsphinx/acknowledgements" }
|
||||
nym-sphinx-addressing = { version = "1.21.0", path = "common/nymsphinx/addressing" }
|
||||
nym-sphinx-anonymous-replies = { version = "1.21.0", path = "common/nymsphinx/anonymous-replies" }
|
||||
nym-sphinx-chunking = { version = "1.21.0", path = "common/nymsphinx/chunking" }
|
||||
nym-sphinx-cover = { version = "1.21.0", path = "common/nymsphinx/cover" }
|
||||
nym-sphinx-forwarding = { version = "1.21.0", path = "common/nymsphinx/forwarding" }
|
||||
nym-sphinx-framing = { version = "1.21.0", path = "common/nymsphinx/framing" }
|
||||
nym-sphinx-params = { version = "1.21.0", path = "common/nymsphinx/params" }
|
||||
nym-sphinx-routing = { version = "1.21.0", path = "common/nymsphinx/routing" }
|
||||
nym-sphinx-types = { version = "1.21.0", path = "common/nymsphinx/types" }
|
||||
nym-statistics-common = { version = "1.21.0", path = "common/statistics" }
|
||||
nym-store-cipher = { version = "1.21.0", path = "common/store-cipher" }
|
||||
nym-task = { version = "1.21.0", path = "common/task" }
|
||||
nym-tun = { version = "1.21.0", path = "common/tun" }
|
||||
nym-test-utils = { version = "1.21.0", path = "common/test-utils" }
|
||||
nym-ticketbooks-merkle = { version = "1.21.0", path = "common/ticketbooks-merkle" }
|
||||
nym-topology = { version = "1.21.0", path = "common/topology" }
|
||||
nym-types = { version = "1.21.0", path = "common/types" }
|
||||
nym-upgrade-mode-check = { version = "1.21.0", path = "common/upgrade-mode-check" }
|
||||
nym-validator-client = { version = "1.21.0", path = "common/client-libs/validator-client", default-features = false }
|
||||
nym-vesting-contract-common = { version = "1.21.0", path = "common/cosmwasm-smart-contracts/vesting-contract" }
|
||||
nym-verloc = { version = "1.21.0", path = "common/verloc" }
|
||||
nym-wireguard = { version = "1.21.0", path = "common/wireguard" }
|
||||
nym-wireguard-types = { version = "1.21.0", path = "common/wireguard-types" }
|
||||
nym-wireguard-private-metadata-shared = { version = "1.21.0", path = "common/wireguard-private-metadata/shared" }
|
||||
nym-wireguard-private-metadata-client = { version = "1.21.0", path = "common/wireguard-private-metadata/client" }
|
||||
nym-wireguard-private-metadata-server = { version = "1.21.0", path = "common/wireguard-private-metadata/server" }
|
||||
nym-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.21.0", path = "common/wasm/client-core" }
|
||||
nym-wasm-storage = { version = "1.21.0", path = "common/wasm/storage" }
|
||||
nym-wasm-utils = { version = "1.21.0", path = "common/wasm/utils", default-features = false }
|
||||
nyxd-scraper-shared = { version = "1.21.0", path = "common/nyxd-scraper-shared" }
|
||||
|
||||
smolmix = { version = "1.21.0", path = "smolmix/core" }
|
||||
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
|
||||
@@ -623,4 +626,3 @@ exit = "deny"
|
||||
panic = "deny"
|
||||
unimplemented = "deny"
|
||||
unreachable = "deny"
|
||||
|
||||
|
||||
@@ -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') }}"
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "nym-client"
|
||||
description = "Implementation of the Nym Client"
|
||||
version = "1.1.76"
|
||||
version = "1.1.77"
|
||||
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>", "Jędrzej Stuczyński <andrew@nymtech.net>"]
|
||||
edition = "2021"
|
||||
license.workspace = true
|
||||
|
||||
@@ -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,7 +1,7 @@
|
||||
[package]
|
||||
name = "nym-socks5-client"
|
||||
description = "A SOCKS5 localhost proxy that converts incoming messages to Sphinx and sends them to a Nym address"
|
||||
version = "1.1.76"
|
||||
version = "1.1.77"
|
||||
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>"]
|
||||
edition = "2021"
|
||||
license.workspace = true
|
||||
|
||||
@@ -60,7 +60,6 @@ nym-client-core-surb-storage = { workspace = true }
|
||||
nym-client-core-gateways-storage = { workspace = true }
|
||||
nym-ecash-time = { workspace = true }
|
||||
nym-mixnet-contract-common = { workspace = true }
|
||||
nym-lp-data = { workspace = true }
|
||||
|
||||
[target."cfg(not(target_arch = \"wasm32\"))".dependencies]
|
||||
nym-mixnet-client = { workspace = true }
|
||||
|
||||
@@ -11,8 +11,6 @@ use crate::client::event_control::EventControl;
|
||||
use crate::client::inbound_messages::{InputMessage, InputMessageReceiver, InputMessageSender};
|
||||
use crate::client::key_manager::ClientKeys;
|
||||
use crate::client::key_manager::persistence::KeyStore;
|
||||
use crate::client::lp::data::LpDataSetup;
|
||||
use crate::client::lp::data::shared::SharedLpDataState;
|
||||
use crate::client::mix_traffic::transceiver::{GatewayReceiver, GatewayTransceiver, RemoteGateway};
|
||||
use crate::client::mix_traffic::{BatchMixMessageSender, MixTrafficController, MixTrafficEvent};
|
||||
use crate::client::real_messages_control;
|
||||
@@ -638,6 +636,7 @@ where
|
||||
{
|
||||
Err(ClientCoreError::CustomGatewaySelectionExpected)
|
||||
} else {
|
||||
// and make sure to invalidate the task client, so we wouldn't cause premature shutdown
|
||||
custom_gateway_transceiver.set_packet_router(packet_router)?;
|
||||
Ok(custom_gateway_transceiver)
|
||||
};
|
||||
@@ -818,24 +817,6 @@ where
|
||||
(mix_tx, client_tx)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn build_lp_data_tasks(
|
||||
config: &Config,
|
||||
encryption_keys: Arc<x25519::KeyPair>,
|
||||
identity_keys: Arc<ed25519::KeyPair>,
|
||||
input_receiver: InputMessageReceiver,
|
||||
shutdown_tracker: &ShutdownTracker,
|
||||
) -> Result<LpDataSetup, ClientCoreError> {
|
||||
let shared_state = SharedLpDataState::new(
|
||||
config.debug,
|
||||
encryption_keys,
|
||||
identity_keys,
|
||||
shutdown_tracker.clone_shutdown_token(),
|
||||
);
|
||||
|
||||
LpDataSetup::new(shared_state, input_receiver, shutdown_tracker.clone())
|
||||
}
|
||||
|
||||
// TODO: rename it as it implies the data is persistent whilst one can use InMemBackend
|
||||
async fn setup_persistent_reply_storage(
|
||||
backend: S::ReplyStore,
|
||||
@@ -1082,27 +1063,12 @@ where
|
||||
)
|
||||
.await?;
|
||||
|
||||
// SW keep all the above
|
||||
|
||||
// LP Data channel
|
||||
// let lp_data_tasks = Self::build_lp_data_tasks(
|
||||
// &self.config,
|
||||
// encryption_keys.clone(),
|
||||
// identity_keys.clone(),
|
||||
// input_receiver,
|
||||
// &shutdown_tracker.clone(),
|
||||
// )?;
|
||||
// lp_data_tasks.start_tasks();
|
||||
|
||||
// SW Piping between inbound and outbound
|
||||
let gateway_packet_router = PacketRouter::new(
|
||||
ack_sender,
|
||||
mixnet_messages_sender,
|
||||
shutdown_tracker.clone_shutdown_token(),
|
||||
);
|
||||
|
||||
// SW this needs to become the IO handler
|
||||
|
||||
let gateway_transceiver = Self::setup_gateway_transceiver(
|
||||
self.custom_gateway_transceiver,
|
||||
&self.config,
|
||||
@@ -1124,7 +1090,6 @@ where
|
||||
)
|
||||
.await?;
|
||||
|
||||
// SW turn into inbound pipeline
|
||||
Self::start_received_messages_buffer_controller(
|
||||
encryption_keys,
|
||||
received_buffer_request_receiver,
|
||||
@@ -1135,8 +1100,6 @@ where
|
||||
&shutdown_tracker.clone(),
|
||||
);
|
||||
|
||||
// SW the rest below is outbound pipeline
|
||||
|
||||
// The message_sender is the transmitter for any component generating sphinx packets
|
||||
// that are to be sent to the mixnet. They are used by cover traffic stream and real
|
||||
// traffic stream.
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use nym_lp_data::packet::frame::LpFrameKind;
|
||||
use nym_sphinx::addressing::nodes::NymNodeRoutingAddressError;
|
||||
use nym_sphinx::forwarding::packet::MixPacketFormattingError;
|
||||
use nym_sphinx::framing::processing::PacketProcessingError;
|
||||
use nym_sphinx::{OutfoxError, SphinxError};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum LpDataHandlerError {
|
||||
#[error(transparent)]
|
||||
PacketFormattingError(#[from] MixPacketFormattingError),
|
||||
|
||||
#[error(transparent)]
|
||||
PacketProcessingError(#[from] PacketProcessingError),
|
||||
|
||||
#[error(transparent)]
|
||||
NymNodeRoutingAddressError(#[from] NymNodeRoutingAddressError),
|
||||
|
||||
#[error("failed to process received sphinx packet: {0}")]
|
||||
SphinxProcessingError(#[from] SphinxError),
|
||||
|
||||
#[error("failed to process received outfox packet: {0}")]
|
||||
OutfoxProcessingError(#[from] OutfoxError),
|
||||
|
||||
#[error("received payload type of an unexpected type: {typ:?}")]
|
||||
UnexpectedLpPayload { typ: LpFrameKind },
|
||||
|
||||
#[error("received an Lp Frame kind that we don't support: {typ:?}")]
|
||||
UnsupportedLpFrameKind { typ: LpFrameKind },
|
||||
|
||||
#[error("unwrapped a packet into a forward hop packet. This is no longer supported")]
|
||||
ForwardHop,
|
||||
|
||||
#[error("{0}")]
|
||||
Internal(String),
|
||||
|
||||
#[error("{0}")]
|
||||
Other(String),
|
||||
}
|
||||
|
||||
impl LpDataHandlerError {
|
||||
pub fn internal(message: impl Into<String>) -> Self {
|
||||
LpDataHandlerError::Internal(message.into())
|
||||
}
|
||||
|
||||
pub fn other(message: impl Into<String>) -> Self {
|
||||
LpDataHandlerError::Other(message.into())
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use nym_lp_data::packet::frame::{LpFrameAttributes, LpFrameHeader, LpFrameKind};
|
||||
use nym_sphinx::forwarding::packet::MixPacketFormattingError;
|
||||
use nym_sphinx::params::SphinxKeyRotation;
|
||||
|
||||
use crate::client::lp::data::handler::error::LpDataHandlerError;
|
||||
|
||||
/// Message types supported by clients
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum ClientMessage {
|
||||
Sphinx(SphinxMessage),
|
||||
Outfox(OutfoxMessage),
|
||||
}
|
||||
|
||||
impl ClientMessage {
|
||||
pub fn from_frame_header(header: LpFrameHeader) -> Result<Self, LpDataHandlerError> {
|
||||
match header.kind {
|
||||
LpFrameKind::SphinxPacket => {
|
||||
Ok(ClientMessage::Sphinx(header.frame_attributes.try_into()?))
|
||||
}
|
||||
LpFrameKind::OutfoxPacket => {
|
||||
Ok(ClientMessage::Outfox(header.frame_attributes.try_into()?))
|
||||
}
|
||||
_ => Err(LpDataHandlerError::UnsupportedLpFrameKind { typ: header.kind }),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct SphinxMessage {
|
||||
pub key_rotation: SphinxKeyRotation,
|
||||
}
|
||||
|
||||
impl TryFrom<LpFrameAttributes> for SphinxMessage {
|
||||
type Error = LpDataHandlerError;
|
||||
|
||||
fn try_from(value: LpFrameAttributes) -> Result<Self, Self::Error> {
|
||||
let key_rotation = value[0]
|
||||
.try_into()
|
||||
.map_err(MixPacketFormattingError::InvalidKeyRotation)?;
|
||||
Ok(SphinxMessage { key_rotation })
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SphinxMessage> for LpFrameAttributes {
|
||||
fn from(value: SphinxMessage) -> Self {
|
||||
let mut attrs = [0; 14];
|
||||
attrs[0] = value.key_rotation as u8;
|
||||
attrs
|
||||
}
|
||||
}
|
||||
|
||||
// For now there are no differences. We can augment this variant when we will need it
|
||||
pub type OutfoxMessage = SphinxMessage;
|
||||
@@ -1,216 +0,0 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::client::inbound_messages::InputMessageReceiver;
|
||||
use crate::client::lp::LpDataHandlerError;
|
||||
use crate::client::lp::data::PACKET_BUFFER_SIZE;
|
||||
use crate::client::lp::data::shared::SharedLpDataState;
|
||||
use nym_lp_data::clients::traits::ClientUnwrappingPipeline;
|
||||
use nym_lp_data::common::traits::TransportUnwrap;
|
||||
use nym_lp_data::packet::{EncryptedLpPacket, MalformedLpPacketError};
|
||||
use nym_lp_data::{AddressedTimedData, TimedData};
|
||||
use std::sync::{Arc, mpsc};
|
||||
use std::time::Instant;
|
||||
use std::{net::SocketAddr, time::Duration};
|
||||
use tokio::sync::mpsc::error::TrySendError;
|
||||
use tokio::time::interval;
|
||||
use tracing::*;
|
||||
|
||||
pub mod error;
|
||||
pub mod messages;
|
||||
pub mod pipeline;
|
||||
mod processing;
|
||||
|
||||
const PIPELINE_TICKING_DURATION: Duration = Duration::from_millis(1);
|
||||
|
||||
/// Bounded queue depth in front of each worker; keeps memory bounded under
|
||||
/// bursty load and provides drop-based backpressure.
|
||||
const WORKER_QUEUE_DEPTH: usize = 128;
|
||||
|
||||
type WorkerOutput = Result<Option<Vec<u8>>, MalformedLpPacketError>;
|
||||
|
||||
/// LP Data Handler for UDP data plane, acts as a pipeline driver and buffer
|
||||
/// for delaying packets. Heavy per-packet processing is fanned out across a
|
||||
/// pool of worker threads spawned on the shared blocking pool tracked by the
|
||||
/// surrounding [`nym_task::ShutdownTracker`].
|
||||
pub struct LpDataHandler {
|
||||
/// Shared state
|
||||
shared_state: Arc<SharedLpDataState>,
|
||||
|
||||
// Outbound pipeline
|
||||
/// Channel to receive data for the outbound pipeline
|
||||
outbound_input_rx: InputMessageReceiver,
|
||||
/// Buffer for outbound packet
|
||||
outbound_pkt_buffer: Vec<AddressedTimedData<EncryptedLpPacket>>,
|
||||
/// Channel to send outgoing data from the outbound pipeline
|
||||
outbound_output_tx: tokio::sync::mpsc::Sender<(EncryptedLpPacket, SocketAddr)>,
|
||||
|
||||
// Inbound pipeline
|
||||
/// Channel to receive incoming data for the inbound pipeline
|
||||
inbound_input_rx: mpsc::Receiver<EncryptedLpPacket>,
|
||||
/// Per-worker job queues (round-robin dispatch).
|
||||
worker_input_txs: Vec<mpsc::SyncSender<TimedData<EncryptedLpPacket>>>,
|
||||
/// Aggregated processed packets returned by the workers. (Inbound data)
|
||||
worker_output_rx: mpsc::Receiver<WorkerOutput>,
|
||||
|
||||
/// Shutdown token
|
||||
shutdown: nym_task::ShutdownToken,
|
||||
}
|
||||
|
||||
impl LpDataHandler {
|
||||
pub(crate) fn new(
|
||||
shared_state: Arc<SharedLpDataState>,
|
||||
outbound_input_rx: InputMessageReceiver,
|
||||
outbound_output_tx: tokio::sync::mpsc::Sender<(EncryptedLpPacket, SocketAddr)>,
|
||||
inbound_input_rx: mpsc::Receiver<EncryptedLpPacket>,
|
||||
// SW TODO : inbound output (worker_output_rx)
|
||||
shutdown_tracker: &nym_task::ShutdownTracker,
|
||||
) -> Result<Self, LpDataHandlerError> {
|
||||
let (worker_output_tx, worker_output_rx) = mpsc::sync_channel(PACKET_BUFFER_SIZE);
|
||||
|
||||
// Allow at least one worker, even if the config says 0
|
||||
let worker_count = 4; // SW Put that in the config
|
||||
|
||||
// Create workers. They will stop naturally when worker_output_rx is dropped.
|
||||
// The mode is decided once here; each closure picks the right pipeline type so
|
||||
// the worker loop monomorphizes against a single concrete pipeline.
|
||||
let worker_input_txs = (0..worker_count)
|
||||
.map(|_| {
|
||||
let (worker_input_tx, _worker_input_rx) = mpsc::sync_channel(WORKER_QUEUE_DEPTH);
|
||||
let _worker_state = shared_state.clone();
|
||||
let _worker_output = worker_output_tx.clone();
|
||||
|
||||
shutdown_tracker.spawn_blocking(move || {
|
||||
// Instantiat pipeline
|
||||
todo!()
|
||||
//Self::run_worker(pipeline, worker_input_rx, worker_output);
|
||||
});
|
||||
|
||||
worker_input_tx
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(Self {
|
||||
shared_state,
|
||||
outbound_input_rx,
|
||||
outbound_pkt_buffer: Vec::new(),
|
||||
outbound_output_tx,
|
||||
inbound_input_rx,
|
||||
worker_input_txs,
|
||||
worker_output_rx,
|
||||
shutdown: shutdown_tracker.clone_shutdown_token(),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn run(&mut self) {
|
||||
info!(
|
||||
workers = self.worker_input_txs.len(),
|
||||
"Starting LP data handler"
|
||||
);
|
||||
let mut ticking_interval = interval(PIPELINE_TICKING_DURATION);
|
||||
let mut next_worker = 0;
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
biased;
|
||||
_ = self.shutdown.cancelled() => {
|
||||
info!("LP data handler: received shutdown signal");
|
||||
break;
|
||||
}
|
||||
|
||||
timestamp = ticking_interval.tick() => {
|
||||
let std_timestamp: Instant = timestamp.into();
|
||||
|
||||
// Drain processed packets returned by workers.
|
||||
while let Ok(processing_result) = self.worker_output_rx.try_recv() {
|
||||
match processing_result {
|
||||
Ok(_packets) => {
|
||||
// Dispatch to application
|
||||
todo!()
|
||||
},
|
||||
Err(e) => {
|
||||
warn!("LP data worker: error processing packet : {e}");
|
||||
},
|
||||
}
|
||||
|
||||
}
|
||||
// Dispatch incoming packets to workers.
|
||||
while let Ok(input) = self.inbound_input_rx.try_recv() {
|
||||
next_worker = self.dispatch_to_workers(
|
||||
TimedData::new(std_timestamp, input),
|
||||
next_worker,
|
||||
);
|
||||
}
|
||||
|
||||
// Run outbound pipeline
|
||||
while let Ok(_input) = self.outbound_input_rx.try_recv() {
|
||||
// Run outbound pipeline and stack result in outbound_pkt_buffer
|
||||
todo!()
|
||||
}
|
||||
|
||||
// Send packets that needs sending
|
||||
for pkt in self.outbound_pkt_buffer.extract_if(.., |p| p.data.timestamp <= std_timestamp) {
|
||||
if let Err(e) = self.outbound_output_tx.try_send((pkt.data.data, pkt.dst)) {
|
||||
match e {
|
||||
TrySendError::Full(_) => {
|
||||
warn!("LP data handler: packet sending buffer is full, the client might be overloaded");
|
||||
},
|
||||
TrySendError::Closed(_) => {
|
||||
break;
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Workers will stop because we are dropping the receiving channel
|
||||
info!("LP data handler shutdown complete");
|
||||
}
|
||||
|
||||
/// Round-robin dispatch a job across worker queues. If the chosen worker is
|
||||
/// full, fall through to the next one; if all are saturated, drop the packet
|
||||
/// (UDP-style) and bump a metric. Returns the worker index to start from on
|
||||
/// the next dispatch.
|
||||
fn dispatch_to_workers(&self, mut job: TimedData<EncryptedLpPacket>, start: usize) -> usize {
|
||||
let n = self.worker_input_txs.len();
|
||||
for offset in 0..n {
|
||||
let idx = (start + offset) % n;
|
||||
match self.worker_input_txs[idx].try_send(job) {
|
||||
Ok(()) => return (idx + 1) % n,
|
||||
Err(mpsc::TrySendError::Full(returned)) => {
|
||||
job = returned;
|
||||
}
|
||||
Err(mpsc::TrySendError::Disconnected(returned)) => {
|
||||
error!(
|
||||
"LP data worker {idx} disconnected; this shouldn't happen outside of shut down"
|
||||
);
|
||||
job = returned;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
warn!("LP data handler: all workers saturated, dropping packet");
|
||||
start
|
||||
}
|
||||
|
||||
fn run_worker<P>(
|
||||
mut pipeline: P,
|
||||
input_rx: mpsc::Receiver<TimedData<EncryptedLpPacket>>,
|
||||
output_tx: mpsc::SyncSender<WorkerOutput>,
|
||||
) where
|
||||
P: ClientUnwrappingPipeline<EncryptedLpPacket, ()> // SW fill in message kind
|
||||
+ TransportUnwrap<EncryptedLpPacket, Error = MalformedLpPacketError>, // This is needed to specify the error type
|
||||
{
|
||||
while let Ok(input) = input_rx.recv() {
|
||||
// Blocking is fine, we don't want to unclog ourself and process a new packet that will be dropped anyway
|
||||
if let Err(e) = output_tx.send(pipeline.unwrap(input.data, input.timestamp)) {
|
||||
trace!(
|
||||
"Failed to send processing data back to handler : {e}. We are probably shutting down"
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
pub(crate) mod outfox;
|
||||
pub(crate) mod sphinx;
|
||||
@@ -1,37 +0,0 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use nym_lp_data::TimedPayload;
|
||||
use nym_sphinx::OutfoxPacket;
|
||||
use tracing::warn;
|
||||
|
||||
use crate::client::lp::data::{
|
||||
handler::{error::LpDataHandlerError, messages::OutfoxMessage},
|
||||
shared::SharedLpDataState,
|
||||
};
|
||||
|
||||
pub(crate) fn process(
|
||||
shared_state: &SharedLpDataState,
|
||||
outfox_packet: TimedPayload,
|
||||
_metadata: OutfoxMessage,
|
||||
) -> Result<TimedPayload, LpDataHandlerError> {
|
||||
let TimedPayload {
|
||||
data: outfox_bytes,
|
||||
timestamp: arrival_timestamp,
|
||||
} = outfox_packet;
|
||||
|
||||
let mut outfox_packet = OutfoxPacket::try_from(outfox_bytes.as_slice())?;
|
||||
|
||||
let _next_address =
|
||||
outfox_packet.decode_next_layer(shared_state.encryption_keys.private_key().as_ref())?;
|
||||
|
||||
if outfox_packet.is_final_hop() {
|
||||
Ok(TimedPayload::new(
|
||||
arrival_timestamp,
|
||||
outfox_packet.payload().to_vec(),
|
||||
))
|
||||
} else {
|
||||
warn!("Dropping forward hop packet in a client");
|
||||
Err(LpDataHandlerError::ForwardHop)
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use nym_lp_data::TimedPayload;
|
||||
use nym_sphinx::{ProcessedPacketData, SphinxPacket};
|
||||
use tracing::warn;
|
||||
|
||||
use crate::client::lp::data::{
|
||||
handler::{error::LpDataHandlerError, messages::SphinxMessage},
|
||||
shared::SharedLpDataState,
|
||||
};
|
||||
|
||||
pub(crate) fn process(
|
||||
shared_state: &SharedLpDataState,
|
||||
sphinx_packet: TimedPayload,
|
||||
_metadata: SphinxMessage,
|
||||
) -> Result<TimedPayload, LpDataHandlerError> {
|
||||
let TimedPayload {
|
||||
data: sphinx_bytes,
|
||||
timestamp: arrival_timestamp,
|
||||
} = sphinx_packet;
|
||||
|
||||
let sphinx_packet = SphinxPacket::from_bytes(&sphinx_bytes)?;
|
||||
|
||||
// Final processing
|
||||
let processed_packet =
|
||||
sphinx_packet.process(shared_state.encryption_keys.private_key().as_ref())?;
|
||||
|
||||
match processed_packet.data {
|
||||
ProcessedPacketData::ForwardHop { .. } => {
|
||||
warn!("Dropping forward hop packet in a client");
|
||||
Err(LpDataHandlerError::ForwardHop)
|
||||
}
|
||||
ProcessedPacketData::FinalHop { payload, .. } => Ok(TimedPayload::new(
|
||||
arrival_timestamp,
|
||||
payload.recover_plaintext()?,
|
||||
)),
|
||||
}
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::client::lp::data::MAX_UDP_PACKET_SIZE;
|
||||
use crate::client::lp::data::shared::SharedLpDataState;
|
||||
use crate::error::ClientCoreError;
|
||||
use nym_lp_data::packet::EncryptedLpPacket;
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::{Arc, mpsc, mpsc::TrySendError};
|
||||
use tokio::net::UdpSocket;
|
||||
use tracing::log::warn;
|
||||
use tracing::{error, info};
|
||||
|
||||
/// LP UDP listener that accepts TCP connections on port 51264 (by default)
|
||||
pub(crate) struct LpDataListener {
|
||||
/// Shared state
|
||||
shared_state: Arc<SharedLpDataState>,
|
||||
|
||||
/// Channel to send incoming data to the processing pipeline
|
||||
inbound_input_tx: mpsc::SyncSender<EncryptedLpPacket>,
|
||||
|
||||
// This has to be a tokio channel, to be async and bounded
|
||||
/// Channel to receive outgoing data from the processling pipeline
|
||||
outbound_output_rx: tokio::sync::mpsc::Receiver<(EncryptedLpPacket, SocketAddr)>,
|
||||
|
||||
/// Shutdown token
|
||||
shutdown: nym_task::ShutdownToken,
|
||||
}
|
||||
|
||||
impl LpDataListener {
|
||||
pub fn new(
|
||||
shared_state: Arc<SharedLpDataState>,
|
||||
inbound_input_tx: mpsc::SyncSender<EncryptedLpPacket>,
|
||||
outbound_output_rx: tokio::sync::mpsc::Receiver<(EncryptedLpPacket, SocketAddr)>,
|
||||
shutdown: nym_task::ShutdownToken,
|
||||
) -> Self {
|
||||
Self {
|
||||
shared_state,
|
||||
inbound_input_tx,
|
||||
outbound_output_rx,
|
||||
shutdown,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn run(&mut self) -> Result<(), ClientCoreError> {
|
||||
let socket = UdpSocket::bind("[::]:0").await.map_err(|source| {
|
||||
error!("Failed to bind LP data socket: {source}");
|
||||
ClientCoreError::LpBindFailure { source }
|
||||
})?;
|
||||
info!("Started LP data socket on {}", socket.local_addr()?);
|
||||
|
||||
let mut buf = vec![0u8; MAX_UDP_PACKET_SIZE];
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
biased;
|
||||
_ = self.shutdown.cancelled() => {
|
||||
info!("LP data listener: received shutdown signal");
|
||||
break;
|
||||
}
|
||||
|
||||
result = self.outbound_output_rx.recv() => {
|
||||
match result {
|
||||
Some((payload, dst_addr)) => {
|
||||
if let Err(e) = socket.send_to(&payload.to_bytes(), dst_addr).await {
|
||||
warn!("LP data packet error to {dst_addr}: {e}");
|
||||
}
|
||||
}
|
||||
None => {
|
||||
warn!("LP outgoing packet channel closed");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result = socket.recv_from(&mut buf) => {
|
||||
match result {
|
||||
Ok((len, src_addr)) => {
|
||||
info!("received {len} bytes from {src_addr} on the LP Data socket");
|
||||
if let Ok(encrypted_packet) = EncryptedLpPacket::decode(&buf[..len]) {
|
||||
if let Err(e) = self.inbound_input_tx.try_send(encrypted_packet) {
|
||||
match e {
|
||||
TrySendError::Full(_) => {
|
||||
warn!("LP data listener: packet sending buffer is full, the client might be overloaded");
|
||||
},
|
||||
TrySendError::Disconnected(_) => {
|
||||
warn!("LP data listener: incoming packet channel is closed");
|
||||
break;
|
||||
},
|
||||
}
|
||||
}
|
||||
} else {
|
||||
warn!("Error reading LP packet from wire");
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("LP data socket recv error: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
info!("LP data handler shutdown complete");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Parking the branch
|
||||
#![allow(clippy::todo)]
|
||||
#![allow(dead_code)]
|
||||
#![allow(clippy::incompatible_msrv)]
|
||||
|
||||
use std::sync::{Arc, mpsc};
|
||||
|
||||
use crate::client::inbound_messages::InputMessageReceiver;
|
||||
use crate::client::lp::data::handler::LpDataHandler;
|
||||
use crate::client::lp::data::listener::LpDataListener;
|
||||
use crate::client::lp::data::shared::SharedLpDataState;
|
||||
use crate::error::ClientCoreError;
|
||||
|
||||
use nym_task::ShutdownTracker;
|
||||
use tracing::error;
|
||||
|
||||
/// Maximum UDP packet size we'll accept
|
||||
/// Sphinx packets are typically ~2KB, LP overhead is ~50 bytes, so 4KB is plenty
|
||||
const MAX_UDP_PACKET_SIZE: usize = 4096;
|
||||
|
||||
pub(crate) const PACKET_BUFFER_SIZE: usize = 100;
|
||||
|
||||
pub mod handler;
|
||||
mod listener;
|
||||
pub mod shared;
|
||||
|
||||
pub struct LpDataSetup {
|
||||
listener: LpDataListener,
|
||||
|
||||
handler: LpDataHandler,
|
||||
|
||||
/// Shutdown coordination
|
||||
shutdown: ShutdownTracker,
|
||||
}
|
||||
|
||||
impl LpDataSetup {
|
||||
pub(crate) fn new(
|
||||
shared_state: SharedLpDataState,
|
||||
outbound_input_rx: InputMessageReceiver,
|
||||
shutdown: ShutdownTracker,
|
||||
) -> Result<Self, ClientCoreError> {
|
||||
let (inbound_input_tx, inbound_input_rx) = mpsc::sync_channel(PACKET_BUFFER_SIZE);
|
||||
let (outbound_output_tx, outbound_output_rx) =
|
||||
tokio::sync::mpsc::channel(PACKET_BUFFER_SIZE);
|
||||
|
||||
let shared_state = Arc::new(shared_state);
|
||||
|
||||
let listener = LpDataListener::new(
|
||||
shared_state.clone(),
|
||||
inbound_input_tx,
|
||||
outbound_output_rx,
|
||||
shutdown.clone_shutdown_token(),
|
||||
);
|
||||
|
||||
let handler = LpDataHandler::new(
|
||||
shared_state,
|
||||
outbound_input_rx,
|
||||
outbound_output_tx,
|
||||
inbound_input_rx,
|
||||
&shutdown,
|
||||
)?;
|
||||
|
||||
Ok(LpDataSetup {
|
||||
listener,
|
||||
handler,
|
||||
shutdown,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn start_tasks(mut self) {
|
||||
// Spawn the UDP data handler for LP data plane
|
||||
// The data handler listens on UDP port 51264 and processes LP-wrapped Sphinx packets
|
||||
// from registered clients. It decrypts the LP layer and forwards the Sphinx packets
|
||||
let shutdown_token = self.shutdown.clone_shutdown_token();
|
||||
let mut listener = self.listener;
|
||||
self.shutdown.try_spawn_named(
|
||||
async move {
|
||||
if let Err(err) = listener.run().await {
|
||||
shutdown_token.cancel();
|
||||
error!("LP data listener error: {err}");
|
||||
}
|
||||
},
|
||||
"LP::LpDataListener",
|
||||
);
|
||||
|
||||
self.shutdown
|
||||
.try_spawn_named(async move { self.handler.run().await }, "LP::LpDataHandler");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
// Sphinx packets are typically around 2KB
|
||||
// 4KB should be plenty with room to spare
|
||||
const _: () = {
|
||||
assert!(MAX_UDP_PACKET_SIZE >= 2048 + 100);
|
||||
};
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use nym_client_core_config_types::DebugConfig;
|
||||
use nym_crypto::asymmetric::{ed25519, x25519};
|
||||
use nym_lp_data::fragmentation::reconstruction::MessageReconstructor;
|
||||
use nym_task::ShutdownToken;
|
||||
|
||||
/// Shared state for LP data plane
|
||||
pub struct SharedLpDataState {
|
||||
pub(crate) config: DebugConfig,
|
||||
|
||||
pub(crate) encryption_keys: Arc<x25519::KeyPair>,
|
||||
pub(crate) identity_keys: Arc<ed25519::KeyPair>,
|
||||
|
||||
pub(crate) message_reconstructor: MessageReconstructor,
|
||||
|
||||
pub(crate) shutdown_token: ShutdownToken,
|
||||
}
|
||||
|
||||
impl SharedLpDataState {
|
||||
pub(crate) fn new(
|
||||
config: DebugConfig,
|
||||
encryption_keys: Arc<x25519::KeyPair>,
|
||||
identity_keys: Arc<ed25519::KeyPair>,
|
||||
shutdown_token: ShutdownToken,
|
||||
) -> Self {
|
||||
SharedLpDataState {
|
||||
config,
|
||||
encryption_keys,
|
||||
identity_keys,
|
||||
message_reconstructor: Default::default(),
|
||||
shutdown_token,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
pub use data::handler::error::LpDataHandlerError;
|
||||
|
||||
pub mod data;
|
||||
@@ -240,7 +240,7 @@ mod nonwasm_sealed {
|
||||
impl GatewaySender for LocalGateway {
|
||||
async fn send_mix_packet(&mut self, packet: MixPacket) -> Result<(), ErasedGatewayError> {
|
||||
self.packet_forwarder
|
||||
.forward_packet(packet)
|
||||
.forward_client_packet_without_delay(packet)
|
||||
.map_err(erase_err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ pub(crate) mod event_control;
|
||||
pub(crate) mod helpers;
|
||||
pub mod inbound_messages;
|
||||
pub mod key_manager;
|
||||
pub mod lp;
|
||||
pub mod mix_traffic;
|
||||
pub mod real_messages_control;
|
||||
pub mod received_buffer;
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// Copyright 2022-2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::client::lp::LpDataHandlerError;
|
||||
use crate::client::mix_traffic::transceiver::ErasedGatewayError;
|
||||
use nym_crypto::asymmetric::ed25519::Ed25519RecoveryError;
|
||||
use nym_gateway_client::error::GatewayClientError;
|
||||
@@ -264,12 +263,6 @@ pub enum ClientCoreError {
|
||||
|
||||
#[error("Could not access task registry, {0}")]
|
||||
RegistryAccess(#[from] RegistryAccessError),
|
||||
|
||||
#[error("failed to bind LP UDP socket: {source}")]
|
||||
LpBindFailure { source: std::io::Error },
|
||||
|
||||
#[error(transparent)]
|
||||
LpFailure(#[from] LpDataHandlerError),
|
||||
}
|
||||
|
||||
impl From<tungstenite::Error> for ClientCoreError {
|
||||
|
||||
@@ -34,3 +34,4 @@ client = ["tokio-util", "nym-task", "tokio/net", "tokio/rt"]
|
||||
[dev-dependencies]
|
||||
nym-crypto = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
tokio = { workspace = true, features = ["macros", "io-util", "rt", "rt-multi-thread"] }
|
||||
|
||||
@@ -2,10 +2,9 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use dashmap::DashMap;
|
||||
use futures::StreamExt;
|
||||
use futures::{SinkExt, StreamExt};
|
||||
use nym_noise::config::NoiseConfig;
|
||||
use nym_noise::upgrade_noise_initiator;
|
||||
use nym_sphinx::addressing::nodes::NymNodeRoutingAddress;
|
||||
use nym_sphinx::forwarding::packet::MixPacket;
|
||||
use nym_sphinx::framing::codec::NymCodec;
|
||||
use nym_sphinx::framing::packet::FramedNymPacket;
|
||||
@@ -15,6 +14,7 @@ use std::ops::Deref;
|
||||
use std::sync::atomic::{AtomicU32, AtomicUsize, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tokio::io::{AsyncRead, AsyncWrite};
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::sync::mpsc::error::TrySendError;
|
||||
@@ -91,13 +91,17 @@ impl Deref for ActiveConnections {
|
||||
pub struct ConnectionSender {
|
||||
channel: mpsc::Sender<FramedNymPacket>,
|
||||
current_reconnection_attempt: Arc<AtomicU32>,
|
||||
// Identifies the `ManagedConnection` task currently owning this entry; used
|
||||
// to ensure drop-time eviction only fires on the still-owning task.
|
||||
handle_token: Arc<()>,
|
||||
}
|
||||
|
||||
impl ConnectionSender {
|
||||
fn new(channel: mpsc::Sender<FramedNymPacket>) -> Self {
|
||||
fn new(channel: mpsc::Sender<FramedNymPacket>, handle_token: Arc<()>) -> Self {
|
||||
ConnectionSender {
|
||||
channel,
|
||||
current_reconnection_attempt: Arc::new(AtomicU32::new(0)),
|
||||
handle_token,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -108,6 +112,31 @@ struct ManagedConnection {
|
||||
message_receiver: ReceiverStream<FramedNymPacket>,
|
||||
connection_timeout: Duration,
|
||||
current_reconnection: Arc<AtomicU32>,
|
||||
active_connections: ActiveConnections,
|
||||
handle_token: Arc<()>,
|
||||
}
|
||||
|
||||
// Evicts the cache entry on task exit (only if still owned by this task).
|
||||
// Without this, a stale `ConnectionSender` survives after the peer disconnects
|
||||
// and the next outbound packet is silently swallowed by the dead TCP.
|
||||
struct EvictOnDrop {
|
||||
active_connections: ActiveConnections,
|
||||
address: SocketAddr,
|
||||
handle_token: Arc<()>,
|
||||
}
|
||||
|
||||
impl Drop for EvictOnDrop {
|
||||
fn drop(&mut self) {
|
||||
let address = self.address;
|
||||
let handle_token = &self.handle_token;
|
||||
self.active_connections.remove_if(&address, |_, sender| {
|
||||
Arc::ptr_eq(&sender.handle_token, handle_token)
|
||||
});
|
||||
trace!(
|
||||
peer = %address,
|
||||
"managed connection task exited; evicted owning cache entry"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
impl ManagedConnection {
|
||||
@@ -117,6 +146,8 @@ impl ManagedConnection {
|
||||
message_receiver: mpsc::Receiver<FramedNymPacket>,
|
||||
connection_timeout: Duration,
|
||||
current_reconnection: Arc<AtomicU32>,
|
||||
active_connections: ActiveConnections,
|
||||
handle_token: Arc<()>,
|
||||
) -> Self {
|
||||
ManagedConnection {
|
||||
address,
|
||||
@@ -124,72 +155,30 @@ impl ManagedConnection {
|
||||
message_receiver: ReceiverStream::new(message_receiver),
|
||||
connection_timeout,
|
||||
current_reconnection,
|
||||
active_connections,
|
||||
handle_token,
|
||||
}
|
||||
}
|
||||
|
||||
async fn run(self) {
|
||||
let address = self.address;
|
||||
let _evict_guard = EvictOnDrop {
|
||||
active_connections: self.active_connections,
|
||||
address,
|
||||
handle_token: self.handle_token,
|
||||
};
|
||||
|
||||
let reconnection_attempt = self.current_reconnection.load(Ordering::Acquire);
|
||||
let connect_start = tokio::time::Instant::now();
|
||||
let connection_fut = TcpStream::connect(address);
|
||||
|
||||
let conn = match tokio::time::timeout(self.connection_timeout, connection_fut).await {
|
||||
Ok(stream_res) => match stream_res {
|
||||
Ok(stream) => {
|
||||
let connect_ms = connect_start.elapsed().as_millis() as u64;
|
||||
debug!(
|
||||
peer = %address,
|
||||
connect_ms,
|
||||
"Managed to establish connection to {}", self.address
|
||||
);
|
||||
|
||||
let noise_start = tokio::time::Instant::now();
|
||||
let noise_stream =
|
||||
match upgrade_noise_initiator(stream, &self.noise_config).await {
|
||||
Ok(noise_stream) => noise_stream,
|
||||
Err(err) => {
|
||||
let noise_handshake_ms = noise_start.elapsed().as_millis() as u64;
|
||||
warn!(
|
||||
event = "connection.failed.noise",
|
||||
peer = %address,
|
||||
error = %err,
|
||||
connect_ms,
|
||||
noise_handshake_ms,
|
||||
reconnection_attempt,
|
||||
exit_reason = "noise_error",
|
||||
"Failed to perform Noise initiator handshake with {address}"
|
||||
);
|
||||
self.current_reconnection.fetch_add(1, Ordering::SeqCst);
|
||||
return;
|
||||
}
|
||||
};
|
||||
let noise_handshake_ms = noise_start.elapsed().as_millis() as u64;
|
||||
self.current_reconnection.store(0, Ordering::Release);
|
||||
debug!(
|
||||
peer = %address,
|
||||
connect_ms,
|
||||
noise_handshake_ms,
|
||||
"Noise initiator handshake completed for {:?}", address
|
||||
);
|
||||
Framed::new(noise_stream, NymCodec)
|
||||
}
|
||||
Err(err) => {
|
||||
let connect_ms = connect_start.elapsed().as_millis() as u64;
|
||||
warn!(
|
||||
event = "connection.failed.connect",
|
||||
peer = %address,
|
||||
error = %err,
|
||||
connect_ms,
|
||||
reconnection_attempt,
|
||||
exit_reason = "connect_error",
|
||||
"failed to establish connection to {address}"
|
||||
);
|
||||
return;
|
||||
}
|
||||
},
|
||||
// 1. attempt to establish the connection with timeout
|
||||
let maybe_stream = match tokio::time::timeout(self.connection_timeout, connection_fut).await
|
||||
{
|
||||
Ok(stream) => stream,
|
||||
Err(_) => {
|
||||
let connect_ms = connect_start.elapsed().as_millis() as u64;
|
||||
warn!(
|
||||
debug!(
|
||||
event = "connection.failed.timeout",
|
||||
peer = %address,
|
||||
timeout_ms = self.connection_timeout.as_millis() as u64,
|
||||
@@ -204,21 +193,133 @@ impl ManagedConnection {
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(err) = self.message_receiver.map(Ok).forward(conn).await {
|
||||
warn!(
|
||||
event = "connection.forward_error",
|
||||
peer = %address,
|
||||
error = %err,
|
||||
exit_reason = "forward_error",
|
||||
"Failed to forward packets to {address}: {err}"
|
||||
);
|
||||
}
|
||||
// 2. check if it actually succeeded
|
||||
let stream = match maybe_stream {
|
||||
Ok(stream) => stream,
|
||||
Err(err) => {
|
||||
let connect_ms = connect_start.elapsed().as_millis() as u64;
|
||||
debug!(
|
||||
event = "connection.failed.connect",
|
||||
peer = %address,
|
||||
error = %err,
|
||||
connect_ms,
|
||||
reconnection_attempt,
|
||||
exit_reason = "connect_error",
|
||||
"failed to establish connection to {address}"
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let connect_ms = connect_start.elapsed().as_millis() as u64;
|
||||
debug!(
|
||||
peer = %address,
|
||||
exit_reason = "sender_dropped",
|
||||
"connection manager to {address} finished"
|
||||
connect_ms,
|
||||
"Managed to establish connection to {}", self.address
|
||||
);
|
||||
|
||||
// 3. perform noise handshake (if applicable)
|
||||
let noise_start = tokio::time::Instant::now();
|
||||
let noise_stream = match upgrade_noise_initiator(stream, &self.noise_config).await {
|
||||
Ok(noise_stream) => noise_stream,
|
||||
Err(err) => {
|
||||
let noise_handshake_ms = noise_start.elapsed().as_millis() as u64;
|
||||
debug!(
|
||||
event = "connection.failed.noise",
|
||||
peer = %address,
|
||||
error = %err,
|
||||
connect_ms,
|
||||
noise_handshake_ms,
|
||||
reconnection_attempt,
|
||||
exit_reason = "noise_error",
|
||||
"Failed to perform Noise initiator handshake with {address}"
|
||||
);
|
||||
self.current_reconnection.fetch_add(1, Ordering::SeqCst);
|
||||
return;
|
||||
}
|
||||
};
|
||||
let noise_handshake_ms = noise_start.elapsed().as_millis() as u64;
|
||||
self.current_reconnection.store(0, Ordering::Release);
|
||||
debug!(
|
||||
peer = %address,
|
||||
connect_ms,
|
||||
noise_handshake_ms,
|
||||
"Noise initiator handshake completed for {:?}", address
|
||||
);
|
||||
let conn = Framed::new(noise_stream, NymCodec);
|
||||
|
||||
// 4. start handling the framed stream
|
||||
run_io_loop(conn, self.message_receiver, address).await;
|
||||
}
|
||||
}
|
||||
|
||||
// The connection is unidirectional (send-only); we read from it solely to
|
||||
// notice peer FIN/RST while idle so we can evict the cache entry before the
|
||||
// next outbound send finds it stale.
|
||||
async fn run_io_loop<T>(
|
||||
conn: Framed<T, NymCodec>,
|
||||
mut receiver: ReceiverStream<FramedNymPacket>,
|
||||
address: SocketAddr,
|
||||
) where
|
||||
T: AsyncRead + AsyncWrite + Unpin,
|
||||
{
|
||||
let (mut sink, mut stream) = conn.split();
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
msg = stream.next() => {
|
||||
match msg {
|
||||
None => {
|
||||
debug!(
|
||||
peer = %address,
|
||||
exit_reason = "peer_closed",
|
||||
"peer closed mixnet connection to {address}"
|
||||
);
|
||||
break;
|
||||
}
|
||||
Some(Err(err)) => {
|
||||
debug!(
|
||||
event = "connection.read_error",
|
||||
peer = %address,
|
||||
error = %err,
|
||||
exit_reason = "read_error",
|
||||
"read error on mixnet connection to {address}: {err}"
|
||||
);
|
||||
break;
|
||||
}
|
||||
Some(Ok(_)) => {
|
||||
trace!(
|
||||
peer = %address,
|
||||
"unexpected inbound packet on mixnet connection to {address}; discarding"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
outgoing = receiver.next() => {
|
||||
match outgoing {
|
||||
None => {
|
||||
debug!(
|
||||
peer = %address,
|
||||
exit_reason = "sender_dropped",
|
||||
"connection manager to {address} finished"
|
||||
);
|
||||
break;
|
||||
}
|
||||
Some(packet) => {
|
||||
if let Err(err) = sink.send(packet).await {
|
||||
debug!(
|
||||
event = "connection.forward_error",
|
||||
peer = %address,
|
||||
error = %err,
|
||||
exit_reason = "forward_error",
|
||||
"Failed to forward packet to {address}: {err}"
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -265,13 +366,18 @@ impl Client {
|
||||
sender.try_send(pending_packet).unwrap();
|
||||
}
|
||||
|
||||
// Ownership token for the task we're about to spawn; lets it tell
|
||||
// on exit whether the cache entry still names it.
|
||||
let handle_token = Arc::new(());
|
||||
|
||||
// if we already tried to connect to `address` before, grab the current attempt count
|
||||
let current_reconnection_attempt =
|
||||
if let Some(mut existing) = self.active_connections.get_mut(&address) {
|
||||
existing.channel = sender;
|
||||
existing.handle_token = Arc::clone(&handle_token);
|
||||
Arc::clone(&existing.current_reconnection_attempt)
|
||||
} else {
|
||||
let new_entry = ConnectionSender::new(sender);
|
||||
let new_entry = ConnectionSender::new(sender, Arc::clone(&handle_token));
|
||||
let current_attempt = Arc::clone(&new_entry.current_reconnection_attempt);
|
||||
self.active_connections.insert(address, new_entry);
|
||||
current_attempt
|
||||
@@ -286,6 +392,7 @@ impl Client {
|
||||
|
||||
let connections_count = self.connections_count.clone();
|
||||
let noise_config = self.noise_config.clone();
|
||||
let active_connections = self.active_connections.clone();
|
||||
tokio::spawn(async move {
|
||||
// before executing the manager, wait for what was specified, if anything
|
||||
if let Some(backoff) = backoff {
|
||||
@@ -300,6 +407,8 @@ impl Client {
|
||||
receiver,
|
||||
initial_connection_timeout,
|
||||
current_reconnection_attempt,
|
||||
active_connections,
|
||||
handle_token,
|
||||
)
|
||||
.run()
|
||||
.await;
|
||||
@@ -310,13 +419,7 @@ impl Client {
|
||||
|
||||
impl SendWithoutResponse for Client {
|
||||
fn send_without_response(&self, packet: MixPacket) -> io::Result<()> {
|
||||
let address = match packet.next_hop() {
|
||||
NymNodeRoutingAddress::Client(_) => {
|
||||
warn!("mix packet addressed to a client in the legacy send_without_response path. This should never happen!");
|
||||
return Ok(());
|
||||
}
|
||||
NymNodeRoutingAddress::Node(address) => address,
|
||||
};
|
||||
let address = packet.next_hop_address();
|
||||
trace!("Sending packet to {address}");
|
||||
|
||||
// TODO: optimisation for the future: rather than constantly using legacy encoding,
|
||||
@@ -435,4 +538,102 @@ mod tests {
|
||||
client.config.maximum_reconnection_backoff
|
||||
);
|
||||
}
|
||||
|
||||
fn test_addr() -> SocketAddr {
|
||||
"127.0.0.1:1".parse().unwrap()
|
||||
}
|
||||
|
||||
fn insert_with_token(
|
||||
active: &ActiveConnections,
|
||||
addr: SocketAddr,
|
||||
token: Arc<()>,
|
||||
) -> mpsc::Receiver<FramedNymPacket> {
|
||||
let (tx, rx) = mpsc::channel(1);
|
||||
active.insert(addr, ConnectionSender::new(tx, token));
|
||||
rx
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn evict_on_drop_removes_entry_when_token_still_matches() {
|
||||
let active = ActiveConnections::default();
|
||||
let addr = test_addr();
|
||||
let token = Arc::new(());
|
||||
let _rx = insert_with_token(&active, addr, Arc::clone(&token));
|
||||
|
||||
assert!(active.get(&addr).is_some());
|
||||
|
||||
{
|
||||
let _guard = EvictOnDrop {
|
||||
active_connections: active.clone(),
|
||||
address: addr,
|
||||
handle_token: token,
|
||||
};
|
||||
}
|
||||
|
||||
assert!(
|
||||
active.get(&addr).is_none(),
|
||||
"owning task's drop should evict the entry"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn evict_on_drop_preserves_entry_replaced_by_newer_make_connection() {
|
||||
// Simulates the race: old task's run() has returned, but before its
|
||||
// drop guard fires, a concurrent `make_connection` replaced the
|
||||
// entry's channel + handle_token with a fresh task's token.
|
||||
let active = ActiveConnections::default();
|
||||
let addr = test_addr();
|
||||
let old_token = Arc::new(());
|
||||
let new_token = Arc::new(());
|
||||
let _rx_new = insert_with_token(&active, addr, Arc::clone(&new_token));
|
||||
|
||||
{
|
||||
let _guard = EvictOnDrop {
|
||||
active_connections: active.clone(),
|
||||
address: addr,
|
||||
handle_token: old_token,
|
||||
};
|
||||
}
|
||||
|
||||
assert!(
|
||||
active.get(&addr).is_some(),
|
||||
"old task's drop must not clobber the newer entry"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn io_loop_exits_when_peer_closes_idle_connection() {
|
||||
// The fix's second half: while no packets are flowing, peer FIN/RST
|
||||
// must still be observed so the cache entry can be evicted before the
|
||||
// next send finds it stale.
|
||||
let (a, b) = tokio::io::duplex(64);
|
||||
let conn = Framed::new(a, NymCodec);
|
||||
let (_tx, rx) = mpsc::channel(1);
|
||||
|
||||
let task = tokio::spawn(run_io_loop(conn, ReceiverStream::new(rx), test_addr()));
|
||||
|
||||
// Simulate peer closing both directions of the connection.
|
||||
drop(b);
|
||||
|
||||
tokio::time::timeout(Duration::from_secs(1), task)
|
||||
.await
|
||||
.expect("io_loop must notice peer close while idle")
|
||||
.expect("io_loop task must not panic");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn io_loop_exits_when_sender_dropped() {
|
||||
let (a, _b) = tokio::io::duplex(64);
|
||||
let conn = Framed::new(a, NymCodec);
|
||||
let (tx, rx) = mpsc::channel(1);
|
||||
|
||||
let task = tokio::spawn(run_io_loop(conn, ReceiverStream::new(rx), test_addr()));
|
||||
|
||||
drop(tx);
|
||||
|
||||
tokio::time::timeout(Duration::from_secs(1), task)
|
||||
.await
|
||||
.expect("io_loop must exit when the upstream sender is dropped")
|
||||
.expect("io_loop task must not panic");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,12 +21,16 @@ impl From<mpsc::UnboundedSender<PacketToForward>> for MixForwardingSender {
|
||||
}
|
||||
|
||||
impl MixForwardingSender {
|
||||
pub fn forward_packet(&self, packet: impl Into<PacketToForward>) -> Result<(), SendError> {
|
||||
pub fn forward_packet(&self, packet: PacketToForward) -> Result<(), SendError> {
|
||||
self.0
|
||||
.unbounded_send(packet.into())
|
||||
.unbounded_send(packet)
|
||||
.map_err(|err| err.into_send_error())
|
||||
}
|
||||
|
||||
pub fn forward_client_packet_without_delay(&self, packet: MixPacket) -> Result<(), SendError> {
|
||||
self.forward_packet(PacketToForward::client_packet_without_delay(packet))
|
||||
}
|
||||
|
||||
#[allow(clippy::len_without_is_empty)]
|
||||
pub fn len(&self) -> usize {
|
||||
self.0.len()
|
||||
@@ -38,35 +42,23 @@ pub type MixForwardingReceiver = mpsc::UnboundedReceiver<PacketToForward>;
|
||||
pub struct PacketToForward {
|
||||
pub packet: MixPacket,
|
||||
pub forward_delay_target: Option<Instant>,
|
||||
}
|
||||
|
||||
impl From<MixPacket> for PacketToForward {
|
||||
fn from(packet: MixPacket) -> Self {
|
||||
PacketToForward::new_no_delay(packet)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(MixPacket, Option<Instant>)> for PacketToForward {
|
||||
fn from((packet, delay_until): (MixPacket, Option<Instant>)) -> Self {
|
||||
PacketToForward::new(packet, delay_until)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(MixPacket, Instant)> for PacketToForward {
|
||||
fn from((packet, delay_until): (MixPacket, Instant)) -> Self {
|
||||
PacketToForward::new(packet, Some(delay_until))
|
||||
}
|
||||
pub network_monitor_packet: bool,
|
||||
}
|
||||
|
||||
impl PacketToForward {
|
||||
pub fn new(packet: MixPacket, forward_delay_target: Option<Instant>) -> Self {
|
||||
pub fn new(
|
||||
packet: MixPacket,
|
||||
forward_delay_target: Option<Instant>,
|
||||
network_monitor_packet: bool,
|
||||
) -> Self {
|
||||
PacketToForward {
|
||||
packet,
|
||||
forward_delay_target,
|
||||
network_monitor_packet,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_no_delay(packet: MixPacket) -> Self {
|
||||
Self::new(packet, None)
|
||||
pub fn client_packet_without_delay(packet: MixPacket) -> Self {
|
||||
Self::new(packet, None, false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ nym-ecash-contract-common = { workspace = true }
|
||||
nym-multisig-contract-common = { workspace = true }
|
||||
nym-group-contract-common = { workspace = true }
|
||||
nym-performance-contract-common = { workspace = true }
|
||||
nym-node-families-contract-common = { workspace = true }
|
||||
nym-network-monitors-contract-common = { workspace = true }
|
||||
nym-serde-helpers = { workspace = true, features = ["hex", "base64"] }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
|
||||
@@ -104,6 +104,14 @@ impl TryFrom<NymNetworkDetails> for Config {
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn new(nyxd_url: Url, api_url: Url, nyxd_config: nyxd::Config) -> Self {
|
||||
Config {
|
||||
api_url,
|
||||
nyxd_url,
|
||||
nyxd_config,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_from_nym_network_details(
|
||||
details: &NymNetworkDetails,
|
||||
) -> Result<Self, ValidatorClientError> {
|
||||
@@ -114,6 +122,15 @@ impl Config {
|
||||
.map(|url| Url::parse(url))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
if let Some(nym_api_urls) = details.nym_api_urls.as_ref() {
|
||||
api_url.extend(
|
||||
nym_api_urls
|
||||
.iter()
|
||||
.map(|url| url.url.parse())
|
||||
.collect::<Result<Vec<_>, _>>()?,
|
||||
);
|
||||
}
|
||||
|
||||
if api_url.is_empty() {
|
||||
return Err(ValidatorClientError::NoAPIUrlAvailable);
|
||||
}
|
||||
|
||||
@@ -15,12 +15,15 @@ use nym_api_requests::ecash::models::{
|
||||
VerifyEcashTicketBody,
|
||||
};
|
||||
use nym_api_requests::ecash::VerificationKeyResponse;
|
||||
use nym_api_requests::models::node_families::NodeFamily;
|
||||
use nym_api_requests::models::network_monitor::{
|
||||
KnownNetworkMonitorResponse, StressTestBatchSubmission,
|
||||
};
|
||||
use nym_api_requests::models::{
|
||||
AnnotationResponse, ApiHealthResponse, BinaryBuildInformationOwned, ChainBlocksStatusResponse,
|
||||
ChainStatusResponse, KeyRotationInfoResponse, NodePerformanceResponse, NodeRefreshBody,
|
||||
NymNodeDescriptionV1, NymNodeDescriptionV2, PerformanceHistoryResponse, RewardedSetResponse,
|
||||
SignerInformationResponse,
|
||||
AnnotationResponseV1, ApiHealthResponse, BinaryBuildInformationOwned,
|
||||
ChainBlocksStatusResponse, ChainStatusResponse, KeyRotationInfoResponse,
|
||||
NodePerformanceResponse, NodeRefreshBody, NymNodeDescriptionV1, NymNodeDescriptionV2,
|
||||
PerformanceHistoryResponse, RewardedSetResponse, SignerInformationResponse,
|
||||
StressTestBatchSubmissionResponse,
|
||||
};
|
||||
use nym_api_requests::pagination::PaginatedResponse;
|
||||
use nym_http_api_client::{ApiClient, NO_PARAMS};
|
||||
@@ -390,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],
|
||||
¶ms,
|
||||
)
|
||||
.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> {
|
||||
@@ -1016,7 +980,7 @@ pub trait NymApiClientExt: ApiClient {
|
||||
async fn get_node_annotation(
|
||||
&self,
|
||||
node_id: NodeId,
|
||||
) -> Result<AnnotationResponse, NymAPIError> {
|
||||
) -> Result<AnnotationResponseV1, NymAPIError> {
|
||||
self.get_json(
|
||||
&[
|
||||
routes::V1_API_VERSION,
|
||||
@@ -1399,6 +1363,53 @@ pub trait NymApiClientExt: ApiClient {
|
||||
|
||||
Ok(SemiSkimmedNodesWithMetadata::new(nodes, metadata))
|
||||
}
|
||||
|
||||
/// Queries the nym-api for whether a particular ed25519 identity key is currently recognised
|
||||
/// as an authorised network monitor permitted to submit stress testing results.
|
||||
///
|
||||
/// `identity_key` is expected to be the base58-encoded form of the ed25519 public key.
|
||||
#[instrument(level = "debug", skip(self))]
|
||||
async fn get_known_network_monitor(
|
||||
&self,
|
||||
identity_key: IdentityKeyRef<'_>,
|
||||
) -> Result<KnownNetworkMonitorResponse, NymAPIError> {
|
||||
self.get_json(
|
||||
&[
|
||||
routes::V3_API_VERSION,
|
||||
routes::NYM_NODES_ROUTES,
|
||||
routes::STRESS_TESTING,
|
||||
routes::STRESS_TESTING_KNOWN_MONITORS,
|
||||
identity_key,
|
||||
],
|
||||
NO_PARAMS,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Submit a signed batch of stress-testing results to nym-api on behalf of a network monitor
|
||||
/// orchestrator.
|
||||
///
|
||||
/// The caller is expected to have produced `request` via
|
||||
/// `StressTestBatchSubmissionContent::new(...)` and signed it with the orchestrator's ed25519
|
||||
/// key; nym-api will reject submissions that are stale, replayed, unauthorised, or whose
|
||||
/// signature fails to verify.
|
||||
#[instrument(level = "debug", skip(self, request))]
|
||||
async fn submit_stress_testing_results(
|
||||
&self,
|
||||
request: &StressTestBatchSubmission,
|
||||
) -> Result<StressTestBatchSubmissionResponse, NymAPIError> {
|
||||
self.post_json(
|
||||
&[
|
||||
routes::V3_API_VERSION,
|
||||
routes::NYM_NODES_ROUTES,
|
||||
routes::STRESS_TESTING,
|
||||
routes::STRESS_TESTING_BATCH_SUBMIT,
|
||||
],
|
||||
NO_PARAMS,
|
||||
request,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
// Client is already nym_http_api_client::Client (re-exported above), so just one impl needed
|
||||
|
||||
@@ -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 {
|
||||
@@ -50,6 +49,9 @@ pub mod nym_nodes {
|
||||
pub const NYM_NODES_REWARDED_SET: &str = "rewarded-set";
|
||||
pub const NYM_NODES_REFRESH_DESCRIBED: &str = "refresh-described";
|
||||
pub const BY_ADDRESSES: &str = "by-addresses";
|
||||
pub const STRESS_TESTING: &str = "stress-testing";
|
||||
pub const STRESS_TESTING_KNOWN_MONITORS: &str = "known-monitors";
|
||||
pub const STRESS_TESTING_BATCH_SUBMIT: &str = "batch-submit";
|
||||
}
|
||||
|
||||
pub const STATUS_ROUTES: &str = "status";
|
||||
|
||||
@@ -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(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ pub mod ecash_query_client;
|
||||
pub mod group_query_client;
|
||||
pub mod mixnet_query_client;
|
||||
pub mod multisig_query_client;
|
||||
pub mod node_families_query_client;
|
||||
pub mod network_monitors_query_client;
|
||||
pub mod performance_query_client;
|
||||
pub mod vesting_query_client;
|
||||
|
||||
@@ -23,7 +23,7 @@ pub mod ecash_signing_client;
|
||||
pub mod group_signing_client;
|
||||
pub mod mixnet_signing_client;
|
||||
pub mod multisig_signing_client;
|
||||
pub mod node_families_signing_client;
|
||||
pub mod network_monitors_signing_client;
|
||||
pub mod performance_signing_client;
|
||||
pub mod vesting_signing_client;
|
||||
|
||||
@@ -33,7 +33,9 @@ pub use ecash_query_client::{EcashQueryClient, PagedEcashQueryClient};
|
||||
pub use group_query_client::{GroupQueryClient, PagedGroupQueryClient};
|
||||
pub use mixnet_query_client::{MixnetQueryClient, PagedMixnetQueryClient};
|
||||
pub use multisig_query_client::{MultisigQueryClient, PagedMultisigQueryClient};
|
||||
pub use node_families_query_client::{NodeFamiliesQueryClient, PagedNodeFamiliesQueryClient};
|
||||
pub use network_monitors_query_client::{
|
||||
NetworkMonitorsQueryClient, PagedNetworkMonitorsQueryClient,
|
||||
};
|
||||
pub use performance_query_client::{PagedPerformanceQueryClient, PerformanceQueryClient};
|
||||
pub use vesting_query_client::{PagedVestingQueryClient, VestingQueryClient};
|
||||
|
||||
@@ -43,7 +45,7 @@ pub use ecash_signing_client::EcashSigningClient;
|
||||
pub use group_signing_client::GroupSigningClient;
|
||||
pub use mixnet_signing_client::MixnetSigningClient;
|
||||
pub use multisig_signing_client::MultisigSigningClient;
|
||||
pub use node_families_signing_client::NodeFamiliesSigningClient;
|
||||
pub use network_monitors_signing_client::NetworkMonitorsSigningClient;
|
||||
pub use performance_signing_client::PerformanceSigningClient;
|
||||
pub use vesting_signing_client::VestingSigningClient;
|
||||
|
||||
@@ -53,7 +55,7 @@ pub trait NymContractsProvider {
|
||||
fn mixnet_contract_address(&self) -> Option<&AccountId>;
|
||||
fn vesting_contract_address(&self) -> Option<&AccountId>;
|
||||
fn performance_contract_address(&self) -> Option<&AccountId>;
|
||||
fn node_families_contract_address(&self) -> Option<&AccountId>;
|
||||
fn network_monitors_contract_address(&self) -> Option<&AccountId>;
|
||||
|
||||
// coconut-related
|
||||
fn ecash_contract_address(&self) -> Option<&AccountId>;
|
||||
@@ -67,7 +69,7 @@ pub struct TypedNymContracts {
|
||||
pub mixnet_contract_address: Option<AccountId>,
|
||||
pub vesting_contract_address: Option<AccountId>,
|
||||
pub performance_contract_address: Option<AccountId>,
|
||||
pub node_families_contract_address: Option<AccountId>,
|
||||
pub network_monitors_contract_address: Option<AccountId>,
|
||||
|
||||
pub ecash_contract_address: Option<AccountId>,
|
||||
pub group_contract_address: Option<AccountId>,
|
||||
@@ -92,8 +94,8 @@ impl TryFrom<NymContracts> for TypedNymContracts {
|
||||
.performance_contract_address
|
||||
.map(|addr| addr.parse())
|
||||
.transpose()?,
|
||||
node_families_contract_address: value
|
||||
.node_families_contract_address
|
||||
network_monitors_contract_address: value
|
||||
.network_monitors_contract_address
|
||||
.map(|addr| addr.parse())
|
||||
.transpose()?,
|
||||
ecash_contract_address: value
|
||||
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::collect_paged;
|
||||
use crate::nyxd::contract_traits::NymContractsProvider;
|
||||
use crate::nyxd::error::NyxdError;
|
||||
use crate::nyxd::CosmWasmClient;
|
||||
use async_trait::async_trait;
|
||||
use nym_network_monitors_contract_common::{
|
||||
AuthorisedNetworkMonitor, AuthorisedNetworkMonitorOrchestratorsResponse,
|
||||
AuthorisedNetworkMonitorsPagedResponse, QueryMsg as NetworkMonitorsQueryMsg,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use std::net::SocketAddr;
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
pub trait NetworkMonitorsQueryClient {
|
||||
async fn query_network_monitors_contract<T>(
|
||||
&self,
|
||||
query: NetworkMonitorsQueryMsg,
|
||||
) -> Result<T, NyxdError>
|
||||
where
|
||||
for<'a> T: Deserialize<'a>;
|
||||
|
||||
async fn get_admin(&self) -> Result<cw_controllers::AdminResponse, NyxdError> {
|
||||
self.query_network_monitors_contract(NetworkMonitorsQueryMsg::Admin {})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_network_monitor_orchestrators(
|
||||
&self,
|
||||
) -> Result<AuthorisedNetworkMonitorOrchestratorsResponse, NyxdError> {
|
||||
self.query_network_monitors_contract(
|
||||
NetworkMonitorsQueryMsg::NetworkMonitorOrchestrators {},
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_network_monitor_agents_paged(
|
||||
&self,
|
||||
start_next_after: Option<SocketAddr>,
|
||||
limit: Option<u32>,
|
||||
) -> Result<AuthorisedNetworkMonitorsPagedResponse, NyxdError> {
|
||||
self.query_network_monitors_contract(NetworkMonitorsQueryMsg::NetworkMonitorAgents {
|
||||
start_next_after,
|
||||
limit,
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
pub trait PagedNetworkMonitorsQueryClient: NetworkMonitorsQueryClient {
|
||||
async fn get_all_network_monitor_agents(
|
||||
&self,
|
||||
) -> Result<Vec<AuthorisedNetworkMonitor>, NyxdError> {
|
||||
collect_paged!(self, get_network_monitor_agents_paged, authorised)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<T> PagedNetworkMonitorsQueryClient for T where T: NetworkMonitorsQueryClient {}
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
impl<C> NetworkMonitorsQueryClient for C
|
||||
where
|
||||
C: CosmWasmClient + NymContractsProvider + Send + Sync,
|
||||
{
|
||||
async fn query_network_monitors_contract<T>(
|
||||
&self,
|
||||
query: NetworkMonitorsQueryMsg,
|
||||
) -> Result<T, NyxdError>
|
||||
where
|
||||
for<'a> T: Deserialize<'a>,
|
||||
{
|
||||
let contract_address = &self
|
||||
.network_monitors_contract_address()
|
||||
.ok_or_else(|| NyxdError::unavailable_contract_address("network monitors contract"))?;
|
||||
self.query_contract_smart(contract_address, &query).await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::nyxd::contract_traits::tests::IgnoreValue;
|
||||
|
||||
// it's enough that this compiles and clippy is happy about it
|
||||
#[allow(dead_code)]
|
||||
fn all_query_variants_are_covered<C: NetworkMonitorsQueryClient + Send + Sync>(
|
||||
client: C,
|
||||
msg: NetworkMonitorsQueryMsg,
|
||||
) {
|
||||
match msg {
|
||||
NetworkMonitorsQueryMsg::Admin {} => client.get_admin().ignore(),
|
||||
NetworkMonitorsQueryMsg::NetworkMonitorOrchestrators {} => {
|
||||
client.get_network_monitor_orchestrators().ignore()
|
||||
}
|
||||
NetworkMonitorsQueryMsg::NetworkMonitorAgents { .. } => {
|
||||
client.get_network_monitor_agents_paged(None, None).ignore()
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
+205
@@ -0,0 +1,205 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::nyxd::contract_traits::NymContractsProvider;
|
||||
use crate::nyxd::cosmwasm_client::types::ExecuteResult;
|
||||
use crate::nyxd::error::NyxdError;
|
||||
use crate::nyxd::{Coin, Fee, SigningCosmWasmClient};
|
||||
use crate::signing::signer::OfflineSigner;
|
||||
use async_trait::async_trait;
|
||||
use nym_network_monitors_contract_common::ExecuteMsg as NetworkMonitorsExecuteMsg;
|
||||
use std::net::SocketAddr;
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
pub trait NetworkMonitorsSigningClient {
|
||||
async fn execute_network_monitors_contract(
|
||||
&self,
|
||||
fee: Option<Fee>,
|
||||
msg: NetworkMonitorsExecuteMsg,
|
||||
memo: String,
|
||||
funds: Vec<Coin>,
|
||||
) -> Result<ExecuteResult, NyxdError>;
|
||||
|
||||
async fn update_admin(
|
||||
&self,
|
||||
admin: String,
|
||||
fee: Option<Fee>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
let msg = NetworkMonitorsExecuteMsg::UpdateAdmin { admin };
|
||||
self.execute_network_monitors_contract(
|
||||
fee,
|
||||
msg,
|
||||
"NetworkMonitorsExecuteMsg::UpdateAdmin".into(),
|
||||
vec![],
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn authorise_network_monitor_orchestrator(
|
||||
&self,
|
||||
address: String,
|
||||
fee: Option<Fee>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
let msg = NetworkMonitorsExecuteMsg::AuthoriseNetworkMonitorOrchestrator { address };
|
||||
self.execute_network_monitors_contract(
|
||||
fee,
|
||||
msg,
|
||||
"NetworkMonitorsExecuteMsg::AuthoriseNetworkMonitorOrchestrator".into(),
|
||||
vec![],
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Announce (or rotate) the ed25519 identity key of the calling network monitor orchestrator.
|
||||
///
|
||||
/// The caller must already be an authorised orchestrator; the contract validates that
|
||||
/// `identity_key` is a well-formed base-58 encoding of a 32-byte ed25519 public key.
|
||||
async fn update_orchestrator_identity_key(
|
||||
&self,
|
||||
identity_key: String,
|
||||
fee: Option<Fee>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
let msg = NetworkMonitorsExecuteMsg::UpdateOrchestratorIdentityKey { key: identity_key };
|
||||
self.execute_network_monitors_contract(
|
||||
fee,
|
||||
msg,
|
||||
"NetworkMonitorsExecuteMsg::UpdateOrchestratorIdentityKey".into(),
|
||||
vec![],
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn revoke_network_monitor_orchestrator(
|
||||
&self,
|
||||
address: String,
|
||||
fee: Option<Fee>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
let msg = NetworkMonitorsExecuteMsg::RevokeNetworkMonitorOrchestrator { address };
|
||||
self.execute_network_monitors_contract(
|
||||
fee,
|
||||
msg,
|
||||
"NetworkMonitorsExecuteMsg::RevokeNetworkMonitorOrchestrator".into(),
|
||||
vec![],
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn authorise_network_monitor(
|
||||
&self,
|
||||
mixnet_address: SocketAddr,
|
||||
bs58_x25519_noise: String,
|
||||
noise_version: u8,
|
||||
fee: Option<Fee>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
let msg = NetworkMonitorsExecuteMsg::AuthoriseNetworkMonitor {
|
||||
mixnet_address,
|
||||
bs58_x25519_noise,
|
||||
noise_version,
|
||||
};
|
||||
self.execute_network_monitors_contract(
|
||||
fee,
|
||||
msg,
|
||||
"NetworkMonitorsExecuteMsg::AuthoriseNetworkMonitor".into(),
|
||||
vec![],
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn revoke_network_monitor(
|
||||
&self,
|
||||
address: SocketAddr,
|
||||
fee: Option<Fee>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
let msg = NetworkMonitorsExecuteMsg::RevokeNetworkMonitor { address };
|
||||
self.execute_network_monitors_contract(
|
||||
fee,
|
||||
msg,
|
||||
"NetworkMonitorsExecuteMsg::RevokeNetworkMonitor".into(),
|
||||
vec![],
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn revoke_all_network_monitors(
|
||||
&self,
|
||||
fee: Option<Fee>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
let msg = NetworkMonitorsExecuteMsg::RevokeAllNetworkMonitors;
|
||||
self.execute_network_monitors_contract(
|
||||
fee,
|
||||
msg,
|
||||
"NetworkMonitorsExecuteMsg::RevokeAllNetworkMonitors".into(),
|
||||
vec![],
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
impl<C> NetworkMonitorsSigningClient for C
|
||||
where
|
||||
C: SigningCosmWasmClient + NymContractsProvider + Sync,
|
||||
NyxdError: From<<Self as OfflineSigner>::Error>,
|
||||
{
|
||||
async fn execute_network_monitors_contract(
|
||||
&self,
|
||||
fee: Option<Fee>,
|
||||
msg: NetworkMonitorsExecuteMsg,
|
||||
memo: String,
|
||||
funds: Vec<Coin>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
let contract_address = &self
|
||||
.network_monitors_contract_address()
|
||||
.ok_or_else(|| NyxdError::unavailable_contract_address("network monitors contract"))?;
|
||||
|
||||
let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier())));
|
||||
|
||||
let signer_address = &self.signer_addresses()[0];
|
||||
self.execute(signer_address, contract_address, &msg, fee, memo, funds)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::nyxd::contract_traits::tests::IgnoreValue;
|
||||
use nym_network_monitors_contract_common::ExecuteMsg;
|
||||
|
||||
// it's enough that this compiles and clippy is happy about it
|
||||
#[allow(dead_code)]
|
||||
fn all_execute_variants_are_covered<C: NetworkMonitorsSigningClient + Send + Sync>(
|
||||
client: C,
|
||||
msg: NetworkMonitorsExecuteMsg,
|
||||
) {
|
||||
match msg {
|
||||
NetworkMonitorsExecuteMsg::UpdateAdmin { admin } => {
|
||||
client.update_admin(admin, None).ignore()
|
||||
}
|
||||
ExecuteMsg::AuthoriseNetworkMonitorOrchestrator { address } => client
|
||||
.authorise_network_monitor_orchestrator(address, None)
|
||||
.ignore(),
|
||||
ExecuteMsg::UpdateOrchestratorIdentityKey { key } => {
|
||||
client.update_orchestrator_identity_key(key, None).ignore()
|
||||
}
|
||||
ExecuteMsg::RevokeNetworkMonitorOrchestrator { address } => client
|
||||
.revoke_network_monitor_orchestrator(address, None)
|
||||
.ignore(),
|
||||
ExecuteMsg::AuthoriseNetworkMonitor {
|
||||
mixnet_address: address,
|
||||
bs58_x25519_noise,
|
||||
noise_version,
|
||||
} => client
|
||||
.authorise_network_monitor(address, bs58_x25519_noise, noise_version, None)
|
||||
.ignore(),
|
||||
ExecuteMsg::RevokeNetworkMonitor { address } => {
|
||||
client.revoke_network_monitor(address, None).ignore()
|
||||
}
|
||||
ExecuteMsg::RevokeAllNetworkMonitors => {
|
||||
client.revoke_all_network_monitors(None).ignore()
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
-441
@@ -1,441 +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 serde::Deserialize;
|
||||
|
||||
use nym_mixnet_contract_common::NodeId;
|
||||
pub use nym_node_families_contract_common::{
|
||||
msg::QueryMsg as NodeFamiliesQueryMsg, AllFamilyMembersPagedResponse,
|
||||
AllPastFamilyInvitationsPagedResponse, 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_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::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(),
|
||||
};
|
||||
}
|
||||
}
|
||||
-254
@@ -1,254 +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
|
||||
}
|
||||
|
||||
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::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()
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,7 @@ pub mod logs;
|
||||
pub mod module_traits;
|
||||
pub mod types;
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct SigningClientOptions {
|
||||
gas_price: GasPrice,
|
||||
simulated_gas_multiplier: f32,
|
||||
@@ -80,6 +80,17 @@ impl<C, S> MaybeSigningClient<C, S> {
|
||||
opts,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn clone_query_client(&self) -> MaybeSigningClient<C, NoSigner>
|
||||
where
|
||||
C: Clone,
|
||||
{
|
||||
MaybeSigningClient {
|
||||
client: self.client.clone(),
|
||||
signer: Default::default(),
|
||||
opts: self.opts.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "http-client")]
|
||||
|
||||
@@ -24,6 +24,8 @@ use async_trait::async_trait;
|
||||
use cosmrs::tendermint::{abci, evidence::Evidence, Genesis};
|
||||
use cosmrs::tx::{Raw, SignDoc};
|
||||
use cosmwasm_std::Addr;
|
||||
use nym_contracts_common::build_information::CONTRACT_BUILD_INFO_STORAGE_KEY;
|
||||
use nym_contracts_common::ContractBuildInformation;
|
||||
use nym_network_defaults::{ChainDetails, NymNetworkDetails};
|
||||
use serde::{de::DeserializeOwned, Serialize};
|
||||
use std::fmt::Debug;
|
||||
@@ -40,6 +42,7 @@ pub use crate::nyxd::{
|
||||
fee::Fee,
|
||||
};
|
||||
pub use crate::rpc::TendermintRpcClient;
|
||||
pub use bip39;
|
||||
pub use coin::Coin;
|
||||
pub use cosmrs::{
|
||||
bank::MsgSend,
|
||||
@@ -70,14 +73,19 @@ pub use tendermint_rpc::{
|
||||
Paging, Request, Response, SimpleRequest,
|
||||
};
|
||||
|
||||
pub use nym_ecash_contract_common;
|
||||
pub use nym_mixnet_contract_common;
|
||||
pub use nym_multisig_contract_common;
|
||||
pub use nym_network_monitors_contract_common;
|
||||
pub use nym_performance_contract_common;
|
||||
pub use nym_vesting_contract_common;
|
||||
|
||||
#[cfg(feature = "http-client")]
|
||||
use crate::http_client;
|
||||
#[cfg(feature = "http-client")]
|
||||
use crate::{DirectSigningHttpRpcNyxdClient, QueryHttpRpcNyxdClient};
|
||||
#[cfg(feature = "http-client")]
|
||||
use cosmrs::rpc::{HttpClient, HttpClientUrl};
|
||||
use nym_contracts_common::build_information::CONTRACT_BUILD_INFO_STORAGE_KEY;
|
||||
use nym_contracts_common::ContractBuildInformation;
|
||||
|
||||
pub mod coin;
|
||||
pub mod contract_traits;
|
||||
@@ -262,6 +270,16 @@ impl<C, S> NyxdClient<C, S> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clone_query_client(&self) -> NyxdClient<C>
|
||||
where
|
||||
C: Clone,
|
||||
{
|
||||
NyxdClient {
|
||||
client: self.client.clone_query_client(),
|
||||
config: self.config.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn current_config(&self) -> &Config {
|
||||
&self.config
|
||||
}
|
||||
@@ -286,13 +304,13 @@ 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;
|
||||
}
|
||||
|
||||
pub fn get_nym_contracts(&self) -> TypedNymContracts {
|
||||
self.config.contracts.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl<C, S> NymContractsProvider for NyxdClient<C, S> {
|
||||
@@ -307,11 +325,10 @@ impl<C, S> NymContractsProvider for NyxdClient<C, S> {
|
||||
fn performance_contract_address(&self) -> Option<&AccountId> {
|
||||
self.config.contracts.performance_contract_address.as_ref()
|
||||
}
|
||||
|
||||
fn node_families_contract_address(&self) -> Option<&AccountId> {
|
||||
fn network_monitors_contract_address(&self) -> Option<&AccountId> {
|
||||
self.config
|
||||
.contracts
|
||||
.node_families_contract_address
|
||||
.network_monitors_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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
+5
-11
@@ -1,16 +1,14 @@
|
||||
[package]
|
||||
name = "nym-node-families-contract-common"
|
||||
description = "Common crate for Nym's node families contract"
|
||||
version.workspace = true
|
||||
name = "nym-network-monitors-contract-common"
|
||||
description = "Common library for the Nym Network Monitors contract"
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
documentation.workspace = true
|
||||
rust-version = "1.85"
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
readme.workspace = true
|
||||
publish = true
|
||||
version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
thiserror = { workspace = true }
|
||||
@@ -20,10 +18,6 @@ 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 = []
|
||||
@@ -0,0 +1,8 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
pub mod storage_keys {
|
||||
pub const CONTRACT_ADMIN: &str = "contract-admin";
|
||||
pub const AUTHORISED_ORCHESTRATORS: &str = "authorised-orchestrators";
|
||||
pub const AUTHORISED_NETWORK_MONITORS: &str = "authorised-network-monitors";
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use cosmwasm_std::Addr;
|
||||
use cw_controllers::AdminError;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug, PartialEq)]
|
||||
pub enum NetworkMonitorsContractError {
|
||||
#[error("could not perform contract migration: {comment}")]
|
||||
FailedMigration { comment: String },
|
||||
|
||||
#[error(transparent)]
|
||||
Admin(#[from] AdminError),
|
||||
|
||||
#[error("unauthorised")]
|
||||
Unauthorized,
|
||||
|
||||
#[error("address {addr} is not an authorised orchestrator")]
|
||||
NotAnOrchestrator { addr: Addr },
|
||||
|
||||
#[error("Failed to recover x25519 public key from its base58 representation: {0}")]
|
||||
MalformedX25519AgentNoiseKey(String),
|
||||
|
||||
#[error("Failed to recover ed25519 public key from its base58 representation: {0}")]
|
||||
MalformedEd25519OrchestratorIdentityKey(String),
|
||||
|
||||
#[error(transparent)]
|
||||
StdErr(#[from] cosmwasm_std::StdError),
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
pub mod constants;
|
||||
pub mod error;
|
||||
pub mod msg;
|
||||
pub mod types;
|
||||
|
||||
pub use error::*;
|
||||
pub use msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg};
|
||||
pub use types::*;
|
||||
@@ -0,0 +1,78 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use cosmwasm_schema::cw_serde;
|
||||
use std::net::SocketAddr;
|
||||
|
||||
#[cfg(feature = "schema")]
|
||||
use crate::{
|
||||
AuthorisedNetworkMonitorOrchestratorsResponse, AuthorisedNetworkMonitorsPagedResponse,
|
||||
};
|
||||
|
||||
#[cw_serde]
|
||||
pub struct InstantiateMsg {
|
||||
/// Address of the initial network monitor orchestrator.
|
||||
pub orchestrator_address: String,
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
pub enum ExecuteMsg {
|
||||
/// Change the admin
|
||||
UpdateAdmin { admin: String },
|
||||
|
||||
/// Authorise new network monitor orchestrator
|
||||
AuthoriseNetworkMonitorOrchestrator { address: String },
|
||||
|
||||
/// Attempt to update the announced identity key of this orchestrator
|
||||
UpdateOrchestratorIdentityKey { key: String },
|
||||
|
||||
/// Revoke network monitor orchestrator authorisation.
|
||||
RevokeNetworkMonitorOrchestrator { address: String },
|
||||
|
||||
/// Authorise new network monitor (or renew authorisation)
|
||||
/// granting additional privileges when sending mixnet packets to Nym nodes.
|
||||
AuthoriseNetworkMonitor {
|
||||
/// Mixnet address of the agent.
|
||||
/// The underlying ip address is going to be used as ingress to the nodes,
|
||||
/// and the full socket address announces the egress and the association with the noise key
|
||||
mixnet_address: SocketAddr,
|
||||
|
||||
/// Base-58 encoded noise key of the agent.
|
||||
bs58_x25519_noise: String,
|
||||
|
||||
/// Version of the noise protocol used by the agent.
|
||||
noise_version: u8,
|
||||
},
|
||||
|
||||
/// Revoke network monitor authorisation.
|
||||
RevokeNetworkMonitor { address: SocketAddr },
|
||||
|
||||
/// Revoke all network monitor authorisations.
|
||||
RevokeAllNetworkMonitors,
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
#[cfg_attr(feature = "schema", derive(cosmwasm_schema::QueryResponses))]
|
||||
pub enum QueryMsg {
|
||||
#[cfg_attr(feature = "schema", returns(cw_controllers::AdminResponse))]
|
||||
Admin {},
|
||||
|
||||
// no need for pagination as we don't expect even a double digit of those
|
||||
#[cfg_attr(
|
||||
feature = "schema",
|
||||
returns(AuthorisedNetworkMonitorOrchestratorsResponse)
|
||||
)]
|
||||
NetworkMonitorOrchestrators {},
|
||||
|
||||
#[cfg_attr(feature = "schema", returns(AuthorisedNetworkMonitorsPagedResponse))]
|
||||
NetworkMonitorAgents {
|
||||
/// Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response.
|
||||
start_next_after: Option<SocketAddr>,
|
||||
|
||||
/// Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default.
|
||||
limit: Option<u32>,
|
||||
},
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
pub struct MigrateMsg {}
|
||||
@@ -0,0 +1,53 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use cosmwasm_schema::cw_serde;
|
||||
use cosmwasm_std::{Addr, Timestamp};
|
||||
use std::net::SocketAddr;
|
||||
|
||||
pub type OrchestratorAddress = Addr;
|
||||
|
||||
#[cw_serde]
|
||||
pub struct AuthorisedNetworkMonitorOrchestrator {
|
||||
/// The address associated with the network monitor orchestrator.
|
||||
pub address: Addr,
|
||||
|
||||
/// Base-58 encoded identity key of the orchestrator, announced by the orchestrator itself
|
||||
/// on startup.
|
||||
pub identity_key: Option<String>,
|
||||
|
||||
/// Timestamp of when the network monitor was authorised.
|
||||
pub authorised_at: Timestamp,
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
pub struct AuthorisedNetworkMonitor {
|
||||
/// Mixnet address of the agent.
|
||||
/// The underlying ip address is going to be used as ingress to the nodes,
|
||||
/// and the full socket address announces the egress and the association with the noise key
|
||||
pub mixnet_address: SocketAddr,
|
||||
|
||||
/// The address of the orchestrator that authorised the network monitor agent.
|
||||
pub authorised_by: OrchestratorAddress,
|
||||
|
||||
/// Timestamp of when the network monitor was authorised.
|
||||
pub authorised_at: Timestamp,
|
||||
|
||||
/// Base-58 encoded noise key of the agent.
|
||||
pub bs58_x25519_noise: String,
|
||||
|
||||
/// Version of the noise protocol used by the agent.
|
||||
pub noise_version: u8,
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
pub struct AuthorisedNetworkMonitorOrchestratorsResponse {
|
||||
pub authorised: Vec<AuthorisedNetworkMonitorOrchestrator>,
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
pub struct AuthorisedNetworkMonitorsPagedResponse {
|
||||
pub authorised: Vec<AuthorisedNetworkMonitor>,
|
||||
|
||||
pub start_next_after: Option<SocketAddr>,
|
||||
}
|
||||
@@ -1,104 +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";
|
||||
}
|
||||
@@ -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,211 +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 },
|
||||
|
||||
/// 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 {
|
||||
/// 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,403 +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, revoked, or until the
|
||||
/// chain advances past `expires_at` (in which case they remain in storage but are
|
||||
/// treated as inert — there is no background process clearing expired invitations).
|
||||
#[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: timed-out invitations are not represented here — they are simply left in
|
||||
/// the pending set (see `FamilyInvitation::expires_at`).
|
||||
#[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 },
|
||||
}
|
||||
|
||||
/// Historical record of an invitation that has reached a terminal state
|
||||
/// (`Accepted`, `Rejected`, or `Revoked`). Timed-out invitations are **not**
|
||||
/// archived here — they remain in the pending map until explicitly cleared.
|
||||
#[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>,
|
||||
}
|
||||
@@ -27,6 +27,9 @@ pub struct QuorumStateChecker {
|
||||
cancellation_token: CancellationToken,
|
||||
check_interval: Duration,
|
||||
quorum_state: QuorumState,
|
||||
|
||||
/// indicates whether the last check has been a failure
|
||||
last_failed: bool,
|
||||
}
|
||||
|
||||
impl QuorumStateChecker {
|
||||
@@ -42,6 +45,7 @@ impl QuorumStateChecker {
|
||||
quorum_state: QuorumState {
|
||||
available: Arc::new(Default::default()),
|
||||
},
|
||||
last_failed: false,
|
||||
};
|
||||
|
||||
// first check MUST succeed, otherwise we shouldn't start
|
||||
@@ -57,6 +61,7 @@ impl QuorumStateChecker {
|
||||
}
|
||||
|
||||
async fn check_quorum_state(&self) -> Result<bool, CredentialProxyError> {
|
||||
info!("checking the current quorum state");
|
||||
let client_guard = self.client.query_chain().await;
|
||||
|
||||
// split the operation as we only need to hold the reference to chain client for the first part
|
||||
@@ -64,7 +69,8 @@ impl QuorumStateChecker {
|
||||
let dkg_details = dkg_details_with_client(client_guard.deref()).await?;
|
||||
drop(client_guard);
|
||||
|
||||
let res = check_known_dealers(dkg_details).await?;
|
||||
let res = check_known_dealers(dkg_details, 4).await?;
|
||||
info!("there are {} known DKG dealers", res.results.len());
|
||||
|
||||
let Some(signing_threshold) = res.threshold else {
|
||||
warn!(
|
||||
@@ -76,15 +82,36 @@ impl QuorumStateChecker {
|
||||
let mut working_issuer = 0;
|
||||
|
||||
for result in res.results {
|
||||
let dealer = &result.information;
|
||||
let info = format!("[id: {}] @ {}", dealer.node_index, dealer.announce_address);
|
||||
if result.chain_available() && result.signing_available() {
|
||||
info!("✅ {info} is fully available");
|
||||
working_issuer += 1;
|
||||
} else if !result.chain_available() && !result.signing_available() {
|
||||
warn!("❌ {info} is not available for both chain and signing");
|
||||
} else if !result.chain_available() {
|
||||
warn!("❌ {info} is not available for chain");
|
||||
} else {
|
||||
warn!("❌ {info} is not available for signing");
|
||||
}
|
||||
}
|
||||
|
||||
Ok((working_issuer as u64) >= signing_threshold)
|
||||
let available = (working_issuer as u64) >= signing_threshold;
|
||||
|
||||
if available {
|
||||
info!(
|
||||
"✅ Quorum state is available with {working_issuer} out of {signing_threshold} issuers"
|
||||
)
|
||||
} else {
|
||||
error!(
|
||||
"❌ Quorum state is not available with {working_issuer} out of {signing_threshold} issuers"
|
||||
)
|
||||
}
|
||||
|
||||
Ok(available)
|
||||
}
|
||||
|
||||
pub async fn run_forever(self) {
|
||||
pub async fn run_forever(mut self) {
|
||||
info!("starting quorum state checker");
|
||||
loop {
|
||||
tokio::select! {
|
||||
@@ -94,7 +121,23 @@ impl QuorumStateChecker {
|
||||
}
|
||||
_ = tokio::time::sleep(self.check_interval) => {
|
||||
match self.check_quorum_state().await {
|
||||
Ok(available) => self.quorum_state.available.store(available, Ordering::SeqCst),
|
||||
Ok(available) => {
|
||||
let previous = self.quorum_state.available.load(Ordering::SeqCst);
|
||||
// only update the quorum state to a failed state if we've had two consecutive failures
|
||||
if available {
|
||||
if !previous {
|
||||
info!("quorum recovered");
|
||||
}
|
||||
self.quorum_state.available.store(true, Ordering::SeqCst);
|
||||
} else if self.last_failed {
|
||||
if previous {
|
||||
warn!("quorum became unavailable after 2 consecutive failed checks");
|
||||
}
|
||||
self.quorum_state.available.store(false, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
self.last_failed = !available;
|
||||
},
|
||||
Err(err) => error!("failed to check current quorum state: {err}"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,5 +36,13 @@ nym-ecash-contract-common = { workspace = true }
|
||||
nym-network-defaults = { workspace = true }
|
||||
nym-serde-helpers = { workspace = true, features = ["date"] }
|
||||
|
||||
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.tokio]
|
||||
workspace = true
|
||||
features = ["time"]
|
||||
|
||||
[target."cfg(target_arch = \"wasm32\")".dependencies.wasmtimer]
|
||||
workspace = true
|
||||
features = ["tokio"]
|
||||
|
||||
[dev-dependencies]
|
||||
rand = { workspace = true }
|
||||
|
||||
@@ -6,6 +6,7 @@ use crate::ecash::bandwidth::serialiser::VersionedSerialise;
|
||||
use crate::ecash::bandwidth::CredentialSigningData;
|
||||
use crate::ecash::utils::cred_exp_date;
|
||||
use crate::error::Error;
|
||||
use log::{debug, warn};
|
||||
use nym_api_requests::ecash::BlindSignRequestBody;
|
||||
use nym_credentials_interface::{
|
||||
aggregate_wallets, generate_keypair_user_from_seed, issue_verify, withdrawal_request,
|
||||
@@ -17,8 +18,15 @@ use nym_ecash_contract_common::deposit::DepositId;
|
||||
use nym_ecash_time::{ecash_default_expiration_date, ecash_today, EcashTime};
|
||||
use nym_validator_client::nym_api::{EpochId, NymApiClientExt};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::time::Duration;
|
||||
use time::Date;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use tokio::time::sleep;
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use wasmtimer::tokio::sleep;
|
||||
|
||||
pub use nym_validator_client::nyxd::{Coin, Hash};
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
@@ -192,6 +200,49 @@ impl IssuanceTicketBook {
|
||||
Ok(unblinded_signature)
|
||||
}
|
||||
|
||||
// ideally this would have been generic over credential type, but we really don't need secp256k1 keys for bandwidth vouchers
|
||||
pub async fn obtain_partial_ticketbook_credential_with_retries(
|
||||
&self,
|
||||
client: &nym_http_api_client::Client,
|
||||
signer_index: u64,
|
||||
validator_vk: &VerificationKeyAuth,
|
||||
signing_data: CredentialSigningData,
|
||||
max_attempts: usize,
|
||||
) -> Result<PartialWallet, Error> {
|
||||
let Some(client_url) = client.base_urls().first() else {
|
||||
return Err(Error::CredentialShareObtainFailed);
|
||||
};
|
||||
let mut last_err = None;
|
||||
for attempt in 0..max_attempts {
|
||||
if attempt > 0 {
|
||||
sleep(Duration::from_millis(500 * attempt as u64)).await;
|
||||
}
|
||||
debug!(
|
||||
"attempt {} / {max_attempts} to obtain partial ticketbook credential from {client_url}",
|
||||
attempt + 1,
|
||||
);
|
||||
match self
|
||||
.obtain_partial_ticketbook_credential(
|
||||
client,
|
||||
signer_index,
|
||||
validator_vk,
|
||||
signing_data.clone(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(partial_wallet) => return Ok(partial_wallet),
|
||||
Err(err) => {
|
||||
warn!(
|
||||
"attempt {} / {max_attempts} to obtain partial ticketbook credential from {client_url} failed: {err}",
|
||||
attempt + 1,
|
||||
);
|
||||
last_err = Some(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(last_err.unwrap_or(Error::CredentialShareObtainFailed))
|
||||
}
|
||||
|
||||
// ideally this would have been generic over credential type, but we really don't need secp256k1 keys for bandwidth vouchers
|
||||
pub async fn obtain_partial_ticketbook_credential(
|
||||
&self,
|
||||
|
||||
@@ -137,6 +137,8 @@ pub async fn obtain_aggregate_wallet(
|
||||
ecash_api_clients: &[EcashApiClient],
|
||||
threshold: u64,
|
||||
) -> Result<WalletSignatures, Error> {
|
||||
const MAX_ATTEMPTS: usize = 2;
|
||||
|
||||
if ecash_api_clients.len() < threshold as usize {
|
||||
return Err(Error::NoValidatorsAvailable);
|
||||
}
|
||||
@@ -154,11 +156,12 @@ pub async fn obtain_aggregate_wallet(
|
||||
);
|
||||
|
||||
match voucher
|
||||
.obtain_partial_ticketbook_credential(
|
||||
.obtain_partial_ticketbook_credential_with_retries(
|
||||
&ecash_api_client.api_client,
|
||||
ecash_api_client.node_id,
|
||||
&ecash_api_client.verification_key,
|
||||
request.clone(),
|
||||
MAX_ATTEMPTS,
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -167,6 +170,11 @@ pub async fn obtain_aggregate_wallet(
|
||||
warn!("failed to obtain partial credential from API {ecash_api_client}: {err}",);
|
||||
}
|
||||
};
|
||||
|
||||
// we got sufficient number of shares
|
||||
if wallets.len() >= threshold as usize {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if wallets.len() < threshold as usize {
|
||||
return Err(Error::NotEnoughShares);
|
||||
|
||||
@@ -63,6 +63,9 @@ pub enum Error {
|
||||
|
||||
#[error("failed to create a secp256k1 signature")]
|
||||
Secp256k1SignFailure,
|
||||
|
||||
#[error("failed to obtain a valid credential share")]
|
||||
CredentialShareObtainFailed,
|
||||
}
|
||||
|
||||
impl From<NymAPIError> for Error {
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -1,21 +1,7 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use super::{PrivateKey, PublicKey};
|
||||
|
||||
pub mod bs58_x25519_private_key {
|
||||
use super::*;
|
||||
use serde::{Deserialize, Deserializer, Serializer};
|
||||
|
||||
pub fn serialize<S: Serializer>(key: &PrivateKey, serializer: S) -> Result<S::Ok, S::Error> {
|
||||
serializer.serialize_str(&key.to_base58_string())
|
||||
}
|
||||
|
||||
pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<PrivateKey, D::Error> {
|
||||
let s = String::deserialize(deserializer)?;
|
||||
PrivateKey::from_base58_string(s).map_err(serde::de::Error::custom)
|
||||
}
|
||||
}
|
||||
use super::PublicKey;
|
||||
|
||||
pub mod bs58_x25519_pubkey {
|
||||
use super::*;
|
||||
|
||||
@@ -31,3 +31,5 @@ pub use aes_gcm_siv::{Aes128GcmSiv, Aes256GcmSiv};
|
||||
pub use blake3;
|
||||
#[cfg(feature = "stream_cipher")]
|
||||
pub use ctr;
|
||||
#[cfg(feature = "hashing")]
|
||||
pub use sha2;
|
||||
|
||||
@@ -36,6 +36,7 @@ pub trait ChainResponse: Verifiable + TimestampedResponse {
|
||||
|
||||
// we rely on information provided from the api itself AS LONG AS it's not too outdated
|
||||
if self.timestamp() + stale_response_threshold < now {
|
||||
warn!("chain status response is stale");
|
||||
return false;
|
||||
}
|
||||
self.chain_synced()
|
||||
@@ -96,26 +97,27 @@ pub trait SignerResponse: Verifiable + TimestampedResponse {
|
||||
|
||||
// we rely on information provided from the api itself AS LONG AS it's not too outdated
|
||||
if self.timestamp() + stale_response_threshold < now {
|
||||
warn!("stale signer response");
|
||||
return false;
|
||||
}
|
||||
|
||||
if !self.has_signing_keys() {
|
||||
debug!("missing signing keys");
|
||||
warn!("missing signing keys");
|
||||
return false;
|
||||
}
|
||||
|
||||
if self.signer_disabled() {
|
||||
debug!("signer functionalities explicitly disabled");
|
||||
warn!("signer functionalities are explicitly disabled");
|
||||
return false;
|
||||
}
|
||||
|
||||
if !self.is_ecash_signer() {
|
||||
debug!("signer doesn't recognise it's a signer for this epoch");
|
||||
warn!("signer doesn't recognise it's a signer for this epoch");
|
||||
return false;
|
||||
}
|
||||
|
||||
if dkg_epoch_id != self.dkg_ecash_epoch_id() {
|
||||
debug!(
|
||||
warn!(
|
||||
"mismatched dkg epoch id. current: {dkg_epoch_id}, signer's: {}",
|
||||
self.dkg_ecash_epoch_id()
|
||||
);
|
||||
|
||||
@@ -11,10 +11,11 @@ use nym_crypto::asymmetric::ed25519;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::time::Duration;
|
||||
use time::OffsetDateTime;
|
||||
use tracing::warn;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
pub(crate) const CHAIN_STALL_THRESHOLD: Duration = Duration::from_secs(5 * 60);
|
||||
pub(crate) const STALE_RESPONSE_THRESHOLD: Duration = Duration::from_secs(5 * 60);
|
||||
pub(crate) const CHAIN_STALL_THRESHOLD: Duration = Duration::from_secs(10 * 60);
|
||||
pub(crate) const STALE_RESPONSE_THRESHOLD: Duration = Duration::from_secs(10 * 60);
|
||||
|
||||
// the reason for generics is not to remove duplication of code,
|
||||
// but because without them, we'd be having problems with circular dependencies,
|
||||
@@ -188,6 +189,7 @@ where
|
||||
};
|
||||
|
||||
let SignerStatus::Tested { result } = &self.status else {
|
||||
warn!("no valid chain response");
|
||||
return false;
|
||||
};
|
||||
result
|
||||
@@ -239,6 +241,7 @@ where
|
||||
};
|
||||
|
||||
let SignerStatus::Tested { result } = &self.status else {
|
||||
warn!("no valid signer response");
|
||||
return false;
|
||||
};
|
||||
result.signing_status.signing_available(
|
||||
|
||||
@@ -195,9 +195,9 @@ impl ClientUnderTest {
|
||||
pub(crate) async fn check_client(
|
||||
dealer_details: DealerDetails,
|
||||
dkg_epoch: u64,
|
||||
contract_share: Option<&ContractVKShare>,
|
||||
contract_share: Option<ContractVKShare>,
|
||||
) -> TypedSignerResult {
|
||||
let dealer_information = RawDealerInformation::new(&dealer_details, contract_share);
|
||||
let dealer_information = RawDealerInformation::new(&dealer_details, contract_share.as_ref());
|
||||
|
||||
// 7. attempt to construct client instances out of them
|
||||
let Ok(parsed_information) = dealer_information.parse() else {
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::client_check::check_client;
|
||||
use futures::stream::{FuturesUnordered, StreamExt};
|
||||
use futures::stream;
|
||||
use futures::stream::StreamExt;
|
||||
use nym_ecash_signer_check_types::status::{SignerResult, Status};
|
||||
use nym_network_defaults::NymNetworkDetails;
|
||||
use nym_validator_client::QueryHttpRpcNyxdClient;
|
||||
@@ -65,7 +66,7 @@ where
|
||||
C: DkgQueryClient + Sync,
|
||||
{
|
||||
let dkg_details = dkg_details_with_client(client).await?;
|
||||
check_known_dealers(dkg_details).await
|
||||
check_known_dealers(dkg_details, None).await
|
||||
}
|
||||
|
||||
pub async fn dkg_details_with_client<C>(client: &C) -> Result<DkgDetails, SignerCheckError>
|
||||
@@ -109,18 +110,21 @@ where
|
||||
|
||||
pub async fn check_known_dealers(
|
||||
dkg_details: DkgDetails,
|
||||
concurrency: impl Into<Option<usize>>,
|
||||
) -> Result<SignersTestResult, SignerCheckError> {
|
||||
// 6. for each dealer attempt to perform the checks
|
||||
let results = dkg_details
|
||||
.network_dealers
|
||||
.into_iter()
|
||||
.map(|d| {
|
||||
let share = dkg_details.submitted_shared.get(&d.assigned_index);
|
||||
check_client(d, dkg_details.dkg_epoch.epoch_id, share)
|
||||
})
|
||||
.collect::<FuturesUnordered<_>>()
|
||||
.collect::<Vec<_>>()
|
||||
.await;
|
||||
let epoch_id = dkg_details.dkg_epoch.epoch_id;
|
||||
let submitted = dkg_details.submitted_shared;
|
||||
let dealers = dkg_details.network_dealers.len();
|
||||
|
||||
let tasks = dkg_details.network_dealers.into_iter().map(move |d| {
|
||||
let share = submitted.get(&d.assigned_index).cloned();
|
||||
check_client(d, epoch_id, share)
|
||||
});
|
||||
|
||||
let limit = concurrency.into().filter(|&n| n > 0).unwrap_or(dealers);
|
||||
|
||||
let results = stream::iter(tasks).buffer_unordered(limit).collect().await;
|
||||
|
||||
Ok(SignersTestResult {
|
||||
threshold: dkg_details.threshold,
|
||||
|
||||
@@ -1173,16 +1173,7 @@ impl ApiClientCore for Client {
|
||||
};
|
||||
|
||||
match response {
|
||||
Ok(resp) => {
|
||||
// Check if the response includes a rate limit error from the vercel API
|
||||
if is_http_rate_limit_err(&resp) {
|
||||
warn!("encountered vercel rate limit error for {}", url.as_str());
|
||||
// if we have multiple urls, update to the next
|
||||
self.maybe_rotate_hosts(Some(url.clone()));
|
||||
}
|
||||
|
||||
return Ok(resp);
|
||||
}
|
||||
Ok(resp) => return Ok(resp),
|
||||
Err(err) => {
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let is_network_err = err.is_timeout();
|
||||
@@ -1235,39 +1226,17 @@ impl ApiClientCore for Client {
|
||||
}
|
||||
}
|
||||
|
||||
const VERCEL_CHALLENGE_HEADER: &str = "x-vercel-mitigated";
|
||||
const VERCEL_CHALLENGE_VALUE: &[u8] = b"challenge";
|
||||
|
||||
/// Check for Rate Limit challenge response from the vercel API
|
||||
pub(crate) fn is_http_rate_limit_err(resp: &Response) -> bool {
|
||||
let status = resp.status() == StatusCode::FORBIDDEN;
|
||||
let header = resp
|
||||
.headers()
|
||||
.get(VERCEL_CHALLENGE_HEADER)
|
||||
.is_some_and(|v| v.as_bytes() == VERCEL_CHALLENGE_VALUE);
|
||||
let content_type = resp
|
||||
.headers()
|
||||
.get(CONTENT_TYPE)
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.and_then(|value| value.parse::<Mime>().ok())
|
||||
.is_some_and(|mime_type| {
|
||||
mime_type.type_() == mime::TEXT && mime_type.subtype() == mime::HTML
|
||||
});
|
||||
|
||||
status && header && content_type
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
const MAX_ERR_SOURCE_ITERATIONS: usize = 4;
|
||||
|
||||
/// This functions attempts to check the error returned by reqwest to see if rotating host
|
||||
/// information (for clients with multiple hosts defined) could be helpful. This looks for
|
||||
/// situations where the error could plausibly be caused by a network adversary, or where rotating
|
||||
/// to an equivalent hostname might help.
|
||||
/// This functions attempts to check the error returned by reqwest to see if
|
||||
/// rotating host informtion (for clients with mutliple hosts defined) could be
|
||||
/// helpful. This looks for situations where the error could plausibly be caused
|
||||
/// by a network adversary, or where rotating to an equival hostname might help.
|
||||
///
|
||||
/// For example --> NetworkUnreachable will not be helped by rotating domains, but ConnectionReset
|
||||
/// might be caused by a network adversary blocking by SNI which could possibly benefit from
|
||||
/// rotating domains.
|
||||
/// For example --> NetworkUnreachable will not be helped by rotating domains,
|
||||
/// but ConnectionReset might be caused by a network adversary blocking by SNI
|
||||
/// which could possibly benefit from rotating domains.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(crate) fn might_be_network_interference(err: &reqwest::Error) -> bool {
|
||||
if err.is_timeout() {
|
||||
@@ -1728,13 +1697,6 @@ where
|
||||
decode_raw_response(&headers, full)
|
||||
} else if res.status() == StatusCode::NOT_FOUND {
|
||||
Err(HttpClientError::NotFound { url: Box::new(url) })
|
||||
} else if is_http_rate_limit_err(&res) {
|
||||
Err(HttpClientError::EndpointFailure {
|
||||
url: Box::new(url),
|
||||
status,
|
||||
headers: Box::new(headers),
|
||||
error: String::from("received vercel rate limit challenge response"),
|
||||
})
|
||||
} else {
|
||||
let Ok(plaintext) = res.text().await else {
|
||||
return Err(HttpClientError::RequestFailure {
|
||||
|
||||
@@ -10,7 +10,9 @@ fn sanitize_fragment(segment: &str) -> &str {
|
||||
segment.trim_matches(|c: char| c.is_whitespace() || c == '/')
|
||||
}
|
||||
|
||||
/// Defines a path that can be used to make a request to an API.
|
||||
pub trait RequestPath: Debug {
|
||||
/// Sanitise the request path by removing empty segments and trimming whitespace and slashes
|
||||
fn to_sanitized_segments(&self) -> Vec<&str>;
|
||||
}
|
||||
|
||||
|
||||
@@ -129,41 +129,6 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Serialize, Deserialize, Copy, Clone)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum OutputV2 {
|
||||
#[default]
|
||||
Json,
|
||||
Yaml,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Serialize, Deserialize, Copy, Clone)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::IntoParams, utoipa::ToSchema))]
|
||||
#[serde(default)]
|
||||
pub struct OutputParamsV2 {
|
||||
pub output: Option<OutputV2>,
|
||||
}
|
||||
|
||||
impl OutputParamsV2 {
|
||||
pub fn get_output(&self) -> OutputV2 {
|
||||
self.output.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn to_response<T: Serialize>(self, data: T) -> FormattedResponse<T> {
|
||||
self.get_output().to_response(data)
|
||||
}
|
||||
}
|
||||
|
||||
impl OutputV2 {
|
||||
pub fn to_response<T: Serialize>(self, data: T) -> FormattedResponse<T> {
|
||||
match self {
|
||||
OutputV2::Json => FormattedResponse::Json(Json::from(data)),
|
||||
OutputV2::Yaml => FormattedResponse::Yaml(Yaml::from(data)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Serialize, Deserialize, Copy, Clone)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user