Compare commits

...

23 Commits

Author SHA1 Message Date
Tommy Verrall 88e39e22fb Update network_tunnel_manager.sh
wg_meta_change - WIP
2025-10-03 13:02:16 +02:00
Jędrzej Stuczyński fc98c497b4 feat: DKG contract method for updating announce address (#6050)
* added new dkg execute methods for ownership transfer and announce address update

* cherry-pick TestableNymContract for the dkg contract from #5091

* tests

* schema fixes

* removed old queued migrations
2025-10-02 17:19:03 +01:00
benedetta davico 92a88cdf9a Merge pull request #6079 from nymtech/release/2025.17-isabirra
Release/2025.17 isabirra
2025-10-02 16:00:53 +02:00
Bogdan-Ștefan Neacşu 026d3a6466 Get wireguard keypair as arg instead of reading it from disk (#6078)
* Get wireguard keypair as arg instead of reading it from disk

* Move keypair out of NymNode

* Remove legacy auth client
2025-10-02 16:27:48 +03:00
import this 53c4fde314 Hotfix: Update API source in node ping tester script (#6082) 2025-10-02 12:53:51 +00:00
Simon Wicky 3f55e62764 ci fixes
(cherry picked from commit caf40e7a37)
2025-10-02 14:05:43 +02:00
import this 00cc54f5c3 [DOCs/operators]: Release notes 2025.17-isabirra & New tools documentation (#6081)
* initialise release update notes

* add api changes

* create tools page and document nym-node-cli usage

* syntax fix

* document cmd tools

* add tools and ufw command to changelog

* add ufw 51830 to nym-node ports snippet

* ready for review except missing version hash info

* finished - ready for review

* add spectre delegation wizzard
2025-10-02 11:56:12 +00:00
import this c1904840e1 Feature: Node rewards tracker (#6064) 2025-10-02 08:52:46 +00:00
benedetta davico c652e3bdcd Benny/ci contract fix (#5962)
* use different runner

* Update Makefile

* Update Makefile

* Update ci-contracts-upload-binaries.yml

change to dtolnay

* Update ci-contracts-upload-binaries.yml

allow features alloc

* Update ci-contracts-upload-binaries.yml

try a specific cosmwasm-check

* Update ci-contracts-upload-binaries.yml

temp disable - until the right cosm check is found

* try new runner

* remove version check

* try to dockerize

* test

* remove rust install

* test

* change runner

* .

* set cargo path

* set path

* diff image

* error

* set path

* .

* aah

* .

* remove singlepass feature

* change runner

* Update ci-contracts-upload-binaries.yml

---------

Co-authored-by: Tommy Verrall <60836166+tommyv1987@users.noreply.github.com>
2025-10-02 09:15:48 +01:00
benedetta davico f9844416df Update ci-contracts.yml 2025-10-01 12:45:18 +02:00
benedetta davico bbea2ff9e9 Add nym-node binary 2025-10-01 12:06:07 +02:00
Simon Wicky 4acaec48b4 update runner for nym stats api build (#6077) 2025-10-01 09:48:16 +02:00
Simon Wicky 51779c06a4 Registration Client (#6059)
* removing wg-gateway-client

* bandwidth_provider trait

* authenticator client

* adapt ip-packet-client

* nit

* registration_client

* accomodate new shutdown and bugfix

* sdk changes

* cleanup and shutdown management

* remove credential mode

* error cleanup

* better error handling

* removing useless cover traffic delay

* wasm client stuff

* cfg unix

* more wasm stuff

* change authenticator client to not be blocked by mixnet client
2025-09-30 15:50:04 +02:00
import this 5cc650e901 Feature: Ping probe all nodes /described from a server (#6074)
* initialise test-nodes-pings.sh

* add retry
2025-09-30 13:30:54 +00:00
Simon Wicky a7ec178c9f [Stats API] Add flat table to stats API (#6073)
* add flat table to stats API

* remove day column
2025-09-30 14:30:05 +02:00
benedetta davico 4e97a2f871 Update push-credential-proxy.yaml 2025-09-30 12:03:04 +02:00
benedetta davico 5fbfc21fb2 Bump cred proxy version 2025-09-30 11:22:18 +02:00
benedettadavico 3d45801bb7 Fix swagger v2 endpoint 2025-09-30 10:26:55 +02:00
benedettadavico 3aea9f127b Update changelog 2025-09-30 10:23:55 +02:00
benedetta davico a26ff644cc Update mainnet.rs 2025-09-29 19:16:49 +02:00
Mark Sinclair a0e37e78e2 Node Status API: add bridge information to dVPN endpoint (#6069)
* ns api: add node scraper for bridge information and add to dVPN gateway output

* extra error reporting

* run sqlx-prepare

* fix clippy

---------

Co-authored-by: Mark Sinclair <mmsinclair@users.noreply.github.com>
Co-authored-by: benedettadavico <benedetta.davico@gmail.com>
2025-09-29 16:27:10 +01:00
Jędrzej Stuczyński b3d02e3ba7 feat: NS ticket faucet (#6047)
* ns-api: remove sqlite support

ns-api: add env var to skip migrations for local dev

ns-api: tidy up imports

ns-api: fix deserialisation fo node descriptions

update dockerfile

update README

fix up README and example env

ns-api: bump major version to 4

ns-api: add more geoip data and new performance field in dvpn responses

* ability to import partial ticketbooks

* wip: adding common ecash state to NS API

* buffering ticketbooks

* wip

* distribute tickets when getting testrun assignment

* passing ticketbook data to gateway probe

* wrapped around storage tx

* ticketbook query fixes

* clippy

* modified testrun assignment to always return tickets

* Update version

* Update push-node-status-agent.yaml

* Update Cargo.toml

* add entrypoint for ns agents

* sqlx prepare and cargo fmt

* clippy fixes

* Update ci-check-ns-api-version.yml

---------

Co-authored-by: Mark Sinclair <mmsinclair@users.noreply.github.com>
Co-authored-by: benedetta davico <46782255+benedettadavico@users.noreply.github.com>
Co-authored-by: benedettadavico <benedetta.davico@gmail.com>
2025-09-29 14:53:15 +01:00
Mark Sinclair f5b5177073 Update push-node-status-api.yaml 2025-09-26 20:14:30 +01:00
215 changed files with 8608 additions and 3518 deletions
@@ -10,7 +10,7 @@ env:
jobs:
check-if-tag-exists:
runs-on: arc-ubuntu-22.04-dind
runs-on: arc-linux-latest-dind
steps:
- name: Checkout repo
uses: actions/checkout@v4
@@ -11,7 +11,7 @@ jobs:
strategy:
fail-fast: false
matrix:
platform: [ arc-ubuntu-22.04 ]
platform: [ arc-linux-latest-dind ]
runs-on: ${{ matrix.platform }}
env:
@@ -28,18 +28,11 @@ jobs:
mkdir -p $OUTPUT_DIR
echo $OUTPUT_DIR
- name: Install Rust stable
uses: actions-rs/toolchain@v1
with:
toolchain: stable
target: wasm32-unknown-unknown
override: true
- name: Build contracts
run: make optimize-contracts
- name: Install cosmwasm-check
run: cargo install cosmwasm-check
- name: Build release contracts
run: make publish-contracts
- name: Check optimized contracts
run: make docker-check-contracts
- name: Prepare build output
shell: bash
+1 -1
View File
@@ -17,7 +17,7 @@ jobs:
build:
# since it's going to be compiled into wasm, there's absolutely
# no point in running CI on different OS-es
runs-on: ubuntu-22.04
runs-on: arc-linux-latest
env:
CARGO_TERM_COLOR: always
RUSTUP_PERMIT_COPY_RENAME: 1
+5 -1
View File
@@ -21,7 +21,7 @@ jobs:
fail-fast: false
matrix:
include:
- os: arc-ubuntu-22.04
- os: arc-linux-latest
target: x86_64-unknown-linux-gnu
runs-on: ${{ matrix.os }}
@@ -30,11 +30,13 @@ jobs:
release_date: ${{ fromJSON(steps.create-release.outputs.assets)[0].published_at }}
client_hash: ${{ steps.binary-hashes.outputs.client_hash }}
nymvisor_hash: ${{ steps.binary-hashes.outputs.nymvisor_hash }}
nymnode_hash: ${{ steps.binary-hashes.outputs.nymnode_hash }}
socks5_hash: ${{ steps.binary-hashes.outputs.socks5_hash }}
netreq_hash: ${{ steps.binary-hashes.outputs.netreq_hash }}
cli_hash: ${{ steps.binary-hashes.outputs.cli_hash }}
client_version: ${{ steps.binary-versions.outputs.client_version }}
nymvisor_version: ${{ steps.binary-versions.outputs.nymvisor_version }}
nymnode_version: ${{ steps.binary-versions.outputs.nymnode_version }}
socks5_version: ${{ steps.binary-versions.outputs.socks5_version }}
netreq_version: ${{ steps.binary-versions.outputs.netreq_version }}
cli_version: ${{ steps.binary-versions.outputs.cli_version }}
@@ -74,6 +76,7 @@ jobs:
target/release/nym-network-requester
target/release/nym-cli
target/release/nymvisor
target/release/nym-node
retention-days: 30
- id: create-release
@@ -88,6 +91,7 @@ jobs:
target/release/nym-network-requester
target/release/nym-cli
target/release/nymvisor
target/release/nym-node
push-release-data-client:
if: ${{ (startsWith(github.ref, 'refs/tags/nym-binaries-') && github.event_name == 'release') || github.event_name == 'workflow_dispatch' }}
+1 -1
View File
@@ -8,7 +8,7 @@ env:
jobs:
build-container:
runs-on: arc-ubuntu-22.04-dind
runs-on: arc-linux-latest-dind
steps:
- name: Login to Harbor
uses: docker/login-action@v3
@@ -20,7 +20,7 @@ env:
jobs:
build-container:
runs-on: arc-ubuntu-22.04-dind
runs-on: arc-linux-latest-dind
steps:
- name: Login to Harbor
uses: docker/login-action@v3
+1 -1
View File
@@ -14,7 +14,7 @@ env:
jobs:
build-container:
runs-on: arc-ubuntu-22.04-dind
runs-on: arc-linux-latest-dind
steps:
- name: Login to Harbor
uses: docker/login-action@v3
@@ -8,7 +8,7 @@ env:
jobs:
build-container:
runs-on: arc-ubuntu-22.04-dind
runs-on: arc-linux-latest-dind
steps:
- name: Login to Harbor
uses: docker/login-action@v3
+78
View File
@@ -4,6 +4,84 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
## [Unreleased]
## [2025.17-isabirra] (2025-09-29)
- Bugfix | Fix the registration handshake ([#6062])
- Convenience for ShutdownTracker ([#6038])
- chore: made http-api-client-macro doctest compile ([#6037])
- feat: refresh mixnet contract on epoch progression ([#6023])
- chore: remove legacy nodes from nym api [and kinda-ish from node status api] ([#6021])
- Feature/credential proxy crate ([#6018])
- Moving clients crate from vpn-client repo to here ([#6015])
- Feature/cancellation migration ([#6014])
- Use default value for the ports until api is deployed ([#6007])
- bugfix: return from MixTrafficController if client request channel has closed ([#6002])
- Revert "Create an axum_test client for more integrated unit testing (… ([#5999])
- chore: upgraded syn to 2.0 and removed nym-execute ([#5998])
- feat: use `ShutdownToken` (`CancellationToken` inside) for nym-api ([#5997])
- bugfix: Recipient deserialisation for deserialisers missing bytes specialisation ([#5991])
- chore: use updated version of simulate endpoint ([#5988])
- chore: purge temp databases on build ([#5984])
- Bump sha.js from 2.4.11 to 2.4.12 ([#5983])
- Feature: Delegation program stake checker and adjuster ([#5980])
- build(deps): bump actions/setup-java from 4 to 5 ([#5975])
- Domain fronting integration ([#5974])
- chore: internal hidden command to force advance nyx epoch ([#5964])
- Create an axum_test client for more integrated unit testing ([#5956])
- feat: shared library for attempting to retrieve update mode attestation ([#5954])
- Bump slab from 0.4.10 to 0.4.11 ([#5952])
- build(deps): bump actions/first-interaction from 1 to 3 ([#5950])
- fix: use WASM compatible time API in client ([#5948])
- feat: credential proxy deposit pool ([#5945])
- build(deps): bump actions/download-artifact from 4 to 5 ([#5939])
- feat: nym signers monitor ([#5933])
- Bump console from 0.15.11 to 0.16.0 ([#5931])
- Bump mock_instant from 0.5.3 to 0.6.0 ([#5930])
- Bump tokio from 1.46.1 to 1.47.1 ([#5929])
- Bump defguard_wireguard_rs from v0.4.7 to v0.7.5 ([#5928])
- Bump indicatif from 0.17.11 to 0.18.0 ([#5924])
- Feature: Nym node autorun CLI ([#5916])
- build(deps): bump mikefarah/yq from 4.45.4 to 4.47.1 ([#5911])
- build(deps): bump pbkdf2 from 3.1.2 to 3.1.3 ([#5869])
[#6062]: https://github.com/nymtech/nym/pull/6062
[#6038]: https://github.com/nymtech/nym/pull/6038
[#6037]: https://github.com/nymtech/nym/pull/6037
[#6023]: https://github.com/nymtech/nym/pull/6023
[#6021]: https://github.com/nymtech/nym/pull/6021
[#6018]: https://github.com/nymtech/nym/pull/6018
[#6015]: https://github.com/nymtech/nym/pull/6015
[#6014]: https://github.com/nymtech/nym/pull/6014
[#6007]: https://github.com/nymtech/nym/pull/6007
[#6002]: https://github.com/nymtech/nym/pull/6002
[#5999]: https://github.com/nymtech/nym/pull/5999
[#5998]: https://github.com/nymtech/nym/pull/5998
[#5997]: https://github.com/nymtech/nym/pull/5997
[#5991]: https://github.com/nymtech/nym/pull/5991
[#5988]: https://github.com/nymtech/nym/pull/5988
[#5984]: https://github.com/nymtech/nym/pull/5984
[#5983]: https://github.com/nymtech/nym/pull/5983
[#5980]: https://github.com/nymtech/nym/pull/5980
[#5975]: https://github.com/nymtech/nym/pull/5975
[#5974]: https://github.com/nymtech/nym/pull/5974
[#5964]: https://github.com/nymtech/nym/pull/5964
[#5956]: https://github.com/nymtech/nym/pull/5956
[#5954]: https://github.com/nymtech/nym/pull/5954
[#5952]: https://github.com/nymtech/nym/pull/5952
[#5950]: https://github.com/nymtech/nym/pull/5950
[#5948]: https://github.com/nymtech/nym/pull/5948
[#5945]: https://github.com/nymtech/nym/pull/5945
[#5939]: https://github.com/nymtech/nym/pull/5939
[#5933]: https://github.com/nymtech/nym/pull/5933
[#5931]: https://github.com/nymtech/nym/pull/5931
[#5930]: https://github.com/nymtech/nym/pull/5930
[#5929]: https://github.com/nymtech/nym/pull/5929
[#5928]: https://github.com/nymtech/nym/pull/5928
[#5924]: https://github.com/nymtech/nym/pull/5924
[#5916]: https://github.com/nymtech/nym/pull/5916
[#5911]: https://github.com/nymtech/nym/pull/5911
[#5869]: https://github.com/nymtech/nym/pull/5869
## [2025.16-halloumi] (2025-09-16)
- Backport metadata endpoint ([#6010])
Generated
+85 -158
View File
@@ -563,7 +563,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f"
dependencies = [
"async-trait",
"axum-core 0.4.5",
"axum-core",
"axum-macros",
"bytes",
"futures-util",
@@ -573,41 +573,7 @@ dependencies = [
"hyper 1.6.0",
"hyper-util",
"itoa",
"matchit 0.7.3",
"memchr",
"mime",
"percent-encoding",
"pin-project-lite",
"rustversion",
"serde",
"serde_json",
"serde_path_to_error",
"serde_urlencoded",
"sync_wrapper 1.0.2",
"tokio",
"tower 0.5.2",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
name = "axum"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "021e862c184ae977658b36c4500f7feac3221ca5da43e3f25bd04ab6c79a29b5"
dependencies = [
"axum-core 0.5.2",
"bytes",
"form_urlencoded",
"futures-util",
"http 1.3.1",
"http-body 1.0.1",
"http-body-util",
"hyper 1.6.0",
"hyper-util",
"itoa",
"matchit 0.8.4",
"matchit",
"memchr",
"mime",
"percent-encoding",
@@ -631,7 +597,7 @@ version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9eefda7e2b27e1bda4d6fa8a06b50803b8793769045918bc37ad062d48a6efac"
dependencies = [
"axum 0.7.9",
"axum",
"forwarded-header-value",
"serde",
]
@@ -657,34 +623,14 @@ dependencies = [
"tracing",
]
[[package]]
name = "axum-core"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68464cd0412f486726fb3373129ef5d2993f90c34bc2bc1c1e9943b2f4fc7ca6"
dependencies = [
"bytes",
"futures-core",
"http 1.3.1",
"http-body 1.0.1",
"http-body-util",
"mime",
"pin-project-lite",
"rustversion",
"sync_wrapper 1.0.2",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
name = "axum-extra"
version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c794b30c904f0a1c2fb7740f7df7f7972dfaa14ef6f57cb6178dc63e5dca2f04"
dependencies = [
"axum 0.7.9",
"axum-core 0.4.5",
"axum",
"axum-core",
"bytes",
"fastrand 2.3.0",
"futures-util",
@@ -721,9 +667,9 @@ dependencies = [
"anyhow",
"assert-json-diff",
"auto-future",
"axum 0.7.9",
"axum",
"bytes",
"bytesize 1.3.3",
"bytesize",
"cookie",
"http 1.3.1",
"http-body-util",
@@ -732,37 +678,7 @@ dependencies = [
"mime",
"pretty_assertions",
"reserve-port",
"rust-multipart-rfc7578_2 0.6.1",
"serde",
"serde_json",
"serde_urlencoded",
"smallvec",
"tokio",
"tower 0.5.2",
"url",
]
[[package]]
name = "axum-test"
version = "17.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eb1dfb84bd48bad8e4aa1acb82ed24c2bb5e855b659959b4e03b4dca118fcac"
dependencies = [
"anyhow",
"assert-json-diff",
"auto-future",
"axum 0.8.4",
"bytes",
"bytesize 2.0.1",
"cookie",
"http 1.3.1",
"http-body-util",
"hyper 1.6.0",
"hyper-util",
"mime",
"pretty_assertions",
"reserve-port",
"rust-multipart-rfc7578_2 0.8.0",
"rust-multipart-rfc7578_2",
"serde",
"serde_json",
"serde_urlencoded",
@@ -1106,12 +1022,6 @@ version = "1.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e93abca9e28e0a1b9877922aacb20576e05d4679ffa78c3d6dc22a26a216659"
[[package]]
name = "bytesize"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3c8f83209414aacf0eeae3cf730b18d6981697fba62f200fcfb92b9f082acba"
[[package]]
name = "camino"
version = "1.1.10"
@@ -4446,12 +4356,6 @@ version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94"
[[package]]
name = "matchit"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
[[package]]
name = "md-5"
version = "0.10.6"
@@ -4887,8 +4791,8 @@ version = "1.1.66"
dependencies = [
"anyhow",
"async-trait",
"axum 0.7.9",
"axum-test 16.4.1",
"axum",
"axum-test",
"bincode",
"bip39",
"bs58",
@@ -4908,6 +4812,7 @@ dependencies = [
"nym-api-requests",
"nym-bandwidth-controller",
"nym-bin-common",
"nym-cache",
"nym-coconut-dkg-common",
"nym-compact-ecash",
"nym-config",
@@ -5018,10 +4923,13 @@ dependencies = [
"bincode",
"futures",
"nym-authenticator-requests",
"nym-bandwidth-controller",
"nym-credentials-interface",
"nym-crypto",
"nym-registration-common",
"nym-sdk",
"nym-service-provider-requests-common",
"nym-validator-client",
"nym-wireguard-types",
"semver 1.0.26",
"thiserror 2.0.12",
@@ -5044,8 +4952,10 @@ dependencies = [
"nym-sphinx",
"nym-wireguard-types",
"rand 0.8.5",
"semver 1.0.26",
"serde",
"sha2 0.10.9",
"strum_macros",
"thiserror 2.0.12",
"x25519-dalek",
]
@@ -5054,6 +4964,7 @@ dependencies = [
name = "nym-bandwidth-controller"
version = "0.1.0"
dependencies = [
"async-trait",
"bip39",
"log",
"nym-credential-storage",
@@ -5093,6 +5004,13 @@ dependencies = [
"vergen",
]
[[package]]
name = "nym-cache"
version = "0.1.0"
dependencies = [
"tokio",
]
[[package]]
name = "nym-cli"
version = "1.1.63"
@@ -5433,6 +5351,7 @@ dependencies = [
"cosmwasm-std",
"cw-multi-test",
"cw-storage-plus",
"nym-contracts-common",
"rand 0.8.5",
"rand_chacha 0.3.1",
"serde",
@@ -5457,7 +5376,7 @@ name = "nym-credential-proxy"
version = "0.2.0"
dependencies = [
"anyhow",
"axum 0.7.9",
"axum",
"bip39",
"bs58",
"cfg-if",
@@ -5503,11 +5422,12 @@ name = "nym-credential-proxy-lib"
version = "0.1.0"
dependencies = [
"anyhow",
"axum 0.7.9",
"axum",
"bip39",
"bs58",
"futures",
"humantime",
"nym-cache",
"nym-compact-ecash",
"nym-credential-proxy-requests",
"nym-credentials",
@@ -5567,7 +5487,10 @@ dependencies = [
"log",
"nym-compact-ecash",
"nym-credentials",
"nym-credentials-interface",
"nym-crypto",
"nym-ecash-time",
"nym-test-utils",
"serde",
"sqlx",
"sqlx-pool-guard",
@@ -6034,7 +5957,7 @@ dependencies = [
name = "nym-http-api-common"
version = "0.1.0"
dependencies = [
"axum 0.7.9",
"axum",
"axum-client-ip",
"bincode",
"bytes",
@@ -6091,6 +6014,7 @@ dependencies = [
name = "nym-ip-packet-client"
version = "0.1.0"
dependencies = [
"bincode",
"bytes",
"futures",
"nym-ip-packet-requests",
@@ -6284,7 +6208,7 @@ name = "nym-network-monitor"
version = "1.0.2"
dependencies = [
"anyhow",
"axum 0.7.9",
"axum",
"clap",
"dashmap",
"futures",
@@ -6372,7 +6296,7 @@ dependencies = [
"arc-swap",
"arrayref",
"async-trait",
"axum 0.7.9",
"axum",
"bip39",
"blake2 0.8.1",
"bloomfilter",
@@ -6491,7 +6415,7 @@ dependencies = [
[[package]]
name = "nym-node-status-agent"
version = "1.0.6"
version = "1.0.7"
dependencies = [
"anyhow",
"clap",
@@ -6508,13 +6432,14 @@ dependencies = [
[[package]]
name = "nym-node-status-api"
version = "4.0.3"
version = "4.0.5"
dependencies = [
"ammonia",
"anyhow",
"axum 0.7.9",
"axum-test 17.3.0",
"axum",
"axum-test",
"bip39",
"bs58",
"celes",
"clap",
"cosmwasm-std",
@@ -6524,7 +6449,10 @@ dependencies = [
"moka",
"nym-bin-common",
"nym-contracts-common",
"nym-credential-proxy-lib",
"nym-credentials",
"nym-crypto",
"nym-ecash-time",
"nym-http-api-client",
"nym-http-api-common",
"nym-mixnet-contract-common",
@@ -6550,6 +6478,7 @@ dependencies = [
"thiserror 2.0.12",
"time",
"tokio",
"tokio-stream",
"tokio-util",
"tower-http 0.5.2",
"tracing",
@@ -6558,6 +6487,7 @@ dependencies = [
"utoipa",
"utoipa-swagger-ui",
"utoipauto",
"zeroize",
]
[[package]]
@@ -6566,6 +6496,8 @@ version = "0.2.0"
dependencies = [
"anyhow",
"bincode",
"bs58",
"nym-credentials",
"nym-crypto",
"reqwest 0.12.22",
"serde",
@@ -6735,6 +6667,36 @@ dependencies = [
"time",
]
[[package]]
name = "nym-registration-client"
version = "0.1.0"
dependencies = [
"nym-authenticator-client",
"nym-bandwidth-controller",
"nym-credential-storage",
"nym-credentials-interface",
"nym-ip-packet-client",
"nym-registration-common",
"nym-sdk",
"nym-validator-client",
"thiserror 2.0.12",
"tokio",
"tokio-util",
"tracing",
"url",
]
[[package]]
name = "nym-registration-common"
version = "0.1.0"
dependencies = [
"nym-authenticator-requests",
"nym-crypto",
"nym-ip-packet-requests",
"nym-sphinx",
"tokio-util",
]
[[package]]
name = "nym-sdk"
version = "0.1.0"
@@ -7112,10 +7074,10 @@ dependencies = [
[[package]]
name = "nym-statistics-api"
version = "0.1.5"
version = "0.2.1"
dependencies = [
"anyhow",
"axum 0.7.9",
"axum",
"axum-client-ip",
"axum-extra",
"celes",
@@ -7480,26 +7442,6 @@ dependencies = [
"ts-rs",
]
[[package]]
name = "nym-wg-gateway-client"
version = "0.1.0"
dependencies = [
"nym-authenticator-client",
"nym-authenticator-requests",
"nym-bandwidth-controller",
"nym-credentials-interface",
"nym-crypto",
"nym-node-requests",
"nym-pemstore",
"nym-sdk",
"nym-statistics-common",
"nym-validator-client",
"rand 0.8.5",
"thiserror 2.0.12",
"tracing",
"url",
]
[[package]]
name = "nym-wireguard"
version = "0.1.0"
@@ -7548,7 +7490,7 @@ version = "1.0.0"
dependencies = [
"anyhow",
"async-trait",
"axum 0.7.9",
"axum",
"futures",
"nym-credential-verification",
"nym-credentials-interface",
@@ -7566,7 +7508,7 @@ dependencies = [
name = "nym-wireguard-private-metadata-shared"
version = "1.0.0"
dependencies = [
"axum 0.7.9",
"axum",
"bincode",
"nym-credentials-interface",
"schemars 0.8.22",
@@ -7580,7 +7522,7 @@ name = "nym-wireguard-private-metadata-tests"
version = "1.0.0"
dependencies = [
"async-trait",
"axum 0.7.9",
"axum",
"nym-credential-verification",
"nym-credentials-interface",
"nym-http-api-client",
@@ -7645,7 +7587,7 @@ version = "0.1.14"
dependencies = [
"anyhow",
"async-trait",
"axum 0.7.9",
"axum",
"chrono",
"clap",
"nym-bin-common",
@@ -8978,21 +8920,6 @@ dependencies = [
"thiserror 1.0.69",
]
[[package]]
name = "rust-multipart-rfc7578_2"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c839d037155ebc06a571e305af66ff9fd9063a6e662447051737e1ac75beea41"
dependencies = [
"bytes",
"futures-core",
"futures-util",
"http 1.3.1",
"mime",
"rand 0.9.2",
"thiserror 2.0.12",
]
[[package]]
name = "rustc-demangle"
version = "0.1.25"
@@ -10884,7 +10811,7 @@ checksum = "877c5b330756d856ffcc4553ab34a5684481ade925ecc54bcd1bf02b1d0d4d52"
dependencies = [
"async-stream",
"async-trait",
"axum 0.7.9",
"axum",
"base64 0.22.1",
"bytes",
"h2 0.4.11",
@@ -11558,7 +11485,7 @@ version = "8.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db4b5ac679cc6dfc5ea3f2823b0291c777750ffd5e13b21137e0f7ac0e8f9617"
dependencies = [
"axum 0.7.9",
"axum",
"base64 0.22.1",
"mime_guess",
"regex",
+7 -5
View File
@@ -66,7 +66,7 @@ members = [
"common/mixnode-common",
"common/network-defaults",
"common/node-tester-utils",
"common/nonexhaustive-delayqueue",
"common/nonexhaustive-delayqueue", "common/nym-cache",
"common/nym-id",
"common/nym-metrics",
"common/nym_offline_compact_ecash",
@@ -85,6 +85,7 @@ members = [
"common/nymsphinx/types",
"common/nyxd-scraper",
"common/pemstore",
"common/registration",
"common/serde-helpers",
"common/service-provider-requests-common",
"common/socks5-client-core",
@@ -92,7 +93,8 @@ members = [
"common/socks5/requests",
"common/statistics",
"common/store-cipher",
"common/task", "common/test-utils",
"common/task",
"common/test-utils",
"common/ticketbooks-merkle",
"common/topology",
"common/tun",
@@ -125,10 +127,11 @@ members = [
"nym-node-status-api/nym-node-status-client",
"nym-node/nym-node-metrics",
"nym-node/nym-node-requests",
"nym-outfox", "nym-signers-monitor",
"nym-outfox",
"nym-registration-client",
"nym-signers-monitor",
"nym-statistics-api",
"nym-validator-rewarder",
"nym-wg-gateway-client",
"nyx-chain-watcher",
"sdk/ffi/cpp",
"sdk/ffi/go",
@@ -145,7 +148,6 @@ members = [
# "tools/internal/sdk-version-bump",
"tools/internal/ssl-inject",
"tools/internal/testnet-manager",
"tools/internal/testnet-manager",
"tools/internal/testnet-manager/dkg-bypass-contract",
"tools/internal/validator-status-check",
"tools/nym-cli",
+8
View File
@@ -154,6 +154,7 @@ CONTRACTS_OUT_DIR = contracts/artifacts
#
COSMWASM_OPTIMIZER_IMAGE ?= cosmwasm/optimizer:0.17.0
COSMWASM_OPTIMIZER_PLATFORM ?= linux/amd64
COSMWASM_CHECK_IMAGE ?= rust:1.88
# Ensure clean build environment and run the optimizer
optimize-contracts:
@@ -179,6 +180,13 @@ optimize-contracts:
# Cleanup temporary artefacts directory
@rm -rf artifacts 2>/dev/null || true
# Check artifacts with cosmwasm-check inside the optimizer image
docker-check-contracts:
@docker run --rm --platform $(COSMWASM_OPTIMIZER_PLATFORM) \
-v $(CURDIR):/code --workdir /code \
--entrypoint /bin/sh \
$(COSMWASM_CHECK_IMAGE) -lc 'apt-get update && apt-get install -y --no-install-recommends llvm-dev libclang-dev pkg-config && export PATH="/usr/local/cargo/bin:/usr/local/rustup/bin:$$PATH" && cargo install cosmwasm-check --locked && WASMER_ENGINE=universal WASMER_COMPILER=singlepass cosmwasm-check contracts/artifacts/*.wasm'
wasm-opt-contracts:
@for WASM in $(WASM_CONTRACT_DIR)/*.wasm; do \
echo "Running wasm-opt on $$WASM"; \
+2
View File
@@ -13,6 +13,8 @@ base64 = { workspace = true }
bincode = { workspace = true }
rand = { workspace = true }
serde = { workspace = true, features = ["derive"] }
semver = { workspace = true }
strum_macros = { workspace = true }
thiserror = { workspace = true }
nym-credentials-interface = { path = "../credentials-interface" }
@@ -0,0 +1,272 @@
// Copyright 2025 Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use nym_sphinx::addressing::Recipient;
use nym_wireguard_types::PeerPublicKey;
use crate::{
latest::registration::IpPair,
traits::{FinalMessage, InitMessage, QueryBandwidthMessage, TopUpMessage, Versionable},
v2, v3, v4, v5, AuthenticatorVersion, Error,
};
// This is very redundant with AuthenticatorRequest and I reckon they could be smooshed.
// It is a bit out of scope for me at the moment though
#[derive(Debug)]
pub enum ClientMessage {
Initial(Box<dyn InitMessage + Send + Sync + 'static>),
Final(Box<dyn FinalMessage + Send + Sync + 'static>),
Query(Box<dyn QueryBandwidthMessage + Send + Sync + 'static>),
TopUp(Box<dyn TopUpMessage + Send + Sync + 'static>),
}
impl ClientMessage {
// check if message is wasteful e.g. contains a credential
pub fn is_wasteful(&self) -> bool {
match self {
Self::Final(msg) => msg.credential().is_some(),
Self::TopUp(_) => true,
Self::Initial(_) | Self::Query(_) => false,
}
}
fn version(&self) -> AuthenticatorVersion {
match self {
ClientMessage::Initial(msg) => msg.version(),
ClientMessage::Final(msg) => msg.version(),
ClientMessage::Query(msg) => msg.version(),
ClientMessage::TopUp(msg) => msg.version(),
}
}
pub fn bytes(&self, reply_to: Recipient) -> Result<(Vec<u8>, u64), Error> {
match self.version() {
AuthenticatorVersion::V1 => Err(Error::UnsupportedVersion),
AuthenticatorVersion::V2 => {
use v2::{
registration::{ClientMac, FinalMessage, GatewayClient, InitMessage},
request::AuthenticatorRequest,
};
match self {
ClientMessage::Initial(init_message) => {
let (req, id) = AuthenticatorRequest::new_initial_request(
InitMessage {
pub_key: init_message.pub_key(),
},
reply_to,
);
Ok((req.to_bytes()?, id))
}
ClientMessage::Final(final_message) => {
let (req, id) = AuthenticatorRequest::new_final_request(
FinalMessage {
gateway_client: GatewayClient {
pub_key: final_message.gateway_client_pub_key(),
private_ip: final_message
.gateway_client_ipv4()
.ok_or(Error::UnsupportedMessage)?
.into(),
mac: ClientMac::new(final_message.gateway_client_mac()),
},
credential: final_message.credential(),
},
reply_to,
);
Ok((req.to_bytes()?, id))
}
ClientMessage::Query(query_message) => {
let (req, id) = AuthenticatorRequest::new_query_request(
query_message.pub_key(),
reply_to,
);
Ok((req.to_bytes()?, id))
}
_ => Err(Error::UnsupportedMessage),
}
}
AuthenticatorVersion::V3 => {
use v3::{
registration::{ClientMac, FinalMessage, GatewayClient, InitMessage},
request::AuthenticatorRequest,
topup::TopUpMessage,
};
match self {
ClientMessage::Initial(init_message) => {
let (req, id) = AuthenticatorRequest::new_initial_request(
InitMessage {
pub_key: init_message.pub_key(),
},
reply_to,
);
Ok((req.to_bytes()?, id))
}
ClientMessage::Final(final_message) => {
let (req, id) = AuthenticatorRequest::new_final_request(
FinalMessage {
gateway_client: GatewayClient {
pub_key: final_message.gateway_client_pub_key(),
private_ip: final_message
.gateway_client_ipv4()
.ok_or(Error::UnsupportedMessage)?
.into(),
mac: ClientMac::new(final_message.gateway_client_mac()),
},
credential: final_message.credential(),
},
reply_to,
);
Ok((req.to_bytes()?, id))
}
ClientMessage::Query(query_message) => {
let (req, id) = AuthenticatorRequest::new_query_request(
query_message.pub_key(),
reply_to,
);
Ok((req.to_bytes()?, id))
}
ClientMessage::TopUp(top_up_message) => {
let (req, id) = AuthenticatorRequest::new_topup_request(
TopUpMessage {
pub_key: top_up_message.pub_key(),
credential: top_up_message.credential(),
},
reply_to,
);
Ok((req.to_bytes()?, id))
}
}
}
AuthenticatorVersion::V4 => {
use v4::{
registration::{ClientMac, FinalMessage, GatewayClient, InitMessage},
request::AuthenticatorRequest,
topup::TopUpMessage,
};
match self {
ClientMessage::Initial(init_message) => {
let (req, id) = AuthenticatorRequest::new_initial_request(
InitMessage {
pub_key: init_message.pub_key(),
},
reply_to,
);
Ok((req.to_bytes()?, id))
}
ClientMessage::Final(final_message) => {
let (req, id) = AuthenticatorRequest::new_final_request(
FinalMessage {
gateway_client: GatewayClient {
pub_key: final_message.gateway_client_pub_key(),
private_ips: IpPair {
ipv4: final_message
.gateway_client_ipv4()
.ok_or(Error::UnsupportedMessage)?,
ipv6: final_message
.gateway_client_ipv6()
.ok_or(Error::UnsupportedMessage)?,
}
.into(),
mac: ClientMac::new(final_message.gateway_client_mac()),
},
credential: final_message.credential(),
},
reply_to,
);
Ok((req.to_bytes()?, id))
}
ClientMessage::Query(query_message) => {
let (req, id) = AuthenticatorRequest::new_query_request(
query_message.pub_key(),
reply_to,
);
Ok((req.to_bytes()?, id))
}
ClientMessage::TopUp(top_up_message) => {
let (req, id) = AuthenticatorRequest::new_topup_request(
TopUpMessage {
pub_key: top_up_message.pub_key(),
credential: top_up_message.credential(),
},
reply_to,
);
Ok((req.to_bytes()?, id))
}
}
}
AuthenticatorVersion::V5 => {
use v5::{
registration::{ClientMac, FinalMessage, GatewayClient, InitMessage},
request::AuthenticatorRequest,
topup::TopUpMessage,
};
match self {
ClientMessage::Initial(init_message) => {
let (req, id) = AuthenticatorRequest::new_initial_request(InitMessage {
pub_key: init_message.pub_key(),
});
Ok((req.to_bytes()?, id))
}
ClientMessage::Final(final_message) => {
let (req, id) = AuthenticatorRequest::new_final_request(FinalMessage {
gateway_client: GatewayClient {
pub_key: final_message.gateway_client_pub_key(),
private_ips: IpPair {
ipv4: final_message
.gateway_client_ipv4()
.ok_or(Error::UnsupportedMessage)?,
ipv6: final_message
.gateway_client_ipv6()
.ok_or(Error::UnsupportedMessage)?,
},
mac: ClientMac::new(final_message.gateway_client_mac()),
},
credential: final_message.credential(),
});
Ok((req.to_bytes()?, id))
}
ClientMessage::Query(query_message) => {
let (req, id) =
AuthenticatorRequest::new_query_request(query_message.pub_key());
Ok((req.to_bytes()?, id))
}
ClientMessage::TopUp(top_up_message) => {
let (req, id) = AuthenticatorRequest::new_topup_request(TopUpMessage {
pub_key: top_up_message.pub_key(),
credential: top_up_message.credential(),
});
Ok((req.to_bytes()?, id))
}
}
}
AuthenticatorVersion::UNKNOWN => Err(Error::UnknownVersion),
}
}
pub fn use_surbs(&self) -> bool {
use AuthenticatorVersion::*;
match self.version() {
V1 | V2 | V3 | V4 => false,
V5 => true,
UNKNOWN => true,
}
}
}
// Same comment as above struct
#[derive(Debug)]
pub struct QueryMessageImpl {
pub pub_key: PeerPublicKey,
pub version: AuthenticatorVersion,
}
impl Versionable for QueryMessageImpl {
fn version(&self) -> AuthenticatorVersion {
self.version
}
}
impl QueryBandwidthMessage for QueryMessageImpl {
fn pub_key(&self) -> PeerPublicKey {
self.pub_key
}
}
+13 -2
View File
@@ -23,6 +23,17 @@ pub enum Error {
#[error("conversion: {0}")]
Conversion(String),
#[error("failed to serialize response packet: {source}")]
FailedToSerializeResponsePacket { source: Box<bincode::ErrorKind> },
// TODO add version number for debugging
#[error("unknown version number")]
UnknownVersion,
// TODO add version number for debugging
#[error("unsupported request version")]
UnsupportedVersion,
#[error("gateway doesn't support this type of message")]
UnsupportedMessage,
#[error(transparent)]
Bincode(#[from] bincode::Error),
}
+6 -1
View File
@@ -1,6 +1,9 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
pub mod client_message;
pub mod request;
pub mod response;
pub mod traits;
pub mod v1;
pub mod v2;
@@ -10,11 +13,13 @@ pub mod v5;
mod error;
mod util;
mod version;
pub use error::Error;
pub use v5 as latest;
pub use version::AuthenticatorVersion;
pub const CURRENT_VERSION: u8 = 5;
pub const CURRENT_VERSION: u8 = latest::VERSION;
fn make_bincode_serializer() -> impl bincode::Options {
use bincode::Options;
@@ -0,0 +1,204 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use nym_service_provider_requests_common::{Protocol, ServiceProviderType};
use nym_sphinx::addressing::Recipient;
use crate::traits::{FinalMessage, InitMessage, QueryBandwidthMessage, TopUpMessage};
use crate::{v1, v2, v3, v4, v5};
#[derive(Debug)]
pub enum AuthenticatorRequest {
Initial {
msg: Box<dyn InitMessage + Send + Sync + 'static>,
protocol: Protocol,
reply_to: Option<Recipient>,
request_id: u64,
},
Final {
msg: Box<dyn FinalMessage + Send + Sync + 'static>,
protocol: Protocol,
reply_to: Option<Recipient>,
request_id: u64,
},
QueryBandwidth {
msg: Box<dyn QueryBandwidthMessage + Send + Sync + 'static>,
protocol: Protocol,
reply_to: Option<Recipient>,
request_id: u64,
},
TopUpBandwidth {
msg: Box<dyn TopUpMessage + Send + Sync + 'static>,
protocol: Protocol,
reply_to: Option<Recipient>,
request_id: u64,
},
}
impl From<v1::request::AuthenticatorRequest> for AuthenticatorRequest {
fn from(value: v1::request::AuthenticatorRequest) -> Self {
match value.data {
v1::request::AuthenticatorRequestData::Initial(init_message) => Self::Initial {
msg: Box::new(init_message),
protocol: Protocol {
version: value.version,
service_provider_type: ServiceProviderType::Authenticator,
},
reply_to: Some(value.reply_to),
request_id: value.request_id,
},
v1::request::AuthenticatorRequestData::Final(gateway_client) => Self::Final {
msg: Box::new(gateway_client),
protocol: Protocol {
version: value.version,
service_provider_type: ServiceProviderType::Authenticator,
},
reply_to: Some(value.reply_to),
request_id: value.request_id,
},
v1::request::AuthenticatorRequestData::QueryBandwidth(peer_public_key) => {
Self::QueryBandwidth {
msg: Box::new(peer_public_key),
protocol: Protocol {
version: value.version,
service_provider_type: ServiceProviderType::Authenticator,
},
reply_to: Some(value.reply_to),
request_id: value.request_id,
}
}
}
}
}
impl From<v2::request::AuthenticatorRequest> for AuthenticatorRequest {
fn from(value: v2::request::AuthenticatorRequest) -> Self {
match value.data {
v2::request::AuthenticatorRequestData::Initial(init_message) => Self::Initial {
msg: Box::new(init_message),
protocol: value.protocol,
reply_to: Some(value.reply_to),
request_id: value.request_id,
},
v2::request::AuthenticatorRequestData::Final(final_message) => Self::Final {
msg: final_message,
protocol: value.protocol,
reply_to: Some(value.reply_to),
request_id: value.request_id,
},
v2::request::AuthenticatorRequestData::QueryBandwidth(peer_public_key) => {
Self::QueryBandwidth {
msg: Box::new(peer_public_key),
protocol: value.protocol,
reply_to: Some(value.reply_to),
request_id: value.request_id,
}
}
}
}
}
impl From<v3::request::AuthenticatorRequest> for AuthenticatorRequest {
fn from(value: v3::request::AuthenticatorRequest) -> Self {
match value.data {
v3::request::AuthenticatorRequestData::Initial(init_message) => Self::Initial {
msg: Box::new(init_message),
protocol: value.protocol,
reply_to: Some(value.reply_to),
request_id: value.request_id,
},
v3::request::AuthenticatorRequestData::Final(final_message) => Self::Final {
msg: final_message,
protocol: value.protocol,
reply_to: Some(value.reply_to),
request_id: value.request_id,
},
v3::request::AuthenticatorRequestData::QueryBandwidth(peer_public_key) => {
Self::QueryBandwidth {
msg: Box::new(peer_public_key),
protocol: value.protocol,
reply_to: Some(value.reply_to),
request_id: value.request_id,
}
}
v3::request::AuthenticatorRequestData::TopUpBandwidth(top_up_message) => {
Self::TopUpBandwidth {
msg: top_up_message,
protocol: value.protocol,
reply_to: Some(value.reply_to),
request_id: value.request_id,
}
}
}
}
}
impl From<v4::request::AuthenticatorRequest> for AuthenticatorRequest {
fn from(value: v4::request::AuthenticatorRequest) -> Self {
match value.data {
v4::request::AuthenticatorRequestData::Initial(init_message) => Self::Initial {
msg: Box::new(init_message),
protocol: value.protocol,
reply_to: Some(value.reply_to),
request_id: value.request_id,
},
v4::request::AuthenticatorRequestData::Final(final_message) => Self::Final {
msg: final_message,
protocol: value.protocol,
reply_to: Some(value.reply_to),
request_id: value.request_id,
},
v4::request::AuthenticatorRequestData::QueryBandwidth(peer_public_key) => {
Self::QueryBandwidth {
msg: Box::new(peer_public_key),
protocol: value.protocol,
reply_to: Some(value.reply_to),
request_id: value.request_id,
}
}
v4::request::AuthenticatorRequestData::TopUpBandwidth(top_up_message) => {
Self::TopUpBandwidth {
msg: top_up_message,
protocol: value.protocol,
reply_to: Some(value.reply_to),
request_id: value.request_id,
}
}
}
}
}
impl From<v5::request::AuthenticatorRequest> for AuthenticatorRequest {
fn from(value: v5::request::AuthenticatorRequest) -> Self {
match value.data {
v5::request::AuthenticatorRequestData::Initial(init_message) => Self::Initial {
msg: Box::new(init_message),
protocol: value.protocol,
reply_to: None,
request_id: value.request_id,
},
v5::request::AuthenticatorRequestData::Final(final_message) => Self::Final {
msg: final_message,
protocol: value.protocol,
reply_to: None,
request_id: value.request_id,
},
v5::request::AuthenticatorRequestData::QueryBandwidth(peer_public_key) => {
Self::QueryBandwidth {
msg: Box::new(peer_public_key),
protocol: value.protocol,
reply_to: None,
request_id: value.request_id,
}
}
v5::request::AuthenticatorRequestData::TopUpBandwidth(top_up_message) => {
Self::TopUpBandwidth {
msg: top_up_message,
protocol: value.protocol,
reply_to: None,
request_id: value.request_id,
}
}
}
}
}
@@ -0,0 +1,106 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::traits::{
Id, PendingRegistrationResponse, RegisteredResponse, RemainingBandwidthResponse,
TopUpBandwidthResponse,
};
use crate::{v2, v3, v4, v5};
#[derive(Debug)]
pub enum AuthenticatorResponse {
PendingRegistration(Box<dyn PendingRegistrationResponse + Send + Sync + 'static>),
Registered(Box<dyn RegisteredResponse + Send + Sync + 'static>),
RemainingBandwidth(Box<dyn RemainingBandwidthResponse + Send + Sync + 'static>),
TopUpBandwidth(Box<dyn TopUpBandwidthResponse + Send + Sync + 'static>),
}
impl Id for AuthenticatorResponse {
fn id(&self) -> u64 {
match self {
AuthenticatorResponse::PendingRegistration(pending_registration_response) => {
pending_registration_response.id()
}
AuthenticatorResponse::Registered(registered_response) => registered_response.id(),
AuthenticatorResponse::RemainingBandwidth(remaining_bandwidth_response) => {
remaining_bandwidth_response.id()
}
AuthenticatorResponse::TopUpBandwidth(top_up_bandwidth_response) => {
top_up_bandwidth_response.id()
}
}
}
}
impl From<v2::response::AuthenticatorResponse> for AuthenticatorResponse {
fn from(value: v2::response::AuthenticatorResponse) -> Self {
match value.data {
v2::response::AuthenticatorResponseData::PendingRegistration(
pending_registration_response,
) => Self::PendingRegistration(Box::new(pending_registration_response)),
v2::response::AuthenticatorResponseData::Registered(registered_response) => {
Self::Registered(Box::new(registered_response))
}
v2::response::AuthenticatorResponseData::RemainingBandwidth(
remaining_bandwidth_response,
) => Self::RemainingBandwidth(Box::new(remaining_bandwidth_response)),
}
}
}
impl From<v3::response::AuthenticatorResponse> for AuthenticatorResponse {
fn from(value: v3::response::AuthenticatorResponse) -> Self {
match value.data {
v3::response::AuthenticatorResponseData::PendingRegistration(
pending_registration_response,
) => Self::PendingRegistration(Box::new(pending_registration_response)),
v3::response::AuthenticatorResponseData::Registered(registered_response) => {
Self::Registered(Box::new(registered_response))
}
v3::response::AuthenticatorResponseData::RemainingBandwidth(
remaining_bandwidth_response,
) => Self::RemainingBandwidth(Box::new(remaining_bandwidth_response)),
v3::response::AuthenticatorResponseData::TopUpBandwidth(top_up_bandwidth_response) => {
Self::TopUpBandwidth(Box::new(top_up_bandwidth_response))
}
}
}
}
impl From<v4::response::AuthenticatorResponse> for AuthenticatorResponse {
fn from(value: v4::response::AuthenticatorResponse) -> Self {
match value.data {
v4::response::AuthenticatorResponseData::PendingRegistration(
pending_registration_response,
) => Self::PendingRegistration(Box::new(pending_registration_response)),
v4::response::AuthenticatorResponseData::Registered(registered_response) => {
Self::Registered(Box::new(registered_response))
}
v4::response::AuthenticatorResponseData::RemainingBandwidth(
remaining_bandwidth_response,
) => Self::RemainingBandwidth(Box::new(remaining_bandwidth_response)),
v4::response::AuthenticatorResponseData::TopUpBandwidth(top_up_bandwidth_response) => {
Self::TopUpBandwidth(Box::new(top_up_bandwidth_response))
}
}
}
}
impl From<v5::response::AuthenticatorResponse> for AuthenticatorResponse {
fn from(value: v5::response::AuthenticatorResponse) -> Self {
match value.data {
v5::response::AuthenticatorResponseData::PendingRegistration(
pending_registration_response,
) => Self::PendingRegistration(Box::new(pending_registration_response)),
v5::response::AuthenticatorResponseData::Registered(registered_response) => {
Self::Registered(Box::new(registered_response))
}
v5::response::AuthenticatorResponseData::RemainingBandwidth(
remaining_bandwidth_response,
) => Self::RemainingBandwidth(Box::new(remaining_bandwidth_response)),
v5::response::AuthenticatorResponseData::TopUpBandwidth(top_up_bandwidth_response) => {
Self::TopUpBandwidth(Box::new(top_up_bandwidth_response))
}
}
}
}
+437 -220
View File
@@ -1,49 +1,105 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use std::fmt;
use std::net::{Ipv4Addr, Ipv6Addr};
use nym_credentials_interface::CredentialSpendingData;
use nym_crypto::asymmetric::x25519::PrivateKey;
use nym_service_provider_requests_common::{Protocol, ServiceProviderType};
use nym_sphinx::addressing::clients::Recipient;
use nym_wireguard_types::PeerPublicKey;
use crate::{
v1, v2, v3, v4,
v5::{self, registration::IpPair},
Error,
};
use crate::latest::registration::IpPair;
use crate::{v1, v2, v3, v4, v5, AuthenticatorVersion, Error};
#[derive(Copy, Clone, Debug)]
pub enum AuthenticatorVersion {
V1,
V2,
V3,
V4,
V5,
UNKNOWN,
pub trait Versionable {
fn version(&self) -> AuthenticatorVersion;
}
impl From<Protocol> for AuthenticatorVersion {
fn from(value: Protocol) -> Self {
if value.service_provider_type != ServiceProviderType::Authenticator {
AuthenticatorVersion::UNKNOWN
} else if value.version == v1::VERSION {
AuthenticatorVersion::V1
} else if value.version == v2::VERSION {
AuthenticatorVersion::V2
} else if value.version == v3::VERSION {
AuthenticatorVersion::V3
} else if value.version == v4::VERSION {
AuthenticatorVersion::V4
} else if value.version == v5::VERSION {
AuthenticatorVersion::V5
} else {
AuthenticatorVersion::UNKNOWN
}
impl Versionable for v1::GatewayClient {
fn version(&self) -> AuthenticatorVersion {
AuthenticatorVersion::V1
}
}
pub trait InitMessage {
impl Versionable for v1::registration::InitMessage {
fn version(&self) -> AuthenticatorVersion {
AuthenticatorVersion::V1
}
}
impl Versionable for v2::registration::InitMessage {
fn version(&self) -> AuthenticatorVersion {
AuthenticatorVersion::V2
}
}
impl Versionable for v3::registration::InitMessage {
fn version(&self) -> AuthenticatorVersion {
AuthenticatorVersion::V3
}
}
impl Versionable for v4::registration::InitMessage {
fn version(&self) -> AuthenticatorVersion {
AuthenticatorVersion::V4
}
}
impl Versionable for v5::registration::InitMessage {
fn version(&self) -> AuthenticatorVersion {
AuthenticatorVersion::V5
}
}
impl Versionable for v2::registration::FinalMessage {
fn version(&self) -> AuthenticatorVersion {
AuthenticatorVersion::V2
}
}
impl Versionable for v3::registration::FinalMessage {
fn version(&self) -> AuthenticatorVersion {
AuthenticatorVersion::V3
}
}
impl Versionable for v4::registration::FinalMessage {
fn version(&self) -> AuthenticatorVersion {
AuthenticatorVersion::V4
}
}
impl Versionable for v5::registration::FinalMessage {
fn version(&self) -> AuthenticatorVersion {
AuthenticatorVersion::V5
}
}
impl Versionable for PeerPublicKey {
fn version(&self) -> AuthenticatorVersion {
AuthenticatorVersion::V3
}
}
impl Versionable for v3::topup::TopUpMessage {
fn version(&self) -> AuthenticatorVersion {
AuthenticatorVersion::V3
}
}
impl Versionable for v4::topup::TopUpMessage {
fn version(&self) -> AuthenticatorVersion {
AuthenticatorVersion::V4
}
}
impl Versionable for v5::topup::TopUpMessage {
fn version(&self) -> AuthenticatorVersion {
AuthenticatorVersion::V5
}
}
pub trait InitMessage: Versionable + fmt::Debug {
fn pub_key(&self) -> PeerPublicKey;
}
@@ -77,15 +133,18 @@ impl InitMessage for v5::registration::InitMessage {
}
}
pub trait FinalMessage {
fn pub_key(&self) -> PeerPublicKey;
pub trait FinalMessage: Versionable + fmt::Debug {
fn gateway_client_pub_key(&self) -> PeerPublicKey;
fn verify(&self, private_key: &PrivateKey, nonce: u64) -> Result<(), Error>;
fn private_ips(&self) -> IpPair;
fn gateway_client_ipv4(&self) -> Option<Ipv4Addr>;
fn gateway_client_ipv6(&self) -> Option<Ipv6Addr>;
fn gateway_client_mac(&self) -> Vec<u8>;
fn credential(&self) -> Option<CredentialSpendingData>;
}
impl FinalMessage for v1::GatewayClient {
fn pub_key(&self) -> PeerPublicKey {
fn gateway_client_pub_key(&self) -> PeerPublicKey {
self.pub_key
}
@@ -97,13 +156,28 @@ impl FinalMessage for v1::GatewayClient {
self.private_ip.into()
}
fn gateway_client_ipv4(&self) -> Option<Ipv4Addr> {
match self.private_ip {
std::net::IpAddr::V4(ipv4_addr) => Some(ipv4_addr),
std::net::IpAddr::V6(_) => None,
}
}
fn gateway_client_ipv6(&self) -> Option<Ipv6Addr> {
None
}
fn gateway_client_mac(&self) -> Vec<u8> {
self.mac.to_vec()
}
fn credential(&self) -> Option<CredentialSpendingData> {
None
}
}
impl FinalMessage for v2::registration::FinalMessage {
fn pub_key(&self) -> PeerPublicKey {
fn gateway_client_pub_key(&self) -> PeerPublicKey {
self.gateway_client.pub_key
}
@@ -115,13 +189,28 @@ impl FinalMessage for v2::registration::FinalMessage {
self.gateway_client.private_ip.into()
}
fn gateway_client_ipv4(&self) -> Option<Ipv4Addr> {
match self.gateway_client.private_ip {
std::net::IpAddr::V4(ipv4_addr) => Some(ipv4_addr),
std::net::IpAddr::V6(_) => None,
}
}
fn gateway_client_ipv6(&self) -> Option<Ipv6Addr> {
None
}
fn gateway_client_mac(&self) -> Vec<u8> {
self.gateway_client.mac.to_vec()
}
fn credential(&self) -> Option<CredentialSpendingData> {
self.credential.clone()
}
}
impl FinalMessage for v3::registration::FinalMessage {
fn pub_key(&self) -> PeerPublicKey {
fn gateway_client_pub_key(&self) -> PeerPublicKey {
self.gateway_client.pub_key
}
@@ -133,13 +222,28 @@ impl FinalMessage for v3::registration::FinalMessage {
self.gateway_client.private_ip.into()
}
fn gateway_client_ipv4(&self) -> Option<Ipv4Addr> {
match self.gateway_client.private_ip {
std::net::IpAddr::V4(ipv4_addr) => Some(ipv4_addr),
std::net::IpAddr::V6(_) => None,
}
}
fn gateway_client_ipv6(&self) -> Option<Ipv6Addr> {
None
}
fn gateway_client_mac(&self) -> Vec<u8> {
self.gateway_client.mac.to_vec()
}
fn credential(&self) -> Option<CredentialSpendingData> {
self.credential.clone()
}
}
impl FinalMessage for v4::registration::FinalMessage {
fn pub_key(&self) -> PeerPublicKey {
fn gateway_client_pub_key(&self) -> PeerPublicKey {
self.gateway_client.pub_key
}
@@ -151,13 +255,25 @@ impl FinalMessage for v4::registration::FinalMessage {
self.gateway_client.private_ips.into()
}
fn gateway_client_ipv4(&self) -> Option<Ipv4Addr> {
Some(self.gateway_client.private_ips.ipv4)
}
fn gateway_client_ipv6(&self) -> Option<Ipv6Addr> {
Some(self.gateway_client.private_ips.ipv6)
}
fn gateway_client_mac(&self) -> Vec<u8> {
self.gateway_client.mac.to_vec()
}
fn credential(&self) -> Option<CredentialSpendingData> {
self.credential.clone()
}
}
impl FinalMessage for v5::registration::FinalMessage {
fn pub_key(&self) -> PeerPublicKey {
fn gateway_client_pub_key(&self) -> PeerPublicKey {
self.gateway_client.pub_key
}
@@ -169,12 +285,24 @@ impl FinalMessage for v5::registration::FinalMessage {
self.gateway_client.private_ips
}
fn gateway_client_ipv4(&self) -> Option<Ipv4Addr> {
Some(self.gateway_client.private_ips.ipv4)
}
fn gateway_client_ipv6(&self) -> Option<Ipv6Addr> {
Some(self.gateway_client.private_ips.ipv6)
}
fn gateway_client_mac(&self) -> Vec<u8> {
self.gateway_client.mac.to_vec()
}
fn credential(&self) -> Option<CredentialSpendingData> {
self.credential.clone()
}
}
pub trait QueryBandwidthMessage {
pub trait QueryBandwidthMessage: Versionable + fmt::Debug {
fn pub_key(&self) -> PeerPublicKey;
}
@@ -184,7 +312,7 @@ impl QueryBandwidthMessage for PeerPublicKey {
}
}
pub trait TopUpMessage {
pub trait TopUpMessage: Versionable + fmt::Debug {
fn pub_key(&self) -> PeerPublicKey;
fn credential(&self) -> CredentialSpendingData;
}
@@ -219,197 +347,286 @@ impl TopUpMessage for v5::topup::TopUpMessage {
}
}
pub enum AuthenticatorRequest {
Initial {
msg: Box<dyn InitMessage + Send + Sync + 'static>,
protocol: Protocol,
reply_to: Option<Recipient>,
request_id: u64,
},
Final {
msg: Box<dyn FinalMessage + Send + Sync + 'static>,
protocol: Protocol,
reply_to: Option<Recipient>,
request_id: u64,
},
QueryBandwidth {
msg: Box<dyn QueryBandwidthMessage + Send + Sync + 'static>,
protocol: Protocol,
reply_to: Option<Recipient>,
request_id: u64,
},
TopUpBandwidth {
msg: Box<dyn TopUpMessage + Send + Sync + 'static>,
protocol: Protocol,
reply_to: Option<Recipient>,
request_id: u64,
},
pub trait Id {
fn id(&self) -> u64;
}
impl From<v1::request::AuthenticatorRequest> for AuthenticatorRequest {
fn from(value: v1::request::AuthenticatorRequest) -> Self {
match value.data {
v1::request::AuthenticatorRequestData::Initial(init_message) => Self::Initial {
msg: Box::new(init_message),
protocol: Protocol {
version: value.version,
service_provider_type: ServiceProviderType::Authenticator,
},
reply_to: Some(value.reply_to),
request_id: value.request_id,
},
v1::request::AuthenticatorRequestData::Final(gateway_client) => Self::Final {
msg: Box::new(gateway_client),
protocol: Protocol {
version: value.version,
service_provider_type: ServiceProviderType::Authenticator,
},
reply_to: Some(value.reply_to),
request_id: value.request_id,
},
v1::request::AuthenticatorRequestData::QueryBandwidth(peer_public_key) => {
Self::QueryBandwidth {
msg: Box::new(peer_public_key),
protocol: Protocol {
version: value.version,
service_provider_type: ServiceProviderType::Authenticator,
},
reply_to: Some(value.reply_to),
request_id: value.request_id,
}
}
}
impl Id for v2::response::PendingRegistrationResponse {
fn id(&self) -> u64 {
self.request_id
}
}
impl From<v2::request::AuthenticatorRequest> for AuthenticatorRequest {
fn from(value: v2::request::AuthenticatorRequest) -> Self {
match value.data {
v2::request::AuthenticatorRequestData::Initial(init_message) => Self::Initial {
msg: Box::new(init_message),
protocol: value.protocol,
reply_to: Some(value.reply_to),
request_id: value.request_id,
},
v2::request::AuthenticatorRequestData::Final(final_message) => Self::Final {
msg: final_message,
protocol: value.protocol,
reply_to: Some(value.reply_to),
request_id: value.request_id,
},
v2::request::AuthenticatorRequestData::QueryBandwidth(peer_public_key) => {
Self::QueryBandwidth {
msg: Box::new(peer_public_key),
protocol: value.protocol,
reply_to: Some(value.reply_to),
request_id: value.request_id,
}
}
}
impl Id for v3::response::PendingRegistrationResponse {
fn id(&self) -> u64 {
self.request_id
}
}
impl From<v3::request::AuthenticatorRequest> for AuthenticatorRequest {
fn from(value: v3::request::AuthenticatorRequest) -> Self {
match value.data {
v3::request::AuthenticatorRequestData::Initial(init_message) => Self::Initial {
msg: Box::new(init_message),
protocol: value.protocol,
reply_to: Some(value.reply_to),
request_id: value.request_id,
},
v3::request::AuthenticatorRequestData::Final(final_message) => Self::Final {
msg: final_message,
protocol: value.protocol,
reply_to: Some(value.reply_to),
request_id: value.request_id,
},
v3::request::AuthenticatorRequestData::QueryBandwidth(peer_public_key) => {
Self::QueryBandwidth {
msg: Box::new(peer_public_key),
protocol: value.protocol,
reply_to: Some(value.reply_to),
request_id: value.request_id,
}
}
v3::request::AuthenticatorRequestData::TopUpBandwidth(top_up_message) => {
Self::TopUpBandwidth {
msg: top_up_message,
protocol: value.protocol,
reply_to: Some(value.reply_to),
request_id: value.request_id,
}
}
}
impl Id for v4::response::PendingRegistrationResponse {
fn id(&self) -> u64 {
self.request_id
}
}
impl From<v4::request::AuthenticatorRequest> for AuthenticatorRequest {
fn from(value: v4::request::AuthenticatorRequest) -> Self {
match value.data {
v4::request::AuthenticatorRequestData::Initial(init_message) => Self::Initial {
msg: Box::new(init_message),
protocol: value.protocol,
reply_to: Some(value.reply_to),
request_id: value.request_id,
},
v4::request::AuthenticatorRequestData::Final(final_message) => Self::Final {
msg: final_message,
protocol: value.protocol,
reply_to: Some(value.reply_to),
request_id: value.request_id,
},
v4::request::AuthenticatorRequestData::QueryBandwidth(peer_public_key) => {
Self::QueryBandwidth {
msg: Box::new(peer_public_key),
protocol: value.protocol,
reply_to: Some(value.reply_to),
request_id: value.request_id,
}
}
v4::request::AuthenticatorRequestData::TopUpBandwidth(top_up_message) => {
Self::TopUpBandwidth {
msg: top_up_message,
protocol: value.protocol,
reply_to: Some(value.reply_to),
request_id: value.request_id,
}
}
}
impl Id for v5::response::PendingRegistrationResponse {
fn id(&self) -> u64 {
self.request_id
}
}
impl From<v5::request::AuthenticatorRequest> for AuthenticatorRequest {
fn from(value: v5::request::AuthenticatorRequest) -> Self {
match value.data {
v5::request::AuthenticatorRequestData::Initial(init_message) => Self::Initial {
msg: Box::new(init_message),
protocol: value.protocol,
reply_to: None,
request_id: value.request_id,
},
v5::request::AuthenticatorRequestData::Final(final_message) => Self::Final {
msg: final_message,
protocol: value.protocol,
reply_to: None,
request_id: value.request_id,
},
v5::request::AuthenticatorRequestData::QueryBandwidth(peer_public_key) => {
Self::QueryBandwidth {
msg: Box::new(peer_public_key),
protocol: value.protocol,
reply_to: None,
request_id: value.request_id,
}
}
v5::request::AuthenticatorRequestData::TopUpBandwidth(top_up_message) => {
Self::TopUpBandwidth {
msg: top_up_message,
protocol: value.protocol,
reply_to: None,
request_id: value.request_id,
}
}
}
impl Id for v2::response::RegisteredResponse {
fn id(&self) -> u64 {
self.request_id
}
}
impl Id for v3::response::RegisteredResponse {
fn id(&self) -> u64 {
self.request_id
}
}
impl Id for v4::response::RegisteredResponse {
fn id(&self) -> u64 {
self.request_id
}
}
impl Id for v5::response::RegisteredResponse {
fn id(&self) -> u64 {
self.request_id
}
}
impl Id for v2::response::RemainingBandwidthResponse {
fn id(&self) -> u64 {
self.request_id
}
}
impl Id for v3::response::RemainingBandwidthResponse {
fn id(&self) -> u64 {
self.request_id
}
}
impl Id for v4::response::RemainingBandwidthResponse {
fn id(&self) -> u64 {
self.request_id
}
}
impl Id for v5::response::RemainingBandwidthResponse {
fn id(&self) -> u64 {
self.request_id
}
}
impl Id for v3::response::TopUpBandwidthResponse {
fn id(&self) -> u64 {
self.request_id
}
}
impl Id for v4::response::TopUpBandwidthResponse {
fn id(&self) -> u64 {
self.request_id
}
}
impl Id for v5::response::TopUpBandwidthResponse {
fn id(&self) -> u64 {
self.request_id
}
}
pub trait PendingRegistrationResponse: Id + fmt::Debug {
fn nonce(&self) -> u64;
fn verify(&self, gateway_key: &PrivateKey) -> std::result::Result<(), Error>;
fn pub_key(&self) -> PeerPublicKey;
fn private_ips(&self) -> IpPair;
}
impl PendingRegistrationResponse for v2::response::PendingRegistrationResponse {
fn nonce(&self) -> u64 {
self.reply.nonce
}
fn verify(&self, gateway_key: &PrivateKey) -> std::result::Result<(), Error> {
self.reply.gateway_data.verify(gateway_key, self.nonce())
}
fn pub_key(&self) -> PeerPublicKey {
self.reply.gateway_data.pub_key
}
fn private_ips(&self) -> IpPair {
self.reply.gateway_data.private_ip.into()
}
}
impl PendingRegistrationResponse for v3::response::PendingRegistrationResponse {
fn nonce(&self) -> u64 {
self.reply.nonce
}
fn verify(&self, gateway_key: &PrivateKey) -> std::result::Result<(), Error> {
self.reply.gateway_data.verify(gateway_key, self.nonce())
}
fn pub_key(&self) -> PeerPublicKey {
self.reply.gateway_data.pub_key
}
fn private_ips(&self) -> IpPair {
self.reply.gateway_data.private_ip.into()
}
}
impl PendingRegistrationResponse for v4::response::PendingRegistrationResponse {
fn nonce(&self) -> u64 {
self.reply.nonce
}
fn verify(&self, gateway_key: &PrivateKey) -> std::result::Result<(), Error> {
self.reply.gateway_data.verify(gateway_key, self.nonce())
}
fn pub_key(&self) -> PeerPublicKey {
self.reply.gateway_data.pub_key
}
fn private_ips(&self) -> IpPair {
self.reply.gateway_data.private_ips.into()
}
}
impl PendingRegistrationResponse for v5::response::PendingRegistrationResponse {
fn nonce(&self) -> u64 {
self.reply.nonce
}
fn verify(&self, gateway_key: &PrivateKey) -> std::result::Result<(), Error> {
self.reply.gateway_data.verify(gateway_key, self.nonce())
}
fn pub_key(&self) -> PeerPublicKey {
self.reply.gateway_data.pub_key
}
fn private_ips(&self) -> IpPair {
self.reply.gateway_data.private_ips
}
}
pub trait RegisteredResponse: Id + fmt::Debug {
fn private_ips(&self) -> IpPair;
fn pub_key(&self) -> PeerPublicKey;
fn wg_port(&self) -> u16;
}
impl RegisteredResponse for v2::response::RegisteredResponse {
fn private_ips(&self) -> IpPair {
self.reply.private_ip.into()
}
fn pub_key(&self) -> PeerPublicKey {
self.reply.pub_key
}
fn wg_port(&self) -> u16 {
self.reply.wg_port
}
}
impl RegisteredResponse for v3::response::RegisteredResponse {
fn private_ips(&self) -> IpPair {
self.reply.private_ip.into()
}
fn pub_key(&self) -> PeerPublicKey {
self.reply.pub_key
}
fn wg_port(&self) -> u16 {
self.reply.wg_port
}
}
impl RegisteredResponse for v4::response::RegisteredResponse {
fn private_ips(&self) -> IpPair {
self.reply.private_ips.into()
}
fn pub_key(&self) -> PeerPublicKey {
self.reply.pub_key
}
fn wg_port(&self) -> u16 {
self.reply.wg_port
}
}
impl RegisteredResponse for v5::response::RegisteredResponse {
fn private_ips(&self) -> IpPair {
self.reply.private_ips
}
fn pub_key(&self) -> PeerPublicKey {
self.reply.pub_key
}
fn wg_port(&self) -> u16 {
self.reply.wg_port
}
}
pub trait RemainingBandwidthResponse: Id + fmt::Debug {
fn available_bandwidth(&self) -> Option<i64>;
}
impl RemainingBandwidthResponse for v2::response::RemainingBandwidthResponse {
fn available_bandwidth(&self) -> Option<i64> {
self.reply.as_ref().map(|r| r.available_bandwidth)
}
}
impl RemainingBandwidthResponse for v3::response::RemainingBandwidthResponse {
fn available_bandwidth(&self) -> Option<i64> {
self.reply.as_ref().map(|r| r.available_bandwidth)
}
}
impl RemainingBandwidthResponse for v4::response::RemainingBandwidthResponse {
fn available_bandwidth(&self) -> Option<i64> {
self.reply.as_ref().map(|r| r.available_bandwidth)
}
}
impl RemainingBandwidthResponse for v5::response::RemainingBandwidthResponse {
fn available_bandwidth(&self) -> Option<i64> {
self.reply.as_ref().map(|r| r.available_bandwidth)
}
}
pub trait TopUpBandwidthResponse: Id + fmt::Debug {
fn available_bandwidth(&self) -> i64;
}
impl TopUpBandwidthResponse for v3::response::TopUpBandwidthResponse {
fn available_bandwidth(&self) -> i64 {
self.reply.available_bandwidth
}
}
impl TopUpBandwidthResponse for v4::response::TopUpBandwidthResponse {
fn available_bandwidth(&self) -> i64 {
self.reply.available_bandwidth
}
}
impl TopUpBandwidthResponse for v5::response::TopUpBandwidthResponse {
fn available_bandwidth(&self) -> i64 {
self.reply.available_bandwidth
}
}
@@ -0,0 +1,191 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use super::{v1, v2, v3, v4, v5};
use nym_service_provider_requests_common::{Protocol, ServiceProviderType};
#[derive(Copy, Clone, Debug, PartialEq, strum_macros::Display)]
#[strum(serialize_all = "snake_case")]
pub enum AuthenticatorVersion {
/// introduced in wispa release (1.1.5)
V1,
/// introduced in aero release (1.1.9)
V2,
/// introduced in magura release (1.1.10)
V3,
/// introduced in crunch release (1.2.0)
V4,
/// introduced in dorina-patched release (1.6.1)
V5,
UNKNOWN,
}
impl AuthenticatorVersion {
pub const LATEST: Self = Self::V5;
pub const fn release_version(&self) -> semver::Version {
match self {
AuthenticatorVersion::V1 => semver::Version::new(1, 1, 5),
AuthenticatorVersion::V2 => semver::Version::new(1, 1, 9),
AuthenticatorVersion::V3 => semver::Version::new(1, 1, 10),
AuthenticatorVersion::V4 => semver::Version::new(1, 2, 0),
AuthenticatorVersion::V5 => semver::Version::new(1, 6, 1),
AuthenticatorVersion::UNKNOWN => semver::Version::new(0, 0, 0),
}
}
}
impl From<Protocol> for AuthenticatorVersion {
fn from(value: Protocol) -> Self {
if value.service_provider_type != ServiceProviderType::Authenticator {
AuthenticatorVersion::UNKNOWN
} else if value.version == v1::VERSION {
AuthenticatorVersion::V1
} else if value.version == v2::VERSION {
AuthenticatorVersion::V2
} else if value.version == v3::VERSION {
AuthenticatorVersion::V3
} else if value.version == v4::VERSION {
AuthenticatorVersion::V4
} else if value.version == v5::VERSION {
AuthenticatorVersion::V5
} else {
AuthenticatorVersion::UNKNOWN
}
}
}
impl From<u8> for AuthenticatorVersion {
fn from(value: u8) -> Self {
if value == v1::VERSION {
AuthenticatorVersion::V1
} else if value == v2::VERSION {
AuthenticatorVersion::V2
} else if value == v3::VERSION {
AuthenticatorVersion::V3
} else if value == v4::VERSION {
AuthenticatorVersion::V4
} else if value == v5::VERSION {
AuthenticatorVersion::V5
} else {
AuthenticatorVersion::UNKNOWN
}
}
}
impl From<&str> for AuthenticatorVersion {
fn from(value: &str) -> Self {
let Ok(semver) = semver::Version::parse(value) else {
return Self::UNKNOWN;
};
semver.into()
}
}
impl From<Option<&String>> for AuthenticatorVersion {
fn from(value: Option<&String>) -> Self {
match value {
None => Self::UNKNOWN,
Some(value) => value.as_str().into(),
}
}
}
impl From<String> for AuthenticatorVersion {
fn from(value: String) -> Self {
Self::from(value.as_str())
}
}
impl From<Option<String>> for AuthenticatorVersion {
fn from(value: Option<String>) -> Self {
value.as_ref().into()
}
}
impl From<semver::Version> for AuthenticatorVersion {
fn from(semver: semver::Version) -> Self {
if semver < AuthenticatorVersion::V1.release_version() {
return Self::UNKNOWN;
}
if semver < AuthenticatorVersion::V2.release_version() {
return Self::V1;
}
if semver < AuthenticatorVersion::V3.release_version() {
return Self::V2;
}
if semver < AuthenticatorVersion::V4.release_version() {
return Self::V3;
}
if semver < AuthenticatorVersion::V5.release_version() {
return Self::V4;
}
// if provided version is higher (or equal) to release version of V5,
// we return the latest (i.e. v5)
debug_assert_eq!(Self::V5, Self::LATEST, "a new AuthenticatorVersion variant has been introduced without adjusting the `From<semver::Version>` trait");
Self::LATEST
}
}
#[cfg(test)]
mod tests {
use super::super::latest;
use super::*;
#[test]
fn strum_display() {
// sanity check on formatting and casing
assert_eq!("v1", AuthenticatorVersion::V1.to_string());
assert_eq!("v2", AuthenticatorVersion::V2.to_string());
assert_eq!("unknown", AuthenticatorVersion::UNKNOWN.to_string());
}
#[test]
fn u8_conversion() {
assert_eq!(AuthenticatorVersion::V1, AuthenticatorVersion::from(1u8));
assert_eq!(AuthenticatorVersion::V2, AuthenticatorVersion::from(2u8));
assert_eq!(
AuthenticatorVersion::UNKNOWN,
AuthenticatorVersion::from(latest::VERSION + 1)
);
assert_eq!(
AuthenticatorVersion::UNKNOWN,
AuthenticatorVersion::from(0u8)
);
assert_eq!(
AuthenticatorVersion::UNKNOWN,
AuthenticatorVersion::from(255u8)
);
}
#[test]
fn semver_checks() {
assert_eq!(AuthenticatorVersion::UNKNOWN, "1.1.4".into());
assert_eq!(AuthenticatorVersion::UNKNOWN, "0.1.0".into());
assert_eq!(AuthenticatorVersion::UNKNOWN, "1.0.4".into());
assert_eq!(AuthenticatorVersion::V1, "1.1.5".into());
assert_eq!(AuthenticatorVersion::V1, "1.1.6".into());
assert_eq!(AuthenticatorVersion::V1, "1.1.8".into());
assert_eq!(AuthenticatorVersion::V2, "1.1.9".into());
assert_eq!(AuthenticatorVersion::V3, "1.1.10".into());
assert_eq!(AuthenticatorVersion::V3, "1.1.11".into());
assert_eq!(AuthenticatorVersion::V3, "1.1.60".into());
assert_eq!(AuthenticatorVersion::V4, "1.2.0".into());
assert_eq!(AuthenticatorVersion::V4, "1.2.1".into());
assert_eq!(AuthenticatorVersion::V4, "1.5.1".into());
assert_eq!(AuthenticatorVersion::V4, "1.6.0".into());
assert_eq!(AuthenticatorVersion::V5, "1.6.1".into());
assert_eq!(AuthenticatorVersion::V5, "1.6.11".into());
assert_eq!(AuthenticatorVersion::V5, "1.7.0".into());
assert_eq!(AuthenticatorVersion::V5, "1.16.11".into());
assert_eq!(AuthenticatorVersion::V5, "1.17.0".into());
}
}
+1
View File
@@ -7,6 +7,7 @@ license.workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
async-trait = { workspace = true }
bip39 = { workspace = true }
log = { workspace = true }
rand = { workspace = true }
+2
View File
@@ -23,10 +23,12 @@ use nym_validator_client::nym_api::EpochId;
use nym_validator_client::nyxd::contract_traits::DkgQueryClient;
pub use event::BandwidthStatusMessage;
pub use traits::{BandwidthTicketProvider, DEFAULT_TICKETS_TO_SPEND};
pub mod acquire;
pub mod error;
mod event;
mod traits;
mod utils;
#[derive(Debug)]
+42
View File
@@ -0,0 +1,42 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use async_trait::async_trait;
use nym_credential_storage::storage::Storage;
use nym_credentials_interface::TicketType;
use nym_crypto::asymmetric::ed25519;
use nym_validator_client::nyxd::contract_traits::DkgQueryClient;
use crate::{error::BandwidthControllerError, BandwidthController, PreparedCredential};
pub const DEFAULT_TICKETS_TO_SPEND: u32 = 1;
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait BandwidthTicketProvider: Send + Sync {
async fn get_ecash_ticket(
&self,
ticket_type: TicketType,
gateway_id: ed25519::PublicKey,
tickets_to_spend: u32,
) -> Result<PreparedCredential, BandwidthControllerError>;
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl<C, St> BandwidthTicketProvider for BandwidthController<C, St>
where
C: DkgQueryClient + Sync + Send,
St: nym_credential_storage::storage::Storage,
<St as Storage>::StorageError: Send + Sync + 'static,
{
async fn get_ecash_ticket(
&self,
ticket_type: TicketType,
gateway_id: ed25519::PublicKey,
tickets_to_spend: u32,
) -> Result<PreparedCredential, BandwidthControllerError> {
self.prepare_ecash_ticket(ticket_type, gateway_id.to_bytes(), tickets_to_spend)
.await
}
}
@@ -1050,7 +1050,7 @@ where
gateway_connection: GatewayConnection { gateway_ws_fd },
},
stats_reporter,
shutdown_handle: Some(shutdown_tracker), // The primary tracker for this client
shutdown_handle: shutdown_tracker, // The primary tracker for this client
client_request_sender,
forget_me: self.config.debug.forget_me,
remember_me: self.config.debug.remember_me,
@@ -1066,7 +1066,7 @@ pub struct BaseClient {
pub client_state: ClientState,
pub stats_reporter: ClientStatsSender,
pub client_request_sender: ClientRequestSender,
pub shutdown_handle: Option<ShutdownTracker>,
pub shutdown_handle: ShutdownTracker,
pub forget_me: ForgetMe,
pub remember_me: RememberMe,
}
@@ -170,7 +170,8 @@ impl MixTrafficController {
},
None => {
trace!("MixTrafficController, client request channel closed");
break}
break
}
},
}
}
@@ -136,6 +136,27 @@ pub trait DkgSigningClient {
self.execute_dkg_contract(fee, req, "trigger DKG resharing".to_string(), vec![])
.await
}
async fn transfer_ownership(
&self,
transfer_to: String,
fee: Option<Fee>,
) -> Result<ExecuteResult, NyxdError> {
let req = DkgExecuteMsg::TransferOwnership { transfer_to };
self.execute_dkg_contract(fee, req, "".to_string(), vec![])
.await
}
async fn update_announce_address(
&self,
new_address: String,
fee: Option<Fee>,
) -> Result<ExecuteResult, NyxdError> {
let req = DkgExecuteMsg::UpdateAnnounceAddress { new_address };
self.execute_dkg_contract(fee, req, "".to_string(), vec![])
.await
}
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
@@ -168,6 +189,7 @@ where
mod tests {
use super::*;
use crate::nyxd::contract_traits::tests::IgnoreValue;
use nym_coconut_dkg_common::msg::ExecuteMsg;
// it's enough that this compiles and clippy is happy about it
#[allow(dead_code)]
@@ -210,6 +232,12 @@ mod tests {
DkgExecuteMsg::AdvanceEpochState {} => client.advance_dkg_epoch_state(None).ignore(),
DkgExecuteMsg::TriggerReset {} => client.trigger_dkg_reset(None).ignore(),
DkgExecuteMsg::TriggerResharing {} => client.trigger_dkg_resharing(None).ignore(),
ExecuteMsg::TransferOwnership { transfer_to } => {
client.transfer_ownership(transfer_to, None).ignore()
}
ExecuteMsg::UpdateAnnounceAddress { new_address } => {
client.update_announce_address(new_address, None).ignore()
}
};
}
}
@@ -5,6 +5,16 @@ use crate::types::{EncodedBTEPublicKeyWithProof, NodeIndex};
use cosmwasm_schema::cw_serde;
use cosmwasm_std::Addr;
pub type BlockHeight = u64;
pub type TransactionIndex = u32;
#[cw_serde]
pub struct OwnershipTransfer {
pub node_index: NodeIndex,
pub from: Addr,
pub to: Addr,
}
#[cw_serde]
pub struct DealerDetails {
pub address: Addr,
@@ -73,6 +73,17 @@ pub enum ExecuteMsg {
TriggerReset {},
TriggerResharing {},
/// Transfers ownership of the epoch dealer to another address.
/// This assumes off-chain hand-over of keys
TransferOwnership {
transfer_to: String,
},
/// Update announce address of this signer
UpdateAnnounceAddress {
new_address: String,
},
}
#[cw_serde]
@@ -20,5 +20,7 @@ rand_chacha = { workspace = true }
rand = { workspace = true }
cw-multi-test = { workspace = true }
nym-contracts-common = { path = "../contracts-common" }
[lints]
workspace = true
@@ -3,18 +3,98 @@
use cosmwasm_std::testing::{message_info, MockApi, MockQuerier, MockStorage};
use cosmwasm_std::{
coins, Addr, BankMsg, CosmosMsg, Empty, Env, MemoryStorage, MessageInfo, Order, OwnedDeps,
Response, StdResult, Storage,
coins, Addr, BankMsg, CosmosMsg, Decimal, Empty, Env, MemoryStorage, MessageInfo, Order,
OwnedDeps, Response, StdResult, Storage,
};
use cw_storage_plus::{KeyDeserialize, Map, Prefix, PrimaryKey};
use nym_contracts_common::events::may_find_attribute;
use rand::{RngCore, SeedableRng};
use rand_chacha::ChaCha20Rng;
use serde::de::DeserializeOwned;
use serde::Serialize;
use std::fmt::Debug;
use std::str::FromStr;
pub const TEST_DENOM: &str = "unym";
pub const TEST_PREFIX: &str = "n";
pub trait FindAttribute {
fn attribute<E, S>(&self, event_type: E, attribute: &str) -> String
where
E: Into<Option<S>>,
S: Into<String>;
fn any_attribute(&self, attribute: &str) -> String {
self.attribute::<_, String>(None, attribute)
}
fn any_parsed_attribute<T>(&self, attribute: &str) -> T
where
T: FromStr,
<T as FromStr>::Err: Debug,
{
self.parsed_attribute::<_, String, T>(None, attribute)
}
fn parsed_attribute<E, S, T>(&self, event_type: E, attribute: &str) -> T
where
E: Into<Option<S>>,
S: Into<String>,
T: FromStr,
<T as FromStr>::Err: Debug;
fn decimal<E, S>(&self, event_type: E, attribute: &str) -> Decimal
where
E: Into<Option<S>>,
S: Into<String>,
{
self.parsed_attribute(event_type, attribute)
}
}
#[track_caller]
pub fn find_attribute<S: Into<String>>(
event_type: Option<S>,
attribute: &str,
response: &Response,
) -> String {
let event_type = event_type.map(Into::into);
for event in &response.events {
if let Some(typ) = &event_type {
if &event.ty != typ {
continue;
}
}
if let Some(attr) = may_find_attribute(event, attribute) {
return attr;
}
}
// this is only used in tests so panic here is fine
panic!("did not find the attribute")
}
impl FindAttribute for Response {
fn attribute<E, S>(&self, event_type: E, attribute: &str) -> String
where
E: Into<Option<S>>,
S: Into<String>,
{
find_attribute(event_type.into(), attribute, self)
}
fn parsed_attribute<E, S, T>(&self, event_type: E, attribute: &str) -> T
where
E: Into<Option<S>>,
S: Into<String>,
T: FromStr,
<T as FromStr>::Err: Debug,
{
find_attribute(event_type.into(), attribute, self)
.parse()
.unwrap()
}
}
pub fn mock_api() -> MockApi {
MockApi::default().with_prefix(TEST_PREFIX)
}
@@ -4,8 +4,8 @@
use crate::{ContractTester, TestableNymContract};
use cosmwasm_std::testing::{message_info, mock_env};
use cosmwasm_std::{
from_json, Addr, Coin, ContractInfo, Deps, DepsMut, Env, MessageInfo, Response, StdResult,
Storage, Timestamp,
from_json, Addr, BlockInfo, Coin, ContractInfo, Deps, DepsMut, Env, MessageInfo, Response,
StdResult, Storage, Timestamp,
};
use cw_multi_test::{next_block, AppResponse, Executor};
use serde::de::DeserializeOwned;
@@ -62,6 +62,8 @@ pub trait ContractOpts {
coins: &[Coin],
message: Self::ExecuteMsg,
) -> Result<Response, Self::ContractError>;
fn unchecked_contract_address<D: TestableNymContract>(&self) -> Addr;
}
impl<C> ContractOpts for ContractTester<C>
@@ -130,14 +132,47 @@ where
C::execute()(self.deps_mut(), env, info, message)
}
fn unchecked_contract_address<D: TestableNymContract>(&self) -> Addr {
self.unchecked_contract_address::<D>()
}
}
pub trait ChainOpts: ContractOpts {
fn set_contract_balance(&mut self, balance: Coin);
fn next_block(&mut self);
fn update_block<F: Fn(&mut BlockInfo)>(&mut self, action: F);
fn set_to_epoch(&mut self) {
self.set_block_time(Timestamp::from_seconds(0))
}
fn set_block_time(&mut self, time: Timestamp);
fn set_to_genesis(&mut self) {
self.update_block(|block| {
block.height = 1;
})
}
fn next_block(&mut self) {
self.update_block(next_block)
}
fn advance_day_of_blocks(&mut self) {
self.update_block(|block| {
block.time = block.time.plus_seconds(24 * 60 * 60);
block.height += 17280;
})
}
fn advance_time_by(&mut self, delta_secs: u64) {
self.update_block(|block| {
block.time = block.time.plus_seconds(delta_secs);
block.height += 1
})
}
fn set_block_time(&mut self, time: Timestamp) {
self.update_block(|b| b.time = time)
}
fn execute_msg(
&mut self,
@@ -186,12 +221,9 @@ where
)
.unwrap();
}
fn next_block(&mut self) {
self.app.update_block(next_block)
}
fn set_block_time(&mut self, time: Timestamp) {
self.app.update_block(|b| b.time = time)
fn update_block<F: Fn(&mut BlockInfo)>(&mut self, action: F) {
self.app.update_block(action)
}
fn execute_msg(
@@ -11,13 +11,14 @@ use cosmwasm_std::{
};
use cw_multi_test::Executor;
use cw_storage_plus::{Key, Path, PrimaryKey};
use rand::RngCore;
use rand_chacha::ChaCha20Rng;
use serde::de::DeserializeOwned;
use serde::Serialize;
use std::any::type_name;
use std::ops::Deref;
pub use rand::prelude::*;
pub trait StorageReader {
fn common_key(&self, key: CommonStorageKeys) -> Option<&[u8]>;
@@ -47,27 +47,11 @@ pub trait TestableNymContract {
fn query() -> QueryFn<Self::QueryMsg, Self::ContractError>;
fn migrate() -> PermissionedFn<Self::MigrateMsg, Self::ContractError>;
fn base_init_msg() -> Self::InitMsg;
// // for now we don't care about custom queriers
// fn contract_wrapper() -> ContractWrapper<
// Self::ExecuteMsg,
// Self::InitMsg,
// Self::QueryMsg,
// Self::ContractError,
// anyhow::Error,
// anyhow::Error,
// Empty,
// Empty,
// Empty,
// Self::ContractError,
// Self::ContractError,
// Self::MigrateMsg,
// Self::ContractError,
// > {
// ContractWrapper::new(Self::execute(), Self::instantiate(), Self::query())
// .with_migrate(Self::migrate())
// }
// not all instances will require default init message, some will always have to provide customised values
#[allow(clippy::unimplemented)]
fn base_init_msg() -> Self::InitMsg {
unimplemented!("attempted to instantiate contract without defining instantiate message")
}
fn dyn_contract() -> Box<dyn Contract<Empty>> {
Box::new(
@@ -92,6 +76,7 @@ pub struct ContractTesterBuilder<C> {
app: App<BankKeeper, MockApi, StorageWrapper>,
storage: StorageWrapper,
pub well_known_contracts: HashMap<&'static str, Addr>,
code_ids: HashMap<&'static str, u64>,
}
impl<C> ContractTesterBuilder<C> {
@@ -125,20 +110,33 @@ impl<C> ContractTesterBuilder<C> {
app,
storage,
well_known_contracts: Default::default(),
code_ids: Default::default(),
}
}
pub fn master_address(&self) -> Addr {
self.master_address.clone()
}
pub fn instantiate<D: TestableNymContract>(
mut self,
custom_init_msg: Option<D::InitMsg>,
) -> ContractTesterBuilder<C> {
self.instantiate_contract::<D>(custom_init_msg);
self
}
pub fn instantiate_contract<D: TestableNymContract>(
&mut self,
custom_init_msg: Option<D::InitMsg>,
) {
let code_id = self.app.store_code(D::dyn_contract());
let contract_address = self
.app
.instantiate_contract(
code_id,
self.master_address.clone(),
&custom_init_msg.unwrap_or(D::base_init_msg()),
&custom_init_msg.unwrap_or_else(|| D::base_init_msg()),
&[],
D::NAME,
Some(self.master_address.to_string()),
@@ -154,8 +152,28 @@ impl<C> ContractTesterBuilder<C> {
)
.unwrap();
self.code_ids.insert(D::NAME, code_id);
self.well_known_contracts.insert(D::NAME, contract_address);
self
}
// uses the SAME code
pub fn migrate_contract<D: TestableNymContract>(&mut self, migrate_msg: &D::MigrateMsg) {
self.app
.migrate_contract(
self.master_address.clone(),
self.unchecked_contract_address::<D>(),
migrate_msg,
self.unchecked_contract_code_id::<D>(),
)
.unwrap();
}
pub fn unchecked_contract_address<D: TestableNymContract>(&self) -> Addr {
self.well_known_contracts.get(D::NAME).unwrap().clone()
}
fn unchecked_contract_code_id<D: TestableNymContract>(&self) -> u64 {
*self.code_ids.get(D::NAME).unwrap()
}
pub fn build(self) -> ContractTester<C>
@@ -229,6 +247,10 @@ where
self.insert_common_storage_key(key, value);
self
}
pub fn unchecked_contract_address<D: TestableNymContract>(&self) -> Addr {
self.well_known_contracts.get(D::NAME).unwrap().clone()
}
}
impl<C> Storage for ContractTester<C>
@@ -0,0 +1,53 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use cosmwasm_std::{from_json, Binary, CustomQuery, QuerierWrapper, StdResult};
use serde::de::DeserializeOwned;
use serde::Serialize;
// re-expose methods from QuerierWrapper as traits so that we could more easily define extension traits
pub trait ContractQuerier {
fn query_contract<T: DeserializeOwned>(
&self,
address: impl Into<String>,
msg: &impl Serialize,
) -> StdResult<T>;
fn query_contract_storage(
&self,
address: impl Into<String>,
key: impl Into<Binary>,
) -> StdResult<Option<Vec<u8>>>;
fn query_contract_storage_value<T: DeserializeOwned>(
&self,
address: impl Into<String>,
key: impl Into<Binary>,
) -> StdResult<Option<T>> {
match self.query_contract_storage(address, key)? {
None => Ok(None),
Some(value) => Ok(Some(from_json(&value)?)),
}
}
}
impl<C> ContractQuerier for QuerierWrapper<'_, C>
where
C: CustomQuery,
{
fn query_contract<T: DeserializeOwned>(
&self,
address: impl Into<String>,
msg: &impl Serialize,
) -> StdResult<T> {
self.query_wasm_smart(address, msg)
}
fn query_contract_storage(
&self,
address: impl Into<String>,
key: impl Into<Binary>,
) -> StdResult<Option<Vec<u8>>> {
self.query_wasm_raw(address, key)
}
}
@@ -10,6 +10,7 @@ pub mod events;
pub mod signing;
pub mod types;
pub mod contract_querier;
pub mod helpers;
pub use types::*;
+1
View File
@@ -42,6 +42,7 @@ nym-ecash-contract-common = { path = "../cosmwasm-smart-contracts/ecash-contract
nym-compact-ecash = { path = "../nym_offline_compact_ecash" }
nym-validator-client = { path = "../client-libs/validator-client" }
nym-network-defaults = { path = "../network-defaults" }
nym-cache = { path = "../nym-cache" }
[dev-dependencies]
tempfile = { workspace = true }
@@ -2,12 +2,18 @@
// SPDX-License-Identifier: GPL-3.0-only
use crate::error::CredentialProxyError;
use crate::shared_state::nyxd_client::ChainClient;
use crate::storage::models::StorableEcashDeposit;
use nym_compact_ecash::WithdrawalRequest;
use nym_credentials::IssuanceTicketBook;
use nym_crypto::asymmetric::ed25519;
use nym_validator_client::nyxd::cosmwasm_client::ContractResponseData;
use nym_validator_client::nyxd::{Coin, Hash};
use rand::rngs::OsRng;
use std::fmt::Debug;
use time::OffsetDateTime;
use tokio_util::sync::CancellationToken;
use tracing::{error, info, instrument};
use zeroize::Zeroizing;
pub struct BufferedDeposit {
@@ -76,7 +82,80 @@ impl PerformedDeposits {
}
}
pub(super) fn request_sizes(total: usize, max_request_size: usize) -> impl Iterator<Item = usize> {
#[instrument(skip(client, cancellation_on_critical_failure), err(Display))]
pub async fn make_deposits_request(
client: &ChainClient,
deposit_amount: Coin,
memo: impl Into<String> + Debug,
amount: usize,
cancellation_on_critical_failure: &CancellationToken,
) -> Result<PerformedDeposits, CredentialProxyError> {
let requested_on = OffsetDateTime::now_utc();
let chain_write_permit = client.start_chain_tx().await;
let mut rng = OsRng;
let keys = (0..amount)
.map(|_| ed25519::PrivateKey::new(&mut rng))
.collect::<Vec<_>>();
info!("starting {amount} deposits");
let mut contents = Vec::new();
for key in &keys {
let public_key: ed25519::PublicKey = key.into();
contents.push((public_key.to_base58_string(), deposit_amount.clone()));
}
let execute_res = chain_write_permit
.make_deposits(memo.into(), contents)
.await?;
let tx_hash = execute_res.transaction_hash;
info!("{amount} deposits made in transaction: {tx_hash}");
let contract_data = match execute_res.to_contract_data() {
Ok(contract_data) => contract_data,
Err(err) => {
// that one is tricky. deposits technically got made, but we somehow failed to parse response,
// in this case terminate the proxy with 0 exit code so it wouldn't get automatically restarted
// because it requires some serious MANUAL intervention
error!("CRITICAL FAILURE: failed to parse out deposit information from the contract transaction. either the chain got upgraded and the schema changed or the ecash contract got changed! terminating the process. it has to be inspected manually. error was: {err}");
cancellation_on_critical_failure.cancel();
return Err(CredentialProxyError::DepositFailure);
}
};
if contract_data.len() != amount {
// another critical failure, that one should be quite impossible and thus has to be manually inspected
error!("CRITICAL FAILURE: failed to parse out all deposit information from the contract transaction. got {} responses while we sent {amount} deposits! either the chain got upgraded and the schema changed or the ecash contract got changed! terminating the process. it has to be inspected manually", contract_data.len());
cancellation_on_critical_failure.cancel();
return Err(CredentialProxyError::DepositFailure);
}
let mut deposits_data = Vec::new();
for (key, response) in keys.into_iter().zip(contract_data) {
let response_index = response.message_index;
let deposit_id = match response.parse_singleton_u32_contract_data() {
Ok(deposit_id) => deposit_id,
Err(err) => {
// another impossibility
error!("CRITICAL FAILURE: failed to parse out deposit id out of the response at index {response_index}: {err}. either the chain got upgraded and the schema changed or the ecash contract got changed! terminating the process. it has to be inspected manually");
cancellation_on_critical_failure.cancel();
return Err(CredentialProxyError::DepositFailure);
}
};
deposits_data.push(BufferedDeposit::new(deposit_id, key));
}
Ok(PerformedDeposits {
deposits_data,
tx_hash,
requested_on,
deposit_amount,
})
}
pub fn split_deposits(total: usize, max_request_size: usize) -> impl Iterator<Item = usize> {
(0..total)
.step_by(max_request_size)
.map(move |start| std::cmp::min(max_request_size, total - start))
@@ -89,13 +168,13 @@ mod tests {
#[test]
fn request_sizes_test() {
assert_eq!(
request_sizes(100, 32).collect::<Vec<_>>(),
split_deposits(100, 32).collect::<Vec<_>>(),
vec![32, 32, 32, 4]
);
assert_eq!(request_sizes(10, 32).collect::<Vec<_>>(), vec![10]);
assert_eq!(request_sizes(32, 32).collect::<Vec<_>>(), vec![32]);
assert_eq!(request_sizes(33, 32).collect::<Vec<_>>(), vec![32, 1]);
assert_eq!(request_sizes(1, 32).collect::<Vec<_>>(), vec![1]);
assert_eq!(split_deposits(10, 32).collect::<Vec<_>>(), vec![10]);
assert_eq!(split_deposits(32, 32).collect::<Vec<_>>(), vec![32]);
assert_eq!(split_deposits(33, 32).collect::<Vec<_>>(), vec![32, 1]);
assert_eq!(split_deposits(1, 32).collect::<Vec<_>>(), vec![1]);
}
}
@@ -1,27 +1,23 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use crate::deposits_buffer::helpers::request_sizes;
use crate::deposits_buffer::refill_task::RefillTask;
use crate::error::CredentialProxyError;
use crate::shared_state::nyxd_client::ChainClient;
use crate::shared_state::required_deposit_cache::RequiredDepositCache;
use crate::storage::CredentialProxyStorage;
use nym_compact_ecash::PublicKeyUser;
use nym_crypto::asymmetric::ed25519;
use nym_ecash_contract_common::deposit::DepositId;
use nym_validator_client::nyxd::cosmwasm_client::ContractResponseData;
use nym_validator_client::nyxd::Coin;
use rand::rngs::OsRng;
use std::sync::Arc;
use std::time::Duration;
use time::OffsetDateTime;
use tokio::sync::Mutex as AsyncMutex;
use tokio_util::sync::CancellationToken;
use tracing::{debug, error, info, instrument, warn};
use tracing::{debug, info, instrument, warn};
use uuid::Uuid;
pub use helpers::{BufferedDeposit, PerformedDeposits};
pub use helpers::{make_deposits_request, split_deposits, BufferedDeposit, PerformedDeposits};
pub(crate) mod helpers;
mod refill_task;
@@ -89,70 +85,20 @@ impl DepositsBuffer {
&self,
amount: usize,
) -> Result<PerformedDeposits, CredentialProxyError> {
let requested_on = OffsetDateTime::now_utc();
let chain_write_permit = self.inner.client.start_chain_tx().await;
let mut rng = OsRng;
let memo = format!(
"credential-proxy-{}: performing {amount} deposits",
self.inner.short_sha
);
let deposit_amount = self.deposit_amount().await?;
let keys = (0..amount)
.map(|_| ed25519::PrivateKey::new(&mut rng))
.collect::<Vec<_>>();
info!("starting {amount} deposits");
let mut contents = Vec::new();
for key in &keys {
let public_key: ed25519::PublicKey = key.into();
contents.push((public_key.to_base58_string(), deposit_amount.clone()));
}
let execute_res = chain_write_permit
.make_deposits(self.inner.short_sha, contents)
.await?;
let tx_hash = execute_res.transaction_hash;
info!("{amount} deposits made in transaction: {tx_hash}");
let contract_data = match execute_res.to_contract_data() {
Ok(contract_data) => contract_data,
Err(err) => {
// that one is tricky. deposits technically got made, but we somehow failed to parse response,
// in this case terminate the proxy with 0 exit code so it wouldn't get automatically restarted
// because it requires some serious MANUAL intervention
error!("CRITICAL FAILURE: failed to parse out deposit information from the contract transaction. either the chain got upgraded and the schema changed or the ecash contract got changed! terminating the process. it has to be inspected manually. error was: {err}");
self.inner.cancellation_token.cancel();
return Err(CredentialProxyError::DepositFailure);
}
};
if contract_data.len() != amount {
// another critical failure, that one should be quite impossible and thus has to be manually inspected
error!("CRITICAL FAILURE: failed to parse out all deposit information from the contract transaction. got {} responses while we sent {amount} deposits! either the chain got upgraded and the schema changed or the ecash contract got changed! terminating the process. it has to be inspected manually", contract_data.len());
self.inner.cancellation_token.cancel();
return Err(CredentialProxyError::DepositFailure);
}
let mut deposits_data = Vec::new();
for (key, response) in keys.into_iter().zip(contract_data) {
let response_index = response.message_index;
let deposit_id = match response.parse_singleton_u32_contract_data() {
Ok(deposit_id) => deposit_id,
Err(err) => {
// another impossibility
error!("CRITICAL FAILURE: failed to parse out deposit id out of the response at index {response_index}: {err}. either the chain got upgraded and the schema changed or the ecash contract got changed! terminating the process. it has to be inspected manually");
self.inner.cancellation_token.cancel();
return Err(CredentialProxyError::DepositFailure);
}
};
deposits_data.push(BufferedDeposit::new(deposit_id, key));
}
Ok(PerformedDeposits {
deposits_data,
tx_hash,
requested_on,
make_deposits_request(
&self.inner.client,
deposit_amount,
})
&memo,
amount,
&self.inner.cancellation_token,
)
.await
}
async fn insert_new_deposits(
@@ -180,7 +126,7 @@ impl DepositsBuffer {
let target = self.deposits_upper_threshold();
let to_request = target - available;
for request_chunk in request_sizes(to_request, self.inner.max_concurrent_deposits) {
for request_chunk in split_deposits(to_request, self.inner.max_concurrent_deposits) {
// note: we check for cancellation between individual requests
// as opposed to wrapping that in tokio::select! so that we would never abandon chain operations
// as we wouldn't want to lose funds
+2 -69
View File
@@ -6,17 +6,15 @@
use crate::error::CredentialProxyError;
use futures::{stream, StreamExt};
use nym_cache::CachedImmutableItems;
use nym_credentials::ecash::utils::{cred_exp_date, ecash_today, EcashTime};
use nym_validator_client::nym_api::EpochId;
use nym_validator_client::nyxd::contract_traits::dkg_query_client::Epoch;
use nym_validator_client::EcashApiClient;
use std::cmp::min;
use std::collections::HashMap;
use std::future::Future;
use std::hash::Hash;
use std::ops::Deref;
use time::{Date, OffsetDateTime};
use tokio::sync::{Mutex, RwLock, RwLockReadGuard};
use tokio::sync::Mutex;
use tracing::warn;
pub struct CachedEpoch {
@@ -57,74 +55,9 @@ impl CachedEpoch {
}
}
// a map of items that never change for given key
pub struct CachedImmutableItems<K, V> {
// I wonder if there's a more efficient structure with OnceLock or OnceCell or something
inner: RwLock<HashMap<K, V>>,
}
// an item that stays constant throughout given epoch
pub type CachedImmutableEpochItem<T> = CachedImmutableItems<EpochId, T>;
impl<K, V> Default for CachedImmutableItems<K, V> {
fn default() -> Self {
CachedImmutableItems {
inner: RwLock::new(HashMap::new()),
}
}
}
impl<K, V> Deref for CachedImmutableItems<K, V> {
type Target = RwLock<HashMap<K, V>>;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl<K, V> CachedImmutableItems<K, V>
where
K: Eq + Hash,
{
pub async fn get_or_init<F, U, E>(&self, key: K, f: F) -> Result<RwLockReadGuard<'_, V>, E>
where
F: FnOnce() -> U,
U: Future<Output = Result<V, E>>,
K: Clone,
{
// 1. see if we already have the item cached
let guard = self.inner.read().await;
if let Ok(item) = RwLockReadGuard::try_map(guard, |map| map.get(&key)) {
return Ok(item);
}
// 2. attempt to retrieve (and cache) it
let mut write_guard = self.inner.write().await;
// see if another task has already set the item whilst we were waiting for the lock
if write_guard.get(&key).is_some() {
let read_guard = write_guard.downgrade();
// SAFETY: we just checked the entry exists and we never dropped the guard
#[allow(clippy::unwrap_used)]
return Ok(RwLockReadGuard::map(read_guard, |map| {
map.get(&key).unwrap()
}));
}
let init = f().await?;
write_guard.insert(key.clone(), init);
let guard = write_guard.downgrade();
// SAFETY:
// we just inserted the entry into the map while NEVER dropping the lock (only downgraded it)
// so it MUST exist and thus the unwrap is fine
#[allow(clippy::unwrap_used)]
Ok(RwLockReadGuard::map(guard, |map| map.get(&key).unwrap()))
}
}
pub fn ensure_sane_expiration_date(expiration_date: Date) -> Result<(), CredentialProxyError> {
let today = ecash_today();
@@ -1,15 +1,37 @@
// Copyright 2025 Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use crate::nym_api_helpers::{CachedEpoch, CachedImmutableEpochItem, CachedImmutableItems};
use crate::error::CredentialProxyError;
use crate::nym_api_helpers::{
ensure_sane_expiration_date, query_all_threshold_apis, CachedEpoch, CachedImmutableEpochItem,
};
use crate::quorum_checker::QuorumState;
use crate::shared_state::nyxd_client::ChainClient;
use crate::shared_state::required_deposit_cache::RequiredDepositCache;
use nym_compact_ecash::VerificationKeyAuth;
use nym_credentials::{AggregatedCoinIndicesSignatures, AggregatedExpirationDateSignatures};
use crate::storage::traits::GlobalEcashDataCache;
use nym_cache::CachedImmutableItems;
use nym_compact_ecash::scheme::coin_indices_signatures::aggregate_annotated_indices_signatures;
use nym_compact_ecash::scheme::expiration_date_signatures::aggregate_annotated_expiration_signatures;
use nym_credentials::ecash::utils::EcashTime;
use nym_credentials::{
AggregatedCoinIndicesSignatures, AggregatedExpirationDateSignatures, EpochVerificationKey,
};
use nym_validator_client::client::NymApiClientExt;
use nym_validator_client::coconut::EcashApiError;
use nym_validator_client::nym_api::EpochId;
use nym_validator_client::nyxd::contract_traits::dkg_query_client::Epoch;
use nym_validator_client::nyxd::contract_traits::{DkgQueryClient, PagedDkgQueryClient};
use nym_validator_client::nyxd::Coin;
use nym_validator_client::EcashApiClient;
use time::Date;
use tokio::sync::RwLock;
use time::{Date, OffsetDateTime};
use tokio::sync::{RwLock, RwLockReadGuard};
use tracing::info;
pub use nym_compact_ecash::scheme::coin_indices_signatures::CoinIndexSignatureShare;
pub use nym_compact_ecash::scheme::expiration_date_signatures::ExpirationDateSignatureShare;
pub use nym_compact_ecash::VerificationKeyAuth;
pub use nym_credentials::{IssuanceTicketBook, IssuedTicketBook};
pub use nym_credentials_interface::{TicketType, TicketTypeRepr};
pub struct EcashState {
pub required_deposit_cache: RequiredDepositCache,
@@ -46,4 +68,309 @@ impl EcashState {
expiration_date_signatures: Default::default(),
}
}
pub async fn ensure_credentials_issuable(
&self,
client: &ChainClient,
) -> Result<(), CredentialProxyError> {
let epoch = self.current_epoch(client).await?;
if epoch.state.is_final() {
Ok(())
} else if let Some(final_timestamp) = epoch.final_timestamp_secs() {
// SAFETY: the timestamp values in our DKG contract should be valid timestamps,
// otherwise it means the chain is seriously misbehaving
#[allow(clippy::unwrap_used)]
let finish_dt = OffsetDateTime::from_unix_timestamp(final_timestamp as i64).unwrap();
Err(CredentialProxyError::CredentialsNotYetIssuable {
availability: finish_dt,
})
} else if epoch.state.is_waiting_initialisation() {
Err(CredentialProxyError::UninitialisedDkg)
} else {
Err(CredentialProxyError::UnknownEcashFailure)
}
}
pub async fn deposit_amount(&self, client: &ChainClient) -> Result<Coin, CredentialProxyError> {
self.required_deposit_cache.get_or_update(client).await
}
pub async fn ecash_clients(
&self,
client: &ChainClient,
epoch_id: EpochId,
) -> Result<RwLockReadGuard<'_, Vec<EcashApiClient>>, CredentialProxyError> {
self.epoch_clients
.get_or_init(epoch_id, || async {
Ok(client
.query_chain()
.await
.get_all_verification_key_shares(epoch_id)
.await?
.into_iter()
.map(TryInto::try_into)
.collect::<anyhow::Result<Vec<_>, EcashApiError>>()?)
})
.await
}
pub async fn current_epoch(&self, client: &ChainClient) -> Result<Epoch, CredentialProxyError> {
let read_guard = self.cached_epoch.read().await;
if read_guard.is_valid() {
return Ok(read_guard.current_epoch);
}
// update cache
drop(read_guard);
let mut write_guard = self.cached_epoch.write().await;
let epoch = client.query_chain().await.get_current_epoch().await?;
write_guard.update(epoch);
Ok(epoch)
}
pub async fn current_epoch_id(
&self,
client: &ChainClient,
) -> Result<EpochId, CredentialProxyError> {
let read_guard = self.cached_epoch.read().await;
if read_guard.is_valid() {
return Ok(read_guard.current_epoch.epoch_id);
}
// update cache
drop(read_guard);
let mut write_guard = self.cached_epoch.write().await;
let epoch = client.query_chain().await.get_current_epoch().await?;
write_guard.update(epoch);
Ok(epoch.epoch_id)
}
pub async fn master_verification_key<S>(
&self,
client: &ChainClient,
storage: &S,
epoch_id: Option<EpochId>,
) -> Result<RwLockReadGuard<'_, VerificationKeyAuth>, CredentialProxyError>
where
S: GlobalEcashDataCache,
{
let epoch_id = match epoch_id {
Some(id) => id,
None => self.current_epoch_id(client).await?,
};
self.master_verification_key
.get_or_init(epoch_id, || async {
// 1. check the storage
if let Some(stored) = storage.get_master_verification_key(epoch_id).await? {
return Ok(stored.key);
}
info!("attempting to establish master verification key for epoch {epoch_id}...");
// 2. perform actual aggregation
let all_apis = self.ecash_clients(client, epoch_id).await?;
let threshold = self.ecash_threshold(client, epoch_id).await?;
if all_apis.len() < threshold as usize {
return Err(CredentialProxyError::InsufficientNumberOfSigners {
threshold,
available: all_apis.len(),
});
}
let master_key = nym_credentials::aggregate_verification_keys(&all_apis)?;
let epoch = EpochVerificationKey {
epoch_id,
key: master_key,
};
// 3. save the key in the storage for when we reboot
storage.insert_master_verification_key(&epoch).await?;
Ok(epoch.key)
})
.await
}
pub async fn master_coin_index_signatures<S>(
&self,
client: &ChainClient,
storage: &S,
epoch_id: Option<EpochId>,
) -> Result<RwLockReadGuard<'_, AggregatedCoinIndicesSignatures>, CredentialProxyError>
where
S: GlobalEcashDataCache,
{
let epoch_id = match epoch_id {
Some(id) => id,
None => self.current_epoch_id(client).await?,
};
self.coin_index_signatures
.get_or_init(epoch_id, || async {
// 1. check the storage
if let Some(master_sigs) =
storage.get_master_coin_index_signatures(epoch_id).await?
{
return Ok(master_sigs);
}
info!(
"attempting to establish master coin index signatures for epoch {epoch_id}..."
);
// 2. go around APIs and attempt to aggregate the data
let master_vk = self
.master_verification_key(client, storage, Some(epoch_id))
.await?;
let all_apis = self.ecash_clients(client, epoch_id).await?;
let threshold = self.ecash_threshold(client, epoch_id).await?;
let get_partial_signatures = |api: EcashApiClient| async {
// move the api into the closure
let api = api;
let node_index = api.node_id;
let partial_vk = api.verification_key;
let partial = api
.api_client
.partial_coin_indices_signatures(Some(epoch_id))
.await?
.signatures;
Ok(CoinIndexSignatureShare {
index: node_index,
key: partial_vk,
signatures: partial,
})
};
let shares =
query_all_threshold_apis(all_apis.clone(), threshold, get_partial_signatures)
.await?;
let aggregated = aggregate_annotated_indices_signatures(
nym_credentials_interface::ecash_parameters(),
&master_vk,
&shares,
)?;
let sigs = AggregatedCoinIndicesSignatures {
epoch_id,
signatures: aggregated,
};
// 3. save the signatures in the storage for when we reboot
storage.insert_master_coin_index_signatures(&sigs).await?;
Ok(sigs)
})
.await
}
pub async fn master_expiration_date_signatures<S>(
&self,
client: &ChainClient,
storage: &S,
epoch_id: EpochId,
expiration_date: Date,
) -> Result<RwLockReadGuard<'_, AggregatedExpirationDateSignatures>, CredentialProxyError>
where
S: GlobalEcashDataCache,
{
self
.expiration_date_signatures
.get_or_init((epoch_id, expiration_date), || async {
// 1. sanity check to see if the expiration_date is not nonsense
ensure_sane_expiration_date(expiration_date)?;
// 2. check the storage
if let Some(master_sigs) = storage
.get_master_expiration_date_signatures(expiration_date, epoch_id)
.await?
{
return Ok(master_sigs);
}
info!(
"attempting to establish master expiration date signatures for {expiration_date} and epoch {epoch_id}..."
);
// 3. go around APIs and attempt to aggregate the data
let epoch_id = self.current_epoch_id(client).await?;
let master_vk = self.master_verification_key(client, storage, Some(epoch_id)).await?;
let all_apis = self.ecash_clients(client, epoch_id).await?;
let threshold = self.ecash_threshold(client, epoch_id).await?;
let get_partial_signatures = |api: EcashApiClient| async {
// move the api into the closure
let api = api;
let node_index = api.node_id;
let partial_vk = api.verification_key;
let partial = api
.api_client
.partial_expiration_date_signatures(Some(expiration_date), Some(epoch_id))
.await?
.signatures;
Ok(ExpirationDateSignatureShare {
index: node_index,
key: partial_vk,
signatures: partial,
})
};
let shares =
query_all_threshold_apis(all_apis.clone(), threshold, get_partial_signatures)
.await?;
let aggregated = aggregate_annotated_expiration_signatures(
&master_vk,
expiration_date.ecash_unix_timestamp(),
&shares,
)?;
let sigs = AggregatedExpirationDateSignatures {
epoch_id,
expiration_date,
signatures: aggregated,
};
// 4. save the signatures in the storage for when we reboot
storage
.insert_master_expiration_date_signatures(&sigs)
.await?;
Ok(sigs)
})
.await
}
pub async fn ecash_threshold(
&self,
client: &ChainClient,
epoch_id: EpochId,
) -> Result<u64, CredentialProxyError> {
self.threshold_values
.get_or_init(epoch_id, || async {
if let Some(threshold) = client
.query_chain()
.await
.get_epoch_threshold(epoch_id)
.await?
{
Ok(threshold)
} else {
Err(CredentialProxyError::UnavailableThreshold { epoch_id })
}
})
.await
.map(|t| *t)
}
}
+24 -278
View File
@@ -3,39 +3,26 @@
use crate::deposits_buffer::{BufferedDeposit, DepositsBuffer};
use crate::error::CredentialProxyError;
use crate::nym_api_helpers::{ensure_sane_expiration_date, query_all_threshold_apis};
use crate::shared_state::ecash_state::EcashState;
use crate::shared_state::nyxd_client::ChainClient;
use crate::storage::CredentialProxyStorage;
use nym_compact_ecash::scheme::coin_indices_signatures::{
aggregate_annotated_indices_signatures, CoinIndexSignatureShare,
};
use nym_compact_ecash::scheme::expiration_date_signatures::{
aggregate_annotated_expiration_signatures, ExpirationDateSignatureShare,
};
use nym_compact_ecash::{Base58, PublicKeyUser, VerificationKeyAuth};
use nym_credential_proxy_requests::api::v1::ticketbook::models::{
AggregatedCoinIndicesSignaturesResponse, AggregatedExpirationDateSignaturesResponse,
GlobalDataParams, MasterVerificationKeyResponse,
};
use nym_credentials::ecash::utils::EcashTime;
use nym_credentials::{
AggregatedCoinIndicesSignatures, AggregatedExpirationDateSignatures, EpochVerificationKey,
};
use nym_credentials::{AggregatedCoinIndicesSignatures, AggregatedExpirationDateSignatures};
use nym_ecash_contract_common::deposit::DepositId;
use nym_validator_client::client::NymApiClientExt;
use nym_validator_client::coconut::EcashApiError;
use nym_validator_client::nym_api::EpochId;
use nym_validator_client::nyxd::contract_traits::dkg_query_client::Epoch;
use nym_validator_client::nyxd::contract_traits::{DkgQueryClient, PagedDkgQueryClient};
use nym_validator_client::nyxd::Coin;
use nym_validator_client::{DirectSigningHttpRpcNyxdClient, EcashApiClient};
use nym_validator_client::EcashApiClient;
use std::sync::Arc;
use std::time::Duration;
use time::{Date, OffsetDateTime};
use tokio::sync::RwLockReadGuard;
use tokio::time::Instant;
use tracing::{debug, error, info, warn};
use tracing::{debug, error, warn};
use uuid::Uuid;
pub mod ecash_state;
@@ -69,10 +56,7 @@ impl CredentialProxyState {
}
pub async fn deposit_amount(&self) -> Result<Coin, CredentialProxyError> {
self.ecash_state()
.required_deposit_cache
.get_or_update(self.client())
.await
self.ecash_state().deposit_amount(self.client()).await
}
pub fn client(&self) -> &ChainClient {
@@ -87,29 +71,10 @@ impl CredentialProxyState {
&self.inner.ecash_state
}
pub(crate) async fn query_chain(&self) -> RwLockReadGuard<'_, DirectSigningHttpRpcNyxdClient> {
self.inner.client.query_chain().await
}
pub async fn ensure_credentials_issuable(&self) -> Result<(), CredentialProxyError> {
let epoch = self.current_epoch().await?;
if epoch.state.is_final() {
Ok(())
} else if let Some(final_timestamp) = epoch.final_timestamp_secs() {
// SAFETY: the timestamp values in our DKG contract should be valid timestamps,
// otherwise it means the chain is seriously misbehaving
#[allow(clippy::unwrap_used)]
let finish_dt = OffsetDateTime::from_unix_timestamp(final_timestamp as i64).unwrap();
Err(CredentialProxyError::CredentialsNotYetIssuable {
availability: finish_dt,
})
} else if epoch.state.is_waiting_initialisation() {
Err(CredentialProxyError::UninitialisedDkg)
} else {
Err(CredentialProxyError::UnknownEcashFailure)
}
self.ecash_state()
.ensure_credentials_issuable(self.client())
.await
}
pub async fn get_deposit(
@@ -146,33 +111,11 @@ impl CredentialProxyState {
}
pub async fn current_epoch_id(&self) -> Result<EpochId, CredentialProxyError> {
let read_guard = self.inner.ecash_state.cached_epoch.read().await;
if read_guard.is_valid() {
return Ok(read_guard.current_epoch.epoch_id);
}
// update cache
drop(read_guard);
let mut write_guard = self.inner.ecash_state.cached_epoch.write().await;
let epoch = self.query_chain().await.get_current_epoch().await?;
write_guard.update(epoch);
Ok(epoch.epoch_id)
self.ecash_state().current_epoch_id(self.client()).await
}
pub async fn current_epoch(&self) -> Result<Epoch, CredentialProxyError> {
let read_guard = self.ecash_state().cached_epoch.read().await;
if read_guard.is_valid() {
return Ok(read_guard.current_epoch);
}
// update cache
drop(read_guard);
let mut write_guard = self.ecash_state().cached_epoch.write().await;
let epoch = self.query_chain().await.get_current_epoch().await?;
write_guard.update(epoch);
Ok(epoch)
self.ecash_state().current_epoch(self.client()).await
}
pub async fn global_data(
@@ -243,53 +186,8 @@ impl CredentialProxyState {
&self,
epoch_id: Option<EpochId>,
) -> Result<RwLockReadGuard<'_, VerificationKeyAuth>, CredentialProxyError> {
let epoch_id = match epoch_id {
Some(id) => id,
None => self.current_epoch_id().await?,
};
self.inner
.ecash_state
.master_verification_key
.get_or_init(epoch_id, || async {
// 1. check the storage
if let Some(stored) = self
.inner
.storage
.get_master_verification_key(epoch_id)
.await?
{
return Ok(stored.key);
}
info!("attempting to establish master verification key for epoch {epoch_id}...");
// 2. perform actual aggregation
let all_apis = self.ecash_clients(epoch_id).await?;
let threshold = self.ecash_threshold(epoch_id).await?;
if all_apis.len() < threshold as usize {
return Err(CredentialProxyError::InsufficientNumberOfSigners {
threshold,
available: all_apis.len(),
});
}
let master_key = nym_credentials::aggregate_verification_keys(&all_apis)?;
let epoch = EpochVerificationKey {
epoch_id,
key: master_key,
};
// 3. save the key in the storage for when we reboot
self.inner
.storage
.insert_master_verification_key(&epoch)
.await?;
Ok(epoch.key)
})
self.ecash_state()
.master_verification_key(self.client(), self.storage(), epoch_id)
.await
}
@@ -297,75 +195,8 @@ impl CredentialProxyState {
&self,
epoch_id: Option<EpochId>,
) -> Result<RwLockReadGuard<'_, AggregatedCoinIndicesSignatures>, CredentialProxyError> {
let epoch_id = match epoch_id {
Some(id) => id,
None => self.current_epoch_id().await?,
};
self.inner
.ecash_state
.coin_index_signatures
.get_or_init(epoch_id, || async {
// 1. check the storage
if let Some(master_sigs) = self
.inner
.storage
.get_master_coin_index_signatures(epoch_id)
.await?
{
return Ok(master_sigs);
}
info!(
"attempting to establish master coin index signatures for epoch {epoch_id}..."
);
// 2. go around APIs and attempt to aggregate the data
let master_vk = self.master_verification_key(Some(epoch_id)).await?;
let all_apis = self.ecash_clients(epoch_id).await?;
let threshold = self.ecash_threshold(epoch_id).await?;
let get_partial_signatures = |api: EcashApiClient| async {
// move the api into the closure
let api = api;
let node_index = api.node_id;
let partial_vk = api.verification_key;
let partial = api
.api_client
.partial_coin_indices_signatures(Some(epoch_id))
.await?
.signatures;
Ok(CoinIndexSignatureShare {
index: node_index,
key: partial_vk,
signatures: partial,
})
};
let shares =
query_all_threshold_apis(all_apis.clone(), threshold, get_partial_signatures)
.await?;
let aggregated = aggregate_annotated_indices_signatures(
nym_credentials_interface::ecash_parameters(),
&master_vk,
&shares,
)?;
let sigs = AggregatedCoinIndicesSignatures {
epoch_id,
signatures: aggregated,
};
// 3. save the signatures in the storage for when we reboot
self.inner
.storage
.insert_master_coin_index_signatures(&sigs)
.await?;
Ok(sigs)
})
self.ecash_state()
.master_coin_index_signatures(self.client(), self.storage(), epoch_id)
.await
}
@@ -374,73 +205,13 @@ impl CredentialProxyState {
epoch_id: EpochId,
expiration_date: Date,
) -> Result<RwLockReadGuard<'_, AggregatedExpirationDateSignatures>, CredentialProxyError> {
self.inner.ecash_state
.expiration_date_signatures
.get_or_init((epoch_id, expiration_date), || async {
// 1. sanity check to see if the expiration_date is not nonsense
ensure_sane_expiration_date(expiration_date)?;
// 2. check the storage
if let Some(master_sigs) = self
.storage()
.get_master_expiration_date_signatures(expiration_date, epoch_id)
.await?
{
return Ok(master_sigs);
}
info!(
"attempting to establish master expiration date signatures for {expiration_date} and epoch {epoch_id}..."
);
// 3. go around APIs and attempt to aggregate the data
let epoch_id = self.current_epoch_id().await?;
let master_vk = self.master_verification_key(Some(epoch_id)).await?;
let all_apis = self.ecash_clients(epoch_id).await?;
let threshold = self.ecash_threshold(epoch_id).await?;
let get_partial_signatures = |api: EcashApiClient| async {
// move the api into the closure
let api = api;
let node_index = api.node_id;
let partial_vk = api.verification_key;
let partial = api
.api_client
.partial_expiration_date_signatures(Some(expiration_date), Some(epoch_id))
.await?
.signatures;
Ok(ExpirationDateSignatureShare {
index: node_index,
key: partial_vk,
signatures: partial,
})
};
let shares =
query_all_threshold_apis(all_apis.clone(), threshold, get_partial_signatures)
.await?;
let aggregated = aggregate_annotated_expiration_signatures(
&master_vk,
expiration_date.ecash_unix_timestamp(),
&shares,
)?;
let sigs = AggregatedExpirationDateSignatures {
epoch_id,
expiration_date,
signatures: aggregated,
};
// 4. save the signatures in the storage for when we reboot
self.inner.storage
.insert_master_expiration_date_signatures(&sigs)
.await?;
Ok(sigs)
})
self.ecash_state()
.master_expiration_date_signatures(
self.client(),
self.storage(),
epoch_id,
expiration_date,
)
.await
}
@@ -448,40 +219,15 @@ impl CredentialProxyState {
&self,
epoch_id: EpochId,
) -> Result<RwLockReadGuard<'_, Vec<EcashApiClient>>, CredentialProxyError> {
self.inner
.ecash_state
.epoch_clients
.get_or_init(epoch_id, || async {
Ok(self
.query_chain()
.await
.get_all_verification_key_shares(epoch_id)
.await?
.into_iter()
.map(TryInto::try_into)
.collect::<anyhow::Result<Vec<_>, EcashApiError>>()?)
})
self.ecash_state()
.ecash_clients(self.client(), epoch_id)
.await
}
pub async fn ecash_threshold(&self, epoch_id: EpochId) -> Result<u64, CredentialProxyError> {
self.inner
.ecash_state
.threshold_values
.get_or_init(epoch_id, || async {
if let Some(threshold) = self
.query_chain()
.await
.get_epoch_threshold(epoch_id)
.await?
{
Ok(threshold)
} else {
Err(CredentialProxyError::UnavailableThreshold { epoch_id })
}
})
self.ecash_state()
.ecash_threshold(self.client(), epoch_id)
.await
.map(|t| *t)
}
}
@@ -6,7 +6,7 @@ use crate::helpers::LockTimer;
use nym_ecash_contract_common::msg::ExecuteMsg;
use nym_validator_client::nyxd::contract_traits::NymContractsProvider;
use nym_validator_client::nyxd::cosmwasm_client::types::ExecuteResult;
use nym_validator_client::nyxd::{Coin, CosmWasmClient, NyxdClient};
use nym_validator_client::nyxd::{Coin, Config, CosmWasmClient, NyxdClient};
use nym_validator_client::{nyxd, DirectSigningHttpRpcNyxdClient};
use std::ops::Deref;
use std::sync::Arc;
@@ -29,6 +29,14 @@ impl ChainClient {
.nyxd_url
.as_str();
Self::new_with_config(client_config, nyxd_url, mnemonic)
}
pub fn new_with_config(
client_config: Config,
nyxd_url: &str,
mnemonic: bip39::Mnemonic,
) -> Result<Self, CredentialProxyError> {
let client = NyxdClient::connect_with_mnemonic(client_config, nyxd_url, mnemonic)?;
if client.ecash_contract_address().is_none() {
@@ -68,17 +76,15 @@ pub struct ChainWritePermit<'a> {
}
impl ChainWritePermit<'_> {
#[instrument(skip(self, short_sha, info), err(Display))]
#[instrument(skip(self, memo, info), err(Display))]
pub async fn make_deposits(
self,
short_sha: &'static str,
memo: String,
info: Vec<(String, Coin)>,
) -> Result<ExecuteResult, CredentialProxyError> {
let address = self.inner.address();
let starting_sequence = self.inner.get_sequence(&address).await?.sequence;
let deposits = info.len();
let ecash_contract = self
.inner
.ecash_contract_address()
@@ -95,12 +101,7 @@ impl ChainWritePermit<'_> {
let res = self
.inner
.execute_multiple(
ecash_contract,
deposit_messages,
None,
format!("cp-{short_sha}: performing {deposits} deposits"),
)
.execute_multiple(ecash_contract, deposit_messages, None, memo)
.await?;
loop {
@@ -26,6 +26,7 @@ use uuid::Uuid;
mod manager;
pub mod models;
pub(crate) mod pruner;
pub mod traits;
// TODO: proper import
type NodeId = u64;
@@ -0,0 +1,94 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use crate::error::CredentialProxyError;
use crate::storage::CredentialProxyStorage;
use nym_validator_client::nym_api::EpochId;
use time::Date;
pub use nym_credentials::ecash::bandwidth::serialiser::VersionedSerialise;
pub use nym_credentials::{
AggregatedCoinIndicesSignatures, AggregatedExpirationDateSignatures, EpochVerificationKey,
};
// we use it in our code so it's fine
#[allow(async_fn_in_trait)]
pub trait GlobalEcashDataCache {
async fn get_master_verification_key(
&self,
epoch_id: EpochId,
) -> Result<Option<EpochVerificationKey>, CredentialProxyError>;
async fn insert_master_verification_key(
&self,
key: &EpochVerificationKey,
) -> Result<(), CredentialProxyError>;
async fn get_master_coin_index_signatures(
&self,
epoch_id: EpochId,
) -> Result<Option<AggregatedCoinIndicesSignatures>, CredentialProxyError>;
async fn insert_master_coin_index_signatures(
&self,
signatures: &AggregatedCoinIndicesSignatures,
) -> Result<(), CredentialProxyError>;
async fn get_master_expiration_date_signatures(
&self,
expiration_date: Date,
epoch_id: EpochId,
) -> Result<Option<AggregatedExpirationDateSignatures>, CredentialProxyError>;
async fn insert_master_expiration_date_signatures(
&self,
signatures: &AggregatedExpirationDateSignatures,
) -> Result<(), CredentialProxyError>;
}
impl GlobalEcashDataCache for CredentialProxyStorage {
async fn get_master_verification_key(
&self,
epoch_id: EpochId,
) -> Result<Option<EpochVerificationKey>, CredentialProxyError> {
self.get_master_verification_key(epoch_id).await
}
async fn insert_master_verification_key(
&self,
key: &EpochVerificationKey,
) -> Result<(), CredentialProxyError> {
self.insert_master_verification_key(key).await
}
async fn get_master_coin_index_signatures(
&self,
epoch_id: EpochId,
) -> Result<Option<AggregatedCoinIndicesSignatures>, CredentialProxyError> {
self.get_master_coin_index_signatures(epoch_id).await
}
async fn insert_master_coin_index_signatures(
&self,
signatures: &AggregatedCoinIndicesSignatures,
) -> Result<(), CredentialProxyError> {
self.insert_master_coin_index_signatures(signatures).await
}
async fn get_master_expiration_date_signatures(
&self,
expiration_date: Date,
epoch_id: EpochId,
) -> Result<Option<AggregatedExpirationDateSignatures>, CredentialProxyError> {
self.get_master_expiration_date_signatures(expiration_date, epoch_id)
.await
}
async fn insert_master_expiration_date_signatures(
&self,
signatures: &AggregatedExpirationDateSignatures,
) -> Result<(), CredentialProxyError> {
self.insert_master_expiration_date_signatures(signatures)
.await
}
}
@@ -11,7 +11,7 @@ use crate::shared_state::CredentialProxyState;
use crate::storage::pruner::StoragePruner;
use crate::storage::CredentialProxyStorage;
use crate::webhook::ZkNymWebhook;
use nym_credentials::ecash::utils::ecash_today;
use nym_credentials::ecash::utils::ecash_default_expiration_date;
use nym_validator_client::nym_api::EpochId;
use std::future::Future;
use std::time::Duration;
@@ -100,7 +100,7 @@ impl TicketbookManager {
}
async fn build_initial_cache(&self) -> Result<(), CredentialProxyError> {
let today = ecash_today().date();
let default_expiration = ecash_default_expiration_date();
let epoch_id = self.state.current_epoch_id().await?;
let _ = self.state.deposit_amount().await?;
@@ -113,7 +113,7 @@ impl TicketbookManager {
.await?;
let _ = self
.state
.master_expiration_date_signatures(epoch_id, today)
.master_expiration_date_signatures(epoch_id, default_expiration)
.await?;
Ok(())
+8
View File
@@ -33,6 +33,14 @@ workspace = true
features = ["rt-multi-thread", "net", "signal", "fs"]
[dev-dependencies]
anyhow = { workspace = true }
nym-crypto = { path = "../crypto", features = ["asymmetric", "rand"] }
nym-test-utils = { path = "../test-utils" }
nym-credentials-interface = { path = "../credentials-interface" }
nym-compact-ecash = { path = "../nym_offline_compact_ecash" }
[build-dependencies]
anyhow = { workspace = true }
sqlx = { workspace = true, features = [
@@ -44,6 +44,7 @@ impl EcashCredentialManagerInner {
fn hack_clone_ticketbook(book: &IssuedTicketBook) -> IssuedTicketBook {
let ser = book.pack();
let data = Zeroizing::new(ser.data);
#[allow(clippy::unwrap_used)]
IssuedTicketBook::try_unpack(&data, None).unwrap()
}
@@ -79,18 +80,24 @@ impl MemoryEcachTicketbookManager {
let mut guard = self.inner.write().await;
for t in guard.ticketbooks.values_mut() {
if !t.ticketbook.expired()
&& t.ticketbook.spent_tickets() + tickets as u64
<= t.ticketbook.params_total_tickets()
&& t.ticketbook.ticketbook_type().to_string() == ticketbook_type
{
t.ticketbook
.update_spent_tickets(t.ticketbook.spent_tickets() + tickets as u64);
return Some(RetrievedTicketbook {
ticketbook_id: t.ticketbook_id,
ticketbook: hack_clone_ticketbook(&t.ticketbook),
});
if t.ticketbook.expired() {
continue;
}
if t.ticketbook.spent_tickets() + tickets as u64 > t.total_tickets as u64 {
continue;
}
if t.ticketbook.ticketbook_type().to_string() != ticketbook_type {
continue;
}
let cloned = hack_clone_ticketbook(&t.ticketbook);
t.ticketbook
.update_spent_tickets(t.ticketbook.spent_tickets() + tickets as u64);
return Some(RetrievedTicketbook {
ticketbook_id: t.ticketbook_id,
total_tickets: t.total_tickets,
ticketbook: cloned,
});
}
None
@@ -156,18 +163,25 @@ impl MemoryEcachTicketbookManager {
guard.pending.remove(&pending_id);
}
pub(crate) async fn insert_new_ticketbook(&self, ticketbook: &IssuedTicketBook) {
pub(crate) async fn insert_new_ticketbook(
&self,
ticketbook: &IssuedTicketBook,
total_tickets: u32,
used_tickets: u32,
) {
let mut guard = self.inner.write().await;
let id = guard.next_id();
// hehe, that's hacky AF, but it works as a **TEMPORARY** workaround
let ser = ticketbook.pack();
let data = Zeroizing::new(ser.data);
#[allow(clippy::unwrap_used)]
let mut nasty_clone = hack_clone_ticketbook(ticketbook);
nasty_clone.update_spent_tickets(used_tickets as u64);
guard.ticketbooks.insert(
id,
RetrievedTicketbook {
ticketbook_id: id,
ticketbook: IssuedTicketBook::try_unpack(&data, None).unwrap(),
total_tickets,
ticketbook: nasty_clone,
},
);
}
@@ -199,7 +213,7 @@ impl MemoryEcachTicketbookManager {
ticketbook_type: t.ticketbook.ticketbook_type().to_string(),
epoch_id: t.ticketbook.epoch_id() as u32,
total_tickets: t.ticketbook.spent_tickets() as u32,
used_tickets: t.ticketbook.params_total_tickets() as u32,
used_tickets: t.total_tickets,
})
.collect()
}
@@ -66,7 +66,42 @@ impl Storage for EphemeralStorage {
&self,
ticketbook: &IssuedTicketBook,
) -> Result<(), StorageError> {
self.storage_manager.insert_new_ticketbook(ticketbook).await;
self.storage_manager
.insert_new_ticketbook(
ticketbook,
ticketbook.params_total_tickets() as u32,
ticketbook.spent_tickets() as u32,
)
.await;
Ok(())
}
async fn insert_partial_issued_ticketbook(
&self,
ticketbook: &IssuedTicketBook,
allowed_start_ticket_index: u32,
allowed_final_ticket_index: u32,
) -> Result<(), Self::StorageError> {
// sanity check: start <= final && final <= params max
if allowed_start_ticket_index > allowed_final_ticket_index {
return Err(StorageError::database_inconsistency(
"start_ticket_index must be less than or equal to final_ticket_index",
));
}
if allowed_final_ticket_index > ticketbook.params_total_tickets() as u32 {
return Err(StorageError::database_inconsistency(
"final ticket index must be less than or equal to params_total_tickets()",
));
}
self.storage_manager
.insert_new_ticketbook(
ticketbook,
allowed_final_ticket_index + 1,
allowed_start_ticket_index,
)
.await;
Ok(())
}
@@ -184,3 +219,104 @@ impl Storage for EphemeralStorage {
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use nym_compact_ecash::tests::helpers::generate_expiration_date_signatures;
use nym_compact_ecash::{issue, ttp_keygen};
use nym_credentials_interface::TicketType;
use nym_crypto::asymmetric::ed25519;
use nym_ecash_time::EcashTime;
use nym_test_utils::helpers::deterministic_rng;
fn mock_ticketbook() -> anyhow::Result<IssuedTicketBook> {
let signing_keys = ttp_keygen(1, 1)?.remove(0);
let deposit_id = 42;
let identifier = "foomp";
let mut rng = deterministic_rng();
let key = ed25519::PrivateKey::new(&mut rng);
let typ = TicketType::V1MixnetEntry;
let issuance = IssuanceTicketBook::new(deposit_id, identifier, key, typ);
let expiration_date = issuance.expiration_date();
let sig_req = issuance.prepare_for_signing();
let _exp_date_sigs = generate_expiration_date_signatures(
sig_req.expiration_date.ecash_unix_timestamp(),
&[signing_keys.secret_key()],
&vec![signing_keys.verification_key()],
&signing_keys.verification_key(),
&[1],
)?;
let blind_sig = issue(
signing_keys.secret_key(),
sig_req.ecash_pub_key,
&sig_req.withdrawal_request,
expiration_date.ecash_unix_timestamp(),
issuance.ticketbook_type().encode(),
)?;
let partial_wallet =
issuance.unblind_signature(&signing_keys.verification_key(), &sig_req, blind_sig, 1)?;
let wallet = issuance.aggregate_signature_shares(
&signing_keys.verification_key(),
&vec![partial_wallet],
sig_req,
)?;
Ok(issuance.into_issued_ticketbook(wallet, 1))
}
#[tokio::test]
async fn storing_partial_ticketbook() -> anyhow::Result<()> {
let storage = EphemeralStorage::default();
let ticketbook = mock_ticketbook()?;
let typ = ticketbook.ticketbook_type();
storage
.insert_partial_issued_ticketbook(&ticketbook, 5, 5)
.await?;
let retrieved = storage
.get_next_unspent_usable_ticketbook(typ.to_string(), 1)
.await?;
assert!(retrieved.is_some());
let val = retrieved.unwrap();
assert_eq!(val.total_tickets, 6);
assert_eq!(val.ticketbook.spent_tickets(), 5);
// we only had 1 ticket
let retrieved2 = storage
.get_next_unspent_usable_ticketbook(typ.to_string(), 1)
.await?;
assert!(retrieved2.is_none());
let _another = mock_ticketbook()?;
let typ = ticketbook.ticketbook_type();
// 3 tickets (4, 5, 6)
storage
.insert_partial_issued_ticketbook(&ticketbook, 4, 6)
.await?;
assert!(storage
.get_next_unspent_usable_ticketbook(typ.to_string(), 1)
.await?
.is_some());
assert!(storage
.get_next_unspent_usable_ticketbook(typ.to_string(), 1)
.await?
.is_some());
assert!(storage
.get_next_unspent_usable_ticketbook(typ.to_string(), 1)
.await?
.is_some());
assert!(storage
.get_next_unspent_usable_ticketbook(typ.to_string(), 1)
.await?
.is_none());
Ok(())
}
}
+1
View File
@@ -7,6 +7,7 @@ use zeroize::{Zeroize, ZeroizeOnDrop};
pub struct RetrievedTicketbook {
pub ticketbook_id: i64,
pub total_tickets: u32,
pub ticketbook: IssuedTicketBook,
}
@@ -149,6 +149,44 @@ impl Storage for PersistentStorage {
Ok(())
}
async fn insert_partial_issued_ticketbook(
&self,
ticketbook: &IssuedTicketBook,
allowed_start_ticket_index: u32,
allowed_final_ticket_index: u32,
) -> Result<(), Self::StorageError> {
// sanity check: start <= final && final <= params max
if allowed_start_ticket_index > allowed_final_ticket_index {
return Err(StorageError::database_inconsistency(
"start_ticket_index must be less than or equal to final_ticket_index",
));
}
if allowed_final_ticket_index > ticketbook.params_total_tickets() as u32 {
return Err(StorageError::database_inconsistency(
"final ticket index must be less than or equal to params_total_tickets()",
));
}
let ser = ticketbook.pack();
let data = Zeroizing::new(ser.data);
let serialisation_revision = ser.revision;
self.storage_manager
.insert_new_ticketbook(
serialisation_revision,
&data,
ticketbook.expiration_date(),
&ticketbook.ticketbook_type().to_string(),
ticketbook.epoch_id() as u32,
allowed_final_ticket_index + 1,
allowed_start_ticket_index,
)
.await?;
Ok(())
}
async fn contains_issued_ticketbook(
&self,
ticketbook: &IssuedTicketBook,
@@ -235,6 +273,7 @@ impl Storage for PersistentStorage {
deserialised.update_spent_tickets(raw.used_tickets as u64);
Ok(Some(RetrievedTicketbook {
ticketbook_id: raw.id,
total_tickets: raw.total_tickets,
ticketbook: deserialised,
}))
}
+11 -2
View File
@@ -3,8 +3,6 @@
use crate::models::{BasicTicketbookInformation, RetrievedPendingTicketbook, RetrievedTicketbook};
use async_trait::async_trait;
use nym_compact_ecash::scheme::coin_indices_signatures::AnnotatedCoinIndexSignature;
use nym_compact_ecash::scheme::expiration_date_signatures::AnnotatedExpirationDateSignature;
use nym_compact_ecash::VerificationKeyAuth;
use nym_credentials::ecash::bandwidth::serialiser::keys::EpochVerificationKey;
use nym_credentials::ecash::bandwidth::serialiser::signatures::{
@@ -14,6 +12,9 @@ use nym_credentials::{IssuanceTicketBook, IssuedTicketBook};
use nym_ecash_time::Date;
use std::error::Error;
pub use nym_compact_ecash::scheme::coin_indices_signatures::AnnotatedCoinIndexSignature;
pub use nym_compact_ecash::scheme::expiration_date_signatures::AnnotatedExpirationDateSignature;
// for future reference, if you want to make a query for "how much bandwidth do we have left"
// do something along the lines of
// `SELECT total_tickets, used_tickets FROM ecash_ticketbook WHERE expiration_date >= ?`, today_date
@@ -37,6 +38,14 @@ pub trait Storage: Clone + Send + Sync {
ticketbook: &IssuedTicketBook,
) -> Result<(), Self::StorageError>;
// note that both start and final are **INCLUSIVE**
async fn insert_partial_issued_ticketbook(
&self,
ticketbook: &IssuedTicketBook,
allowed_start_ticket_index: u32,
allowed_final_ticket_index: u32,
) -> Result<(), Self::StorageError>;
async fn contains_issued_ticketbook(
&self,
ticketbook: &IssuedTicketBook,
+1 -1
View File
@@ -1,7 +1,6 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use nym_network_defaults::TicketTypeRepr;
use rand::Rng;
use serde::{Deserialize, Serialize};
use thiserror::Error;
@@ -29,6 +28,7 @@ pub use nym_compact_ecash::{
PartialWallet, PayInfo, PublicKeyUser, SecretKeyUser, VerificationKeyAuth, WithdrawalRequest,
};
pub use nym_ecash_time::{ecash_today, EcashTime};
pub use nym_network_defaults::TicketTypeRepr;
#[derive(Debug, Clone)]
pub struct CredentialSigningData {
@@ -64,6 +64,22 @@ impl IssuanceTicketBook {
expiration_date: Date,
) -> Self {
let ecash_keypair = generate_keypair_user_from_seed(identifier);
Self::new_with_keypair(
deposit_id,
ecash_keypair,
signing_key,
ticketbook_type,
expiration_date,
)
}
pub fn new_with_keypair(
deposit_id: DepositId,
ecash_keypair: KeyPairUser,
signing_key: ed25519::PrivateKey,
ticketbook_type: TicketType,
expiration_date: Date,
) -> Self {
IssuanceTicketBook {
deposit_id,
signing_key,
@@ -103,10 +103,14 @@ impl IssuedTicketBook {
self.expiration_date < ecash_today().date()
}
pub fn params_total_tickets(&self) -> u64 {
pub fn global_total_tickets() -> u64 {
nym_credentials_interface::ecash_parameters().get_total_coins()
}
pub fn params_total_tickets(&self) -> u64 {
Self::global_total_tickets()
}
pub fn spent_tickets(&self) -> u64 {
self.spent_tickets
}
@@ -17,7 +17,6 @@ pub struct VersionSerialised<T: ?Sized> {
pub data: Vec<u8>,
pub revision: u8,
// still wondering if there's any point in having the phantom in here
#[zeroize(skip)]
#[serde(skip)]
_phantom: PhantomData<T>,
+3 -1
View File
@@ -13,7 +13,9 @@ use nym_validator_client::client::EcashApiClient;
use nym_validator_client::nym_api::NymApiClientExt;
// so we wouldn't break all the existing imports
pub use nym_ecash_time::{cred_exp_date, ecash_date_offset, ecash_today, EcashTime};
pub use nym_ecash_time::{
cred_exp_date, ecash_date_offset, ecash_default_expiration_date, ecash_today, EcashTime,
};
pub fn aggregate_verification_keys(
api_clients: &[EcashApiClient],
+17
View File
@@ -0,0 +1,17 @@
[package]
name = "nym-cache"
version = "0.1.0"
authors.workspace = true
repository.workspace = true
homepage.workspace = true
documentation.workspace = true
edition.workspace = true
license.workspace = true
rust-version.workspace = true
readme.workspace = true
[dependencies]
tokio = { workspace = true, features = ["sync"] }
[lints]
workspace = true
+77
View File
@@ -0,0 +1,77 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use std::collections::HashMap;
use std::future::Future;
use std::hash::Hash;
use std::ops::Deref;
use tokio::sync::{RwLock, RwLockReadGuard};
/// A map of items that never change for given key
pub struct CachedImmutableItems<K, V> {
// I wonder if there's a more efficient structure with OnceLock or OnceCell or something
inner: RwLock<HashMap<K, V>>,
}
impl<K, V> Default for CachedImmutableItems<K, V> {
fn default() -> Self {
CachedImmutableItems {
inner: RwLock::new(HashMap::new()),
}
}
}
impl<K, V> Deref for CachedImmutableItems<K, V> {
type Target = RwLock<HashMap<K, V>>;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl<K, V> CachedImmutableItems<K, V>
where
K: Eq + Hash,
{
pub async fn get_or_init<F, U, E>(
&self,
key: K,
initialiser: F,
) -> Result<RwLockReadGuard<'_, V>, E>
where
F: FnOnce() -> U,
U: Future<Output = Result<V, E>>,
K: Clone,
{
// 1. see if we already have the item cached
let guard = self.inner.read().await;
if let Ok(item) = RwLockReadGuard::try_map(guard, |map| map.get(&key)) {
return Ok(item);
}
// 2. attempt to retrieve (and cache) it
let mut write_guard = self.inner.write().await;
// see if another task has already set the item whilst we were waiting for the lock
if write_guard.get(&key).is_some() {
let read_guard = write_guard.downgrade();
// SAFETY: we just checked the entry exists and we never dropped the guard
#[allow(clippy::unwrap_used)]
return Ok(RwLockReadGuard::map(read_guard, |map| {
map.get(&key).unwrap()
}));
}
let init = initialiser().await?;
write_guard.insert(key.clone(), init);
let guard = write_guard.downgrade();
// SAFETY:
// we just inserted the entry into the map while NEVER dropping the lock (only downgraded it)
// so it MUST exist and thus the unwrap is fine
#[allow(clippy::unwrap_used)]
Ok(RwLockReadGuard::map(guard, |map| map.get(&key).unwrap()))
}
}
+20
View File
@@ -0,0 +1,20 @@
[package]
name = "nym-registration-common"
version = "0.1.0"
authors.workspace = true
repository.workspace = true
homepage.workspace = true
documentation.workspace = true
edition.workspace = true
license.workspace = true
[lints]
workspace = true
[dependencies]
tokio-util.workspace = true
nym-authenticator-requests = { path = "../authenticator-requests" }
nym-crypto = { path = "../crypto" }
nym-ip-packet-requests = { path = "../ip-packet-requests" }
nym-sphinx = { path = "../nymsphinx" }
+35
View File
@@ -0,0 +1,35 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
use nym_authenticator_requests::AuthenticatorVersion;
use nym_crypto::asymmetric::x25519::PublicKey;
use nym_ip_packet_requests::IpPair;
use nym_sphinx::addressing::{NodeIdentity, Recipient};
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct NymNode {
pub identity: NodeIdentity,
pub ip_address: IpAddr,
pub ipr_address: Option<Recipient>,
pub authenticator_address: Option<Recipient>,
pub version: AuthenticatorVersion,
}
#[derive(Clone, Debug)]
pub struct GatewayData {
pub public_key: PublicKey,
pub endpoint: SocketAddr,
pub private_ipv4: Ipv4Addr,
pub private_ipv6: Ipv6Addr,
}
#[derive(Clone, Copy, Debug)]
pub struct AssignedAddresses {
pub entry_mixnet_gateway_ip: IpAddr,
pub exit_mixnet_gateway_ip: IpAddr,
pub mixnet_client_address: Recipient,
pub exit_mix_address: Recipient,
pub interface_addresses: IpPair,
}
@@ -42,6 +42,40 @@ impl TryFrom<u8> for ServiceProviderType {
}
}
pub trait ServiceProviderTypeExt {
fn is_network_requester(&self) -> bool;
fn is_ip_packet_router(&self) -> bool;
fn is_authenticator(&self) -> bool;
}
impl ServiceProviderTypeExt for ServiceProviderType {
fn is_network_requester(&self) -> bool {
matches!(self, Self::NetworkRequester)
}
fn is_ip_packet_router(&self) -> bool {
matches!(self, Self::IpPacketRouter)
}
fn is_authenticator(&self) -> bool {
matches!(self, Self::Authenticator)
}
}
impl ServiceProviderTypeExt for u8 {
fn is_network_requester(&self) -> bool {
ServiceProviderType::NetworkRequester as u8 == *self
}
fn is_ip_packet_router(&self) -> bool {
ServiceProviderType::IpPacketRouter as u8 == *self
}
fn is_authenticator(&self) -> bool {
ServiceProviderType::Authenticator as u8 == *self
}
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct Protocol {
pub version: u8,
+5
View File
@@ -633,6 +633,7 @@ dependencies = [
"cw4-group",
"easy-addr",
"nym-contracts-common",
"nym-contracts-common-testing",
"nym-group-contract-common",
"nym-multisig-contract-common",
]
@@ -663,6 +664,7 @@ dependencies = [
"cw4",
"easy-addr",
"nym-contracts-common",
"nym-contracts-common-testing",
"nym-group-contract-common",
"schemars",
"serde",
@@ -1098,11 +1100,13 @@ dependencies = [
"cw-multi-test",
"cw-storage-plus",
"cw2",
"cw3-flex-multisig",
"cw4",
"cw4-group",
"easy-addr",
"nym-coconut-dkg-common",
"nym-contracts-common",
"nym-contracts-common-testing",
"nym-group-contract-common",
"thiserror 2.0.12",
]
@@ -1142,6 +1146,7 @@ dependencies = [
"cosmwasm-std",
"cw-multi-test",
"cw-storage-plus",
"nym-contracts-common",
"rand",
"rand_chacha",
"serde",
+9 -1
View File
@@ -18,6 +18,8 @@ crate-type = ["cdylib", "rlib"]
[dependencies]
nym-coconut-dkg-common = { path = "../../common/cosmwasm-smart-contracts/coconut-dkg" }
nym-contracts-common = { path = "../../common/cosmwasm-smart-contracts/contracts-common" }
nym-contracts-common-testing = { path = "../../common/cosmwasm-smart-contracts/contracts-common-testing", optional = true }
nym-group-contract-common = { path = "../../common/cosmwasm-smart-contracts/group-contract", optional = true }
cosmwasm-schema = { workspace = true, optional = true }
cosmwasm-std = { workspace = true }
@@ -28,13 +30,19 @@ cw2 = { workspace = true }
cw4 = { workspace = true }
thiserror = { workspace = true }
cw3-flex-multisig = { path = "../multisig/cw3-flex-multisig", features = ["testable-cw3-contract"], optional = true }
cw4-group = { path = "../multisig/cw4-group", features = ["testable-cw4-contract"], optional = true }
[dev-dependencies]
anyhow = { workspace = true }
easy-addr = { path = "../../common/cosmwasm-smart-contracts/easy_addr" }
nym-group-contract-common = { path = "../../common/cosmwasm-smart-contracts/group-contract" }
cw-multi-test = { workspace = true }
cw4-group = { path = "../multisig/cw4-group" }
nym-group-contract-common = { path = "../../common/cosmwasm-smart-contracts/group-contract" }
[features]
schema-gen = ["nym-coconut-dkg-common/schema", "cosmwasm-schema"]
testable-dkg-contract = ["nym-contracts-common-testing", "cw3-flex-multisig/testable-cw3-contract", "nym-group-contract-common", "cw4-group/testable-cw4-contract"]
[lints]
workspace = true
@@ -280,6 +280,50 @@
}
},
"additionalProperties": false
},
{
"description": "Transfers ownership of the epoch dealer to another address. This assumes off-chain hand-over of keys",
"type": "object",
"required": [
"transfer_ownership"
],
"properties": {
"transfer_ownership": {
"type": "object",
"required": [
"transfer_to"
],
"properties": {
"transfer_to": {
"type": "string"
}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
{
"description": "Update announce address of this signer",
"type": "object",
"required": [
"update_announce_address"
],
"properties": {
"update_announce_address": {
"type": "object",
"required": [
"new_address"
],
"properties": {
"new_address": {
"type": "string"
}
},
"additionalProperties": false
}
},
"additionalProperties": false
}
],
"definitions": {
@@ -191,6 +191,50 @@
}
},
"additionalProperties": false
},
{
"description": "Transfers ownership of the epoch dealer to another address. This assumes off-chain hand-over of keys",
"type": "object",
"required": [
"transfer_ownership"
],
"properties": {
"transfer_ownership": {
"type": "object",
"required": [
"transfer_to"
],
"properties": {
"transfer_to": {
"type": "string"
}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
{
"description": "Update announce address of this signer",
"type": "object",
"required": [
"update_announce_address"
],
"properties": {
"update_announce_address": {
"type": "object",
"required": [
"new_address"
],
"properties": {
"new_address": {
"type": "string"
}
},
"additionalProperties": false
}
},
"additionalProperties": false
}
],
"definitions": {
+10 -5
View File
@@ -6,7 +6,9 @@ use crate::dealers::queries::{
query_epoch_dealers_addresses_paged, query_epoch_dealers_paged,
query_registered_dealer_details,
};
use crate::dealers::transactions::try_add_dealer;
use crate::dealers::transactions::{
try_add_dealer, try_transfer_ownership, try_update_announce_address,
};
use crate::dealings::queries::{
query_dealer_dealings_status, query_dealing_chunk, query_dealing_chunk_status,
query_dealing_metadata, query_dealing_status,
@@ -21,7 +23,6 @@ use crate::epoch_state::transactions::{
try_advance_epoch_state, try_initiate_dkg, try_trigger_reset, try_trigger_resharing,
};
use crate::error::ContractError;
use crate::queued_migrations::introduce_historical_epochs;
use crate::state::queries::query_state;
use crate::state::storage::{DKG_ADMIN, MULTISIG, STATE};
use crate::verification_key_shares::queries::{query_vk_share, query_vk_shares_paged};
@@ -127,6 +128,12 @@ pub fn execute(
ExecuteMsg::AdvanceEpochState {} => try_advance_epoch_state(deps, env),
ExecuteMsg::TriggerReset {} => try_trigger_reset(deps, env, info),
ExecuteMsg::TriggerResharing {} => try_trigger_resharing(deps, env, info),
ExecuteMsg::TransferOwnership { transfer_to } => {
try_transfer_ownership(deps, env, info, transfer_to)
}
ExecuteMsg::UpdateAnnounceAddress { new_address } => {
try_update_announce_address(deps, info, new_address)
}
}
}
@@ -248,12 +255,10 @@ pub fn query(deps: Deps<'_>, env: Env, msg: QueryMsg) -> Result<QueryResponse, C
}
#[entry_point]
pub fn migrate(deps: DepsMut<'_>, env: Env, _msg: MigrateMsg) -> Result<Response, ContractError> {
pub fn migrate(deps: DepsMut<'_>, _env: Env, _msg: MigrateMsg) -> Result<Response, ContractError> {
set_build_information!(deps.storage)?;
cw2::ensure_from_older_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
introduce_historical_epochs(deps, env)?;
Ok(Response::new())
}
@@ -5,6 +5,7 @@ use crate::error::ContractError;
use crate::Dealer;
use cosmwasm_std::{StdResult, Storage};
use cw_storage_plus::{Item, Map};
use nym_coconut_dkg_common::dealer::{BlockHeight, OwnershipTransfer, TransactionIndex};
use nym_coconut_dkg_common::types::{DealerDetails, DealerRegistrationDetails, EpochId, NodeIndex};
pub(crate) const DEALER_INDICES_PAGE_MAX_LIMIT: u32 = 80;
@@ -23,6 +24,9 @@ pub(crate) const DEALERS_INDICES: Map<Dealer, NodeIndex> = Map::new("dealer_inde
pub(crate) const EPOCH_DEALERS_MAP: Map<(EpochId, Dealer), DealerRegistrationDetails> =
Map::new("epoch_dealers");
pub const OWNERSHIP_TRANSFER_LOG: Map<(Dealer, BlockHeight, TransactionIndex), OwnershipTransfer> =
Map::new("transfer_log");
/// Attempts to retrieve a pre-assign node index associated with given dealer.
/// If one doesn't exist, a new one is assigned.
pub(crate) fn get_or_assign_index(
@@ -2,15 +2,16 @@
// SPDX-License-Identifier: Apache-2.0
use crate::dealers::storage::{
get_or_assign_index, is_dealer, save_dealer_details_if_not_a_dealer,
ensure_dealer, get_or_assign_index, is_dealer, save_dealer_details_if_not_a_dealer,
DEALERS_INDICES, EPOCH_DEALERS_MAP, OWNERSHIP_TRANSFER_LOG,
};
use crate::epoch_state::storage::{load_current_epoch, save_epoch};
use crate::epoch_state::utils::check_epoch_state;
use crate::error::ContractError;
use crate::state::storage::STATE;
use crate::Dealer;
use cosmwasm_std::{Deps, DepsMut, Env, MessageInfo, Response};
use nym_coconut_dkg_common::dealer::DealerRegistrationDetails;
use cosmwasm_std::{Deps, DepsMut, Env, Event, MessageInfo, Response};
use nym_coconut_dkg_common::dealer::{DealerRegistrationDetails, OwnershipTransfer};
use nym_coconut_dkg_common::types::{EncodedBTEPublicKeyWithProof, EpochState};
fn ensure_group_member(deps: Deps, dealer: Dealer) -> Result<(), ContractError> {
@@ -57,7 +58,8 @@ pub fn try_add_dealer(
)?;
// check if it's a resharing dealer
// SAFETY: resharing isn't allowed on 0th epoch
#[allow(clippy::expect_used)]
let is_resharing_dealer = resharing
&& is_dealer(
deps.storage,
@@ -83,6 +85,90 @@ pub fn try_add_dealer(
Ok(Response::new().add_attribute("node_index", node_index.to_string()))
}
pub fn try_transfer_ownership(
deps: DepsMut<'_>,
env: Env,
info: MessageInfo,
transfer_to: String,
) -> Result<Response, ContractError> {
let transfer_to = deps.api.addr_validate(&transfer_to)?;
let epoch = load_current_epoch(deps.storage)?;
// make sure we're not mid-exchange
check_epoch_state(deps.storage, EpochState::InProgress)?;
// make sure the requester is actually a dealer for this epoch
ensure_dealer(deps.storage, &info.sender, epoch.epoch_id)?;
// make sure the new target dealer actually belong to the group
ensure_group_member(deps.as_ref(), &transfer_to)?;
// update the index information
let current_index = DEALERS_INDICES.load(deps.storage, &info.sender)?;
DEALERS_INDICES.save(deps.storage, &transfer_to, &current_index)?;
DEALERS_INDICES.remove(deps.storage, &info.sender);
// update registration detail for every epoch the current dealer has participated in the protocol
// ideally, we'd have only updated the current epoch, but the way the contract is constructed
// forbids that otherwise we'd have introduced inconsistency
for epoch_id in 0..=epoch.epoch_id {
if let Some(details) = EPOCH_DEALERS_MAP.may_load(deps.storage, (epoch_id, &info.sender))? {
EPOCH_DEALERS_MAP.remove(deps.storage, (epoch_id, &info.sender));
EPOCH_DEALERS_MAP.save(deps.storage, (epoch_id, &transfer_to), &details)?;
}
}
let Some(transaction_info) = env.transaction else {
return Err(ContractError::ExecutedOutsideTransaction);
};
// save information about the transfer for more convenient history rebuilding
OWNERSHIP_TRANSFER_LOG.save(
deps.storage,
(&info.sender, env.block.height, transaction_info.index),
&OwnershipTransfer {
node_index: current_index,
from: info.sender.clone(),
to: transfer_to.clone(),
},
)?;
Ok(Response::new().add_event(
Event::new("dkg-ownership-transfer")
.add_attribute("from", info.sender)
.add_attribute("to", transfer_to)
.add_attribute("node_index", current_index.to_string()),
))
}
pub fn try_update_announce_address(
deps: DepsMut<'_>,
info: MessageInfo,
new_address: String,
) -> Result<Response, ContractError> {
let epoch = load_current_epoch(deps.storage)?;
// make sure we're not mid-exchange
check_epoch_state(deps.storage, EpochState::InProgress)?;
// make sure the requester is actually a dealer for this epoch
ensure_dealer(deps.storage, &info.sender, epoch.epoch_id)?;
let mut details = EPOCH_DEALERS_MAP.load(deps.storage, (epoch.epoch_id, &info.sender))?;
let old_address = details.announce_address;
details.announce_address = new_address.clone();
EPOCH_DEALERS_MAP.save(deps.storage, (epoch.epoch_id, &info.sender), &details)?;
Ok(Response::new().add_event(
Event::new("dkg-announce-address-update")
.add_attribute("dealer", info.sender)
.add_attribute("old_address", old_address)
.add_attribute("new_address", new_address),
))
}
#[cfg(test)]
pub(crate) mod tests {
use super::*;
@@ -137,3 +223,222 @@ pub(crate) mod tests {
);
}
}
#[cfg(test)]
#[cfg(feature = "testable-dkg-contract")]
mod tests_with_mock {
use super::*;
use crate::testable_dkg_contract::{init_contract_tester, DkgContractTesterExt};
use cosmwasm_std::testing::message_info;
use nym_contracts_common_testing::ContractOpts;
#[test]
fn transferring_ownership() -> anyhow::Result<()> {
let mut contract = init_contract_tester();
let group_member = contract.random_group_member();
// sanity check, pre-dkg
assert!(DEALERS_INDICES
.may_load(&contract, &group_member)?
.is_none());
assert!(EPOCH_DEALERS_MAP
.may_load(&contract, (0, &group_member))?
.is_none());
contract.run_initial_dummy_dkg();
let old_index = DEALERS_INDICES.load(&contract, &group_member)?;
let old_details = EPOCH_DEALERS_MAP.load(&contract, (0, &group_member))?;
let not_group_member = contract.addr_make("not_group_member");
let (deps, env) = contract.deps_mut_env();
assert!(try_transfer_ownership(
deps,
env,
message_info(&group_member, &[]),
not_group_member.to_string()
)
.is_err());
let new_group_member = contract.addr_make("new_group_member");
contract.add_group_member(new_group_member.clone());
let (deps, env) = contract.deps_mut_env();
assert!(try_transfer_ownership(
deps,
env.clone(),
message_info(&group_member, &[]),
new_group_member.to_string()
)
.is_ok());
// data under old key doesn't exist anymore
assert!(DEALERS_INDICES
.may_load(&contract, &group_member)?
.is_none());
assert!(EPOCH_DEALERS_MAP
.may_load(&contract, (0, &group_member))?
.is_none());
let new_index = DEALERS_INDICES.load(&contract, &new_group_member)?;
let new_details = EPOCH_DEALERS_MAP.load(&contract, (0, &new_group_member))?;
// the underlying info hasn't changed
assert_eq!(old_index, new_index);
assert_eq!(old_details, new_details);
assert_eq!(
OWNERSHIP_TRANSFER_LOG.load(
&contract,
(
&group_member,
env.block.height,
env.transaction.unwrap().index
)
)?,
OwnershipTransfer {
node_index: new_index,
from: group_member,
to: new_group_member,
}
);
Ok(())
}
#[test]
fn transferring_ownership_in_next_epochs() -> anyhow::Result<()> {
let mut contract = init_contract_tester();
let group_member = contract.random_group_member();
contract.run_initial_dummy_dkg(); // => epoch 0
contract.run_reset_dkg(); // => epoch 1
// LEAVE DKG MEMBERSHIP
contract.remove_group_member(group_member.clone());
contract.run_reset_dkg(); // => epoch 2
// COME BACK
contract.add_group_member(group_member.clone());
contract.run_reset_dkg(); // => epoch 3
let old_index = DEALERS_INDICES.load(&contract, &group_member)?;
let old_details0 = EPOCH_DEALERS_MAP.load(&contract, (0, &group_member))?;
let old_details1 = EPOCH_DEALERS_MAP.load(&contract, (1, &group_member))?;
let old_details2 = EPOCH_DEALERS_MAP.may_load(&contract, (2, &group_member))?;
assert!(old_details2.is_none());
let old_details3 = EPOCH_DEALERS_MAP.load(&contract, (3, &group_member))?;
// sanity check because we haven't changed our registration details:
assert_eq!(old_details0, old_details1);
assert_eq!(old_details1, old_details3);
let new_group_member = contract.addr_make("new_group_member");
contract.add_group_member(new_group_member.clone());
let (deps, env) = contract.deps_mut_env();
assert!(try_transfer_ownership(
deps,
env.clone(),
message_info(&group_member, &[]),
new_group_member.to_string()
)
.is_ok());
// data under old key doesn't exist anymore
assert!(DEALERS_INDICES
.may_load(&contract, &group_member)?
.is_none());
assert!(EPOCH_DEALERS_MAP
.may_load(&contract, (0, &group_member))?
.is_none());
assert!(EPOCH_DEALERS_MAP
.may_load(&contract, (1, &group_member))?
.is_none());
assert!(EPOCH_DEALERS_MAP
.may_load(&contract, (2, &group_member))?
.is_none());
assert!(EPOCH_DEALERS_MAP
.may_load(&contract, (3, &group_member))?
.is_none());
let new_index = DEALERS_INDICES.load(&contract, &new_group_member)?;
let new_details0 = EPOCH_DEALERS_MAP.load(&contract, (0, &new_group_member))?;
let new_details1 = EPOCH_DEALERS_MAP.load(&contract, (1, &new_group_member))?;
let new_details2 = EPOCH_DEALERS_MAP.may_load(&contract, (2, &new_group_member))?;
let new_details3 = EPOCH_DEALERS_MAP.load(&contract, (3, &new_group_member))?;
// the underlying info hasn't changed
assert_eq!(old_index, new_index);
assert_eq!(old_details0, new_details0);
assert_eq!(old_details1, new_details1);
assert_eq!(old_details2, new_details2);
assert_eq!(old_details3, new_details3);
assert_eq!(
OWNERSHIP_TRANSFER_LOG.load(
&contract,
(
&group_member,
env.block.height,
env.transaction.unwrap().index
)
)?,
OwnershipTransfer {
node_index: new_index,
from: group_member,
to: new_group_member,
}
);
Ok(())
}
#[test]
fn updating_announce_address() -> anyhow::Result<()> {
let mut contract = init_contract_tester();
let group_member = contract.random_group_member();
contract.run_initial_dummy_dkg(); // => epoch 0
contract.run_reset_dkg(); // => epoch 1
// LEAVE DKG MEMBERSHIP
contract.remove_group_member(group_member.clone());
contract.run_reset_dkg(); // => epoch 2
// COME BACK
contract.add_group_member(group_member.clone());
contract.run_reset_dkg(); // => epoch 3
let old_details0 = EPOCH_DEALERS_MAP.load(&contract, (0, &group_member))?;
let old_details1 = EPOCH_DEALERS_MAP.load(&contract, (1, &group_member))?;
let old_details2 = EPOCH_DEALERS_MAP.may_load(&contract, (2, &group_member))?;
assert!(old_details2.is_none());
let old_details3 = EPOCH_DEALERS_MAP.load(&contract, (3, &group_member))?;
// sanity check because we haven't changed our registration details:
assert_eq!(old_details0, old_details1);
assert_eq!(old_details1, old_details3);
let new_address = "https://new-address.com".to_string();
try_update_announce_address(
contract.deps_mut(),
message_info(&group_member, &[]),
new_address.clone(),
)?;
let new_details0 = EPOCH_DEALERS_MAP.load(&contract, (0, &group_member))?;
let new_details1 = EPOCH_DEALERS_MAP.load(&contract, (1, &group_member))?;
let new_details2 = EPOCH_DEALERS_MAP.may_load(&contract, (2, &group_member))?;
assert!(new_details2.is_none());
let new_details3 = EPOCH_DEALERS_MAP.load(&contract, (3, &group_member))?;
// old epoch data is unchanged
assert_eq!(old_details0, new_details0);
assert_eq!(old_details1, new_details1);
assert_eq!(old_details2, new_details2);
// most recent entry is updated
assert_eq!(new_details3.announce_address, new_address);
Ok(())
}
}
+3
View File
@@ -139,4 +139,7 @@ pub enum ContractError {
#[error("retrieved the maximum allowed number of cw4 members. for more the contracts have to be refactored")]
PossiblyIncompleteGroupMembersQuery,
#[error("this method has been called outside transaction context")]
ExecutedOutsideTransaction,
}
+3
View File
@@ -15,3 +15,6 @@ mod queued_migrations;
mod state;
mod support;
mod verification_key_shares;
#[cfg(feature = "testable-dkg-contract")]
pub mod testable_dkg_contract;
@@ -1,21 +1,2 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::epoch_state::storage::HISTORICAL_EPOCH;
use crate::error::ContractError;
use cosmwasm_std::{DepsMut, Env};
pub fn introduce_historical_epochs(deps: DepsMut, env: Env) -> Result<(), ContractError> {
if HISTORICAL_EPOCH.may_load(deps.storage)?.is_some() {
return Err(ContractError::FailedMigration {
comment: "this migration has already been run before".to_string(),
});
}
#[allow(deprecated)]
let current = crate::epoch_state::storage::CURRENT_EPOCH.load(deps.storage)?;
// we won't have information on intermediate states prior to now, but that's not the end of the world
HISTORICAL_EPOCH.save(deps.storage, &current, env.block.height)?;
Ok(())
}
@@ -1,6 +1,7 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use super::fixtures::TEST_MIX_DENOM;
use crate::contract::instantiate;
use crate::dealers::storage::{DEALERS_INDICES, EPOCH_DEALERS_MAP};
use crate::epoch_state::storage::load_current_epoch;
@@ -17,8 +18,6 @@ use nym_coconut_dkg_common::msg::InstantiateMsg;
use nym_coconut_dkg_common::types::{DealerDetails, EpochId};
use std::sync::Mutex;
use super::fixtures::TEST_MIX_DENOM;
pub const ADMIN_ADDRESS: &str = addr!("admin address");
pub const GROUP_CONTRACT: &str = addr!("group contract address");
pub const MULTISIG_CONTRACT: &str = addr!("multisig contract address");
@@ -74,6 +73,7 @@ pub fn add_fixture_dealer(deps: DepsMut<'_>) {
);
}
#[allow(clippy::panic)]
fn querier_handler(query: &WasmQuery) -> QuerierResult {
let bin = match query {
WasmQuery::Smart { contract_addr, msg } => {
@@ -0,0 +1,57 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::error::ContractError;
use cosmwasm_std::{Addr, QuerierWrapper};
use cw4::Cw4Contract;
pub(crate) fn group_members(
querier_wrapper: &QuerierWrapper,
contract: &Cw4Contract,
) -> Result<Vec<Addr>, ContractError> {
// we shouldn't ever have more group members than the default limit but IN CASE
// something changes down the line, do go through the pagination flow
let mut group_members = Vec::new();
// current max limit
let limit = 30;
let mut start_after = None;
loop {
let members = contract.list_members(querier_wrapper, start_after, Some(limit))?;
start_after = members.last().as_ref().map(|d| d.addr.clone());
for member in &members {
group_members.push(Addr::unchecked(&member.addr));
}
if members.len() < limit as usize {
// we have already exhausted the data
break;
}
}
Ok(group_members)
}
#[cfg(test)]
mod tests {
use crate::testable_dkg_contract::helpers::group_members;
use crate::testable_dkg_contract::init_contract_tester_with_group_members;
use cw4::Cw4Contract;
use cw4_group::testable_cw4_contract::GroupContract;
use nym_contracts_common_testing::ContractOpts;
#[test]
fn getting_group_members() -> anyhow::Result<()> {
for members in [0, 10, 100, 1000] {
let tester = init_contract_tester_with_group_members(members);
let group_contract =
Cw4Contract::new(tester.unchecked_contract_address::<GroupContract>());
let querier = tester.deps().querier;
let addresses = group_members(&querier, &group_contract)?;
assert_eq!(addresses.len(), members);
}
Ok(())
}
}
@@ -0,0 +1,404 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
// fine in test code
#![allow(clippy::unwrap_used)]
#![allow(clippy::expect_used)]
use crate::contract::{execute, instantiate, migrate, query};
use crate::error::ContractError;
use cosmwasm_std::testing::message_info;
use cosmwasm_std::Addr;
use cw4::{Cw4Contract, Member};
use nym_contracts_common_testing::{
AdminExt, ArbitraryContractStorageReader, ArbitraryContractStorageWriter, BankExt, ChainOpts,
CommonStorageKeys, ContractFn, ContractOpts, ContractTester, ContractTesterBuilder, DenomExt,
PermissionedFn, QueryFn, RandExt, SliceRandom, TEST_DENOM,
};
use crate::epoch_state::storage::load_current_epoch;
use crate::state::storage::{MULTISIG, STATE};
use crate::testable_dkg_contract::helpers::group_members;
use nym_coconut_dkg_common::dealing::{DealingChunkInfo, PartialContractDealing};
use nym_coconut_dkg_common::types::{Epoch, EpochState};
use nym_contracts_common::dealings::ContractSafeBytes;
pub use cw3_flex_multisig::testable_cw3_contract::{Duration, MultisigContract, Threshold};
pub use cw4_group::testable_cw4_contract::GroupContract;
pub use nym_coconut_dkg_common::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg};
pub use nym_contracts_common_testing::TestableNymContract;
pub(crate) mod helpers;
pub struct DkgContract;
const DEFAULT_GROUP_MEMBERS: usize = 15;
impl TestableNymContract for DkgContract {
const NAME: &'static str = "dkg-contract";
type InitMsg = InstantiateMsg;
type ExecuteMsg = ExecuteMsg;
type QueryMsg = QueryMsg;
type MigrateMsg = MigrateMsg;
type ContractError = ContractError;
fn instantiate() -> ContractFn<Self::InitMsg, Self::ContractError> {
instantiate
}
fn execute() -> ContractFn<Self::ExecuteMsg, Self::ContractError> {
execute
}
fn query() -> QueryFn<Self::QueryMsg, Self::ContractError> {
query
}
fn migrate() -> PermissionedFn<Self::MigrateMsg, Self::ContractError> {
migrate
}
fn init() -> ContractTester<Self>
where
Self: Sized,
{
init_contract_tester_with_group_members(DEFAULT_GROUP_MEMBERS)
}
}
pub fn init_contract_tester() -> ContractTester<DkgContract> {
DkgContract::init().with_common_storage_key(CommonStorageKeys::Admin, "dkg-admin")
}
pub fn prepare_contract_tester_builder_with_group_members<C>(
members: usize,
) -> ContractTesterBuilder<C>
where
C: TestableNymContract,
{
let mut builder = ContractTesterBuilder::<C>::new();
let api = builder.api();
// 1. init the CW4 group contract
let group_init_msg = cw4_group::testable_cw4_contract::InstantiateMsg {
admin: Some(builder.master_address().to_string()),
members: (0..members)
.map(|i| Member {
addr: api.addr_make(&format!("group-member-{i}")).to_string(),
weight: 1,
})
.collect(),
};
builder.instantiate_contract::<GroupContract>(Some(group_init_msg));
// we just instantiated it
let group_contract_address = builder.unchecked_contract_address::<GroupContract>();
// 2. init the CW3 multisig contract WITH DUMMY VALUES
let multisig_init_msg = cw3_flex_multisig::testable_cw3_contract::InstantiateMsg {
group_addr: group_contract_address.to_string(),
// \/ PLACEHOLDERS
coconut_bandwidth_contract_address: group_contract_address.to_string(),
coconut_dkg_contract_address: group_contract_address.to_string(),
// /\ PLACEHOLDERS
threshold: Threshold::AbsolutePercentage {
percentage: "0.67".parse().unwrap(),
},
max_voting_period: Duration::Time(3600),
executor: None,
proposal_deposit: None,
};
builder.instantiate_contract::<MultisigContract>(Some(multisig_init_msg));
// we just instantiated it
let multisig_contract_address = builder.unchecked_contract_address::<MultisigContract>();
// 3. init the DKG contract
let dkg_init_msg = InstantiateMsg {
group_addr: group_contract_address.to_string(),
multisig_addr: multisig_contract_address.to_string(),
time_configuration: None,
mix_denom: TEST_DENOM.to_string(),
key_size: 5,
};
builder.instantiate_contract::<DkgContract>(Some(dkg_init_msg));
// we just instantiated it
let dkg_contract_address = builder.unchecked_contract_address::<DkgContract>();
// 4. migrate the multisig contract to hold correct addresses
let multisig_migrate_msg = cw3_flex_multisig::testable_cw3_contract::MigrateMsg {
// \/ STILL A PLACEHOLDER (this contract does not care about interactions with the ecash contract)
coconut_bandwidth_address: dkg_contract_address.to_string(),
// /\ STILL A PLACEHOLDER
coconut_dkg_address: dkg_contract_address.to_string(),
};
builder.migrate_contract::<MultisigContract>(&multisig_migrate_msg);
builder
}
pub fn init_contract_tester_with_group_members(members: usize) -> ContractTester<DkgContract> {
prepare_contract_tester_builder_with_group_members(members)
.build()
.with_common_storage_key(CommonStorageKeys::Admin, "dkg-admin")
}
pub trait DkgContractTesterExt:
ContractOpts<ExecuteMsg = ExecuteMsg, QueryMsg = QueryMsg, ContractError = ContractError>
+ ChainOpts
+ AdminExt
+ DenomExt
+ RandExt
+ BankExt
+ ArbitraryContractStorageReader
+ ArbitraryContractStorageWriter
{
fn epoch(&self) -> Epoch {
load_current_epoch(self.storage()).unwrap()
}
fn multisig_contract(&self) -> Addr {
MULTISIG.get(self.deps()).unwrap().unwrap()
}
fn group_contract_wrapper(&self) -> Cw4Contract {
STATE.load(self.storage()).unwrap().group_addr
}
fn remove_group_member(&mut self, addr: Addr) {
// we have the same admin for all contracts
let admin = self.admin().unwrap();
self.execute_arbitrary_contract(
self.unchecked_contract_address::<GroupContract>(),
message_info(&admin, &[]),
&nym_group_contract_common::msg::ExecuteMsg::UpdateMembers {
remove: vec![addr.to_string()],
add: vec![],
},
)
.unwrap();
}
fn add_group_member(&mut self, addr: Addr) {
let querier = self.deps().querier;
let members = self
.group_contract_wrapper()
.list_members(&querier, None, None)
.unwrap();
let weight = members.first().map(|m| m.weight).unwrap_or(1);
// we have the same admin for all contracts
let admin = self.admin().unwrap();
self.execute_arbitrary_contract(
self.unchecked_contract_address::<GroupContract>(),
message_info(&admin, &[]),
&nym_group_contract_common::msg::ExecuteMsg::UpdateMembers {
remove: vec![],
add: vec![Member {
addr: addr.to_string(),
weight,
}],
},
)
.unwrap();
}
fn group_members(&self) -> Vec<Addr> {
let querier = self.deps().querier;
let group_contract = self.group_contract_wrapper();
group_members(&querier, &group_contract).unwrap()
}
fn random_group_member(&mut self) -> Addr {
let members = self.group_members();
members
.choose(&mut self.raw_rng())
.expect("no group members available")
.clone()
}
fn dummy_dkg_steps(&mut self, resharing: bool) {
let admin = self.admin().unwrap();
let group_members = self.group_members();
// 2. register dealers
for group_member in &group_members {
self.execute_msg(
group_member.clone(),
&ExecuteMsg::RegisterDealer {
bte_key_with_proof: format!("btekey-{group_member}"),
identity_key: format!("identity-{group_member}"),
announce_address: format!("announce-address-{group_member}"),
resharing,
},
)
.unwrap();
}
// PublicKeySubmission => DealingExchange
self.advance_time_by(600);
self.execute_msg(admin.clone(), &ExecuteMsg::AdvanceEpochState {})
.unwrap();
assert_eq!(
self.epoch().state,
EpochState::DealingExchange { resharing }
);
// 3. exchange dealings
for group_member in &group_members {
self.execute_msg(
group_member.clone(),
&ExecuteMsg::CommitDealingsMetadata {
dealing_index: 1,
chunks: vec![DealingChunkInfo { size: 1 }],
resharing,
},
)
.unwrap();
self.execute_msg(
group_member.clone(),
&ExecuteMsg::CommitDealingsChunk {
chunk: PartialContractDealing {
dealing_index: 1,
chunk_index: 0,
data: ContractSafeBytes(vec![0]),
},
},
)
.unwrap();
}
// DealingExchange => VerificationKeySubmission
self.advance_time_by(300);
self.execute_msg(admin.clone(), &ExecuteMsg::AdvanceEpochState {})
.unwrap();
assert_eq!(
self.epoch().state,
EpochState::VerificationKeySubmission { resharing }
);
// 4. derive keypairs
for group_member in &group_members {
self.execute_msg(
group_member.clone(),
&ExecuteMsg::CommitVerificationKeyShare {
share: format!("partial-vk-{group_member}"),
resharing,
},
)
.unwrap();
}
// VerificationKeySubmission => VerificationKeyValidation
self.execute_msg(admin.clone(), &ExecuteMsg::AdvanceEpochState {})
.unwrap();
self.advance_time_by(60);
assert_eq!(
self.epoch().state,
EpochState::VerificationKeyValidation { resharing }
);
// VerificationKeyValidation => VerificationKeyFinalization
self.execute_msg(admin.clone(), &ExecuteMsg::AdvanceEpochState {})
.unwrap();
assert_eq!(
self.epoch().state,
EpochState::VerificationKeyFinalization { resharing }
);
// 5. validate keys
for group_member in &group_members {
self.execute_msg(
self.multisig_contract(),
&ExecuteMsg::VerifyVerificationKeyShare {
owner: group_member.to_string(),
resharing,
},
)
.unwrap();
}
// VerificationKeyFinalization => InProgress
self.execute_msg(admin.clone(), &ExecuteMsg::AdvanceEpochState {})
.unwrap();
assert_eq!(self.epoch().state, EpochState::InProgress)
}
fn run_initial_dummy_dkg(&mut self) {
assert_eq!(self.epoch().state, EpochState::WaitingInitialisation);
// 1. initiate DKG
// WaitingInitialisation => PublicKeySubmission
let admin = self.admin().unwrap();
self.execute_msg(admin.clone(), &ExecuteMsg::InitiateDkg {})
.unwrap();
assert_eq!(
self.epoch().state,
EpochState::PublicKeySubmission { resharing: false }
);
self.dummy_dkg_steps(false)
}
fn run_reset_dkg(&mut self) {
// 1. reset DKG
// InProgress => PublicKeySubmission
let admin = self.admin().unwrap();
self.execute_msg(admin.clone(), &ExecuteMsg::TriggerReset {})
.unwrap();
assert_eq!(
self.epoch().state,
EpochState::PublicKeySubmission { resharing: false }
);
self.dummy_dkg_steps(false)
}
fn run_resharing_dkg(&mut self) {
assert_eq!(self.epoch().state, EpochState::InProgress);
let group_members = self.group_members();
println!(
"epoch: {} members: {}",
self.epoch().epoch_id,
group_members.len()
);
// 1. initiate DKG
// InProgress => PublicKeySubmission
let admin = self.admin().unwrap();
self.execute_msg(admin.clone(), &ExecuteMsg::TriggerResharing {})
.unwrap();
assert_eq!(
self.epoch().state,
EpochState::PublicKeySubmission { resharing: true }
);
self.dummy_dkg_steps(true)
}
}
impl DkgContractTesterExt for ContractTester<DkgContract> {}
#[cfg(test)]
mod tests {
use super::*;
use crate::dealers::storage::EPOCH_DEALERS_MAP;
#[test]
fn dummy_resharing() {
let mut contract = init_contract_tester_with_group_members(10);
contract.run_initial_dummy_dkg();
let dealer = contract.random_group_member();
let details = EPOCH_DEALERS_MAP
.may_load(contract.storage(), (0, &dealer))
.unwrap();
assert!(details.is_some());
assert_eq!(contract.epoch().epoch_id, 0);
contract.run_resharing_dkg();
assert_eq!(contract.epoch().epoch_id, 1);
}
}
+3 -80
View File
@@ -67,7 +67,7 @@ pub mod test_helpers {
use cosmwasm_std::{Env, Response, Timestamp, Uint128};
use mixnet_contract_common::error::MixnetContractError;
use mixnet_contract_common::events::{
may_find_attribute, MixnetEventType, DELEGATES_REWARD_KEY, OPERATOR_REWARD_KEY,
MixnetEventType, DELEGATES_REWARD_KEY, OPERATOR_REWARD_KEY,
};
use mixnet_contract_common::helpers::compare_decimals;
use mixnet_contract_common::mixnode::{NodeRewarding, UnbondedMixnode};
@@ -100,8 +100,8 @@ pub mod test_helpers {
use rand_chacha::ChaCha20Rng;
use serde::Serialize;
use std::collections::HashMap;
use std::fmt::Debug;
use std::str::FromStr;
pub(crate) use nym_contracts_common_testing::helpers::{find_attribute, FindAttribute};
pub(crate) fn sorted_addresses(n: usize) -> Vec<Addr> {
let mut rng = test_rng();
@@ -1592,83 +1592,6 @@ pub mod test_helpers {
None
}
#[track_caller]
pub fn find_attribute<S: Into<String>>(
event_type: Option<S>,
attribute: &str,
response: &Response,
) -> String {
let event_type = event_type.map(Into::into);
for event in &response.events {
if let Some(typ) = &event_type {
if &event.ty != typ {
continue;
}
}
if let Some(attr) = may_find_attribute(event, attribute) {
return attr;
}
}
// this is only used in tests so panic here is fine
panic!("did not find the attribute")
}
pub(crate) trait FindAttribute {
fn attribute<E, S>(&self, event_type: E, attribute: &str) -> String
where
E: Into<Option<S>>,
S: Into<String>;
fn any_attribute(&self, attribute: &str) -> String {
self.attribute::<_, String>(None, attribute)
}
fn any_parsed_attribute<T>(&self, attribute: &str) -> T
where
T: FromStr,
<T as FromStr>::Err: Debug,
{
self.parsed_attribute::<_, String, T>(None, attribute)
}
fn parsed_attribute<E, S, T>(&self, event_type: E, attribute: &str) -> T
where
E: Into<Option<S>>,
S: Into<String>,
T: FromStr,
<T as FromStr>::Err: Debug;
fn decimal<E, S>(&self, event_type: E, attribute: &str) -> Decimal
where
E: Into<Option<S>>,
S: Into<String>,
{
self.parsed_attribute(event_type, attribute)
}
}
impl FindAttribute for Response {
fn attribute<E, S>(&self, event_type: E, attribute: &str) -> String
where
E: Into<Option<S>>,
S: Into<String>,
{
find_attribute(event_type.into(), attribute, self)
}
fn parsed_attribute<E, S, T>(&self, event_type: E, attribute: &str) -> T
where
E: Into<Option<S>>,
S: Into<String>,
T: FromStr,
<T as FromStr>::Err: Debug,
{
find_attribute(event_type.into(), attribute, self)
.parse()
.unwrap()
}
}
// using floats in tests is fine
// (what it does is converting % value, like 12.34 into `Performance` (`Percent`)
// which internally is represented by decimal `0.1234`
@@ -16,10 +16,6 @@ required-features = ["cosmwasm-schema"]
[lib]
crate-type = ["cdylib", "rlib"]
[features]
# use library feature to disable all instantiate/execute/query exports
library = []
[dependencies]
cw-utils = { workspace = true }
cw2 = { workspace = true }
@@ -34,9 +30,15 @@ cosmwasm-std = { workspace = true }
nym-group-contract-common = { path = "../../../common/cosmwasm-smart-contracts/group-contract" }
nym-multisig-contract-common = { path = "../../../common/cosmwasm-smart-contracts/multisig-contract" }
nym-contracts-common = { path = "../../../common/cosmwasm-smart-contracts/contracts-common" }
nym-contracts-common-testing = { path = "../../../common/cosmwasm-smart-contracts/contracts-common-testing", optional = true }
[dev-dependencies]
easy-addr = { path = "../../../common/cosmwasm-smart-contracts/easy_addr" }
cw4-group = { path = "../cw4-group" }
cw-multi-test = { workspace = true }
cw20-base = { workspace = true }
[features]
# use library feature to disable all instantiate/execute/query exports
library = []
testable-cw3-contract = ["nym-contracts-common-testing"]
@@ -23,3 +23,6 @@ For more information on this contract, please check out the
*/
pub mod contract;
#[cfg(feature = "testable-cw3-contract")]
pub mod testable_cw3_contract;
@@ -0,0 +1,41 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::contract::{execute, instantiate, migrate, query};
use nym_contracts_common_testing::{ContractFn, PermissionedFn, QueryFn};
use nym_multisig_contract_common::error::ContractError;
pub use cw_utils::{Duration, Threshold};
pub use nym_contracts_common_testing::TestableNymContract;
pub use nym_multisig_contract_common::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg};
pub struct MultisigContract;
impl TestableNymContract for MultisigContract {
const NAME: &'static str = "cw3-flex-multisig-contract";
type InitMsg = InstantiateMsg;
type ExecuteMsg = ExecuteMsg;
type QueryMsg = QueryMsg;
type MigrateMsg = MigrateMsg;
type ContractError = ContractError;
fn instantiate() -> ContractFn<Self::InitMsg, Self::ContractError> {
instantiate
}
fn execute() -> ContractFn<Self::ExecuteMsg, Self::ContractError> {
execute
}
fn query() -> QueryFn<Self::QueryMsg, Self::ContractError> {
|deps, env, msg| query(deps, env, msg).map_err(Into::into)
}
fn migrate() -> PermissionedFn<Self::MigrateMsg, Self::ContractError> {
migrate
}
fn base_init_msg() -> Self::InitMsg {
unimplemented!()
}
}
+9 -8
View File
@@ -22,14 +22,10 @@ name = "schema"
[lib]
crate-type = ["cdylib", "rlib"]
[features]
# use library feature to disable all instantiate/execute/query exports
library = []
[dependencies]
nym-group-contract-common = { path = "../../../common/cosmwasm-smart-contracts/group-contract" }
nym-contracts-common = { path = "../../../common/cosmwasm-smart-contracts/contracts-common" }
nym-contracts-common-testing = { path = "../../../common/cosmwasm-smart-contracts/contracts-common-testing", optional = true }
cw-utils = { workspace = true }
cw2 = { workspace = true }
@@ -38,9 +34,14 @@ cw-controllers = { workspace = true }
cw-storage-plus = { workspace = true }
cosmwasm-schema = { workspace = true }
cosmwasm-std = { workspace = true }
schemars = "0.8.1"
serde = { version = "1.0.103", default-features = false, features = ["derive"] }
schemars = { workspace = true }
serde = { workspace = true, default-features = false, features = ["derive"] }
thiserror = { workspace = true }
[dev-dependencies]
easy-addr = { path = "../../../common/cosmwasm-smart-contracts/easy_addr" }
easy-addr = { path = "../../../common/cosmwasm-smart-contracts/easy_addr" }
[features]
# use library feature to disable all instantiate/execute/query exports
library = []
testable-cw4-contract = ["nym-contracts-common-testing"]
+3
View File
@@ -23,3 +23,6 @@ pub use crate::error::ContractError;
#[cfg(test)]
mod tests;
#[cfg(feature = "testable-cw4-contract")]
pub mod testable_cw4_contract;
@@ -0,0 +1,40 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::contract::{execute, instantiate, migrate, query};
use crate::error::ContractError;
use nym_contracts_common_testing::{ContractFn, PermissionedFn, QueryFn};
pub use nym_contracts_common_testing::TestableNymContract;
pub use nym_group_contract_common::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg};
pub struct GroupContract;
impl TestableNymContract for GroupContract {
const NAME: &'static str = "cw4-group-contract";
type InitMsg = InstantiateMsg;
type ExecuteMsg = ExecuteMsg;
type QueryMsg = QueryMsg;
type MigrateMsg = MigrateMsg;
type ContractError = ContractError;
fn instantiate() -> ContractFn<Self::InitMsg, Self::ContractError> {
instantiate
}
fn execute() -> ContractFn<Self::ExecuteMsg, Self::ContractError> {
execute
}
fn query() -> QueryFn<Self::QueryMsg, Self::ContractError> {
|deps, env, msg| query(deps, env, msg).map_err(Into::into)
}
fn migrate() -> PermissionedFn<Self::MigrateMsg, Self::ContractError> {
migrate
}
fn base_init_msg() -> Self::InitMsg {
unimplemented!()
}
}
@@ -1,13 +1,14 @@
Open the needed ports for `nym-node` by running these commands:
```sh
ufw allow 22/tcp # SSH - you're in control of these ports
ufw allow 80/tcp # HTTP
ufw allow 443/tcp # HTTPS
ufw allow 1789/tcp # Nym specific
ufw allow 1790/tcp # Nym specific
ufw allow 8080/tcp # Nym specific - nym-node-api
ufw allow 9000/tcp # Nym Specific - clients port
ufw allow 9001/tcp # Nym specific - wss port
ufw allow 51822/udp # WireGuard
ufw allow 22/tcp # SSH - you're in control of these ports
ufw allow 80/tcp # HTTP
ufw allow 443/tcp # HTTPS
ufw allow 1789/tcp # Nym specific - Mixnet
ufw allow 1790/tcp # Nym specific - Verloc
ufw allow 8080/tcp # Nym specific - nym-node-api
ufw allow 9000/tcp # Nym Specific - clients port
ufw allow 9001/tcp # Nym specific - wss port
ufw allow 51822/udp # WireGuard
ufw allow in on nymwg to any port 51830 proto tcp # bandwidth queries/topup - inside the tunnel
```
@@ -5,7 +5,7 @@
},
"mixmining_reserve": {
"denom": "unym",
"amount": "182883243257647"
"amount": "180875972213757"
},
"vesting_tokens": {
"denom": "unym",
@@ -13,6 +13,6 @@
},
"circulating_supply": {
"denom": "unym",
"amount": "817116756742353"
"amount": "819124027786243"
}
}
@@ -1 +1 @@
817_116_756
819_124_027
@@ -1 +1 @@
60_000_000
60_147_392
@@ -1 +1 @@
60_000_000
60_147_391
@@ -1,7 +1,7 @@
| **Item** | **Description** | **Amount in NYM** |
|:-------------------|:------------------------------------------------------|--------------------:|
| Total Supply | Maximum amount of NYM token in existence | 1_000_000_000 |
| Mixmining Reserve | Tokens releasing for operators rewards | 182_883_243 |
| Mixmining Reserve | Tokens releasing for operators rewards | 180_875_972 |
| Vesting Tokens | Tokens locked outside of cicrulation for future claim | 0 |
| Circulating Supply | Amount of unlocked tokens | 817_116_756 |
| Stake Saturation | Optimal size of node self-bond + delegation | 250_000 |
| Circulating Supply | Amount of unlocked tokens | 819_124_027 |
| Stake Saturation | Optimal size of node self-bond + delegation | 250_614 |
@@ -1,10 +1,10 @@
{
"interval": {
"reward_pool": "182883243257647.891553460395608456",
"staking_supply": "60000000000000",
"reward_pool": "180875972213757.135840914936141948",
"staking_supply": "60147391744900.170789956043539529",
"staking_supply_scale_factor": "0.07342892",
"epoch_reward_budget": "5080090090.490219209818344322",
"stake_saturation_point": "250000000000",
"epoch_reward_budget": "5024332561.493253773358748226",
"stake_saturation_point": "250614132270.417378291483514748",
"sybil_resistance": "0.3",
"active_set_work_factor": "10",
"interval_pool_emission": "0.02"
@@ -1 +1 @@
Tuesday, September 16th 2025, 11:07:26 UTC
Wednesday, October 1st 2025, 11:16:15 UTC
@@ -58,8 +58,8 @@ Options:
Specifies whether the wireguard service is enabled on this node [env: NYMNODE_WG_ENABLED=] [possible values: true, false]
--wireguard-bind-address <WIREGUARD_BIND_ADDRESS>
Socket address this node will use for binding its wireguard interface. default: `[::]:51822` [env: NYMNODE_WG_BIND_ADDRESS=]
--wireguard-announced-port <WIREGUARD_ANNOUNCED_PORT>
Port announced to external clients wishing to connect to the wireguard interface. Useful in the instances where the node is behind a proxy [env: NYMNODE_WG_ANNOUNCED_PORT=]
--wireguard-tunnel-announced-port <WIREGUARD_TUNNEL_ANNOUNCED_PORT>
Tunnel port announced to external clients wishing to connect to the wireguard interface. Useful in the instances where the node is behind a proxy [env: NYMNODE_WG_ANNOUNCED_PORT=]
--wireguard-private-network-prefix <WIREGUARD_PRIVATE_NETWORK_PREFIX>
The prefix denoting the maximum number of the clients that can be connected via Wireguard. The maximum value for IPv4 is 32 and for IPv6 is 128 [env: NYMNODE_WG_PRIVATE_NETWORK_PREFIX=]
--verloc-bind-address <VERLOC_BIND_ADDRESS>
@@ -6,6 +6,7 @@
"sandbox": "Sandbox Testnet",
"binaries": "Binaries",
"nodes": "Nodes & Validators Guides",
"tools": "Tools",
"troubleshooting": "Troubleshooting",
"tokenomics": "Tokenomics",
"faq": "FAQ",
@@ -48,6 +48,174 @@ This page displays a full list of all the changes during our release cycle from
<VarInfo />
## `v2025.17-isabirra`
- [Release Binaries](https://github.com/nymtech/nym/releases/tag/nym-binaries-v2025.17-isabirra)
- [`nym-node`](nodes/nym-node.mdx) version `1.18.0`
```sh
nym-node
Binary Name: nym-node
Build Timestamp: 2025-10-01T10:42:58.647419869Z
Build Version: 1.18.0
Commit SHA: bbea2ff9e913f49cb7bf6c7bafa9d9b158c80de5
Commit Date: 2025-10-01T12:06:07.000000000+02:00
Commit Branch: HEAD
rustc Version: 1.88.0
rustc Channel: stable
cargo Profile: release
```
### Operators Updates & Tools
<Callout type="info" emoji="️">
**With `nym-node` version `1.18.0` operators need to make `tcp/51830` reachable on the WG interface (for bandwidth queries/topup).**
This is inside the tunnel - no need to expose it publicly unless you want an external test.
**Run this command to expose it:**
```
ufw allow in on nymwg to any port 51830 proto tcp
```
</Callout>
With this platform upgrade we are releasing several tools to improve operator experience. Check them out and let us know what you think.
- **New program for `nym-node` automated installation**: [`nym-node-cli.py`](tools#nym-node-cli)
- **New node reward tracker**: [`node_rewards_tracker.py`](tools#cmd-reward-tracker)
- **New server ping testing diagnostic tool**: [`test-nodes-pings.sh`](tools#node-ping-tester)
- **New [Tools page](tools)**
- **New Gateway landing page [template](https://raw.githubusercontent.com/nymtech/nym/refs/heads/develop/scripts/nym-node-setup/landing-page.html)** and simplified [documentation for reverse proxy configuration](nodes/nym-node/configuration/proxy-configuration#html-file-customization)
- SpectreDAO Explorer has a new [Delegation Wizzard](https://explorer.nym.spectredao.net/delegate): Allowing for a smart multi-delegation with optimal node selection form Keplr Wallet
### API Changes
Please read carefully below to follow up with API removal and changes to ensure that your development is up to date.
#### Nym API Removed Routes
All of the routes removed had already been deprecated over a year ago. This is mostly due to the fact that either they were returning only data on legacy nodes (that could never be used anyway) or required some non-sense conversions. Open the dropdown below to see removed routes
<br/>
<AccordionTemplate name="Removed API routes">
### Legacy mixnodes related:
- `/v1/mixnodes`
- `/v1/mixnodes/active`
- `/v1/mixnodes/active/detailed`
- `/v1/mixnodes/described`
- `/v1/mixnodes/rewarded`
- `/v1/mixnodes/rewarded/detailed`
- `/v1/mixnodes/detailed`
- `/v1/mixnodes/blacklisted`
- `/v1/status/mixnodes/active/detailed`
- `/v1/status/mixnodes/rewarded/detailed`
- `/v1/status/mixnodes/detailed`
- `/v1/status/mixnodes/inclusion-probability`
- `/v1/status/mixnodes/inclusion-probability`
- `/v1/status/mixnode/{mix_id}/inclusion-probability`
- `/v1/status/mixnode/{mix_id}/stake-saturation`
- `/v1/status/mixnode/{mix_id}/status`
- `/v1/status/mixnode/{mix_id}/reward-estimation`
- `/v1/status/mixnode/{mix_id}/compute-reward-estimation`
- `/v1/status/mixnodes/detailed-unfiltered`
- `/v1/status/mixnode/{mix_id}/report`
- `/v1/status/mixnode/{mix_id}/avg_uptime`
### Legacy gateways related:
- `/v1/gateways`
- `/v1/gateways/described`
- `/v1/gateways/blacklisted`
- `/v1/status/gateways/detailed`
- `/v1/status/gateways/detailed-unfiltered`
- `/v1/status/gateway/{identity}/report`
- `/v1/status/gateway/{identity}/avg_uptime`
</AccordionTemplate>
#### Structs changes:
- `MixnodeUptimeHistoryResponse` no longer has `owner` field
- `GatewayUptimeHistoryResponse` no longer has `owner` field
#### New Routes Added
- `/v1/nym-nodes/stake-saturation/{node_id}` - as a better replacement for `/v1/status/mixnode/{mix_id}/stake-saturation` as this information might be potentially useful and can be applied to any nym-node, not just a legacy mixnode.
- `/v1/legacy/mixnodes` - returns a list of bonded legacy mixnodes that haven't migrated to nym-nodes
- `/v1/legacy/gateways` - returns a list of bonded legacy gateways that haven't migrated to nym-nodes
#### Node Status API
Furthermore the changes remove all scraping of legacy mixnodes from NS and the following routes are removed:
- `/v2/mixnodes/{mix_id}`
- `/v2/mixnodes`
### Features
- [Refresh mixnet contract on epoch progression](https://github.com/nymtech/nym/pull/6023): Currently when an epoch has advanced, `nym-api` might still be serving data from the previous iteration. This PR makes sure the data is refreshed as soon as possible so new role assignment would be available quickly after.
- [Explorer-v2: Replace recommended servers with automated selection](https://github.com/nymtech/nym/pull/6019): Use params to get 10 nodes, no more manual paste.
- [Credential proxy crate](https://github.com/nymtech/nym/pull/6018): This PR moves 90% of the credential proxy functionalities into a common crate instead.
- [Moving clients crate from vpn-client repo to here](https://github.com/nymtech/nym/pull/6015): As part of the registration-client work, moved `nym-authenticator-client`, `nym-ip-packet-client` and `nym-wg-gateway-client` from the vpn-client repo into this repository.
- [Cancellation migration](https://github.com/nymtech/nym/pull/6014): Migrates shutdown watchers from `TaskManager` into the new `ShutdownManager`, which relies on tokios `CancellationToken` and `TaskTracker`.
- [Use `ShutdownToken` for nym-api](https://github.com/nymtech/nym/pull/5997): Makes `nym-api` use `ShutdownToken` (tokio `CancellationToken` inside) for cancellation; groundwork for migrating clients to the same approach.
- [Delegation program stake checker and adjuster](https://github.com/nymtech/nym/pull/5980): Adds a script to generate a CSV for delegation adjustments according to Delegation Program rules, including node/stake/uptime/version and other fields.
- [Domain fronting integration](https://github.com/nymtech/nym/pull/5974): Completes migration to `nym_http_api_client::Client`, enabling domain fronting and unifying HTTP client usage across the monorepo.
- [Shared library for attempting to retrieve update mode attestation](https://github.com/nymtech/nym/pull/5954): Part of NET-341, provides a shared library to attempt retrieving update mode attestation.
- [Credential proxy deposit pool](https://github.com/nymtech/nym/pull/5945): Changes the credential proxy to hold a pool of available deposits and monitor quorum state, reducing wastage and improving reliability.
- [Nym signers monitor](https://github.com/nymtech/nym/pull/5933): Independent tool for checking the status of network signers and sending notifications if “upgrade” mode is detected.
- [Nym node autorun CLI](https://github.com/nymtech/nym/pull/5916): Adds an interactive CLI to install, set up, and configure `nym-node` as a systemd service, improving operator experience.
### Bugfix
- [Fix the registration handshake](https://github.com/nymtech/nym/pull/6062): Resolves issues in the registration handshake process.
- [Return from MixTrafficController if client request channel has closed](https://github.com/nymtech/nym/pull/6002): Ensures the MixTrafficController exits properly when the client request channel closes.
- [Use default value for the ports until api is deployed](https://github.com/nymtech/nym/pull/6007): Fixes VPN querying mainnet for the API and not finding some of the newly added values by using default port values until the API is deployed.
- [Recipient deserialisation for deserialisers missing bytes specialisation](https://github.com/nymtech/nym/pull/5991): Fixes deserialization where formats like TOML/JSON defaulted incorrectly, ignoring bytes-related optimisations.
- [Use WASM compatible time API in client](https://github.com/nymtech/nym/pull/5948): Prevents client crashes in WASM by replacing time APIs unavailable in that environment.
### Refactors & Maintenance
- [Convenience for ShutdownTracker](https://github.com/nymtech/nym/pull/6038): Adds a method to create a `ShutdownToken` from a `CancellationToken`. Exposes `ShutdownTracker` in the SDK.
- [Made http-api-client-macro doctest compile](https://github.com/nymtech/nym/pull/6037): Adjusts doctests in `http-api-client-macro` so they compile.
- [Remove legacy nodes from nym api](https://github.com/nymtech/nym/pull/6021): Removes nearly all references to legacy nodes from `nym-api` and node status API; cleans up deprecated endpoints and adjusts structs.
- [Upgraded syn to 2.0 and removed nym-execute](https://github.com/nymtech/nym/pull/5998): Updates the `syn` dependency and removes `nym-execute`.
- [Use updated version of simulate endpoint](https://github.com/nymtech/nym/pull/5988): Switches to the updated simulate endpoint.
- [Purge temp databases on build](https://github.com/nymtech/nym/pull/5984): Prevents build failures when switching to branches without DB migrations by cleaning temp databases during builds.
- [Internal hidden command to force advance nyx epoch](https://github.com/nymtech/nym/pull/5964): Adds a hidden developer command to advance the Nyx epoch manually.
- [Create an axum_test client for more integrated unit testing](https://github.com/nymtech/nym/pull/5956): Adds an `axum_test` client to support more integrated unit tests.
- [Revert "Create an axum_test client for more integrated unit testing"](https://github.com/nymtech/nym/pull/5999): Reverts PR #5956 due to OpenSSL dependency issues.
## `v2025.16-halloumi`
- **[`nym-node`](nodes/nym-node.mdx) is not part of the release, the latest stays on version `1.16.0`**
@@ -29,14 +29,13 @@ The commands in this setup need to be run with root permission. Either add a pre
## Reverse Proxy Setup
The following snippet needs be modified as described below according to the public identity that you may want to show on this public notice, i.e. your graphics and your email.
It would allow you to serve it as a landing page resembling the one proposed by [Tor](https://gitlab.torproject.org/tpo/core/tor/-/raw/HEAD/contrib/operator-tools/tor-exit-notice.html) but with all the changes needed to adhere to the Nym's operators case.
Operators running nodes facing open internet may benefit from having a landing page. This page serves as a source of useful information about Nym network and the node when they try to search node IP or hostname.
### HTML File Customization
File for html configuration are by convention located at `/var/www/<HOSTNAME>` directory and it's subdirectories. We refer to this directory as `<LANDING_PAGE_ASSETS_PATH>`.
File for html configuration are by convention located at `/var/www/<HOSTNAME>` directory and it's sub-directories. We refer to this directory as `<LANDING_PAGE_ASSETS_PATH>`.
<Steps>
@@ -47,264 +46,21 @@ mkdir -p /var/www/<HOSTNAME>
###### 2. Create html landing page
- Use your own html code (check this [markdown to html tool](https://markdowntohtml.com/)) or copy the template below to a new file called `index.html` located in `/var/www/<HOSTNAME>` directory.
- Use your own html code (check this [markdown to html tool](https://markdowntohtml.com/)) or use [this template](https://raw.githubusercontent.com/nymtech/nym/refs/heads/develop/scripts/nym-node-setup/landing-page.html)
- Copy the [template](https://raw.githubusercontent.com/nymtech/nym/refs/heads/develop/scripts/nym-node-setup/landing-page.html) a new file called `index.html` located in `/var/www/<HOSTNAME>` directory.
<AccordionTemplate name={<IndexPage/>}>
```html
<!DOCTYPE html>
<html lang="en-US">
<head>
<meta charset="UTF-8">
<title>This is a NYM Exit Gateway</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/png" href="">
<style>
:root {
font-family: Consolas, "Ubuntu Mono", Menlo, "DejaVu Sans Mono", monospace;
}
:root{
--background-color: #121726;
--text-color: #f2f2f2;
--link-color: #fb6e4e;
}
html{
background: var(--background-color);
}
body{
margin-left: auto;
margin-right: auto;
padding-left: 5vw;
padding-right: 5vw;
max-width: 1000px;
}
h1{
font-size: 55px;
text-align: center;
color: var(--title-color)
}
p{
color: var(--text-color);
}
p, a{
font-size: 20px;
}
a{
color: var(--link-color);
text-decoration: none;
}
a:hover{
filter: brightness(.8);
text-decoration: underline;
}
.links{
display: flex;
flex-wrap: wrap;
justify-content: space-evenly;
}
.links > a{
margin: 10px;
white-space: nowrap;
}
</style>
###### 3. If you used the template above - before you save and close the file, make sure to edit the email address:
</head>
<body>
<main>
<h1>This is a NYM Exit Gateway</h1>
<p style="text-align:center">
<img class="logo" src="<FIXME>">
</p>
<p>
You are most likely accessing this website because you've had some issue with
the traffic coming from this IP. This router is part of the <a
href="https://nym.com/">NYM project</a>, which is
dedicated to <a href="https://nym.com/about/mission">create</a> outstanding
privacy software that is legally compliant without sacrificing integrity or
having any backdoors.
This router IP should be generating no other traffic, unless it has been
compromised.</p>
<p>
The Nym mixnet is operated by a decentralised community of node operators
and stakers. The Nym mixnet is trustless, meaning that no parts of the system
nor its operators have access to information that might compromise the privacy
of users. Nym software enacts a strict principle of data minimisation and has
no back doors. The Nym mixnet works by encrypting packets in several layers
and relaying those through a multi-layered network called a mixnet, eventually
letting the traffic exit the Nym mixnet through an exit gateway like this one.
This design makes it very hard for a service to know which user is connecting to it,
since it can only see the IP-address of the Nym exit gateway:</p>
<p style="text-align:center;margin:40px 0">
<svg xmlns="http://www.w3.org/2000/svg" width="500" viewBox="0 0 490.28 293.73" style="width:100%;max-width:600px">
<desc>Illustration showing how a user might connect to a service through the Nym network. The user first sends their data through three daisy-chained encrypted Nym nodes that exist on three different continents. Then the last Nym node in the chain connects to the target service over the normal internet.</desc>
<defs>
<style>
.t{
fill: var(--text-color);
stroke: var(--text-color);
}
</style>
</defs>
<path fill="#6fc8b7" d="M257.89 69.4c-6.61-6.36-10.62-7.73-18.36-8.62-7.97-1.83-20.06-7.99-24.17-.67-3.29 5.85-18.2 12.3-16.87 2.08.92-7.03 11.06-13.28 17-17.37 8.69-5.99 24.97-2.87 26.1-10.28 1.04-6.86-8.33-13.22-8.55-2.3-.38 12.84-19.62 2.24-8.73-6.2 8.92-6.9 16.05-9.02 25.61-6.15 12.37 4.83 25.58-2.05 33.73-.71 12.37-2.01 24.69-5.25 37.39-3.96 13 .43 24.08-.14 37.06.63 9.8 1.58 16.5 2.87 26.37 3.6 6.6.48 17.68-.82 24.3 1.9 8.3 4.24.44 10.94-6.89 11.8-8.79 1.05-23.59-1.19-26.6 1.86-5.8 7.41 10.75 5.68 11.27 14.54.57 9.45-5.42 9.38-8.72 16-2.7 4.2.3 13.93-1.18 18.45-1.85 5.64-19.64 4.47-14.7 14.4 4.16 8.34 1.17 19.14-10.33 12.02-5.88-3.65-9.85-22.04-15.66-21.9-11.06.27-11.37 13.18-12.7 17.52-1.3 4.27-3.79 2.33-6-.63-3.54-4.76-7.75-14.22-12.01-17.32-6.12-4.46-10.75-1.17-15.55 2.83-5.63 4.69-8.78 7.82-7.46 16.5.78 9.1-12.9 15.84-14.98 24.09-2.61 10.32-2.57 22.12-8.81 31.47-4 5.98-14.03 20.12-21.27 14.97-7.5-5.34-7.22-14.6-9.56-23.08-2.5-9.02.6-17.35-2.57-26.2-2.45-6.82-6.23-14.54-13.01-13.24-6.5.92-15.08 1.38-19.23-2.97-5.65-5.93-6-10.1-6.61-18.56 1.65-6.94 5.79-12.64 10.38-18.63 3.4-4.42 17.45-10.39 25.26-7.83 10.35 3.38 17.43 10.5 28.95 8.57 3.12-.53 9.14-4.65 7.1-6.62zm-145.6 37.27c-4.96-1.27-11.57 1.13-11.8 6.94-1.48 5.59-4.82 10.62-5.8 16.32.56 6.42 4.34 12.02 8.18 16.97 3.72 3.85 8.58 7.37 9.3 13.1 1.24 5.88 1.6 11.92 2.28 17.87.34 9.37.95 19.67 7.29 27.16 4.26 3.83 8.4-2.15 6.52-6.3-.54-4.54-.6-9.11 1.01-13.27 4.2-6.7 7.32-10.57 12.44-16.64 5.6-7.16 12.74-11.75 14-20.9.56-4.26 5.72-13.86 1.7-16.72-3.14-2.3-15.83-4-18.86-6.49-2.36-1.71-3.86-9.2-9.86-12.07-4.91-3.1-10.28-6.73-16.4-5.97zm11.16-49.42c6.13-2.93 10.58-4.77 14.61-10.25 3.5-4.28 2.46-12.62-2.59-15.45-7.27-3.22-13.08 5.78-18.81 8.71-5.96 4.2-12.07-5.48-6.44-10.6 5.53-4.13.38-9.2-5.66-8.48-6.12.8-12.48-1.45-18.6-1.73-5.3-.7-10.13-1-15.45-1.37-5.37-.05-16.51-2.23-25.13.87-5.42 1.79-12.5 5.3-16.73 9.06-4.85 4.2.2 7.56 5.54 7.45 5.3-.22 16.8-5.36 20.16.98 3.68 8.13-5.82 18.29-5.2 26.69.1 6.2 3.37 11 4.74 16.98 1.62 5.94 6.17 10.45 10 15.14 4.7 5.06 13.06 6.3 19.53 8.23 7.46.14 3.34-9.23 3.01-14.11 1.77-7.15 8.49-7.82 12.68-13.5 7.14-7.72 16.41-13.4 24.34-18.62zM190.88 3.1c-4.69 0-13.33.04-18.17-.34-7.65.12-13.1-.62-19.48-1.09-3.67.39-9.09 3.34-5.28 7.04 3.8.94 7.32 4.92 7.1 9.31 1.32 4.68 1.2 11.96 6.53 13.88 4.76-.2 7.12-7.6 11.93-8.25 6.85-2.05 12.5-4.58 17.87-9.09 2.48-2.76 7.94-6.38 5.26-10.33-1.55-1.31-2.18-.64-5.76-1.13zm178.81 157.37c-2.66 10.08-5.88 24.97 9.4 15.43 7.97-5.72 12.58-2.02 17.47 1.15.5.43 2.65 9.2 7.19 8.53 5.43-2.1 11.55-5.1 14.96-11.2 2.6-4.62 3.6-12.39 2.76-13.22-3.18-3.43-6.24-11.03-7.7-15.1-.76-2.14-2.24-2.6-2.74-.4-2.82 12.85-6.04 1.22-10.12-.05-8.2-1.67-29.62 7.17-31.22 14.86z"/>
<g fill="none">
<path stroke="#cf63a6" stroke-linecap="round" stroke-width="2.76" d="M135.2 140.58c61.4-3.82 115.95-118.83 151.45-103.33"/>
<path stroke="#cf63a6" stroke-linecap="round" stroke-width="2.76" d="M74.43 46.66c38.15 8.21 64.05 42.26 60.78 93.92M286.65 37.25c-9.6 39.44-3.57 57.12-35.64 91.98"/>
<path stroke="#e4c101" stroke-dasharray="9.06,2.265" stroke-width="2.27" d="M397.92 162.52c-31.38 1.26-90.89-53.54-148.3-36.17"/>
<path stroke="#cf63a6" stroke-linecap="round" stroke-width="2.77" d="M17.6 245.88c14.35 0 14.4.05 28-.03"/>
<path stroke="#e3bf01" stroke-dasharray="9.06,2.265" stroke-width="2.27" d="M46.26 274.14c-17.52-.12-16.68.08-30.34.07"/>
</g>
<g transform="translate(120.8 -35.81)">
<circle cx="509.78" cy="68.74" r="18.12" fill="#240a3b" transform="translate(-93.3 38.03) scale(.50637)"/>
<circle cx="440.95" cy="251.87" r="18.12" fill="#240a3b" transform="translate(-93.3 38.03) scale(.50637)"/>
<circle cx="212.62" cy="272.19" r="18.12" fill="#240a3b" transform="translate(-93.3 38.03) scale(.50637)"/>
<circle cx="92.12" cy="87.56" r="18.12" fill="#240a3b" transform="translate(-93.3 38.03) scale(.50637)"/>
<circle cx="730.88" cy="315.83" r="18.12" fill="#67727b" transform="translate(-93.3 38.03) scale(.50637)"/>
<circle cx="-102.85" cy="282.18" r="9.18" fill="#240a3b"/>
<circle cx="-102.85" cy="309.94" r="9.18" fill="#67727b"/>
</g>
<g class="t">
<text xml:space="preserve" x="-24.76" y="10.37" stroke-width=".26" font-size="16.93" font-weight="700" style="line-height:1.25" transform="translate(27.79 2.5)" word-spacing="0"><tspan x="-24.76" y="10.37">The user</tspan></text>
<text xml:space="preserve" x="150.63" y="196.62" stroke-width=".26" font-size="16.93" font-weight="700" style="line-height:1.25" transform="translate(27.79 2.5)" word-spacing="0"><tspan x="150.63" y="196.62">This server</tspan></text>
<text xml:space="preserve" x="346.39" y="202.63" stroke-width=".26" font-size="16.93" font-weight="700" style="line-height:1.25" transform="translate(27.79 2.5)" word-spacing="0"><tspan x="346.39" y="202.63">Your service</tspan></text>
<text xml:space="preserve" x="34.52" y="249.07" stroke-width=".26" font-size="16.93" font-weight="700" style="line-height:1.25" transform="translate(27.79 2.5)" word-spacing="0"><tspan x="34.52" y="249.07">Nym network link</tspan></text>
<text xml:space="preserve" x="34.13" y="276.05" stroke-width=".26" font-size="16.93" font-weight="700" style="line-height:1.25" transform="translate(27.79 2.5)" word-spacing="0"><tspan x="34.13" y="276.05">Unencrypted link</tspan></text>
<path fill="none" stroke-linecap="round" stroke-width="1.67" d="M222.6 184.1c-2.6-15.27 8.95-23.6 18.43-38.86m186.75 45.61c-.68-10.17-9.4-17.68-18.08-23.49"/>
<path fill="none" stroke-linecap="round" stroke-width="1.67" d="M240.99 153.41c.35-3.41 1.19-6.17.04-8.17m-7.15 5.48c1.83-2.8 4.58-4.45 7.15-5.48"/>
<path fill="none" stroke-linecap="round" stroke-width="1.67" d="M412.43 173.21c-2.2-3.15-2.54-3.85-2.73-5.85m0 0c2.46-.65 3.85.01 6.67 1.24M61.62 40.8C48.89 36.98 36.45 27.54 36.9 18.96M61.62 40.8c.05-2.58-3.58-4.8-5.25-5.26m-2.65 6.04c1.8.54 6.8 1.31 7.9-.78"/>
<path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="2.44" d="M1.22 229.4h247.74v63.1H1.22z"/>
</g>
</svg>
</p>
<p>
<a href="https://nym.com/about/mixnet">Read more about how Nym works.</a></p>
<p>
Nym relies on a growing ecosystem of users, developers and researcher partners
aligned with the mission to make sure Nym software is running, remains usable
and solves real problems. While Nym is not designed for malicious computer
users, it is true that they can use the network for malicious ends. This
is largely because criminals and hackers have significantly better access to
privacy and anonymity than do the regular users whom they prey upon. Criminals
can and do build, sell, and trade far larger and more powerful networks than
Nym on a daily basis. Thus, in the mind of this operator, the social need for
easily accessible censorship-resistant private, anonymous communication trumps
the risk of unskilled bad actors, who are almost always more easily uncovered
by traditional police work than by extensive monitoring and surveillance anyway.</p>
<p>
In terms of applicable law, the best way to understand Nym is to consider it a
network of routers operating as common carriers, much like the Internet
backbone. However, unlike the Internet backbone routers, Nym mixnodes do not
contain identifiable routing information about the source of a packet and do
mix the user internet traffic with that of other users, making communications
private and protecting not just the user content but the metadata
(user's IP address, who the user talks to, when, where, from what device and
more) and no single Nym node can determine both the origin and destination
of a given transmission.</p>
<p>
As such, there is little the operator of this Exit Gateway can do to help you
track the connection further. This Exit Gateway maintains no logs of any of the
Nym mixnet traffic, so there is little that can be done to trace either legitimate or
illegitimate traffic (or to filter one from the other). Attempts to
seize this router will accomplish nothing.</p>
<!-- FIXME: US-Only section. Remove if you are a non-US operator -->
<!--
<p>
Furthermore, this machine also serves as a carrier of email, which means that
its contents are further protected under the ECPA. <a
href="https://www.law.cornell.edu/uscode/text/18/2707">18
USC 2707</a> explicitly allows for civil remedies ($1000/account
<i>plus</i> legal fees)
in the event of a seizure executed without good faith or probable cause (it
should be clear at this point that traffic with an originating IP address of
FIXME_DNS_NAME should not constitute probable cause to seize the
machine). Similar considerations exist for 1st amendment content on this
machine.</p>
-->
<!-- FIXME: May or may not be US-only. Some non-US tor nodes have in
fact reported DMCA harassment... -->
<!--
<p>
If you are a representative of a company who feels that this router is being
used to violate the DMCA, please be aware that this machine does not host or
contain any illegal content. Also be aware that network infrastructure
maintainers are not liable for the type of content that passes over their
equipment, in accordance with <a
href="https://www.law.cornell.edu/uscode/text/17/512">DMCA
"safe harbor" provisions</a>. In other words, you will have just as much luck
sending a takedown notice to the Internet backbone providers.
</p>
-->
<p>To decentralise and enable privacy for a broad range of services, this
Exit Gateway adopts an <a href="https://nymtech.net/.wellknown/network-requester/exit-policy.txt">Exit Policy</a>
in accordance with the <a href="https://tornull.org/">Tor Null deny list</a>
and the <a href="https://tornull.org/tor-reduced-reduced-exit-policy.php">Tor reduced policy</a>,
which are two established safeguards.
</p>
<p>
That being said, if you still have a complaint about the router, you may email the
<a href="mailto:>YOUR_EMAIL_ADDRESS>">maintainer</a>. If complaints are related
to a particular service that is being abused, the maintainer will submit that to the
NYM Operators Community in order to add it to the Exit Policy cited above.
If approved, that would prevent this router from allowing that traffic to exit through it.
That can be done only on an IP+destination port basis, however. Common P2P ports are already blocked.</p>
<p>
You also have the option of blocking this IP address and others on the Nym network if you so desire.
The Nym project provides a <a href="https://nym.com/explorer">
web service</a> to fetch a list of all IP addresses of Nym Gateway Exit nodes that allow exiting to a
specified IP:port combination. Please be considerate when using these options.</p>
</main>
</body>
</html>
```
</AccordionTemplate>
###### 3. If you used the template above - before you save and close the file, make sure to edit the text, especially the information in these points:
- Add your own favicon logo on the line:
```html
<link rel="icon" type="image/png" href="">
```
- Add your header logo on the line:
```html
<img class="logo" src="<FIXME>">
```
- By either setting the URl to the image (if you're hosting it publicly, i.e. on your web server)
```html
href="<PATH_TO_YOUR_PUBLIC_URL>"
# and
src="<PATH_TO_YOUR_PUBLIC_URL>"
```
- **or** by adding the image inline as base64 encoded image
```html
href="href="data:image/x-icon;base64,AAABAAMA....""
# and
src="href="data:image/x-icon;base64,AAABAAMA....""
```
- Add the email address you're willing to use for being contacted.
- Change the email address you're willing to use for being contacted.
```
<a href="mailto:><YOUR_EMAIL_ADDRESS>">maintainer</a>
```
- If you're running the node within the US check the sections marked as `FIXME`, add your DNS name and un-comment those.
- Additionally you can add your own favicon logo on the line:
```html
<link rel="icon" type="YOUR_FAVICON_IMAGE_PATH" href="">
```
###### 4. Save and exit
@@ -20,12 +20,12 @@ This documentation page provides a guide on how to set up and run a [NYM NODE](.
```sh
nym-node
Binary Name: nym-node
Build Timestamp: 2025-08-05T09:14:30.322593213Z
Build Version: 1.16.0
Commit SHA: 7f97f13799342f864e1b106e8cafc9f6d6c24c0f
Commit Date: 2025-07-24T11:00:58.000000000+01:00
Build Timestamp: 2025-10-01T10:42:58.647419869Z
Build Version: 1.18.0
Commit SHA: bbea2ff9e913f49cb7bf6c7bafa9d9b158c80de5
Commit Date: 2025-10-01T12:06:07.000000000+02:00
Commit Branch: HEAD
rustc Version: 1.86.0
rustc Version: 1.88.0
rustc Channel: stable
cargo Profile: release
```
@@ -87,11 +87,6 @@ ufw status
</Tabs>
</div>
- In case of reverse proxy setup add:
```sh
ufw allow 443/tcp
```
- Re-check the status of the firewall:
```sh
ufw status
@@ -0,0 +1,156 @@
import { Callout } from 'nextra/components';
import { Tabs } from 'nextra/components';
import { MyTab } from 'components/generic-tabs.tsx';
import { RunTabs } from 'components/operators/nodes/node-run-command-tabs';
import { VarInfo } from 'components/variable-info.tsx';
import { AccordionTemplate } from 'components/accordion-template.tsx';
import { Steps } from 'nextra/components';
# Tools
On this page you can find tools to [setup a node automatically](#nym-node-cli), [explorers](#explorers) and other useful dashboards and [scripts](#cmd-reward-tracker).
<VarInfo />
## Explorers
Nym Network stats can be humanly read on some of the explorers and dashboards.
- **[Nym Explorer v2](https://nym.com/explorer):** Official Nym Explorer
- **[SpectreDAO Explorer](https://explorer.nym.spectredao.net/):** By operators for operators - currently the most used Nym explorer
- **[Nymesis](https://nymesis.vercel.app/):** A slick dashboard by operator community
- **[Nym Harbourmaster](https://harbourmaster.nymtech.net/):** A dashboard showing results of Gateway probes and more (*in development*)
## Nym Node CLI
This interactive command-line-based tool takes an operator through a journey of installing, configuring and starting a `nym-node` as a systemd service, doing most of the steps automatically for them.
**Installation & Running**
<Steps>
###### 1. SSH into your server (VPS)
- The installation of `nym-node` using this program requires you to run as `root`
###### 2. Download `nym-node-cli.py` and make executable
```sh
wget https://raw.githubusercontent.com/nymtech/nym/refs/heads/develop/scripts/nym-node-setup/nym-node-cli.py && \
chmod +x ./nym-node-cli.py
```
###### 3. Run the program
```
./nym-node-cli.py
```
###### 4. Read and follow the prompts
</ Steps>
## CMD Reward Tracker
A command-line-based program locally calculating nodes rewards based on provided Nyx account addresses in `data/wallet-addresses.csv`.
**Installation & Running**
<Steps>
###### 1. Pull / clone `nymtech/nym` repository
- Open terminal and navigate to where you want to have `nym` repostiry and run:
```sh
git clone https://github.com/nymtech/nym
```
###### 2. Add your Nyx accounts to `wallet-addresses.csv`
- Navigate to `nym/scripts/rewards-tracker/data`
- Open `wallet-addresses.csv` in your favourite text editor or a sheet managing tool (Like Libre Office Calc)
- To the first collumn called `address` add all Nyx addresses you want to track
- Delete all `add_wallet_or_delete` template examples
###### 3. Add entity to `wallet-addresses.csv` - optional
- In the same file operators who want to separate their nodes by an entity, can add this entity to the `tag` column
- If not leave this column empty - delete all `optional_tag_or_delete` fields
- Csv example with `tag`s:
```
address, tag
n1foofoofoo, personal
n1barbarbar, personal
n1bazbazbaz, mysquad
n1lollollol, mysquad
```
- For operators having all nodes under one entity, the tag field will be left empty. Example:
```csv
address, tag
n1foofoofoo
n1barbarbar
n1bazbazbaz
```
###### 4. Save `wallet-addresses.csv` and exit
###### 5. Run the program
- In terminal navigate to `nym/scripts/rewards-tracker`
- Run the program:
```
./node_rewards_tracker.py
```
</ Steps>
**The Output**
The result of running `node_rewards_tracker.py` is:
1. Printed table in terminal
2. Updated sheet with complete info stored in `data/node-balances.csv`
3. Historical data file stored in `data/data.yaml` - this file should not be changed manually, as all values older than 30 days get auto-removed
## Node Ping Tester
This tool is used to diagnose how many nodes providing self-described endpoint allow your IP to ping them. It's a very simple script fetching all [`/described`](https://validator.nymtech.net/api/v1/nym-nodes/described) nodes and trying to ping each of them.
The output is collected into two files:
```
├── ping_not_working.csv
└── ping_works.csv
```
**Installation & Running**
<Steps>
###### 1. SSH into your node server (VPS)
###### 2. Download and make executable
- Navigate to the directory where you want to have this script
- Download and make executable:
```sh
wget https://raw.githubusercontent.com/nymtech/nym/refs/heads/develop/scripts/test-nodes-pings.sh && \
chmod +x test-nodes-pings.sh
```
###### 3. Run the script
- Default running command is straight forward
```sh
./test-nodes-pings.sh
```
- If you want to increase the `ping` attempts from default 2 or lower the concurency, feel free to change the variables, like this:
```sh
PING_RETRIES=10 PING_TIMEOUT=5 CONCURRENCY=16 ./test-nodes-pings.sh
```
</Steps>
You can look up the IPs from `ping_not_working.csv`, using some online database, like [ipinfo.io](https://ipinfo.io).
Feel invited to share the outcome with Nym team, mentors and the rest of the operators in our [Matrix Node Operators channel](https://matrix.to/#/#operators:nymtech.chat).
@@ -19,11 +19,9 @@ use nym_authenticator_requests::{
};
use nym_authenticator_requests::{
latest::registration::{GatewayClient, PendingRegistrations, PrivateIPs},
traits::{
AuthenticatorRequest, AuthenticatorVersion, FinalMessage, InitMessage,
QueryBandwidthMessage, TopUpMessage,
},
v1, v2, v3, v4, v5, CURRENT_VERSION,
request::AuthenticatorRequest,
traits::{FinalMessage, InitMessage, QueryBandwidthMessage, TopUpMessage},
v1, v2, v3, v4, v5, AuthenticatorVersion, CURRENT_VERSION,
};
use nym_credential_verification::ecash::traits::EcashManager;
use nym_credential_verification::{
@@ -480,7 +478,7 @@ impl MixnetListener {
let mut registred_and_free = self.registred_and_free.write().await;
let registration_data = registred_and_free
.registration_in_progres
.get(&final_message.pub_key())
.get(&final_message.gateway_client_pub_key())
.ok_or(AuthenticatorError::RegistrationNotInProgress)?
.clone();
@@ -491,7 +489,7 @@ impl MixnetListener {
return Err(AuthenticatorError::MacVerificationFailure);
}
let mut peer = Peer::new(Key::new(final_message.pub_key().to_bytes()));
let mut peer = Peer::new(Key::new(final_message.gateway_client_pub_key().to_bytes()));
peer.allowed_ips
.push(IpAddrMask::new(final_message.private_ips().ipv4.into(), 32));
peer.allowed_ips.push(IpAddrMask::new(
@@ -532,7 +530,7 @@ impl MixnetListener {
registred_and_free
.registration_in_progres
.remove(&final_message.pub_key());
.remove(&final_message.gateway_client_pub_key());
let bytes = match AuthenticatorVersion::from(protocol) {
AuthenticatorVersion::V1 => v1::response::AuthenticatorResponse::new_registered(
+1
View File
@@ -67,6 +67,7 @@ utoipa-swagger-ui = { workspace = true, features = ["axum"] }
tracing = { workspace = true }
## internal
nym-cache = { path = "../common/nym-cache" }
nym-bandwidth-controller = { path = "../common/bandwidth-controller" }
nym-ecash-contract-common = { path = "../common/cosmwasm-smart-contracts/ecash-contract" }
nym-ecash-time = { path = "../common/ecash-time", features = ["expiration"] }

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