Compare commits
127 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8573004c34 | |||
| 5636c5afc4 | |||
| f505c29926 | |||
| 95bec7422c | |||
| c02c28f7cb | |||
| 6fb4a98667 | |||
| 4a50f6dcd0 | |||
| 53dec68378 | |||
| f0ecdfd295 | |||
| 668477c5c3 | |||
| 53aaa71178 | |||
| 35517f1df6 | |||
| ed5ddf0170 | |||
| 644e669a15 | |||
| 1fd25529ce | |||
| 8677b98bcb | |||
| ca031af69a | |||
| 7c0264b839 | |||
| bde2b07d0d | |||
| 26538f5a40 | |||
| 483bb6f477 | |||
| a68355a75a | |||
| 1572d8e5c2 | |||
| fd76c5ca4d | |||
| f94589c2d1 | |||
| 1c40499829 | |||
| f8a4d5f1ff | |||
| 42807890af | |||
| 5aa576b596 | |||
| 0215ad9294 | |||
| 227e6a10e1 | |||
| d3b6a270de | |||
| e12ada0105 | |||
| 71d50d79c2 | |||
| a21a01cf1a | |||
| 362f84b5f6 | |||
| daed9cd15b | |||
| a53ca71bd2 | |||
| a70e68c7bd | |||
| fdebed7c38 | |||
| f576a4ee2d | |||
| a9aafd785e | |||
| 0f7dbb94a8 | |||
| 2d72b1b201 | |||
| 412657f773 | |||
| b501ddd534 | |||
| e9f6d1d47a | |||
| 52b4490e80 | |||
| 7b30c83f9a | |||
| 4aabb4ed56 | |||
| b14c28a462 | |||
| 664782c0c6 | |||
| aeb2f1f0f6 | |||
| 268ba36700 | |||
| c4df05157a | |||
| 09548a9aa9 | |||
| 78b796bf24 | |||
| f5ab7b3eb6 | |||
| 9cf679dadb | |||
| 97a382520c | |||
| f87ce06865 | |||
| 6095215a73 | |||
| 8c6ff79cd1 | |||
| 16678537f7 | |||
| ae877e3867 | |||
| 21479bfb80 | |||
| f84de25302 | |||
| db8edfe752 | |||
| 73edf28f39 | |||
| d23a42f7f5 | |||
| d0f2c08cd1 | |||
| 5599987d89 | |||
| a93763d73b | |||
| 8e8b6f4467 | |||
| 7feeed41d5 | |||
| e9a20653b8 | |||
| 9438691506 | |||
| 84a4924e77 | |||
| 49277310ba | |||
| 944d2eb7d5 | |||
| bfaf17540e | |||
| 6dbc4efbd9 | |||
| cabbeaf1bf | |||
| e554f1e0ad | |||
| 62a4a2ed70 | |||
| caad74c73d | |||
| 917993d8fb | |||
| 1451db39e6 | |||
| f13a2a6c06 | |||
| ce39fb6675 | |||
| 02a926b74a | |||
| 54ba710ea0 | |||
| 2653d12e55 | |||
| f94d6d51cf | |||
| a0116f9aec | |||
| aaa8ee9d53 | |||
| ab0f6af4b9 | |||
| 7669d0933f | |||
| 50433fe265 | |||
| 42aade29eb | |||
| 9f26759b8d | |||
| 9e642c6354 | |||
| cca19f36c2 | |||
| 17894880e0 | |||
| a99b8348d7 | |||
| ef6fc82c39 | |||
| 0c83ae2408 | |||
| 92490731e7 | |||
| 0ce93e366e | |||
| 0d031875f6 | |||
| e6103e4c43 | |||
| bf85e9eb79 | |||
| 3f1e04ebd4 | |||
| d4c5131bcb | |||
| ef1c1b50d5 | |||
| 23b745d353 | |||
| 3dc94cc85a | |||
| a4c4345257 | |||
| a0fb92cf17 | |||
| 52cc77356e | |||
| a671084f4e | |||
| 3ae986acc8 | |||
| 754994ba01 | |||
| 33b181b26b | |||
| 809559e6dc | |||
| e32c042c8d | |||
| 00cc2f215a |
@@ -25,14 +25,14 @@ jobs:
|
||||
echo "file2=$(ls nym-vpn*.deb)" >> $GITHUB_ENV
|
||||
|
||||
- name: Upload nym-repo-setup
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: ${{ env.file1 }}
|
||||
path: ppa/packages/nym-repo-setup*.deb
|
||||
retention-days: 10
|
||||
|
||||
- name: Upload nym-vpn
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
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@v4.2.0
|
||||
uses: pnpm/action-setup@v5.0.0
|
||||
with:
|
||||
version: 9
|
||||
- uses: actions/setup-node@v4
|
||||
@@ -37,6 +37,9 @@ 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
|
||||
|
||||
@@ -36,7 +36,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
platform: [ubuntu-22.04]
|
||||
platform: [arc-ubuntu-22.04]
|
||||
|
||||
runs-on: ${{ matrix.platform }}
|
||||
env:
|
||||
@@ -110,7 +110,7 @@ jobs:
|
||||
|
||||
- name: Upload Artifact
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: nym-binaries-artifacts
|
||||
path: |
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
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/"
|
||||
@@ -0,0 +1,19 @@
|
||||
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,7 +57,8 @@ jobs:
|
||||
|
||||
- name: Update workspace dependencies
|
||||
run: |
|
||||
sed -i '/path = /s/version = "${{ steps.current_version.outputs.version }}"/version = "${{ inputs.version }}"/g' Cargo.toml
|
||||
# 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
|
||||
|
||||
- name: Bump versions (local only)
|
||||
run: |
|
||||
|
||||
@@ -33,7 +33,11 @@ jobs:
|
||||
- name: Install cargo-workspaces
|
||||
run: cargo install cargo-workspaces
|
||||
|
||||
# `--publish-as-is` skips version bumping since that's done in a separate CI job.
|
||||
- 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.
|
||||
- name: Publish
|
||||
env:
|
||||
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
||||
|
||||
@@ -19,6 +19,7 @@ jobs:
|
||||
RUSTUP_PERMIT_COPY_RENAME: 1
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v6
|
||||
@@ -58,7 +59,9 @@ jobs:
|
||||
|
||||
- name: Update workspace dependencies
|
||||
run: |
|
||||
sed -i '/path = /s/version = "${{ steps.current_version.outputs.version }}"/version = "${{ inputs.version }}"/g' Cargo.toml
|
||||
# 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
|
||||
|
||||
- name: Bump versions
|
||||
run: |
|
||||
@@ -68,9 +71,33 @@ 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
|
||||
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 }}
|
||||
|
||||
- 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@v4.2.0
|
||||
uses: pnpm/action-setup@v5.0.0
|
||||
with:
|
||||
version: 9
|
||||
- uses: actions/setup-node@v4
|
||||
@@ -61,6 +61,9 @@ 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
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
name: ci-nym-wallet-frontend
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'nym-wallet/**'
|
||||
- '.github/workflows/ci-nym-wallet-frontend.yml'
|
||||
|
||||
jobs:
|
||||
types-lint:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: nym-wallet/.nvmrc
|
||||
cache: yarn
|
||||
cache-dependency-path: yarn.lock
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install --network-timeout 100000
|
||||
|
||||
- name: Build TypeScript packages (wallet depends on @nymproject/types, etc.)
|
||||
run: yarn build:types
|
||||
|
||||
- name: Build @nymproject/mui-theme and @nymproject/react (wallet imports subpaths)
|
||||
run: yarn build:packages
|
||||
|
||||
- name: Typecheck nym-wallet
|
||||
run: yarn --cwd nym-wallet tsc
|
||||
|
||||
- name: Lint nym-wallet
|
||||
run: yarn --cwd nym-wallet lint
|
||||
|
||||
- name: Yarn audit (workspace lockfile; informational)
|
||||
run: yarn audit --level critical
|
||||
continue-on-error: true
|
||||
|
||||
- name: Unit tests (nym-wallet)
|
||||
run: yarn --cwd nym-wallet test
|
||||
@@ -41,6 +41,9 @@ jobs:
|
||||
sed -i.bak '1s/^/\[profile.dev\]\ndebug = false\n\n/' Cargo.toml
|
||||
git diff
|
||||
|
||||
- name: Ensure nym-wallet/dist exists for Tauri
|
||||
run: mkdir -p nym-wallet/dist
|
||||
|
||||
- name: Build all binaries
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
@@ -71,3 +74,16 @@ jobs:
|
||||
with:
|
||||
command: clippy
|
||||
args: --manifest-path nym-wallet/Cargo.toml --workspace --all-features --all-targets -- -D warnings
|
||||
|
||||
- name: Install cargo-audit
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: install
|
||||
args: cargo-audit --locked
|
||||
|
||||
- name: Cargo audit (nym-wallet workspace)
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: audit
|
||||
working-directory: nym-wallet
|
||||
continue-on-error: true
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
name: ci-nym-wallet-storybook
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'nym-wallet/**'
|
||||
- '.github/workflows/ci-nym-wallet-storybook.yml'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: arc-linux-latest-dind
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Install rsync
|
||||
run: sudo apt-get install rsync
|
||||
continue-on-error: true
|
||||
|
||||
- uses: rlespinasse/github-slug-action@v3.x
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Setup yarn
|
||||
run: npm install -g yarn
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: ${{ vars.REQUIRED_RUSTC_VERSION }}
|
||||
|
||||
- name: Install wasm-pack
|
||||
run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
|
||||
|
||||
- name: Build dependencies
|
||||
run: yarn && yarn build
|
||||
|
||||
- name: Build storybook
|
||||
run: yarn storybook:build
|
||||
working-directory: ./nym-wallet
|
||||
|
||||
- name: Deploy branch to CI www (storybook)
|
||||
continue-on-error: true
|
||||
uses: easingthemes/ssh-deploy@main
|
||||
env:
|
||||
SSH_PRIVATE_KEY: ${{ secrets.CI_WWW_SSH_PRIVATE_KEY }}
|
||||
ARGS: "-rltgoDzvO --delete"
|
||||
SOURCE: "nym-wallet/storybook-static/"
|
||||
REMOTE_HOST: ${{ secrets.CI_WWW_REMOTE_HOST }}
|
||||
REMOTE_USER: ${{ secrets.CI_WWW_REMOTE_USER }}
|
||||
TARGET: ${{ secrets.CI_WWW_REMOTE_TARGET }}/wallet-${{ env.GITHUB_REF_SLUG }}
|
||||
EXCLUDE: "/dist/, /node_modules/"
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install Dependencies (Linux)
|
||||
run: sudo apt-get update && sudo apt-get install -y libwebkit2gtk-4.0-dev build-essential curl wget libssl-dev libgtk-3-dev squashfs-tools
|
||||
run: sudo apt-get update && sudo apt-get install -y libwebkit2gtk-4.1-dev build-essential curl wget libssl-dev libgtk-3-dev squashfs-tools libsoup-3.0-dev libjavascriptcoregtk-4.1-dev
|
||||
if: matrix.os == 'ubuntu-22.04'
|
||||
|
||||
- name: Install rust toolchain
|
||||
|
||||
@@ -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@v6
|
||||
- uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: report
|
||||
path: .github/workflows/support-files/notifications/deny.message
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-22.04
|
||||
- os: arc-ubuntu-22.04
|
||||
target: x86_64-unknown-linux-gnu
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
@@ -66,7 +66,7 @@ jobs:
|
||||
args: --workspace --release ${{ env.CARGO_FEATURES }}
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: my-artifact
|
||||
path: |
|
||||
|
||||
@@ -27,14 +27,14 @@ jobs:
|
||||
run: make contracts
|
||||
|
||||
- name: Upload Mixnet Contract Artifact
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
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@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: vesting_contract.wasm
|
||||
path: contracts/target/wasm32-unknown-unknown/release/vesting_contract.wasm
|
||||
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
- name: Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 21
|
||||
node-version: 22.13.0
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
@@ -108,7 +108,7 @@ jobs:
|
||||
cd -
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: nym-wallet.app.tar.gz
|
||||
path: nym-wallet/target/x86_64-apple-darwin/release/bundle/macos/nym-wallet.app.tar.gz
|
||||
|
||||
@@ -30,7 +30,7 @@ jobs:
|
||||
- name: Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 21
|
||||
node-version: 22.13.0
|
||||
cache: 'yarn'
|
||||
|
||||
- name: Install Rust toolchain
|
||||
@@ -72,6 +72,41 @@ jobs:
|
||||
find target/release/bundle -type d -name "*appimage*" -o -name "*AppImage*" || echo "No AppImage directories found"
|
||||
find target/release/bundle -name "*.AppImage" -o -name "*.appimage" || echo "No AppImage files found"
|
||||
fi
|
||||
|
||||
- name: Inspect AppImage (hook + bundled graphics libs)
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
APPIMAGE_REL=$(find target/release/bundle -name '*.AppImage' | head -n 1)
|
||||
if [ -z "${APPIMAGE_REL}" ]; then
|
||||
echo "No AppImage under target/release/bundle"
|
||||
exit 1
|
||||
fi
|
||||
APPIMAGE_ABS="${GITHUB_WORKSPACE}/nym-wallet/${APPIMAGE_REL}"
|
||||
chmod +x "${APPIMAGE_ABS}"
|
||||
EXTRACT_DIR=$(mktemp -d)
|
||||
cd "${EXTRACT_DIR}"
|
||||
"${APPIMAGE_ABS}" --appimage-extract
|
||||
# Tauri only stages appimage "files" under /usr/ into the AppDir; paths like /apprun-hooks/ never reach the image.
|
||||
# Wayland + WEBKIT_DISABLE_DMABUF_RENDERER defaults are applied in main() instead (see configure_linux_wayland_defaults).
|
||||
HOOK=$(find squashfs-root -name '99-nym-wayland.sh' 2>/dev/null | head -n 1)
|
||||
if [ -n "${HOOK}" ]; then
|
||||
echo "Found legacy apprun hook at ${HOOK}"
|
||||
else
|
||||
echo "No apprun-hooks/99-nym-wayland.sh (expected): Wayland defaults are set in-process."
|
||||
fi
|
||||
find squashfs-root/usr/lib -maxdepth 6 \
|
||||
\( -name 'libwayland-client.so*' -o -name 'libEGL.so*' -o -name 'libgbm.so*' \) \
|
||||
2>/dev/null | sort > "${GITHUB_WORKSPACE}/nym-wallet/appimage-bundled-graphics-libs.txt"
|
||||
wc -l "${GITHUB_WORKSPACE}/nym-wallet/appimage-bundled-graphics-libs.txt"
|
||||
head -50 "${GITHUB_WORKSPACE}/nym-wallet/appimage-bundled-graphics-libs.txt" || true
|
||||
|
||||
- name: Upload AppImage graphics lib inventory
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: nym-wallet-appimage-lib-inventory
|
||||
path: nym-wallet/appimage-bundled-graphics-libs.txt
|
||||
retention-days: 30
|
||||
|
||||
- name: Create AppImage tarball if needed
|
||||
run: |
|
||||
@@ -97,7 +132,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: nym-wallet-appimage.tar.gz
|
||||
path: |
|
||||
|
||||
@@ -26,6 +26,9 @@ jobs:
|
||||
outputs:
|
||||
release_tag: ${{ github.ref_name }}
|
||||
|
||||
env:
|
||||
SIGN_WINDOWS: ${{ github.event_name == 'release' || inputs.sign }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
@@ -35,50 +38,84 @@ jobs:
|
||||
toolchain: ${{ vars.REQUIRED_RUSTC_VERSION }}
|
||||
|
||||
- name: Setup MSBuild.exe
|
||||
uses: microsoft/setup-msbuild@v2
|
||||
uses: microsoft/setup-msbuild@v3
|
||||
|
||||
# No cache:yarn here: setup-node needs yarn on PATH to populate the cache, but this runner
|
||||
# only gets yarn from the step below.
|
||||
- name: Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 21
|
||||
node-version: 22.13.0
|
||||
|
||||
- name: Install Yarn (classic)
|
||||
shell: bash
|
||||
run: npm install -g yarn@1.22.22
|
||||
|
||||
- name: Strip Authenticode thumbprint (avoid signtool on runner)
|
||||
working-directory: nym-wallet/src-tauri
|
||||
if: ${{ env.SIGN_WINDOWS == 'true' || (github.event_name == 'workflow_dispatch' && !inputs.sign) }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if ! command -v yq >/dev/null 2>&1; then
|
||||
echo "yq is required on this runner to edit tauri.conf.json"
|
||||
exit 1
|
||||
fi
|
||||
yq eval --inplace '
|
||||
del(.bundle.windows.certificateThumbprint) |
|
||||
del(.bundle.windows.digestAlgorithm) |
|
||||
del(.bundle.windows.timestampUrl)
|
||||
' tauri.conf.json
|
||||
|
||||
- name: Download EV CodeSignTool from ssl.com
|
||||
working-directory: nym-wallet/src-tauri
|
||||
if: ${{ inputs.sign }}
|
||||
if: env.SIGN_WINDOWS == 'true'
|
||||
shell: bash
|
||||
run: |
|
||||
curl -L0 https://www.ssl.com/download/codesigntool-for-linux-and-macos/ -o codesigntool.zip
|
||||
unzip codesigntool.zip
|
||||
- name: Get EV certificate credential id
|
||||
working-directory: nym-wallet/src-tauri
|
||||
if: ${{ inputs.sign }}
|
||||
if: env.SIGN_WINDOWS == 'true'
|
||||
id: get_credential_ids
|
||||
shell: bash
|
||||
run: |
|
||||
echo "SSL_COM_CREDENTIAL_ID=$(./CodeSignTool.sh get_credential_ids -username=${{ secrets.SSL_COM_USERNAME }} -password=${{ secrets.SSL_COM_PASSWORD }} | sed -n '1!p' | sed 's/- //')" >> "$GITHUB_OUTPUT"
|
||||
- name: Add custom sign command to tauri.conf.json
|
||||
working-directory: nym-wallet/src-tauri
|
||||
if: ${{ inputs.sign }}
|
||||
if: env.SIGN_WINDOWS == 'true'
|
||||
shell: bash
|
||||
env:
|
||||
SSL_SIGN_USER: ${{ secrets.SSL_COM_USERNAME }}
|
||||
SSL_SIGN_PASS: ${{ secrets.SSL_COM_PASSWORD }}
|
||||
SSL_SIGN_CRED: ${{ steps.get_credential_ids.outputs.SSL_COM_CREDENTIAL_ID }}
|
||||
SSL_SIGN_TOTP: ${{ secrets.SSL_COM_TOTP_SECRET }}
|
||||
run: |
|
||||
yq eval --inplace '.bundle.windows +=
|
||||
{
|
||||
"signCommand": {
|
||||
"cmd": "C:\Program Files\Git\bin\bash.EXE",
|
||||
"args": [
|
||||
"/c/actions-runner/_work/nym/nym/nym-wallet/src-tauri/CodeSignTool.sh",
|
||||
"sign",
|
||||
"-username ${{ secrets.SSL_COM_USERNAME }}",
|
||||
"-password ${{ secrets.SSL_COM_PASSWORD }}",
|
||||
"-credential_id ${{ steps.get_credential_ids.outputs.SSL_COM_CREDENTIAL_ID }}",
|
||||
"-totp_secret ${{ secrets.SSL_COM_TOTP_SECRET }}",
|
||||
"-program_name NymWallet",
|
||||
"-input_file_path",
|
||||
"%1",
|
||||
"-override"
|
||||
]
|
||||
set -euo pipefail
|
||||
if ! command -v cygpath >/dev/null 2>&1; then
|
||||
echo "cygpath not found; install Git for Windows or use bash from Git SDK"
|
||||
exit 1
|
||||
fi
|
||||
export SCRIPT_UNIX="$(cygpath -u "$GITHUB_WORKSPACE/nym-wallet/src-tauri/CodeSignTool.sh")"
|
||||
yq eval --inplace '
|
||||
.bundle.windows += {
|
||||
"signCommand": {
|
||||
"cmd": "C:/Program Files/Git/bin/bash.exe",
|
||||
"args": [
|
||||
strenv(SCRIPT_UNIX),
|
||||
"sign",
|
||||
("-username " + strenv(SSL_SIGN_USER)),
|
||||
("-password " + strenv(SSL_SIGN_PASS)),
|
||||
("-credential_id " + strenv(SSL_SIGN_CRED)),
|
||||
("-totp_secret " + strenv(SSL_SIGN_TOTP)),
|
||||
"-program_name NymWallet",
|
||||
"-input_file_path",
|
||||
"%1",
|
||||
"-override"
|
||||
]
|
||||
}
|
||||
}
|
||||
}' tauri.conf.json
|
||||
' tauri.conf.json
|
||||
- name: Install project dependencies
|
||||
shell: bash
|
||||
run: cd .. && yarn --network-timeout 100000
|
||||
@@ -93,10 +130,10 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
SSL_COM_USERNAME: ${{ inputs.sign && secrets.SSL_COM_USERNAME }}
|
||||
SSL_COM_PASSWORD: ${{ inputs.sign && secrets.SSL_COM_PASSWORD }}
|
||||
SSL_COM_CREDENTIAL_ID: ${{ inputs.sign && steps.get_credential_ids.outputs.SSL_COM_CREDENTIAL_ID }}
|
||||
SSL_COM_TOTP_SECRET: ${{ inputs.sign && secrets.SSL_COM_TOTP_SECRET }}
|
||||
SSL_COM_USERNAME: ${{ env.SIGN_WINDOWS == 'true' && secrets.SSL_COM_USERNAME }}
|
||||
SSL_COM_PASSWORD: ${{ env.SIGN_WINDOWS == 'true' && secrets.SSL_COM_PASSWORD }}
|
||||
SSL_COM_CREDENTIAL_ID: ${{ env.SIGN_WINDOWS == 'true' && steps.get_credential_ids.outputs.SSL_COM_CREDENTIAL_ID }}
|
||||
SSL_COM_TOTP_SECRET: ${{ env.SIGN_WINDOWS == 'true' && secrets.SSL_COM_TOTP_SECRET }}
|
||||
run: |
|
||||
echo "Starting build process..."
|
||||
yarn build
|
||||
@@ -128,7 +165,7 @@ jobs:
|
||||
find . -name "*.msi" -type f
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: nym-wallet.msi
|
||||
path: |
|
||||
@@ -167,4 +204,4 @@ jobs:
|
||||
needs: publish-tauri
|
||||
with:
|
||||
release_tag: ${{ needs.publish-tauri.outputs.release_tag || github.ref_name }}
|
||||
secrets: inherit
|
||||
secrets: inherit
|
||||
|
||||
@@ -76,7 +76,7 @@ jobs:
|
||||
apk/nyms5-arch64-release.apk
|
||||
|
||||
- name: Upload APKs
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
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@v7
|
||||
uses: actions/download-artifact@v8
|
||||
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@v3
|
||||
uses: docker/login-action@v4
|
||||
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@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: harbor.nymte.ch
|
||||
username: ${{ secrets.HARBOR_ROBOT_USERNAME }}
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
name: Build and upload Network Monitor Agent container to harbor.nymte.ch
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
release_image:
|
||||
description: 'Tag image as a release (prefix with golden-)'
|
||||
required: true
|
||||
default: false
|
||||
type: boolean
|
||||
|
||||
env:
|
||||
WORKING_DIRECTORY: "nym-network-monitor-v3/nym-network-monitor-agent"
|
||||
CONTAINER_NAME: "network-monitor-agent"
|
||||
|
||||
jobs:
|
||||
build-container:
|
||||
runs-on: arc-linux-latest-dind
|
||||
steps:
|
||||
- name: Login to Harbor
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: harbor.nymte.ch
|
||||
username: ${{ secrets.HARBOR_ROBOT_USERNAME }}
|
||||
password: ${{ secrets.HARBOR_ROBOT_SECRET }}
|
||||
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Configure git identity
|
||||
run: |
|
||||
git config --global user.email "lawrence@nymtech.net"
|
||||
git config --global user.name "Lawrence Stalder"
|
||||
|
||||
- name: Get version from Cargo.toml
|
||||
id: get_version
|
||||
run: |
|
||||
VERSION=$(yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml)
|
||||
echo "result=$VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Set GIT_TAG variable
|
||||
run: echo "GIT_TAG=${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }}" >> $GITHUB_ENV
|
||||
|
||||
- name: Initialize RELEASE_TAG
|
||||
run: echo "RELEASE_TAG=" >> $GITHUB_ENV
|
||||
|
||||
- name: Set RELEASE_TAG for release
|
||||
if: github.event.inputs.release_image == 'true'
|
||||
run: echo "RELEASE_TAG=golden-" >> $GITHUB_ENV
|
||||
|
||||
- name: Set IMAGE_NAME_AND_TAGS variable
|
||||
run: echo "IMAGE_NAME_AND_TAGS=${{ env.CONTAINER_NAME }}:${{ env.RELEASE_TAG }}${{ steps.get_version.outputs.result }}" >> $GITHUB_ENV
|
||||
|
||||
- name: New env vars
|
||||
run: echo "RELEASE_TAG='$RELEASE_TAG' GIT_TAG='$GIT_TAG' IMAGE_NAME_AND_TAGS='$IMAGE_NAME_AND_TAGS'"
|
||||
|
||||
- name: Build and push image to Harbor
|
||||
run: |
|
||||
docker build -f ${{ env.WORKING_DIRECTORY }}/Dockerfile . -t harbor.nymte.ch/nym/${{ env.IMAGE_NAME_AND_TAGS }}
|
||||
docker push harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }} --all-tags
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
name: Build and upload Network Monitor Orchestrator container to harbor.nymte.ch
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
release_image:
|
||||
description: 'Tag image as a release (prefix with golden-)'
|
||||
required: true
|
||||
default: false
|
||||
type: boolean
|
||||
|
||||
env:
|
||||
WORKING_DIRECTORY: "nym-network-monitor-v3/nym-network-monitor-orchestrator"
|
||||
CONTAINER_NAME: "network-monitor-orchestrator"
|
||||
|
||||
jobs:
|
||||
build-container:
|
||||
runs-on: arc-linux-latest-dind
|
||||
steps:
|
||||
- name: Login to Harbor
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: harbor.nymte.ch
|
||||
username: ${{ secrets.HARBOR_ROBOT_USERNAME }}
|
||||
password: ${{ secrets.HARBOR_ROBOT_SECRET }}
|
||||
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Configure git identity
|
||||
run: |
|
||||
git config --global user.email "lawrence@nymtech.net"
|
||||
git config --global user.name "Lawrence Stalder"
|
||||
|
||||
- name: Get version from Cargo.toml
|
||||
id: get_version
|
||||
run: |
|
||||
VERSION=$(yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml)
|
||||
echo "result=$VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Initialize RELEASE_TAG
|
||||
run: echo "RELEASE_TAG=" >> $GITHUB_ENV
|
||||
|
||||
- name: Set RELEASE_TAG for release
|
||||
if: github.event.inputs.release_image == 'true'
|
||||
run: echo "RELEASE_TAG=golden-" >> $GITHUB_ENV
|
||||
|
||||
- name: Set IMAGE_NAME_AND_TAGS variable
|
||||
run: echo "IMAGE_NAME_AND_TAGS=${{ env.CONTAINER_NAME }}:${{ env.RELEASE_TAG }}${{ steps.get_version.outputs.result }}" >> $GITHUB_ENV
|
||||
|
||||
- name: Log image name
|
||||
run: echo "RELEASE_TAG='$RELEASE_TAG' IMAGE_NAME_AND_TAGS='$IMAGE_NAME_AND_TAGS'"
|
||||
|
||||
- name: Build and push image to Harbor
|
||||
run: |
|
||||
docker build -f ${{ env.WORKING_DIRECTORY }}/Dockerfile . -t harbor.nymte.ch/nym/${{ env.IMAGE_NAME_AND_TAGS }}
|
||||
docker push harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }} --all-tags
|
||||
@@ -11,7 +11,7 @@ jobs:
|
||||
runs-on: arc-ubuntu-22.04-dind
|
||||
steps:
|
||||
- name: Login to Harbor
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
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@v3
|
||||
uses: docker/login-action@v4
|
||||
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@v3
|
||||
uses: docker/login-action@v4
|
||||
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@v3
|
||||
uses: docker/login-action@v4
|
||||
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@v3
|
||||
uses: docker/login-action@v4
|
||||
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@v3
|
||||
uses: docker/login-action@v4
|
||||
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@v3
|
||||
uses: docker/login-action@v4
|
||||
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@v3
|
||||
uses: docker/login-action@v4
|
||||
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@v6
|
||||
- uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: Asset Hashes
|
||||
path: hashes.json
|
||||
|
||||
@@ -27,6 +27,7 @@ v6-topology.json
|
||||
/explorer/public/downloads/mixmining.json
|
||||
/explorer/public/downloads/topology.json
|
||||
/nym-wallet/dist/*
|
||||
/nym-wallet/appimage-bundled-graphics-libs.txt
|
||||
/clients/validator/examples/nym-driver-example/current-contract.txt
|
||||
validator-api/v4.json
|
||||
validator-api/v6.json
|
||||
@@ -77,3 +78,4 @@ CLAUDE.md
|
||||
|
||||
/notes
|
||||
/target-otel
|
||||
test-tutorials/
|
||||
|
||||
@@ -4,6 +4,34 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [2026.9-venaco] (2026-05-06)
|
||||
|
||||
- Fix for v9 IPR ([#6710])
|
||||
- Only init SHARED_CLIENT if requested ([#6708])
|
||||
- Fixes to crates and CI ([#6686])
|
||||
- Return ipv6 addresses as well ([#6684])
|
||||
- Fix invalid ticket spend ([#6683])
|
||||
- Block non-public IPR/NR checks ([#6670])
|
||||
|
||||
[#6710]: https://github.com/nymtech/nym/pull/6710
|
||||
[#6708]: https://github.com/nymtech/nym/pull/6708
|
||||
[#6686]: https://github.com/nymtech/nym/pull/6686
|
||||
[#6684]: https://github.com/nymtech/nym/pull/6684
|
||||
[#6683]: https://github.com/nymtech/nym/pull/6683
|
||||
[#6670]: https://github.com/nymtech/nym/pull/6670
|
||||
|
||||
## [2026.8-urda] (2026-04-20)
|
||||
|
||||
- Include all gateways in the returned list ([#6649])
|
||||
- Optimize GW probe in NS agent ([#6636])
|
||||
- Max/sdk docrs ([#6566])
|
||||
- Max/sdk stream wrapper ([#6320])
|
||||
|
||||
[#6649]: https://github.com/nymtech/nym/pull/6649
|
||||
[#6636]: https://github.com/nymtech/nym/pull/6636
|
||||
[#6566]: https://github.com/nymtech/nym/pull/6566
|
||||
[#6320]: https://github.com/nymtech/nym/pull/6320
|
||||
|
||||
## [2026.7-tola] (2026-04-07)
|
||||
|
||||
- Simon/ecash contract serde fix ([#6634])
|
||||
|
||||
Generated
+887
-462
File diff suppressed because it is too large
Load Diff
+133
-124
@@ -31,7 +31,6 @@ 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",
|
||||
@@ -41,6 +40,7 @@ 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",
|
||||
@@ -70,11 +70,15 @@ 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-lp",
|
||||
"common/nym-kkt",
|
||||
"common/nym-kkt-ciphersuite",
|
||||
"common/nym-kkt-context",
|
||||
"common/nym-lp",
|
||||
"common/nym-lp-data",
|
||||
"common/nym-metrics",
|
||||
"common/nym_offline_compact_ecash",
|
||||
"common/nymnoise",
|
||||
@@ -90,9 +94,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",
|
||||
@@ -122,6 +126,7 @@ members = [
|
||||
"common/zulip-client",
|
||||
"documentation/autodoc",
|
||||
"gateway",
|
||||
"integration-tests",
|
||||
"nym-api",
|
||||
"nym-api/nym-api-requests",
|
||||
"nym-authenticator-client",
|
||||
@@ -129,7 +134,9 @@ 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",
|
||||
@@ -140,6 +147,7 @@ members = [
|
||||
"nym-outfox",
|
||||
"nym-registration-client",
|
||||
"nym-signers-monitor",
|
||||
"nym-sqlx-pool-guard",
|
||||
"nym-statistics-api",
|
||||
"nym-validator-rewarder",
|
||||
"nyx-chain-watcher",
|
||||
@@ -147,19 +155,18 @@ 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",
|
||||
"nym-sqlx-pool-guard",
|
||||
"smolmix/core",
|
||||
"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",
|
||||
@@ -172,27 +179,25 @@ members = [
|
||||
"wasm/mix-fetch",
|
||||
"wasm/node-tester",
|
||||
"wasm/zknym-lib",
|
||||
"nym-gateway-probe",
|
||||
"integration-tests",
|
||||
"common/nym-kkt-ciphersuite",
|
||||
"common/nym-kkt-context",
|
||||
]
|
||||
|
||||
default-members = [
|
||||
"clients/native",
|
||||
"clients/socks5",
|
||||
"nym-authenticator-client",
|
||||
"nym-api",
|
||||
"nym-authenticator-client",
|
||||
"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",
|
||||
"tools/internal/localnet-orchestrator"
|
||||
"tools/internal/localnet-orchestrator",
|
||||
]
|
||||
|
||||
exclude = ["contracts", "nym-wallet", "cpu-cycles"]
|
||||
@@ -206,7 +211,7 @@ edition = "2024"
|
||||
license = "Apache-2.0"
|
||||
rust-version = "1.87.0"
|
||||
readme = "README.md"
|
||||
version = "1.20.4"
|
||||
version = "1.21.0"
|
||||
|
||||
[workspace.dependencies]
|
||||
addr = "0.15.6"
|
||||
@@ -229,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.0"
|
||||
blake3 = ">=1.7, <1.8.4" # blake3 1.8.4+ requires digest 0.11; workspace is on 0.10
|
||||
bloomfilter = "3.0.1"
|
||||
bs58 = "0.5.1"
|
||||
bytecodec = "0.4.15"
|
||||
@@ -280,8 +285,8 @@ getrandom03 = { package = "getrandom", version = "=0.3.3" }
|
||||
glob = "0.3"
|
||||
handlebars = "3.5.5"
|
||||
hex = "0.4.3"
|
||||
hickory-proto = "0.25.2"
|
||||
hickory-resolver = "0.25.2"
|
||||
hickory-proto = "0.26.1"
|
||||
hickory-resolver = "0.26.1"
|
||||
hkdf = "0.12.3"
|
||||
hmac = "0.12.1"
|
||||
http = "1"
|
||||
@@ -325,7 +330,7 @@ pnet_packet = "0.35.0"
|
||||
publicsuffix = "2.3.0"
|
||||
proc_pidinfo = "0.1.3"
|
||||
quote = "1"
|
||||
rand = "0.8.5"
|
||||
rand = "0.8.6"
|
||||
rand09 = { package = "rand", version = "=0.9.2" }
|
||||
rand_chacha = "0.3"
|
||||
rand_chacha09 = { package = "rand_chacha", version = "=0.9.0" }
|
||||
@@ -349,7 +354,6 @@ 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"
|
||||
@@ -360,7 +364,7 @@ subtle-encoding = "0.5"
|
||||
syn = "2"
|
||||
sysinfo = "0.37.0"
|
||||
tap = "1.0.1"
|
||||
tar = "0.4.44"
|
||||
tar = "0.4.45"
|
||||
test-with = { version = "0.15.4", default-features = false }
|
||||
tempfile = "3.20"
|
||||
thiserror = "2.0"
|
||||
@@ -411,113 +415,117 @@ libcrux-chacha20poly1305 = "0.0.7"
|
||||
libcrux-psq = "0.0.8"
|
||||
libcrux-ml-kem = "0.0.8"
|
||||
libcrux-sha3 = "0.0.8"
|
||||
libcrux-traits = "0.0.8"
|
||||
libcrux-traits = "0.0.6"
|
||||
|
||||
# Workspace dep definitions required by crates.io publication - we need a workspace version since `cargo workspaces` doesn't work with path imports from crate manifests
|
||||
nym-api-requests = { version = "1.20.4", path = "nym-api/nym-api-requests" }
|
||||
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-api-requests = { version = "1.21.0", path = "nym-api/nym-api-requests" }
|
||||
nym-authenticator-requests = { version = "1.21.0", path = "common/authenticator-requests" }
|
||||
nym-async-file-watcher = { version = "1.21.0", path = "common/async-file-watcher" }
|
||||
nym-authenticator-client = { version = "1.21.0", path = "nym-authenticator-client" }
|
||||
nym-bandwidth-controller = { version = "1.21.0", path = "common/bandwidth-controller" }
|
||||
nym-bin-common = { version = "1.21.0", path = "common/bin-common" }
|
||||
nym-cache = { version = "1.21.0", path = "common/nym-cache" }
|
||||
nym-client-core = { version = "1.21.0", path = "common/client-core", default-features = false }
|
||||
nym-client-core-config-types = { version = "1.21.0", path = "common/client-core/config-types" }
|
||||
nym-client-core-gateways-storage = { version = "1.21.0", path = "common/client-core/gateways-storage" }
|
||||
nym-client-core-surb-storage = { version = "1.21.0", path = "common/client-core/surb-storage" }
|
||||
nym-client-websocket-requests = { version = "1.21.0", path = "clients/native/websocket-requests" }
|
||||
nym-common = { version = "1.21.0", path = "common/nym-common" }
|
||||
nym-compact-ecash = { version = "1.21.0", path = "common/nym_offline_compact_ecash" }
|
||||
nym-config = { version = "1.21.0", path = "common/config" }
|
||||
nym-contracts-common = { version = "1.21.0", path = "common/cosmwasm-smart-contracts/contracts-common" }
|
||||
nym-coconut-dkg-common = { version = "1.21.0", path = "common/cosmwasm-smart-contracts/coconut-dkg" }
|
||||
nym-credential-storage = { version = "1.21.0", path = "common/credential-storage" }
|
||||
nym-credential-utils = { version = "1.21.0", path = "common/credential-utils" }
|
||||
nym-credential-proxy-lib = { version = "1.21.0", path = "common/credential-proxy" }
|
||||
nym-credentials = { version = "1.21.0", path = "common/credentials", default-features = false }
|
||||
nym-credentials-interface = { version = "1.21.0", path = "common/credentials-interface" }
|
||||
nym-credential-proxy-requests = { version = "1.21.0", path = "nym-credential-proxy/nym-credential-proxy-requests", default-features = false }
|
||||
nym-credential-verification = { version = "1.21.0", path = "common/credential-verification" }
|
||||
nym-crypto = { version = "1.21.0", path = "common/crypto", default-features = false }
|
||||
nym-dkg = { version = "1.21.0", path = "common/dkg" }
|
||||
nym-ecash-contract-common = { version = "1.21.0", path = "common/cosmwasm-smart-contracts/ecash-contract" }
|
||||
nym-ecash-signer-check = { version = "1.21.0", path = "common/ecash-signer-check" }
|
||||
nym-ecash-signer-check-types = { version = "1.21.0", path = "common/ecash-signer-check-types" }
|
||||
nym-ecash-time = { version = "1.21.0", path = "common/ecash-time" }
|
||||
nym-exit-policy = { version = "1.21.0", path = "common/exit-policy" }
|
||||
nym-ffi-shared = { version = "1.21.0", path = "sdk/ffi/shared" }
|
||||
nym-gateway-client = { version = "1.21.0", path = "common/client-libs/gateway-client", default-features = false }
|
||||
nym-gateway-probe = { version = "1.18.0", path = "nym-gateway-probe" }
|
||||
nym-gateway-requests = { version = "1.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-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-gateway-requests = { version = "1.21.0", path = "common/gateway-requests" }
|
||||
nym-gateway-storage = { version = "1.21.0", path = "common/gateway-storage" }
|
||||
nym-gateway-stats-storage = { version = "1.21.0", path = "common/gateway-stats-storage" }
|
||||
nym-group-contract-common = { version = "1.21.0", path = "common/cosmwasm-smart-contracts/group-contract" }
|
||||
nym-http-api-client = { version = "1.21.0", path = "common/http-api-client" }
|
||||
nym-http-api-client-macro = { version = "1.21.0", path = "common/http-api-client-macro" }
|
||||
nym-http-api-common = { version = "1.21.0", path = "common/http-api-common", default-features = false }
|
||||
nym-id = { version = "1.21.0", path = "common/nym-id" }
|
||||
nym-ip-packet-client = { version = "1.21.0", path = "nym-ip-packet-client" }
|
||||
nym-ip-packet-requests = { version = "1.21.0", path = "common/ip-packet-requests" }
|
||||
nym-lp = { version = "1.21.0", path = "common/nym-lp" }
|
||||
nym-lp-data = { version = "1.21.0", path = "common/nym-lp-data" }
|
||||
nym-kkt = { version = "1.21.0", path = "common/nym-kkt" }
|
||||
nym-kkt-ciphersuite = { version = "1.21.0", path = "common/nym-kkt-ciphersuite" }
|
||||
nym-kkt-context = { version = "1.21.0", path = "common/nym-kkt-context" }
|
||||
nym-metrics = { version = "1.21.0", path = "common/nym-metrics" }
|
||||
nym-mixnet-client = { version = "1.21.0", path = "common/client-libs/mixnet-client" }
|
||||
nym-mixnet-contract-common = { version = "1.21.0", path = "common/cosmwasm-smart-contracts/mixnet-contract" }
|
||||
nym-multisig-contract-common = { version = "1.21.0", path = "common/cosmwasm-smart-contracts/multisig-contract" }
|
||||
nym-network-defaults = { version = "1.21.0", path = "common/network-defaults" }
|
||||
nym-node-tester-utils = { version = "1.21.0", path = "common/node-tester-utils" }
|
||||
nym-noise = { version = "1.21.0", path = "common/nymnoise" }
|
||||
nym-noise-keys = { version = "1.21.0", path = "common/nymnoise/keys" }
|
||||
nym-nonexhaustive-delayqueue = { version = "1.21.0", path = "common/nonexhaustive-delayqueue" }
|
||||
nym-node-requests = { version = "1.21.0", path = "nym-node/nym-node-requests", default-features = false }
|
||||
nym-node-metrics = { version = "1.21.0", path = "nym-node/nym-node-metrics" }
|
||||
nym-node-families-contract-common = { version = "1.21.0", path = "common/cosmwasm-smart-contracts/node-families-contract" }
|
||||
nym-ordered-buffer = { version = "1.21.0", path = "common/socks5/ordered-buffer" }
|
||||
nym-outfox = { version = "1.21.0", path = "nym-outfox" }
|
||||
nym-registration-common = { version = "1.21.0", path = "common/registration" }
|
||||
nym-pemstore = { version = "1.21.0", path = "common/pemstore" }
|
||||
nym-performance-contract-common = { version = "1.21.0", path = "common/cosmwasm-smart-contracts/nym-performance-contract" }
|
||||
nym-sdk = { version = "1.21.0", path = "sdk/rust/nym-sdk" }
|
||||
nym-serde-helpers = { version = "1.21.0", path = "common/serde-helpers" }
|
||||
nym-service-providers-common = { version = "1.21.0", path = "service-providers/common" }
|
||||
nym-service-provider-requests-common = { version = "1.21.0", path = "common/service-provider-requests-common" }
|
||||
nym-socks5-client-core = { version = "1.21.0", path = "common/socks5-client-core" }
|
||||
nym-socks5-proxy-helpers = { version = "1.21.0", path = "common/socks5/proxy-helpers" }
|
||||
nym-socks5-requests = { version = "1.21.0", path = "common/socks5/requests" }
|
||||
nym-sphinx = { version = "1.21.0", path = "common/nymsphinx" }
|
||||
nym-sphinx-acknowledgements = { version = "1.21.0", path = "common/nymsphinx/acknowledgements" }
|
||||
nym-sphinx-addressing = { version = "1.21.0", path = "common/nymsphinx/addressing" }
|
||||
nym-sphinx-anonymous-replies = { version = "1.21.0", path = "common/nymsphinx/anonymous-replies" }
|
||||
nym-sphinx-chunking = { version = "1.21.0", path = "common/nymsphinx/chunking" }
|
||||
nym-sphinx-cover = { version = "1.21.0", path = "common/nymsphinx/cover" }
|
||||
nym-sphinx-forwarding = { version = "1.21.0", path = "common/nymsphinx/forwarding" }
|
||||
nym-sphinx-framing = { version = "1.21.0", path = "common/nymsphinx/framing" }
|
||||
nym-sphinx-params = { version = "1.21.0", path = "common/nymsphinx/params" }
|
||||
nym-sphinx-routing = { version = "1.21.0", path = "common/nymsphinx/routing" }
|
||||
nym-sphinx-types = { version = "1.21.0", path = "common/nymsphinx/types" }
|
||||
nym-statistics-common = { version = "1.21.0", path = "common/statistics" }
|
||||
nym-store-cipher = { version = "1.21.0", path = "common/store-cipher" }
|
||||
nym-task = { version = "1.21.0", path = "common/task" }
|
||||
nym-tun = { version = "1.21.0", path = "common/tun" }
|
||||
nym-test-utils = { version = "1.21.0", path = "common/test-utils" }
|
||||
nym-ticketbooks-merkle = { version = "1.21.0", path = "common/ticketbooks-merkle" }
|
||||
nym-topology = { version = "1.21.0", path = "common/topology" }
|
||||
nym-types = { version = "1.21.0", path = "common/types" }
|
||||
nym-upgrade-mode-check = { version = "1.21.0", path = "common/upgrade-mode-check" }
|
||||
nym-validator-client = { version = "1.21.0", path = "common/client-libs/validator-client", default-features = false }
|
||||
nym-vesting-contract-common = { version = "1.21.0", path = "common/cosmwasm-smart-contracts/vesting-contract" }
|
||||
nym-verloc = { version = "1.21.0", path = "common/verloc" }
|
||||
nym-wireguard = { version = "1.21.0", path = "common/wireguard" }
|
||||
nym-wireguard-types = { version = "1.21.0", path = "common/wireguard-types" }
|
||||
nym-wireguard-private-metadata-shared = { version = "1.21.0", path = "common/wireguard-private-metadata/shared" }
|
||||
nym-wireguard-private-metadata-client = { version = "1.21.0", path = "common/wireguard-private-metadata/client" }
|
||||
nym-wireguard-private-metadata-server = { version = "1.21.0", path = "common/wireguard-private-metadata/server" }
|
||||
nym-sqlx-pool-guard = { version = "1.2.0", path = "nym-sqlx-pool-guard" }
|
||||
nym-wasm-client-core = { version = "1.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" }
|
||||
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" }
|
||||
|
||||
# coconut/DKG related
|
||||
# unfortunately until https://github.com/zkcrypto/nym-bls12_381-fork/issues/10 is resolved, we have to rely on the fork
|
||||
@@ -616,3 +624,4 @@ exit = "deny"
|
||||
panic = "deny"
|
||||
unimplemented = "deny"
|
||||
unreachable = "deny"
|
||||
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
# 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
|
||||
@@ -0,0 +1,111 @@
|
||||
############################################################################################
|
||||
############################################################################################
|
||||
############################################################################################
|
||||
#### 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.74"
|
||||
version = "1.1.76"
|
||||
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>", "Jędrzej Stuczyński <andrew@nymtech.net>"]
|
||||
edition = "2021"
|
||||
license.workspace = true
|
||||
|
||||
@@ -472,6 +472,7 @@ 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)
|
||||
@@ -484,6 +485,7 @@ 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.74"
|
||||
version = "1.1.76"
|
||||
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>"]
|
||||
edition = "2021"
|
||||
license.workspace = true
|
||||
|
||||
@@ -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.into_iter()) {
|
||||
for (raw, prepared) in fragments.into_iter().zip(prepared_fragments) {
|
||||
let lane = raw.0;
|
||||
let FragmentWithMaxRetransmissions {
|
||||
fragment,
|
||||
@@ -670,7 +670,7 @@ where
|
||||
|
||||
Ok(fragments
|
||||
.into_iter()
|
||||
.zip(reply_surbs.into_iter())
|
||||
.zip(reply_surbs)
|
||||
.map(|(fragment, reply_surb)| {
|
||||
// unwrap here is fine as we know we have a valid topology
|
||||
#[allow(clippy::unwrap_used)]
|
||||
|
||||
@@ -26,6 +26,7 @@ nym-ecash-contract-common = { workspace = true }
|
||||
nym-multisig-contract-common = { workspace = true }
|
||||
nym-group-contract-common = { workspace = true }
|
||||
nym-performance-contract-common = { workspace = true }
|
||||
nym-node-families-contract-common = { workspace = true }
|
||||
nym-serde-helpers = { workspace = true, features = ["hex", "base64"] }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
|
||||
@@ -15,6 +15,7 @@ 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::{
|
||||
AnnotationResponse, ApiHealthResponse, BinaryBuildInformationOwned, ChainBlocksStatusResponse,
|
||||
ChainStatusResponse, KeyRotationInfoResponse, NodePerformanceResponse, NodeRefreshBody,
|
||||
@@ -389,6 +390,45 @@ 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> {
|
||||
|
||||
@@ -38,6 +38,7 @@ pub mod ecash {
|
||||
}
|
||||
|
||||
pub const NYM_NODES_ROUTES: &str = "nym-nodes";
|
||||
pub const NODE_FAMILIES_ROUTES: &str = "node-families";
|
||||
|
||||
pub use nym_nodes::*;
|
||||
pub mod nym_nodes {
|
||||
|
||||
@@ -867,6 +867,10 @@ 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,6 +13,7 @@ pub mod ecash_query_client;
|
||||
pub mod group_query_client;
|
||||
pub mod mixnet_query_client;
|
||||
pub mod multisig_query_client;
|
||||
pub mod node_families_query_client;
|
||||
pub mod performance_query_client;
|
||||
pub mod vesting_query_client;
|
||||
|
||||
@@ -22,6 +23,7 @@ pub mod ecash_signing_client;
|
||||
pub mod group_signing_client;
|
||||
pub mod mixnet_signing_client;
|
||||
pub mod multisig_signing_client;
|
||||
pub mod node_families_signing_client;
|
||||
pub mod performance_signing_client;
|
||||
pub mod vesting_signing_client;
|
||||
|
||||
@@ -31,6 +33,7 @@ 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 performance_query_client::{PagedPerformanceQueryClient, PerformanceQueryClient};
|
||||
pub use vesting_query_client::{PagedVestingQueryClient, VestingQueryClient};
|
||||
|
||||
@@ -40,6 +43,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 performance_signing_client::PerformanceSigningClient;
|
||||
pub use vesting_signing_client::VestingSigningClient;
|
||||
|
||||
@@ -49,6 +53,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>;
|
||||
|
||||
// coconut-related
|
||||
fn ecash_contract_address(&self) -> Option<&AccountId>;
|
||||
@@ -62,6 +67,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 ecash_contract_address: Option<AccountId>,
|
||||
pub group_contract_address: Option<AccountId>,
|
||||
@@ -86,6 +92,10 @@ impl TryFrom<NymContracts> for TypedNymContracts {
|
||||
.performance_contract_address
|
||||
.map(|addr| addr.parse())
|
||||
.transpose()?,
|
||||
node_families_contract_address: value
|
||||
.node_families_contract_address
|
||||
.map(|addr| addr.parse())
|
||||
.transpose()?,
|
||||
ecash_contract_address: value
|
||||
.ecash_contract_address
|
||||
.map(|addr| addr.parse())
|
||||
|
||||
+441
@@ -0,0 +1,441 @@
|
||||
// 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
@@ -0,0 +1,254 @@
|
||||
// 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()
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -286,6 +286,10 @@ 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;
|
||||
}
|
||||
@@ -304,6 +308,13 @@ impl<C, S> NymContractsProvider for NyxdClient<C, S> {
|
||||
self.config.contracts.performance_contract_address.as_ref()
|
||||
}
|
||||
|
||||
fn node_families_contract_address(&self) -> Option<&AccountId> {
|
||||
self.config
|
||||
.contracts
|
||||
.node_families_contract_address
|
||||
.as_ref()
|
||||
}
|
||||
|
||||
fn ecash_contract_address(&self) -> Option<&AccountId> {
|
||||
self.config.contracts.ecash_contract_address.as_ref()
|
||||
}
|
||||
|
||||
@@ -30,6 +30,9 @@ 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>,
|
||||
|
||||
@@ -130,6 +133,14 @@ 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")
|
||||
@@ -142,6 +153,7 @@ 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,6 +26,14 @@ 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,12 +3,121 @@
|
||||
|
||||
use crate::error::MixnetContractError;
|
||||
use crate::mixnode::PendingMixNodeChanges;
|
||||
use crate::nym_node::NodeOwnershipResponse;
|
||||
use crate::{
|
||||
EpochEventId, IntervalEventId, MixNodeBond, MixNodeDetails, NodeId, NodeRewarding, NymNodeBond,
|
||||
NymNodeDetails, PendingNodeChanges,
|
||||
EpochEventId, EpochId, Interval, IntervalEventId, MixNodeBond, MixNodeDetails, NodeId,
|
||||
NodeRewarding, NymNodeBond, NymNodeDetails, PendingNodeChanges, QueryMsg,
|
||||
};
|
||||
use cosmwasm_std::{Coin, Decimal, StdError, StdResult, Uint128};
|
||||
use cosmwasm_std::{
|
||||
Addr, Binary, Coin, CustomQuery, Decimal, QuerierWrapper, StdError, StdResult, Uint128,
|
||||
from_json,
|
||||
};
|
||||
use cw_storage_plus::{Key, Namespace, Path, PrimaryKey};
|
||||
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,6 +30,7 @@ pub use gateway::{
|
||||
Gateway, GatewayBond, GatewayBondResponse, GatewayConfigUpdate, GatewayOwnershipResponse,
|
||||
PagedGatewayResponse,
|
||||
};
|
||||
pub use helpers::MixnetContractQuerier;
|
||||
pub use interval::{
|
||||
CurrentIntervalResponse, EpochId, EpochState, EpochStatus, Interval, IntervalId,
|
||||
};
|
||||
|
||||
@@ -190,6 +190,10 @@ 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,6 +63,7 @@ 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,
|
||||
@@ -305,6 +306,22 @@ 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")]
|
||||
@@ -394,6 +411,15 @@ 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(),
|
||||
@@ -881,8 +907,15 @@ 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,6 +212,10 @@ 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,
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
[package]
|
||||
name = "nym-node-families-contract-common"
|
||||
description = "Common crate for Nym's node families contract"
|
||||
version.workspace = true
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
documentation.workspace = true
|
||||
rust-version = "1.85"
|
||||
readme.workspace = true
|
||||
publish = true
|
||||
|
||||
[dependencies]
|
||||
thiserror = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
schemars = { workspace = true }
|
||||
|
||||
cosmwasm-std = { workspace = true }
|
||||
cosmwasm-schema = { workspace = true }
|
||||
cw-controllers = { workspace = true }
|
||||
cw-utils = { workspace = true }
|
||||
|
||||
nym-contracts-common = { workspace = true }
|
||||
nym-mixnet-contract-common = { workspace = true }
|
||||
|
||||
[features]
|
||||
schema = []
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -0,0 +1,104 @@
|
||||
// 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";
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
// 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),
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// 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::*;
|
||||
@@ -0,0 +1,211 @@
|
||||
// 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 {
|
||||
//
|
||||
}
|
||||
@@ -0,0 +1,403 @@
|
||||
// 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>,
|
||||
}
|
||||
@@ -13,8 +13,9 @@ use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use time::OffsetDateTime;
|
||||
use tokio::sync::Mutex as AsyncMutex;
|
||||
use tokio::time::Instant;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use tracing::{debug, info, instrument, warn};
|
||||
use tracing::{debug, error, info, instrument, warn};
|
||||
use uuid::Uuid;
|
||||
|
||||
pub use helpers::{BufferedDeposit, PerformedDeposits, make_deposits_request, split_deposits};
|
||||
@@ -146,9 +147,14 @@ impl DepositsBuffer {
|
||||
|
||||
// if we're here, we know we're below the threshold
|
||||
fn maybe_refill_deposits(&self) {
|
||||
if let Some(mut guard) = self.inner.deposits_refill_task.try_get_new_task_guard() {
|
||||
if let Some((mut guard, completion_guard)) =
|
||||
self.inner.deposits_refill_task.try_get_new_task_guard()
|
||||
{
|
||||
let this = self.clone();
|
||||
*guard = Some(tokio::spawn(async move { this.refill_deposits().await }));
|
||||
*guard = Some(tokio::spawn(async move {
|
||||
let _completion_guard = completion_guard;
|
||||
this.refill_deposits().await
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,6 +185,8 @@ impl DepositsBuffer {
|
||||
requested_on: OffsetDateTime,
|
||||
client_pubkey: PublicKeyUser,
|
||||
) -> Result<BufferedDeposit, CredentialProxyError> {
|
||||
let wait_start = Instant::now();
|
||||
let mut i = 0;
|
||||
loop {
|
||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||
if let Some(buffered_deposit) = self.inner.unused_deposits.lock().await.pop() {
|
||||
@@ -195,6 +203,15 @@ impl DepositsBuffer {
|
||||
// make sure there's always a task working in the background in case deposits get used up too quickly
|
||||
self.maybe_refill_deposits()
|
||||
}
|
||||
i += 1;
|
||||
let elapsed = wait_start.elapsed();
|
||||
if elapsed > Duration::from_secs(5) && i % 10 == 0 {
|
||||
warn!("we've been waiting for over 5s to make a deposit - something is wrong!")
|
||||
} else if elapsed > Duration::from_secs(10) && i % 5 == 0 {
|
||||
error!(
|
||||
"we've been waiting for over 10s to make a deposit - something is SERIOUSLY wrong!"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,12 +3,22 @@
|
||||
|
||||
use crate::error::CredentialProxyError;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::{Mutex as StdMutex, MutexGuard};
|
||||
use std::sync::{Arc, Mutex as StdMutex, MutexGuard};
|
||||
use tokio::task::JoinHandle;
|
||||
use tracing::{debug, error};
|
||||
|
||||
pub(super) type RefillTaskResult = Result<(), CredentialProxyError>;
|
||||
|
||||
pub(super) struct InProgressGuard {
|
||||
in_progress: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
impl Drop for InProgressGuard {
|
||||
fn drop(&mut self) {
|
||||
self.in_progress.store(false, Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub(super) struct RefillTask {
|
||||
// note that we can only have a single transaction in progress (or it'd mess up with our sequence numbers)
|
||||
@@ -16,7 +26,7 @@ pub(super) struct RefillTask {
|
||||
// we'll have to increase the number of deposits per transaction
|
||||
join_handle: StdMutex<Option<JoinHandle<RefillTaskResult>>>,
|
||||
|
||||
in_progress: AtomicBool,
|
||||
in_progress: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
impl RefillTask {
|
||||
@@ -28,9 +38,15 @@ impl RefillTask {
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
/// Returns `None` if a refill is already in progress. On success, returns the
|
||||
/// join-handle guard (to store the new `JoinHandle` into) and an [`InProgressGuard`]
|
||||
/// that **must be moved into the spawned task** — it resets the flag when dropped.
|
||||
pub(super) fn try_get_new_task_guard(
|
||||
&self,
|
||||
) -> Option<MutexGuard<'_, Option<JoinHandle<RefillTaskResult>>>> {
|
||||
) -> Option<(
|
||||
MutexGuard<'_, Option<JoinHandle<RefillTaskResult>>>,
|
||||
InProgressGuard,
|
||||
)> {
|
||||
// sanity check for concurrent request
|
||||
if !self.try_set_in_progress() {
|
||||
debug!("another task has already started deposit refill request");
|
||||
@@ -48,7 +64,11 @@ impl RefillTask {
|
||||
}
|
||||
}
|
||||
|
||||
Some(guard)
|
||||
let completion_guard = InProgressGuard {
|
||||
in_progress: Arc::clone(&self.in_progress),
|
||||
};
|
||||
|
||||
Some((guard, completion_guard))
|
||||
}
|
||||
|
||||
pub(super) fn take_task_join_handle(&self) -> Option<JoinHandle<RefillTaskResult>> {
|
||||
@@ -56,3 +76,34 @@ impl RefillTask {
|
||||
self.join_handle.lock().expect("mutex got poisoned").take()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn in_progress_resets_after_guard_drop() {
|
||||
let task = RefillTask::default();
|
||||
|
||||
let (guard, completion_guard) = task.try_get_new_task_guard().unwrap();
|
||||
drop(guard);
|
||||
assert!(task.try_get_new_task_guard().is_none());
|
||||
|
||||
drop(completion_guard);
|
||||
assert!(task.try_get_new_task_guard().is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn in_progress_resets_on_panic() {
|
||||
let task = RefillTask::default();
|
||||
|
||||
let (guard, completion_guard) = task.try_get_new_task_guard().unwrap();
|
||||
drop(guard);
|
||||
|
||||
let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||
let _g = completion_guard;
|
||||
panic!("simulated refill task panic");
|
||||
}));
|
||||
assert!(task.try_get_new_task_guard().is_some());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,9 @@ license = { workspace = true }
|
||||
repository = { workspace = true }
|
||||
homepage.workspace = true
|
||||
documentation.workspace = true
|
||||
rust-version.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"
|
||||
readme.workspace = true
|
||||
publish = true
|
||||
|
||||
|
||||
@@ -1,7 +1,21 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use super::PublicKey;
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
pub mod bs58_x25519_pubkey {
|
||||
use super::*;
|
||||
|
||||
@@ -36,6 +36,7 @@ thiserror = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
itertools = { workspace = true }
|
||||
inventory = { workspace = true }
|
||||
fastrand = { workspace = true }
|
||||
tokio = { workspace = true, features = ["rt", "macros", "time"] }
|
||||
rustls = { workspace=true }
|
||||
# used for decoding text responses (they were already implicitly included)
|
||||
|
||||
@@ -55,9 +55,8 @@ use std::{
|
||||
|
||||
use hickory_resolver::{
|
||||
TokioResolver,
|
||||
config::{NameServerConfig, NameServerConfigGroup, ResolverConfig, ResolverOpts},
|
||||
lookup_ip::LookupIpIntoIter,
|
||||
name_server::TokioConnectionProvider,
|
||||
config::{CLOUDFLARE, NameServerConfig, QUAD9, ResolverConfig, ResolverOpts},
|
||||
net::{NetError, runtime::TokioRuntimeProvider},
|
||||
};
|
||||
use once_cell::sync::OnceCell;
|
||||
use reqwest::dns::{Addrs, Name, Resolve, Resolving};
|
||||
@@ -113,7 +112,7 @@ pub enum ResolveError {
|
||||
#[error("invalid name: {0}")]
|
||||
InvalidNameError(String),
|
||||
#[error("hickory-dns resolver error: {0}")]
|
||||
ResolveError(#[from] hickory_resolver::ResolveError),
|
||||
ResolveError(#[from] NetError),
|
||||
#[error("high level lookup timed out")]
|
||||
Timeout,
|
||||
#[error("hostname not found in static lookup table")]
|
||||
@@ -123,7 +122,10 @@ pub enum ResolveError {
|
||||
impl ResolveError {
|
||||
/// Returns true if the error is a timeout.
|
||||
pub fn is_timeout(&self) -> bool {
|
||||
matches!(self, ResolveError::Timeout)
|
||||
matches!(
|
||||
self,
|
||||
ResolveError::Timeout | ResolveError::ResolveError(NetError::Timeout)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,18 +169,17 @@ impl Resolve for HickoryDnsResolver {
|
||||
fn resolve(&self, name: Name) -> Resolving {
|
||||
let use_system = self.use_system.load(std::sync::atomic::Ordering::Relaxed);
|
||||
let use_shared = self.use_shared;
|
||||
let resolver = if use_system {
|
||||
match self
|
||||
.system_resolver
|
||||
let result = if use_system {
|
||||
self.system_resolver
|
||||
.get_or_try_init(|| HickoryDnsResolver::new_resolver_system(use_shared))
|
||||
{
|
||||
Ok(r) => r.clone(),
|
||||
Err(e) => return Box::pin(return_err(e)),
|
||||
}
|
||||
} else {
|
||||
self.state
|
||||
.get_or_init(|| HickoryDnsResolver::new_resolver(use_shared))
|
||||
.clone()
|
||||
.get_or_try_init(|| HickoryDnsResolver::new_resolver(use_shared))
|
||||
};
|
||||
|
||||
let resolver = match result {
|
||||
Ok(r) => r.clone(),
|
||||
Err(err) => return Box::pin(return_err(err)),
|
||||
};
|
||||
|
||||
let maybe_static = self.static_base.clone();
|
||||
@@ -227,9 +228,11 @@ async fn resolve(
|
||||
let primary_err = match resolve_fut.await {
|
||||
Err(_) => ResolveError::Timeout,
|
||||
Ok(Ok(lookup)) => {
|
||||
let addrs: Addrs = Box::new(SocketAddrs {
|
||||
iter: lookup.into_iter(),
|
||||
});
|
||||
// Shuffle so that successive connection attempts cycle through all
|
||||
// returned IPs rather than always hitting the same first address.
|
||||
let mut ips = Vec::from_iter(lookup.iter());
|
||||
fastrand::shuffle(&mut ips);
|
||||
let addrs: Addrs = Box::new(ips.into_iter().map(|ip| SocketAddr::new(ip, 0)));
|
||||
return Ok(addrs);
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
@@ -256,18 +259,6 @@ async fn resolve(
|
||||
Err(primary_err)
|
||||
}
|
||||
|
||||
struct SocketAddrs {
|
||||
iter: LookupIpIntoIter,
|
||||
}
|
||||
|
||||
impl Iterator for SocketAddrs {
|
||||
type Item = SocketAddr;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
self.iter.next().map(|ip_addr| SocketAddr::new(ip_addr, 0))
|
||||
}
|
||||
}
|
||||
|
||||
impl HickoryDnsResolver {
|
||||
/// Returns an instance of the shared resolver.
|
||||
pub fn shared() -> Self {
|
||||
@@ -288,7 +279,7 @@ impl HickoryDnsResolver {
|
||||
.clone()
|
||||
} else {
|
||||
self.state
|
||||
.get_or_init(|| HickoryDnsResolver::new_resolver(self.use_shared))
|
||||
.get_or_try_init(|| HickoryDnsResolver::new_resolver(self.use_shared))?
|
||||
.clone()
|
||||
};
|
||||
|
||||
@@ -311,11 +302,11 @@ impl HickoryDnsResolver {
|
||||
}
|
||||
}
|
||||
|
||||
fn new_resolver(use_shared: bool) -> TokioResolver {
|
||||
fn new_resolver(use_shared: bool) -> Result<TokioResolver, ResolveError> {
|
||||
// using a closure here is slightly gross, but this makes sure that if the
|
||||
// lazy-init returns an error it can be handled by the client
|
||||
if use_shared {
|
||||
SHARED_RESOLVER.state.get_or_init(new_resolver).clone()
|
||||
SHARED_RESOLVER.state.get_or_try_init(new_resolver).cloned()
|
||||
} else {
|
||||
new_resolver()
|
||||
}
|
||||
@@ -367,7 +358,7 @@ impl HickoryDnsResolver {
|
||||
/// Clear entries from the static table that would return entries during the pre-resolve stage.
|
||||
/// This means that all lookups will attempt to use the network resolver again before the static
|
||||
/// table is consulted.
|
||||
///
|
||||
///
|
||||
/// Entries elevated to pre-resolve from fallback (added from default or using
|
||||
/// [`set_fallback`]`) will have their cache timeout cleared. Entries added directly to
|
||||
/// pre-resolve (using [`Self::set_static_preresolve`]) will be removed.
|
||||
@@ -438,20 +429,7 @@ impl HickoryDnsResolver {
|
||||
|
||||
/// Get the list of currently available nameserver configs.
|
||||
pub fn all_configured_name_servers(&self) -> Vec<NameServerConfig> {
|
||||
default_nameserver_group().to_vec()
|
||||
}
|
||||
|
||||
/// Get the list of currently used nameserver configs.
|
||||
pub fn active_name_servers(&self) -> Vec<NameServerConfig> {
|
||||
if !self.use_shared {
|
||||
return self
|
||||
.state
|
||||
.get()
|
||||
.map(|r| r.config().name_servers().to_vec())
|
||||
.unwrap_or(self.all_configured_name_servers());
|
||||
}
|
||||
|
||||
SHARED_RESOLVER.active_name_servers()
|
||||
default_nameserver_group()
|
||||
}
|
||||
|
||||
/// Do a trial resolution using each nameserver individually to test which are working and which
|
||||
@@ -477,65 +455,60 @@ impl HickoryDnsResolver {
|
||||
///
|
||||
/// Caches successfully resolved addresses for 30 minutes to prevent continual use of remote lookup.
|
||||
/// This resolver is intended to be used for OUR API endpoints that do not rapidly rotate IPs.
|
||||
fn new_resolver() -> TokioResolver {
|
||||
fn new_resolver() -> Result<TokioResolver, ResolveError> {
|
||||
let name_servers = default_nameserver_group_ipv4_only();
|
||||
|
||||
configure_and_build_resolver(name_servers)
|
||||
}
|
||||
|
||||
fn configure_and_build_resolver<G>(name_servers: G) -> TokioResolver
|
||||
where
|
||||
G: Into<NameServerConfigGroup>,
|
||||
{
|
||||
fn configure_and_build_resolver(
|
||||
name_servers: Vec<NameServerConfig>,
|
||||
) -> Result<TokioResolver, ResolveError> {
|
||||
let options = HickoryDnsResolver::default_options();
|
||||
let name_servers: NameServerConfigGroup = name_servers.into();
|
||||
info!("building new configured resolver");
|
||||
debug!("configuring resolver with {options:?}, {name_servers:?}");
|
||||
|
||||
let config = ResolverConfig::from_parts(None, Vec::new(), name_servers);
|
||||
let mut resolver_builder =
|
||||
TokioResolver::builder_with_config(config, TokioConnectionProvider::default());
|
||||
TokioResolver::builder_with_config(config, TokioRuntimeProvider::default());
|
||||
|
||||
resolver_builder = resolver_builder.with_options(options);
|
||||
|
||||
resolver_builder.build()
|
||||
Ok(resolver_builder.build()?)
|
||||
}
|
||||
|
||||
fn filter_ipv4(nameservers: impl AsRef<[NameServerConfig]>) -> Vec<NameServerConfig> {
|
||||
fn filter_ipv4(nameservers: impl IntoIterator<Item = NameServerConfig>) -> Vec<NameServerConfig> {
|
||||
nameservers
|
||||
.as_ref()
|
||||
.iter()
|
||||
.filter(|ns| ns.socket_addr.is_ipv4())
|
||||
.cloned()
|
||||
.into_iter()
|
||||
.filter(|ns| ns.ip.is_ipv4())
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
fn filter_ipv6(nameservers: impl AsRef<[NameServerConfig]>) -> Vec<NameServerConfig> {
|
||||
fn filter_ipv6(nameservers: impl IntoIterator<Item = NameServerConfig>) -> Vec<NameServerConfig> {
|
||||
nameservers
|
||||
.as_ref()
|
||||
.iter()
|
||||
.filter(|ns| ns.socket_addr.is_ipv6())
|
||||
.cloned()
|
||||
.into_iter()
|
||||
.filter(|ns| ns.ip.is_ipv6())
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
fn default_nameserver_group() -> NameServerConfigGroup {
|
||||
let mut name_servers = NameServerConfigGroup::quad9_tls();
|
||||
name_servers.merge(NameServerConfigGroup::quad9_https());
|
||||
name_servers.merge(NameServerConfigGroup::cloudflare_tls());
|
||||
name_servers.merge(NameServerConfigGroup::cloudflare_https());
|
||||
name_servers
|
||||
fn default_nameserver_group() -> Vec<NameServerConfig> {
|
||||
QUAD9
|
||||
.tls()
|
||||
.chain(QUAD9.https())
|
||||
.chain(CLOUDFLARE.tls())
|
||||
.chain(CLOUDFLARE.https())
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn default_nameserver_group_ipv4_only() -> NameServerConfigGroup {
|
||||
filter_ipv4(&default_nameserver_group() as &[NameServerConfig]).into()
|
||||
fn default_nameserver_group_ipv4_only() -> Vec<NameServerConfig> {
|
||||
filter_ipv4(default_nameserver_group())
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
fn default_nameserver_group_ipv6_only() -> NameServerConfigGroup {
|
||||
filter_ipv6(&default_nameserver_group() as &[NameServerConfig]).into()
|
||||
fn default_nameserver_group_ipv6_only() -> Vec<NameServerConfig> {
|
||||
filter_ipv6(default_nameserver_group())
|
||||
}
|
||||
|
||||
/// Create a new resolver with the default configuration, which reads from the system DNS config
|
||||
@@ -550,7 +523,7 @@ fn new_resolver_system() -> Result<TokioResolver, ResolveError> {
|
||||
|
||||
resolver_builder = resolver_builder.with_options(options);
|
||||
|
||||
Ok(resolver_builder.build())
|
||||
Ok(resolver_builder.build()?)
|
||||
}
|
||||
|
||||
fn new_default_static_fallback() -> StaticResolver {
|
||||
@@ -577,7 +550,7 @@ async fn trial_nameservers_inner(
|
||||
async fn trial_lookup(name_server: NameServerConfig, query: &str) -> Result<(), ResolveError> {
|
||||
debug!("running ns trial {name_server:?} query={query}");
|
||||
|
||||
let resolver = configure_and_build_resolver(vec![name_server]);
|
||||
let resolver = configure_and_build_resolver(vec![name_server])?;
|
||||
|
||||
match tokio::time::timeout(DEFAULT_OVERALL_LOOKUP_TIMEOUT, resolver.ipv4_lookup(query)).await {
|
||||
Ok(Ok(_)) => Ok(()),
|
||||
@@ -590,8 +563,10 @@ async fn trial_lookup(name_server: NameServerConfig, query: &str) -> Result<(),
|
||||
mod test {
|
||||
use super::*;
|
||||
use itertools::Itertools;
|
||||
use std::collections::HashMap;
|
||||
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
net::{IpAddr, Ipv4Addr, Ipv6Addr},
|
||||
};
|
||||
|
||||
/// IP addresses guaranteed to fail attempts to resolve
|
||||
///
|
||||
@@ -670,26 +645,16 @@ mod test {
|
||||
let mut ns_ips = GUARANTEED_BROKEN_IPS_1.to_vec();
|
||||
ns_ips.push(good_cf_ip);
|
||||
|
||||
let broken_ns_https = NameServerConfigGroup::from_ips_https(
|
||||
&ns_ips,
|
||||
443,
|
||||
"cloudflare-dns.com".to_string(),
|
||||
true,
|
||||
);
|
||||
let domain = Arc::<str>::from("cloudflare-dns.com");
|
||||
let path = Arc::<str>::from("/dns-query");
|
||||
let broken_ns_https = GUARANTEED_BROKEN_IPS_1
|
||||
.iter()
|
||||
.chain([&good_cf_ip])
|
||||
.map(|ip| NameServerConfig::https(*ip, domain.clone(), Some(path.clone())))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let inner = configure_and_build_resolver(broken_ns_https);
|
||||
|
||||
// create a new resolver that won't mess with the shared resolver used by other tests
|
||||
let resolver = HickoryDnsResolver {
|
||||
use_shared: false,
|
||||
state: Arc::new(OnceCell::with_value(inner)),
|
||||
static_base: Some(Default::default()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let name_servers = resolver.state.get().unwrap().config().name_servers();
|
||||
for (ns, result) in trial_nameservers_inner(name_servers).await {
|
||||
if ns.socket_addr.ip() == good_cf_ip {
|
||||
for (ns, result) in trial_nameservers_inner(&broken_ns_https).await {
|
||||
if ns.ip == good_cf_ip {
|
||||
assert!(result.is_ok())
|
||||
} else {
|
||||
assert!(result.is_err())
|
||||
@@ -705,21 +670,20 @@ mod test {
|
||||
fn build_broken_resolver() -> Result<TokioResolver, ResolveError> {
|
||||
info!("building new faulty resolver");
|
||||
|
||||
let mut broken_ns_group = NameServerConfigGroup::from_ips_tls(
|
||||
GUARANTEED_BROKEN_IPS_1,
|
||||
853,
|
||||
"cloudflare-dns.com".to_string(),
|
||||
true,
|
||||
);
|
||||
let broken_ns_https = NameServerConfigGroup::from_ips_https(
|
||||
GUARANTEED_BROKEN_IPS_1,
|
||||
443,
|
||||
"cloudflare-dns.com".to_string(),
|
||||
true,
|
||||
);
|
||||
broken_ns_group.merge(broken_ns_https);
|
||||
let domain = Arc::<str>::from("cloudflare-dns.com");
|
||||
let path = Arc::<str>::from("/dns-query");
|
||||
let broken_ns_group = GUARANTEED_BROKEN_IPS_1
|
||||
.iter()
|
||||
.map(|ip| NameServerConfig::tls(*ip, domain.clone()))
|
||||
.chain(
|
||||
GUARANTEED_BROKEN_IPS_1
|
||||
.iter()
|
||||
.map(|ip| NameServerConfig::https(*ip, domain.clone(), Some(path.clone())))
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Ok(configure_and_build_resolver(broken_ns_group))
|
||||
configure_and_build_resolver(broken_ns_group)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -740,7 +704,7 @@ mod test {
|
||||
build_broken_resolver()?;
|
||||
let domain = "ifconfig.me";
|
||||
let result = resolver.resolve_str(domain).await;
|
||||
assert!(result.is_err_and(|e| matches!(e, ResolveError::Timeout)));
|
||||
assert!(result.is_err_and(|e| e.is_timeout()));
|
||||
|
||||
let duration = time_start.elapsed();
|
||||
assert!(duration < resolver.overall_dns_timeout + Duration::from_secs(1));
|
||||
@@ -774,25 +738,11 @@ mod test {
|
||||
// unsuccessful lookup - primary times out, and not in static table
|
||||
let domain = "non-existent.nymtech.net";
|
||||
let result = resolver.resolve_str(domain).await;
|
||||
assert!(result.is_err_and(|e| matches!(e, ResolveError::Timeout)));
|
||||
assert!(result.is_err_and(|e| e.is_timeout()));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_resolver_uses_ipv4_only_nameservers() {
|
||||
let resolver = HickoryDnsResolver::thread_resolver();
|
||||
resolver
|
||||
.active_name_servers()
|
||||
.iter()
|
||||
.all(|cfg| cfg.socket_addr.is_ipv4());
|
||||
|
||||
SHARED_RESOLVER
|
||||
.active_name_servers()
|
||||
.iter()
|
||||
.all(|cfg| cfg.socket_addr.is_ipv4());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[cfg(any())] // #[ignore] we run --ignore in CI/CD assuming it just means slow -_-
|
||||
// This test impacts the state of the shared resolver and as such is disabled to avoid
|
||||
|
||||
@@ -141,9 +141,7 @@
|
||||
|
||||
use http::header::USER_AGENT;
|
||||
pub use inventory;
|
||||
pub use reqwest;
|
||||
pub use reqwest::ClientBuilder as ReqwestClientBuilder;
|
||||
pub use reqwest::StatusCode;
|
||||
pub use reqwest::{self, ClientBuilder as ReqwestClientBuilder, StatusCode};
|
||||
use std::error::Error;
|
||||
|
||||
pub mod registry;
|
||||
@@ -152,19 +150,21 @@ use crate::path::RequestPath;
|
||||
use async_trait::async_trait;
|
||||
use bytes::Bytes;
|
||||
use cfg_if::cfg_if;
|
||||
use http::HeaderMap;
|
||||
use http::header::{ACCEPT, CONTENT_TYPE};
|
||||
use http::{
|
||||
HeaderMap,
|
||||
header::{ACCEPT, CONTENT_TYPE},
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use mime::Mime;
|
||||
use reqwest::header::HeaderValue;
|
||||
use reqwest::{RequestBuilder, Response};
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::Display;
|
||||
use reqwest::{RequestBuilder, Response, header::HeaderValue};
|
||||
use serde::{Deserialize, Serialize, de::DeserializeOwned};
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use std::io::ErrorKind;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::time::Duration;
|
||||
use std::{
|
||||
fmt::Display,
|
||||
sync::atomic::{AtomicUsize, Ordering},
|
||||
time::Duration,
|
||||
};
|
||||
use thiserror::Error;
|
||||
use tracing::{debug, instrument, warn};
|
||||
|
||||
@@ -1152,7 +1152,10 @@ impl ApiClientCore for Client {
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let response: Result<Response, HttpClientError> = {
|
||||
let client = self.reqwest_client.as_ref().unwrap_or(&*SHARED_CLIENT);
|
||||
let client = self
|
||||
.reqwest_client
|
||||
.as_ref()
|
||||
.unwrap_or_else(|| &*SHARED_CLIENT);
|
||||
Ok(
|
||||
wasmtimer::tokio::timeout(self.request_timeout, client.execute(req))
|
||||
.await
|
||||
@@ -1162,12 +1165,24 @@ impl ApiClientCore for Client {
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let response = {
|
||||
let client = self.reqwest_client.as_ref().unwrap_or(&*SHARED_CLIENT);
|
||||
let client = self
|
||||
.reqwest_client
|
||||
.as_ref()
|
||||
.unwrap_or_else(|| &*SHARED_CLIENT);
|
||||
client.execute(req).await
|
||||
};
|
||||
|
||||
match response {
|
||||
Ok(resp) => return Ok(resp),
|
||||
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);
|
||||
}
|
||||
Err(err) => {
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let is_network_err = err.is_timeout();
|
||||
@@ -1220,17 +1235,39 @@ 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 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.
|
||||
/// 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.
|
||||
///
|
||||
/// 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() {
|
||||
@@ -1268,7 +1305,7 @@ pub(crate) fn might_be_network_interference(err: &reqwest::Error) -> bool {
|
||||
} else if let Some(_tls_err) = e.downcast_ref::<rustls::Error>() {
|
||||
// try downcast to TLS error
|
||||
return true;
|
||||
} else if let Some(resolve_err) = e.downcast_ref::<hickory_resolver::ResolveError>() {
|
||||
} else if let Some(resolve_err) = e.downcast_ref::<hickory_resolver::net::NetError>() {
|
||||
// try downcast to DNS error
|
||||
return resolve_err.is_nx_domain();
|
||||
} else {
|
||||
@@ -1691,6 +1728,13 @@ 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 {
|
||||
|
||||
@@ -129,6 +129,41 @@ 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")]
|
||||
|
||||
@@ -12,6 +12,30 @@ pub mod v7;
|
||||
pub mod v8;
|
||||
pub mod v9;
|
||||
|
||||
/// Highest IPR protocol version that is allowed to be sent as a **non-stream** mixnet payload
|
||||
/// (i.e. not wrapped in `LpFrameKind::SphinxStream`).
|
||||
pub const MAX_NON_STREAM_VERSION: u8 = v8::VERSION;
|
||||
|
||||
/// First IPR protocol version that **requires** the SphinxStream (LP) transport for non-stream
|
||||
/// mixnet sends, matching the node-side enforcement in `ip-packet-router`.
|
||||
pub const SPHINX_STREAM_VERSION_THRESHOLD: u8 = v9::VERSION;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
const _: () = {
|
||||
assert!(SPHINX_STREAM_VERSION_THRESHOLD > MAX_NON_STREAM_VERSION);
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn stream_transport_threshold_is_consistent() {
|
||||
assert_eq!(MAX_NON_STREAM_VERSION, 8);
|
||||
assert_eq!(SPHINX_STREAM_VERSION_THRESHOLD, 9);
|
||||
const _: () = assert!(SPHINX_STREAM_VERSION_THRESHOLD > MAX_NON_STREAM_VERSION);
|
||||
}
|
||||
}
|
||||
|
||||
// version 3: initial version
|
||||
// version 4: IPv6 support
|
||||
// version 5: Add severity level to info response
|
||||
|
||||
@@ -8,7 +8,9 @@ license.workspace = true
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
documentation.workspace = true
|
||||
rust-version.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"
|
||||
readme.workspace = true
|
||||
publish = true
|
||||
# Exclude build.rs from published crate - it's only used for dev-time sync
|
||||
|
||||
@@ -22,6 +22,10 @@ pub const VESTING_CONTRACT_ADDRESS: &str =
|
||||
pub const PERFORMANCE_CONTRACT_ADDRESS: &str = "";
|
||||
// /\ TODO: this has to be updated once the contract is deployed
|
||||
|
||||
// \/ TODO: this has to be updated once the contract is deployed
|
||||
pub const NODE_FAMILIES_CONTRACT_ADDRESS: &str = "";
|
||||
// /\ TODO: this has to be updated once the contract is deployed
|
||||
|
||||
pub const ECASH_CONTRACT_ADDRESS: &str =
|
||||
"n1r7s6aksyc6pqardx88k3rkgfagwvj4z4zum9mmz2sfk3zm2mha0sd4dnun";
|
||||
pub const GROUP_CONTRACT_ADDRESS: &str =
|
||||
@@ -43,10 +47,6 @@ pub const NYM_APIS: &[ApiUrlConst] = &[
|
||||
url: NYM_API,
|
||||
front_hosts: None,
|
||||
},
|
||||
ApiUrlConst {
|
||||
url: "https://nym-frontdoor.vercel.app/api/",
|
||||
front_hosts: Some(&["vercel.app", "vercel.com"]),
|
||||
},
|
||||
ApiUrlConst {
|
||||
url: "https://nym-frontdoor.global.ssl.fastly.net/api/",
|
||||
front_hosts: Some(&["yelp.global.ssl.fastly.net"]),
|
||||
@@ -68,7 +68,7 @@ pub const UPGRADE_MODE_ATTESTER_ED25519_BS58_PUBKEY: &str =
|
||||
pub const NYM_VPN_APIS: &[ApiUrlConst] = &[
|
||||
ApiUrlConst {
|
||||
url: NYM_VPN_API,
|
||||
front_hosts: Some(&["vercel.app", "vercel.com"]),
|
||||
front_hosts: None,
|
||||
},
|
||||
ApiUrlConst {
|
||||
url: "https://nymvpn-frontdoor.global.ssl.fastly.net/api/",
|
||||
|
||||
@@ -39,6 +39,8 @@ pub struct NymContracts {
|
||||
pub vesting_contract_address: Option<String>,
|
||||
#[serde(default)]
|
||||
pub performance_contract_address: Option<String>,
|
||||
#[serde(default)]
|
||||
pub node_families_contract_address: Option<String>,
|
||||
pub ecash_contract_address: Option<String>,
|
||||
pub group_contract_address: Option<String>,
|
||||
pub multisig_contract_address: Option<String>,
|
||||
@@ -174,6 +176,9 @@ impl NymNetworkDetails {
|
||||
))
|
||||
.with_mixnet_contract(get_optional_env(var_names::MIXNET_CONTRACT_ADDRESS))
|
||||
.with_vesting_contract(get_optional_env(var_names::VESTING_CONTRACT_ADDRESS))
|
||||
.with_node_families_contract(get_optional_env(
|
||||
var_names::NODE_FAMILIES_CONTRACT_ADDRESS,
|
||||
))
|
||||
.with_ecash_contract(get_optional_env(var_names::ECASH_CONTRACT_ADDRESS))
|
||||
.with_group_contract(get_optional_env(var_names::GROUP_CONTRACT_ADDRESS))
|
||||
.with_multisig_contract(get_optional_env(var_names::MULTISIG_CONTRACT_ADDRESS))
|
||||
@@ -199,6 +204,9 @@ impl NymNetworkDetails {
|
||||
performance_contract_address: parse_optional_str(
|
||||
mainnet::PERFORMANCE_CONTRACT_ADDRESS,
|
||||
),
|
||||
node_families_contract_address: parse_optional_str(
|
||||
mainnet::NODE_FAMILIES_CONTRACT_ADDRESS,
|
||||
),
|
||||
ecash_contract_address: parse_optional_str(mainnet::ECASH_CONTRACT_ADDRESS),
|
||||
group_contract_address: parse_optional_str(mainnet::GROUP_CONTRACT_ADDRESS),
|
||||
multisig_contract_address: parse_optional_str(mainnet::MULTISIG_CONTRACT_ADDRESS),
|
||||
@@ -252,6 +260,7 @@ impl NymNetworkDetails {
|
||||
|
||||
set_optional_var(var_names::MIXNET_CONTRACT_ADDRESS, self.contracts.mixnet_contract_address);
|
||||
set_optional_var(var_names::VESTING_CONTRACT_ADDRESS, self.contracts.vesting_contract_address);
|
||||
set_optional_var(var_names::NODE_FAMILIES_CONTRACT_ADDRESS, self.contracts.node_families_contract_address);
|
||||
set_optional_var(var_names::ECASH_CONTRACT_ADDRESS, self.contracts.ecash_contract_address);
|
||||
set_optional_var(var_names::GROUP_CONTRACT_ADDRESS, self.contracts.group_contract_address);
|
||||
set_optional_var(var_names::MULTISIG_CONTRACT_ADDRESS, self.contracts.multisig_contract_address);
|
||||
@@ -340,6 +349,12 @@ impl NymNetworkDetails {
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_node_families_contract<S: Into<String>>(mut self, contract: Option<S>) -> Self {
|
||||
self.contracts.node_families_contract_address = contract.map(Into::into);
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_ecash_contract<S: Into<String>>(mut self, contract: Option<S>) -> Self {
|
||||
self.contracts.ecash_contract_address = contract.map(Into::into);
|
||||
|
||||
@@ -17,6 +17,7 @@ pub const VESTING_CONTRACT_ADDRESS: &str = "VESTING_CONTRACT_ADDRESS";
|
||||
pub const ECASH_CONTRACT_ADDRESS: &str = "ECASH_CONTRACT_ADDRESS";
|
||||
pub const GROUP_CONTRACT_ADDRESS: &str = "GROUP_CONTRACT_ADDRESS";
|
||||
pub const MULTISIG_CONTRACT_ADDRESS: &str = "MULTISIG_CONTRACT_ADDRESS";
|
||||
pub const NODE_FAMILIES_CONTRACT_ADDRESS: &str = "NODE_FAMILIES_CONTRACT_ADDRESS";
|
||||
pub const COCONUT_DKG_CONTRACT_ADDRESS: &str = "COCONUT_DKG_CONTRACT_ADDRESS";
|
||||
pub const REWARDING_VALIDATOR_ADDRESS: &str = "REWARDING_VALIDATOR_ADDRESS";
|
||||
pub const NYXD: &str = "NYXD";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "nym-kkt"
|
||||
description = "Key transport protocol for the Nym network"
|
||||
version = "0.1.0"
|
||||
version = "1.21.0"
|
||||
authors = ["Georgio Nicolas <georgio@nymtech.net>"]
|
||||
edition = { workspace = true }
|
||||
license.workspace = true
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
[package]
|
||||
name = "nym-lp-data"
|
||||
description = "Lewes Protocol data structure for the Nym network"
|
||||
version.workspace = true
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
documentation.workspace = true
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
bytes.workspace = true
|
||||
num_enum.workspace = true
|
||||
tracing.workspace = true
|
||||
thiserror.workspace = true
|
||||
|
||||
nym-common.workspace = true
|
||||
|
||||
|
||||
[dev-dependencies]
|
||||
nym-lp.workspace = true
|
||||
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -0,0 +1,103 @@
|
||||
# nym-lp-data
|
||||
|
||||
Trait definitions and data structures for Lewes Protocol (LP) processing pipelines in the Nym mixnet.
|
||||
|
||||
This crate is a *vocabulary* crate — it defines the traits that clients and mix nodes implement to compose a packet-processing pipeline, plus a few generic data wrappers (`TimedData`, `AddressedTimedData`, `PipelineData`) that thread per-packet state through every stage. It contains no concrete cryptography, transport, or network code. A concrete implementation live in [`nym-mix-sim`](../../nym-mix-sim).
|
||||
|
||||
## Crate layout
|
||||
|
||||
| Module | Purpose |
|
||||
|--------|---------|
|
||||
| [`common`](src/common) | Wire-layer traits ([`Framing`], [`FramingUnwrap`], [`Transport`], [`TransportUnwrap`]) and their composed supertraits ([`WireWrappingPipeline`], [`WireUnwrappingPipeline`]) shared by both clients and mixnodes, plus [`NoOpWireWrapper`] / [`NoOpWireUnwrapper`] marker traits for opting into a pass-through wire layer |
|
||||
| [`clients`](src/clients) | Client-side outbound/inbound pipeline traits: [`Chunking`], [`Reliability`], [`Obfuscation`], [`RoutingSecurity`], plus the supertraits [`ClientWrappingPipeline`] / [`ClientUnwrappingPipeline`], a `Pipeline` composition struct, no-op marker traits, and a tick-driven [`ClientWrappingPipelineDriver`] |
|
||||
| [`mixnodes`](src/mixnodes) | Mixnode processing trait [`NymNodeProcessingPipeline`] (unwrap → mix → re-wrap) and a `Pipeline` composition struct |
|
||||
|
||||
[`Framing`]: src/common/traits.rs
|
||||
[`FramingUnwrap`]: src/common/traits.rs
|
||||
[`Transport`]: src/common/traits.rs
|
||||
[`TransportUnwrap`]: src/common/traits.rs
|
||||
[`WireWrappingPipeline`]: src/common/traits.rs
|
||||
[`WireUnwrappingPipeline`]: src/common/traits.rs
|
||||
[`NoOpWireWrapper`]: src/common/helpers.rs
|
||||
[`NoOpWireUnwrapper`]: src/common/helpers.rs
|
||||
[`Chunking`]: src/clients/traits.rs
|
||||
[`Reliability`]: src/clients/traits.rs
|
||||
[`Obfuscation`]: src/clients/traits.rs
|
||||
[`RoutingSecurity`]: src/clients/traits.rs
|
||||
[`ClientWrappingPipeline`]: src/clients/traits.rs
|
||||
[`ClientUnwrappingPipeline`]: src/clients/traits.rs
|
||||
[`ClientWrappingPipelineDriver`]: src/clients/driver.rs
|
||||
[`NymNodeProcessingPipeline`]: src/mixnodes/traits.rs
|
||||
|
||||
## Core data types
|
||||
|
||||
```text
|
||||
TimedData<Ts, D> ── pairs a value of type D with a timestamp Ts
|
||||
TimedPayload<Ts> ── alias for TimedData<Ts, Vec<u8>>
|
||||
|
||||
AddressedTimedData<Ts, D, NdId> ── TimedData plus a destination address
|
||||
AddressedTimedPayload<Ts, NdId> ── alias for AddressedTimedData<Ts, Vec<u8>, NdId>
|
||||
|
||||
PipelineData<Ts, D, Opts, NdId> ── TimedData plus per-message Opts
|
||||
(used inside the client wrapping pipeline)
|
||||
PipelinePayload<Ts, Opts, NdId> ── alias for PipelineData<Ts, Vec<u8>, Opts, NdId>
|
||||
```
|
||||
|
||||
`Ts` is the timestamp / tick-context type, `NdId` is the next-hop identifier type, and `Opts` is an [`InputOptions`](src/clients/mod.rs)-implementing per-message marker that toggles which optional pipeline stages run for a given payload (reliability, obfuscation, routing security).
|
||||
|
||||
## Client wrapping pipeline
|
||||
|
||||
The outbound client pipeline composes six stages, each represented by its own trait:
|
||||
|
||||
```text
|
||||
Vec<u8> ──▶ Chunking ──▶ Reliability ──▶ Obfuscation
|
||||
│
|
||||
▼
|
||||
AddressedTimedData<Ts, Pkt, NdId> ◀── Transport ◀── Framing ◀── RoutingSecurity
|
||||
```
|
||||
|
||||
[`ClientWrappingPipeline`] is the supertrait that ties them together and provides a default `process()` method which runs all six stages in order on every tick. Each stage is opt-in per message via the active [`InputOptions`].
|
||||
|
||||
### Pipeline tick semantics
|
||||
|
||||
`process()` is intended to be called on every tick (with or without an input payload):
|
||||
|
||||
- [`Reliability::reliable_encode`] is always called once with `Some(input)` (when present), then once more with `None` so that timer-driven retransmissions can fire even when no new payload arrived.
|
||||
- [`Obfuscation::obfuscate`] follows the same pattern — once with the real input and once with `None` so that cover-traffic loops can fire on idle ticks.
|
||||
- [`Chunking`] and [`RoutingSecurity`] only run when a payload is actually present.
|
||||
|
||||
This convention is what allows pipelines to support Poisson cover traffic and SURB-ACK retransmission without the caller having to know whether anything is in flight.
|
||||
|
||||
## Mixnode processing pipeline
|
||||
|
||||
The mixnode pipeline is simpler — three stages that consume a packet and emit zero or more re-wrapped output packets:
|
||||
|
||||
```text
|
||||
Pkt ──▶ WireUnwrappingPipeline ──▶ mix ──▶ WireWrappingPipeline ──▶ Vec<AddressedTimedData<Ts, Pkt, NdId>>
|
||||
(TransportUnwrap + ▲ (Framing + Transport)
|
||||
FramingUnwrap) │
|
||||
└── implementor decrypts, routes,
|
||||
schedules delays, etc.
|
||||
```
|
||||
|
||||
Implementors fill in `mix()`; everything else is provided by the [`NymNodeProcessingPipeline`] supertrait's default `process()`.
|
||||
|
||||
## Helpers
|
||||
|
||||
- **Client-stage no-op marker traits** ([`NoOpReliability`], [`NoOpRoutingSecurity`], [`NoOpObfuscation`] in [`clients/helpers.rs`](src/clients/helpers.rs)) — implement these to opt out of a pipeline stage with zero overhead. Useful for stub or testing pipelines.
|
||||
- **Wire-layer no-op marker traits** ([`NoOpWireWrapper`], [`NoOpWireUnwrapper`] in [`common/helpers.rs`](src/common/helpers.rs)) — collapse the entire wire layer (framing + transport, or their inverses) to a pass-through. Use these when your packet type is already self-contained on the wire (e.g. a Sphinx packet) and needs no extra framing or transport header. `NoOpWireWrapper` requires `Pkt: From<Vec<u8>>`; `NoOpWireUnwrapper` requires `Pkt: Into<Vec<u8>>` and `Mk: Default`.
|
||||
- **`Pipeline` composition structs** (in [`clients/types.rs`](src/clients/types.rs)) — generic structs that aggregate one component per pipeline stage and provide blanket impls of the relevant supertraits, so you can build a working pipeline by plugging in any combination of stage implementations.
|
||||
- **[`ClientWrappingPipelineDriver`](src/clients/driver.rs)** — wraps a dyn-compatible client pipeline behind a tick-driven `tick(timestamp) -> Vec<(Pkt, NdId)>` interface, with an internal mpsc channel for application-supplied input payloads. Reads new input only when the internal buffer is empty so buffered packets do not stack additional latency on top.
|
||||
|
||||
[`NoOpReliability`]: src/clients/helpers.rs
|
||||
[`NoOpRoutingSecurity`]: src/clients/helpers.rs
|
||||
[`NoOpObfuscation`]: src/clients/helpers.rs
|
||||
[`InputOptions`]: src/clients/mod.rs
|
||||
[`Reliability::reliable_encode`]: src/clients/traits.rs
|
||||
[`Obfuscation::obfuscate`]: src/clients/traits.rs
|
||||
|
||||
## Example users
|
||||
|
||||
[`nym-mix-sim`](../../nym-mix-sim) is the reference consumer: it ships two complete pipeline implementations (a pass-through `Simple*` family and a full Sphinx + Poisson + SURB-ACK family) on top of the traits defined here. See its source for end-to-end examples of implementing each pipeline stage.
|
||||
|
||||
The integration test under [`tests/integration`](tests/integration) wires together a small synthetic pipeline (`MockChunking`, `KcpReliability`, `SphinxSecurity`, `KekwObfuscation`, `LpFraming`, `LpTransport`) against the [`nym-lp`](../nym-lp) packet types — a useful starting point if you want to read a self-contained example of every trait being implemented.
|
||||
@@ -0,0 +1,93 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use std::sync::mpsc;
|
||||
|
||||
use crate::AddressedTimedData;
|
||||
use crate::clients::InputOptions;
|
||||
use crate::clients::traits::DynClientWrappingPipeline;
|
||||
|
||||
/// Drives a [`DynClientWrappingPipeline`] tick-by-tick, feeding it raw application
|
||||
/// payloads and emitting transport packets whose scheduled timestamp is due.
|
||||
///
|
||||
/// ## How it works
|
||||
///
|
||||
/// 1. The caller submits raw byte payloads via [`ClientWrappingPipelineDriver::input_sender`].
|
||||
/// 2. On each call to [`ClientWrappingPipelineDriver::tick`], the driver reads one pending
|
||||
/// payload (only when both the packet buffer and the obfuscation buffer are
|
||||
/// empty, to avoid adding extra latency on top of buffered data), runs it
|
||||
/// through the pipeline, and appends the resulting timestamped packets to an
|
||||
/// internal buffer.
|
||||
/// 3. Packets whose `timestamp ≤ now` are extracted from the buffer and
|
||||
/// returned to the caller for sending.
|
||||
///
|
||||
/// `Ts` must implement `Clone + PartialOrd` so that timestamps can be compared
|
||||
/// to decide which packets are due.
|
||||
///
|
||||
pub struct ClientWrappingPipelineDriver<Ts, Pkt, Opts, NdId>
|
||||
where
|
||||
Ts: Clone + PartialOrd,
|
||||
Opts: InputOptions<NdId>,
|
||||
{
|
||||
pipeline: Box<dyn DynClientWrappingPipeline<Ts, Pkt, Opts, NdId>>,
|
||||
|
||||
packet_buffer: Vec<AddressedTimedData<Ts, Pkt, NdId>>,
|
||||
|
||||
input: mpsc::Receiver<(Vec<u8>, Opts)>,
|
||||
|
||||
// Keeping a ref so we don't have problem about it being dropped
|
||||
input_sender: mpsc::SyncSender<(Vec<u8>, Opts)>,
|
||||
}
|
||||
|
||||
impl<Ts, Pkt, Opts, NdId> ClientWrappingPipelineDriver<Ts, Pkt, Opts, NdId>
|
||||
where
|
||||
Ts: Clone + PartialOrd,
|
||||
Opts: InputOptions<NdId>,
|
||||
{
|
||||
/// Create a new driver wrapping `pipeline`.
|
||||
///
|
||||
/// Internally allocates a zero-capacity `sync_channel` for input payloads.
|
||||
pub fn new(pipeline: impl DynClientWrappingPipeline<Ts, Pkt, Opts, NdId> + 'static) -> Self {
|
||||
let (input_sender, input_receiver) = mpsc::sync_channel(0);
|
||||
|
||||
Self {
|
||||
pipeline: Box::new(pipeline),
|
||||
packet_buffer: Vec::new(),
|
||||
input: input_receiver,
|
||||
input_sender,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return a clone of the sender half of the input channel.
|
||||
///
|
||||
/// Send raw application payloads here; they will be picked up on the next
|
||||
/// tick when the pipeline's internal buffers are empty.
|
||||
pub fn input_sender(&self) -> mpsc::SyncSender<(Vec<u8>, Opts)> {
|
||||
self.input_sender.clone()
|
||||
}
|
||||
|
||||
/// Advance the driver by one tick.
|
||||
///
|
||||
/// Reads a pending input payload (if both the packet buffer and the
|
||||
/// obfuscation buffer are empty), runs it through the pipeline, then
|
||||
/// returns all packets whose `timestamp ≤ now`.
|
||||
pub fn tick(&mut self, timestamp: Ts) -> Vec<(Pkt, NdId)> {
|
||||
// We're reading a message only if our buffer is empty
|
||||
// Otherwise, we will have buffers adding latencies to data
|
||||
let next_message = if self.packet_buffer.is_empty() {
|
||||
self.input
|
||||
.try_recv()
|
||||
.inspect_err(|_| tracing::trace!("No message in the queue"))
|
||||
.ok()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
self.packet_buffer
|
||||
.extend(self.pipeline.process(next_message, timestamp.clone()));
|
||||
|
||||
self.packet_buffer
|
||||
.extract_if(.., |p| p.data.timestamp <= timestamp)
|
||||
.map(|pkt| (pkt.data.data, pkt.dst))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::PipelinePayload;
|
||||
use crate::clients::traits::{Obfuscation, Reliability, RoutingSecurity};
|
||||
|
||||
/// Marker trait for a no-op [`Reliability`] implementation.
|
||||
///
|
||||
/// Implement this for your pipeline type to get a [`Reliability`] impl that
|
||||
/// passes the payload through unchanged with zero byte overhead.
|
||||
pub trait NoOpReliability {}
|
||||
|
||||
impl<T, Ts, Opts, NdId> Reliability<Ts, Opts, NdId> for T
|
||||
where
|
||||
T: NoOpReliability,
|
||||
{
|
||||
const OVERHEAD_SIZE: usize = 0;
|
||||
fn reliable_encode(
|
||||
&mut self,
|
||||
input: Option<PipelinePayload<Ts, Opts, NdId>>,
|
||||
_: Ts,
|
||||
) -> Vec<PipelinePayload<Ts, Opts, NdId>> {
|
||||
input.map(|payload| vec![payload]).unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Marker trait for a no-op [`RoutingSecurity`] implementation.
|
||||
///
|
||||
/// Implement this for your pipeline type to get a [`RoutingSecurity`] impl that
|
||||
/// passes the payload through unchanged with zero byte overhead and `nb_frames() == 1`.
|
||||
pub trait NoOpRoutingSecurity {}
|
||||
|
||||
impl<T, Ts, Opts, NdId> RoutingSecurity<Ts, Opts, NdId> for T
|
||||
where
|
||||
T: NoOpRoutingSecurity,
|
||||
{
|
||||
const OVERHEAD_SIZE: usize = 0;
|
||||
|
||||
fn nb_frames(&self) -> usize {
|
||||
1
|
||||
}
|
||||
|
||||
fn encrypt(
|
||||
&mut self,
|
||||
input: PipelinePayload<Ts, Opts, NdId>,
|
||||
) -> PipelinePayload<Ts, Opts, NdId> {
|
||||
input
|
||||
}
|
||||
}
|
||||
|
||||
/// Marker trait for a no-op [`Obfuscation`] implementation.
|
||||
///
|
||||
/// Implement this for your pipeline type to get an [`Obfuscation`] impl that
|
||||
/// passes the input through unchanged with no cover traffic, delay, or
|
||||
/// buffering.
|
||||
pub trait NoOpObfuscation {}
|
||||
|
||||
impl<T, Ts, Opts, NdId> Obfuscation<Ts, Opts, NdId> for T
|
||||
where
|
||||
T: NoOpObfuscation,
|
||||
{
|
||||
fn obfuscate(
|
||||
&mut self,
|
||||
input: Option<PipelinePayload<Ts, Opts, NdId>>,
|
||||
_: Ts,
|
||||
) -> Vec<PipelinePayload<Ts, Opts, NdId>> {
|
||||
input.map(|payload| vec![payload]).unwrap_or_default()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
pub mod driver;
|
||||
pub mod helpers;
|
||||
pub mod traits;
|
||||
pub mod types;
|
||||
|
||||
/// Per-message pipeline configuration carried alongside every payload.
|
||||
///
|
||||
/// Each pipeline stage (reliability, routing security, obfuscation) is optional
|
||||
/// and toggled per-message by the corresponding accessor. The next-hop
|
||||
/// destination is also resolved from the options so that addressing is decided
|
||||
/// before the payload reaches [`Framing`].
|
||||
///
|
||||
/// # Type Parameters
|
||||
/// - `NdId`: addressing type used to identify the next-hop destination.
|
||||
///
|
||||
/// [`Framing`]: crate::common::traits::Framing
|
||||
pub trait InputOptions<NdId>: Clone {
|
||||
/// Whether reliability encoding (e.g. SURB ACKs) should be applied.
|
||||
fn reliability(&self) -> bool;
|
||||
/// Whether routing-security encryption (e.g. Sphinx) should be applied.
|
||||
fn routing_security(&self) -> bool;
|
||||
/// Whether obfuscation (e.g. cover traffic) should be applied.
|
||||
fn obfuscation(&self) -> bool;
|
||||
|
||||
/// Identifier of the next-hop node this message should be sent to.
|
||||
fn next_hop(&self) -> NdId;
|
||||
}
|
||||
@@ -0,0 +1,304 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::PipelinePayload;
|
||||
use crate::clients::InputOptions;
|
||||
use crate::common::traits::{WireUnwrappingPipeline, WireWrappingPipeline};
|
||||
use crate::{AddressedTimedData, TimedPayload};
|
||||
|
||||
/// Trait for splitting an incoming payload into timestamped chunks.
|
||||
///
|
||||
/// # Type Parameters
|
||||
/// - `Ts`: Timestamp type associated with each produced [`PipelinePayload`].
|
||||
/// - `Opts`: Per-message pipeline options (must implement [`InputOptions`]).
|
||||
/// - `NdId`: Addressing type for the next-hop destination.
|
||||
///
|
||||
/// # Required Methods
|
||||
/// - `chunked`: Split `input` into chunks of at most `chunk_size` bytes, tagging
|
||||
/// each chunk with `timestamp` and `input_options`. Returns one
|
||||
/// [`PipelinePayload`] per chunk, ready to be fed through the rest of the
|
||||
/// pipeline.
|
||||
pub trait Chunking<Ts, Opts, NdId>
|
||||
where
|
||||
Opts: InputOptions<NdId>,
|
||||
{
|
||||
fn chunked(
|
||||
&mut self,
|
||||
input: Vec<u8>,
|
||||
input_options: Opts,
|
||||
chunk_size: usize,
|
||||
timestamp: Ts,
|
||||
) -> Vec<PipelinePayload<Ts, Opts, NdId>>;
|
||||
}
|
||||
|
||||
/// Trait for applying reliability encoding (e.g. SURB ACKs, retransmissions) to
|
||||
/// a timed payload.
|
||||
///
|
||||
/// # Type Parameters
|
||||
/// - `Ts`: Timestamp type carried by the [`PipelinePayload`].
|
||||
/// - `Opts`: Per-message pipeline options.
|
||||
/// - `NdId`: Addressing type for the next-hop destination.
|
||||
///
|
||||
/// # Associated Constants
|
||||
/// - `OVERHEAD_SIZE`: Number of additional bytes added by the reliability scheme.
|
||||
///
|
||||
/// # Required Methods
|
||||
/// - `reliable_encode`: Encode `input` with the reliability mechanism. When
|
||||
/// `input` is `None`, the method is still called every tick so the layer can
|
||||
/// emit pending retransmissions or scheduled control packets.
|
||||
pub trait Reliability<Ts, Opts, NdId> {
|
||||
const OVERHEAD_SIZE: usize;
|
||||
fn reliable_encode(
|
||||
&mut self,
|
||||
input: Option<PipelinePayload<Ts, Opts, NdId>>,
|
||||
timestamp: Ts,
|
||||
) -> Vec<PipelinePayload<Ts, Opts, NdId>>;
|
||||
}
|
||||
|
||||
/// Trait for applying obfuscation (cover traffic, traffic shaping) to a timed payload.
|
||||
///
|
||||
/// When obfuscation is enabled, `obfuscate` must be called on every tick — not
|
||||
/// only on ticks that carry input — so the layer can produce cover traffic on
|
||||
/// schedule even when the application has nothing to send.
|
||||
///
|
||||
/// # Type Parameters
|
||||
/// - `Ts`: Timestamp type carried by the [`PipelinePayload`].
|
||||
/// - `Opts`: Per-message pipeline options.
|
||||
/// - `NdId`: Addressing type for the next-hop destination.
|
||||
pub trait Obfuscation<Ts, Opts, NdId> {
|
||||
/// Obfuscate `input` at the given `timestamp`.
|
||||
///
|
||||
/// # Parameters
|
||||
/// - `input`: Payload to obfuscate, or `None` when the pipeline is ticking
|
||||
/// with no real message available.
|
||||
/// - `timestamp`: Current timestamp.
|
||||
///
|
||||
/// # Returns
|
||||
/// A `Vec` of obfuscated payloads, possibly empty when no packet is due to be
|
||||
/// emitted at this tick.
|
||||
fn obfuscate(
|
||||
&mut self,
|
||||
input: Option<PipelinePayload<Ts, Opts, NdId>>,
|
||||
timestamp: Ts,
|
||||
) -> Vec<PipelinePayload<Ts, Opts, NdId>>;
|
||||
}
|
||||
|
||||
/// Trait for applying routing-security encryption (e.g. Sphinx) to a timed payload.
|
||||
///
|
||||
/// # Type Parameters
|
||||
/// - `Ts`: Timestamp type carried by the [`PipelinePayload`].
|
||||
/// - `Opts`: Per-message pipeline options.
|
||||
/// - `NdId`: Addressing type for the next-hop destination.
|
||||
///
|
||||
/// # Associated Constants
|
||||
/// - `OVERHEAD_SIZE`: Number of additional bytes added by the encryption scheme.
|
||||
///
|
||||
/// # Required Methods
|
||||
/// - `encrypt`: Encrypt the given payload, returning a new [`PipelinePayload`].
|
||||
///
|
||||
/// # Provided Methods
|
||||
/// - `nb_frames`: Number of transport frames that one encrypted payload expands
|
||||
/// into; defaults to `1`. Override when the encryption scheme (e.g. Sphinx)
|
||||
/// produces multiple frames per input chunk.
|
||||
pub trait RoutingSecurity<Ts, Opts, NdId> {
|
||||
const OVERHEAD_SIZE: usize;
|
||||
fn nb_frames(&self) -> usize {
|
||||
1
|
||||
}
|
||||
fn encrypt(
|
||||
&mut self,
|
||||
input: PipelinePayload<Ts, Opts, NdId>,
|
||||
) -> PipelinePayload<Ts, Opts, NdId>;
|
||||
}
|
||||
|
||||
/// Full client-side outbound message pipeline.
|
||||
///
|
||||
/// Composes all six processing stages — [`Chunking`], [`Reliability`],
|
||||
/// [`Obfuscation`], [`RoutingSecurity`], and the shared [`WireWrappingPipeline`]
|
||||
/// (framing + transport) — into a single `process` call that takes a raw byte
|
||||
/// payload and returns a list of timestamped transport packets ready for sending.
|
||||
///
|
||||
/// # Type Parameters
|
||||
/// - `Ts`: Timestamp type carried through the pipeline.
|
||||
/// - `Pkt`: Final transport packet type produced by transport.
|
||||
/// - `Opts`: Per-message pipeline options (must implement [`InputOptions`]).
|
||||
/// - `NdId`: Addressing type for the next-hop destination.
|
||||
///
|
||||
/// # Provided Methods
|
||||
/// - `chunk_size`: Derived from `frame_size` (via [`WireWrappingPipeline`]) minus
|
||||
/// routing-security and reliability overheads, accounting for `nb_frames` expansion.
|
||||
/// - `process`: Runs the full pipeline in order:
|
||||
/// chunk → reliability encode → obfuscate → encrypt → frame → transport.
|
||||
pub trait ClientWrappingPipeline<Ts, Pkt, Opts, NdId>:
|
||||
Chunking<Ts, Opts, NdId>
|
||||
+ Reliability<Ts, Opts, NdId>
|
||||
+ Obfuscation<Ts, Opts, NdId>
|
||||
+ RoutingSecurity<Ts, Opts, NdId>
|
||||
+ WireWrappingPipeline<Ts, Pkt, Opts, NdId>
|
||||
where
|
||||
Ts: Clone,
|
||||
NdId: Clone,
|
||||
Opts: InputOptions<NdId>,
|
||||
{
|
||||
fn chunk_size(&self, input_options: Opts) -> usize {
|
||||
// Frame size comes from WireWrappingPipeline
|
||||
let mut chunk_size = self.frame_size();
|
||||
|
||||
if input_options.routing_security() {
|
||||
// SAFETY : While this CAN technically fail, it means that something is wrong in the code and it's pointless to continue anyway
|
||||
#[allow(clippy::expect_used)]
|
||||
let pre_security_chunk_size = (chunk_size * self.nb_frames())
|
||||
.checked_sub(<Self as RoutingSecurity<_, _, _>>::OVERHEAD_SIZE)
|
||||
.expect("not enough room in a packet for routing security overhead");
|
||||
chunk_size = pre_security_chunk_size;
|
||||
}
|
||||
|
||||
if input_options.reliability() {
|
||||
// SAFETY : While this CAN technically fail, it means that something is wrong in the code and it's pointless to continue anyway
|
||||
#[allow(clippy::expect_used)]
|
||||
let pre_reliability_chunk_size = chunk_size
|
||||
.checked_sub(<Self as Reliability<_, _, _>>::OVERHEAD_SIZE)
|
||||
.expect("not enough room in a packet for reliability overhead");
|
||||
chunk_size = pre_reliability_chunk_size;
|
||||
}
|
||||
|
||||
chunk_size
|
||||
}
|
||||
|
||||
fn process(
|
||||
&mut self,
|
||||
input: Option<(Vec<u8>, Opts)>, // Optional to be able to tick the pipeline without input
|
||||
timestamp: Ts,
|
||||
) -> Vec<AddressedTimedData<Ts, Pkt, NdId>> {
|
||||
let mut chunks = if let Some((input_data, input_options)) = input {
|
||||
self.chunked(
|
||||
input_data,
|
||||
input_options.clone(),
|
||||
self.chunk_size(input_options.clone()),
|
||||
timestamp.clone(),
|
||||
)
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
// Reliability stage with chunks that needs reliability
|
||||
chunks = chunks
|
||||
.into_iter()
|
||||
.flat_map(|chunk| {
|
||||
if chunk.options.reliability() {
|
||||
self.reliable_encode(Some(chunk), timestamp.clone())
|
||||
} else {
|
||||
vec![chunk]
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Even if we had nothing go into the reliability stage, we need to catch potential retransmissions
|
||||
// If we had, this should be a no-op, since it already has been called with the same timestamp
|
||||
chunks.append(&mut self.reliable_encode(None, timestamp.clone()));
|
||||
|
||||
chunks = chunks
|
||||
.into_iter()
|
||||
.flat_map(|chunk| {
|
||||
if chunk.options.obfuscation() {
|
||||
self.obfuscate(Some(chunk), timestamp.clone())
|
||||
} else {
|
||||
vec![chunk]
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Even if we had nothing go into the obfuscation stage, we need to catch potential cover traffic
|
||||
// If we had, this should be a no-op, since it already has been called with the same timestamp
|
||||
chunks.append(&mut self.obfuscate(None, timestamp.clone()));
|
||||
|
||||
chunks = chunks
|
||||
.into_iter()
|
||||
.map(|chunk| {
|
||||
if chunk.options.routing_security() {
|
||||
self.encrypt(chunk)
|
||||
} else {
|
||||
chunk
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
chunks
|
||||
.into_iter()
|
||||
.flat_map(|payload| self.wire_wrap(payload))
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
}
|
||||
|
||||
/// Dyn-compatible mirror of [`ClientWrappingPipeline`].
|
||||
///
|
||||
/// All associated constants from the sub-traits are exposed as methods so the
|
||||
/// trait can be used as `dyn DynClientWrappingPipeline<Ts, Pkt, Opts, NdId>`,
|
||||
/// erasing the concrete pipeline type while keeping `Ts`, `Pkt`, `Opts`, and
|
||||
/// `NdId` visible.
|
||||
///
|
||||
/// Implement [`ClientWrappingPipeline`] on your concrete type; the blanket impl
|
||||
/// below provides `DynClientWrappingPipeline` for free.
|
||||
pub trait DynClientWrappingPipeline<Ts, Pkt, Opts, NdId> {
|
||||
/// On-wire size of an output packet in bytes.
|
||||
fn packet_size(&self) -> usize;
|
||||
|
||||
/// Run the full client wrapping pipeline; see [`ClientWrappingPipeline::process`].
|
||||
fn process(
|
||||
&mut self,
|
||||
input: Option<(Vec<u8>, Opts)>,
|
||||
timestamp: Ts,
|
||||
) -> Vec<AddressedTimedData<Ts, Pkt, NdId>>;
|
||||
}
|
||||
|
||||
impl<T, Ts, Pkt, Opts, NdId> DynClientWrappingPipeline<Ts, Pkt, Opts, NdId> for T
|
||||
where
|
||||
Ts: Clone,
|
||||
NdId: Clone,
|
||||
Opts: InputOptions<NdId>,
|
||||
T: ClientWrappingPipeline<Ts, Pkt, Opts, NdId>,
|
||||
{
|
||||
fn packet_size(&self) -> usize {
|
||||
WireWrappingPipeline::packet_size(self)
|
||||
}
|
||||
|
||||
fn process(
|
||||
&mut self,
|
||||
input: Option<(Vec<u8>, Opts)>,
|
||||
timestamp: Ts,
|
||||
) -> Vec<AddressedTimedData<Ts, Pkt, NdId>> {
|
||||
ClientWrappingPipeline::process(self, input, timestamp)
|
||||
}
|
||||
}
|
||||
|
||||
/// Full client-side inbound pipeline.
|
||||
///
|
||||
/// Combines the shared [`WireUnwrappingPipeline`] (transport + framing unwrap) with a
|
||||
/// blank [`process_unwrapped`](Self::process_unwrapped) step that the implementor
|
||||
/// fills in (routing-security decrypt, reliability decode, chunk reassembly, etc.).
|
||||
///
|
||||
/// # Type Parameters
|
||||
/// - `Ts`: Timestamp type.
|
||||
/// - `Pkt`: Transport packet type consumed as input.
|
||||
/// - `Mk`: Message-kind marker returned alongside reassembled payloads.
|
||||
///
|
||||
/// # Required Methods
|
||||
/// - `process_unwrapped`: Called with the reassembled payload and its message kind
|
||||
/// once a complete message is available. Returns the decoded application bytes,
|
||||
/// or `None` if reassembly is still in progress.
|
||||
///
|
||||
/// # Provided Methods
|
||||
/// - `unwrap`: Strips the wire layers via [`WireUnwrappingPipeline::wire_unwrap`],
|
||||
/// then delegates to `process_unwrapped`.
|
||||
pub trait ClientUnwrappingPipeline<Ts, Pkt, Mk>: WireUnwrappingPipeline<Ts, Pkt, Mk>
|
||||
where
|
||||
Ts: Clone,
|
||||
{
|
||||
fn process_unwrapped(&mut self, payload: TimedPayload<Ts>, kind: Mk) -> Option<Vec<u8>>;
|
||||
|
||||
fn unwrap(&mut self, input: Pkt, timestamp: Ts) -> Result<Option<Vec<u8>>, Self::Error> {
|
||||
Ok(self
|
||||
.wire_unwrap(input, timestamp)?
|
||||
.and_then(|(payload, kind)| self.process_unwrapped(payload, kind)))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::clients::InputOptions;
|
||||
use crate::clients::traits::{
|
||||
Chunking, ClientWrappingPipeline, Obfuscation, Reliability, RoutingSecurity,
|
||||
};
|
||||
use crate::common::traits::{Framing, Transport, WireWrappingPipeline};
|
||||
use crate::{AddressedTimedData, PipelinePayload};
|
||||
|
||||
/// Generic composition struct that implements [`ClientWrappingPipeline`] by
|
||||
/// delegating each stage to a held component.
|
||||
///
|
||||
/// Type parameters correspond to the six pipeline stages:
|
||||
/// - `C`: [`Chunking`]
|
||||
/// - `R`: [`Reliability`]
|
||||
/// - `O`: [`Obfuscation`]
|
||||
/// - `Rs`: [`RoutingSecurity`]
|
||||
/// - `F`: [`Framing`]
|
||||
/// - `T`: [`Transport`]
|
||||
pub struct Pipeline<C, R, O, Rs, F, T> {
|
||||
/// On-wire size of an output packet in bytes; returned by
|
||||
/// [`WireWrappingPipeline::packet_size`].
|
||||
pub packet_size: usize,
|
||||
/// [`Chunking`] stage.
|
||||
pub chunking: C,
|
||||
/// [`Reliability`] stage.
|
||||
pub reliability: R,
|
||||
/// [`Obfuscation`] stage.
|
||||
pub obfuscation: O,
|
||||
/// [`RoutingSecurity`] stage.
|
||||
pub security: Rs,
|
||||
/// [`Framing`] stage.
|
||||
pub framing: F,
|
||||
/// [`Transport`] stage.
|
||||
pub transport: T,
|
||||
}
|
||||
|
||||
impl<Ts, Opts, NdId, C, R, O, Rs, F, T> Chunking<Ts, Opts, NdId> for Pipeline<C, R, O, Rs, F, T>
|
||||
where
|
||||
Opts: InputOptions<NdId>,
|
||||
C: Chunking<Ts, Opts, NdId>,
|
||||
{
|
||||
fn chunked(
|
||||
&mut self,
|
||||
input: Vec<u8>,
|
||||
input_options: Opts,
|
||||
chunk_size: usize,
|
||||
timestamp: Ts,
|
||||
) -> Vec<PipelinePayload<Ts, Opts, NdId>> {
|
||||
self.chunking
|
||||
.chunked(input, input_options, chunk_size, timestamp)
|
||||
}
|
||||
}
|
||||
|
||||
impl<Ts, Opts, NdId, C, R, O, Rs, F, T> Reliability<Ts, Opts, NdId> for Pipeline<C, R, O, Rs, F, T>
|
||||
where
|
||||
R: Reliability<Ts, Opts, NdId>,
|
||||
{
|
||||
const OVERHEAD_SIZE: usize = R::OVERHEAD_SIZE;
|
||||
|
||||
fn reliable_encode(
|
||||
&mut self,
|
||||
input: Option<PipelinePayload<Ts, Opts, NdId>>,
|
||||
timestamp: Ts,
|
||||
) -> Vec<PipelinePayload<Ts, Opts, NdId>> {
|
||||
self.reliability.reliable_encode(input, timestamp)
|
||||
}
|
||||
}
|
||||
|
||||
impl<Ts, Opts, NdId, C, R, O, Rs, F, T> Obfuscation<Ts, Opts, NdId> for Pipeline<C, R, O, Rs, F, T>
|
||||
where
|
||||
O: Obfuscation<Ts, Opts, NdId>,
|
||||
{
|
||||
fn obfuscate(
|
||||
&mut self,
|
||||
input: Option<PipelinePayload<Ts, Opts, NdId>>,
|
||||
timestamp: Ts,
|
||||
) -> Vec<PipelinePayload<Ts, Opts, NdId>> {
|
||||
self.obfuscation.obfuscate(input, timestamp)
|
||||
}
|
||||
}
|
||||
|
||||
impl<Ts, Opts, NdId, C, R, O, Rs, F, T> RoutingSecurity<Ts, Opts, NdId>
|
||||
for Pipeline<C, R, O, Rs, F, T>
|
||||
where
|
||||
Rs: RoutingSecurity<Ts, Opts, NdId>,
|
||||
{
|
||||
const OVERHEAD_SIZE: usize = Rs::OVERHEAD_SIZE;
|
||||
|
||||
fn nb_frames(&self) -> usize {
|
||||
self.security.nb_frames()
|
||||
}
|
||||
|
||||
fn encrypt(
|
||||
&mut self,
|
||||
input: PipelinePayload<Ts, Opts, NdId>,
|
||||
) -> PipelinePayload<Ts, Opts, NdId> {
|
||||
self.security.encrypt(input)
|
||||
}
|
||||
}
|
||||
|
||||
impl<Ts, Opts, NdId, C, R, O, Rs, F, T> Framing<Ts, Opts, NdId> for Pipeline<C, R, O, Rs, F, T>
|
||||
where
|
||||
F: Framing<Ts, Opts, NdId>,
|
||||
{
|
||||
type Frame = F::Frame;
|
||||
const OVERHEAD_SIZE: usize = F::OVERHEAD_SIZE;
|
||||
|
||||
fn to_frame(
|
||||
&mut self,
|
||||
payload: PipelinePayload<Ts, Opts, NdId>,
|
||||
frame_size: usize,
|
||||
) -> Vec<AddressedTimedData<Ts, F::Frame, NdId>> {
|
||||
self.framing.to_frame(payload, frame_size)
|
||||
}
|
||||
}
|
||||
|
||||
impl<Ts, Pkt, NdId, C, R, O, Rs, F, T> Transport<Ts, Pkt, NdId> for Pipeline<C, R, O, Rs, F, T>
|
||||
where
|
||||
T: Transport<Ts, Pkt, NdId>,
|
||||
{
|
||||
type Frame = T::Frame;
|
||||
const OVERHEAD_SIZE: usize = T::OVERHEAD_SIZE;
|
||||
|
||||
fn to_transport_packet(
|
||||
&mut self,
|
||||
frame: AddressedTimedData<Ts, T::Frame, NdId>,
|
||||
) -> AddressedTimedData<Ts, Pkt, NdId> {
|
||||
self.transport.to_transport_packet(frame)
|
||||
}
|
||||
}
|
||||
|
||||
impl<Ts, Pkt, Opts, NdId, C, R, O, Rs, F, T> WireWrappingPipeline<Ts, Pkt, Opts, NdId>
|
||||
for Pipeline<C, R, O, Rs, F, T>
|
||||
where
|
||||
Ts: Clone,
|
||||
NdId: Clone,
|
||||
F: Framing<Ts, Opts, NdId>,
|
||||
T: Transport<Ts, Pkt, NdId, Frame = F::Frame>,
|
||||
{
|
||||
fn packet_size(&self) -> usize {
|
||||
self.packet_size
|
||||
}
|
||||
}
|
||||
|
||||
impl<Ts, Pkt, Opts, NdId, C, R, O, Rs, F, T> ClientWrappingPipeline<Ts, Pkt, Opts, NdId>
|
||||
for Pipeline<C, R, O, Rs, F, T>
|
||||
where
|
||||
Ts: Clone,
|
||||
NdId: Clone,
|
||||
Opts: InputOptions<NdId>,
|
||||
C: Chunking<Ts, Opts, NdId>,
|
||||
R: Reliability<Ts, Opts, NdId>,
|
||||
O: Obfuscation<Ts, Opts, NdId>,
|
||||
Rs: RoutingSecurity<Ts, Opts, NdId>,
|
||||
F: Framing<Ts, Opts, NdId>,
|
||||
T: Transport<Ts, Pkt, NdId, Frame = F::Frame>,
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::{
|
||||
AddressedTimedData, AddressedTimedPayload, PipelinePayload, TimedData, TimedPayload,
|
||||
common::traits::{
|
||||
Framing, FramingUnwrap, Transport, TransportUnwrap, WireUnwrappingPipeline,
|
||||
WireWrappingPipeline,
|
||||
},
|
||||
};
|
||||
|
||||
/// Marker trait for a no-op [`WireWrappingPipeline`] implementation.
|
||||
///
|
||||
/// Implement this for your pipeline type to get a [`WireWrappingPipeline`] impl that
|
||||
/// passes the payload through unchanged with zero byte overhead.
|
||||
pub trait NoOpWireWrapper {
|
||||
const PACKET_SIZE: usize = 1500;
|
||||
}
|
||||
|
||||
impl<T, Ts, Opts, NdId> Framing<Ts, Opts, NdId> for T
|
||||
where
|
||||
T: NoOpWireWrapper,
|
||||
{
|
||||
type Frame = Vec<u8>;
|
||||
const OVERHEAD_SIZE: usize = 0;
|
||||
fn to_frame(
|
||||
&mut self,
|
||||
payload: PipelinePayload<Ts, Opts, NdId>,
|
||||
_: usize,
|
||||
) -> Vec<AddressedTimedPayload<Ts, NdId>> {
|
||||
vec![payload.into_addressed()]
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, Ts, Pkt, NdId> Transport<Ts, Pkt, NdId> for T
|
||||
where
|
||||
T: NoOpWireWrapper,
|
||||
Pkt: From<Vec<u8>>,
|
||||
{
|
||||
type Frame = Vec<u8>;
|
||||
const OVERHEAD_SIZE: usize = 0;
|
||||
fn to_transport_packet(
|
||||
&mut self,
|
||||
frame: AddressedTimedPayload<Ts, NdId>,
|
||||
) -> AddressedTimedData<Ts, Pkt, NdId> {
|
||||
frame.data_transform(|data| data.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, Ts, Pkt, Opts, NdId> WireWrappingPipeline<Ts, Pkt, Opts, NdId> for T
|
||||
where
|
||||
T: NoOpWireWrapper,
|
||||
Ts: Clone,
|
||||
Pkt: From<Vec<u8>>,
|
||||
NdId: Clone,
|
||||
{
|
||||
fn packet_size(&self) -> usize {
|
||||
T::PACKET_SIZE
|
||||
}
|
||||
}
|
||||
|
||||
/// Marker trait for a no-op [`WireUnwrappingPipeline`] implementation.
|
||||
///
|
||||
/// Implement this for your pipeline type to get a [`WireUnwrappingPipeline`] impl that
|
||||
/// passes the payload through unchanged.
|
||||
pub trait NoOpWireUnwrapper {}
|
||||
|
||||
impl<T, Ts, Mk> FramingUnwrap<Ts, Mk> for T
|
||||
where
|
||||
T: NoOpWireUnwrapper,
|
||||
Mk: Default,
|
||||
{
|
||||
type Frame = Vec<u8>;
|
||||
fn frame_to_message(&mut self, frame: TimedPayload<Ts>) -> Option<(TimedPayload<Ts>, Mk)> {
|
||||
Some((frame, Default::default()))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, Ts, Pkt> TransportUnwrap<Ts, Pkt> for T
|
||||
where
|
||||
T: NoOpWireUnwrapper,
|
||||
Pkt: Into<Vec<u8>>,
|
||||
{
|
||||
type Frame = Vec<u8>;
|
||||
type Error = std::convert::Infallible;
|
||||
fn packet_to_frame(
|
||||
&mut self,
|
||||
packet: Pkt,
|
||||
timestamp: Ts,
|
||||
) -> Result<TimedPayload<Ts>, Self::Error> {
|
||||
Ok(TimedData {
|
||||
timestamp,
|
||||
data: packet.into(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, Ts, Pkt, Mk> WireUnwrappingPipeline<Ts, Pkt, Mk> for T
|
||||
where
|
||||
T: NoOpWireUnwrapper,
|
||||
Ts: Clone,
|
||||
Pkt: Into<Vec<u8>>,
|
||||
Mk: Default,
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
pub mod helpers;
|
||||
pub mod traits;
|
||||
@@ -0,0 +1,182 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::{AddressedTimedData, PipelinePayload, TimedData, TimedPayload};
|
||||
|
||||
/// Trait for applying framing to a timed payload.
|
||||
///
|
||||
/// # Type Parameters
|
||||
/// - `Ts`: Timestamp type carried by the `PipelinePayload`.
|
||||
/// - `Opts` : Opts type carried by the `PipelinePayload`
|
||||
/// - `NdId` : Addressing type
|
||||
///
|
||||
/// # Associated Types
|
||||
/// - `Frame`: Frame type produced by the framing operation.
|
||||
///
|
||||
/// # Associated Constants
|
||||
/// - `OVERHEAD_SIZE`: Number of additional bytes added by the framing scheme.
|
||||
///
|
||||
/// # Required Methods
|
||||
/// - `to_frame`: Splits the payload into a `Vec<TimedData<Ts, Self::Frame>>` of frames of the given size.
|
||||
pub trait Framing<Ts, Opts, NdId> {
|
||||
type Frame;
|
||||
const OVERHEAD_SIZE: usize;
|
||||
fn to_frame(
|
||||
&mut self,
|
||||
payload: PipelinePayload<Ts, Opts, NdId>,
|
||||
frame_size: usize,
|
||||
) -> Vec<AddressedTimedData<Ts, Self::Frame, NdId>>;
|
||||
}
|
||||
|
||||
/// Trait for unwrapping framing from a frame back into a payload.
|
||||
///
|
||||
/// # Type Parameters
|
||||
/// - `Ts`: Timestamp type carried by the `TimedPayload`.
|
||||
/// - `Mk`: Enum describing the kind of message that can be returned.
|
||||
///
|
||||
/// # Associated Types
|
||||
/// - `Frame`: Frame type consumed as input.
|
||||
///
|
||||
/// # Required Methods
|
||||
/// - `frame_to_message`: Attempts to reassemble a payload from the given frame, returning
|
||||
/// `Some((payload, kind))` when a complete message is available, or `None` otherwise.
|
||||
pub trait FramingUnwrap<Ts, Mk> {
|
||||
type Frame;
|
||||
fn frame_to_message(
|
||||
&mut self,
|
||||
frame: TimedData<Ts, Self::Frame>,
|
||||
) -> Option<(TimedPayload<Ts>, Mk)>;
|
||||
}
|
||||
|
||||
/// Trait for applying a transport layer to a framed payload.
|
||||
///
|
||||
/// # Type Parameters
|
||||
/// - `Ts`: Timestamp type carried by the `TimedPayload`.
|
||||
/// - `Pkt`: Transport packet type produced as output.
|
||||
///
|
||||
/// # Associated Types
|
||||
/// - `Frame`: Frame type consumed as input.
|
||||
///
|
||||
/// # Associated Constants
|
||||
/// - `OVERHEAD_SIZE`: Number of additional bytes added by the transport scheme.
|
||||
///
|
||||
/// # Required Methods
|
||||
/// - `to_transport_packet`: Wraps a frame into a transport packet.
|
||||
pub trait Transport<Ts, Pkt, NdId> {
|
||||
type Frame;
|
||||
const OVERHEAD_SIZE: usize;
|
||||
fn to_transport_packet(
|
||||
&mut self,
|
||||
frame: AddressedTimedData<Ts, Self::Frame, NdId>,
|
||||
) -> AddressedTimedData<Ts, Pkt, NdId>;
|
||||
}
|
||||
|
||||
/// Trait for unwrapping a transport packet back into a frame.
|
||||
///
|
||||
/// # Type Parameters
|
||||
/// - `Ts`: Timestamp type carried by the `TimedPayload`.
|
||||
/// - `Pkt`: Transport packet type consumed as input.
|
||||
///
|
||||
/// # Associated Types
|
||||
/// - `Frame`: Frame type produced as output.
|
||||
/// - `Error`: Error type
|
||||
///
|
||||
/// # Required Methods
|
||||
/// - `packet_to_frame`: Strips the transport layer from a packet, returning the inner frame
|
||||
/// tagged with the given timestamp.
|
||||
pub trait TransportUnwrap<Ts, Pkt> {
|
||||
type Frame;
|
||||
type Error;
|
||||
fn packet_to_frame(
|
||||
&mut self,
|
||||
packet: Pkt,
|
||||
timestamp: Ts,
|
||||
) -> Result<TimedData<Ts, Self::Frame>, Self::Error>;
|
||||
}
|
||||
|
||||
/// Supertrait combining [`Framing`] and [`Transport`] into a reusable wire-wrapping layer.
|
||||
///
|
||||
/// Used as the bottom stage of any outbound pipeline (client or mixnode).
|
||||
///
|
||||
/// # Type Parameters
|
||||
/// - `Ts`: Timestamp type.
|
||||
/// - `Pkt`: Final transport packet type.
|
||||
/// - `Opts` : Option type
|
||||
/// - `NdId` : Addressing type
|
||||
///
|
||||
/// Both [`Framing`] and [`Transport`] declare their own `type Frame`; this
|
||||
/// supertrait cross-constrains them so `to_frame`'s output feeds directly into
|
||||
/// `to_transport_packet`.
|
||||
///
|
||||
/// # Required Methods
|
||||
/// - `packet_size`: Total on-wire size of an output packet in bytes.
|
||||
///
|
||||
/// # Provided Methods
|
||||
/// - `frame_size`: Derived from `packet_size` minus transport and framing overheads.
|
||||
/// - `wire_wrap`: Frames a payload and wraps each frame into a transport packet.
|
||||
pub trait WireWrappingPipeline<Ts, Pkt, Opts, NdId>:
|
||||
Transport<Ts, Pkt, NdId>
|
||||
+ Framing<Ts, Opts, NdId, Frame = <Self as Transport<Ts, Pkt, NdId>>::Frame>
|
||||
where
|
||||
Ts: Clone,
|
||||
NdId: Clone,
|
||||
{
|
||||
// IMPORTANT NOTE : This fn can be not constant to allow e.g. flexible MTU
|
||||
// However, every possible value must be able to accommodate the different overhead.
|
||||
// If it doesn't, the pipeline becomes unusable
|
||||
fn packet_size(&self) -> usize;
|
||||
|
||||
fn frame_size(&self) -> usize {
|
||||
// SAFETY : While this CAN technically fail, it means that something is wrong in the code and it's pointless to continue anyway
|
||||
#[allow(clippy::expect_used)]
|
||||
self.packet_size()
|
||||
.checked_sub(
|
||||
<Self as Transport<Ts, Pkt, NdId>>::OVERHEAD_SIZE
|
||||
+ <Self as Framing<Ts, Opts, NdId>>::OVERHEAD_SIZE,
|
||||
)
|
||||
.expect("packet_size smaller than transport + framing overhead")
|
||||
}
|
||||
|
||||
fn wire_wrap(
|
||||
&mut self,
|
||||
payload: PipelinePayload<Ts, Opts, NdId>,
|
||||
) -> Vec<AddressedTimedData<Ts, Pkt, NdId>> {
|
||||
let frame_size = self.frame_size();
|
||||
self.to_frame(payload, frame_size)
|
||||
.into_iter()
|
||||
.map(|frame| self.to_transport_packet(frame))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// Supertrait combining [`TransportUnwrap`] and [`FramingUnwrap`] into a reusable
|
||||
/// wire-unwrapping layer.
|
||||
///
|
||||
/// Used as the bottom stage of any inbound pipeline (client or mixnode).
|
||||
///
|
||||
/// # Type Parameters
|
||||
/// - `Ts`: Timestamp type.
|
||||
/// - `Pkt`: Transport packet type consumed as input.
|
||||
/// - `Mk`: Message-kind marker returned alongside the reassembled payload.
|
||||
///
|
||||
/// Both [`TransportUnwrap`] and [`FramingUnwrap`] declare their own `type Frame`;
|
||||
/// this supertrait cross-constrains them so `packet_to_frame`'s output feeds
|
||||
/// directly into `frame_to_message`.
|
||||
///
|
||||
/// # Provided Methods
|
||||
/// - `wire_unwrap`: Strips the transport layer from a packet and attempts to reassemble
|
||||
/// a payload, returning `Some((payload, kind))` when a complete message is available.
|
||||
pub trait WireUnwrappingPipeline<Ts, Pkt, Mk>:
|
||||
TransportUnwrap<Ts, Pkt> + FramingUnwrap<Ts, Mk, Frame = <Self as TransportUnwrap<Ts, Pkt>>::Frame>
|
||||
where
|
||||
Ts: Clone,
|
||||
{
|
||||
fn wire_unwrap(
|
||||
&mut self,
|
||||
input: Pkt,
|
||||
timestamp: Ts,
|
||||
) -> Result<Option<(TimedPayload<Ts>, Mk)>, Self::Error> {
|
||||
let frame = self.packet_to_frame(input, timestamp)?;
|
||||
Ok(self.frame_to_message(frame))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//! Trait definitions and data structures for low-level packet (LP) processing
|
||||
//! pipelines in the Nym mixnet.
|
||||
//!
|
||||
//! ## Crate layout
|
||||
//!
|
||||
//! | Module | Purpose |
|
||||
//! |--------|---------|
|
||||
//! | [`clients`] | Client-side pipeline traits and types: chunking, reliability, obfuscation, routing security, framing, transport |
|
||||
//! | [`common`] | Shared framing and transport traits used by both clients and mixnodes |
|
||||
//! | [`mixnodes`] | Mixnode-side pipeline traits: unwrap incoming packets, re-wrap and forward them |
|
||||
//!
|
||||
//! ## Core types
|
||||
//!
|
||||
//! [`TimedData`] is the foundational wrapper that pairs any piece of data with a
|
||||
//! timestamp, threading timing information through every stage of the pipeline.
|
||||
//! [`TimedPayload`] is a convenience alias for `TimedData<Ts, Vec<u8>>`.
|
||||
|
||||
use std::fmt::Debug;
|
||||
|
||||
pub mod clients;
|
||||
pub mod common;
|
||||
pub mod nymnodes;
|
||||
pub mod packet;
|
||||
|
||||
/// Convenience alias for [`TimedData`] when the payload is a raw byte buffer.
|
||||
pub type TimedPayload<Ts> = TimedData<Ts, Vec<u8>>;
|
||||
/// Convenience alias for [`AddressedTimedData`] when the payload is a raw byte buffer.
|
||||
pub type AddressedTimedPayload<Ts, NdId> = AddressedTimedData<Ts, Vec<u8>, NdId>;
|
||||
/// Convenience alias for [`PipelineData`] when the payload is a raw byte buffer.
|
||||
pub type PipelinePayload<Ts, Opts, NdId> = PipelineData<Ts, Vec<u8>, Opts, NdId>;
|
||||
|
||||
/// A value of type `D` tagged with a timestamp of type `Ts`.
|
||||
///
|
||||
/// `TimedData` threads timing information through every stage of the LP
|
||||
/// pipeline. It is produced by [`clients::traits::Chunking`] and propagated
|
||||
/// unchanged (or with the timestamp transformed) through every subsequent
|
||||
/// pipeline stage until the packet is sent on the wire.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct TimedData<Ts, D> {
|
||||
pub timestamp: Ts,
|
||||
pub data: D,
|
||||
}
|
||||
|
||||
impl<Ts, D> TimedData<Ts, D> {
|
||||
pub fn new(timestamp: Ts, data: D) -> Self {
|
||||
TimedData { timestamp, data }
|
||||
}
|
||||
/// Apply `op` to the data component, leaving the timestamp unchanged.
|
||||
///
|
||||
/// `Nd` can differ from `D`, so this also acts as a type transform.
|
||||
pub fn data_transform<F, Nd>(self, mut op: F) -> TimedData<Ts, Nd>
|
||||
where
|
||||
F: FnMut(D) -> Nd,
|
||||
{
|
||||
TimedData {
|
||||
data: op(self.data),
|
||||
timestamp: self.timestamp,
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply `op` to the timestamp component, leaving the data unchanged.
|
||||
pub fn ts_transform<F>(self, mut op: F) -> Self
|
||||
where
|
||||
F: FnMut(Ts) -> Ts,
|
||||
{
|
||||
TimedData {
|
||||
data: self.data,
|
||||
timestamp: op(self.timestamp),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A timestamped payload extended with pipeline-stage options and a destination address.
|
||||
///
|
||||
/// `PipelineData` is the value flowing between client-side pipeline stages
|
||||
/// ([`Chunking`], [`Reliability`], [`Obfuscation`], [`RoutingSecurity`], [`Framing`],
|
||||
/// [`Transport`]). It carries:
|
||||
///
|
||||
/// - `data`: a [`TimedData`] pairing the payload with its scheduled timestamp,
|
||||
/// - `options`: per-message configuration consumed by the pipeline (typically an
|
||||
/// [`InputOptions`] implementor on the client side; `()` once the message is
|
||||
/// reduced to an addressed payload),
|
||||
/// - `dst`: the next-hop destination identifier the wire layer should send to.
|
||||
///
|
||||
/// [`Chunking`]: crate::clients::traits::Chunking
|
||||
/// [`Reliability`]: crate::clients::traits::Reliability
|
||||
/// [`Obfuscation`]: crate::clients::traits::Obfuscation
|
||||
/// [`RoutingSecurity`]: crate::clients::traits::RoutingSecurity
|
||||
/// [`Framing`]: crate::common::traits::Framing
|
||||
/// [`Transport`]: crate::common::traits::Transport
|
||||
/// [`InputOptions`]: crate::clients::InputOptions
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct PipelineData<Ts, D, Opts, NdId> {
|
||||
pub data: TimedData<Ts, D>,
|
||||
pub options: Opts,
|
||||
pub dst: NdId,
|
||||
}
|
||||
|
||||
impl<Ts, D, Opts, NdId> PipelineData<Ts, D, Opts, NdId> {
|
||||
/// Construct a new [`PipelineData`] from its parts.
|
||||
pub fn new(timestamp: Ts, data: D, options: Opts, dst: NdId) -> Self {
|
||||
PipelineData {
|
||||
data: TimedData::new(timestamp, data),
|
||||
options,
|
||||
dst,
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply `op` to the data component, leaving the timestamp, options, and
|
||||
/// destination unchanged.
|
||||
///
|
||||
/// `Nd` can differ from `D`, so this also acts as a type transform.
|
||||
pub fn data_transform<F, Nd>(self, op: F) -> PipelineData<Ts, Nd, Opts, NdId>
|
||||
where
|
||||
F: FnMut(D) -> Nd,
|
||||
{
|
||||
PipelineData {
|
||||
data: self.data.data_transform(op),
|
||||
options: self.options,
|
||||
dst: self.dst,
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply `op` to the timestamp component, leaving the data unchanged.
|
||||
pub fn ts_transform<F>(self, op: F) -> Self
|
||||
where
|
||||
F: FnMut(Ts) -> Ts,
|
||||
{
|
||||
PipelineData {
|
||||
data: self.data.ts_transform(op),
|
||||
options: self.options,
|
||||
dst: self.dst,
|
||||
}
|
||||
}
|
||||
|
||||
/// Drop the pipeline options, producing a plain addressed payload.
|
||||
pub fn into_addressed(self) -> AddressedTimedData<Ts, D, NdId> {
|
||||
AddressedTimedData {
|
||||
data: self.data,
|
||||
options: (),
|
||||
dst: self.dst,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Convenience alias for [`PipelineData`] when no per-message pipeline options
|
||||
/// are needed. Avoids duplicating the pipeline data structure.
|
||||
pub type AddressedTimedData<Ts, D, NdId> = PipelineData<Ts, D, (), NdId>;
|
||||
|
||||
impl<Ts, D, NdId> AddressedTimedData<Ts, D, NdId> {
|
||||
/// Construct a new [`AddressedTimedData`] with unit `options`.
|
||||
pub fn new_addressed(timestamp: Ts, data: D, dst: NdId) -> Self {
|
||||
AddressedTimedData {
|
||||
data: TimedData::new(timestamp, data),
|
||||
options: (),
|
||||
dst,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
pub mod traits;
|
||||
@@ -0,0 +1,60 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::{AddressedTimedData, PipelinePayload, TimedPayload};
|
||||
|
||||
use crate::common::traits::{WireUnwrappingPipeline, WireWrappingPipeline};
|
||||
|
||||
/// Top-level processing trait for a mix node.
|
||||
///
|
||||
/// Combines [`WireUnwrappingPipeline`] and [`WireWrappingPipeline`] with a blank [`mix`]
|
||||
/// step that the implementor fills in (decrypt, route, re-encrypt, cover traffic, etc.).
|
||||
///
|
||||
/// # Type Parameters
|
||||
/// - `Ts`: Timestamp / tick-context type.
|
||||
/// - `Pkt`: Transport packet type; the same type is consumed and produced.
|
||||
/// - `Opts`: Per-message pipeline options carried into the re-wrapping side.
|
||||
/// - `Mk`: Message-kind marker returned by the unwrap side.
|
||||
/// - `NdId`: Identifier type for the next-hop destination.
|
||||
///
|
||||
/// Frame types are owned by the wire sub-traits as associated items and do not
|
||||
/// appear in this trait's parameter list.
|
||||
///
|
||||
/// # Required Methods
|
||||
/// - `mix`: Given a reassembled payload and the current timestamp, return zero or more
|
||||
/// [`PipelinePayload`]s carrying their next-hop addresses to be re-wrapped and forwarded.
|
||||
///
|
||||
/// # Provided Methods
|
||||
/// - `process`: Unwraps the incoming packet via [`WireUnwrappingPipeline::wire_unwrap`],
|
||||
/// passes the result to [`mix`], and re-wraps each output payload via
|
||||
/// [`WireWrappingPipeline::wire_wrap`].
|
||||
///
|
||||
/// [`mix`]: NymNodeProcessingPipeline::mix
|
||||
pub trait NymNodeProcessingPipeline<Ts, Pkt, Opts, Mk, NdId>:
|
||||
WireUnwrappingPipeline<Ts, Pkt, Mk> + WireWrappingPipeline<Ts, Pkt, Opts, NdId>
|
||||
where
|
||||
Ts: Clone,
|
||||
NdId: Clone,
|
||||
{
|
||||
fn mix(
|
||||
&mut self,
|
||||
message_kind: Mk,
|
||||
payload: TimedPayload<Ts>,
|
||||
timestamp: Ts,
|
||||
) -> Vec<PipelinePayload<Ts, Opts, NdId>>;
|
||||
|
||||
fn process(
|
||||
&mut self,
|
||||
input: Pkt,
|
||||
timestamp: Ts,
|
||||
) -> Result<Vec<AddressedTimedData<Ts, Pkt, NdId>>, Self::Error> {
|
||||
let Some((payload, kind)) = self.wire_unwrap(input, timestamp.clone())? else {
|
||||
return Ok(Vec::new());
|
||||
};
|
||||
let mixed = self.mix(kind, payload, timestamp);
|
||||
Ok(mixed
|
||||
.into_iter()
|
||||
.flat_map(|addressed_data| self.wire_wrap(addressed_data).into_iter())
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
@@ -110,7 +110,9 @@ impl LpFrame {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn len(&self) -> usize {
|
||||
// is_empty in the sense len == 0 doesn't make sense in that case
|
||||
#[allow(clippy::len_without_is_empty)]
|
||||
pub fn len(&self) -> usize {
|
||||
LpFrameHeader::SIZE + self.content.len()
|
||||
}
|
||||
}
|
||||
@@ -165,6 +167,8 @@ impl SphinxStreamFrameAttributes {
|
||||
}
|
||||
|
||||
pub fn parse(attrs: &LpFrameAttributes) -> Result<Self, MalformedLpPacketError> {
|
||||
// SAFETY : 8 bytes slice into 8 bytes array
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let stream_id = u64::from_be_bytes(attrs[0..8].try_into().unwrap());
|
||||
let msg_type = match attrs[8] {
|
||||
0 => SphinxStreamMsgType::Open,
|
||||
@@ -175,6 +179,8 @@ impl SphinxStreamFrameAttributes {
|
||||
)));
|
||||
}
|
||||
};
|
||||
// SAFETY : 4 bytes slice into 4 bytes array
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let sequence_num = u32::from_be_bytes(attrs[9..13].try_into().unwrap());
|
||||
Ok(Self {
|
||||
stream_id,
|
||||
@@ -1,11 +1,13 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::packet::error::MalformedLpPacketError;
|
||||
use crate::packet::version;
|
||||
use crate::{packet::error::MalformedLpPacketError, peer_config::LpReceiverIndex};
|
||||
use bytes::{BufMut, BytesMut};
|
||||
use tracing::warn;
|
||||
|
||||
pub type LpReceiverIndex = u32;
|
||||
|
||||
/// Outer header (12 bytes) - always cleartext, used for routing.
|
||||
///
|
||||
/// This is the first 12 bytes of every LP packet, containing only the fields
|
||||
@@ -118,6 +120,8 @@ pub struct LpHeader {
|
||||
}
|
||||
|
||||
impl LpHeader {
|
||||
pub const SIZE: usize = OuterHeader::SIZE + InnerHeader::SIZE;
|
||||
|
||||
pub fn new(receiver_idx: LpReceiverIndex, counter: u64, protocol_version: u8) -> Self {
|
||||
Self {
|
||||
outer: OuterHeader {
|
||||
@@ -13,7 +13,6 @@ pub use header::{InnerHeader, LpHeader, OuterHeader};
|
||||
pub mod error;
|
||||
pub mod frame;
|
||||
pub mod header;
|
||||
pub mod replay;
|
||||
|
||||
pub mod version {
|
||||
/// The current version of the Lewes Protocol that is put into each new constructed header.
|
||||
@@ -0,0 +1,222 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use nym_lp_data::packet::{
|
||||
LpFrame, LpHeader, LpPacket,
|
||||
frame::{LpFrameHeader, LpFrameKind},
|
||||
};
|
||||
|
||||
use nym_lp_data::{
|
||||
AddressedTimedData, PipelinePayload,
|
||||
clients::{
|
||||
InputOptions,
|
||||
traits::{Chunking, Obfuscation, Reliability, RoutingSecurity},
|
||||
},
|
||||
common::traits::{Framing, Transport},
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct BasicOptions {
|
||||
pub reliability: bool,
|
||||
pub security: bool,
|
||||
pub obfuscation: bool,
|
||||
pub next_hop: u8,
|
||||
}
|
||||
|
||||
impl InputOptions<u8> for BasicOptions {
|
||||
fn reliability(&self) -> bool {
|
||||
self.reliability
|
||||
}
|
||||
|
||||
fn routing_security(&self) -> bool {
|
||||
self.security
|
||||
}
|
||||
|
||||
fn obfuscation(&self) -> bool {
|
||||
self.obfuscation
|
||||
}
|
||||
|
||||
fn next_hop(&self) -> u8 {
|
||||
self.next_hop
|
||||
}
|
||||
}
|
||||
|
||||
pub type BasicPipelinePayload<Ts> = PipelinePayload<Ts, BasicOptions, u8>;
|
||||
|
||||
pub struct MockChunking;
|
||||
impl<Ts> Chunking<Ts, BasicOptions, u8> for MockChunking
|
||||
where
|
||||
Ts: Clone,
|
||||
{
|
||||
fn chunked(
|
||||
&mut self,
|
||||
input: Vec<u8>,
|
||||
input_options: BasicOptions,
|
||||
chunk_size: usize,
|
||||
timestamp: Ts,
|
||||
) -> Vec<BasicPipelinePayload<Ts>> {
|
||||
input
|
||||
.chunks(chunk_size)
|
||||
.map(|chunk| {
|
||||
BasicPipelinePayload::new(
|
||||
timestamp.clone(),
|
||||
chunk.to_vec(),
|
||||
input_options,
|
||||
input_options.next_hop(),
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MockReliability;
|
||||
|
||||
impl MockReliability {
|
||||
const HEADER: &[u8; 5] = b"0KCP0";
|
||||
}
|
||||
|
||||
impl<Ts> Reliability<Ts, BasicOptions, u8> for MockReliability {
|
||||
const OVERHEAD_SIZE: usize = Self::HEADER.len();
|
||||
fn reliable_encode(
|
||||
&mut self,
|
||||
input: Option<BasicPipelinePayload<Ts>>,
|
||||
_: Ts,
|
||||
) -> Vec<BasicPipelinePayload<Ts>> {
|
||||
input
|
||||
.map(|data| {
|
||||
vec![data.data_transform(|data| {
|
||||
let mut packet = Self::HEADER.to_vec();
|
||||
packet.extend(data);
|
||||
packet
|
||||
})]
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MockSphinxSecurity {
|
||||
pub nb_frames: usize,
|
||||
}
|
||||
|
||||
impl MockSphinxSecurity {
|
||||
const HEADER: &[u8; 8] = b"0SPHINX0";
|
||||
}
|
||||
|
||||
impl<Ts> RoutingSecurity<Ts, BasicOptions, u8> for MockSphinxSecurity {
|
||||
const OVERHEAD_SIZE: usize = Self::HEADER.len();
|
||||
|
||||
fn nb_frames(&self) -> usize {
|
||||
self.nb_frames
|
||||
}
|
||||
|
||||
fn encrypt(&mut self, input: BasicPipelinePayload<Ts>) -> BasicPipelinePayload<Ts> {
|
||||
input.data_transform(|data| {
|
||||
let mut packet = Self::HEADER.to_vec();
|
||||
packet.extend(data);
|
||||
packet
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct KekwObfuscation;
|
||||
|
||||
impl Obfuscation<u32, BasicOptions, u8> for KekwObfuscation {
|
||||
fn obfuscate(
|
||||
&mut self,
|
||||
input: Option<BasicPipelinePayload<u32>>,
|
||||
_timestamp: u32,
|
||||
) -> Vec<BasicPipelinePayload<u32>> {
|
||||
if let Some(input) = input {
|
||||
vec![input.ts_transform(|ts| ts + 1)]
|
||||
} else {
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub struct ReallyOddObfuscation {
|
||||
next_ts: u32,
|
||||
}
|
||||
|
||||
impl ReallyOddObfuscation {
|
||||
#[allow(dead_code)]
|
||||
pub fn new(start_ts: u32) -> Self {
|
||||
let next_ts = if !start_ts.is_multiple_of(2) {
|
||||
start_ts
|
||||
} else {
|
||||
start_ts + 1
|
||||
};
|
||||
Self { next_ts }
|
||||
}
|
||||
}
|
||||
|
||||
impl Obfuscation<u32, BasicOptions, u8> for ReallyOddObfuscation {
|
||||
fn obfuscate(
|
||||
&mut self,
|
||||
input: Option<BasicPipelinePayload<u32>>,
|
||||
_timestamp: u32,
|
||||
) -> Vec<BasicPipelinePayload<u32>> {
|
||||
if let Some(input) = input {
|
||||
let pkt = input.ts_transform(|_| self.next_ts);
|
||||
self.next_ts += 2;
|
||||
vec![pkt]
|
||||
} else {
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MockLpFraming;
|
||||
|
||||
impl MockLpFraming {
|
||||
const FRAME_ATTRIBUTES: &[u8; 14] = b"0LpFrameAttrs0";
|
||||
}
|
||||
|
||||
impl<Ts> Framing<Ts, BasicOptions, u8> for MockLpFraming
|
||||
where
|
||||
Ts: Clone,
|
||||
{
|
||||
type Frame = LpFrame;
|
||||
const OVERHEAD_SIZE: usize = LpFrameHeader::SIZE;
|
||||
fn to_frame(
|
||||
&mut self,
|
||||
input: PipelinePayload<Ts, BasicOptions, u8>,
|
||||
frame_size: usize,
|
||||
) -> Vec<AddressedTimedData<Ts, LpFrame, u8>> {
|
||||
input
|
||||
.data
|
||||
.data
|
||||
.chunks(frame_size)
|
||||
.map(|frame_payload| {
|
||||
let header = LpFrameHeader::new(LpFrameKind::Opaque, *Self::FRAME_ATTRIBUTES);
|
||||
|
||||
AddressedTimedData::new_addressed(
|
||||
input.data.timestamp.clone(),
|
||||
LpFrame {
|
||||
header,
|
||||
content: frame_payload.to_vec().into(),
|
||||
},
|
||||
input.dst,
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MockLpTransport;
|
||||
|
||||
impl<Ts> Transport<Ts, LpPacket, u8> for MockLpTransport {
|
||||
type Frame = LpFrame;
|
||||
const OVERHEAD_SIZE: usize = LpHeader::SIZE;
|
||||
fn to_transport_packet(
|
||||
&mut self,
|
||||
input: AddressedTimedData<Ts, Self::Frame, u8>,
|
||||
) -> AddressedTimedData<Ts, LpPacket, u8> {
|
||||
AddressedTimedData::new_addressed(
|
||||
input.data.timestamp,
|
||||
LpPacket::new(LpHeader::new(7, 7, 7), input.data.data),
|
||||
input.dst,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use nym_lp_data::clients::{traits::ClientWrappingPipeline, types::Pipeline};
|
||||
|
||||
use crate::common::{
|
||||
KekwObfuscation, MockChunking, MockLpFraming, MockLpTransport, MockReliability,
|
||||
MockSphinxSecurity,
|
||||
};
|
||||
|
||||
mod common;
|
||||
|
||||
#[test]
|
||||
fn empty_input_yields_empty_output() {
|
||||
let packet_size = 64;
|
||||
let security_layer_nb_frames = 2;
|
||||
|
||||
let mut mock_pipeline = Pipeline {
|
||||
chunking: MockChunking,
|
||||
reliability: MockReliability,
|
||||
security: MockSphinxSecurity {
|
||||
nb_frames: security_layer_nb_frames,
|
||||
},
|
||||
obfuscation: KekwObfuscation,
|
||||
framing: MockLpFraming,
|
||||
transport: MockLpTransport,
|
||||
packet_size,
|
||||
};
|
||||
|
||||
let output = mock_pipeline.process(None, 1);
|
||||
|
||||
assert!(output.is_empty());
|
||||
}
|
||||
|
||||
// TODO More test to come later
|
||||
@@ -25,10 +25,10 @@ nym-crypto = { workspace = true, features = ["hashing"] }
|
||||
nym-common.workspace = true
|
||||
nym-kkt = { workspace = true }
|
||||
nym-kkt-ciphersuite = { workspace = true }
|
||||
nym-lp-data.workspace = true
|
||||
|
||||
# libcrux dependencies for PSQ (Post-Quantum PSK derivation)
|
||||
libcrux-psq = { workspace = true, features = ["test-utils"] }
|
||||
num_enum = { workspace = true }
|
||||
zeroize = { workspace = true, features = ["zeroize_derive"] }
|
||||
|
||||
|
||||
@@ -48,3 +48,6 @@ mock = ["nym-test-utils"]
|
||||
[[bench]]
|
||||
name = "replay_protection"
|
||||
harness = false
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use criterion::{BenchmarkId, Criterion, Throughput, black_box, criterion_group, criterion_main};
|
||||
use nym_lp::replay::ReceivingKeyCounterValidator;
|
||||
use nym_test_utils::helpers::deterministic_rng_09;
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::LpError;
|
||||
use crate::packet::{EncryptedLpPacket, InnerHeader, LpFrame, LpHeader, LpPacket};
|
||||
use bytes::BytesMut;
|
||||
use libcrux_psq::Channel;
|
||||
use nym_lp_data::packet::{EncryptedLpPacket, InnerHeader, LpFrame, LpHeader, LpPacket};
|
||||
|
||||
// needs to be equal or above to the actual overhead
|
||||
pub(crate) const SANE_ENC_OVERHEAD: usize = 32;
|
||||
@@ -82,12 +82,12 @@ pub(crate) fn decrypt_lp_packet(
|
||||
mod tests {
|
||||
use crate::LpError;
|
||||
use crate::codec::{decrypt_data, decrypt_lp_packet, encrypt_data, encrypt_lp_packet};
|
||||
use crate::packet::{EncryptedLpPacket, LpFrame, LpHeader, LpPacket};
|
||||
use crate::peer::mock_peers;
|
||||
use crate::psq::initiator::{build_psq_ciphersuite, build_psq_principal};
|
||||
use crate::psq::{PSQ_MSG2_SIZE, psq_msg1_size, responder};
|
||||
use libcrux_psq::{Channel, IntoSession};
|
||||
use nym_kkt_ciphersuite::KEM;
|
||||
use nym_lp_data::packet::{EncryptedLpPacket, LpFrame, LpHeader, LpPacket};
|
||||
use nym_test_utils::helpers::u64_seeded_rng_09;
|
||||
|
||||
fn mock_transport() -> (
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user