Compare commits

...

35 Commits

Author SHA1 Message Date
mfahampshire 23f33bb982 ignore pnpm 2026-05-28 10:30:49 +01:00
serinko ea4d657fdf syntax fix 2026-05-20 11:46:43 +02:00
serinko fe144da4b2 syntax fix 2026-05-20 11:32:25 +02:00
serinko 00aedbb144 turtle beats the rabbitai 2026-05-20 10:45:55 +02:00
serinko 6a33354f84 correct comment syntax 2026-05-20 10:43:20 +02:00
serinko 19d8381950 new component import 2026-05-20 10:39:40 +02:00
serinko 27266e5e68 placeholding reminder for later 2026-05-20 10:39:09 +02:00
serinko 371a0b2ba8 operators updates 2026-05-20 10:38:40 +02:00
import this bde2b07d0d NTM: NIP-11 exit policy update (#6807) 2026-05-20 08:30:49 +00:00
import this 26538f5a40 Patch: Linux kernel vulnerability patch (#6773)
* add guide component

* add mitigate kernel playbook

* add to troubleshooting

* remove redundant

* remove redundant

* FIX ISSUES

* fix

* fix url to raw

* update docs and add new playbook

* update and simplify docs and ansible

* create ntm explanation component and import it

* rm mistaken empty file

* rm crap

* rm crap

* rm all crap

* try to fix nextra screaming seagul

* try to fix nextra screaming seagul

* try to fix nextra screaming seagul

* UX improvement by logic refactoring

* UX improvement by logic refactoring

* UX improvement by logic refactoring

* UX improvement by logic refactoring

* fix header urls

* fix command syntax

* fix indentation

* update auto-stats

* resolve review comments

* resolve review comments in docs

* fix remove kernel book

* soften warning

* address comments

* address comments

* update stats
2026-05-20 09:17:36 +02:00
dependabot[bot] 483bb6f477 build(deps): bump pnpm/action-setup from 4.2.0 to 5.0.0 (#6571)
Bumps [pnpm/action-setup](https://github.com/pnpm/action-setup) from 4.2.0 to 5.0.0.
- [Release notes](https://github.com/pnpm/action-setup/releases)
- [Commits](https://github.com/pnpm/action-setup/compare/v4.2.0...v5.0.0)

---
updated-dependencies:
- dependency-name: pnpm/action-setup
  dependency-version: 5.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-19 16:37:52 +01:00
dependabot[bot] a68355a75a Bump tauri from 2.10.3 to 2.11.1 in /nym-wallet (#6742)
Bumps [tauri](https://github.com/tauri-apps/tauri) from 2.10.3 to 2.11.1.
- [Release notes](https://github.com/tauri-apps/tauri/releases)
- [Commits](https://github.com/tauri-apps/tauri/compare/tauri-v2.10.3...tauri-v2.11.1)

---
updated-dependencies:
- dependency-name: tauri
  dependency-version: 2.11.1
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-19 16:36:30 +01:00
dependabot[bot] 1572d8e5c2 Bump rand from 0.8.5 to 0.8.6 in /contracts (#6702)
Bumps [rand](https://github.com/rust-random/rand) from 0.8.5 to 0.8.6.
- [Release notes](https://github.com/rust-random/rand/releases)
- [Changelog](https://github.com/rust-random/rand/blob/0.8.6/CHANGELOG.md)
- [Commits](https://github.com/rust-random/rand/compare/0.8.5...0.8.6)

---
updated-dependencies:
- dependency-name: rand
  dependency-version: 0.8.6
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-19 15:49:54 +01:00
dependabot[bot] fd76c5ca4d build(deps): bump microsoft/setup-msbuild from 2 to 3 (#6602)
Bumps [microsoft/setup-msbuild](https://github.com/microsoft/setup-msbuild) from 2 to 3.
- [Release notes](https://github.com/microsoft/setup-msbuild/releases)
- [Commits](https://github.com/microsoft/setup-msbuild/compare/v2...v3)

---
updated-dependencies:
- dependency-name: microsoft/setup-msbuild
  dependency-version: '3'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-19 15:49:24 +01:00
dependabot[bot] f94589c2d1 build(deps): bump tar from 0.4.44 to 0.4.45 (#6595)
Bumps [tar](https://github.com/alexcrichton/tar-rs) from 0.4.44 to 0.4.45.
- [Commits](https://github.com/alexcrichton/tar-rs/compare/0.4.44...0.4.45)

---
updated-dependencies:
- dependency-name: tar
  dependency-version: 0.4.45
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-19 15:49:14 +01:00
dependabot[bot] 1c40499829 build(deps): bump quinn-proto from 0.11.12 to 0.11.14 (#6549)
Bumps [quinn-proto](https://github.com/quinn-rs/quinn) from 0.11.12 to 0.11.14.
- [Release notes](https://github.com/quinn-rs/quinn/releases)
- [Commits](https://github.com/quinn-rs/quinn/compare/quinn-proto-0.11.12...quinn-proto-0.11.14)

---
updated-dependencies:
- dependency-name: quinn-proto
  dependency-version: 0.11.14
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-19 15:48:31 +01:00
dependabot[bot] f8a4d5f1ff build(deps): bump quinn-proto from 0.11.10 to 0.11.14 in /nym-wallet (#6548)
Bumps [quinn-proto](https://github.com/quinn-rs/quinn) from 0.11.10 to 0.11.14.
- [Release notes](https://github.com/quinn-rs/quinn/releases)
- [Commits](https://github.com/quinn-rs/quinn/compare/quinn-proto-0.11.10...quinn-proto-0.11.14)

---
updated-dependencies:
- dependency-name: quinn-proto
  dependency-version: 0.11.14
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-19 15:48:20 +01:00
dependabot[bot] 42807890af build(deps): bump docker/login-action from 3 to 4 (#6518)
Bumps [docker/login-action](https://github.com/docker/login-action) from 3 to 4.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-19 15:48:09 +01:00
dependabot[bot] 5aa576b596 build(deps): bump actions/download-artifact from 7 to 8 (#6497)
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 7 to 8.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v7...v8)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-version: '8'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-19 15:47:59 +01:00
dependabot[bot] 0215ad9294 build(deps): bump actions/upload-artifact from 6 to 7 (#6496)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 6 to 7.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-19 15:47:26 +01:00
ZM 227e6a10e1 fix(ecash): cast usize to u64 in to_bytes() for 32-bit platform compatibility (#6528)
VerificationKeyAuth::to_bytes() and SecretKeyAuth::to_bytes() used
usize::to_le_bytes() to serialize vector lengths, producing 4 bytes on
32-bit and 8 bytes on 64-bit. Since from_bytes() always reads 8 bytes
(u64), this caused ZK proof challenge hash mismatches when a 32-bit
client's proof was verified by a 64-bit gateway, resulting in
"the provided ticket failed to get verified" on all 32-bit platforms.
2026-05-19 15:17:24 +01:00
Jędrzej Stuczyński d3b6a270de chore: expose admin method for migrating vesting delegations/mixnodes (#6795)
* chore: expose admin method for migrating vesting delegations/mixnodes

* don't error out on vested delegation no longer existing - perform a noop instead

* cargo fmt

* add message for batch migration
2026-05-19 15:13:03 +01:00
mfahampshire e12ada0105 Point mobile reference section at nymvpn setup (#6776) 2026-05-19 13:00:51 +00:00
Simon Wicky 71d50d79c2 fix clippy 1.95 lints (#6794) 2026-05-19 14:21:12 +02:00
Jędrzej Stuczyński a21a01cf1a node families (#6715)
* start node families topic branch

* start node families topic branch

* initialise node families contract

* define contract storage

* registering new family in storage

* accepting family invitation

* add_pending_invitation

* revoke_pending_invitation

* remove_family_member

* reject_pending_invitation

* disband_family

* added unit tests for the storage methods

* added restriction on uniquness of family names

* update rustc version for node families contract common

* clippy

* basic queries by id

* query_families_paged

* change family membership storage and expose query for all members of a family

* queries for pending invitations

* queries for past invitations

* queries for past data per node

* queries for past family members

* query_past_members_for_node_paged

* queries for family by name and by owner

* fixup family name normalisation

* fixed incorrect lower bound for queries for past data

* implement contract and storage initialisation

* stubbing tx messages that are to be exposed by the contract

* handler for updating config

* removed partial fee return

* wip: create family

* move mixnet contract interaction traits to shared location

* store original family name alongside the normalised variant

* prevent family creation if owner has a node in another family

* try_disband_family

* try_invite_to_family + shared helpers

* try_revoke_family_invitation

* accept_family_invitation

* stub method for node unbonding

* try_reject_family_invitation

* unit tests for family name normalisation

* try_leave_family

* try_kick_from_family

* fix outdated comments and add paid fee event attribute

* feat: NMv3: leave family upon node unbonding

* NF contract handling of unbonding

* lints

* init node families contract when creating performance contract tester

* clippy

* avoid self-dep in the contract dev deps

* introduced client traits for interacting with the node families contract

* add node families contract to cache refresher

* added query for all node family members (globally) and started scaffolding nym-api caches

* docs and cache -> api conversion

* calculating average node age based on individual timestamps

* wire up node families cache

* http stubs

* filled in the implementation

* route tests + extracting shared code

* review fixes

* feat: expose family information for all dvpn gateway endpoints within NS API

* expose family information for explorer v3 route

* clippy

* review comments and optimise db family update

* feat: Node Families: expose stake information inside DVpnGateway

* chore: update lock files after rebase

* chore: sort workspace members

* explicitly require providing node families contract address for mixnet contract migration

* fix missing node families contract address env export

* dont swallow cache overwrite failures in fixture

* pin network-defaults rustc version due to contracts dep

* further version pinning

* chore: update mixnet contract schema
2026-05-19 10:36:20 +01:00
Jack Wampler 362f84b5f6 Handle Rate Limit Challenge Response (#6786)
rotate urls on HTTP response error indicating API rate limiting
2026-05-18 08:47:41 -06:00
benedettadavico daed9cd15b Merge branch 'release/2026.9-venaco' into develop 2026-05-16 06:27:52 +02:00
Jack Wampler a53ca71bd2 Re-order default API urls for network details (#6767) 2026-05-15 09:46:33 -06:00
mfahampshire a70e68c7bd Max/smolmix docs (#6716)
* Smolmix documentation

* Add smolmix docs: landing page, tutorials, and developer page links

* Add Exit Gateway services page (NR vs IPR) and link from existing docs

* Update auto-generated command and API outputs

* Reorg of tutorials and architecture pages

* License information + remove TODO from docs.rs visibile comment + reorg
readme

* Add versions file for doc-wide versioning

* Relative -> absolute links

* Relative -> absolute links

* Update license + add old tutorial code as examples

* Streamline smolmix docs

* Clippy

* Clean up doc comments

* Last pass

* Add larger file download to list

* set new versions

* Clippy

* Remove blake pin from docs + add version range to root Cargo.toml

* Format example logging

* Remove crate blocked component

* Loose whitespace

* Add doc verification script for inline mdx

* Formatting

* Components regen

* Reorg + tighten text

* Voicing cohesion pass + remove bloated examples

* Voicing cont.

* Reduce max download size

* Small suggested clarifications

* Max/docs voicing consistency (#6769)

* Reduce max download size

* voicing consistency across docs

* New landing order w smolmix

* Tweaks

* Final tweaks
2026-05-13 11:19:44 +00:00
import this fdebed7c38 Bugfix: nym-node-cly.py argument mismatch fix and sync up with NTM updates (#6743)
* fix argument missmatch and sync args with recent NTM update

* fix wg_enabled check & name consistency

* correct env.os saving persisting vars logic

* fix naming issue
2026-05-12 11:52:46 +02:00
benedetta davico f576a4ee2d Merge pull request #6764 from nymtech/bdq/add-ci-build-NM-agents
add ci for NM agent binary
2026-05-12 10:40:23 +02:00
benedettadavico a9aafd785e publish NM agent binary 2026-05-12 10:34:34 +02:00
benedetta davico 0f7dbb94a8 fix for crates (#6745)
* version fix

* try to publish core crates first

* bump version ci

* fix to yaml

* Slight modifications to ordering, remove core-crates and rely on  ordering as test + sed tweak

* crates release: bump version to 1.21.0 (#6744)

Co-authored-by: Nym bot <nym-bot@users.noreply.github.com>
Co-authored-by: mfahampshire <maxhampshire@pm.me>

* Remove unnecessary verification step becase of dryrun (doubled)

* Revert some changes to develop

* Add preflight to its own workflow

* Clippy

* Update crate publishing file

* Clippy

---------

Co-authored-by: benedettadavico <benedettadavico@users.noreply.github.com>
Co-authored-by: mfahampshire <maxhampshire@pm.me>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Nym bot <nym-bot@users.noreply.github.com>
2026-05-11 14:50:14 +00:00
Jędrzej Stuczyński 2d72b1b201 feat: introduce shared contract caches within Nym API (#6760)
it has been extracted from the mix stress testing branch and it is going to be used within node families branch
2026-05-11 13:02:37 +01:00
Jędrzej Stuczyński 412657f773 chore: removed dead code for redundant mixnet-vesting integration tests (#6759) 2026-05-11 10:03:56 +01:00
311 changed files with 23983 additions and 5243 deletions
+2 -2
View File
@@ -25,14 +25,14 @@ jobs:
echo "file2=$(ls nym-vpn*.deb)" >> $GITHUB_ENV
- name: Upload nym-repo-setup
uses: actions/upload-artifact@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
+4 -1
View File
@@ -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
@@ -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/"
+19
View File
@@ -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: |
+5 -1
View File
@@ -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 }}
+29 -2
View File
@@ -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
+4 -1
View File
@@ -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
+1 -1
View File
@@ -20,7 +20,7 @@ jobs:
find . -name Cargo.toml -exec cargo deny --manifest-path {} check \
advisories -A advisory-not-detected --hide-inclusion-graph \; &> \
>(uniq &> .github/workflows/support-files/notifications/deny.message )
- uses: actions/upload-artifact@v6
- uses: actions/upload-artifact@v7
with:
name: report
path: .github/workflows/support-files/notifications/deny.message
+1 -1
View File
@@ -66,7 +66,7 @@ jobs:
args: --workspace --release ${{ env.CARGO_FEATURES }}
- name: Upload Artifact
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v7
with:
name: my-artifact
path: |
+2 -2
View File
@@ -27,14 +27,14 @@ jobs:
run: make contracts
- name: Upload Mixnet Contract Artifact
uses: actions/upload-artifact@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
@@ -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
@@ -132,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: |
@@ -38,7 +38,7 @@ 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.
@@ -165,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: |
@@ -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
+1 -1
View File
@@ -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 }}
+1 -1
View File
@@ -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 }}
+1 -1
View File
@@ -11,7 +11,7 @@ jobs:
runs-on: arc-ubuntu-22.04-dind
steps:
- name: Login to Harbor
uses: docker/login-action@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 }}
+1 -1
View File
@@ -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 }}
+1 -1
View File
@@ -11,7 +11,7 @@ jobs:
runs-on: arc-ubuntu-22.04-dind
steps:
- name: Login to Harbor
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
registry: harbor.nymte.ch
username: ${{ secrets.HARBOR_ROBOT_USERNAME }}
+1 -1
View File
@@ -11,7 +11,7 @@ jobs:
runs-on: arc-ubuntu-22.04-dind
steps:
- name: Login to Harbor
uses: docker/login-action@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 }}
+1 -1
View File
@@ -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
+1
View File
@@ -78,3 +78,4 @@ CLAUDE.md
/notes
/target-otel
test-tutorials/
Generated
+141 -123
View File
File diff suppressed because it is too large Load Diff
+125 -121
View File
@@ -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,14 @@ 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-metrics",
"common/nym_offline_compact_ecash",
"common/nymnoise",
@@ -90,9 +93,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 +125,7 @@ members = [
"common/zulip-client",
"documentation/autodoc",
"gateway",
"integration-tests",
"nym-api",
"nym-api/nym-api-requests",
"nym-authenticator-client",
@@ -129,6 +133,7 @@ members = [
"nym-credential-proxy/nym-credential-proxy",
"nym-credential-proxy/nym-credential-proxy-requests",
"nym-data-observatory",
"nym-gateway-probe",
"nym-ip-packet-client",
"nym-network-monitor",
"nym-node",
@@ -140,6 +145,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 +153,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 +177,23 @@ 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-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"
]
exclude = ["contracts", "nym-wallet", "cpu-cycles"]
@@ -206,7 +207,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 +230,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"
@@ -325,7 +326,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 +350,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 +360,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"
@@ -414,110 +414,113 @@ libcrux-sha3 = "0.0.8"
libcrux-traits = "0.0.6"
# Workspace dep definitions required by crates.io publication - we need a workspace version since `cargo workspaces` doesn't work with path imports from crate manifests
nym-api-requests = { version = "1.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-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 +619,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') }}"
+2
View File
@@ -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)
@@ -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],
&params,
)
.await
}
async fn get_all_node_families(&self) -> Result<Vec<NodeFamily>, NymAPIError> {
// TODO: deal with paging in macro or some helper function or something, because it's the same pattern everywhere
let mut page = 0;
let mut families = Vec::new();
loop {
let mut res = self.get_node_families(Some(page), None).await?;
families.append(&mut res.data);
if families.len() < res.pagination.total {
page += 1
} else {
break;
}
}
Ok(families)
}
#[deprecated]
#[tracing::instrument(level = "debug", skip_all)]
async fn get_basic_mixnodes(&self) -> Result<CachedNodesResponse<SkimmedNodeV1>, NymAPIError> {
@@ -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())
@@ -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(),
};
}
}
@@ -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>,
}
+3 -1
View File
@@ -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
+46 -8
View File
@@ -1173,7 +1173,16 @@ impl ApiClientCore for Client {
};
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();
@@ -1226,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() {
@@ -1697,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")]
+1
View File
@@ -32,6 +32,7 @@ mod tests {
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);
}
}
+3 -1
View File
@@ -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
+5 -5
View File
@@ -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/",
+15
View File
@@ -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);
+1
View File
@@ -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 -1
View File
@@ -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
@@ -31,9 +31,7 @@ where
// instead we could maybe use the `from_bytes` variant and adding some suffix
// when computing the digest until we produce a valid scalar.
let mut bytes = [0u8; 64];
let pad_size = 64usize
.checked_sub(D::OutputSize::to_usize())
.unwrap_or_default();
let pad_size = 64usize.saturating_sub(D::OutputSize::to_usize());
bytes[pad_size..].copy_from_slice(&digest);
@@ -112,7 +112,7 @@ impl SecretKeyAuth {
let ys_len = self.ys.len();
let mut bytes = Vec::with_capacity(8 + (ys_len + 1) * 32);
bytes.extend_from_slice(&self.x.to_bytes());
bytes.extend_from_slice(&ys_len.to_le_bytes());
bytes.extend_from_slice(&(ys_len as u64).to_le_bytes());
for y in self.ys.iter() {
bytes.extend_from_slice(&y.to_bytes())
}
@@ -337,7 +337,7 @@ impl VerificationKeyAuth {
bytes.extend_from_slice(&self.alpha.to_affine().to_compressed());
bytes.extend_from_slice(&beta_g1_len.to_le_bytes());
bytes.extend_from_slice(&(beta_g1_len as u64).to_le_bytes());
for beta_g1 in self.beta_g1.iter() {
bytes.extend_from_slice(&beta_g1.to_affine().to_compressed())
+3 -1
View File
@@ -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
+2 -3
View File
@@ -109,8 +109,8 @@ impl PacketSender {
let mut results = Vec::with_capacity(self.packets_per_node);
let mut seq = self.random_sequence_number();
for _ in 0..self.packets_per_node {
let start_seq = self.random_sequence_number();
for seq in start_seq..start_seq + self.packets_per_node as u64 {
let packet = EchoPacket::new(seq, &self.identity);
let start = Instant::now();
// TODO: should we get the start time after or before actually sending the data?
@@ -210,7 +210,6 @@ impl PacketSender {
let time_taken = Instant::now().duration_since(start);
results.push(time_taken);
seq += 1;
sleep(self.delay_between_packets).await;
}
+78 -261
View File
@@ -2,17 +2,6 @@
# It is not intended for manual editing.
version = 4
[[package]]
name = "aes"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
dependencies = [
"cfg-if",
"cipher",
"cpufeatures",
]
[[package]]
name = "ahash"
version = "0.8.11"
@@ -87,7 +76,7 @@ dependencies = [
"ark-serialize",
"ark-std",
"derivative",
"digest 0.10.7",
"digest",
"itertools 0.10.5",
"num-bigint",
"num-traits",
@@ -141,7 +130,7 @@ checksum = "adb7b85a02b83d2f22f89bd5cac66c9c89474240cb6207cb1efc16d098e822a5"
dependencies = [
"ark-serialize-derive",
"ark-std",
"digest 0.10.7",
"digest",
"num-bigint",
]
@@ -167,12 +156,6 @@ dependencies = [
"rayon",
]
[[package]]
name = "arrayref"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb"
[[package]]
name = "autocfg"
version = "1.2.0"
@@ -215,25 +198,13 @@ version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d965446196e3b7decd44aa7ee49e31d630118f90ef12f97900f262eb915c951d"
[[package]]
name = "blake2"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94cb07b0da6a73955f8fb85d24c466778e70cda767a568229b104f0264089330"
dependencies = [
"byte-tools",
"crypto-mac",
"digest 0.8.1",
"opaque-debug",
]
[[package]]
name = "block-buffer"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
dependencies = [
"generic-array 0.14.7",
"generic-array",
]
[[package]]
@@ -251,12 +222,6 @@ dependencies = [
"tinyvec",
]
[[package]]
name = "byte-tools"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7"
[[package]]
name = "byteorder"
version = "1.5.0"
@@ -321,26 +286,6 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "chacha"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddf3c081b5fba1e5615640aae998e0fbd10c24cbd897ee39ed754a77601a4862"
dependencies = [
"byteorder",
"keystream",
]
[[package]]
name = "cipher"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
dependencies = [
"crypto-common",
"inout",
]
[[package]]
name = "const-oid"
version = "0.9.6"
@@ -380,7 +325,7 @@ dependencies = [
"ark-serialize",
"cosmwasm-core",
"curve25519-dalek",
"digest 0.10.7",
"digest",
"ecdsa",
"ed25519-zebra",
"k256",
@@ -491,9 +436,9 @@ version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76"
dependencies = [
"generic-array 0.14.7",
"generic-array",
"rand_core",
"subtle 2.4.1",
"subtle",
"zeroize",
]
@@ -503,29 +448,10 @@ version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
dependencies = [
"generic-array 0.14.7",
"generic-array",
"typenum",
]
[[package]]
name = "crypto-mac"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4434400df11d95d556bac068ddfedd482915eb18fe8bea89bc80b6e4b1c179e5"
dependencies = [
"generic-array 0.12.4",
"subtle 1.0.0",
]
[[package]]
name = "ctr"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835"
dependencies = [
"cipher",
]
[[package]]
name = "curve25519-dalek"
version = "4.1.3"
@@ -535,10 +461,10 @@ dependencies = [
"cfg-if",
"cpufeatures",
"curve25519-dalek-derive",
"digest 0.10.7",
"digest",
"fiat-crypto",
"rustc_version",
"subtle 2.4.1",
"subtle",
"zeroize",
]
@@ -709,7 +635,7 @@ dependencies = [
"nym-contracts-common",
"nym-contracts-common-testing",
"nym-group-contract-common",
"nym-multisig-contract-common 1.20.4 (registry+https://github.com/rust-lang/crates.io-index)",
"nym-multisig-contract-common 1.21.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
@@ -796,15 +722,6 @@ dependencies = [
"unicode-xid",
]
[[package]]
name = "digest"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5"
dependencies = [
"generic-array 0.12.4",
]
[[package]]
name = "digest"
version = "0.10.7"
@@ -814,7 +731,7 @@ dependencies = [
"block-buffer",
"const-oid",
"crypto-common",
"subtle 2.4.1",
"subtle",
]
[[package]]
@@ -825,7 +742,7 @@ checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125"
[[package]]
name = "easy-addr"
version = "1.20.4"
version = "1.21.0"
dependencies = [
"cosmwasm-std",
"quote",
@@ -839,7 +756,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca"
dependencies = [
"der",
"digest 0.10.7",
"digest",
"elliptic-curve",
"rfc6979",
"signature",
@@ -866,7 +783,7 @@ dependencies = [
"rand_core",
"serde",
"sha2",
"subtle 2.4.1",
"subtle",
"zeroize",
]
@@ -899,13 +816,13 @@ checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47"
dependencies = [
"base16ct",
"crypto-bigint",
"digest 0.10.7",
"digest",
"ff",
"generic-array 0.14.7",
"generic-array",
"group",
"rand_core",
"sec1",
"subtle 2.4.1",
"subtle",
"zeroize",
]
@@ -922,7 +839,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449"
dependencies = [
"rand_core",
"subtle 2.4.1",
"subtle",
]
[[package]]
@@ -931,15 +848,6 @@ version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c007b1ae3abe1cb6f85a16305acd418b7ca6343b953633fee2b76d8f108b830f"
[[package]]
name = "generic-array"
version = "0.12.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ffdf9f34f1447443d37393cc6c2b8313aebddcd96906caf34e54c68d8e57d7bd"
dependencies = [
"typenum",
]
[[package]]
name = "generic-array"
version = "0.14.7"
@@ -970,7 +878,7 @@ checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63"
dependencies = [
"ff",
"rand_core",
"subtle 2.4.1",
"subtle",
]
[[package]]
@@ -1004,22 +912,13 @@ version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "hkdf"
version = "0.12.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7"
dependencies = [
"hmac",
]
[[package]]
name = "hmac"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
dependencies = [
"digest 0.10.7",
"digest",
]
[[package]]
@@ -1048,15 +947,6 @@ dependencies = [
"hashbrown 0.15.2",
]
[[package]]
name = "inout"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
dependencies = [
"generic-array 0.14.7",
]
[[package]]
name = "itertools"
version = "0.10.5"
@@ -1102,12 +992,6 @@ dependencies = [
"sha2",
]
[[package]]
name = "keystream"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c33070833c9ee02266356de0c43f723152bd38bd96ddf52c82b3af10c9138b28"
[[package]]
name = "konst"
version = "0.3.16"
@@ -1141,24 +1025,6 @@ version = "0.2.180"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc"
[[package]]
name = "libm"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de"
[[package]]
name = "lioness"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ae926706ba42c425c9457121178330d75e273df2e82e28b758faf3de3a9acb9"
dependencies = [
"arrayref",
"blake2",
"chacha",
"keystream",
]
[[package]]
name = "memchr"
version = "2.7.2"
@@ -1166,18 +1032,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d"
[[package]]
name = "mixnet-vesting-integration-tests"
name = "node-families"
version = "0.1.0"
dependencies = [
"anyhow",
"cosmwasm-schema",
"cosmwasm-std",
"cw-multi-test",
"cw-controllers",
"cw-storage-plus",
"cw-utils",
"cw2",
"nym-contracts-common",
"nym-contracts-common-testing",
"nym-crypto",
"nym-mixnet-contract",
"nym-mixnet-contract-common",
"nym-vesting-contract",
"nym-vesting-contract-common",
"rand_chacha",
"nym-node-families-contract-common",
"serde",
]
[[package]]
@@ -1212,7 +1083,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a"
dependencies = [
"autocfg",
"libm",
]
[[package]]
@@ -1248,9 +1118,9 @@ dependencies = [
[[package]]
name = "nym-coconut-dkg-common"
version = "1.20.4"
version = "1.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a95dc43ef8954a4f79846e3224434cf389d4a9c14b77f526dae3cfd2221c6141"
checksum = "fcdfaf17c8b2a73bd6d14a3b9432118d66cc2335a371f185791099d32c25feb0"
dependencies = [
"cosmwasm-schema",
"cosmwasm-std",
@@ -1258,14 +1128,12 @@ dependencies = [
"cw2",
"cw4",
"nym-contracts-common",
"nym-multisig-contract-common 1.20.4 (registry+https://github.com/rust-lang/crates.io-index)",
"nym-multisig-contract-common 1.21.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "nym-contracts-common"
version = "1.20.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47bb3e8427c193cd500c802274b11879086863c3643525b6ece3e9ab1c77bddc"
version = "1.21.0"
dependencies = [
"bs58",
"cosmwasm-schema",
@@ -1279,9 +1147,7 @@ dependencies = [
[[package]]
name = "nym-contracts-common-testing"
version = "1.20.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3628aac6715e844f3ab20e3b8ae8c4684f144ccb78e205f002c1c3ae375e956"
version = "1.21.0"
dependencies = [
"anyhow",
"cosmwasm-std",
@@ -1295,16 +1161,13 @@ dependencies = [
[[package]]
name = "nym-crypto"
version = "1.20.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b710addc28c9950dd961e7dd3837ef3b479492d2b21b5f2437eb7d2899403027"
version = "1.21.0"
dependencies = [
"base64 0.22.1",
"bs58",
"curve25519-dalek",
"ed25519-dalek",
"nym-pemstore",
"nym-sphinx-types",
"rand",
"sha2",
"subtle-encoding",
@@ -1332,7 +1195,7 @@ dependencies = [
"nym-contracts-common-testing",
"nym-crypto",
"nym-ecash-contract-common",
"nym-multisig-contract-common 1.20.4 (registry+https://github.com/rust-lang/crates.io-index)",
"nym-multisig-contract-common 1.21.0 (registry+https://github.com/rust-lang/crates.io-index)",
"nym-network-defaults",
"schemars",
"semver",
@@ -1343,7 +1206,7 @@ dependencies = [
[[package]]
name = "nym-ecash-contract-common"
version = "1.20.4"
version = "1.21.0"
dependencies = [
"bs58",
"cosmwasm-schema",
@@ -1351,15 +1214,15 @@ dependencies = [
"cw-controllers",
"cw-utils",
"cw2",
"nym-multisig-contract-common 1.20.4",
"nym-multisig-contract-common 1.21.0",
"thiserror 2.0.12",
]
[[package]]
name = "nym-group-contract-common"
version = "1.20.4"
version = "1.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb13102740426a4a2b683f54bbd6614fe9ecd745f5117bcf197c49c300b15edf"
checksum = "f6f9773b3adc4b979c4e0f86d8932a78d10149df36e136f770653ad5405ce41a"
dependencies = [
"cosmwasm-schema",
"cw-controllers",
@@ -1385,6 +1248,7 @@ dependencies = [
"nym-crypto",
"nym-mixnet-contract",
"nym-mixnet-contract-common",
"nym-node-families-contract-common",
"nym-vesting-contract-common",
"rand",
"rand_chacha",
@@ -1394,9 +1258,7 @@ dependencies = [
[[package]]
name = "nym-mixnet-contract-common"
version = "1.20.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41c21bceb3bb8ee2789851b3f381fc035485af825bf7290b7c99a5af4e8f6ba1"
version = "1.21.0"
dependencies = [
"bs58",
"cosmwasm-schema",
@@ -1416,7 +1278,7 @@ dependencies = [
[[package]]
name = "nym-multisig-contract-common"
version = "1.20.4"
version = "1.21.0"
dependencies = [
"cosmwasm-schema",
"cosmwasm-std",
@@ -1431,9 +1293,9 @@ dependencies = [
[[package]]
name = "nym-multisig-contract-common"
version = "1.20.4"
version = "1.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4a20b931ee849f6179ce2b387accd058720017f644ffbc8c2422f3e9ac3ff54"
checksum = "7681c7b43201d45a4958eab012a93e285ce47f7bba405e1ba1808edd195f3347"
dependencies = [
"cosmwasm-schema",
"cosmwasm-std",
@@ -1448,19 +1310,30 @@ dependencies = [
[[package]]
name = "nym-network-defaults"
version = "1.20.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9834193b4641acdf9f360aea684a6bd841cad287930bc0d7c3241a133756464"
version = "1.21.0"
dependencies = [
"cargo_metadata 0.19.2",
"regex",
]
[[package]]
name = "nym-node-families-contract-common"
version = "1.21.0"
dependencies = [
"cosmwasm-schema",
"cosmwasm-std",
"cw-controllers",
"cw-utils",
"nym-contracts-common",
"nym-mixnet-contract-common",
"schemars",
"serde",
"thiserror 2.0.12",
]
[[package]]
name = "nym-pemstore"
version = "1.20.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03077f9ebeb40caf8aa8e6f7bf8728449f73733e7a246986e492fa34ad3e70ab"
version = "1.21.0"
dependencies = [
"pem",
"tracing",
@@ -1477,20 +1350,22 @@ dependencies = [
"cw-controllers",
"cw-storage-plus",
"cw2",
"node-families",
"nym-contracts-common",
"nym-contracts-common-testing",
"nym-crypto",
"nym-mixnet-contract",
"nym-mixnet-contract-common",
"nym-node-families-contract-common",
"nym-performance-contract-common",
"serde",
]
[[package]]
name = "nym-performance-contract-common"
version = "1.20.4"
version = "1.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42129a72f4b0dc0304a48b0ca1769b27694d913687ace5692d4c6924ca9f2a13"
checksum = "c6e47f2a04d8b0e1c492cdd7dcfc98face5efc2a4cb999c03887a5b77e4cf055"
dependencies = [
"cosmwasm-schema",
"cosmwasm-std",
@@ -1518,9 +1393,9 @@ dependencies = [
[[package]]
name = "nym-pool-contract-common"
version = "1.20.4"
version = "1.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70239cc26beda3ad19289188c50d554522af29646d7d3f855bda6fc8ed332fe7"
checksum = "9965dce8aaee8e9943b342695ff3fda45c8a4f1daab943ed752a8a0b47aa04c7"
dependencies = [
"cosmwasm-schema",
"cosmwasm-std",
@@ -1530,16 +1405,6 @@ dependencies = [
"thiserror 2.0.12",
]
[[package]]
name = "nym-sphinx-types"
version = "1.20.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ba662d39fd6da9e13166fa1162ff41c2cfaed78a77c70df72fbda6fef5eb4f5"
dependencies = [
"sphinx-packet",
"thiserror 2.0.12",
]
[[package]]
name = "nym-vesting-contract"
version = "1.4.1"
@@ -1563,9 +1428,9 @@ dependencies = [
[[package]]
name = "nym-vesting-contract-common"
version = "1.20.4"
version = "1.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "676c2793efbf9ccdf86bb788c903f778a2d5993a5174729303f9511a297f4ca8"
checksum = "af0179bfbb30551414277ca7bed631d48f3321df7b18e3032f23f2f111a577d7"
dependencies = [
"cosmwasm-schema",
"cosmwasm-std",
@@ -1582,12 +1447,6 @@ version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
[[package]]
name = "opaque-debug"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c"
[[package]]
name = "p256"
version = "0.13.2"
@@ -1730,9 +1589,9 @@ dependencies = [
[[package]]
name = "rand"
version = "0.8.5"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a"
dependencies = [
"libc",
"rand_chacha",
@@ -1758,16 +1617,6 @@ dependencies = [
"getrandom",
]
[[package]]
name = "rand_distr"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31"
dependencies = [
"num-traits",
"rand",
]
[[package]]
name = "rayon"
version = "1.10.0"
@@ -1824,7 +1673,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2"
dependencies = [
"hmac",
"subtle 2.4.1",
"subtle",
]
[[package]]
@@ -1902,8 +1751,8 @@ checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc"
dependencies = [
"base16ct",
"der",
"generic-array 0.14.7",
"subtle 2.4.1",
"generic-array",
"subtle",
"zeroize",
]
@@ -1996,7 +1845,7 @@ checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8"
dependencies = [
"cfg-if",
"cpufeatures",
"digest 0.10.7",
"digest",
]
[[package]]
@@ -2005,36 +1854,10 @@ version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
dependencies = [
"digest 0.10.7",
"digest",
"rand_core",
]
[[package]]
name = "sphinx-packet"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c26f0c20d909fdda1c5d0ece3973127ca421984d55b000215df365e93722fc6e"
dependencies = [
"aes",
"arrayref",
"blake2",
"bs58",
"byteorder",
"chacha",
"ctr",
"curve25519-dalek",
"digest 0.10.7",
"hkdf",
"hmac",
"lioness",
"rand",
"rand_distr",
"sha2",
"subtle 2.4.1",
"x25519-dalek",
"zeroize",
]
[[package]]
name = "spki"
version = "0.7.3"
@@ -2051,12 +1874,6 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]]
name = "subtle"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d67a5a62ba6e01cb2192ff309324cb4875d0c451d55fe2319433abe7a05a8ee"
[[package]]
name = "subtle"
version = "2.4.1"
+30 -20
View File
@@ -4,12 +4,12 @@ members = [
"coconut-dkg",
"ecash",
"mixnet",
"mixnet-vesting-integration-tests",
"nym-pool",
"multisig/cw3-flex-multisig",
"multisig/cw4-group",
"vesting",
"node-families",
"nym-pool",
"performance",
"vesting",
]
[workspace.package]
@@ -19,6 +19,8 @@ homepage = "https://nymtech.net"
documentation = "https://nymtech.net"
edition = "2021"
license = "Apache-2.0"
rust-version = "1.86.0"
readme = "../README.md"
[profile.release]
opt-level = 3
@@ -48,7 +50,7 @@ cw3-fixed-multisig = "=2.0.0"
cw4 = "=2.0.0"
cw20 = "=2.0.0"
cw20-base = "2.0.0"
rand = "0.8.5"
rand = "0.8.6"
rand_chacha = "0.3.1"
semver = "1.0.21"
serde = "1.0.196"
@@ -57,31 +59,33 @@ schemars = "0.8.16"
thiserror = "2.0.11"
easy-addr = { version = "1.20.1", path = "../common/cosmwasm-smart-contracts/easy_addr" }
easy-addr = { version = "1.21.0", path = "../common/cosmwasm-smart-contracts/easy_addr" }
# For local development with modifications, add a [patch.crates-io] section (see bottom of file)
nym-coconut-dkg-common = "1.20.4"
nym-contracts-common = "1.20.4"
nym-contracts-common-testing = "1.20.4"
nym-crypto = { version = "1.20.4", default-features = false }
nym-ecash-contract-common = "1.20.4"
nym-group-contract-common = "1.20.4"
nym-mixnet-contract-common = "1.20.4"
nym-multisig-contract-common = "1.20.4"
nym-network-defaults = { version = "1.20.4", default-features = false }
nym-performance-contract-common = "1.20.4"
nym-pool-contract-common = "1.20.4"
nym-vesting-contract-common = "1.20.4"
nym-coconut-dkg-common = "1.21.0"
nym-contracts-common = "1.21.0"
nym-contracts-common-testing = "1.21.0"
nym-crypto = { version = "1.21.0", default-features = false }
nym-ecash-contract-common = "1.21.0"
nym-group-contract-common = "1.21.0"
nym-mixnet-contract-common = "1.21.0"
nym-multisig-contract-common = "1.21.0"
nym-network-defaults = { version = "1.21.0", default-features = false }
nym-performance-contract-common = "1.21.0"
nym-pool-contract-common = "1.21.0"
nym-vesting-contract-common = "1.21.0"
# Aliases for crates that some contracts import under different names
contracts-common = { version = "1.20.4", package = "nym-contracts-common" }
mixnet-contract-common = { version = "1.20.4", package = "nym-mixnet-contract-common" }
vesting-contract-common = { version = "1.20.4", package = "nym-vesting-contract-common" }
contracts-common = { version = "1.21.0", package = "nym-contracts-common" }
mixnet-contract-common = { version = "1.21.0", package = "nym-mixnet-contract-common" }
vesting-contract-common = { version = "1.21.0", package = "nym-vesting-contract-common" }
nym-node-families-contract-common = { version = "1.21.0", package = "nym-node-families-contract-common" }
# Internal contract workspace members (for cross-contract testing)
cw3-flex-multisig = { version = "2.0.0", path = "multisig/cw3-flex-multisig" }
cw4-group = { version = "2.0.0", path = "multisig/cw4-group" }
nym-mixnet-contract = { version = "1.5.1", path = "mixnet" }
nym-vesting-contract = { version = "1.4.1", path = "vesting" }
node-families = { version = "0.1.0", path = "node-families" }
[workspace.lints.clippy]
unwrap_used = "deny"
@@ -98,4 +102,10 @@ unreachable = "deny"
# nym-coconut-dkg-common = { path = "../common/cosmwasm-smart-contracts/coconut-dkg" }
[patch.crates-io]
nym-network-defaults = { path = "../common/network-defaults" }
nym-crypto = { path = "../common/crypto" }
nym-contracts-common-testing = { path = "../common/cosmwasm-smart-contracts/contracts-common-testing" }
nym-contracts-common = { path = "../common/cosmwasm-smart-contracts/contracts-common" }
nym-ecash-contract-common = { path = "../common/cosmwasm-smart-contracts/ecash-contract" }
nym-mixnet-contract-common = { path = "../common/cosmwasm-smart-contracts/mixnet-contract" }
nym-node-families-contract-common = { path = "../common/cosmwasm-smart-contracts/node-families-contract" }
@@ -1,34 +0,0 @@
[package]
name = "mixnet-vesting-integration-tests"
version = "0.1.0"
authors = { workspace = true }
edition = { workspace = true }
license = { workspace = true }
repository = { workspace = true }
publish = false
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
# cosmwasm dependencies
cosmwasm-std = { workspace = true }
cw-multi-test = { workspace = true }
# contracts dependencies
nym-mixnet-contract-common = { workspace = true }
nym-vesting-contract-common = { workspace = true }
nym-contracts-common = { workspace = true }
nym-mixnet-contract = { workspace = true }
nym-vesting-contract = { workspace = true }
# other local dependencies
nym-crypto = { workspace = true, features = ["asymmetric", "rand"] }
# external dependencies
rand_chacha = "0.3"
[[test]]
name = "mixnet-vesting-test"
path = "src/tests.rs"
@@ -1,39 +0,0 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::support::setup::{MIX_DENOM, REWARDING_VALIDATOR};
use cosmwasm_std::Decimal;
use nym_contracts_common::Percent;
use nym_mixnet_contract_common::reward_params::RewardedSetParams;
use nym_mixnet_contract_common::InitialRewardingParams;
use std::time::Duration;
pub fn default_mixnet_init_msg() -> nym_mixnet_contract_common::InstantiateMsg {
nym_mixnet_contract_common::InstantiateMsg {
rewarding_validator_address: REWARDING_VALIDATOR.to_string(),
vesting_contract_address: "placeholder".to_string(),
rewarding_denom: MIX_DENOM.to_string(),
epochs_in_interval: 720,
epoch_duration: Duration::from_secs(60 * 60),
initial_rewarding_params: InitialRewardingParams {
initial_reward_pool: Decimal::from_atomics(250_000_000_000_000u128, 0).unwrap(),
initial_staking_supply: Decimal::from_atomics(223_000_000_000_000u128, 0).unwrap(),
staking_supply_scale_factor: Percent::hundred(),
sybil_resistance: Percent::from_percentage_value(30).unwrap(),
active_set_work_factor: Decimal::from_atomics(10u32, 0).unwrap(),
interval_pool_emission: Percent::from_percentage_value(2).unwrap(),
rewarded_set_params: RewardedSetParams {
entry_gateways: 70,
exit_gateways: 50,
mixnodes: 120,
standby: 0,
},
},
current_nym_node_version: "1.1.10".to_string(),
version_score_weights: Default::default(),
version_score_params: Default::default(),
profit_margin: Default::default(),
interval_operating_cost: Default::default(),
key_validity_in_epochs: None,
}
}
@@ -1,62 +0,0 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::support::setup::{MIXNET_OWNER, MIX_DENOM, REWARDING_VALIDATOR, VESTING_OWNER};
use cosmwasm_std::{coin, coins, Addr, Coin, Empty};
use cw_multi_test::{Contract, ContractWrapper};
use rand_chacha::rand_core::SeedableRng;
use rand_chacha::ChaCha20Rng;
#[allow(unused)]
pub fn mixnet_owner() -> Addr {
Addr::unchecked(MIXNET_OWNER)
}
#[allow(unused)]
pub fn vesting_owner() -> Addr {
Addr::unchecked(VESTING_OWNER)
}
#[allow(unused)]
pub fn rewarding_validator() -> Addr {
Addr::unchecked(REWARDING_VALIDATOR)
}
#[allow(unused)]
pub fn mix_coins(amount: u128) -> Vec<Coin> {
coins(amount, MIX_DENOM)
}
#[allow(unused)]
pub fn mix_coin(amount: u128) -> Coin {
coin(amount, MIX_DENOM)
}
#[allow(unused)]
pub fn test_rng() -> ChaCha20Rng {
let dummy_seed = [42u8; 32];
ChaCha20Rng::from_seed(dummy_seed)
}
#[allow(unused)]
pub fn mixnet_contract_wrapper() -> Box<dyn Contract<Empty>> {
Box::new(
ContractWrapper::new(
mixnet_contract::contract::execute,
mixnet_contract::contract::instantiate,
mixnet_contract::contract::query,
)
.with_migrate(mixnet_contract::contract::migrate),
)
}
pub fn vesting_contract_wrapper() -> Box<dyn Contract<Empty>> {
Box::new(
ContractWrapper::new(
vesting_contract::contract::execute,
vesting_contract::contract::instantiate,
vesting_contract::contract::query,
)
.with_migrate(vesting_contract::contract::migrate),
)
}
@@ -1,6 +0,0 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
pub mod fixtures;
pub mod helpers;
pub mod setup;
@@ -1,480 +0,0 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::support::fixtures;
use crate::support::helpers::{
mixnet_contract_wrapper, rewarding_validator, test_rng, vesting_contract_wrapper,
};
use cosmwasm_std::{coins, Addr, Coin, Decimal, Timestamp};
use cw_multi_test::{App, AppBuilder, Executor};
use nym_contracts_common::signing::{ContractMessageContent, MessageSignature, Nonce};
use nym_crypto::asymmetric::ed25519;
use nym_mixnet_contract_common::nym_node::{EpochAssignmentResponse, Role, RolesMetadataResponse};
use nym_mixnet_contract_common::reward_params::{NodeRewardingParameters, Performance};
use nym_mixnet_contract_common::{
CurrentIntervalResponse, MixnodeBondingPayload, NodeCostParams, RewardedSet, RewardingParams,
RoleAssignment, SignableMixNodeBondingMsg,
};
use nym_mixnet_contract_common::{
ExecuteMsg as MixnetExecuteMsg, MixNode, QueryMsg as MixnetQueryMsg,
};
use rand_chacha::ChaCha20Rng;
use std::collections::HashMap;
// our global accounts that should always get some coins at the start
pub const MIXNET_OWNER: &str = "mixnet-owner";
pub const VESTING_OWNER: &str = "vesting-owner";
pub const REWARDING_VALIDATOR: &str = "rewarding-validator";
pub const MIX_DENOM: &str = "unym";
#[allow(unused)]
pub struct ContractInstantiationResult {
mixnet_contract_address: Addr,
vesting_contract_address: Addr,
}
#[allow(unused)]
pub struct TestSetupBuilder {
mixnet_init_msg: nym_mixnet_contract_common::InstantiateMsg,
initial_balances: HashMap<Addr, Vec<Coin>>,
}
#[allow(unused)]
impl TestSetupBuilder {
pub fn new() -> Self {
TestSetupBuilder {
mixnet_init_msg: fixtures::default_mixnet_init_msg(),
initial_balances: Default::default(),
}
}
pub fn with_mixnet_init_msg(
mut self,
mixnet_init_msg: nym_mixnet_contract_common::InstantiateMsg,
) -> Self {
self.mixnet_init_msg = mixnet_init_msg;
self
}
pub fn with_initial_balances(mut self, initial_balances: HashMap<Addr, Vec<Coin>>) -> Self {
self.initial_balances = initial_balances;
self
}
pub fn with_initial_balance(mut self, addr: impl Into<String>, balance: Vec<Coin>) -> Self {
self.initial_balances.insert(Addr::unchecked(addr), balance);
self
}
pub fn build(self) -> TestSetup {
TestSetup::new(self.initial_balances, self.mixnet_init_msg)
}
}
#[allow(unused)]
pub struct TestSetup {
pub app: App,
pub rng: ChaCha20Rng,
pub mixnet_contract: Addr,
}
#[allow(unused)]
impl TestSetup {
pub fn new_simple() -> Self {
TestSetup::new(Default::default(), fixtures::default_mixnet_init_msg())
}
pub fn new(
initial_balances: HashMap<Addr, Vec<Coin>>,
custom_mixnet_init: nym_mixnet_contract_common::InstantiateMsg,
) -> Self {
let (app, contracts) = instantiate_contracts(initial_balances, Some(custom_mixnet_init));
TestSetup {
app,
rng: test_rng(),
mixnet_contract: contracts.mixnet_contract_address,
}
}
pub fn mixnet_contract(&self) -> Addr {
self.mixnet_contract.clone()
}
pub fn skip_to_current_epoch_end(&mut self) {
let current_interval: CurrentIntervalResponse = self
.app
.wrap()
.query_wasm_smart(
self.mixnet_contract(),
&MixnetQueryMsg::GetCurrentIntervalDetails {},
)
.unwrap();
let epoch_end = current_interval.interval.current_epoch_end_unix_timestamp();
self.app.update_block(|current_block| {
// skip few blocks just in case
current_block.height += 10;
current_block.time = Timestamp::from_seconds(epoch_end as u64)
})
}
fn get_rewarded_set(&self) -> RewardedSet {
let metadata: RolesMetadataResponse = self
.app
.wrap()
.query_wasm_smart(
self.mixnet_contract(),
&MixnetQueryMsg::GetRewardedSetMetadata {},
)
.unwrap();
let entry: EpochAssignmentResponse = self
.app
.wrap()
.query_wasm_smart(
self.mixnet_contract(),
&MixnetQueryMsg::GetRoleAssignment {
role: Role::EntryGateway,
},
)
.unwrap();
assert_eq!(entry.epoch_id, metadata.metadata.epoch_id);
let exit: EpochAssignmentResponse = self
.app
.wrap()
.query_wasm_smart(
self.mixnet_contract(),
&MixnetQueryMsg::GetRoleAssignment {
role: Role::ExitGateway,
},
)
.unwrap();
assert_eq!(exit.epoch_id, metadata.metadata.epoch_id);
let layer1: EpochAssignmentResponse = self
.app
.wrap()
.query_wasm_smart(
self.mixnet_contract(),
&MixnetQueryMsg::GetRoleAssignment { role: Role::Layer1 },
)
.unwrap();
assert_eq!(layer1.epoch_id, metadata.metadata.epoch_id);
let layer2: EpochAssignmentResponse = self
.app
.wrap()
.query_wasm_smart(
self.mixnet_contract(),
&MixnetQueryMsg::GetRoleAssignment { role: Role::Layer2 },
)
.unwrap();
assert_eq!(layer2.epoch_id, metadata.metadata.epoch_id);
let layer3: EpochAssignmentResponse = self
.app
.wrap()
.query_wasm_smart(
self.mixnet_contract(),
&MixnetQueryMsg::GetRoleAssignment { role: Role::Layer3 },
)
.unwrap();
assert_eq!(layer3.epoch_id, metadata.metadata.epoch_id);
let standby: EpochAssignmentResponse = self
.app
.wrap()
.query_wasm_smart(
self.mixnet_contract(),
&MixnetQueryMsg::GetRoleAssignment {
role: Role::Standby,
},
)
.unwrap();
assert_eq!(standby.epoch_id, metadata.metadata.epoch_id);
RewardedSet {
entry_gateways: entry.nodes,
exit_gateways: exit.nodes,
layer1: layer1.nodes,
layer2: layer2.nodes,
layer3: layer3.nodes,
standby: standby.nodes,
}
}
pub fn full_mixnet_epoch_operations(&mut self) {
let rewarded_set = self.get_rewarded_set();
let current_params: RewardingParams = self
.app
.wrap()
.query_wasm_smart(
self.mixnet_contract(),
&MixnetQueryMsg::GetRewardingParams {},
)
.unwrap();
// TODO: handle paging
// begin epoch transition
self.app
.execute_contract(
rewarding_validator(),
self.mixnet_contract(),
&MixnetExecuteMsg::BeginEpochTransition {},
&[],
)
.unwrap();
let work =
Decimal::one() / Decimal::from_ratio(rewarded_set.rewarded_set_size() as u64, 1u64);
let params = NodeRewardingParameters::new(Performance::hundred(), work);
let mut nodes = rewarded_set
.layer1
.iter()
.chain(rewarded_set.layer2.iter())
.chain(rewarded_set.layer3.iter())
.chain(rewarded_set.entry_gateways.iter())
.chain(rewarded_set.exit_gateways.iter())
.chain(rewarded_set.standby.iter())
.copied()
.collect::<Vec<_>>();
nodes.sort();
// reward
for (node_id) in nodes {
self.app
.execute_contract(
rewarding_validator(),
self.mixnet_contract(),
&MixnetExecuteMsg::RewardNode { node_id, params },
&[],
)
.unwrap();
}
// events
self.app
.execute_contract(
rewarding_validator(),
self.mixnet_contract(),
&MixnetExecuteMsg::ReconcileEpochEvents { limit: None },
&[],
)
.unwrap();
// don't bother changing the active set, use the same node for update and advance
self.app
.execute_contract(
rewarding_validator(),
self.mixnet_contract(),
&MixnetExecuteMsg::AssignRoles {
assignment: RoleAssignment {
role: Role::EntryGateway,
nodes: rewarded_set.entry_gateways,
},
},
&[],
)
.unwrap();
self.app
.execute_contract(
rewarding_validator(),
self.mixnet_contract(),
&MixnetExecuteMsg::AssignRoles {
assignment: RoleAssignment {
role: Role::ExitGateway,
nodes: rewarded_set.exit_gateways,
},
},
&[],
)
.unwrap();
self.app
.execute_contract(
rewarding_validator(),
self.mixnet_contract(),
&MixnetExecuteMsg::AssignRoles {
assignment: RoleAssignment {
role: Role::Layer1,
nodes: rewarded_set.layer1,
},
},
&[],
)
.unwrap();
self.app
.execute_contract(
rewarding_validator(),
self.mixnet_contract(),
&MixnetExecuteMsg::AssignRoles {
assignment: RoleAssignment {
role: Role::Layer2,
nodes: rewarded_set.layer2,
},
},
&[],
)
.unwrap();
self.app
.execute_contract(
rewarding_validator(),
self.mixnet_contract(),
&MixnetExecuteMsg::AssignRoles {
assignment: RoleAssignment {
role: Role::Layer3,
nodes: rewarded_set.layer3,
},
},
&[],
)
.unwrap();
self.app
.execute_contract(
rewarding_validator(),
self.mixnet_contract(),
&MixnetExecuteMsg::AssignRoles {
assignment: RoleAssignment {
role: Role::Standby,
nodes: rewarded_set.standby,
},
},
&[],
)
.unwrap();
}
pub fn advance_mixnet_epoch(&mut self) {
self.skip_to_current_epoch_end();
self.full_mixnet_epoch_operations();
}
pub fn valid_mixnode_with_sig(
&mut self,
owner: &str,
cost_params: NodeCostParams,
stake: Coin,
) -> (MixNode, MessageSignature) {
let signing_nonce: Nonce = self
.app
.wrap()
.query_wasm_smart(
self.mixnet_contract(),
&MixnetQueryMsg::GetSigningNonce {
address: owner.to_string(),
},
)
.unwrap();
let keypair = ed25519::KeyPair::new(&mut self.rng);
let identity_key = keypair.public_key().to_base58_string();
let legit_sphinx_keys = nym_crypto::asymmetric::x25519::KeyPair::new(&mut self.rng);
let mixnode = MixNode {
identity_key,
sphinx_key: legit_sphinx_keys.public_key().to_base58_string(),
host: "mix.node.org".to_string(),
mix_port: 1789,
verloc_port: 1790,
http_api_port: 8000,
version: "1.1.14".to_string(),
};
let payload = MixnodeBondingPayload::new(mixnode.clone(), cost_params);
let content = ContractMessageContent::new(Addr::unchecked(owner), vec![stake], payload);
let sign_payload = SignableMixNodeBondingMsg::new(signing_nonce, content);
let plaintext = sign_payload.to_plaintext().unwrap();
let signature = keypair.private_key().sign(plaintext);
let msg_signature = MessageSignature::from(signature.to_bytes().as_ref());
(mixnode, msg_signature)
}
}
pub fn instantiate_contracts(
mut initial_funds: HashMap<Addr, Vec<Coin>>,
custom_mixnet_init: Option<nym_mixnet_contract_common::InstantiateMsg>,
) -> (App, ContractInstantiationResult) {
// add our global addresses to the map
initial_funds.insert(
Addr::unchecked(MIXNET_OWNER),
coins(100_000_000_000, MIX_DENOM),
);
initial_funds.insert(
Addr::unchecked(VESTING_OWNER),
coins(100_000_000_000, MIX_DENOM),
);
initial_funds.insert(
Addr::unchecked(REWARDING_VALIDATOR),
coins(1_000_000_000_000, MIX_DENOM),
);
let mut app = AppBuilder::new().build(|router, _api, storage| {
for (addr, funds) in initial_funds {
router
.bank
.init_balance(storage, &addr, funds.clone())
.unwrap()
}
});
let mixnet_code_id = app.store_code(mixnet_contract_wrapper());
let vesting_code_id = app.store_code(vesting_contract_wrapper());
let mixnet_contract_address = app
.instantiate_contract(
mixnet_code_id,
Addr::unchecked(MIXNET_OWNER),
&custom_mixnet_init.unwrap_or(fixtures::default_mixnet_init_msg()),
&[],
"mixnet-contract",
Some(MIXNET_OWNER.to_string()),
)
.unwrap();
let vesting_contract_address = app
.instantiate_contract(
vesting_code_id,
Addr::unchecked(VESTING_OWNER),
&nym_vesting_contract_common::InitMsg {
mixnet_contract_address: mixnet_contract_address.to_string(),
mix_denom: MIX_DENOM.to_string(),
},
&[],
"vesting-contract",
Some(VESTING_OWNER.to_string()),
)
.unwrap();
// now fix up vesting contract address...
app.migrate_contract(
Addr::unchecked(MIXNET_OWNER),
mixnet_contract_address.clone(),
&nym_mixnet_contract_common::MigrateMsg {
vesting_contract_address: Some(vesting_contract_address.to_string()),
unsafe_skip_state_updates: Some(true),
},
mixnet_code_id,
)
.unwrap();
(
app,
ContractInstantiationResult {
mixnet_contract_address,
vesting_contract_address,
},
)
}
@@ -1,4 +0,0 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
mod support;
+4 -1
View File
@@ -28,6 +28,7 @@ crate-type = ["cdylib", "rlib"]
[dependencies]
mixnet-contract-common = { workspace = true }
vesting-contract-common = { workspace = true }
nym-node-families-contract-common = { workspace = true }
nym-contracts-common = { workspace = true }
nym-contracts-common-testing = { workspace = true, optional = true }
@@ -41,6 +42,8 @@ bs58 = { workspace = true }
serde = { workspace = true, default-features = false, features = ["derive"] }
semver = { workspace = true }
nym-crypto = { workspace = true, optional = true }
[dev-dependencies]
anyhow.workspace = true
@@ -56,7 +59,7 @@ nym-contracts-common-testing = { workspace = true }
[features]
default = []
contract-testing = ["mixnet-contract-common/contract-testing"]
testable-mixnet-contract = ["nym-contracts-common-testing"]
testable-mixnet-contract = ["nym-contracts-common-testing", "nym-crypto", "nym-crypto/asymmetric", "nym-crypto/rand"]
schema-gen = ["mixnet-contract-common/schema", "cosmwasm-schema"]
[lints]
@@ -11,6 +11,7 @@
"epoch_duration",
"epochs_in_interval",
"initial_rewarding_params",
"node_families_contract_address",
"rewarding_denom",
"rewarding_validator_address",
"vesting_contract_address"
@@ -50,6 +51,9 @@
"format": "uint32",
"minimum": 0.0
},
"node_families_contract_address": {
"type": "string"
},
"profit_margin": {
"default": {
"maximum": "1",
@@ -1267,6 +1271,81 @@
}
},
"additionalProperties": false
},
{
"description": "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.",
"type": "object",
"required": [
"admin_migrate_vested_mix_node"
],
"properties": {
"admin_migrate_vested_mix_node": {
"type": "object",
"required": [
"owner"
],
"properties": {
"owner": {
"type": "string"
}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
{
"description": "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.",
"type": "object",
"required": [
"admin_migrate_vested_delegation"
],
"properties": {
"admin_migrate_vested_delegation": {
"type": "object",
"required": [
"mix_id",
"owner"
],
"properties": {
"mix_id": {
"type": "integer",
"format": "uint32",
"minimum": 0.0
},
"owner": {
"type": "string"
}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
{
"description": "Admin-only: batch variant of [`ExecuteMsg::AdminMigrateVestedDelegation`]. Reverts the entire batch on the first error, so callers should treat it as all-or-nothing.",
"type": "object",
"required": [
"admin_batch_migrate_vested_delegations"
],
"properties": {
"admin_batch_migrate_vested_delegations": {
"type": "object",
"required": [
"entries"
],
"properties": {
"entries": {
"type": "array",
"items": {
"$ref": "#/definitions/VestedDelegationMigrationEntry"
}
}
},
"additionalProperties": false
}
},
"additionalProperties": false
}
],
"definitions": {
@@ -1958,6 +2037,24 @@
}
},
"additionalProperties": false
},
"VestedDelegationMigrationEntry": {
"type": "object",
"required": [
"mix_id",
"owner"
],
"properties": {
"mix_id": {
"type": "integer",
"format": "uint32",
"minimum": 0.0
},
"owner": {
"type": "string"
}
},
"additionalProperties": false
}
}
},
@@ -3509,7 +3606,13 @@
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "MigrateMsg",
"type": "object",
"required": [
"node_families_contract_address"
],
"properties": {
"node_families_contract_address": {
"type": "string"
},
"unsafe_skip_state_updates": {
"type": [
"boolean",
@@ -10774,12 +10877,21 @@
"description": "The current state of the mixnet contract.",
"type": "object",
"required": [
"node_families_contract_address",
"params",
"rewarding_denom",
"rewarding_validator_address",
"vesting_contract_address"
],
"properties": {
"node_families_contract_address": {
"description": "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.",
"allOf": [
{
"$ref": "#/definitions/Addr"
}
]
},
"owner": {
"description": "Address of the contract owner.",
"default": null,
+93
View File
@@ -976,6 +976,81 @@
}
},
"additionalProperties": false
},
{
"description": "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.",
"type": "object",
"required": [
"admin_migrate_vested_mix_node"
],
"properties": {
"admin_migrate_vested_mix_node": {
"type": "object",
"required": [
"owner"
],
"properties": {
"owner": {
"type": "string"
}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
{
"description": "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.",
"type": "object",
"required": [
"admin_migrate_vested_delegation"
],
"properties": {
"admin_migrate_vested_delegation": {
"type": "object",
"required": [
"mix_id",
"owner"
],
"properties": {
"mix_id": {
"type": "integer",
"format": "uint32",
"minimum": 0.0
},
"owner": {
"type": "string"
}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
{
"description": "Admin-only: batch variant of [`ExecuteMsg::AdminMigrateVestedDelegation`]. Reverts the entire batch on the first error, so callers should treat it as all-or-nothing.",
"type": "object",
"required": [
"admin_batch_migrate_vested_delegations"
],
"properties": {
"admin_batch_migrate_vested_delegations": {
"type": "object",
"required": [
"entries"
],
"properties": {
"entries": {
"type": "array",
"items": {
"$ref": "#/definitions/VestedDelegationMigrationEntry"
}
}
},
"additionalProperties": false
}
},
"additionalProperties": false
}
],
"definitions": {
@@ -1667,6 +1742,24 @@
}
},
"additionalProperties": false
},
"VestedDelegationMigrationEntry": {
"type": "object",
"required": [
"mix_id",
"owner"
],
"properties": {
"mix_id": {
"type": "integer",
"format": "uint32",
"minimum": 0.0
},
"owner": {
"type": "string"
}
},
"additionalProperties": false
}
}
}
@@ -7,6 +7,7 @@
"epoch_duration",
"epochs_in_interval",
"initial_rewarding_params",
"node_families_contract_address",
"rewarding_denom",
"rewarding_validator_address",
"vesting_contract_address"
@@ -46,6 +47,9 @@
"format": "uint32",
"minimum": 0.0
},
"node_families_contract_address": {
"type": "string"
},
"profit_margin": {
"default": {
"maximum": "1",
+6
View File
@@ -2,7 +2,13 @@
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "MigrateMsg",
"type": "object",
"required": [
"node_families_contract_address"
],
"properties": {
"node_families_contract_address": {
"type": "string"
},
"unsafe_skip_state_updates": {
"type": [
"boolean",
@@ -4,12 +4,21 @@
"description": "The current state of the mixnet contract.",
"type": "object",
"required": [
"node_families_contract_address",
"params",
"rewarding_denom",
"rewarding_validator_address",
"vesting_contract_address"
],
"properties": {
"node_families_contract_address": {
"description": "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.",
"allOf": [
{
"$ref": "#/definitions/Addr"
}
]
},
"owner": {
"description": "Address of the contract owner.",
"default": null,
+26 -2
View File
@@ -5,7 +5,7 @@ use crate::constants::INITIAL_PLEDGE_AMOUNT;
use crate::interval::storage as interval_storage;
use crate::mixnet_contract_settings::storage as mixnet_params_storage;
use crate::nodes::storage as nymnodes_storage;
use crate::queued_migrations::introduce_key_rotation_id;
use crate::queued_migrations::introduce_node_families_contract;
use crate::rewards::storage::RewardingStorage;
use cosmwasm_std::{
entry_point, to_json_binary, Addr, Coin, Deps, DepsMut, Env, MessageInfo, QueryResponse,
@@ -28,6 +28,7 @@ fn default_initial_state(
owner: Addr,
rewarding_validator_address: Addr,
vesting_contract_address: Addr,
node_families_contract_address: Addr,
) -> ContractState {
// we have to temporarily preserve this functionalities until it can be removed
#[allow(deprecated)]
@@ -35,6 +36,7 @@ fn default_initial_state(
owner: Some(owner),
rewarding_validator_address,
vesting_contract_address,
node_families_contract_address,
rewarding_denom: msg.rewarding_denom.clone(),
params: ContractStateParams {
delegations_params: DelegationsParams {
@@ -90,11 +92,15 @@ pub fn instantiate(
let rewarding_validator_address = deps.api.addr_validate(&msg.rewarding_validator_address)?;
let vesting_contract_address = deps.api.addr_validate(&msg.vesting_contract_address)?;
let node_families_contract_address = deps
.api
.addr_validate(&msg.node_families_contract_address)?;
let state = default_initial_state(
&msg,
info.sender.clone(),
rewarding_validator_address.clone(),
vesting_contract_address,
node_families_contract_address,
);
let starting_interval =
Interval::init_interval(msg.epochs_in_interval, msg.epoch_duration, &env);
@@ -301,6 +307,19 @@ pub fn execute(
ExecuteMsg::MigrateVestedDelegation { mix_id } => {
crate::vesting_migration::try_migrate_vested_delegation(deps, env, info, mix_id)
}
ExecuteMsg::AdminMigrateVestedMixNode { owner } => {
crate::vesting_migration::try_admin_migrate_vested_mixnode(deps, info, owner)
}
ExecuteMsg::AdminMigrateVestedDelegation { mix_id, owner } => {
crate::vesting_migration::try_admin_migrate_vested_delegation(
deps, env, info, mix_id, owner,
)
}
ExecuteMsg::AdminBatchMigrateVestedDelegations { entries } => {
crate::vesting_migration::try_admin_batch_migrate_vested_delegations(
deps, env, info, entries,
)
}
// legacy vesting
ExecuteMsg::BondMixnodeOnBehalf { .. }
@@ -629,7 +648,10 @@ pub fn migrate(
let skip_state_updates = msg.unsafe_skip_state_updates.unwrap_or(false);
if !skip_state_updates {
introduce_key_rotation_id(deps.branch())?;
let addr = deps
.api
.addr_validate(&msg.node_families_contract_address)?;
introduce_node_families_contract(deps.branch(), addr)?;
}
// due to circular dependency on contract addresses (i.e. mixnet contract requiring vesting contract address
@@ -668,6 +690,7 @@ mod tests {
let init_msg = InstantiateMsg {
rewarding_validator_address: deps.api.addr_make("foomp123").to_string(),
vesting_contract_address: deps.api.addr_make("bar456").to_string(),
node_families_contract_address: deps.api.addr_make("baz789").to_string(),
rewarding_denom: "uatom".to_string(),
epochs_in_interval: 1234,
epoch_duration: Duration::from_secs(4321),
@@ -708,6 +731,7 @@ mod tests {
owner: Some(deps.api.addr_make("sender")),
rewarding_validator_address: deps.api.addr_make("foomp123"),
vesting_contract_address: deps.api.addr_make("bar456"),
node_families_contract_address: deps.api.addr_make("baz789"),
rewarding_denom: "uatom".into(),
params: ContractStateParams {
delegations_params: DelegationsParams {
@@ -71,7 +71,7 @@ pub(crate) fn query_current_nym_node_version(
pub(crate) mod tests {
use super::*;
use crate::support::tests::test_helpers;
use cosmwasm_std::{coin, Addr};
use cosmwasm_std::coin;
use mixnet_contract_common::{ConfigScoreParams, DelegationsParams, OperatorsParams};
#[test]
@@ -80,9 +80,10 @@ pub(crate) mod tests {
#[allow(deprecated)]
let dummy_state = ContractState {
owner: Some(Addr::unchecked("foomp")),
rewarding_validator_address: Addr::unchecked("monitor"),
vesting_contract_address: Addr::unchecked("foomp"),
owner: Some(deps.api.addr_make("foomp")),
rewarding_validator_address: deps.api.addr_make("monitor"),
vesting_contract_address: deps.api.addr_make("foomp"),
node_families_contract_address: deps.api.addr_make("bar"),
rewarding_denom: "unym".to_string(),
params: ContractStateParams {
delegations_params: DelegationsParams {
@@ -156,6 +156,14 @@ pub(crate) fn vesting_contract_address(storage: &dyn Storage) -> Result<Addr, Mi
.map(|state| state.vesting_contract_address)?)
}
pub(crate) fn node_families_contract_address(
storage: &dyn Storage,
) -> Result<Addr, MixnetContractError> {
Ok(CONTRACT_STATE
.load(storage)
.map(|state| state.node_families_contract_address)?)
}
pub(crate) fn state_params(
storage: &dyn Storage,
) -> Result<ContractStateParams, MixnetContractError> {
+17 -5
View File
@@ -16,7 +16,7 @@ use crate::support::helpers::{
ensure_epoch_in_progress_state, ensure_no_existing_bond, ensure_operating_cost_within_range,
ensure_profit_margin_within_range, validate_pledge,
};
use cosmwasm_std::{coin, Coin, DepsMut, Env, MessageInfo, Response};
use cosmwasm_std::{coin, wasm_execute, Coin, DepsMut, Env, MessageInfo, Response};
use mixnet_contract_common::error::MixnetContractError;
use mixnet_contract_common::events::{
new_nym_node_bonding_event, new_pending_cost_params_update_event,
@@ -29,6 +29,7 @@ use mixnet_contract_common::{
PendingIntervalEventKind,
};
use nym_contracts_common::signing::{MessageSignature, SigningPurpose};
use nym_node_families_contract_common::msg::ExecuteMsg as NodeFamiliesExecuteMsg;
use serde::Serialize;
pub fn try_add_nym_node(
@@ -147,13 +148,24 @@ pub(crate) fn try_remove_nym_node(
};
interval_storage::push_new_epoch_event(deps.storage, &env, epoch_event)?;
Ok(
Response::new().add_event(new_pending_nym_node_unbonding_event(
// send message to the node families contract to remove this node from any family it might be a member of
let node_families_contract_addr =
mixnet_params_storage::node_families_contract_address(deps.storage)?;
let remove_from_family_exec = wasm_execute(
node_families_contract_addr,
&NodeFamiliesExecuteMsg::OnNymNodeUnbond {
node_id: existing_bond.node_id,
},
vec![],
)?;
Ok(Response::new()
.add_message(remove_from_family_exec)
.add_event(new_pending_nym_node_unbonding_event(
&existing_bond.owner,
existing_bond.identity(),
existing_bond.node_id,
)),
)
)))
}
pub(crate) fn try_update_node_config(
+32 -14
View File
@@ -1,21 +1,39 @@
// Copyright 2022-2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::interval::storage as interval_storage;
use crate::nodes::storage as nymnodes_storage;
use cosmwasm_std::DepsMut;
use crate::constants::CONTRACT_STATE_KEY;
use crate::mixnet_contract_settings::storage as mixnet_params_storage;
use cosmwasm_std::{Addr, DepsMut};
use cw_storage_plus::Item;
use mixnet_contract_common::error::MixnetContractError;
use mixnet_contract_common::KeyRotationState;
use mixnet_contract_common::{ContractState, ContractStateParams};
pub fn introduce_node_families_contract(
deps: DepsMut,
node_families_contract_address: Addr,
) -> Result<(), MixnetContractError> {
#[derive(serde::Serialize, serde::Deserialize)]
struct OldContractState {
owner: Option<Addr>,
rewarding_validator_address: Addr,
vesting_contract_address: Addr,
rewarding_denom: String,
params: ContractStateParams,
}
const OLD_CONTRACT_STATE: Item<OldContractState> = Item::new(CONTRACT_STATE_KEY);
let old = OLD_CONTRACT_STATE.load(deps.storage)?;
#[allow(deprecated)]
let updated = ContractState {
owner: old.owner,
rewarding_validator_address: old.rewarding_validator_address,
vesting_contract_address: old.vesting_contract_address,
rewarding_denom: old.rewarding_denom,
params: old.params,
node_families_contract_address,
};
mixnet_params_storage::CONTRACT_STATE.save(deps.storage, &updated)?;
pub fn introduce_key_rotation_id(deps: DepsMut) -> Result<(), MixnetContractError> {
let current_epoch_id =
interval_storage::current_interval(deps.storage)?.current_epoch_absolute_id();
nymnodes_storage::KEY_ROTATION_STATE.save(
deps.storage,
&KeyRotationState {
validity_epochs: 24,
initial_epoch_id: current_epoch_id,
},
)?;
Ok(())
}
@@ -5,16 +5,26 @@
#![allow(clippy::unwrap_used)]
use crate::contract::{execute, instantiate, migrate, query};
use cosmwasm_std::Decimal;
use cosmwasm_std::testing::{message_info, mock_env};
use cosmwasm_std::{coin, coins, Addr, Decimal, MessageInfo, StdError, StdResult};
use mixnet_contract_common::error::MixnetContractError;
use mixnet_contract_common::nym_node::{NodeDetailsResponse, NodeOwnershipResponse, Role};
use mixnet_contract_common::reward_params::RewardedSetParams;
use mixnet_contract_common::{
ExecuteMsg, InitialRewardingParams, InstantiateMsg, MigrateMsg, QueryMsg,
CurrentIntervalResponse, EpochId, ExecuteMsg, InitialRewardingParams, InstantiateMsg, Interval,
MigrateMsg, MixnetContractQuerier, NodeCostParams, NodeId, NymNode, NymNodeBondingPayload,
QueryMsg, RoleAssignment, SignableNymNodeBondingMsg, DEFAULT_INTERVAL_OPERATING_COST_AMOUNT,
DEFAULT_PROFIT_MARGIN_PERCENT,
};
use nym_contracts_common::signing::{ContractMessageContent, MessageSignature};
use nym_contracts_common::Percent;
use nym_contracts_common_testing::{
mock_dependencies, ContractFn, PermissionedFn, QueryFn, TEST_DENOM,
mock_dependencies, ArbitraryContractStorageReader, ArbitraryContractStorageWriter, BankExt,
ChainOpts, ContractFn, ContractTester, PermissionedFn, QueryFn, RandExt, TEST_DENOM,
};
use nym_crypto::asymmetric::ed25519;
use serde::de::DeserializeOwned;
use serde::Serialize;
use std::time::Duration;
pub use nym_contracts_common_testing::TestableNymContract;
@@ -74,6 +84,10 @@ impl TestableNymContract for MixnetContract {
InstantiateMsg {
rewarding_validator_address: deps.api.addr_make("rewarder").to_string(),
vesting_contract_address: deps.api.addr_make("vesting-contract").to_string(),
node_families_contract_address: deps
.api
.addr_make("node-families-contract")
.to_string(),
rewarding_denom: TEST_DENOM.to_string(),
epochs_in_interval: 720,
epoch_duration: Duration::from_secs(60 * 60),
@@ -87,3 +101,203 @@ impl TestableNymContract for MixnetContract {
}
}
}
pub trait EmbeddedMixnetContractExt:
ChainOpts + ArbitraryContractStorageWriter + ArbitraryContractStorageReader + RandExt + BankExt
{
fn mixnet_contract_address(&self) -> StdResult<Addr>;
fn execute_mixnet_contract(&mut self, sender: MessageInfo, msg: &ExecuteMsg) -> StdResult<()> {
let address = self.mixnet_contract_address()?;
self.execute_arbitrary_contract(address, sender, msg)
.map_err(|err| {
StdError::generic_err(format!("mixnet contract execution failure: {err}"))
})?;
Ok(())
}
fn read_from_mixnet_contract_storage<T: DeserializeOwned>(
&self,
key: impl AsRef<[u8]>,
) -> StdResult<T> {
let address = self.mixnet_contract_address()?;
self.must_read_value_from_contract_storage(address, key)
}
fn write_to_mixnet_contract_storage(
&mut self,
key: impl AsRef<[u8]>,
value: impl AsRef<[u8]>,
) -> StdResult<()> {
let address = self.mixnet_contract_address()?;
<Self as ArbitraryContractStorageWriter>::set_contract_storage(self, address, key, value);
Ok(())
}
fn write_to_mixnet_contract_storage_value<T: Serialize>(
&mut self,
key: impl AsRef<[u8]>,
value: &T,
) -> StdResult<()> {
let address = self.mixnet_contract_address()?;
self.set_contract_storage_value(address, key, value)
}
fn current_mixnet_epoch(&self) -> StdResult<EpochId> {
let address = self.mixnet_contract_address()?;
Ok(self
.deps()
.querier
.query_current_mixnet_interval(address.clone())?
.current_epoch_absolute_id())
}
fn advance_mixnet_epoch(&mut self) -> StdResult<()> {
let interval_details: CurrentIntervalResponse = self.query_arbitrary_contract(
self.mixnet_contract_address()?,
&QueryMsg::GetCurrentIntervalDetails {},
)?;
let until_end = interval_details.time_until_current_epoch_end().as_secs();
let timestamp = self.env().block.time.plus_seconds(until_end + 1);
self.set_block_time(timestamp);
self.next_block();
// this was hardcoded in mixnet init
let mixnet_rewarder = self.addr_make("rewarder");
let rewarder = message_info(&mixnet_rewarder, &[]);
self.execute_mixnet_contract(rewarder.clone(), &ExecuteMsg::BeginEpochTransition {})?;
self.execute_mixnet_contract(
rewarder.clone(),
&ExecuteMsg::ReconcileEpochEvents { limit: None },
)?;
for role in [
Role::ExitGateway,
Role::EntryGateway,
Role::Layer1,
Role::Layer2,
Role::Layer3,
Role::Standby,
] {
self.execute_mixnet_contract(
rewarder.clone(),
&ExecuteMsg::AssignRoles {
assignment: RoleAssignment {
role,
nodes: vec![],
},
},
)?;
}
Ok(())
}
fn set_mixnet_epoch(&mut self, epoch_id: EpochId) -> StdResult<()> {
let address = self.mixnet_contract_address()?;
let interval = self
.deps()
.querier
.query_current_mixnet_interval(address.clone())?;
let mut to_update = if interval.current_epoch_absolute_id() <= epoch_id {
interval
} else {
Interval::init_interval(
interval.epochs_in_interval(),
interval.epoch_length(),
&mock_env(),
)
};
let current = to_update.current_epoch_absolute_id();
let diff = epoch_id - current;
for _ in 0..diff {
to_update = to_update.advance_epoch();
}
self.set_contract_storage_value(&address, b"ci", &to_update)
}
fn bond_dummy_nymnode_for(&mut self, node_owner: &Addr) -> Result<NodeId, StdError> {
let pledge = coins(100_000000, TEST_DENOM);
let keypair = ed25519::KeyPair::new(self.raw_rng());
let identity_key = keypair.public_key().to_base58_string();
let node = NymNode {
host: "1.2.3.4".to_string(),
custom_http_port: None,
identity_key,
};
let cost_params = NodeCostParams {
profit_margin_percent: Percent::from_percentage_value(DEFAULT_PROFIT_MARGIN_PERCENT)
.unwrap(),
interval_operating_cost: coin(DEFAULT_INTERVAL_OPERATING_COST_AMOUNT, TEST_DENOM),
};
// initial signing nonce is 0 for a new address
let signing_nonce = 0;
let payload = NymNodeBondingPayload::new(node.clone(), cost_params.clone());
let content = ContractMessageContent::new(node_owner.clone(), pledge.clone(), payload);
let msg = SignableNymNodeBondingMsg::new(signing_nonce, content);
let owner_signature = keypair.private_key().sign(msg.to_plaintext()?);
let owner_signature = MessageSignature::from(owner_signature.to_bytes().as_ref());
self.execute_mixnet_contract(
message_info(node_owner, &pledge),
&ExecuteMsg::BondNymNode {
node,
cost_params,
owner_signature,
},
)?;
let bond: NodeOwnershipResponse = self.query_arbitrary_contract(
self.mixnet_contract_address()?,
&QueryMsg::GetOwnedNymNode {
address: node_owner.to_string(),
},
)?;
Ok(bond.details.unwrap().bond_information.node_id)
}
fn bond_dummy_nymnode(&mut self) -> Result<NodeId, StdError> {
let node_owner = self.generate_account_with_balance();
self.bond_dummy_nymnode_for(&node_owner)
}
fn unbond_nymnode(&mut self, node_id: NodeId) -> Result<(), StdError> {
let bond: NodeDetailsResponse = self.query_arbitrary_contract(
self.mixnet_contract_address()?,
&QueryMsg::GetNymNodeDetails { node_id },
)?;
let node_owner = bond.details.unwrap().bond_information.owner;
self.execute_mixnet_contract(
message_info(&node_owner, &[]),
&ExecuteMsg::UnbondNymNode {},
)?;
self.advance_mixnet_epoch()?;
Ok(())
}
}
impl<C> EmbeddedMixnetContractExt for ContractTester<C>
where
C: TestableNymContract,
{
fn mixnet_contract_address(&self) -> StdResult<Addr> {
self.well_known_contracts
.get(MixnetContract::NAME)
.ok_or_else(|| StdError::generic_err("mixnet contract not part of the tester"))
.cloned()
}
}
+405 -17
View File
@@ -3,24 +3,42 @@
use crate::delegations::storage as delegations_storage;
use crate::mixnet_contract_settings::storage as mixnet_params_storage;
use crate::mixnet_contract_settings::storage::ADMIN;
use crate::mixnodes::helpers::get_mixnode_details_by_owner;
use crate::mixnodes::storage as mixnodes_storage;
use crate::rewards::storage as rewards_storage;
use crate::support::helpers::{
ensure_bonded, ensure_epoch_in_progress_state, ensure_no_pending_pledge_changes,
};
use cosmwasm_std::{wasm_execute, DepsMut, Env, Event, MessageInfo, Response};
use cosmwasm_std::{wasm_execute, Addr, DepsMut, Env, Event, MessageInfo, Response};
use mixnet_contract_common::error::MixnetContractError;
use mixnet_contract_common::{Delegation, NodeId};
use mixnet_contract_common::{Delegation, NodeId, VestedDelegationMigrationEntry};
use vesting_contract_common::messages::ExecuteMsg as VestingExecuteMsg;
pub(crate) fn try_migrate_vested_mixnode(
deps: DepsMut<'_>,
info: MessageInfo,
) -> Result<Response, MixnetContractError> {
let mix_details = get_mixnode_details_by_owner(deps.storage, info.sender.clone())?.ok_or(
migrate_vested_mixnode_for_owner(deps, info.sender)
}
pub(crate) fn try_admin_migrate_vested_mixnode(
deps: DepsMut<'_>,
info: MessageInfo,
owner: String,
) -> Result<Response, MixnetContractError> {
ADMIN.assert_admin(deps.as_ref(), &info.sender)?;
let owner = deps.api.addr_validate(&owner)?;
migrate_vested_mixnode_for_owner(deps, owner)
}
fn migrate_vested_mixnode_for_owner(
deps: DepsMut<'_>,
owner: Addr,
) -> Result<Response, MixnetContractError> {
let mix_details = get_mixnode_details_by_owner(deps.storage, owner.clone())?.ok_or(
MixnetContractError::NoAssociatedMixNodeBond {
owner: info.sender.clone(),
owner: owner.clone(),
},
)?;
let mix_id = mix_details.mix_id();
@@ -55,7 +73,7 @@ pub(crate) fn try_migrate_vested_mixnode(
.add_message(wasm_execute(
vesting_contract,
&VestingExecuteMsg::TrackMigratedMixnode {
owner: info.sender.into_string(),
owner: owner.into_string(),
},
vec![],
)?))
@@ -66,6 +84,46 @@ pub(crate) fn try_migrate_vested_delegation(
env: Env,
info: MessageInfo,
mix_id: NodeId,
) -> Result<Response, MixnetContractError> {
migrate_vested_delegation_for_owner(deps, env, info.sender, mix_id)
}
pub(crate) fn try_admin_migrate_vested_delegation(
deps: DepsMut<'_>,
env: Env,
info: MessageInfo,
mix_id: NodeId,
owner: String,
) -> Result<Response, MixnetContractError> {
ADMIN.assert_admin(deps.as_ref(), &info.sender)?;
let owner = deps.api.addr_validate(&owner)?;
migrate_vested_delegation_for_owner(deps, env, owner, mix_id)
}
pub(crate) fn try_admin_batch_migrate_vested_delegations(
mut deps: DepsMut<'_>,
env: Env,
info: MessageInfo,
entries: Vec<VestedDelegationMigrationEntry>,
) -> Result<Response, MixnetContractError> {
ADMIN.assert_admin(deps.as_ref(), &info.sender)?;
let mut response = Response::new();
for VestedDelegationMigrationEntry { mix_id, owner } in entries {
let owner = deps.api.addr_validate(&owner)?;
let sub = migrate_vested_delegation_for_owner(deps.branch(), env.clone(), owner, mix_id)?;
response = response
.add_submessages(sub.messages)
.add_events(sub.events);
}
Ok(response)
}
fn migrate_vested_delegation_for_owner(
deps: DepsMut<'_>,
env: Env,
owner: Addr,
mix_id: NodeId,
) -> Result<Response, MixnetContractError> {
let mut response = Response::new();
@@ -73,12 +131,16 @@ pub(crate) fn try_migrate_vested_delegation(
let vesting_contract = mixnet_params_storage::vesting_contract_address(deps.storage)?;
let storage_key =
Delegation::generate_storage_key(mix_id, &info.sender, Some(&vesting_contract));
let storage_key = Delegation::generate_storage_key(mix_id, &owner, Some(&vesting_contract));
let Some(vested_delegation) =
delegations_storage::delegations().may_load(deps.storage, storage_key.clone())?
else {
return Err(MixnetContractError::NotAVestingDelegation);
return Ok(Response::new().add_event(
Event::new("migrate-vested-delegation-noop")
.add_attribute("owner", owner.as_str())
.add_attribute("mix_id", mix_id.to_string())
.add_attribute("reason", "no_vested_delegation"),
));
};
// sanity check that's meant to blow up the contract
@@ -88,7 +150,7 @@ pub(crate) fn try_migrate_vested_delegation(
let mut updated_delegation = vested_delegation.clone();
updated_delegation.proxy = None;
let new_storage_key = Delegation::generate_storage_key(mix_id, &info.sender, None);
let new_storage_key = Delegation::generate_storage_key(mix_id, &owner, None);
// remove the old (vested) delegation
delegations_storage::delegations().remove(deps.storage, storage_key)?;
@@ -205,7 +267,7 @@ pub(crate) fn try_migrate_vested_delegation(
Ok(response.add_message(wasm_execute(
vesting_contract,
&VestingExecuteMsg::TrackMigratedDelegation {
owner: info.sender.into_string(),
owner: owner.into_string(),
mix_id,
},
vec![],
@@ -307,9 +369,10 @@ mod tests {
let sender = test.make_sender("owner-without-any-delegations");
// it simply fails for there is nothing to migrate
let res = try_migrate_vested_delegation(test.deps_mut(), env, sender, 42).unwrap_err();
assert_eq!(res, MixnetContractError::NotAVestingDelegation);
// nothing to migrate -> idempotent no-op (so admin batches don't fail on stale entries)
let res = try_migrate_vested_delegation(test.deps_mut(), env, sender, 42).unwrap();
assert!(res.messages.is_empty());
assert_eq!(res.events[0].ty, "migrate-vested-delegation-noop");
}
#[test]
@@ -333,10 +396,10 @@ mod tests {
let sender = message_info(&delegation.owner, &[]);
let mix_id = delegation.node_id;
// it also fails because the method is only allowed for vested delegations
let res =
try_migrate_vested_delegation(test.deps_mut(), env, sender, mix_id).unwrap_err();
assert_eq!(res, MixnetContractError::NotAVestingDelegation);
// liquid-only delegations are no-ops (nothing vested to migrate)
let res = try_migrate_vested_delegation(test.deps_mut(), env, sender, mix_id).unwrap();
assert!(res.messages.is_empty());
assert_eq!(res.events[0].ty, "migrate-vested-delegation-noop");
}
#[test]
@@ -566,4 +629,329 @@ mod tests {
compare_decimals(rewards, new_rewards_twin, Some("0.01".parse().unwrap()))
}
}
#[cfg(test)]
mod admin_migrating_vested_mixnode {
use super::*;
use crate::mixnodes::helpers::get_mixnode_details_by_id;
use crate::support::tests::test_helpers::TestSetup;
use cosmwasm_std::testing::message_info;
use cosmwasm_std::{from_json, CosmosMsg, WasmMsg};
#[test]
fn rejects_non_admin_caller() {
let mut test = TestSetup::new();
let owner = test.make_addr("owner");
let mix_id = test.add_legacy_mixnode_with_legal_proxy(&owner, None);
let intruder = message_info(&test.make_addr("not-admin"), &[]);
let owner_str = owner.to_string();
let res =
try_admin_migrate_vested_mixnode(test.deps_mut(), intruder, owner_str).unwrap_err();
assert!(matches!(res, MixnetContractError::Admin(_)));
// bond is untouched
let existing = get_mixnode_details_by_id(test.deps().storage, mix_id)
.unwrap()
.unwrap();
assert!(existing.bond_information.proxy.is_some());
}
#[test]
fn admin_can_migrate_someone_elses_vested_node() {
let mut test = TestSetup::new();
let owner = test.make_addr("owner");
let mix_id = test.add_legacy_mixnode_with_legal_proxy(&owner, None);
let admin = test.admin();
let info = message_info(&admin, &[]);
let owner_str = owner.to_string();
let res =
try_admin_migrate_vested_mixnode(test.deps_mut(), info, owner_str.clone()).unwrap();
let CosmosMsg::Wasm(WasmMsg::Execute { msg, .. }) = &res.messages[0].msg else {
panic!("no track message present")
};
assert_eq!(
from_json::<VestingExecuteMsg>(msg).unwrap(),
VestingExecuteMsg::TrackMigratedMixnode { owner: owner_str }
);
// proxy was cleared on the bond owned by `owner` (not by the admin)
let migrated = get_mixnode_details_by_id(test.deps().storage, mix_id)
.unwrap()
.unwrap();
assert!(migrated.bond_information.proxy.is_none());
}
#[test]
fn rejects_invalid_owner_address() {
let mut test = TestSetup::new();
let admin = test.admin();
let info = message_info(&admin, &[]);
let res =
try_admin_migrate_vested_mixnode(test.deps_mut(), info, "not a bech32".to_string())
.unwrap_err();
assert!(matches!(res, MixnetContractError::StdErr { .. }));
}
}
#[cfg(test)]
mod admin_migrating_vested_delegation {
use super::*;
use crate::delegations::storage::delegations;
use crate::support::tests::test_helpers::TestSetup;
use cosmwasm_std::testing::message_info;
use cosmwasm_std::{from_json, CosmosMsg, Order, WasmMsg};
#[test]
fn rejects_non_admin_caller() {
let mut test = TestSetup::new_complex();
let env = test.env();
let vested = delegations()
.range(test.deps().storage, None, None, Order::Ascending)
.filter_map(|d| d.map(|(_, del)| del).ok())
.find(|d| d.proxy.is_some())
.unwrap();
let intruder = message_info(&test.make_addr("not-admin"), &[]);
let res = try_admin_migrate_vested_delegation(
test.deps_mut(),
env,
intruder,
vested.node_id,
vested.owner.to_string(),
)
.unwrap_err();
assert!(matches!(res, MixnetContractError::Admin(_)));
// delegation is untouched
assert!(delegations()
.may_load(test.deps().storage, vested.storage_key())
.unwrap()
.is_some());
}
#[test]
fn admin_can_migrate_someone_elses_vested_delegation() {
let mut test = TestSetup::new_complex();
let env = test.env();
let admin = test.admin();
let vested = delegations()
.range(test.deps().storage, None, None, Order::Ascending)
.filter_map(|d| d.map(|(_, del)| del).ok())
.find(|d| d.proxy.is_some())
.unwrap();
// pick an owner that has no liquid twin so we exercise the simple branch
let has_liquid_twin = delegations()
.range(test.deps().storage, None, None, Order::Ascending)
.filter_map(|d| d.map(|(_, del)| del).ok())
.any(|d| {
d.proxy.is_none()
&& d.owner.as_str() == vested.owner.as_str()
&& d.node_id == vested.node_id
});
assert!(!has_liquid_twin);
let old_key = vested.storage_key();
let mut expected_liquid = vested.clone();
expected_liquid.proxy = None;
let new_key = expected_liquid.storage_key();
let info = message_info(&admin, &[]);
let res = try_admin_migrate_vested_delegation(
test.deps_mut(),
env,
info,
vested.node_id,
vested.owner.to_string(),
)
.unwrap();
let CosmosMsg::Wasm(WasmMsg::Execute { msg, .. }) = &res.messages[0].msg else {
panic!("no track message present")
};
assert_eq!(
from_json::<VestingExecuteMsg>(msg).unwrap(),
VestingExecuteMsg::TrackMigratedDelegation {
owner: vested.owner.to_string(),
mix_id: vested.node_id,
}
);
assert!(delegations()
.may_load(test.deps().storage, old_key)
.unwrap()
.is_none());
assert_eq!(
expected_liquid,
delegations().load(test.deps().storage, new_key).unwrap()
);
}
}
#[cfg(test)]
mod admin_batch_migrating_vested_delegations {
use super::*;
use crate::delegations::storage::delegations;
use crate::support::tests::test_helpers::TestSetup;
use cosmwasm_std::testing::message_info;
use cosmwasm_std::{CosmosMsg, Order, WasmMsg};
fn collect_vested(test: &TestSetup, n: usize) -> Vec<Delegation> {
delegations()
.range(test.deps().storage, None, None, Order::Ascending)
.filter_map(|d| d.map(|(_, del)| del).ok())
.filter(|d| d.proxy.is_some())
.take(n)
.collect()
}
#[test]
fn rejects_non_admin_caller() {
let mut test = TestSetup::new_complex();
let env = test.env();
let vested = collect_vested(&test, 2);
let entries = vested
.iter()
.map(|d| VestedDelegationMigrationEntry {
mix_id: d.node_id,
owner: d.owner.to_string(),
})
.collect();
let intruder = message_info(&test.make_addr("not-admin"), &[]);
let res =
try_admin_batch_migrate_vested_delegations(test.deps_mut(), env, intruder, entries)
.unwrap_err();
assert!(matches!(res, MixnetContractError::Admin(_)));
// nothing was touched
for d in &vested {
assert!(delegations()
.may_load(test.deps().storage, d.storage_key())
.unwrap()
.is_some());
}
}
#[test]
fn admin_can_migrate_multiple_entries_in_one_call() {
let mut test = TestSetup::new_complex();
let env = test.env();
let admin = test.admin();
let vested = collect_vested(&test, 3);
assert_eq!(vested.len(), 3, "fixture must have ≥3 vested delegations");
let entries: Vec<_> = vested
.iter()
.map(|d| VestedDelegationMigrationEntry {
mix_id: d.node_id,
owner: d.owner.to_string(),
})
.collect();
let info = message_info(&admin, &[]);
let res =
try_admin_batch_migrate_vested_delegations(test.deps_mut(), env, info, entries)
.unwrap();
// one TrackMigratedDelegation WasmMsg per entry
let track_msgs: Vec<_> = res
.messages
.iter()
.filter(|sub| matches!(sub.msg, CosmosMsg::Wasm(WasmMsg::Execute { .. })))
.collect();
assert_eq!(track_msgs.len(), 3);
// each vested entry was removed and re-saved under the liquid key
for d in &vested {
assert!(delegations()
.may_load(test.deps().storage, d.storage_key())
.unwrap()
.is_none());
let liquid_key = Delegation::generate_storage_key(d.node_id, &d.owner, None);
let liquid = delegations().load(test.deps().storage, liquid_key).unwrap();
assert!(liquid.proxy.is_none());
}
}
#[test]
fn batch_with_noop_entries_still_succeeds() {
// batch contains: 1 valid vested, 1 stale (non-existent) — the stale one is a noop
let mut test = TestSetup::new_complex();
let env = test.env();
let admin = test.admin();
let vested = collect_vested(&test, 1);
let real = &vested[0];
let entries = vec![
VestedDelegationMigrationEntry {
mix_id: real.node_id,
owner: real.owner.to_string(),
},
VestedDelegationMigrationEntry {
mix_id: 999_999,
owner: test.make_addr("ghost").to_string(),
},
];
let info = message_info(&admin, &[]);
let res =
try_admin_batch_migrate_vested_delegations(test.deps_mut(), env, info, entries)
.unwrap();
// only the real entry dispatched a track message; the ghost was a noop
let track_msgs: Vec<_> = res
.messages
.iter()
.filter(|sub| matches!(sub.msg, CosmosMsg::Wasm(WasmMsg::Execute { .. })))
.collect();
assert_eq!(track_msgs.len(), 1);
assert!(res
.events
.iter()
.any(|e| e.ty == "migrate-vested-delegation-noop"));
}
#[test]
fn bad_owner_address_propagates_as_error() {
// a malformed entry causes the handler to return Err; on-chain this reverts
// the entire batch atomically. (Unit tests use a raw `DepsMut` that does not
// simulate chain-level rollback, so we only assert the error and ensure no
// entry after the bad one was processed.)
let mut test = TestSetup::new_complex();
let env = test.env();
let admin = test.admin();
let vested = collect_vested(&test, 1);
let entries = vec![
VestedDelegationMigrationEntry {
mix_id: vested[0].node_id,
owner: "not-a-bech32".to_string(),
},
VestedDelegationMigrationEntry {
mix_id: vested[0].node_id,
owner: vested[0].owner.to_string(),
},
];
let info = message_info(&admin, &[]);
let err =
try_admin_batch_migrate_vested_delegations(test.deps_mut(), env, info, entries)
.unwrap_err();
assert!(matches!(err, MixnetContractError::StdErr { .. }));
// bailed out before reaching the second (valid) entry, so the vested record
// is still in storage
assert!(delegations()
.may_load(test.deps().storage, vested[0].storage_key())
.unwrap()
.is_some());
}
}
}
+4
View File
@@ -0,0 +1,4 @@
[alias]
wasm = "build --release --lib --target wasm32-unknown-unknown"
unit-test = "test --lib"
schema = "run --bin schema --features=schema-gen"
+62
View File
@@ -0,0 +1,62 @@
[package]
name = "node-families"
description = "Nym Node Families contract"
version = "0.1.0"
authors.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
homepage.workspace = true
documentation.workspace = true
rust-version.workspace = true
readme.workspace = true
publish = false
[[bin]]
name = "schema"
required-features = ["schema-gen"]
[lib]
name = "node_families_contract"
crate-type = ["cdylib", "rlib"]
[dependencies]
cosmwasm-std = { workspace = true }
cw2 = { workspace = true }
cw-storage-plus = { workspace = true }
cw-controllers = { workspace = true }
serde = { workspace = true }
cosmwasm-schema = { workspace = true, optional = true }
cw-utils = { workspace = true }
nym-contracts-common = { workspace = true }
nym-node-families-contract-common = { workspace = true }
nym-mixnet-contract-common = { workspace = true }
# Optional deps activated by the `testable-node-families-contract` feature so
# downstream crates can pull in `crate::testing` for their own test harnesses.
nym-contracts-common-testing = { workspace = true, optional = true }
nym-mixnet-contract = { workspace = true, optional = true, features = ["testable-mixnet-contract"] }
nym-crypto = { workspace = true, optional = true }
[dev-dependencies]
anyhow = { workspace = true }
# make the testing helpers available for this crate's own unit tests via
# `cfg(test)`; downstream crates instead pull these in through the
# `testable-node-families-contract` feature.
nym-contracts-common-testing = { workspace = true }
nym-mixnet-contract = { workspace = true, features = ["testable-mixnet-contract"] }
nym-crypto = { workspace = true, features = ["asymmetric"] }
[features]
schema-gen = ["nym-node-families-contract-common/schema", "cosmwasm-schema"]
testable-node-families-contract = [
"nym-contracts-common-testing",
"nym-mixnet-contract",
"nym-crypto",
"nym-crypto/asymmetric",
]
[lints]
workspace = true
+5
View File
@@ -0,0 +1,5 @@
wasm:
RUSTFLAGS='-C link-arg=-s' cargo build --release --target wasm32-unknown-unknown
generate-schema:
cargo schema
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,318 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "ExecuteMsg",
"description": "Execute messages accepted by the contract.",
"oneOf": [
{
"description": "Replace the contract's runtime [`Config`]. Restricted to the contract admin.",
"type": "object",
"required": [
"update_config"
],
"properties": {
"update_config": {
"type": "object",
"required": [
"config"
],
"properties": {
"config": {
"$ref": "#/definitions/Config"
}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
{
"description": "Create a new family owned by the message sender. The configured `create_family_fee` must be attached as funds.",
"type": "object",
"required": [
"create_family"
],
"properties": {
"create_family": {
"type": "object",
"required": [
"description",
"name"
],
"properties": {
"description": {
"type": "string"
},
"name": {
"type": "string"
}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
{
"description": "Disband the family owned by the message sender. The family must have no current members; any still-pending invitations are revoked.",
"type": "object",
"required": [
"disband_family"
],
"properties": {
"disband_family": {
"type": "object",
"additionalProperties": false
}
},
"additionalProperties": false
},
{
"description": "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.",
"type": "object",
"required": [
"invite_to_family"
],
"properties": {
"invite_to_family": {
"type": "object",
"required": [
"node_id"
],
"properties": {
"node_id": {
"type": "integer",
"format": "uint32",
"minimum": 0.0
},
"validity_secs": {
"type": [
"integer",
"null"
],
"format": "uint64",
"minimum": 0.0
}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
{
"description": "Revoke a still-pending invitation previously issued by the sender's family.",
"type": "object",
"required": [
"revoke_family_invitation"
],
"properties": {
"revoke_family_invitation": {
"type": "object",
"required": [
"node_id"
],
"properties": {
"node_id": {
"type": "integer",
"format": "uint32",
"minimum": 0.0
}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
{
"description": "Accept a pending invitation. The sender must control `node_id`.",
"type": "object",
"required": [
"accept_family_invitation"
],
"properties": {
"accept_family_invitation": {
"type": "object",
"required": [
"family_id",
"node_id"
],
"properties": {
"family_id": {
"type": "integer",
"format": "uint32",
"minimum": 0.0
},
"node_id": {
"type": "integer",
"format": "uint32",
"minimum": 0.0
}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
{
"description": "Reject a pending invitation. The sender must control `node_id`.",
"type": "object",
"required": [
"reject_family_invitation"
],
"properties": {
"reject_family_invitation": {
"type": "object",
"required": [
"family_id",
"node_id"
],
"properties": {
"family_id": {
"type": "integer",
"format": "uint32",
"minimum": 0.0
},
"node_id": {
"type": "integer",
"format": "uint32",
"minimum": 0.0
}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
{
"description": "Leave the family `node_id` currently belongs to. The sender must control `node_id`.",
"type": "object",
"required": [
"leave_family"
],
"properties": {
"leave_family": {
"type": "object",
"required": [
"node_id"
],
"properties": {
"node_id": {
"type": "integer",
"format": "uint32",
"minimum": 0.0
}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
{
"description": "Remove `node_id` from the family owned by the message sender.",
"type": "object",
"required": [
"kick_from_family"
],
"properties": {
"kick_from_family": {
"type": "object",
"required": [
"node_id"
],
"properties": {
"node_id": {
"type": "integer",
"format": "uint32",
"minimum": 0.0
}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
{
"description": "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.",
"type": "object",
"required": [
"on_nym_node_unbond"
],
"properties": {
"on_nym_node_unbond": {
"type": "object",
"required": [
"node_id"
],
"properties": {
"node_id": {
"type": "integer",
"format": "uint32",
"minimum": 0.0
}
},
"additionalProperties": false
}
},
"additionalProperties": false
}
],
"definitions": {
"Coin": {
"type": "object",
"required": [
"amount",
"denom"
],
"properties": {
"amount": {
"$ref": "#/definitions/Uint128"
},
"denom": {
"type": "string"
}
},
"additionalProperties": false
},
"Config": {
"description": "Runtime configuration of the node families contract.",
"type": "object",
"required": [
"create_family_fee",
"default_invitation_validity_secs",
"family_description_length_limit",
"family_name_length_limit"
],
"properties": {
"create_family_fee": {
"description": "Fee charged on each successful `create_family` execution.",
"allOf": [
{
"$ref": "#/definitions/Coin"
}
]
},
"default_invitation_validity_secs": {
"description": "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.",
"type": "integer",
"format": "uint64",
"minimum": 0.0
},
"family_description_length_limit": {
"description": "Maximum allowed length, in characters, of a family description.",
"type": "integer",
"format": "uint",
"minimum": 0.0
},
"family_name_length_limit": {
"description": "Maximum allowed length, in characters, of a family name.",
"type": "integer",
"format": "uint",
"minimum": 0.0
}
},
"additionalProperties": false
},
"Uint128": {
"description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```",
"type": "string"
}
}
}
@@ -0,0 +1,80 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "InstantiateMsg",
"description": "Message used to instantiate the node families contract.",
"type": "object",
"required": [
"config",
"mixnet_contract_address"
],
"properties": {
"config": {
"$ref": "#/definitions/Config"
},
"mixnet_contract_address": {
"type": "string"
}
},
"additionalProperties": false,
"definitions": {
"Coin": {
"type": "object",
"required": [
"amount",
"denom"
],
"properties": {
"amount": {
"$ref": "#/definitions/Uint128"
},
"denom": {
"type": "string"
}
},
"additionalProperties": false
},
"Config": {
"description": "Runtime configuration of the node families contract.",
"type": "object",
"required": [
"create_family_fee",
"default_invitation_validity_secs",
"family_description_length_limit",
"family_name_length_limit"
],
"properties": {
"create_family_fee": {
"description": "Fee charged on each successful `create_family` execution.",
"allOf": [
{
"$ref": "#/definitions/Coin"
}
]
},
"default_invitation_validity_secs": {
"description": "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.",
"type": "integer",
"format": "uint64",
"minimum": 0.0
},
"family_description_length_limit": {
"description": "Maximum allowed length, in characters, of a family description.",
"type": "integer",
"format": "uint",
"minimum": 0.0
},
"family_name_length_limit": {
"description": "Maximum allowed length, in characters, of a family name.",
"type": "integer",
"format": "uint",
"minimum": 0.0
}
},
"additionalProperties": false
},
"Uint128": {
"description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```",
"type": "string"
}
}
}
@@ -0,0 +1,7 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "MigrateMsg",
"description": "Message passed to the contract's `migrate` entry point.",
"type": "object",
"additionalProperties": false
}
@@ -0,0 +1,620 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "QueryMsg",
"description": "Query messages accepted by the contract.",
"oneOf": [
{
"description": "Look up a single family by its id.",
"type": "object",
"required": [
"get_family_by_id"
],
"properties": {
"get_family_by_id": {
"type": "object",
"required": [
"family_id"
],
"properties": {
"family_id": {
"type": "integer",
"format": "uint32",
"minimum": 0.0
}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
{
"description": "Look up the (at most one) family owned by a given address.",
"type": "object",
"required": [
"get_family_by_owner"
],
"properties": {
"get_family_by_owner": {
"type": "object",
"required": [
"owner"
],
"properties": {
"owner": {
"type": "string"
}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
{
"description": "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.",
"type": "object",
"required": [
"get_family_by_name"
],
"properties": {
"get_family_by_name": {
"type": "object",
"required": [
"name"
],
"properties": {
"name": {
"type": "string"
}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
{
"type": "object",
"required": [
"get_families_paged"
],
"properties": {
"get_families_paged": {
"type": "object",
"properties": {
"limit": {
"type": [
"integer",
"null"
],
"format": "uint32",
"minimum": 0.0
},
"start_after": {
"type": [
"integer",
"null"
],
"format": "uint32",
"minimum": 0.0
}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
{
"description": "Look up which family — if any — a node currently belongs to.",
"type": "object",
"required": [
"get_family_membership"
],
"properties": {
"get_family_membership": {
"type": "object",
"required": [
"node_id"
],
"properties": {
"node_id": {
"type": "integer",
"format": "uint32",
"minimum": 0.0
}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
{
"description": "Page through every node currently in a given family.",
"type": "object",
"required": [
"get_family_members_paged"
],
"properties": {
"get_family_members_paged": {
"type": "object",
"required": [
"family_id"
],
"properties": {
"family_id": {
"type": "integer",
"format": "uint32",
"minimum": 0.0
},
"limit": {
"type": [
"integer",
"null"
],
"format": "uint32",
"minimum": 0.0
},
"start_after": {
"type": [
"integer",
"null"
],
"format": "uint32",
"minimum": 0.0
}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
{
"description": "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).",
"type": "object",
"required": [
"get_all_family_members_paged"
],
"properties": {
"get_all_family_members_paged": {
"type": "object",
"properties": {
"limit": {
"type": [
"integer",
"null"
],
"format": "uint32",
"minimum": 0.0
},
"start_after": {
"type": [
"integer",
"null"
],
"format": "uint32",
"minimum": 0.0
}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
{
"description": "Look up the pending invitation for a specific `(family_id, node_id)` pair.",
"type": "object",
"required": [
"get_pending_invitation"
],
"properties": {
"get_pending_invitation": {
"type": "object",
"required": [
"family_id",
"node_id"
],
"properties": {
"family_id": {
"type": "integer",
"format": "uint32",
"minimum": 0.0
},
"node_id": {
"type": "integer",
"format": "uint32",
"minimum": 0.0
}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
{
"description": "Page through every pending invitation issued by a given family.",
"type": "object",
"required": [
"get_pending_invitations_for_family_paged"
],
"properties": {
"get_pending_invitations_for_family_paged": {
"type": "object",
"required": [
"family_id"
],
"properties": {
"family_id": {
"type": "integer",
"format": "uint32",
"minimum": 0.0
},
"limit": {
"type": [
"integer",
"null"
],
"format": "uint32",
"minimum": 0.0
},
"start_after": {
"type": [
"integer",
"null"
],
"format": "uint32",
"minimum": 0.0
}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
{
"description": "Page through every pending invitation issued for a given node.",
"type": "object",
"required": [
"get_pending_invitations_for_node_paged"
],
"properties": {
"get_pending_invitations_for_node_paged": {
"type": "object",
"required": [
"node_id"
],
"properties": {
"limit": {
"type": [
"integer",
"null"
],
"format": "uint32",
"minimum": 0.0
},
"node_id": {
"type": "integer",
"format": "uint32",
"minimum": 0.0
},
"start_after": {
"type": [
"integer",
"null"
],
"format": "uint32",
"minimum": 0.0
}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
{
"description": "Page through every pending invitation across all families.",
"type": "object",
"required": [
"get_all_pending_invitations_paged"
],
"properties": {
"get_all_pending_invitations_paged": {
"type": "object",
"properties": {
"limit": {
"type": [
"integer",
"null"
],
"format": "uint32",
"minimum": 0.0
},
"start_after": {
"type": [
"array",
"null"
],
"items": [
{
"type": "integer",
"format": "uint32",
"minimum": 0.0
},
{
"type": "integer",
"format": "uint32",
"minimum": 0.0
}
],
"maxItems": 2,
"minItems": 2
}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
{
"description": "Page through every archived (terminal-state) invitation issued by a given family.",
"type": "object",
"required": [
"get_past_invitations_for_family_paged"
],
"properties": {
"get_past_invitations_for_family_paged": {
"type": "object",
"required": [
"family_id"
],
"properties": {
"family_id": {
"type": "integer",
"format": "uint32",
"minimum": 0.0
},
"limit": {
"type": [
"integer",
"null"
],
"format": "uint32",
"minimum": 0.0
},
"start_after": {
"type": [
"array",
"null"
],
"items": [
{
"type": "integer",
"format": "uint32",
"minimum": 0.0
},
{
"type": "integer",
"format": "uint64",
"minimum": 0.0
}
],
"maxItems": 2,
"minItems": 2
}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
{
"description": "Page through every archived (terminal-state) invitation issued to a given node.",
"type": "object",
"required": [
"get_past_invitations_for_node_paged"
],
"properties": {
"get_past_invitations_for_node_paged": {
"type": "object",
"required": [
"node_id"
],
"properties": {
"limit": {
"type": [
"integer",
"null"
],
"format": "uint32",
"minimum": 0.0
},
"node_id": {
"type": "integer",
"format": "uint32",
"minimum": 0.0
},
"start_after": {
"type": [
"array",
"null"
],
"items": [
{
"type": "integer",
"format": "uint32",
"minimum": 0.0
},
{
"type": "integer",
"format": "uint64",
"minimum": 0.0
}
],
"maxItems": 2,
"minItems": 2
}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
{
"description": "Page through every archived (terminal-state) invitation across all families.",
"type": "object",
"required": [
"get_all_past_invitations_paged"
],
"properties": {
"get_all_past_invitations_paged": {
"type": "object",
"properties": {
"limit": {
"type": [
"integer",
"null"
],
"format": "uint32",
"minimum": 0.0
},
"start_after": {
"type": [
"array",
"null"
],
"items": [
{
"type": "array",
"items": [
{
"type": "integer",
"format": "uint32",
"minimum": 0.0
},
{
"type": "integer",
"format": "uint32",
"minimum": 0.0
}
],
"maxItems": 2,
"minItems": 2
},
{
"type": "integer",
"format": "uint64",
"minimum": 0.0
}
],
"maxItems": 2,
"minItems": 2
}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
{
"description": "Page through every archived membership record for a given family (nodes that used to belong to it but have since been removed).",
"type": "object",
"required": [
"get_past_members_for_family_paged"
],
"properties": {
"get_past_members_for_family_paged": {
"type": "object",
"required": [
"family_id"
],
"properties": {
"family_id": {
"type": "integer",
"format": "uint32",
"minimum": 0.0
},
"limit": {
"type": [
"integer",
"null"
],
"format": "uint32",
"minimum": 0.0
},
"start_after": {
"type": [
"array",
"null"
],
"items": [
{
"type": "integer",
"format": "uint32",
"minimum": 0.0
},
{
"type": "integer",
"format": "uint64",
"minimum": 0.0
}
],
"maxItems": 2,
"minItems": 2
}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
{
"description": "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.",
"type": "object",
"required": [
"get_past_members_for_node_paged"
],
"properties": {
"get_past_members_for_node_paged": {
"type": "object",
"required": [
"node_id"
],
"properties": {
"limit": {
"type": [
"integer",
"null"
],
"format": "uint32",
"minimum": 0.0
},
"node_id": {
"type": "integer",
"format": "uint32",
"minimum": 0.0
},
"start_after": {
"type": [
"array",
"null"
],
"items": [
{
"type": "integer",
"format": "uint32",
"minimum": 0.0
},
{
"type": "integer",
"format": "uint64",
"minimum": 0.0
}
],
"maxItems": 2,
"minItems": 2
}
},
"additionalProperties": false
}
},
"additionalProperties": false
}
]
}
@@ -0,0 +1,78 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "AllFamilyMembersPagedResponse",
"description": "Response to [`QueryMsg::GetAllFamilyMembersPaged`](crate::QueryMsg::GetAllFamilyMembersPaged).",
"type": "object",
"required": [
"members"
],
"properties": {
"members": {
"description": "The members on this page, in ascending [`NodeId`] order across every family.",
"type": "array",
"items": {
"$ref": "#/definitions/FamilyMemberRecord"
}
},
"start_next_after": {
"description": "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).",
"type": [
"integer",
"null"
],
"format": "uint32",
"minimum": 0.0
}
},
"additionalProperties": false,
"definitions": {
"FamilyMemberRecord": {
"description": "One entry in a [`FamilyMembersPagedResponse`] page — pairs a node id with its [`FamilyMembership`] record (notably its `joined_at` timestamp).",
"type": "object",
"required": [
"membership",
"node_id"
],
"properties": {
"membership": {
"description": "The membership record (carries `family_id` and `joined_at`).",
"allOf": [
{
"$ref": "#/definitions/FamilyMembership"
}
]
},
"node_id": {
"description": "The node currently in the family.",
"type": "integer",
"format": "uint32",
"minimum": 0.0
}
},
"additionalProperties": false
},
"FamilyMembership": {
"description": "On-chain record of a node's current family membership.\n\nA 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.",
"type": "object",
"required": [
"family_id",
"joined_at"
],
"properties": {
"family_id": {
"description": "The family the node is currently a member of.",
"type": "integer",
"format": "uint32",
"minimum": 0.0
},
"joined_at": {
"description": "Block timestamp (unix seconds) at which the node accepted its invitation and joined the family.",
"type": "integer",
"format": "uint64",
"minimum": 0.0
}
},
"additionalProperties": false
}
}
}
@@ -0,0 +1,212 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "AllPastFamilyInvitationsPagedResponse",
"description": "Response to [`QueryMsg::GetAllPastInvitationsPaged`](crate::QueryMsg::GetAllPastInvitationsPaged).",
"type": "object",
"required": [
"invitations"
],
"properties": {
"invitations": {
"description": "The archived invitations on this page, in ascending `((family_id, node_id), counter)` order across all terminal statuses.",
"type": "array",
"items": {
"$ref": "#/definitions/PastFamilyInvitation"
}
},
"start_next_after": {
"description": "Cursor to pass as `start_after` on the next call, or `None` if this page is empty (treat as end-of-list).",
"type": [
"array",
"null"
],
"items": [
{
"type": "array",
"items": [
{
"type": "integer",
"format": "uint32",
"minimum": 0.0
},
{
"type": "integer",
"format": "uint32",
"minimum": 0.0
}
],
"maxItems": 2,
"minItems": 2
},
{
"type": "integer",
"format": "uint64",
"minimum": 0.0
}
],
"maxItems": 2,
"minItems": 2
}
},
"additionalProperties": false,
"definitions": {
"FamilyInvitation": {
"description": "A pending invitation for a node to join a particular family.\n\nInvitations 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).",
"type": "object",
"required": [
"expires_at",
"family_id",
"node_id"
],
"properties": {
"expires_at": {
"description": "Block timestamp (unix seconds) after which the invitation is no longer valid.",
"type": "integer",
"format": "uint64",
"minimum": 0.0
},
"family_id": {
"description": "The family that issued the invitation.",
"type": "integer",
"format": "uint32",
"minimum": 0.0
},
"node_id": {
"description": "The node being invited.",
"type": "integer",
"format": "uint32",
"minimum": 0.0
}
},
"additionalProperties": false
},
"FamilyInvitationStatus": {
"description": "Terminal status for an invitation that has been moved out of the pending set.\n\nNote: timed-out invitations are not represented here — they are simply left in the pending set (see `FamilyInvitation::expires_at`).",
"oneOf": [
{
"description": "Still awaiting a response. Recorded with a timestamp for completeness even though pending invitations live in a separate map.",
"type": "object",
"required": [
"pending"
],
"properties": {
"pending": {
"type": "object",
"required": [
"at"
],
"properties": {
"at": {
"type": "integer",
"format": "uint64",
"minimum": 0.0
}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
{
"description": "The invitee accepted and joined the family at the given timestamp.",
"type": "object",
"required": [
"accepted"
],
"properties": {
"accepted": {
"type": "object",
"required": [
"at"
],
"properties": {
"at": {
"type": "integer",
"format": "uint64",
"minimum": 0.0
}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
{
"description": "The invitee explicitly rejected the invitation at the given timestamp.",
"type": "object",
"required": [
"rejected"
],
"properties": {
"rejected": {
"type": "object",
"required": [
"at"
],
"properties": {
"at": {
"type": "integer",
"format": "uint64",
"minimum": 0.0
}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
{
"description": "The family revoked the invitation at the given timestamp before it could be accepted or rejected.",
"type": "object",
"required": [
"revoked"
],
"properties": {
"revoked": {
"type": "object",
"required": [
"at"
],
"properties": {
"at": {
"type": "integer",
"format": "uint64",
"minimum": 0.0
}
},
"additionalProperties": false
}
},
"additionalProperties": false
}
]
},
"PastFamilyInvitation": {
"description": "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.",
"type": "object",
"required": [
"invitation",
"status"
],
"properties": {
"invitation": {
"description": "The original invitation as it was issued.",
"allOf": [
{
"$ref": "#/definitions/FamilyInvitation"
}
]
},
"status": {
"description": "What ultimately happened to it.",
"allOf": [
{
"$ref": "#/definitions/FamilyInvitationStatus"
}
]
}
},
"additionalProperties": false
}
}
}
@@ -0,0 +1,95 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "PendingInvitationsPagedResponse",
"description": "Response to [`QueryMsg::GetAllPendingInvitationsPaged`](crate::QueryMsg::GetAllPendingInvitationsPaged).",
"type": "object",
"required": [
"invitations"
],
"properties": {
"invitations": {
"description": "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.",
"type": "array",
"items": {
"$ref": "#/definitions/PendingFamilyInvitationDetails"
}
},
"start_next_after": {
"description": "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).",
"type": [
"array",
"null"
],
"items": [
{
"type": "integer",
"format": "uint32",
"minimum": 0.0
},
{
"type": "integer",
"format": "uint32",
"minimum": 0.0
}
],
"maxItems": 2,
"minItems": 2
}
},
"additionalProperties": false,
"definitions": {
"FamilyInvitation": {
"description": "A pending invitation for a node to join a particular family.\n\nInvitations 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).",
"type": "object",
"required": [
"expires_at",
"family_id",
"node_id"
],
"properties": {
"expires_at": {
"description": "Block timestamp (unix seconds) after which the invitation is no longer valid.",
"type": "integer",
"format": "uint64",
"minimum": 0.0
},
"family_id": {
"description": "The family that issued the invitation.",
"type": "integer",
"format": "uint32",
"minimum": 0.0
},
"node_id": {
"description": "The node being invited.",
"type": "integer",
"format": "uint32",
"minimum": 0.0
}
},
"additionalProperties": false
},
"PendingFamilyInvitationDetails": {
"description": "A pending [`FamilyInvitation`] paired with whether it has already timed out at the time the query was served.",
"type": "object",
"required": [
"expired",
"invitation"
],
"properties": {
"expired": {
"description": "`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.",
"type": "boolean"
},
"invitation": {
"description": "The stored invitation as it was issued.",
"allOf": [
{
"$ref": "#/definitions/FamilyInvitation"
}
]
}
},
"additionalProperties": false
}
}
}

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