Compare commits

..

15 Commits

Author SHA1 Message Date
benedetta davico 1487c0efa0 TEMP remove nym-node from publishing 2025-09-15 14:26:46 +02:00
benedettadavico 40e1cbc7a9 update changelog 2025-08-20 12:34:04 +02:00
benedettadavico 2d3deeb424 bump versions 2025-08-06 09:56:08 +02:00
benedetta davico 3827dc357d Merge pull request #5936 from nymtech/release/2025.14-feta
Merge release/2025.14-feta to develop
2025-08-06 09:54:29 +02:00
benedetta davico a70e9e23d3 Merge branch 'develop' into release/2025.14-feta 2025-08-06 09:48:22 +02:00
Jędrzej Stuczyński dc59149a5d squashed feature/ecash-liveness-check (#5890) (#5890)
delay to gruyere

chore: delay to Feta

added threshold information to the response

nym api test clippy

bugfixes and endpoint improvements

expose results on api endpoints

wip: making nym api monitor network signers

added fallback legacy queries to get basic support idea

refactored the code to expose bool-only methods for status

ecash-signer-check lib for obtaining basic ecash signer information
2025-08-05 12:28:42 +01:00
benedetta davico e418c7587a Merge pull request #5914 from nymtech/feature/nym-node-gw-reset
nym-node debug command to reset providers db
2025-08-05 12:05:44 +02:00
import this 33339c085d [DOCs/operators]: Update ISP list (#5918)
* update ISP list

* remove typo
2025-07-31 13:47:27 +00:00
Sachin Kamath 863f329106 docs: update validator instructions and waitlist callout (#5922) 2025-07-30 15:03:39 +00:00
import this 314a37cabe WG exit policy scripts update (#5921)
* add NIP-3 ports to WG manager script

* add monero ports to local testing script

* console output snippet update
2025-07-30 09:43:39 +00:00
Jack Wampler 917f391948 Make DNS Resolver fallback optional (#5920)
default to no dns system fallback, but keep support
2025-07-29 11:00:24 -06:00
Jędrzej Stuczyński 0b4deda621 nym-node debug command to reset providers db 2025-07-25 13:33:12 +01:00
Jędrzej Stuczyński a4e674c98b basic zulip client for sending messages (#5913) 2025-07-24 16:22:35 +01:00
Jędrzej Stuczyński 7f97f13799 chore: nym node tokio console (#5909)
* conditionally enable console-subscriber within nym-node

* Update ci-build-upload-binaries.yml

* Update ci-build-upload-binaries.yml

add features console

* updated feature name

* fixed filtering on tracing layers

* add track_caller when spawning futures for better tokio-console support

* allow [client] tasks to specify their names when used within tokio console

* clippy

* pre-emptively fix wasm clippy

---------

Co-authored-by: Tommy Verrall <60836166+tommyv1987@users.noreply.github.com>
2025-07-24 11:00:58 +01:00
benedettadavico 85604e8305 bump versions 2025-07-23 10:18:45 +02:00
118 changed files with 6269 additions and 3511 deletions
@@ -38,15 +38,14 @@ jobs:
rm -rf ci-builds || true
mkdir -p $OUTPUT_DIR
echo $OUTPUT_DIR
- name: Install Dependencies (Linux)
run: sudo apt-get update && sudo apt-get -y install libudev-dev
- name: Sets env vars for tokio if set in manual dispatch inputs
run: |
echo 'RUSTFLAGS="--cfg tokio_unstable"' >> $GITHUB_ENV
if: github.event_name == 'workflow_dispatch' && inputs.add_tokio_unstable == true
run: |
echo "RUSTFLAGS=--cfg tokio_unstable" >> $GITHUB_ENV
echo "CARGO_FEATURES=--features tokio-console" >> $GITHUB_ENV
- name: Install Rust stable
uses: actions-rs/toolchain@v1
with:
@@ -103,7 +102,6 @@ jobs:
if [ ${{ github.event_name == 'workflow_dispatch' && inputs.enable_deb == true }} = true ]; then
cp target/debian/*.deb $OUTPUT_DIR
fi
- name: Deploy branch to CI www
continue-on-error: true
uses: easingthemes/ssh-deploy@main
@@ -30,13 +30,11 @@ 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 }}
@@ -76,7 +74,6 @@ jobs:
target/release/nym-network-requester
target/release/nym-cli
target/release/nymvisor
target/release/nym-node
retention-days: 30
- id: create-release
@@ -91,7 +88,6 @@ 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' }}
+36
View File
@@ -4,6 +4,42 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
## [Unreleased]
## [2025.15-gruyere] (2025-08-20)
- Migrate strum to 0.27.2 ([#5960])
- WG exit policy scripts update ([#5921])
- Make DNS Resolver fallback optional ([#5920])
- nym-node debug command to reset providers db ([#5914])
- basic zulip client for sending messages ([#5913])
- chore: allow compatibility with 'CDLA-Permissive-2.0' ([#5910])
- feat: ecash liveness check ([#5890])
- Remove old free credential handle ([#5864])
[#5960]: https://github.com/nymtech/nym/pull/5960
[#5921]: https://github.com/nymtech/nym/pull/5921
[#5920]: https://github.com/nymtech/nym/pull/5920
[#5914]: https://github.com/nymtech/nym/pull/5914
[#5913]: https://github.com/nymtech/nym/pull/5913
[#5910]: https://github.com/nymtech/nym/pull/5910
[#5890]: https://github.com/nymtech/nym/pull/5890
[#5864]: https://github.com/nymtech/nym/pull/5864
## [2025.14-feta] (2025-08-05)
- chore: nym node tokio console ([#5909])
- Feature/dkg snapshot epoch ([#5900])
- Feature/dkg epoch dealers query ([#5899])
- sqlx-pool-guard: allocate more memory on windows ([#5896])
- Support mnemonic in the NS agent ([#5883])
- Allow PG database backend ([#5880])
[#5909]: https://github.com/nymtech/nym/pull/5909
[#5900]: https://github.com/nymtech/nym/pull/5900
[#5899]: https://github.com/nymtech/nym/pull/5899
[#5896]: https://github.com/nymtech/nym/pull/5896
[#5883]: https://github.com/nymtech/nym/pull/5883
[#5880]: https://github.com/nymtech/nym/pull/5880
## [2025.13-emmental] (2025-07-22)
- fix: don't allow mixnode running in exit mode ([#5898])
Generated
+98 -126
View File
@@ -556,34 +556,6 @@ dependencies = [
"log",
]
[[package]]
name = "axum"
version = "0.6.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf"
dependencies = [
"async-trait",
"axum-core 0.3.4",
"bitflags 1.3.2",
"bytes",
"futures-util",
"http 0.2.12",
"http-body 0.4.6",
"hyper 0.14.32",
"itoa",
"matchit 0.7.3",
"memchr",
"mime",
"percent-encoding",
"pin-project-lite",
"rustversion",
"serde",
"sync_wrapper 0.1.2",
"tower 0.4.13",
"tower-layer",
"tower-service",
]
[[package]]
name = "axum"
version = "0.7.9"
@@ -664,23 +636,6 @@ dependencies = [
"serde",
]
[[package]]
name = "axum-core"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "759fa577a247914fd3f7f76d62972792636412fbfd634cd452f6a385a74d2d2c"
dependencies = [
"async-trait",
"bytes",
"futures-util",
"http 0.2.12",
"http-body 0.4.6",
"mime",
"rustversion",
"tower-layer",
"tower-service",
]
[[package]]
name = "axum-core"
version = "0.4.5"
@@ -1441,11 +1396,12 @@ dependencies = [
[[package]]
name = "console-api"
version = "0.5.0"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2895653b4d9f1538a83970077cb01dfc77a4810524e51a110944688e916b18e"
checksum = "8030735ecb0d128428b64cd379809817e620a40e5001c54465b99ec5feec2857"
dependencies = [
"prost 0.11.9",
"futures-core",
"prost",
"prost-types",
"tonic",
"tracing-core",
@@ -1453,16 +1409,18 @@ dependencies = [
[[package]]
name = "console-subscriber"
version = "0.1.10"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4cf42660ac07fcebed809cfe561dd8730bcd35b075215e6479c516bcd0d11cb"
checksum = "6539aa9c6a4cd31f4b1c040f860a1eac9aa80e7df6b05d506a6e7179936d6a01"
dependencies = [
"console-api",
"crossbeam-channel",
"crossbeam-utils",
"futures",
"futures-task",
"hdrhistogram",
"humantime",
"hyper-util",
"prost",
"prost-types",
"serde",
"serde_json",
@@ -1555,7 +1513,7 @@ version = "0.26.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "462e1f6a8e005acc8835d32d60cbd7973ed65ea2a8d8473830e675f050956427"
dependencies = [
"prost 0.13.5",
"prost",
"tendermint-proto",
]
@@ -2028,7 +1986,7 @@ dependencies = [
"cw-storage-plus",
"cw-utils",
"itertools 0.14.0",
"prost 0.13.5",
"prost",
"schemars 0.8.22",
"serde",
"sha2 0.10.9",
@@ -3580,6 +3538,7 @@ dependencies = [
"bytes",
"futures-channel",
"futures-util",
"h2 0.4.11",
"http 1.3.1",
"http-body 1.0.1",
"httparse",
@@ -3624,14 +3583,15 @@ dependencies = [
[[package]]
name = "hyper-timeout"
version = "0.4.1"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1"
checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0"
dependencies = [
"hyper 0.14.32",
"hyper 1.6.0",
"hyper-util",
"pin-project-lite",
"tokio",
"tokio-io-timeout",
"tower-service",
]
[[package]]
@@ -4895,18 +4855,16 @@ checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3"
[[package]]
name = "nym-api"
version = "1.1.62"
version = "1.1.64"
dependencies = [
"anyhow",
"async-trait",
"axum 0.7.9",
"axum-extra",
"axum-test 16.4.1",
"bincode",
"bip39",
"bs58",
"cfg-if",
"chrono",
"clap",
"console-subscriber",
"cosmwasm-std",
@@ -4915,13 +4873,9 @@ dependencies = [
"cw3",
"cw4",
"dashmap",
"dirs",
"dotenv",
"futures",
"getset",
"humantime-serde",
"itertools 0.14.0",
"k256",
"moka",
"nym-api-requests 0.1.0",
"nym-bandwidth-controller",
@@ -4936,12 +4890,11 @@ dependencies = [
"nym-crypto 0.4.0",
"nym-dkg",
"nym-ecash-contract-common 0.1.0",
"nym-ecash-signer-check",
"nym-ecash-time 0.1.0",
"nym-gateway-client",
"nym-http-api-common 0.1.0",
"nym-inclusion-probability",
"nym-mixnet-contract-common 0.6.0",
"nym-multisig-contract-common 0.1.0",
"nym-node-requests 0.1.0",
"nym-node-tester-utils",
"nym-pemstore 0.3.0",
@@ -4953,7 +4906,6 @@ dependencies = [
"nym-topology 0.1.0",
"nym-types",
"nym-validator-client 0.1.0",
"nym-vesting-contract-common 0.7.0",
"pin-project",
"rand 0.8.5",
"rand_chacha 0.3.1",
@@ -4989,14 +4941,15 @@ dependencies = [
"cosmrs",
"cosmwasm-std",
"ecdsa",
"getset",
"hex",
"humantime-serde",
"nym-coconut-dkg-common 0.1.0",
"nym-compact-ecash 0.1.0",
"nym-config 0.1.0",
"nym-contracts-common 0.5.0",
"nym-credentials-interface 0.1.0",
"nym-crypto 0.4.0",
"nym-ecash-signer-check-types",
"nym-ecash-time 0.1.0",
"nym-mixnet-contract-common 0.6.0",
"nym-network-defaults 0.1.0",
@@ -5185,7 +5138,7 @@ dependencies = [
[[package]]
name = "nym-cli"
version = "1.1.59"
version = "1.1.61"
dependencies = [
"anyhow",
"base64 0.22.1",
@@ -5267,7 +5220,7 @@ dependencies = [
[[package]]
name = "nym-client"
version = "1.1.59"
version = "1.1.61"
dependencies = [
"bs58",
"clap",
@@ -5309,6 +5262,7 @@ dependencies = [
"async-trait",
"base64 0.22.1",
"bs58",
"cfg-if",
"clap",
"comfy-table",
"futures",
@@ -5901,6 +5855,36 @@ dependencies = [
"thiserror 2.0.12",
]
[[package]]
name = "nym-ecash-signer-check"
version = "0.1.0"
dependencies = [
"futures",
"nym-ecash-signer-check-types",
"nym-network-defaults 0.1.0",
"nym-validator-client 0.1.0",
"semver 1.0.26",
"thiserror 2.0.12",
"tokio",
"tracing",
"url",
]
[[package]]
name = "nym-ecash-signer-check-types"
version = "0.1.0"
dependencies = [
"nym-coconut-dkg-common 0.1.0",
"nym-crypto 0.4.0",
"semver 1.0.26",
"serde",
"thiserror 2.0.12",
"time",
"tracing",
"url",
"utoipa",
]
[[package]]
name = "nym-ecash-time"
version = "0.1.0"
@@ -6556,7 +6540,7 @@ dependencies = [
[[package]]
name = "nym-network-requester"
version = "1.1.60"
version = "1.1.62"
dependencies = [
"addr",
"anyhow",
@@ -6606,7 +6590,7 @@ dependencies = [
[[package]]
name = "nym-node"
version = "1.15.0"
version = "1.17.0"
dependencies = [
"anyhow",
"arc-swap",
@@ -6619,9 +6603,11 @@ dependencies = [
"bs58",
"cargo_metadata 0.19.2",
"celes",
"cfg-if",
"chacha",
"clap",
"colored",
"console-subscriber",
"criterion",
"csv",
"cupid",
@@ -7130,7 +7116,7 @@ dependencies = [
[[package]]
name = "nym-socks5-client"
version = "1.1.59"
version = "1.1.61"
dependencies = [
"bs58",
"clap",
@@ -7830,7 +7816,7 @@ dependencies = [
"nym-performance-contract-common",
"nym-serde-helpers 0.1.0",
"nym-vesting-contract-common 0.7.0",
"prost 0.13.5",
"prost",
"reqwest 0.12.22",
"serde",
"serde_json",
@@ -7880,7 +7866,7 @@ dependencies = [
"nym-network-defaults 0.1.0 (git+https://github.com/nymtech/nym.git?branch=release/2025.11-cheddar)",
"nym-serde-helpers 0.1.0 (git+https://github.com/nymtech/nym.git?branch=release/2025.11-cheddar)",
"nym-vesting-contract-common 0.7.0 (git+https://github.com/nymtech/nym.git?branch=release/2025.11-cheddar)",
"prost 0.13.5",
"prost",
"reqwest 0.12.22",
"serde",
"serde_json",
@@ -8088,7 +8074,7 @@ dependencies = [
[[package]]
name = "nymvisor"
version = "0.1.24"
version = "0.1.26"
dependencies = [
"anyhow",
"bytes",
@@ -8849,16 +8835,6 @@ dependencies = [
"thiserror 2.0.12",
]
[[package]]
name = "prost"
version = "0.11.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b82eaa1d779e9a4bc1c3217db8ffbeabaae1dca241bf70183242128d48681cd"
dependencies = [
"bytes",
"prost-derive 0.11.9",
]
[[package]]
name = "prost"
version = "0.13.5"
@@ -8866,20 +8842,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5"
dependencies = [
"bytes",
"prost-derive 0.13.5",
]
[[package]]
name = "prost-derive"
version = "0.11.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5d2d8d10f3c6ded6da8b05b5fb3b8a5082514344d56c9f871412d29b4e075b4"
dependencies = [
"anyhow",
"itertools 0.10.5",
"proc-macro2",
"quote",
"syn 1.0.109",
"prost-derive",
]
[[package]]
@@ -8897,11 +8860,11 @@ dependencies = [
[[package]]
name = "prost-types"
version = "0.11.9"
version = "0.13.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "213622a1460818959ac1181aaeb2dc9c7f63df720db7d788b3e24eacd1983e13"
checksum = "52c2c1bf36ddb1a1c396b3601a3cec27c2462e45f07c386894ec3ccf5332bd16"
dependencies = [
"prost 0.11.9",
"prost",
]
[[package]]
@@ -10816,7 +10779,7 @@ dependencies = [
"k256",
"num-traits",
"once_cell",
"prost 0.13.5",
"prost",
"ripemd",
"serde",
"serde_bytes",
@@ -10853,7 +10816,7 @@ checksum = "d2c40e13d39ca19082d8a7ed22de7595979350319833698f8b1080f29620a094"
dependencies = [
"bytes",
"flex-error",
"prost 0.13.5",
"prost",
"serde",
"serde_bytes",
"subtle-encoding",
@@ -11125,16 +11088,6 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "tokio-io-timeout"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bd86198d9ee903fedd2f9a2e72014287c0d9167e4ae43b5853007205dda1b76"
dependencies = [
"pin-project-lite",
"tokio",
]
[[package]]
name = "tokio-macros"
version = "2.5.0"
@@ -11323,24 +11276,26 @@ checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
[[package]]
name = "tonic"
version = "0.9.2"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3082666a3a6433f7f511c7192923fa1fe07c69332d3c6a2e6bb040b569199d5a"
checksum = "877c5b330756d856ffcc4553ab34a5684481ade925ecc54bcd1bf02b1d0d4d52"
dependencies = [
"async-stream",
"async-trait",
"axum 0.6.20",
"base64 0.21.7",
"axum 0.7.9",
"base64 0.22.1",
"bytes",
"futures-core",
"futures-util",
"h2 0.3.27",
"http 0.2.12",
"http-body 0.4.6",
"hyper 0.14.32",
"h2 0.4.11",
"http 1.3.1",
"http-body 1.0.1",
"http-body-util",
"hyper 1.6.0",
"hyper-timeout",
"hyper-util",
"percent-encoding",
"pin-project",
"prost 0.11.9",
"prost",
"socket2",
"tokio",
"tokio-stream",
"tower 0.4.13",
@@ -13219,3 +13174,20 @@ dependencies = [
"cc",
"pkg-config",
]
[[package]]
name = "zulip-client"
version = "0.1.0"
dependencies = [
"itertools 0.14.0",
"nym-bin-common 0.6.0",
"nym-http-api-client 0.1.0",
"reqwest 0.12.22",
"serde",
"serde_json",
"thiserror 2.0.12",
"tokio",
"tracing",
"url",
"zeroize",
]
+10 -3
View File
@@ -39,7 +39,8 @@ members = [
"common/cosmwasm-smart-contracts/ecash-contract",
"common/cosmwasm-smart-contracts/group-contract",
"common/cosmwasm-smart-contracts/mixnet-contract",
"common/cosmwasm-smart-contracts/multisig-contract", "common/cosmwasm-smart-contracts/nym-performance-contract",
"common/cosmwasm-smart-contracts/multisig-contract",
"common/cosmwasm-smart-contracts/nym-performance-contract",
"common/cosmwasm-smart-contracts/nym-pool-contract",
"common/cosmwasm-smart-contracts/vesting-contract",
"common/credential-storage",
@@ -49,6 +50,8 @@ members = [
"common/credentials-interface",
"common/crypto",
"common/dkg",
"common/ecash-signer-check",
"common/ecash-signer-check-types",
"common/ecash-time",
"common/execute",
"common/exit-policy",
@@ -99,7 +102,7 @@ members = [
"common/wasm/storage",
"common/wasm/utils",
"common/wireguard",
"common/wireguard-types",
"common/wireguard-types", "common/zulip-client",
"documentation/autodoc",
"gateway",
"nym-api",
@@ -218,7 +221,7 @@ clap_complete_fig = "4.5"
colored = "2.2"
comfy-table = "7.1.4"
console = "0.15.11"
console-subscriber = "0.1.1"
console-subscriber = "0.4.1"
console_error_panic_hook = "0.1"
const-str = "0.5.6"
const_format = "0.2.34"
@@ -435,6 +438,9 @@ opt-level = 'z'
# lto = true
opt-level = 'z'
[workspace.lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(tokio_unstable)'] }
[workspace.lints.clippy]
unwrap_used = "deny"
expect_used = "deny"
@@ -444,3 +450,4 @@ exit = "deny"
panic = "deny"
unimplemented = "deny"
unreachable = "deny"
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "nym-client"
version = "1.1.59"
version = "1.1.61"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>", "Jędrzej Stuczyński <andrew@nymtech.net>"]
description = "Implementation of the Nym Client"
edition = "2021"
+1 -1
View File
@@ -111,7 +111,7 @@ impl SocketClient {
let dkg_query_client = if self.config.base.client.disabled_credentials_mode {
None
} else {
Some(default_query_dkg_client_from_config(&self.config.base))
Some(default_query_dkg_client_from_config(&self.config.base)?)
};
let storage = self.initialise_storage().await?;
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "nym-socks5-client"
version = "1.1.59"
version = "1.1.61"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>"]
description = "A SOCKS5 localhost proxy that converts incoming messages to Sphinx and sends them to a Nym address"
edition = "2021"
+4
View File
@@ -13,6 +13,7 @@ async-trait = { workspace = true }
base64 = { workspace = true }
bs58 = { workspace = true }
clap = { workspace = true, optional = true }
cfg-if = { workspace = true }
comfy-table = { workspace = true, optional = true }
futures = { workspace = true }
humantime = { workspace = true }
@@ -123,3 +124,6 @@ fs-surb-storage = ["nym-client-core-surb-storage/fs-surb-storage"]
fs-gateways-storage = ["nym-client-core-gateways-storage/fs-gateways-storage"]
wasm = ["nym-gateway-client/wasm"]
metrics-server = []
[lints]
workspace = true
@@ -58,6 +58,7 @@ where
Some(data) => data,
None => {
// SAFETY: one of those arguments must have been set
#[allow(clippy::unwrap_used)]
fs::read(common_args.signatures_path.unwrap())?
}
};
@@ -64,6 +64,7 @@ where
Some(data) => data,
None => {
// SAFETY: one of those arguments must have been set
#[allow(clippy::unwrap_used)]
fs::read(common_args.credential_path.unwrap())?
}
};
@@ -58,6 +58,7 @@ where
Some(data) => data,
None => {
// SAFETY: one of those arguments must have been set
#[allow(clippy::unwrap_used)]
fs::read(common_args.signatures_path.unwrap())?
}
};
@@ -58,6 +58,7 @@ where
Some(data) => data,
None => {
// SAFETY: one of those arguments must have been set
#[allow(clippy::unwrap_used)]
fs::read(common_args.key_path.unwrap())?
}
};
@@ -135,9 +135,11 @@ pub enum ClientInputStatus {
}
impl ClientInputStatus {
#[allow(clippy::panic)]
pub fn register_producer(&mut self) -> ClientInput {
match std::mem::replace(self, ClientInputStatus::Connected) {
ClientInputStatus::AwaitingProducer { client_input } => client_input,
// critical failure implying misuse of software
ClientInputStatus::Connected => panic!("producer was already registered before"),
}
}
@@ -149,9 +151,11 @@ pub enum ClientOutputStatus {
}
impl ClientOutputStatus {
#[allow(clippy::panic)]
pub fn register_consumer(&mut self) -> ClientOutput {
match std::mem::replace(self, ClientOutputStatus::Connected) {
ClientOutputStatus::AwaitingConsumer { client_output } => client_output,
// critical failure implying misuse of software
ClientOutputStatus::Connected => panic!("consumer was already registered before"),
}
}
@@ -707,11 +711,14 @@ where
})?;
let store_clone = mem_store.clone();
spawn_future(async move {
persistent_storage
.flush_on_shutdown(store_clone, shutdown)
.await
});
spawn_future!(
async move {
persistent_storage
.flush_on_shutdown(store_clone, shutdown)
.await
},
"PersistentReplyStorage::flush_on_shutdown"
);
Ok(mem_store)
}
@@ -732,7 +739,7 @@ where
let mut rng = OsRng;
let keys = if let Some(derivation_material) = derivation_material {
ClientKeys::from_master_key(&mut rng, &derivation_material)
.map_err(|_| ClientCoreError::HkdfDerivationError {})?
.map_err(|_| ClientCoreError::HkdfDerivationError)?
} else {
ClientKeys::generate_new(&mut rng)
};
@@ -114,41 +114,32 @@ pub async fn setup_fs_gateways_storage<P: AsRef<Path>>(
})
}
pub fn create_bandwidth_controller<St: CredentialStorage>(
config: &Config,
storage: St,
) -> BandwidthController<QueryHttpRpcNyxdClient, St> {
let nyxd_url = config
.get_validator_endpoints()
.pop()
.expect("No nyxd validator endpoint provided");
create_bandwidth_controller_with_urls(nyxd_url, storage)
}
pub fn create_bandwidth_controller_with_urls<St: CredentialStorage>(
nyxd_url: Url,
storage: St,
) -> BandwidthController<QueryHttpRpcNyxdClient, St> {
let client = default_query_dkg_client(nyxd_url);
) -> Result<BandwidthController<QueryHttpRpcNyxdClient, St>, ClientCoreError> {
let client = default_query_dkg_client(nyxd_url)?;
BandwidthController::new(storage, client)
Ok(BandwidthController::new(storage, client))
}
pub fn default_query_dkg_client_from_config(config: &Config) -> QueryHttpRpcNyxdClient {
pub fn default_query_dkg_client_from_config(
config: &Config,
) -> Result<QueryHttpRpcNyxdClient, ClientCoreError> {
let nyxd_url = config
.get_validator_endpoints()
.pop()
.expect("No nyxd validator endpoint provided");
.ok_or(ClientCoreError::RpcClientMissingUrl)?;
default_query_dkg_client(nyxd_url)
}
pub fn default_query_dkg_client(nyxd_url: Url) -> QueryHttpRpcNyxdClient {
pub fn default_query_dkg_client(nyxd_url: Url) -> Result<QueryHttpRpcNyxdClient, ClientCoreError> {
let details = nym_network_defaults::NymNetworkDetails::new_from_env();
let client_config = nyxd::Config::try_from_nym_network_details(&details)
.expect("failed to construct validator client config");
.map_err(|source| ClientCoreError::InvalidNetworkDetails { source })?;
// overwrite env configuration with config URLs
QueryHttpRpcNyxdClient::connect(client_config, nyxd_url.as_str())
.expect("Could not construct query client")
.map_err(|source| ClientCoreError::RpcClientCreationFailure { source })
}
@@ -235,6 +235,7 @@ impl LoopCoverTrafficStream<OsRng> {
tokio::task::yield_now().await;
}
#[allow(clippy::panic)]
pub fn start(mut self) {
if self.cover_traffic.disable_loop_cover_traffic_stream {
// we should have never got here in the first place - the task should have never been created to begin with
@@ -251,27 +252,30 @@ impl LoopCoverTrafficStream<OsRng> {
let mut shutdown = self.task_client.fork("select");
spawn_future(async move {
debug!("Started LoopCoverTrafficStream with graceful shutdown support");
spawn_future!(
async move {
debug!("Started LoopCoverTrafficStream with graceful shutdown support");
while !shutdown.is_shutdown() {
tokio::select! {
biased;
_ = shutdown.recv() => {
tracing::trace!("LoopCoverTrafficStream: Received shutdown");
}
next = self.next() => {
if next.is_some() {
self.on_new_message().await;
} else {
tracing::trace!("LoopCoverTrafficStream: Stopping since channel closed");
break;
while !shutdown.is_shutdown() {
tokio::select! {
biased;
_ = shutdown.recv() => {
tracing::trace!("LoopCoverTrafficStream: Received shutdown");
}
next = self.next() => {
if next.is_some() {
self.on_new_message().await;
} else {
tracing::trace!("LoopCoverTrafficStream: Stopping since channel closed");
break;
}
}
}
}
}
shutdown.recv_timeout().await;
tracing::debug!("LoopCoverTrafficStream: Exiting");
})
shutdown.recv_timeout().await;
tracing::debug!("LoopCoverTrafficStream: Exiting");
},
"LoopCoverTrafficStream"
)
}
}
@@ -98,6 +98,8 @@ impl MixTrafficController {
debug_assert!(!mix_packets.is_empty());
let result = if mix_packets.len() == 1 {
// SAFETY: we just checked we have one packet
#[allow(clippy::unwrap_used)]
let mix_packet = mix_packets.pop().unwrap();
self.gateway_transceiver.send_mix_packet(mix_packet).await
} else {
@@ -117,51 +119,54 @@ impl MixTrafficController {
}
pub fn start(mut self) {
spawn_future(async move {
debug!("Started MixTrafficController with graceful shutdown support");
spawn_future!(
async move {
debug!("Started MixTrafficController with graceful shutdown support");
while !self.task_client.is_shutdown() {
tokio::select! {
mix_packets = self.mix_rx.recv() => match mix_packets {
Some(mix_packets) => {
if let Err(err) = self.on_messages(mix_packets).await {
error!("Failed to send sphinx packet(s) to the gateway: {err}");
if self.consecutive_gateway_failure_count == MAX_FAILURE_COUNT {
// Disconnect from the gateway. If we should try to re-connect
// is handled at a higher layer.
error!("Failed to send sphinx packet to the gateway {MAX_FAILURE_COUNT} times in a row - assuming the gateway is dead");
// Do we need to handle the embedded mixnet client case
// separately?
self.task_client.send_we_stopped(Box::new(ClientCoreError::GatewayFailedToForwardMessages));
break;
while !self.task_client.is_shutdown() {
tokio::select! {
mix_packets = self.mix_rx.recv() => match mix_packets {
Some(mix_packets) => {
if let Err(err) = self.on_messages(mix_packets).await {
error!("Failed to send sphinx packet(s) to the gateway: {err}");
if self.consecutive_gateway_failure_count == MAX_FAILURE_COUNT {
// Disconnect from the gateway. If we should try to re-connect
// is handled at a higher layer.
error!("Failed to send sphinx packet to the gateway {MAX_FAILURE_COUNT} times in a row - assuming the gateway is dead");
// Do we need to handle the embedded mixnet client case
// separately?
self.task_client.send_we_stopped(Box::new(ClientCoreError::GatewayFailedToForwardMessages));
break;
}
}
},
None => {
tracing::trace!("MixTrafficController: Stopping since channel closed");
break;
}
},
None => {
tracing::trace!("MixTrafficController: Stopping since channel closed");
client_request = self.client_rx.recv() => match client_request {
Some(client_request) => {
match self.gateway_transceiver.send_client_request(client_request).await {
Ok(_) => (),
Err(e) => error!("Failed to send client request: {e}"),
};
},
None => {
tracing::trace!("MixTrafficController, client request channel closed");
}
},
_ = self.task_client.recv() => {
tracing::trace!("MixTrafficController: Received shutdown");
break;
}
},
client_request = self.client_rx.recv() => match client_request {
Some(client_request) => {
match self.gateway_transceiver.send_client_request(client_request).await {
Ok(_) => (),
Err(e) => error!("Failed to send client request: {e}"),
};
},
None => {
tracing::trace!("MixTrafficController, client request channel closed");
}
},
_ = self.task_client.recv() => {
tracing::trace!("MixTrafficController: Received shutdown");
break;
}
}
}
self.task_client.recv_timeout().await;
self.task_client.recv_timeout().await;
tracing::debug!("MixTrafficController: Exiting");
});
tracing::debug!("MixTrafficController: Exiting");
},
"MixTrafficController"
);
}
}
@@ -269,6 +269,8 @@ pub struct MockGateway {
}
impl Default for MockGateway {
// test code
#[allow(clippy::unwrap_used)]
fn default() -> Self {
MockGateway {
dummy_identity: "3ebjp1Fb9hdcS1AR6AZihgeJiMHkB5jjJUsvqNnfQwU7"
@@ -194,10 +194,11 @@ impl ActionController {
trace!("{frag_id} is updating its delay");
// TODO: is it possible to solve this without either locking or temporarily removing the value?
if let Some((pending_ack_data, queue_key)) = self.pending_acks_data.remove(&frag_id) {
// this Action is triggered by `RetransmissionRequestListener` (for 'normal' packets)
// SAFETY: this Action is triggered by `RetransmissionRequestListener` (for 'normal' packets)
// or `ReplyController` (for 'reply' packets) which held the other potential
// reference to this Arc. HOWEVER, before the Action was pushed onto the queue, the reference
// was dropped hence this unwrap is safe.
#[allow(clippy::unwrap_used)]
let mut inner_data = Arc::try_unwrap(pending_ack_data).unwrap();
inner_data.update_retransmitted(delay);
@@ -209,6 +210,7 @@ impl ActionController {
}
// note: when the entry expires it's automatically removed from pending_acks_timers
#[allow(clippy::panic)]
fn handle_expired_ack_timer(&mut self, expired_ack: Expired<FragmentIdentifier>) {
let frag_id = expired_ack.into_inner();
@@ -120,6 +120,7 @@ where
}
}
#[allow(clippy::panic)]
async fn on_input_message(&mut self, msg: InputMessage) {
match msg {
InputMessage::Regular {
@@ -213,7 +214,9 @@ where
self.handle_premade_packets(msgs, lane).await
}
// MessageWrappers can't be nested
InputMessage::MessageWrapper { .. } => unimplemented!(),
InputMessage::MessageWrapper { .. } => {
panic!("attempted to use nested MessageWrapper")
}
},
};
}
@@ -298,29 +298,44 @@ where
let mut sent_notification_listener = self.sent_notification_listener;
let mut action_controller = self.action_controller;
spawn_future(async move {
acknowledgement_listener.run().await;
debug!("The acknowledgement listener has finished execution!");
});
spawn_future!(
async move {
acknowledgement_listener.run().await;
debug!("The acknowledgement listener has finished execution!");
},
"AcknowledgementController::AcknowledgementListener"
);
spawn_future(async move {
input_message_listener.run().await;
debug!("The input listener has finished execution!");
});
spawn_future!(
async move {
input_message_listener.run().await;
debug!("The input listener has finished execution!");
},
"AcknowledgementController::InputMessageListener"
);
spawn_future(async move {
retransmission_request_listener.run(packet_type).await;
debug!("The retransmission request listener has finished execution!");
});
spawn_future!(
async move {
retransmission_request_listener.run(packet_type).await;
debug!("The retransmission request listener has finished execution!");
},
"AcknowledgementController::RetransmissionRequestListener"
);
spawn_future(async move {
sent_notification_listener.run().await;
debug!("The sent notification listener has finished execution!");
});
spawn_future!(
async move {
sent_notification_listener.run().await;
debug!("The sent notification listener has finished execution!");
},
"AcknowledgementController::SentNotificationListener"
);
spawn_future(async move {
action_controller.run().await;
debug!("The controller has finished execution!");
});
spawn_future!(
async move {
action_controller.run().await;
debug!("The controller has finished execution!");
},
"AcknowledgementController::ActionController"
);
}
}
@@ -35,6 +35,9 @@ pub enum PreparationError {
#[error(transparent)]
NymTopologyError(#[from] NymTopologyError),
#[error("message wasn't split into any fragments!")]
EmptyFragments,
#[error("message too long for a single SURB, splitting into {fragments} fragments.")]
MessageTooLongForSingleSurb { fragments: usize },
@@ -320,6 +323,16 @@ where
});
}
if fragment.is_empty() {
error!("CRITICAL FAILURE: our split message didn't result in any sendable fragments");
return Err(SurbWrappedPreparationError {
source: PreparationError::EmptyFragments,
returned_surbs: Some(vec![reply_surb]),
});
}
// SAFETY: we just checked we have one fragment
#[allow(clippy::unwrap_used)]
let chunk = fragment.pop().unwrap();
let chunk_clone = chunk.clone();
let prepared_fragment = self
@@ -657,6 +670,7 @@ where
.zip(reply_surbs.into_iter())
.map(|(fragment, reply_surb)| {
// unwrap here is fine as we know we have a valid topology
#[allow(clippy::unwrap_used)]
self.message_preparer
.prepare_reply_chunk_for_sending(
fragment,
@@ -224,14 +224,20 @@ impl RealMessagesController<OsRng> {
let ack_control = self.ack_control;
let mut reply_control = self.reply_control;
spawn_future(async move {
out_queue_control.run().await;
debug!("The out queue controller has finished execution!");
});
spawn_future(async move {
reply_control.run().await;
debug!("The reply controller has finished execution!");
});
spawn_future!(
async move {
out_queue_control.run().await;
debug!("The out queue controller has finished execution!");
},
"RealMessagesController::OutQueueControl)"
);
spawn_future!(
async move {
reply_control.run().await;
debug!("The reply controller has finished execution!");
},
"RealMessagesController::ReplyController"
);
ack_control.start(packet_type);
}
@@ -249,6 +249,8 @@ where
}
};
// SAFETY: our topology must be valid at this point
#[allow(clippy::expect_used)]
(
generate_loop_cover_packet(
&mut self.rng,
@@ -439,6 +441,8 @@ where
tracing::trace!("handling real_messages: size: {}", real_messages.len());
self.transmission_buffer.store(&conn_id, real_messages);
// SAFETY: we just stored the message
#[allow(clippy::expect_used)]
let real_next = self.pop_next_message().expect("Just stored one");
Poll::Ready(Some(StreamMessage::Real(Box::new(real_next))))
@@ -487,6 +491,8 @@ where
// First store what we got for the given connection id
self.transmission_buffer.store(&conn_id, real_messages);
// SAFETY: we just stored the message
#[allow(clippy::expect_used)]
let real_next = self.pop_next_message().expect("we just added one");
Poll::Ready(Some(StreamMessage::Real(Box::new(real_next))))
@@ -198,6 +198,7 @@ impl<R: MessageReceiver> ReceivedMessagesBuffer<R> {
}
}
#[allow(clippy::panic)]
async fn disconnect_sender(&mut self) {
let mut guard = self.inner.lock().await;
if guard.message_sender.is_none() {
@@ -208,6 +209,7 @@ impl<R: MessageReceiver> ReceivedMessagesBuffer<R> {
guard.message_sender = None;
}
#[allow(clippy::panic)]
async fn connect_sender(&mut self, sender: ReconstructedMessagesSender) {
let mut guard = self.inner.lock().await;
if guard.message_sender.is_some() {
@@ -599,14 +601,20 @@ impl<R: MessageReceiver + Clone + Send + 'static> ReceivedMessagesBufferControll
let mut fragmented_message_receiver = self.fragmented_message_receiver;
let mut request_receiver = self.request_receiver;
spawn_future(async move {
match fragmented_message_receiver.run().await {
Ok(_) => {}
Err(e) => error!("{e}"),
}
});
spawn_future(async move {
request_receiver.run().await;
});
spawn_future!(
async move {
match fragmented_message_receiver.run().await {
Ok(_) => {}
Err(e) => error!("{e}"),
}
},
"ReceivedMessagesBufferController::FragmentedMessageReceiver"
);
spawn_future!(
async move {
request_receiver.run().await;
},
"ReceivedMessagesBufferController::RequestReceiver"
);
}
}
@@ -155,8 +155,9 @@ where
data: Vec<Arc<PendingAcknowledgement>>,
) {
trace!("re-inserting pending retransmissions for {recipient}");
// the underlying entry MUST exist as we've just got data from there
// SAFETY: the underlying entry MUST exist as we've just got data from there
// and we hold a mut reference
#[allow(clippy::expect_used)]
let map_entry = &mut self
.surb_senders
.get_mut(recipient)
@@ -429,6 +430,7 @@ where
.pop_at_most_n_next_messages_at_random(amount)
}
#[allow(clippy::panic)]
async fn try_clear_pending_queue(&mut self, target: AnonymousSenderTag) {
trace!("trying to clear pending queue");
let available_surbs = self.surbs_storage.available_surbs(&target);
@@ -165,9 +165,12 @@ impl StatisticsControl {
}
pub(crate) fn start(mut self) {
spawn_future(async move {
self.run().await;
})
spawn_future!(
async move {
self.run().await;
},
"StatisticsControl"
)
}
pub(crate) fn create_and_start(
@@ -145,36 +145,39 @@ impl TopologyRefresher {
}
pub fn start(mut self) {
spawn_future(async move {
debug!("Started TopologyRefresher with graceful shutdown support");
spawn_future!(
async move {
debug!("Started TopologyRefresher with graceful shutdown support");
#[cfg(not(target_arch = "wasm32"))]
let mut interval = tokio_stream::wrappers::IntervalStream::new(tokio::time::interval(
self.refresh_rate,
));
#[cfg(not(target_arch = "wasm32"))]
let mut interval = tokio_stream::wrappers::IntervalStream::new(
tokio::time::interval(self.refresh_rate),
);
#[cfg(target_arch = "wasm32")]
let mut interval =
gloo_timers::future::IntervalStream::new(self.refresh_rate.as_millis() as u32);
#[cfg(target_arch = "wasm32")]
let mut interval =
gloo_timers::future::IntervalStream::new(self.refresh_rate.as_millis() as u32);
// We already have an initial topology, so no need to refresh it immediately.
// My understanding is that js setInterval does not fire immediately, so it's not
// needed there.
#[cfg(not(target_arch = "wasm32"))]
interval.next().await;
// We already have an initial topology, so no need to refresh it immediately.
// My understanding is that js setInterval does not fire immediately, so it's not
// needed there.
#[cfg(not(target_arch = "wasm32"))]
interval.next().await;
while !self.task_client.is_shutdown() {
tokio::select! {
_ = interval.next() => {
self.try_refresh().await;
},
_ = self.task_client.recv() => {
tracing::trace!("TopologyRefresher: Received shutdown");
},
while !self.task_client.is_shutdown() {
tokio::select! {
_ = interval.next() => {
self.try_refresh().await;
},
_ = self.task_client.recv() => {
tracing::trace!("TopologyRefresher: Received shutdown");
},
}
}
}
self.task_client.recv_timeout().await;
tracing::debug!("TopologyRefresher: Exiting");
})
self.task_client.recv_timeout().await;
tracing::debug!("TopologyRefresher: Exiting");
},
"TopologyRefresher"
)
}
}
+15 -1
View File
@@ -7,7 +7,9 @@ use nym_gateway_client::error::GatewayClientError;
use nym_topology::node::RoutingNodeError;
use nym_topology::{NodeId, NymTopologyError};
use nym_validator_client::nym_api::error::NymAPIError;
use nym_validator_client::nyxd::error::NyxdError;
use nym_validator_client::ValidatorClientError;
use rand::distributions::WeightedError;
use std::error::Error;
use std::path::PathBuf;
@@ -230,7 +232,19 @@ pub enum ClientCoreError {
UnexpectedKeyUpgrade { gateway_id: String },
#[error("failed to derive keys from master key")]
HkdfDerivationError {},
HkdfDerivationError,
#[error("missing url for constructing RPC client")]
RpcClientMissingUrl,
#[error("provided nym network details were malformed: {source}")]
InvalidNetworkDetails { source: NyxdError },
#[error("failed to construct RPC client: {source}")]
RpcClientCreationFailure { source: NyxdError },
#[error("failed to select valid gateway due to incomputable latency")]
GatewaySelectionFailure { source: WeightedError },
}
impl From<tungstenite::Error> for ClientCoreError {
+1 -1
View File
@@ -245,7 +245,7 @@ pub async fn choose_gateway_by_latency<R: Rng, G: ConnectableGateway + Clone>(
let gateways_with_latency = gateways_with_latency.lock().await;
let chosen = gateways_with_latency
.choose_weighted(rng, |item| 1. / item.latency.as_secs_f32())
.expect("invalid selection weight!");
.map_err(|source| ClientCoreError::GatewaySelectionFailure { source })?;
info!(
"chose gateway {} with average latency of {:?}",
+38 -2
View File
@@ -18,18 +18,54 @@ pub use nym_topology::{
};
#[cfg(target_arch = "wasm32")]
pub(crate) fn spawn_future<F>(future: F)
pub fn spawn_future<F>(future: F)
where
F: Future<Output = ()> + 'static,
{
wasm_bindgen_futures::spawn_local(future);
}
// TODO: expose similar API to the rest of the codebase,
// perhaps with some simple trait for a task to define its name
#[cfg(not(target_arch = "wasm32"))]
pub(crate) fn spawn_future<F>(future: F)
#[track_caller]
pub fn spawn_future<F>(future: F)
where
F: Future + Send + 'static,
F::Output: Send + 'static,
{
tokio::spawn(future);
}
#[cfg(not(target_arch = "wasm32"))]
#[track_caller]
pub fn spawn_named_future<F>(future: F, name: &str)
where
F: Future + Send + 'static,
F::Output: Send + 'static,
{
cfg_if::cfg_if! {if #[cfg(tokio_unstable)] {
#[allow(clippy::expect_used)]
tokio::task::Builder::new().name(name).spawn(future).expect("failed to spawn future");
} else {
let _ = name;
tracing::debug!(r#"the underlying binary hasn't been built with `RUSTFLAGS="--cfg tokio_unstable"` - the future naming won't do anything"#);
spawn_future(future);
}}
}
#[macro_export]
macro_rules! spawn_future {
($future:expr) => {{
$crate::spawn_future($future)
}};
($future:expr, $name:expr) => {{
cfg_if::cfg_if! {if #[cfg(not(target_arch = "wasm32"))] {
$crate::spawn_named_future($future, $name)
} else {
let _ = $name;
$crate::spawn_future($future)
}}
}};
}
@@ -6,16 +6,18 @@ use crate::nym_api::routes::{ecash, CORE_STATUS_COUNT, SINCE_ARG};
use async_trait::async_trait;
use nym_api_requests::ecash::models::{
AggregatedCoinIndicesSignatureResponse, AggregatedExpirationDateSignatureResponse,
BatchRedeemTicketsBody, EcashBatchTicketRedemptionResponse, EcashTicketVerificationResponse,
IssuedTicketbooksChallengeCommitmentRequest, IssuedTicketbooksChallengeCommitmentResponse,
IssuedTicketbooksDataRequest, IssuedTicketbooksDataResponse, IssuedTicketbooksForCountResponse,
IssuedTicketbooksForResponse, VerifyEcashTicketBody,
BatchRedeemTicketsBody, EcashBatchTicketRedemptionResponse, EcashSignerStatusResponse,
EcashTicketVerificationResponse, IssuedTicketbooksChallengeCommitmentRequest,
IssuedTicketbooksChallengeCommitmentResponse, IssuedTicketbooksDataRequest,
IssuedTicketbooksDataResponse, IssuedTicketbooksForCountResponse, IssuedTicketbooksForResponse,
VerifyEcashTicketBody,
};
use nym_api_requests::ecash::VerificationKeyResponse;
use nym_api_requests::models::{
AnnotationResponse, ApiHealthResponse, BinaryBuildInformationOwned, ChainStatusResponse,
KeyRotationInfoResponse, LegacyDescribedMixNode, NodePerformanceResponse, NodeRefreshBody,
NymNodeDescription, PerformanceHistoryResponse, RewardedSetResponse,
AnnotationResponse, ApiHealthResponse, BinaryBuildInformationOwned, ChainBlocksStatusResponse,
ChainStatusResponse, KeyRotationInfoResponse, LegacyDescribedMixNode, NodePerformanceResponse,
NodeRefreshBody, NymNodeDescription, PerformanceHistoryResponse, RewardedSetResponse,
SignerInformationResponse,
};
use nym_api_requests::nym_nodes::{
NodesByAddressesRequestBody, NodesByAddressesResponse, PaginatedCachedNodesResponseV1,
@@ -1331,6 +1333,22 @@ pub trait NymApiClientExt: ApiClient {
.await
}
async fn get_chain_blocks_status(&self) -> Result<ChainBlocksStatusResponse, NymAPIError> {
self.get_json("/v1/network/chain-blocks-status", NO_PARAMS)
.await
}
#[instrument(level = "debug", skip(self))]
async fn get_signer_status(&self) -> Result<EcashSignerStatusResponse, NymAPIError> {
self.get_json("/v1/ecash/signer-status", NO_PARAMS).await
}
#[instrument(level = "debug", skip(self))]
async fn get_signer_information(&self) -> Result<SignerInformationResponse, NymAPIError> {
self.get_json("/v1/api-status/signer-information", NO_PARAMS)
.await
}
#[instrument(level = "debug", skip(self))]
async fn get_key_rotation_info(&self) -> Result<KeyRotationInfoResponse, NymAPIError> {
self.get_json(
@@ -8,11 +8,11 @@ use crate::nyxd::CosmWasmClient;
use async_trait::async_trait;
use cosmrs::AccountId;
use cosmwasm_std::Addr;
use nym_coconut_dkg_common::dealer::RegisteredDealerDetails;
use nym_coconut_dkg_common::types::{ChunkIndex, NodeIndex, StateAdvanceResponse};
use serde::Deserialize;
use tracing::trace;
use nym_coconut_dkg_common::dealer::RegisteredDealerDetails;
pub use nym_coconut_dkg_common::{
dealer::{DealerDetailsResponse, PagedDealerIndexResponse, PagedDealerResponse},
dealing::{
@@ -21,7 +21,9 @@ pub use nym_coconut_dkg_common::{
},
msg::QueryMsg as DkgQueryMsg,
types::{DealerDetails, DealingIndex, Epoch, EpochId, EpochState, State},
verification_key::{ContractVKShare, PagedVKSharesResponse, VkShareResponse},
verification_key::{
ContractVKShare, PagedVKSharesResponse, VerificationKeyShare, VkShareResponse,
},
};
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
@@ -139,12 +139,22 @@ impl NyxdClient<HttpClient> {
})
}
pub fn connect_with_network_details<U>(
endpoint: U,
network_details: NymNetworkDetails,
) -> Result<QueryHttpRpcNyxdClient, NyxdError>
where
U: TryInto<HttpClientUrl, Error = TendermintRpcError>,
{
let config = Config::try_from_nym_network_details(&network_details)?;
Self::connect(config, endpoint)
}
pub fn connect_to_default_env<U>(endpoint: U) -> Result<QueryHttpRpcNyxdClient, NyxdError>
where
U: TryInto<HttpClientUrl, Error = TendermintRpcError>,
{
let config = Config::try_from_nym_network_details(&NymNetworkDetails::new_from_env())?;
Self::connect(config, endpoint)
Self::connect_with_network_details(endpoint, NymNetworkDetails::new_from_env())
}
}
@@ -0,0 +1,27 @@
[package]
name = "nym-ecash-signer-check-types"
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]
semver = { workspace = true }
serde = { workspace = true, features = ["derive"] }
url = { workspace = true }
thiserror = { workspace = true }
time = { workspace = true }
tracing = { workspace = true }
utoipa = { workspace = true }
nym-coconut-dkg-common = { path = "../cosmwasm-smart-contracts/coconut-dkg" }
nym-crypto = { path = "../crypto", features = ["asymmetric"] }
[lints]
workspace = true
@@ -0,0 +1,97 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use nym_coconut_dkg_common::dealer::DealerDetails;
use nym_coconut_dkg_common::verification_key::{ContractVKShare, VerificationKeyShare};
use nym_crypto::asymmetric::ed25519;
use nym_crypto::asymmetric::ed25519::Ed25519RecoveryError;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use url::Url;
use utoipa::ToSchema;
#[derive(Debug, Error)]
pub enum MalformedDealer {
#[error("dealer at {dealer_url} has provided invalid ed25519 pubkey: {source}")]
InvalidDealerPubkey {
dealer_url: String,
source: Ed25519RecoveryError,
},
#[error("dealer at {dealer_url} has provided invalid announce url: {source}")]
InvalidDealerAddress {
dealer_url: String,
source: url::ParseError,
},
}
#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]
pub struct RawDealerInformation {
pub announce_address: String,
pub owner_address: String,
pub node_index: u64,
pub public_key: String,
pub verification_key_share: Option<VerificationKeyShare>,
pub share_verified: bool,
}
impl RawDealerInformation {
pub fn new(
dealer_details: &DealerDetails,
contract_share: Option<&ContractVKShare>,
) -> RawDealerInformation {
RawDealerInformation {
announce_address: dealer_details.announce_address.clone(),
owner_address: dealer_details.address.to_string(),
node_index: dealer_details.assigned_index,
public_key: dealer_details.ed25519_identity.clone(),
verification_key_share: contract_share.map(|s| s.share.clone()),
share_verified: contract_share.map(|s| s.verified).unwrap_or(false),
}
}
pub fn parse(&self) -> Result<DealerInformation, MalformedDealer> {
Ok(DealerInformation {
announce_address: self.announce_address.parse().map_err(|source| {
MalformedDealer::InvalidDealerAddress {
dealer_url: self.announce_address.clone(),
source,
}
})?,
owner_address: self.owner_address.clone(),
node_index: self.node_index,
public_key: self.public_key.parse().map_err(|source| {
MalformedDealer::InvalidDealerPubkey {
dealer_url: self.announce_address.clone(),
source,
}
})?,
verification_key_share: self.verification_key_share.clone(),
share_verified: self.share_verified,
})
}
}
#[derive(Debug)]
pub struct DealerInformation {
pub announce_address: Url,
pub owner_address: String,
pub node_index: u64,
pub public_key: ed25519::PublicKey,
// no need to parse it into the full type as it doesn't get us anything
pub verification_key_share: Option<VerificationKeyShare>,
pub share_verified: bool,
}
impl From<DealerInformation> for RawDealerInformation {
fn from(d: DealerInformation) -> Self {
RawDealerInformation {
announce_address: d.announce_address.to_string(),
owner_address: d.owner_address,
node_index: d.node_index,
public_key: d.public_key.to_base58_string(),
verification_key_share: d.verification_key_share,
share_verified: d.share_verified,
}
}
}
@@ -0,0 +1,127 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use nym_coconut_dkg_common::types::EpochId;
use nym_coconut_dkg_common::verification_key::VerificationKeyShare;
use nym_crypto::asymmetric::ed25519;
use std::time::Duration;
use time::OffsetDateTime;
use tracing::{debug, warn};
pub trait Verifiable {
fn verify_signature(&self, pub_key: &ed25519::PublicKey) -> bool;
}
pub trait TimestampedResponse {
fn timestamp(&self) -> OffsetDateTime;
}
pub trait LegacyChainResponse {
fn chain_synced(&self, now: OffsetDateTime, stall_threshold: Duration) -> bool;
}
pub trait ChainResponse: Verifiable + TimestampedResponse {
fn chain_synced(&self) -> bool;
fn chain_available(
&self,
pub_key: &ed25519::PublicKey,
now: OffsetDateTime,
stale_response_threshold: Duration,
) -> bool {
if !self.verify_signature(pub_key) {
warn!("failed signature verification on chain status response");
return false;
}
// we rely on information provided from the api itself AS LONG AS it's not too outdated
if self.timestamp() + stale_response_threshold < now {
return false;
}
self.chain_synced()
}
}
pub trait LegacySignerResponse {
fn signer_identity(&self) -> &str;
fn signer_verification_key(&self) -> &Option<String>;
fn unprovable_signing_available(
&self,
pub_key: &ed25519::PublicKey,
expected_verification_key: Option<VerificationKeyShare>,
share_verified: bool,
) -> bool {
if self.signer_identity() != pub_key.to_base58_string() {
warn!("mismatched identity key on the legacy response");
return false;
}
// the contract share hasn't been verified yet, so we're probably in the middle of DKG
// thus if there's a bit of desync in the state, it's fine
if !share_verified {
return true;
}
if self.signer_verification_key() != &expected_verification_key {
warn!("mismatched [ecash] verification key on the legacy response");
return false;
}
true
}
}
pub trait SignerResponse: Verifiable + TimestampedResponse {
fn has_signing_keys(&self) -> bool;
fn signer_disabled(&self) -> bool;
fn is_ecash_signer(&self) -> bool;
fn dkg_ecash_epoch_id(&self) -> EpochId;
fn provable_signing_available(
&self,
pub_key: &ed25519::PublicKey,
dkg_epoch_id: EpochId,
now: OffsetDateTime,
stale_response_threshold: Duration,
) -> bool {
if !self.verify_signature(pub_key) {
warn!("failed signature verification on chain status response");
return false;
}
// we rely on information provided from the api itself AS LONG AS it's not too outdated
if self.timestamp() + stale_response_threshold < now {
return false;
}
if !self.has_signing_keys() {
debug!("missing signing keys");
return false;
}
if self.signer_disabled() {
debug!("signer functionalities explicitly disabled");
return false;
}
if !self.is_ecash_signer() {
debug!("signer doesn't recognise it's a signer for this epoch");
return false;
}
if dkg_epoch_id != self.dkg_ecash_epoch_id() {
debug!(
"mismatched dkg epoch id. current: {dkg_epoch_id}, signer's: {}",
self.dkg_ecash_epoch_id()
);
return false;
}
true
}
}
@@ -0,0 +1,6 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
pub mod dealer_information;
pub mod helper_traits;
pub mod status;
@@ -0,0 +1,303 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::dealer_information::RawDealerInformation;
use crate::helper_traits::{
ChainResponse, LegacyChainResponse, LegacySignerResponse, SignerResponse,
};
use nym_coconut_dkg_common::types::EpochId;
use nym_coconut_dkg_common::verification_key::VerificationKeyShare;
use nym_crypto::asymmetric::ed25519;
use serde::{Deserialize, Serialize};
use std::time::Duration;
use time::OffsetDateTime;
use utoipa::ToSchema;
pub(crate) const CHAIN_STALL_THRESHOLD: Duration = Duration::from_secs(5 * 60);
pub(crate) const STALE_RESPONSE_THRESHOLD: Duration = Duration::from_secs(5 * 60);
// the reason for generics is not to remove duplication of code,
// but because without them, we'd be having problems with circular dependencies,
// i.e. nym-api-requests depending on ecash-signer-check-types and
// ecash-signer-check-types needing nym-api-requests
#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]
pub enum Status<L, T> {
/// The API, even though it reports correct version, did not response to the status query
Unreachable,
/// The API is running an outdated version that does not expose the required endpoint
Outdated,
/// Response to the legacy (unsigned) status query
ReachableLegacy { response: Box<L> },
/// Response to the current (signed) status query
Reachable { response: Box<T> },
}
impl<L, T> Status<L, T>
where
L: LegacyChainResponse,
T: ChainResponse,
{
pub fn chain_available(&self, pub_key: ed25519::PublicKey) -> bool {
let now = OffsetDateTime::now_utc();
match self {
Status::Unreachable | Status::Outdated => false,
Status::ReachableLegacy { response } => {
response.chain_synced(now, CHAIN_STALL_THRESHOLD)
}
Status::Reachable { response } => {
response.chain_available(&pub_key, now, STALE_RESPONSE_THRESHOLD)
}
}
}
pub fn chain_provably_stalled(&self, pub_key: ed25519::PublicKey) -> bool {
let now = OffsetDateTime::now_utc();
match self {
Status::Unreachable | Status::Outdated | Status::ReachableLegacy { .. } => false,
Status::Reachable { response } => {
!response.chain_available(&pub_key, now, STALE_RESPONSE_THRESHOLD)
}
}
}
pub fn chain_unprovably_stalled(&self) -> bool {
let now = OffsetDateTime::now_utc();
match self {
Status::Unreachable | Status::Outdated | Status::Reachable { .. } => false,
Status::ReachableLegacy { response } => {
!response.chain_synced(now, CHAIN_STALL_THRESHOLD)
}
}
}
}
impl<L, T> Status<L, T>
where
L: LegacySignerResponse,
T: SignerResponse,
{
pub fn signing_available(
&self,
pub_key: ed25519::PublicKey,
dkg_epoch_id: u64,
expected_verification_key: Option<VerificationKeyShare>,
share_verified: bool,
) -> bool {
let now = OffsetDateTime::now_utc();
match self {
Status::Unreachable | Status::Outdated => false,
Status::ReachableLegacy { response } => response.unprovable_signing_available(
&pub_key,
expected_verification_key,
share_verified,
),
Status::Reachable { response } => response.provable_signing_available(
&pub_key,
dkg_epoch_id,
now,
STALE_RESPONSE_THRESHOLD,
),
}
}
pub fn signing_provably_unavailable(
&self,
pub_key: ed25519::PublicKey,
dkg_epoch_id: EpochId,
) -> bool {
let now = OffsetDateTime::now_utc();
match self {
Status::Unreachable | Status::Outdated | Status::ReachableLegacy { .. } => false,
Status::Reachable { response } => !response.provable_signing_available(
&pub_key,
dkg_epoch_id,
now,
STALE_RESPONSE_THRESHOLD,
),
}
}
pub fn signing_unprovably_unavailable(
&self,
pub_key: ed25519::PublicKey,
expected_verification_key: Option<VerificationKeyShare>,
share_verified: bool,
) -> bool {
match self {
Status::Unreachable | Status::Outdated | Status::Reachable { .. } => false,
Status::ReachableLegacy { response } => !response.unprovable_signing_available(
&pub_key,
expected_verification_key,
share_verified,
),
}
}
}
#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]
pub struct SignerResult<LS, TS, LC, TC> {
pub dkg_epoch_id: u64,
pub information: RawDealerInformation,
pub status: SignerStatus<LS, TS, LC, TC>,
}
impl<LS, TS, LC, TC> SignerResult<LS, TS, LC, TC> {
pub fn signer_unreachable(&self) -> bool {
matches!(self.status, SignerStatus::Unreachable)
}
pub fn malformed_details(&self) -> bool {
self.information.parse().is_err()
}
}
impl<LS, TS, LC, TC> SignerResult<LS, TS, LC, TC>
where
LC: LegacyChainResponse,
TC: ChainResponse,
{
pub fn unknown_chain_status(&self) -> bool {
let Ok(_) = self.information.parse() else {
return true;
};
if let SignerStatus::Tested { .. } = &self.status {
return false;
}
true
}
pub fn chain_available(&self) -> bool {
let Ok(parsed_info) = self.information.parse() else {
return false;
};
let SignerStatus::Tested { result } = &self.status else {
return false;
};
result
.local_chain_status
.chain_available(parsed_info.public_key)
}
pub fn chain_provably_stalled(&self) -> bool {
let Ok(parsed_info) = self.information.parse() else {
return false;
};
let SignerStatus::Tested { result } = &self.status else {
return false;
};
result
.local_chain_status
.chain_provably_stalled(parsed_info.public_key)
}
pub fn chain_unprovably_stalled(&self) -> bool {
let SignerStatus::Tested { result } = &self.status else {
return false;
};
result.local_chain_status.chain_unprovably_stalled()
}
}
impl<LS, TS, LC, TC> SignerResult<LS, TS, LC, TC>
where
LS: LegacySignerResponse,
TS: SignerResponse,
{
pub fn unknown_signing_status(&self) -> bool {
let Ok(_) = self.information.parse() else {
return true;
};
if let SignerStatus::Tested { .. } = &self.status {
return false;
}
true
}
pub fn signing_available(&self) -> bool {
let Ok(parsed_info) = self.information.parse() else {
return false;
};
let SignerStatus::Tested { result } = &self.status else {
return false;
};
result.signing_status.signing_available(
parsed_info.public_key,
self.dkg_epoch_id,
parsed_info.verification_key_share,
parsed_info.share_verified,
)
}
pub fn signing_provably_unavailable(&self) -> bool {
let Ok(parsed_info) = self.information.parse() else {
return false;
};
let SignerStatus::Tested { result } = &self.status else {
return false;
};
result
.signing_status
.signing_provably_unavailable(parsed_info.public_key, self.dkg_epoch_id)
}
pub fn signing_unprovably_unavailable(&self) -> bool {
let Ok(parsed_info) = self.information.parse() else {
return false;
};
let SignerStatus::Tested { result } = &self.status else {
return false;
};
result.signing_status.signing_unprovably_unavailable(
parsed_info.public_key,
parsed_info.verification_key_share,
parsed_info.share_verified,
)
}
}
#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]
pub enum SignerStatus<LS, TS, LC, TC> {
Unreachable,
ProvidedInvalidDetails,
Tested {
result: SignerTestResult<LS, TS, LC, TC>,
},
}
impl<LS, TS, LC, TC> SignerStatus<LS, TS, LC, TC> {
pub fn with_details(
self,
information: impl Into<RawDealerInformation>,
dkg_epoch_id: u64,
) -> SignerResult<LS, TS, LC, TC> {
SignerResult {
dkg_epoch_id,
status: self,
information: information.into(),
}
}
}
#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]
pub struct SignerTestResult<LS, TS, LC, TC> {
pub reported_version: String,
pub signing_status: Status<LS, TS>,
pub local_chain_status: Status<LC, TC>,
}
+27
View File
@@ -0,0 +1,27 @@
[package]
name = "nym-ecash-signer-check"
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]
futures = { workspace = true }
thiserror = { workspace = true }
semver = { workspace = true }
tokio = { workspace = true, features = ["time"] }
tracing = { workspace = true }
url = { workspace = true }
nym-validator-client = { path = "../client-libs/validator-client" }
nym-network-defaults = { path = "../network-defaults" }
nym-ecash-signer-check-types = { path = "../ecash-signer-check-types" }
[lints]
workspace = true
@@ -0,0 +1,225 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::{LocalChainStatus, SigningStatus, TypedSignerResult};
use nym_ecash_signer_check_types::dealer_information::RawDealerInformation;
use nym_ecash_signer_check_types::status::{SignerStatus, SignerTestResult};
use nym_validator_client::client::NymApiClientExt;
use nym_validator_client::models::BinaryBuildInformationOwned;
use nym_validator_client::nyxd::contract_traits::dkg_query_client::{
ContractVKShare, DealerDetails,
};
use nym_validator_client::NymApiClient;
use std::time::Duration;
use tracing::{error, warn};
use url::Url;
pub(crate) mod chain_status {
// Dorina
pub(crate) const MINIMUM_VERSION_LEGACY: semver::Version = semver::Version::new(1, 1, 51);
// Gruyere
pub(crate) const MINIMUM_VERSION: semver::Version = semver::Version::new(1, 1, 64);
}
pub(crate) mod signing_status {
// Magura (possibly earlier)
pub(crate) const MINIMUM_LEGACY_VERSION: semver::Version = semver::Version::new(1, 1, 46);
// Gruyere
pub(crate) const MINIMUM_VERSION: semver::Version = semver::Version::new(1, 1, 64);
}
struct ClientUnderTest {
api_client: NymApiClient,
build_info: Option<BinaryBuildInformationOwned>,
}
impl ClientUnderTest {
pub(crate) fn new(api_url: &Url) -> Self {
ClientUnderTest {
api_client: NymApiClient::new(api_url.clone()),
build_info: None,
}
}
pub(crate) async fn try_retrieve_build_information(&mut self) -> bool {
match tokio::time::timeout(
Duration::from_secs(5),
self.api_client.nym_api.build_information(),
)
.await
{
Ok(Ok(build_information)) => {
self.build_info = Some(build_information);
true
}
Ok(Err(err)) => {
warn!("{}: failed to retrieve build information: {err}. the signer is most likely down", self.api_client.api_url());
false
}
Err(_timeout) => {
warn!(
"{}: timed out while attempting to retrieve build information",
self.api_client.api_url()
);
false
}
}
}
pub(crate) fn version(&self) -> Option<semver::Version> {
self.build_info.as_ref().and_then(|build_info| {
build_info
.build_version
.parse()
.inspect_err(|err| {
error!(
"ecash signer '{}' reports invalid version {}: {err}",
self.api_client.api_url(),
build_info.build_version
)
})
.ok()
})
}
pub(crate) fn supports_legacy_signing_status_query(&self) -> bool {
let Some(version) = self.version() else {
return false;
};
version >= signing_status::MINIMUM_LEGACY_VERSION
}
pub(crate) fn supports_signing_status_query(&self) -> bool {
let Some(version) = self.version() else {
return false;
};
version >= signing_status::MINIMUM_VERSION
}
pub(crate) fn supports_chain_status_query(&self) -> bool {
let Some(version) = self.version() else {
return false;
};
version >= chain_status::MINIMUM_VERSION
}
pub(crate) fn supports_legacy_chain_status_query(&self) -> bool {
let Some(version) = self.version() else {
return false;
};
version >= chain_status::MINIMUM_VERSION_LEGACY
}
pub(crate) async fn check_local_chain(&self) -> LocalChainStatus {
// check if it at least supports legacy query
if !self.supports_legacy_chain_status_query() {
return LocalChainStatus::Outdated;
}
// check if it supports the current query
if self.supports_chain_status_query() {
return match self.api_client.nym_api.get_chain_blocks_status().await {
Ok(status) => LocalChainStatus::Reachable {
response: Box::new(status),
},
Err(err) => {
warn!(
"{}: failed to retrieve local chain status: {err}",
self.api_client.api_url()
);
LocalChainStatus::Unreachable
}
};
}
// fallback to the legacy query
match self.api_client.nym_api.get_chain_status().await {
Ok(status) => LocalChainStatus::ReachableLegacy {
response: Box::new(status),
},
Err(err) => {
warn!(
"{}: failed to retrieve [legacy] local chain status: {err}",
self.api_client.api_url()
);
LocalChainStatus::Unreachable
}
}
}
pub(crate) async fn check_signing_status(&self) -> SigningStatus {
// check if it at least supports legacy query
if !self.supports_legacy_signing_status_query() {
return SigningStatus::Outdated;
}
// check if it supports the current query
if self.supports_signing_status_query() {
return match self.api_client.nym_api.get_signer_status().await {
Ok(response) => SigningStatus::Reachable {
response: Box::new(response),
},
Err(err) => {
warn!(
"{}: failed to retrieve signer chain status: {err}",
self.api_client.api_url()
);
SigningStatus::Unreachable
}
};
}
// fallback to the legacy query
match self.api_client.nym_api.get_signer_information().await {
Ok(status) => SigningStatus::ReachableLegacy {
response: Box::new(status),
},
Err(err) => {
warn!(
"{}: failed to retrieve [legacy] signer chain status: {err}",
self.api_client.api_url()
);
// NOTE: this might equally mean the signing is disabled
SigningStatus::Unreachable
}
}
}
}
pub(crate) async fn check_client(
dealer_details: DealerDetails,
dkg_epoch: u64,
contract_share: Option<&ContractVKShare>,
) -> TypedSignerResult {
let dealer_information = RawDealerInformation::new(&dealer_details, contract_share);
// 7. attempt to construct client instances out of them
let Ok(parsed_information) = dealer_information.parse() else {
return SignerStatus::ProvidedInvalidDetails.with_details(dealer_information, dkg_epoch);
};
let mut client = ClientUnderTest::new(&parsed_information.announce_address);
// 8. check basic connection status - can you retrieve build information?
if !client.try_retrieve_build_information().await {
return SignerStatus::Unreachable.with_details(dealer_information, dkg_epoch);
}
// 9. check perceived chain status
let local_chain_status = client.check_local_chain().await;
// 10. check signer status
let signing_status = client.check_signing_status().await;
SignerStatus::Tested {
result: SignerTestResult {
reported_version: client.version().map(|v| v.to_string()).unwrap_or_default(),
signing_status,
local_chain_status,
},
}
.with_details(dealer_information, dkg_epoch)
}
@@ -0,0 +1,80 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::SignerCheckError;
use nym_crypto::asymmetric::ed25519;
use nym_validator_client::nyxd::contract_traits::dkg_query_client::{
ContractVKShare, DealerDetails, VerificationKeyShare,
};
use url::Url;
#[derive(Debug)]
pub struct RawDealerInformation {
pub announce_address: String,
pub owner_address: String,
pub node_index: u64,
pub public_key: String,
pub verification_key_share: Option<VerificationKeyShare>,
pub share_verified: bool,
}
impl RawDealerInformation {
pub fn new(
dealer_details: &DealerDetails,
contract_share: Option<&ContractVKShare>,
) -> RawDealerInformation {
RawDealerInformation {
announce_address: dealer_details.announce_address.clone(),
owner_address: dealer_details.address.to_string(),
node_index: dealer_details.assigned_index,
public_key: dealer_details.ed25519_identity.clone(),
verification_key_share: contract_share.map(|s| s.share.clone()),
share_verified: contract_share.map(|s| s.verified).unwrap_or(false),
}
}
pub fn parse(&self) -> Result<DealerInformation, SignerCheckError> {
Ok(DealerInformation {
announce_address: self.announce_address.parse().map_err(|source| {
SignerCheckError::InvalidDealerAddress {
dealer_url: self.announce_address.clone(),
source,
}
})?,
owner_address: self.owner_address.clone(),
node_index: self.node_index,
public_key: self.announce_address.parse().map_err(|source| {
SignerCheckError::InvalidDealerPubkey {
dealer_url: self.announce_address.clone(),
source,
}
})?,
verification_key_share: self.verification_key_share.clone(),
share_verified: self.share_verified,
})
}
}
#[derive(Debug)]
pub struct DealerInformation {
pub announce_address: Url,
pub owner_address: String,
pub node_index: u64,
pub public_key: ed25519::PublicKey,
// no need to parse it into the full type as it doesn't get us anything
pub verification_key_share: Option<VerificationKeyShare>,
pub share_verified: bool,
}
impl From<DealerInformation> for RawDealerInformation {
fn from(d: DealerInformation) -> Self {
RawDealerInformation {
announce_address: d.announce_address.to_string(),
owner_address: d.owner_address,
node_index: d.node_index,
public_key: d.public_key.to_base58_string(),
verification_key_share: d.verification_key_share,
share_verified: d.share_verified,
}
}
}
+24
View File
@@ -0,0 +1,24 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use nym_validator_client::nyxd::error::NyxdError;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum SignerCheckError {
#[error("failed to connect to nyxd chain due to invalid connection details: {source}")]
InvalidNyxdConnectionDetails { source: NyxdError },
#[error("failed to query the DKG contract: {source}")]
DKGContractQueryFailure { source: NyxdError },
}
impl SignerCheckError {
pub fn invalid_nyxd_connection_details(source: NyxdError) -> Self {
Self::InvalidNyxdConnectionDetails { source }
}
pub fn dkg_contract_query_failure(source: NyxdError) -> Self {
Self::DKGContractQueryFailure { source }
}
}
+94
View File
@@ -0,0 +1,94 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::client_check::check_client;
use futures::stream::{FuturesUnordered, StreamExt};
use nym_network_defaults::NymNetworkDetails;
use nym_validator_client::nyxd::contract_traits::{DkgQueryClient, PagedDkgQueryClient};
use nym_validator_client::QueryHttpRpcNyxdClient;
use std::collections::HashMap;
use url::Url;
pub use error::SignerCheckError;
use nym_ecash_signer_check_types::status::{SignerResult, Status};
use nym_validator_client::ecash::models::EcashSignerStatusResponse;
use nym_validator_client::models::{
ChainBlocksStatusResponse, ChainStatusResponse, SignerInformationResponse,
};
mod client_check;
pub mod error;
pub type TypedSignerResult = SignerResult<
SignerInformationResponse,
EcashSignerStatusResponse,
ChainStatusResponse,
ChainBlocksStatusResponse,
>;
pub type LocalChainStatus = Status<ChainStatusResponse, ChainBlocksStatusResponse>;
pub type SigningStatus = Status<SignerInformationResponse, EcashSignerStatusResponse>;
pub struct SignersTestResult {
pub threshold: Option<u64>,
pub results: Vec<TypedSignerResult>,
}
pub async fn check_signers(
rpc_endpoint: Url,
// details such as denoms, prefixes, etc.
network_details: NymNetworkDetails,
) -> Result<SignersTestResult, SignerCheckError> {
// 1. create nyx client instance
let client = QueryHttpRpcNyxdClient::connect_with_network_details(
rpc_endpoint.as_str(),
network_details,
)
.map_err(SignerCheckError::invalid_nyxd_connection_details)?;
check_signers_with_client(&client).await
}
pub async fn check_signers_with_client<C>(client: &C) -> Result<SignersTestResult, SignerCheckError>
where
C: DkgQueryClient + Sync,
{
// 2. retrieve current dkg epoch
let dkg_epoch = client
.get_current_epoch()
.await
.map_err(SignerCheckError::dkg_contract_query_failure)?;
// 3. retrieve the dkg threshold as reference point
let threshold = client
.get_epoch_threshold(dkg_epoch.epoch_id)
.await
.map_err(SignerCheckError::dkg_contract_query_failure)?;
// 4. retrieve information on current DKG dealers (i.e. eligible signers)
let dealers = client
.get_all_current_dealers()
.await
.map_err(SignerCheckError::dkg_contract_query_failure)?;
// 5. retrieve their published keys (if available)
let shares: HashMap<_, _> = client
.get_all_verification_key_shares(dkg_epoch.epoch_id)
.await
.map_err(SignerCheckError::dkg_contract_query_failure)?
.into_iter()
.map(|share| (share.node_index, share))
.collect();
// 6. for each dealer attempt to perform the checks
let results = dealers
.into_iter()
.map(|d| {
let share = shares.get(&d.assigned_index);
check_client(d, dkg_epoch.epoch_id, share)
})
.collect::<FuturesUnordered<_>>()
.collect::<Vec<_>>()
.await;
Ok(SignersTestResult { threshold, results })
}
+73
View File
@@ -0,0 +1,73 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::chain_status::LocalChainStatus;
use crate::dealer_information::RawDealerInformation;
use crate::signing_status::SigningStatus;
use std::time::Duration;
pub(crate) const STALE_RESPONSE_THRESHOLD: Duration = Duration::from_secs(5 * 60);
#[derive(Debug)]
pub struct SignerResult {
pub dkg_epoch_id: u64,
pub information: RawDealerInformation,
pub status: SignerStatus,
}
impl SignerResult {
pub fn chain_available(&self) -> bool {
let Ok(parsed_info) = self.information.parse() else {
return false;
};
let SignerStatus::Tested { result } = &self.status else {
return false;
};
result.local_chain_status.available(parsed_info.public_key)
}
pub fn signer_available(&self) -> bool {
let Ok(parsed_info) = self.information.parse() else {
return false;
};
let SignerStatus::Tested { result } = &self.status else {
return false;
};
result.signing_status.available(
parsed_info.public_key,
self.dkg_epoch_id,
parsed_info.verification_key_share,
parsed_info.share_verified,
)
}
}
#[derive(Debug)]
pub enum SignerStatus {
Unreachable,
ProvidedInvalidDetails,
Tested { result: SignerTestResult },
}
impl SignerStatus {
pub fn with_details(
self,
information: impl Into<RawDealerInformation>,
dkg_epoch_id: u64,
) -> SignerResult {
SignerResult {
dkg_epoch_id,
status: self,
information: information.into(),
}
}
}
#[derive(Debug)]
pub struct SignerTestResult {
pub reported_version: String,
pub signing_status: SigningStatus,
pub local_chain_status: LocalChainStatus,
}
@@ -8,7 +8,7 @@ DELETE FROM wireguard_peer WHERE client_id IS NULL;
CREATE TABLE wireguard_peer_new
(
public_key TEXT NOT NULL PRIMARY KEY UNIQUE,
allowed_ips BLOB NOT NULL UNIQUE,
allowed_ips BLOB NOT NULL,
client_id INTEGER REFERENCES clients(id) NOT NULL
);
+57 -28
View File
@@ -6,9 +6,9 @@
//! The resolver itself is the set combination of the google, cloudflare, and quad9 endpoints
//! supporting DoH and DoT.
//!
//! This resolver implements a fallback mechanism where, should the DNS-over-TLS resolution fail, a
//! This resolver supports a fallback mechanism where, should the DNS-over-TLS resolution fail, a
//! followup resolution will be done using the hosts configured default (e.g. `/etc/resolve.conf` on
//! linux).
//! linux). This is disabled by default and can be enabled using [`enable_system_fallback`].
//!
//! Requires the `dns-over-https-rustls`, `webpki-roots` feature for the
//! `hickory-resolver` crate
@@ -93,14 +93,14 @@ pub struct HickoryDnsResolver {
// Tokio Runtime in initialization, so we must delay the actual
// construction of the resolver.
state: Arc<OnceCell<TokioResolver>>,
fallback: Arc<OnceCell<TokioResolver>>,
fallback: Option<Arc<OnceCell<TokioResolver>>>,
dont_use_shared: bool,
}
impl Resolve for HickoryDnsResolver {
fn resolve(&self, name: Name) -> Resolving {
let resolver = self.state.clone();
let fallback = self.fallback.clone();
let maybe_fallback = self.fallback.clone();
let independent = self.dont_use_shared;
Box::pin(async move {
let resolver = resolver.get_or_try_init(|| {
@@ -117,23 +117,30 @@ impl Resolve for HickoryDnsResolver {
let lookup = match resolver.lookup_ip(name.as_str()).await {
Ok(res) => res,
Err(e) => {
// on failure use the fall back system configured DNS resolver
if !e.is_no_records_found() {
warn!("primary DNS failed w/ error {e}: using system fallback");
}
let resolver = fallback.get_or_try_init(|| {
// using a closure here is slightly gross, but this makes sure that if the
// lazy-init returns an error it can be handled by the client
if independent {
new_resolver_system()
} else {
Ok(SHARED_RESOLVER
.fallback
.get_or_try_init(new_resolver_system)?
.clone())
if let Some(ref fallback) = maybe_fallback {
// on failure use the fall back system configured DNS resolver
if !e.is_no_records_found() {
warn!("primary DNS failed w/ error {e}: using system fallback");
}
})?;
resolver.lookup_ip(name.as_str()).await?
let resolver = fallback.get_or_try_init(|| {
// using a closure here is slightly gross, but this makes sure that if the
// lazy-init returns an error it can be handled by the client
if independent {
new_resolver_system()
} else {
Ok(SHARED_RESOLVER
.fallback
.as_ref()
.ok_or(e)? // if the shared resolver has no fallback return the original error
.get_or_try_init(new_resolver_system)?
.clone())
}
})?;
resolver.lookup_ip(name.as_str()).await?
} else {
return Err(e.into());
}
}
};
@@ -162,14 +169,17 @@ impl HickoryDnsResolver {
let lookup = match resolver.lookup_ip(name).await {
Ok(res) => res,
Err(e) => {
// on failure use the fall back system configured DNS resolver
if !e.is_no_records_found() {
warn!("primary DNS failed w/ error {e}: using system fallback");
if let Some(ref fallback) = self.fallback {
// on failure use the fall back system configured DNS resolver
if !e.is_no_records_found() {
warn!("primary DNS failed w/ error {e}: using system fallback");
}
let resolver = fallback.get_or_try_init(|| self.new_resolver_system())?;
resolver.lookup_ip(name).await?
} else {
return Err(e.into());
}
let resolver = self
.fallback
.get_or_try_init(|| self.new_resolver_system())?;
resolver.lookup_ip(name).await?
}
};
@@ -193,15 +203,34 @@ impl HickoryDnsResolver {
}
fn new_resolver_system(&self) -> Result<TokioResolver, HickoryDnsError> {
if self.dont_use_shared {
if self.dont_use_shared || SHARED_RESOLVER.fallback.is_none() {
new_resolver_system()
} else {
Ok(SHARED_RESOLVER
.fallback
.as_ref()
.unwrap()
.get_or_try_init(new_resolver_system)?
.clone())
}
}
/// Enable fallback to the system default resolver if the primary (DoX) resolver fails
pub fn enable_system_fallback(&mut self) -> Result<(), HickoryDnsError> {
self.fallback = Some(Default::default());
let _ = self
.fallback
.as_ref()
.unwrap()
.get_or_try_init(new_resolver_system)?;
Ok(())
}
/// Disable fallback resolution. If the primary resolver fails the error is
/// returned immediately
pub fn disable_system_fallback(&mut self) {
self.fallback = None;
}
}
/// Create a new resolver with a custom DoT based configuration. The options are overridden to look
+1 -1
View File
@@ -16,7 +16,7 @@ pub struct UserAgent {
pub version: String,
/// client platform
pub platform: String,
/// source commit version for the calling calling crate / subsystem
/// source commit version for the calling crate / subsystem
pub git_commit: String,
}
@@ -146,6 +146,12 @@ pub struct OutputParams {
pub output: Option<Output>,
}
impl OutputParams {
pub fn get_output(&self) -> Output {
self.output.unwrap_or_default()
}
}
impl Output {
pub fn to_response<T: Serialize>(self, data: T) -> FormattedResponse<T> {
match self {
+1 -1
View File
@@ -233,7 +233,7 @@ where
let dkg_query_client = if self.config.base.client.disabled_credentials_mode {
None
} else {
Some(default_query_dkg_client_from_config(&self.config.base))
Some(default_query_dkg_client_from_config(&self.config.base)?)
};
let mut base_builder =
+5
View File
@@ -265,6 +265,7 @@ impl ShutdownManager {
}
#[must_use]
#[track_caller]
pub fn with_shutdown<F>(mut self, shutdown: F) -> Self
where
F: Future<Output = ()>,
@@ -281,6 +282,7 @@ impl ShutdownManager {
}
#[cfg(unix)]
#[track_caller]
pub fn with_shutdown_signal(self, signal_kind: SignalKind) -> std::io::Result<Self> {
let mut sig = signal(signal_kind)?;
Ok(self.with_shutdown(async move {
@@ -289,6 +291,7 @@ impl ShutdownManager {
}
#[cfg(not(target_arch = "wasm32"))]
#[track_caller]
pub fn with_interrupt_signal(self) -> Self {
self.with_shutdown(async move {
let _ = tokio::signal::ctrl_c().await;
@@ -296,11 +299,13 @@ impl ShutdownManager {
}
#[cfg(unix)]
#[track_caller]
pub fn with_terminate_signal(self) -> std::io::Result<Self> {
self.with_shutdown_signal(SignalKind::terminate())
}
#[cfg(unix)]
#[track_caller]
pub fn with_quit_signal(self) -> std::io::Result<Self> {
self.with_shutdown_signal(SignalKind::quit())
}
+31
View File
@@ -0,0 +1,31 @@
[package]
name = "zulip-client"
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]
thiserror = { workspace = true }
itertools = { workspace = true }
url = { workspace = true, features = ["serde"] }
serde = { workspace = true, features = ["derive"] }
zeroize = { workspace = true }
nym-bin-common = { path = "../bin-common" }
nym-http-api-client = { path = "../http-api-client" }
reqwest = { workspace = true }
tracing = { workspace = true }
[dev-dependencies]
tokio = { workspace = true, features = ["full"] }
serde_json = { workspace = true }
[lints]
workspace = true
+151
View File
@@ -0,0 +1,151 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
//! An incomplete Zulip API Client
//!
//! Currently, it serves a single purpose: to send a message to a server,
//! however, it could very easily be extended with additional functionalities.
//!
//! ## Sending Direct Message
//!
//! ```rust
//! # use zulip_client::{Client, ZulipClientError};
//! # use zulip_client::message::DirectMessage;
//! # async fn try_send() -> Result<(), ZulipClientError> {
//! let api_key = "your-api-key";
//! let email = "associated-email-address";
//! let server = "https://server-address.com";
//! let client = Client::builder(email, api_key, server)?.build()?;
//! // send to userid 12
//! client.send_message((12u32, "hello world")).await?;
//! // more concrete typing
//! client.send_message(DirectMessage::new(12, "hello world2")).await?;
//! # Ok(())
//! # }
//! ```
use crate::error::ZulipClientError;
use crate::message::{SendMessageResponse, SendableMessage};
use nym_bin_common::bin_info;
use nym_http_api_client::UserAgent;
use reqwest::{header, Method, RequestBuilder};
use serde::{Deserialize, Serialize};
use std::str::FromStr;
use tracing::trace;
use url::Url;
use zeroize::Zeroizing;
#[derive(Serialize, Deserialize)]
pub struct ClientConfig {
pub user_email: String,
pub api_key: String,
// TODO: introduce validation
pub user_agent: Option<String>,
pub server_url: Url,
}
pub struct Client {
server_url: Url,
api_key: Zeroizing<String>,
user_email: String,
inner_client: reqwest::Client,
}
fn default_user_agent() -> String {
UserAgent::from(bin_info!()).to_string()
}
impl Client {
const MESSAGES_ENDPOINT: &'static str = "/api/v1/messages";
pub fn builder(
user_email: impl Into<String>,
api_key: impl Into<String>,
server_url: impl Into<String>,
) -> Result<ClientBuilder, ZulipClientError> {
ClientBuilder::new(user_email, api_key, server_url)
}
pub fn new(config: ClientConfig) -> Result<Self, ZulipClientError> {
let builder = ClientBuilder::new(config.user_email, config.api_key, config.server_url)?;
match config.user_agent {
Some(user_agent) => builder.user_agent(user_agent).build(),
None => builder.build(),
}
}
pub async fn send_message(
&self,
msg: impl Into<SendableMessage>,
) -> Result<SendMessageResponse, ZulipClientError> {
let url = format!("{}{}", self.server_url, Self::MESSAGES_ENDPOINT);
self.build_request(Method::POST, Self::MESSAGES_ENDPOINT)
.form(&msg.into())
.send()
.await
.map_err(|source| ZulipClientError::RequestSendingFailure { source, url })?
.json()
.await
.map_err(|source| ZulipClientError::RequestDecodeFailure { source })
}
fn build_request(&self, method: Method, endpoint: &'static str) -> RequestBuilder {
let url = format!("{}{endpoint}", self.server_url);
trace!("posting to {url}");
self.inner_client
.request(method, url)
.basic_auth(&self.user_email, Some(self.api_key.to_string()))
.header(header::CONTENT_TYPE, "application/x-www-form-urlencoded")
}
}
pub struct ClientBuilder {
api_key: Zeroizing<String>,
user_email: String,
server_url: Url,
user_agent: Option<String>,
}
impl ClientBuilder {
pub fn new(
user_email: impl Into<String>,
api_key: impl Into<String>,
server_url: impl Into<String>,
) -> Result<Self, ZulipClientError> {
let server_url = server_url.into();
let parsed_url =
Url::from_str(&server_url).map_err(|source| ZulipClientError::MalformedServerUrl {
raw: server_url,
source,
})?;
Ok(ClientBuilder {
api_key: Zeroizing::new(api_key.into()),
user_email: user_email.into(),
server_url: parsed_url,
user_agent: None,
})
}
#[must_use]
pub fn user_agent(mut self, user_agent: impl Into<String>) -> Self {
self.user_agent = Some(user_agent.into());
self
}
pub fn build(self) -> Result<Client, ZulipClientError> {
let user_agent = self.user_agent.unwrap_or_else(default_user_agent);
Ok(Client {
api_key: self.api_key,
server_url: self.server_url,
user_email: self.user_email,
inner_client: reqwest::ClientBuilder::new()
.user_agent(user_agent)
.build()
.map_err(|source| ZulipClientError::ClientBuildFailure { source })?,
})
}
}
+22
View File
@@ -0,0 +1,22 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use thiserror::Error;
#[derive(Error, Debug)]
pub enum ZulipClientError {
#[error("failed to send request to {url}: {source}")]
RequestSendingFailure { url: String, source: reqwest::Error },
#[error("failed to decode received response: {source}")]
RequestDecodeFailure { source: reqwest::Error },
#[error("failed to build internal client: {source}")]
ClientBuildFailure { source: reqwest::Error },
#[error("provided url ({raw}) is malformed: {source}")]
MalformedServerUrl {
raw: String,
source: url::ParseError,
},
}
+11
View File
@@ -0,0 +1,11 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
pub mod client;
pub mod error;
pub mod message;
pub type Id = u32;
pub use client::{Client, ClientBuilder};
pub use error::ZulipClientError;
+215
View File
@@ -0,0 +1,215 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use crate::message::to::{ToChannel, ToDirect};
use serde::{Deserialize, Serialize};
pub mod to;
#[derive(Serialize, Deserialize, Debug)]
#[serde(tag = "result")]
#[serde(rename_all = "snake_case")]
pub enum SendMessageResponse {
Success {
id: i64,
automatic_new_visibility_policy: Option<i64>,
msg: String,
},
Error {
code: String,
msg: String,
stream: Option<String>,
},
}
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[serde(tag = "type")]
pub enum SendableMessageContent {
// old name: 'private'
Direct {
// internally this is a list
to: String,
content: String,
},
// alternative name: 'channel'
Stream {
to: String,
topic: Option<String>,
content: String,
},
}
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct SendableMessage {
#[serde(flatten)]
content: SendableMessageContent,
/// For clients supporting local echo, the event queue ID for the client.
/// If passed, `local_id` is required. If the message is successfully sent,
/// the server will include `local_id` in the message event that the client with this `queue_id`
/// will receive notifying it of the new message via `GET /events`.
/// This lets the client know unambiguously that it should replace the locally echoed message,
/// rather than adding this new message
/// (which would be correct if the user had sent the new message from another device).
/// example: "fb67bf8a-c031-47cc-84cf-ed80accacda8"
queue_id: Option<String>,
/// For clients supporting local echo, a unique string-format identifier chosen freely by the client;
/// the server will pass it back to the client without inspecting it, as described in the `queue_id` description.
/// example: "100.01"
local_id: Option<String>,
/// Whether the message should be initially marked read by its sender.
/// If unspecified, the server uses a heuristic based on the client name.
read_by_sender: bool,
}
impl SendableMessage {
pub fn new(content: impl Into<SendableMessageContent>) -> Self {
SendableMessage {
content: content.into(),
queue_id: None,
local_id: None,
read_by_sender: false,
}
}
#[must_use]
pub fn with_queue(mut self, queue_id: impl Into<String>, local_id: impl Into<String>) -> Self {
self.queue_id = Some(queue_id.into());
self.local_id = Some(local_id.into());
self
}
#[must_use]
pub fn read_by_sender(mut self, read_by_sender: bool) -> Self {
self.read_by_sender = read_by_sender;
self
}
}
pub type PrivateMessage = DirectMessage;
pub struct DirectMessage {
to: String,
content: String,
}
impl DirectMessage {
pub fn new(to: impl Into<ToDirect>, content: impl Into<String>) -> Self {
DirectMessage {
to: to.into().to_string(),
content: content.into(),
}
}
}
pub type ChannelMessage = StreamMessage;
pub struct StreamMessage {
to: String,
topic: Option<String>,
content: String,
}
impl StreamMessage {
pub fn new(
to: impl Into<ToChannel>,
content: impl Into<String>,
topic: Option<String>,
) -> Self {
StreamMessage {
to: to.into().to_string(),
topic,
content: content.into(),
}
}
pub fn no_topic(to: impl Into<ToChannel>, content: impl Into<String>) -> Self {
Self::new(to, content, None)
}
#[must_use]
pub fn with_topic(mut self, topic: impl Into<String>) -> Self {
self.topic = Some(topic.into());
self
}
}
impl From<SendableMessageContent> for SendableMessage {
fn from(content: SendableMessageContent) -> Self {
SendableMessage::new(content)
}
}
impl From<DirectMessage> for SendableMessage {
fn from(msg: DirectMessage) -> Self {
SendableMessageContent::from(msg).into()
}
}
impl From<DirectMessage> for SendableMessageContent {
fn from(msg: DirectMessage) -> Self {
SendableMessageContent::Direct {
to: msg.to,
content: msg.content,
}
}
}
impl<T, S> From<(T, S)> for DirectMessage
where
T: Into<ToDirect>,
S: Into<String>,
{
fn from((to, content): (T, S)) -> Self {
DirectMessage::new(to, content)
}
}
impl<T, S> From<(T, S)> for SendableMessage
where
T: Into<ToDirect>,
S: Into<String>,
{
fn from((to, content): (T, S)) -> Self {
DirectMessage::new(to, content).into()
}
}
impl From<StreamMessage> for SendableMessage {
fn from(msg: StreamMessage) -> Self {
SendableMessageContent::from(msg).into()
}
}
impl From<StreamMessage> for SendableMessageContent {
fn from(msg: StreamMessage) -> Self {
SendableMessageContent::Stream {
to: msg.to,
topic: msg.topic,
content: msg.content,
}
}
}
impl<T, S> From<(T, S, Option<S>)> for StreamMessage
where
T: Into<ToChannel>,
S: Into<String>,
{
fn from((to, content, topic): (T, S, Option<S>)) -> Self {
StreamMessage::new(to, content, topic.map(Into::into))
}
}
impl<T, S> From<(T, S, Option<S>)> for SendableMessage
where
T: Into<ToChannel>,
S: Into<String>,
{
fn from(inner: (T, S, Option<S>)) -> Self {
StreamMessage::from(inner).into()
}
}
+128
View File
@@ -0,0 +1,128 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use crate::Id;
use itertools::Itertools;
use std::fmt::Display;
// from the docs:
// For channel messages, either the name or integer ID of the channel.
// For direct messages, either a list containing integer user IDs
// or a list containing string Zulip API email addresses.
pub enum ToDirect {
ByIds(Vec<Id>),
ByNames(Vec<String>),
}
impl Display for ToDirect {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ToDirect::ByIds(ids) => write!(f, "[{}]", ids.iter().join(",")),
ToDirect::ByNames(names) => {
write!(f, "[{}]", names.join(","))
}
}
}
}
impl From<Vec<String>> for ToDirect {
fn from(names: Vec<String>) -> Self {
ToDirect::ByNames(names)
}
}
impl From<&[String]> for ToDirect {
fn from(names: &[String]) -> Self {
names.to_vec().into()
}
}
impl From<&[&str]> for ToDirect {
fn from(names: &[&str]) -> Self {
names
.iter()
.map(|s| s.to_string())
.collect::<Vec<_>>()
.into()
}
}
impl<const N: usize> From<&[&str; N]> for ToDirect {
fn from(names: &[&str; N]) -> Self {
names.as_slice().into()
}
}
impl From<Vec<&str>> for ToDirect {
fn from(names: Vec<&str>) -> Self {
names.as_slice().into()
}
}
impl From<String> for ToDirect {
fn from(name: String) -> Self {
ToDirect::ByNames(vec![name])
}
}
impl From<&str> for ToDirect {
fn from(name: &str) -> Self {
name.to_string().into()
}
}
impl From<Id> for ToDirect {
fn from(id: Id) -> Self {
ToDirect::ByIds(vec![id])
}
}
impl From<&[Id]> for ToDirect {
fn from(ids: &[Id]) -> Self {
ids.to_vec().into()
}
}
impl<const N: usize> From<&[Id; N]> for ToDirect {
fn from(ids: &[Id; N]) -> Self {
ids.as_slice().into()
}
}
impl From<Vec<Id>> for ToDirect {
fn from(ids: Vec<Id>) -> Self {
ToDirect::ByIds(ids)
}
}
pub enum ToChannel {
ByName(String),
ById(Id),
}
impl Display for ToChannel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ToChannel::ByName(name) => name.fmt(f),
ToChannel::ById(id) => id.fmt(f),
}
}
}
impl From<String> for ToChannel {
fn from(name: String) -> Self {
ToChannel::ByName(name)
}
}
impl From<&str> for ToChannel {
fn from(name: &str) -> Self {
name.to_string().into()
}
}
impl From<Id> for ToChannel {
fn from(id: Id) -> Self {
ToChannel::ById(id)
}
}
@@ -7,20 +7,32 @@ net.ipv6.conf.all.forwarding = 1
net.ipv4.ip_forward = 1
IP forwarding configured successfully.
Creating Nym exit policy chain...
Creating chain NYM-EXIT...
Creating chain NYM-EXIT in ip6tables...
Linking NYM-EXIT to FORWARD chain...
Linking NYM-EXIT to IPv6 FORWARD chain...
Chain NYM-EXIT already exists. Flushing it...
Chain NYM-EXIT already exists in ip6tables. Flushing it...
NYM-EXIT all opt -- in * out nymwg 0.0.0.0/0 -> 0.0.0.0/0
NYM-EXIT all opt in * out nymwg ::/0 -> ::/0
Setting up NAT rules...
MASQUERADE all opt -- in * out ens3 0.0.0.0/0 -> 0.0.0.0/0
IPv4 NAT rule already exists.
MASQUERADE all opt in * out ens3 ::/0 -> ::/0
IPv6 NAT rule already exists.
ACCEPT all opt -- in nymwg out ens3 0.0.0.0/0 -> 0.0.0.0/0
ACCEPT all opt -- in ens3 out nymwg 0.0.0.0/0 -> 0.0.0.0/0 state RELATED,ESTABLISHED
ACCEPT all opt in nymwg out ens3 ::/0 -> ::/0
ACCEPT all opt in ens3 out nymwg ::/0 -> ::/0 state RELATED,ESTABLISHED
Configuring DNS and ICMP rules...
Added IPv6 ICMP rule (allow ping6).
Added IPv6 DNS rule (UDP).
Added IPv6 DNS rule (TCP).
ACCEPT icmp opt -- in * out * 0.0.0.0/0 -> 0.0.0.0/0 icmptype 8
ACCEPT icmp opt -- in * out * 0.0.0.0/0 -> 0.0.0.0/0 icmptype 0
ACCEPT icmpv6 opt in * out * ::/0 -> ::/0
ACCEPT udp opt -- in * out * 0.0.0.0/0 -> 0.0.0.0/0 udp dpt:53
ACCEPT tcp opt -- in * out * 0.0.0.0/0 -> 0.0.0.0/0 tcp dpt:53
ACCEPT udp opt in * out * ::/0 -> ::/0 udp dpt:53
ACCEPT tcp opt in * out * ::/0 -> ::/0 tcp dpt:53
Applying Spamhaus blocklist...
Downloading exit policy from https://nymtech.net/.wellknown/network-requester/exit-policy.txt
Processing 429 blocklist rules...
REJECT all opt -- in * out * 0.0.0.0/0 -> 205.189.71.0/24 reject-with icmp-port-unreachable
REJECT all opt -- in * out * 0.0.0.0/0 -> 205.189.72.0/23 reject-with icmp-port-unreachable
Blocklist applied successfully.
Applying allowed ports...
Adding rules for SILC (Port: 706)
@@ -103,6 +115,11 @@ Adding rules for POP3OverTLS (Port: 995)
Added: NYM-EXIT tcp port 995
Added: NYM-EXIT udp port 995
Added: NYM-EXIT udp port 995
Adding rules for DarkFiTor (Port: 25551)
Added: NYM-EXIT tcp port 25551
Added: NYM-EXIT tcp port 25551
Added: NYM-EXIT udp port 25551
Added: NYM-EXIT udp port 25551
Adding rules for MMCC (Port: 5050)
Added: NYM-EXIT tcp port 5050
Added: NYM-EXIT tcp port 5050
@@ -268,6 +285,11 @@ Adding rules for Mumble (Port: 64738)
Added: NYM-EXIT tcp port 64738
Added: NYM-EXIT udp port 64738
Added: NYM-EXIT udp port 64738
Adding rules for DarkFi (Port: 26661)
Added: NYM-EXIT tcp port 26661
Added: NYM-EXIT tcp port 26661
Added: NYM-EXIT udp port 26661
Added: NYM-EXIT udp port 26661
Adding rules for PPTP (Port: 1723)
Added: NYM-EXIT tcp port 1723
Added: NYM-EXIT tcp port 1723
@@ -333,6 +355,11 @@ Adding rules for Kpasswd (Port: 464)
Added: NYM-EXIT tcp port 464
Added: NYM-EXIT udp port 464
Added: NYM-EXIT udp port 464
Adding rules for MoneroRPC (Port: 18089)
Added: NYM-EXIT tcp port 18089
Added: NYM-EXIT tcp port 18089
Added: NYM-EXIT udp port 18089
Added: NYM-EXIT udp port 18089
Adding rules for RemoteHTTPS (Port: 981)
Added: NYM-EXIT tcp port 981
Added: NYM-EXIT tcp port 981
@@ -398,6 +425,11 @@ Adding rules for GroupWise (Port: 1677)
Added: NYM-EXIT tcp port 1677
Added: NYM-EXIT udp port 1677
Added: NYM-EXIT udp port 1677
Adding rules for Monero (Port: 18080-18081)
Added: NYM-EXIT tcp port range 18080:18081
Added: NYM-EXIT tcp port range 18080:18081
Added: NYM-EXIT udp port range 18080:18081
Added: NYM-EXIT udp port range 18080:18081
Adding rules for EnsimControlPanel (Port: 19638)
Added: NYM-EXIT tcp port 19638
Added: NYM-EXIT tcp port 19638
File diff suppressed because it is too large Load Diff
@@ -2,41 +2,61 @@
Running Nym Exit Policy Verification Tests...
Testing Port Range Rules...
Testing FTP tcp port range 20-21
ACCEPT tcp opt -- in * out * 0.0.0.0/0 -> 0.0.0.0/0 tcp dpts:20:21
✓ Rule exists: NYM-EXIT tcp port range 20:21
Testing HTTP tcp port range 80-81
ACCEPT tcp opt -- in * out * 0.0.0.0/0 -> 0.0.0.0/0 tcp dpts:80:81
✓ Rule exists: NYM-EXIT tcp port range 80:81
Testing CPanel tcp port range 2082-2083
ACCEPT tcp opt -- in * out * 0.0.0.0/0 -> 0.0.0.0/0 tcp dpts:2082:2083
✓ Rule exists: NYM-EXIT tcp port range 2082:2083
Testing XMPP tcp port range 5222-5223
ACCEPT tcp opt -- in * out * 0.0.0.0/0 -> 0.0.0.0/0 tcp dpts:5222:5223
✓ Rule exists: NYM-EXIT tcp port range 5222:5223
Testing Steam (sampling) tcp port range 27000-27050
ACCEPT tcp opt -- in * out * 0.0.0.0/0 -> 0.0.0.0/0 tcp dpts:27000:27050
✓ Rule exists: NYM-EXIT tcp port range 27000:27050
Testing FTP over TLS tcp port range 989-990
ACCEPT tcp opt -- in * out * 0.0.0.0/0 -> 0.0.0.0/0 tcp dpts:989:990
✓ Rule exists: NYM-EXIT tcp port range 989:990
Testing RTP/VoIP tcp port range 5000-5005
ACCEPT tcp opt -- in * out * 0.0.0.0/0 -> 0.0.0.0/0 tcp dpts:5000:5005
✓ Rule exists: NYM-EXIT tcp port range 5000:5005
Testing Simplify Media tcp port range 8087-8088
ACCEPT tcp opt -- in * out * 0.0.0.0/0 -> 0.0.0.0/0 tcp dpts:8087:8088
✓ Rule exists: NYM-EXIT tcp port range 8087:8088
Testing Zcash tcp port range 8232-8233
ACCEPT tcp opt -- in * out * 0.0.0.0/0 -> 0.0.0.0/0 tcp dpts:8232:8233
✓ Rule exists: NYM-EXIT tcp port range 8232:8233
Testing Bitcoin tcp port range 8332-8333
ACCEPT tcp opt -- in * out * 0.0.0.0/0 -> 0.0.0.0/0 tcp dpts:8332:8333
✓ Rule exists: NYM-EXIT tcp port range 8332:8333
Testing Monero tcp port range 18080-18081
ACCEPT tcp opt -- in * out * 0.0.0.0/0 -> 0.0.0.0/0 tcp dpts:18080:18081
✓ Rule exists: NYM-EXIT tcp port range 18080:18081
Test test_port_range_rules PASSED
Testing Critical Service Rules...
ACCEPT tcp opt -- in * out * 0.0.0.0/0 -> 0.0.0.0/0 tcp dpt:22
✓ Rule exists: NYM-EXIT tcp port 22
ACCEPT tcp opt -- in * out * 0.0.0.0/0 -> 0.0.0.0/0 tcp dpt:53
✓ Rule exists: NYM-EXIT tcp port 53
ACCEPT tcp opt -- in * out * 0.0.0.0/0 -> 0.0.0.0/0 tcp dpt:443
✓ Rule exists: NYM-EXIT tcp port 443
ACCEPT tcp opt -- in * out * 0.0.0.0/0 -> 0.0.0.0/0 tcp dpt:853
✓ Rule exists: NYM-EXIT tcp port 853
ACCEPT tcp opt -- in * out * 0.0.0.0/0 -> 0.0.0.0/0 tcp dpt:1194
✓ Rule exists: NYM-EXIT tcp port 1194
ACCEPT udp opt -- in * out * 0.0.0.0/0 -> 0.0.0.0/0 udp dpt:53
✓ Rule exists: NYM-EXIT udp port 53
ACCEPT udp opt -- in * out * 0.0.0.0/0 -> 0.0.0.0/0 udp dpt:123
✓ Rule exists: NYM-EXIT udp port 123
ACCEPT udp opt -- in * out * 0.0.0.0/0 -> 0.0.0.0/0 udp dpt:1194
✓ Rule exists: NYM-EXIT udp port 1194
Relevant existing rules for HTTP (port 80):
Test test_critical_services PASSED
This test takes some time, do not quit the process
Testing Default Reject Rule...
✓ Default REJECT rule exists
Test test_default_reject_rule PASSED
@@ -1 +1 @@
Tuesday, July 22nd 2025, 11:37:27 UTC
Wednesday, July 30th 2025, 09:32:50 UTC
@@ -1,21 +1,44 @@
| **ISP** | **Locations** | **Public IPv6** | **Crypto Payments** | **Comments** | **Last Updated** |
|:------------------------------------------------|:---------------------------------------------------------------------------------------------------------|:-------------------------------------|:---------------------------------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-------------------|
| [AlexHost](https://alexhost.com) | Moldova, Bulgaria, Sweden, Netherlands | Yes, on by default | Yes | They allow TOR Bridges, Relays. Exit nodes are only allowed on dedicated servers (prices start from 26 EUR) | 07/2024 |
| [BitLaunch](https://bitlaunch.io) | Canada, USA, UK | No | Yes | Expensive. Digial Ocean through BitLanch has IPv6 | 05/2024 |
| [Cherry Servers](https://www.cherryservers.com) | Lithuania, Netherlands, USA, Singapore | No | Yes | Issued IP doesnt match the location offered by the provider. | 05/2024 |
| [Flokinet](https://flokinet.is) | Netherlands, Iceland, Romania,France | Yes, needs a ticket and custom setup | yes, including XMR | Very slow customer support | 05/2024 |
| [HostSailor](https://hostsailor.com) | USA | Yes, based on ticket | Yes | The IPv6 setup needs custom research and is not documented | 05/2024 |
| [Hostiko](https://hostiko.com.ua) | Ukraine, Germany | Yes, on by default | Yes | Ukrainian provider. They allow Exit nodes on Germany boxes but limit the bandwidth, you also have to restrict certain ports like 25 and 587. Make sure you open a ticket. | 07/2024 |
| [Hostinger](https://hostinger.com) | France, Lithuania, India, USA, Brazil | Yes, out of the box | Yes | Crypto payments must be done per each server monthly or annually. | 05/2024 |
| [Hostslick](https://hostslick.com) | Netherlands, Germany | Yes, on by default | Yes | Good amount of bandwidth for the price. Make sure you open the ticket if you want to run Exit node | 07/2024 |
| [Incognet](https://incognet.io) | Netherlands and USA | Yes, on by default | Yes | They allow Tor exit nodes but you must adhere to their rules https://incognet.io/tor-exits | 07/2024 |
| [IsHosting](https://ishosting.com/en) | Brazil, Netherlands | Yes, based on ticket | Yes | Expensive | 05/2024 |
| [Linode](https://linode.com) | USA, Canada, Japan, India, Indonesia, Sweden, Netherlands, Germany, Brazil, France, UK, Australia, Italy | Yes out of the box | No, only through [BitLAunch](https://bitlaunch.io) | IPv6 sometimes need to be re-added in Networking tab, no reboot needed | 05/2024 |
| [LiteServer](https://liteserver.nl) | Netherlands | Yes, on by default | Yes | Very reliable Dutch provider. They do allow Relay nodes but for Exit nodes you need to contact them. Always check T&C https://liteserver.nl/legal | 07/2024 |
| [Mevspace](https://mevspace.com) | Poland | Yes, on by default | Yes | Flexible Polish providers with 3 DCs in Poland. They do allow Tor Exit nodes but you may need a dedicated server for this. Make sure you open a ticket to check. As of today's date, they have 48h for 1 EUR tariff | 07/2024 |
| [Misaka](https://www.misaka.io/) | South Africa | Yes, native support | No | Very Expensive | 05/2024 |
| [Njalla](https://nja.la) | Sweden | Yes | Yes | Privacy vandguards! The biggest VPS 45 is 3 cores only, but it works better than many “larger” servers on the market. | 05/2024 |
| [RDP](https://rdp.sh) | Netherlands, USA, Poland | Yes, on by default | Yes | German provider. Exit nodes are allowed, policy is here https://rdp.sh/docs/faq/tor ports 25,465,587 must be closed. Make sure you open a ticket before running an exit node. | 07/2024 |
| [TerraHost](https://terrahost.no) | Norway | Yes, on by default | Yes | Very reliable Norwegian provider. Only allow exit nodes on Dedicated servers subject to certain caveats (you must open a ticket). Always check T&C https://terrahost.no/avtalebetingelser | 07/2024 |
| [iHostArt](https://ihostart.com) | Romania | Yes, on by default | Yes | Super permissive provider. They do allow Tor Exit/Relay/Bridge. Pro-free speech etc. Recently, IPv6 geolocation was set to North Korea, so be aware. | 07/2024 |
| [vSys Host](https://vsys.host) | Ukraine, Netherlands, USA | Yes, on by default | Yes | Pretty permissive provider registered in Ukraine. Should allow Relay/Exit nodes but nothing in T&C, so better double check. | 07/2024 |
| **ISP** | **Locations** | **Public IPv6** | **Crypto Payments** | **Comments** | **Last Updated** |
|:---------------------------------------------------------------------------------------------------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------|:-------------------------------------|:---------------------------------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-------------------|
| [AlexHost](https://alexhost.com) | Moldova, Bulgaria, Sweden, Netherlands | Yes, on by default | Yes | They allow TOR Bridges, Relays. Exit nodes are only allowed on dedicated servers (prices start from 26 EUR) | 07/2024 |
| [AmeriNoc](https://www.amerinoc.com) | USA | Yes | nan | nan | 07/2025 |
| [BitLaunch](https://bitlaunch.io) | Canada, USA, UK | No | Yes | Expensive. Digial Ocean through BitLanch has IPv6 | 05/2024 |
| [Cherry Servers](https://www.cherryservers.com) | Lithuania, Netherlands, USA, Singapore | No | Yes | Issued IP doesnt match the location offered by the provider. | 05/2024 |
| [Colocall](https://www.colocall.net/) | Ukraine | Yes | nan | 07/2025 | nan |
| [DataPacket](https://www.datapacket.com/pricing) | NL, GR, SK, BE, RO, HU, DK, IE, DE, UA, PT, GB, ES, FR, IT, NO, CZ, BG, SE, AT, PL, HR, CH, USA, CO, AR, PE, MX, CL, TR, ZA, NG, IL, HK, AU, SG, JP | Yes | nan | nan | 07/2025 |
| [Dataclub](https://www.dataclub.eu/) | Latvia, Sweden, Netherlands | Yes | nan | nan | 07/2027 |
| [Flokinet](https://flokinet.is) | Netherlands, Iceland, Romania,France | Yes, needs a ticket and custom setup | yes, including XMR | Very slow customer support | 05/2024 |
| [FranTech](https://my.frantech.ca) | USA | Yes | nan | nan | 07/2025 |
| [Fsit](https://www.fsit.com/server/vps-vserver-kvm) | Swiss | Yes | Yes | nan | 07/2025 |
| [HostSailor](https://hostsailor.com) | USA | Yes, based on ticket | Yes | The IPv6 setup needs custom research and is not documented | 05/2024 |
| [Hostiko](https://hostiko.com.ua) | Ukraine, Germany | Yes, on by default | Yes | Ukrainian provider. They allow Exit nodes on Germany boxes but limit the bandwidth, you also have to restrict certain ports like 25 and 587. Make sure you open a ticket. | 07/2024 |
| [Hostinger](https://hostinger.com) | France, Lithuania, India, USA, Brazil | Yes, out of the box | Yes | Not fast enough, Crypto payments must be done per each server monthly or annually. | 07/2025 |
| [Hostroyale](https://hostroyale.com/hosting/dedicated-server/) | Various countries with different pricing | nan | Yes | nan | 07/2025 |
| [Hostslick](https://hostslick.com) | Netherlands, Germany | Yes, on by default | Yes | Good amount of bandwidth for the price. Make sure you open the ticket if you want to run Exit node | 07/2024 |
| [Incognet](https://incognet.io) | Netherlands and USA | Yes, on by default | Yes | They allow Tor exit nodes but you must adhere to their rules https://incognet.io/tor-exits | 07/2024 |
| [Incognet](https://incognet.io/kansas-city-dedicated-servers) | USA, Netherlands | Yes | nan | nan | 07/2025 |
| [Ionos](https://www.ionos.com/servers/amd-servers) | US, DE, UK, ESP, FR | nan | No | nan | 07/2025 |
| [IsHosting](https://ishosting.com/en) | Brazil, Netherlands | Yes, based on ticket | Yes | Expensive | 05/2024 |
| [Leaseweb](https://www.leaseweb.com/en/configure/vc/product/entityKey/DEDSER02_NEW_ORDER_BUSINESS_R740XD-24SFF-6134) | US, NL, DE, UK, CA, SG, JP, AUS, HK | nan | No | KYC mandatory | 07/2025 |
| [Linode](https://linode.com) | USA, Canada, Japan, India, Indonesia, Sweden, Netherlands, Germany, Brazil, France, UK, Australia, Italy | Yes out of the box | No, only through [BitLAunch](https://bitlaunch.io) | IPv6 sometimes need to be re-added in Networking tab, no reboot needed | 05/2024 |
| [LiteServer](https://liteserver.nl) | Netherlands | Yes, on by default | Yes | Very reliable Dutch provider. They do allow Relay nodes but for Exit nodes you need to contact them. Always check T&C https://liteserver.nl/legal | 07/2024 |
| [Lowendbox](https://lowendbox.com/category/dedicated-servers) | | | | Just an aggregator with good offers | 07/2025 |
| [M247](https://m247.com/eu/services/host/dedicated-servers/) | UK, Austria, Br, Sw, Jp, Poland, Fr, USA, Netherlands | Yes | No | nan | 07/2025 |
| [Mebilcom](https://www.melbicom.net/dedicatedserver/) | NL, US, DE, UAE, NG, ESP, IN, IT, FR, LT, SG, BG, LV, PL | nan | No | nan | 07/2025 |
| [Mevspace](https://mevspace.com) | Poland | Yes, on by default | Yes | Flexible Polish providers with 3 DCs in Poland. They do allow Tor Exit nodes but you may need a dedicated server for this. Make sure you open a ticket to check. As of today's date, they have 48h for 1 EUR tariff | 07/2024 |
| [Misaka](https://www.misaka.io/) | South Africa | Yes, native support | No | Very Expensive | 05/2024 |
| [NiceVPS](https://nicevps.net/) | Netherlands | Yes | nan | nan | 07/2025 |
| [Njalla](https://nja.la) | Sweden | Yes | Yes | Privacy vandguards! The biggest VPS 45 is 3 cores only, but it works better than many “larger” servers on the market. | 05/2024 |
| [OVH](https://us.ovhcloud.com/bare-metal/rise/rise-3/) | USA, DE, FR, UK, PL, CA | | No | Not all locations always available | 07/2025 |
| [Oneprovider](https://oneprovider.com/en/dedicated-servers/ipv6) | PL, FR, NL, UA, US, BG, RO, DK, ESP, NO, CZ, RS, IE, IT, UK, HU, CH, SK, AT, BE, BA, HK, JP, SG, LU, AU, SWE, UAE, BR, CR, MX, GR, CL, MA, AR | Yes | No | nan | 07/2025 |
| [PrivateLayer](https://privatelayer.com) | Swiss | Yes | Yes | Slow customer response | 07/2025 |
| [Privex](https://www.privex.io/tor-exit-policy/) | USA, Germany, Sweden | Yes | Yes | nan | 07/2025 |
| [Psychz](https://www.psychz.net) | US, UK, Brazil, Japan, Russia, South Africa and many more | Yes | nan | nan | 07/2025 |
| [RDP](https://rdp.sh) | Netherlands, USA, Poland | Yes, on by default | Yes | German provider. Exit nodes are allowed, policy is here https://rdp.sh/docs/faq/tor ports 25,465,587 must be closed. Make sure you open a ticket before running an exit node. | 07/2024 |
| [Servermania](https://www.servermania.com/dedicated-servers-hosting.htm) | USA, Canada | nan | No | nan | 07/2025 |
| [Svea](https://svea.net/vps) | Sweden | Yes | nan | nan | 07/2025 |
| [TerraHost](https://terrahost.no) | Norway | Yes, on by default | Yes | Very reliable Norwegian provider. Only allow exit nodes on Dedicated servers subject to certain caveats (you must open a ticket). Always check T&C https://terrahost.no/avtalebetingelser | 07/2024 |
| [Thundervm](https://thundervm.com/en/hosting/dedicated-server) | USA, UK, France, Italy, Switzerland, Netherlands | nan | Yes | | 07/2025 |
| [Zenlayer](https://www.zenlayer.com/bare-metal/) | [advertised over 50 locations](50+ https://www.zenlayer.com/global-network) | nan | nan | nan | 07/2025 |
| [iHostArt](https://ihostart.com) | Romania | Yes, on by default | Yes | Super permissive provider. They do allow Tor Exit/Relay/Bridge. Pro-free speech etc. Recently, IPv6 geolocation was set to North Korea, so be aware. | 07/2024 |
| [vSys Host](https://vsys.host) | Ukraine, Netherlands, USA | Yes, on by default | Yes | Pretty permissive provider registered in Ukraine. Should allow Relay/Exit nodes but nothing in T&C, so better double check. | 07/2024 |
+42 -25
View File
@@ -1,26 +1,43 @@
**ISP**,**Locations**,**Public IPv6**,**Crypto Payments**,**Comments**,**Last Updated**
[Flokinet](https://flokinet.is),"Netherlands, Iceland, Romania,France","Yes, needs a ticket and custom setup","yes, including XMR","Very slow customer support","05/2024"
[BitLaunch](https://bitlaunch.io),"Canada, USA, UK","No","Yes","Expensive. Digial Ocean through BitLanch has IPv6","05/2024"
[Hostinger](https://hostinger.com),"France, Lithuania, India, USA, Brazil","Yes, out of the box","Yes","Crypto payments must be done per each server monthly or annually.","05/2024"
[Linode](https://linode.com),"USA, Canada, Japan, India, Indonesia, Sweden, Netherlands, Germany, Brazil, France, UK, Australia, Italy","Yes out of the box","No, only through [BitLAunch](https://bitlaunch.io)","IPv6 sometimes need to be re-added in Networking tab, no reboot needed","05/2024"
[Cherry Servers](https://www.cherryservers.com),"Lithuania, Netherlands, USA, Singapore","No","Yes","Issued IP doesnt match the location offered by the provider.","05/2024"
[Njalla](https://nja.la),"Sweden","Yes","Yes","Privacy vandguards! The biggest VPS 45 is 3 cores only, but it works better than many “larger” servers on the market.","05/2024"
[HostSailor](https://hostsailor.com),"USA","Yes, based on ticket","Yes","The IPv6 setup needs custom research and is not documented","05/2024"
[Misaka](https://www.misaka.io/),"South Africa","Yes, native support","No","Very Expensive","05/2024"
[IsHosting](https://ishosting.com/en),"Brazil, Netherlands","Yes, based on ticket","Yes","Expensive","05/2024"
[AlexHost](https://alexhost.com),"Moldova, Bulgaria, Sweden, Netherlands","Yes, on by default","Yes","They allow TOR Bridges, Relays. Exit nodes are only allowed on dedicated servers (prices start from 26 EUR)","07/2024"
[iHostArt](https://ihostart.com),"Romania","Yes, on by default","Yes","Super permissive provider. They do allow Tor Exit/Relay/Bridge. Pro-free speech etc. Recently, IPv6 geolocation was set to North Korea, so be aware.","07/2024"
[Incognet](https://incognet.io),"Netherlands and USA","Yes, on by default","Yes","They allow Tor exit nodes but you must adhere to their rules https://incognet.io/tor-exits","07/2024"
[vSys Host](https://vsys.host),"Ukraine, Netherlands, USA","Yes, on by default","Yes","Pretty permissive provider registered in Ukraine. Should allow Relay/Exit nodes but nothing in T&C, so better double check.","07/2024"
[LiteServer](https://liteserver.nl),"Netherlands","Yes, on by default","Yes","Very reliable Dutch provider. They do allow Relay nodes but for Exit nodes you need to contact them. Always check T&C https://liteserver.nl/legal","07/2024"
[TerraHost](https://terrahost.no),"Norway","Yes, on by default","Yes","Very reliable Norwegian provider. Only allow exit nodes on Dedicated servers subject to certain caveats (you must open a ticket). Always check T&C https://terrahost.no/avtalebetingelser","07/2024"
[Mevspace](https://mevspace.com),"Poland","Yes, on by default","Yes","Flexible Polish providers with 3 DCs in Poland. They do allow Tor Exit nodes but you may need a dedicated server for this. Make sure you open a ticket to check. As of today's date, they have 48h for 1 EUR tariff","07/2024"
[Hostiko](https://hostiko.com.ua),"Ukraine, Germany","Yes, on by default","Yes","Ukrainian provider. They allow Exit nodes on Germany boxes but limit the bandwidth, you also have to restrict certain ports like 25 and 587. Make sure you open a ticket.","07/2024"
[Hostslick](https://hostslick.com),"Netherlands, Germany","Yes, on by default","Yes","Good amount of bandwidth for the price. Make sure you open the ticket if you want to run Exit node","07/2024"
[RDP](https://rdp.sh),"Netherlands, USA, Poland","Yes, on by default","Yes","German provider. Exit nodes are allowed, policy is here https://rdp.sh/docs/faq/tor ports 25,465,587 must be closed. Make sure you open a ticket before running an exit node.","07/2024"
[Flokinet](https://flokinet.is),"Netherlands, Iceland, Romania,France","Yes, needs a ticket and custom setup","yes, including XMR",Very slow customer support,05/2024
[BitLaunch](https://bitlaunch.io),"Canada, USA, UK",No,Yes,Expensive. Digial Ocean through BitLanch has IPv6,05/2024
[Hostinger](https://hostinger.com),"France, Lithuania, India, USA, Brazil","Yes, out of the box",Yes,"Not fast enough, Crypto payments must be done per each server monthly or annually.",07/2025
[Linode](https://linode.com),"USA, Canada, Japan, India, Indonesia, Sweden, Netherlands, Germany, Brazil, France, UK, Australia, Italy",Yes out of the box,"No, only through [BitLAunch](https://bitlaunch.io)","IPv6 sometimes need to be re-added in Networking tab, no reboot needed",05/2024
[Cherry Servers](https://www.cherryservers.com),"Lithuania, Netherlands, USA, Singapore",No,Yes,Issued IP doesnt match the location offered by the provider.,05/2024
[Njalla](https://nja.la),Sweden,Yes,Yes,"Privacy vandguards! The biggest VPS 45 is 3 cores only, but it works better than many “larger” servers on the market.",05/2024
[HostSailor](https://hostsailor.com),USA,"Yes, based on ticket",Yes,The IPv6 setup needs custom research and is not documented,05/2024
[Misaka](https://www.misaka.io/),South Africa,"Yes, native support",No,Very Expensive,05/2024
[IsHosting](https://ishosting.com/en),"Brazil, Netherlands","Yes, based on ticket",Yes,Expensive,05/2024
[AlexHost](https://alexhost.com),"Moldova, Bulgaria, Sweden, Netherlands","Yes, on by default",Yes,"They allow TOR Bridges, Relays. Exit nodes are only allowed on dedicated servers (prices start from 26 EUR)",07/2024
[iHostArt](https://ihostart.com),Romania,"Yes, on by default",Yes,"Super permissive provider. They do allow Tor Exit/Relay/Bridge. Pro-free speech etc. Recently, IPv6 geolocation was set to North Korea, so be aware.",07/2024
[Incognet](https://incognet.io),Netherlands and USA,"Yes, on by default",Yes,They allow Tor exit nodes but you must adhere to their rules https://incognet.io/tor-exits,07/2024
[vSys Host](https://vsys.host),"Ukraine, Netherlands, USA","Yes, on by default",Yes,"Pretty permissive provider registered in Ukraine. Should allow Relay/Exit nodes but nothing in T&C, so better double check.",07/2024
[LiteServer](https://liteserver.nl),Netherlands,"Yes, on by default",Yes,Very reliable Dutch provider. They do allow Relay nodes but for Exit nodes you need to contact them. Always check T&C https://liteserver.nl/legal,07/2024
[TerraHost](https://terrahost.no),Norway,"Yes, on by default",Yes,Very reliable Norwegian provider. Only allow exit nodes on Dedicated servers subject to certain caveats (you must open a ticket). Always check T&C https://terrahost.no/avtalebetingelser,07/2024
[Mevspace](https://mevspace.com),Poland,"Yes, on by default",Yes,"Flexible Polish providers with 3 DCs in Poland. They do allow Tor Exit nodes but you may need a dedicated server for this. Make sure you open a ticket to check. As of today's date, they have 48h for 1 EUR tariff",07/2024
[Hostiko](https://hostiko.com.ua),"Ukraine, Germany","Yes, on by default",Yes,"Ukrainian provider. They allow Exit nodes on Germany boxes but limit the bandwidth, you also have to restrict certain ports like 25 and 587. Make sure you open a ticket.",07/2024
[Hostslick](https://hostslick.com),"Netherlands, Germany","Yes, on by default",Yes,Good amount of bandwidth for the price. Make sure you open the ticket if you want to run Exit node,07/2024
[RDP](https://rdp.sh),"Netherlands, USA, Poland","Yes, on by default",Yes,"German provider. Exit nodes are allowed, policy is here https://rdp.sh/docs/faq/tor ports 25,465,587 must be closed. Make sure you open a ticket before running an exit node.",07/2024
[Lowendbox](https://lowendbox.com/category/dedicated-servers), , , ,Just an aggregator with good offers,07/2025
[Thundervm](https://thundervm.com/en/hosting/dedicated-server),"USA, UK, France, Italy, Switzerland, Netherlands",,Yes, ,07/2025
[OVH](https://us.ovhcloud.com/bare-metal/rise/rise-3/),"USA, DE, FR, UK, PL, CA", ,No,Not all locations always available,07/2025
[Mebilcom](https://www.melbicom.net/dedicatedserver/),"NL, US, DE, UAE, NG, ESP, IN, IT, FR, LT, SG, BG, LV, PL",,No,,07/2025
[Servermania](https://www.servermania.com/dedicated-servers-hosting.htm),"USA, Canada",,No,,07/2025
[Oneprovider](https://oneprovider.com/en/dedicated-servers/ipv6),"PL, FR, NL, UA, US, BG, RO, DK, ESP, NO, CZ, RS, IE, IT, UK, HU, CH, SK, AT, BE, BA, HK, JP, SG, LU, AU, SWE, UAE, BR, CR, MX, GR, CL, MA, AR",Yes,No,,07/2025
[Ionos](https://www.ionos.com/servers/amd-servers),"US, DE, UK, ESP, FR",,No,,07/2025
[Leaseweb](https://www.leaseweb.com/en/configure/vc/product/entityKey/DEDSER02_NEW_ORDER_BUSINESS_R740XD-24SFF-6134),"US, NL, DE, UK, CA, SG, JP, AUS, HK",,No,KYC mandatory,07/2025
[M247](https://m247.com/eu/services/host/dedicated-servers/),"UK, Austria, Br, Sw, Jp, Poland, Fr, USA, Netherlands",Yes,No,,07/2025
[Hostroyale](https://hostroyale.com/hosting/dedicated-server/),Various countries with different pricing,, Yes,,07/2025
[DataPacket](https://www.datapacket.com/pricing),"NL, GR, SK, BE, RO, HU, DK, IE, DE, UA, PT, GB, ES, FR, IT, NO, CZ, BG, SE, AT, PL, HR, CH, USA, CO, AR, PE, MX, CL, TR, ZA, NG, IL, HK, AU, SG, JP",Yes,,,07/2025
[Zenlayer](https://www.zenlayer.com/bare-metal/), [advertised over 50 locations](50+ https://www.zenlayer.com/global-network),,,,07/2025
[PrivateLayer](https://privatelayer.com),Swiss,Yes,Yes,Slow customer response,07/2025
[AmeriNoc](https://www.amerinoc.com),USA,Yes,,,07/2025
[Colocall](https://www.colocall.net/),Ukraine,Yes,,07/2025,
[Incognet](https://incognet.io/kansas-city-dedicated-servers),"USA, Netherlands",Yes,,,07/2025
[FranTech](https://my.frantech.ca),USA,Yes,,,07/2025
[Psychz](https://www.psychz.net),"US, UK, Brazil, Japan, Russia, South Africa and many more",Yes,,,07/2025
[Fsit](https://www.fsit.com/server/vps-vserver-kvm),Swiss,Yes,Yes,,07/2025
[NiceVPS](https://nicevps.net/),Netherlands,Yes,,,07/2025
[Dataclub](https://www.dataclub.eu/),"Latvia, Sweden, Netherlands",Yes,,,07/2027
[Privex](https://www.privex.io/tor-exit-policy/),"USA, Germany, Sweden",Yes,Yes,,07/2025
[Svea](https://svea.net/vps),Sweden,Yes,,,07/2025
1 **ISP** **Locations** **Public IPv6** **Crypto Payments** **Comments** **Last Updated**
2 [Flokinet](https://flokinet.is) Netherlands, Iceland, Romania,France Yes, needs a ticket and custom setup yes, including XMR Very slow customer support 05/2024
3 [BitLaunch](https://bitlaunch.io) Canada, USA, UK No Yes Expensive. Digial Ocean through BitLanch has IPv6 05/2024
4 [Hostinger](https://hostinger.com) France, Lithuania, India, USA, Brazil Yes, out of the box Yes Crypto payments must be done per each server monthly or annually. Not fast enough, Crypto payments must be done per each server monthly or annually. 05/2024 07/2025
5 [Linode](https://linode.com) USA, Canada, Japan, India, Indonesia, Sweden, Netherlands, Germany, Brazil, France, UK, Australia, Italy Yes out of the box No, only through [BitLAunch](https://bitlaunch.io) IPv6 sometimes need to be re-added in Networking tab, no reboot needed 05/2024
6 [Cherry Servers](https://www.cherryservers.com) Lithuania, Netherlands, USA, Singapore No Yes Issued IP doesn’t match the location offered by the provider. 05/2024
7 [Njalla](https://nja.la) Sweden Yes Yes Privacy vandguards! The biggest VPS 45 is 3 cores only, but it works better than many “larger” servers on the market. 05/2024
8 [HostSailor](https://hostsailor.com) USA Yes, based on ticket Yes The IPv6 setup needs custom research and is not documented 05/2024
9 [Misaka](https://www.misaka.io/) South Africa Yes, native support No Very Expensive 05/2024
10 [IsHosting](https://ishosting.com/en) Brazil, Netherlands Yes, based on ticket Yes Expensive 05/2024
11 [AlexHost](https://alexhost.com) Moldova, Bulgaria, Sweden, Netherlands Yes, on by default Yes They allow TOR Bridges, Relays. Exit nodes are only allowed on dedicated servers (prices start from 26 EUR) 07/2024
12 [iHostArt](https://ihostart.com) Romania Yes, on by default Yes Super permissive provider. They do allow Tor Exit/Relay/Bridge. Pro-free speech etc. Recently, IPv6 geolocation was set to North Korea, so be aware. 07/2024
13 [Incognet](https://incognet.io) Netherlands and USA Yes, on by default Yes They allow Tor exit nodes but you must adhere to their rules https://incognet.io/tor-exits 07/2024
14 [vSys Host](https://vsys.host) Ukraine, Netherlands, USA Yes, on by default Yes Pretty permissive provider registered in Ukraine. Should allow Relay/Exit nodes but nothing in T&C, so better double check. 07/2024
15 [LiteServer](https://liteserver.nl) Netherlands Yes, on by default Yes Very reliable Dutch provider. They do allow Relay nodes but for Exit nodes you need to contact them. Always check T&C https://liteserver.nl/legal 07/2024
16 [TerraHost](https://terrahost.no) Norway Yes, on by default Yes Very reliable Norwegian provider. Only allow exit nodes on Dedicated servers subject to certain caveats (you must open a ticket). Always check T&C https://terrahost.no/avtalebetingelser 07/2024
17 [Mevspace](https://mevspace.com) Poland Yes, on by default Yes Flexible Polish providers with 3 DCs in Poland. They do allow Tor Exit nodes but you may need a dedicated server for this. Make sure you open a ticket to check. As of today's date, they have 48h for 1 EUR tariff 07/2024
18 [Hostiko](https://hostiko.com.ua) Ukraine, Germany Yes, on by default Yes Ukrainian provider. They allow Exit nodes on Germany boxes but limit the bandwidth, you also have to restrict certain ports like 25 and 587. Make sure you open a ticket. 07/2024
19 [Hostslick](https://hostslick.com) Netherlands, Germany Yes, on by default Yes Good amount of bandwidth for the price. Make sure you open the ticket if you want to run Exit node 07/2024
20 [RDP](https://rdp.sh) Netherlands, USA, Poland Yes, on by default Yes German provider. Exit nodes are allowed, policy is here https://rdp.sh/docs/faq/tor ports 25,465,587 must be closed. Make sure you open a ticket before running an exit node. 07/2024
21 [Lowendbox](https://lowendbox.com/category/dedicated-servers) Just an aggregator with good offers 07/2025
22 [Thundervm](https://thundervm.com/en/hosting/dedicated-server) USA, UK, France, Italy, Switzerland, Netherlands Yes 07/2025
23 [OVH](https://us.ovhcloud.com/bare-metal/rise/rise-3/) USA, DE, FR, UK, PL, CA No Not all locations always available 07/2025
24 [Mebilcom](https://www.melbicom.net/dedicatedserver/) NL, US, DE, UAE, NG, ESP, IN, IT, FR, LT, SG, BG, LV, PL No 07/2025
25 [Servermania](https://www.servermania.com/dedicated-servers-hosting.htm) USA, Canada No 07/2025
26 [Oneprovider](https://oneprovider.com/en/dedicated-servers/ipv6) PL, FR, NL, UA, US, BG, RO, DK, ESP, NO, CZ, RS, IE, IT, UK, HU, CH, SK, AT, BE, BA, HK, JP, SG, LU, AU, SWE, UAE, BR, CR, MX, GR, CL, MA, AR Yes No 07/2025
27 [Ionos](https://www.ionos.com/servers/amd-servers) US, DE, UK, ESP, FR No 07/2025
28 [Leaseweb](https://www.leaseweb.com/en/configure/vc/product/entityKey/DEDSER02_NEW_ORDER_BUSINESS_R740XD-24SFF-6134) US, NL, DE, UK, CA, SG, JP, AUS, HK No KYC mandatory 07/2025
29 [M247](https://m247.com/eu/services/host/dedicated-servers/) UK, Austria, Br, Sw, Jp, Poland, Fr, USA, Netherlands Yes No 07/2025
30 [Hostroyale](https://hostroyale.com/hosting/dedicated-server/) Various countries with different pricing Yes 07/2025
31 [DataPacket](https://www.datapacket.com/pricing) NL, GR, SK, BE, RO, HU, DK, IE, DE, UA, PT, GB, ES, FR, IT, NO, CZ, BG, SE, AT, PL, HR, CH, USA, CO, AR, PE, MX, CL, TR, ZA, NG, IL, HK, AU, SG, JP Yes 07/2025
32 [Zenlayer](https://www.zenlayer.com/bare-metal/) [advertised over 50 locations](50+ https://www.zenlayer.com/global-network) 07/2025
33 [PrivateLayer](https://privatelayer.com) Swiss Yes Yes Slow customer response 07/2025
34 [AmeriNoc](https://www.amerinoc.com) USA Yes 07/2025
35 [Colocall](https://www.colocall.net/) Ukraine Yes 07/2025
36 [Incognet](https://incognet.io/kansas-city-dedicated-servers) USA, Netherlands Yes 07/2025
37 [FranTech](https://my.frantech.ca) USA Yes 07/2025
38 [Psychz](https://www.psychz.net) US, UK, Brazil, Japan, Russia, South Africa and many more Yes 07/2025
39 [Fsit](https://www.fsit.com/server/vps-vserver-kvm) Swiss Yes Yes 07/2025
40 [NiceVPS](https://nicevps.net/) Netherlands Yes 07/2025
41 [Dataclub](https://www.dataclub.eu/) Latvia, Sweden, Netherlands Yes 07/2027
42 [Privex](https://www.privex.io/tor-exit-policy/) USA, Germany, Sweden Yes Yes 07/2025
43 [Svea](https://svea.net/vps) Sweden Yes 07/2025
@@ -24,25 +24,23 @@ Write a message to your provider telling them about your intention to run a `nym
</AccordionTemplate>
#### Join Operators Legal Forum
This [Matrix channel]((https://matrix.to/#/!YfoUFsJjsXbWmijbPG:nymtech.chat?via=nymtech.chat&via=matrix.org&via=matrix.su4ka.icu)) is the best place to ask questions and share your experience with others. You can share screen prints of abuse reports and ask for support.
This [Matrix channel]((https://matrix.to/#/!YfoUFsJjsXbWmijbPG:nymtech.chat?via=nymtech.chat&via=matrix.org&via=matrix.su4ka.icu)) is the best place to ask questions and share your experience with others. You can share screen prints of abuse reports and ask for support.
#### Join Operators Legal Clinic
Do you have any questions directed for lawyers? Come and chat with Nym COO Alexis Roussel, every Wednesday 14:30 UTC for 60min in our [Operator Legal Forum channel on Matrix](https://matrix.to/#/!YfoUFsJjsXbWmijbPG:nymtech.chat?via=nymtech.chat&via=matrix.org&via=matrix.su4ka.icu).
#### Use a friendly provider
Nym operators community shares their experience with different ISPs on [this page](isp-list). At the same time, consider to move away from these provides:
- Servinga / VPS2day (AS39378)
- Frantech / Ponynet / BuyVM (AS53667)
- OVH SAS / OVHcloud (AS16276)
- Online S.A.S. / Scaleway (AS12876)
- Hetzner Online GmbH (AS24940)
- IONOS SE (AS8560)
- Psychz Networks (AS40676)
- 1337 Services GmbH / RDP.sh (AS210558)
- Stark Industries Solutions Ltd. / PQ.Hosting / The.Hosting / UFO-AS (AS44477 / ASN 33993)
#### Backup your nodes
Your only way to restore your node is when you have an access to `.nym` directory locally. Follow [node](../nodes/maintenance#backup-a-node) and [proxy configuration](../nodes/maintenance#backup-proxy-configuration) backup guides to be able to [restore your node](../nodes/maintenance#restoring-a-node) on another machine later on, without losing your delegation.
Your only way to restore your node is when you have an access to `.nym` directory locally. Follow [node](../nodes/maintenance#backup-a-node) and [proxy configuration](../nodes/maintenance#backup-proxy-configuration) backup guides to be able to [restore your node](../nodes/maintenance#restoring-a-node) on another machine later on, without losing your delegation.
#### Use `nym-exit` prefix on your landing page URL
We would like to ask operators to use [reverse proxy](../nodes/nym-node/configuration/proxy-configuration) with a [landing page](landing-pages). When assigning a domain please use a common convention with `nym-exit` in the beginning of the the page URL as this will create a reputation and reference. The entire address should have this new format:
@@ -69,10 +67,10 @@ nym-exit.mysquad.org
**The `NYM-EXIT` part in the beginning is what's important.**
#### Chose the right TLD
When registering a domain, check [Top Level Domain (TLD)](https://www.techopedia.com/definition/1348/top-level-domain-tld) terms and conditions. For example `.icu` is a no go. Having a wrong TLD may lead to your domain being taken away from you when facing a DMCA report.
When registering a domain, check [Top Level Domain (TLD)](https://www.techopedia.com/definition/1348/top-level-domain-tld) terms and conditions. For example `.icu` is a no go. Having a wrong TLD may lead to your domain being taken away from you when facing a DMCA report.
#### Respond to abuse reports
Make sure to read notifications from your account provider and if you receive an abuse report, respond to it in time. Here is a template explaining the essential legal background of Nym Node Exit Gateway. Don't forget to adjust the variables.
Make sure to read notifications from your account provider and if you receive an abuse report, respond to it in time. Here is a template explaining the essential legal background of Nym Node Exit Gateway. Don't forget to adjust the variables.
<br/>
<AccordionTemplate name="Email template: responding to DMCA take-down notices">
@@ -81,4 +79,3 @@ Make sure to read notifications from your account provider and if you receive an
#### Help us to improve these pages
Add your findings by opening a [Pull Request](add-content).
@@ -454,7 +454,7 @@ The exit policy is same for all NRs, the content is shaped by an offchain govern
There is a caveat though. NR is only routing TCP streams and therefore any other type of routing is *not* filtered thorugh the exit policy. To ensure that Nym Nodes follow the same exit policy when routing IP packets through wireguard and don't act as open proxies, the operators have to set up these rules via IP tables rules.
**Follow these steps, using a [setup script]i(https://raw.githubusercontent.com/nymtech/nym/refs/heads/develop/scripts/wireguard-exit-policy/wireguard-exit-policy-manager.sh) and [testing scripts](https://raw.githubusercontent.com/nymtech/nym/refs/heads/develop/scripts/wireguard-exit-policy/exit-policy-tests.sh) written by Nym quality assurance team, to setup exit policy for wireguard:**
**Follow these steps, using a [setup script](https://raw.githubusercontent.com/nymtech/nym/refs/heads/develop/scripts/wireguard-exit-policy/wireguard-exit-policy-manager.sh) and [testing scripts](https://raw.githubusercontent.com/nymtech/nym/refs/heads/develop/scripts/wireguard-exit-policy/exit-policy-tests.sh) written by Nym quality assurance team, to setup exit policy for wireguard:**
<Steps>
@@ -9,12 +9,12 @@ import { AccordionTemplate } from 'components/accordion-template.tsx';
> Nym has two main codebases:
> - the [Nym platform](https://github.com/nymtech/nym), written in Rust. This contains all of our code except for the validators.
> - the [Nym validators](https://github.com/nymtech/nyxd), written in Go & maintained as a no-modification fork of [wasmd](https://github.com/CosmWasm/wasmd)
> - the [Nym validators](https://github.com/nymtech/nyxd), written in Go & maintained as fork of [wasmd](https://github.com/CosmWasm/wasmd)
The validator is a Go application which implements it's functionalities using [Cosmos SDK](https://v1.cosmos.network/sdk). The underlying state-replication engine is powered by [CometBFT](https://cometbft.com/), where the consensus mechanism is based on the [Tendermint Consensus Algorithm](https://arxiv.org/abs/1807.04938). Finally, a [CosmWasm](https://cosmwasm.com) smart contract module controls crucial mixnet functionalities like decentralised directory service, node bonding, and delegated mixnet staking.
<Callout type="info" emoji="️">
We are currently running mainnet with a closed set of reputable validators. To ensure decentralisation of Nyx chain, we are working on a mechanism to onboard new validators to the network. To join the waitlist, please drop an email to `validators [at] nymtech.net` with details of your setup, experience and any other relevent information
At present, our mainnet operates with a select group of reputed validators. We are not accepting new validators at this time. Any updates or changes to this policy will be promptly announced.
</Callout>
## Building your validator
@@ -52,7 +52,7 @@ pacman -S git gcc jq
- First remove any existing old Go installation and extract the archive you just downloaded into /usr/local:
```sh
rm -rf /usr/local/go && tar -C /usr/local -xzf go1.20.10.linux-amd64.tar.gz
rm -rf /usr/local/go && tar -C /usr/local -xzf go1.23.11.linux-amd64.tar.gz
```
- Then add /usr/local/go/bin to the PATH environment variable
@@ -69,14 +69,14 @@ go version
- Should return something like:
```sh
go version go1.20.10 linux/amd64
go version go1.23.11 linux/amd64
```
</Steps>
### Download a precompiled validator binary
You can find pre-compiled binaries for Ubuntu `22.04` and `20.04` [here](https://github.com/nymtech/nyxd/releases).
You can find pre-compiled binaries for Ubuntu `22.04` and `24.04` [here](https://github.com/nymtech/nyxd/releases).
### Manually compiling your validator binary
@@ -109,8 +109,9 @@ Usage:
nyxd [command]
Available Commands:
comet CometBFT subcommands
completion Generate the autocompletion script for the specified shell
config Create or query an application CLI configuration file
config Utilities for managing application configuration
debug Tool for helping with debugging your application
export Export state to JSON
genesis Application's genesis-related subcommands
@@ -119,20 +120,20 @@ Available Commands:
keys Manage your application's keys
prune Prune app history states by keeping the recent heights and deleting old heights
query Querying subcommands
rollback rollback cosmos-sdk and tendermint state by one height
rosetta spin up a rosetta server
rollback rollback Cosmos SDK and CometBFT state by one height
snapshots Manage local snapshots
start Run the full node
status Query remote node for status
tendermint Tendermint subcommands
testnet subcommands for starting or configuring local testnets
tx Transactions subcommands
version Print the application binary version information
Flags:
-h, --help help for nyxd
--home string directory for config and data
--home string directory for config and data (default "/Users/neo/.nyxd")
--log_format string The logging format (json|plain) (default "plain")
--log_level string The logging level (trace|debug|info|warn|error|fatal|panic) (default "info")
--log_level string The logging level (trace|debug|info|warn|error|fatal|panic|disabled or '*:<level>,<key>:<level>') (default "info")
--log_no_color Disable colored logs
--trace print out full stack trace on errors
Use "nyxd [command] --help" for more information about a command.
@@ -180,8 +181,9 @@ Usage:
nyxd [command]
Available Commands:
comet CometBFT subcommands
completion Generate the autocompletion script for the specified shell
config Create or query an application CLI configuration file
config Utilities for managing application configuration
debug Tool for helping with debugging your application
export Export state to JSON
genesis Application's genesis-related subcommands
@@ -190,20 +192,20 @@ Available Commands:
keys Manage your application's keys
prune Prune app history states by keeping the recent heights and deleting old heights
query Querying subcommands
rollback rollback cosmos-sdk and tendermint state by one height
rosetta spin up a rosetta server
rollback rollback Cosmos SDK and CometBFT state by one height
snapshots Manage local snapshots
start Run the full node
status Query remote node for status
tendermint Tendermint subcommands
testnet subcommands for starting or configuring local testnets
tx Transactions subcommands
version Print the application binary version information
Flags:
-h, --help help for nyxd
--home string directory for config and data
--home string directory for config and data (default "/Users/neo/.nyxd")
--log_format string The logging format (json|plain) (default "plain")
--log_level string The logging level (trace|debug|info|warn|error|fatal|panic) (default "info")
--log_level string The logging level (trace|debug|info|warn|error|fatal|panic|disabled or '*:<level>,<key>:<level>') (default "info")
--log_no_color Disable colored logs
--trace print out full stack trace on errors
Use "nyxd [command] --help" for more information about a command.
@@ -246,7 +248,7 @@ You can use the following command to download them for the correct network:
wget -O $HOME/.nyxd/config/genesis.json https://nymtech.net/genesis/genesis.json
# Sandbox testnet
curl https://rpc.sandbox.nymtech.net/snapshots/genesis.json | jq '.result.genesis' > $HOME/.nyxd/config/genesis.json
curl https://rpc.sandbox.nymtech.net/genesis | jq '.result.genesis' > $HOME/.nyxd/config/genesis.json
```
### `config.toml` configuration
@@ -512,7 +514,7 @@ nyxd tx slashing unjail
--from="KEYRING_NAME"
--chain-id=nyx
--gas=auto
--gas-adjustment=1.4
--gas-adjustment=1.5
--gas-prices=0.025unyx
```
@@ -523,7 +525,7 @@ nyxd tx slashing unjail
--from="KEYRING_NAME"
--chain-id=sandbox
--gas=auto
--gas-adjustment=1.4
--gas-adjustment=1.5
--gas-prices=0.025unyx
```
+4 -23
View File
@@ -4,7 +4,7 @@
[package]
name = "nym-api"
license = "GPL-3.0"
version = "1.1.62"
version = "1.1.64"
authors.workspace = true
edition = "2021"
rust-version.workspace = true
@@ -16,17 +16,12 @@ async-trait = { workspace = true }
bs58 = { workspace = true }
bip39 = { workspace = true }
bincode.workspace = true
console-subscriber = { workspace = true, optional = true } # nym-api needs to be built with RUSTFLAGS="--cfg tokio_unstable"
cfg-if = { workspace = true }
clap = { workspace = true, features = ["cargo", "derive", "env"] }
console-subscriber = { workspace = true, optional = true } # validator-api needs to be built with RUSTFLAGS="--cfg tokio_unstable"
dashmap = { workspace = true }
dirs = { workspace = true }
futures = { workspace = true }
itertools = { workspace = true }
humantime-serde = { workspace = true }
k256 = { workspace = true, features = [
"ecdsa-core",
] } # needed for the Verifier trait; pull whatever version is used by other dependencies
moka = { workspace = true }
pin-project = { workspace = true }
rand = { workspace = true }
@@ -51,7 +46,6 @@ tendermint = { workspace = true }
ts-rs = { workspace = true, optional = true }
anyhow = { workspace = true }
getset = { workspace = true }
sqlx = { workspace = true, features = [
"runtime-tokio-rustls",
@@ -66,30 +60,19 @@ zeroize = { workspace = true }
# for axum server
axum = { workspace = true, features = ["tokio"] }
axum-extra = { workspace = true, features = ["typed-header"] }
tower-http = { workspace = true, features = ["cors", "trace", "compression-br", "compression-deflate", "compression-gzip", "compression-zstd"] }
utoipa = { workspace = true, features = ["axum_extras", "time"] }
utoipauto = { workspace = true }
utoipa-swagger-ui = { workspace = true, features = ["axum"] }
tracing = { workspace = true }
## ephemera-specific
#actix-web = "4"
#array-bytes = "6.0.0"
#chrono = { version = "0.4.24", default-features = false, features = ["clock"] }
#futures-util = "0.3.25"
#serde_derive = "1.0.149"
#uuid = { version = "1.3.0", features = ["serde", "v4"] }
## internal
#ephemera = { path = "../ephemera" }
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"] }
nym-coconut-dkg-common = { path = "../common/cosmwasm-smart-contracts/coconut-dkg" }
nym-compact-ecash = { path = "../common/nym_offline_compact_ecash" }
nym-credentials-interface = { path = "../common/credentials-interface" }
#nym-ephemera-common = { path = "../common/cosmwasm-smart-contracts/ephemera" }
nym-config = { path = "../common/config" }
cosmwasm-std = { workspace = true }
nym-credential-storage = { path = "../common/credential-storage", features = [
@@ -102,11 +85,8 @@ cw3 = { workspace = true }
cw4 = { workspace = true }
nym-dkg = { path = "../common/dkg", features = ["cw-types"] }
nym-gateway-client = { path = "../common/client-libs/gateway-client" }
nym-inclusion-probability = { path = "../common/inclusion-probability" }
nym-mixnet-contract-common = { path = "../common/cosmwasm-smart-contracts/mixnet-contract", features = ["utoipa"] }
nym-vesting-contract-common = { path = "../common/cosmwasm-smart-contracts/vesting-contract" }
nym-contracts-common = { path = "../common/cosmwasm-smart-contracts/contracts-common", features = ["naive_float", "utoipa"] }
nym-multisig-contract-common = { path = "../common/cosmwasm-smart-contracts/multisig-contract" }
nym-sphinx = { path = "../common/nymsphinx" }
nym-pemstore = { path = "../common/pemstore" }
nym-task = { path = "../common/task" }
@@ -121,7 +101,8 @@ nym-http-api-common = { path = "../common/http-api-common", features = ["utoipa"
nym-serde-helpers = { path = "../common/serde-helpers", features = ["date"] }
nym-ticketbooks-merkle = { path = "../common/ticketbooks-merkle" }
nym-statistics-common = { path = "../common/statistics" }
chrono.workspace = true
nym-ecash-signer-check = { path = "../common/ecash-signer-check" }
[features]
no-reward = []
+4 -2
View File
@@ -10,7 +10,6 @@ license.workspace = true
bs58 = { workspace = true }
cosmrs = { workspace = true }
cosmwasm-std = { workspace = true }
getset = { workspace = true }
schemars = { workspace = true, features = ["preserve_order"] }
serde = { workspace = true, features = ["derive"] }
humantime-serde = { workspace = true }
@@ -37,10 +36,13 @@ nym-ecash-time = { path = "../../common/ecash-time" }
nym-compact-ecash = { path = "../../common/nym_offline_compact_ecash" }
nym-contracts-common = { path = "../../common/cosmwasm-smart-contracts/contracts-common", features = ["naive_float"] }
nym-mixnet-contract-common = { path = "../../common/cosmwasm-smart-contracts/mixnet-contract", features = ["utoipa"] }
nym-coconut-dkg-common = { path = "../../common/cosmwasm-smart-contracts/coconut-dkg" }
nym-node-requests = { path = "../../nym-node/nym-node-requests", default-features = false, features = ["openapi"] }
nym-noise-keys = { path = "../../common/nymnoise/keys"}
nym-noise-keys = { path = "../../common/nymnoise/keys" }
nym-network-defaults = { path = "../../common/network-defaults" }
nym-ticketbooks-merkle = { path = "../../common/ticketbooks-merkle" }
nym-ecash-signer-check-types = { path = "../../common/ecash-signer-check-types" }
[dev-dependencies]
rand_chacha = { workspace = true }
+28 -70
View File
@@ -1,7 +1,9 @@
// Copyright 2023-2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::signable::SignedMessage;
use cosmrs::AccountId;
use nym_coconut_dkg_common::types::EpochId;
use nym_compact_ecash::scheme::coin_indices_signatures::AnnotatedCoinIndexSignature;
use nym_compact_ecash::scheme::expiration_date_signatures::AnnotatedExpirationDateSignature;
use nym_compact_ecash::utils::try_deserialize_g1_projective;
@@ -12,14 +14,13 @@ use nym_credentials_interface::{
VerificationKeyAuth, WithdrawalRequest,
};
use nym_crypto::asymmetric::ed25519;
use nym_crypto::asymmetric::ed25519::serde_helpers::bs58_ed25519_signature;
use nym_ticketbooks_merkle::{IssuedTicketbook, IssuedTicketbooksFullMerkleProof};
use serde::{Deserialize, Serialize};
use sha2::Digest;
use std::collections::BTreeMap;
use std::ops::Deref;
use thiserror::Error;
use time::Date;
use time::{Date, OffsetDateTime};
use utoipa::ToSchema;
#[derive(Serialize, Deserialize, Clone, ToSchema)]
@@ -541,74 +542,6 @@ pub struct CommitedDeposit {
pub merkle_index: usize,
}
//
//
// make sure only our types can implement this trait (to ensure infallible serialisation)
mod private {
use crate::ecash::models::{
IssuedTicketbooksChallengeCommitmentRequestBody,
IssuedTicketbooksChallengeCommitmentResponseBody, IssuedTicketbooksDataRequestBody,
IssuedTicketbooksDataResponseBody, IssuedTicketbooksForResponseBody,
};
pub trait Sealed {}
// requests
impl Sealed for IssuedTicketbooksChallengeCommitmentRequestBody {}
impl Sealed for IssuedTicketbooksDataRequestBody {}
// responses
impl Sealed for IssuedTicketbooksChallengeCommitmentResponseBody {}
impl Sealed for IssuedTicketbooksForResponseBody {}
impl Sealed for IssuedTicketbooksDataResponseBody {}
}
// the trait is not public as it's only defined on types that are guaranteed to not panic when serialised
pub trait SignableMessageBody: Serialize + private::Sealed {
fn sign(self, key: &ed25519::PrivateKey) -> SignedMessage<Self>
where
Self: Sized,
{
let signature = key.sign(self.plaintext());
SignedMessage {
body: self,
signature,
}
}
fn plaintext(&self) -> Vec<u8> {
#[allow(clippy::unwrap_used)]
// SAFETY: all types that implement this trait have valid serialisations
serde_json::to_vec(&self).unwrap()
}
}
impl<T> SignableMessageBody for T where T: Serialize + private::Sealed {}
#[derive(Clone, Serialize, Deserialize, Debug, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct SignedMessage<T> {
pub body: T,
#[schema(value_type = String)]
#[serde(with = "bs58_ed25519_signature")]
pub signature: ed25519::Signature,
}
impl<T> SignedMessage<T> {
pub fn verify_signature(&self, pub_key: &ed25519::PublicKey) -> bool
where
T: SignableMessageBody,
{
let plaintext = self.body.plaintext();
if plaintext.is_empty() {
return false;
}
pub_key.verify(&plaintext, &self.signature).is_ok()
}
}
pub type IssuedTicketbooksDataRequest = SignedMessage<IssuedTicketbooksDataRequestBody>;
pub type IssuedTicketbooksChallengeCommitmentRequest =
SignedMessage<IssuedTicketbooksChallengeCommitmentRequestBody>;
@@ -826,6 +759,31 @@ pub struct IssuedTicketbooksForCount {
pub count: u32,
}
pub type EcashSignerStatusResponse = SignedMessage<EcashSignerStatusResponseBody>;
#[derive(Serialize, Deserialize, ToSchema, Clone, Debug)]
#[serde(rename_all = "camelCase")]
// includes all pre-requisites for successful (assuming valid request) `/blind-sign`
pub struct EcashSignerStatusResponseBody {
#[serde(with = "time::serde::rfc3339")]
#[schema(value_type = String)]
pub current_time: OffsetDateTime,
/// Current, perceived, dkg epoch id
pub dkg_ecash_epoch_id: EpochId,
/// Flag indicating whether the operator has explicitly disabled signer functionalities in the api
pub signer_disabled: bool,
/// Flag indicating whether this api thinks it's a valid ecash signer for the current epoch
pub is_ecash_signer: bool,
/// Flag indicating whether this api thinks it has valid signing keys.
/// It might be a valid signer that's not disabled, but the keys might have accidentally been
/// removed due to invalid data migration.
pub has_signing_keys: bool,
}
#[cfg(test)]
mod tests {
use super::*;
+1
View File
@@ -11,6 +11,7 @@ pub mod legacy;
pub mod models;
pub mod nym_nodes;
pub mod pagination;
pub mod signable;
// The response type we fetch from the network details endpoint.
#[derive(Clone, Debug, Serialize, Deserialize)]
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,68 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use serde::{Deserialize, Serialize};
use std::time::Duration;
use utoipa::ToSchema;
#[derive(Clone, Copy, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub struct ApiHealthResponse {
pub status: ApiStatus,
#[serde(default)]
pub chain_status: ChainStatus,
pub uptime: u64,
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
#[serde(rename_all = "lowercase")]
pub enum ApiStatus {
Up,
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Default, schemars::JsonSchema, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum ChainStatus {
Synced,
#[default]
Unknown,
Stalled {
#[serde(
serialize_with = "humantime_serde::serialize",
deserialize_with = "humantime_serde::deserialize"
)]
approximate_amount: Duration,
},
}
impl ChainStatus {
pub fn is_synced(&self) -> bool {
matches!(self, ChainStatus::Synced)
}
}
impl ApiHealthResponse {
pub fn new_healthy(uptime: Duration) -> Self {
ApiHealthResponse {
status: ApiStatus::Up,
chain_status: ChainStatus::Synced,
uptime: uptime.as_secs(),
}
}
}
impl ApiStatus {
pub fn is_up(&self) -> bool {
matches!(self, ApiStatus::Up)
}
}
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub struct SignerInformationResponse {
pub cosmos_address: String,
pub identity: String,
pub announce_address: String,
pub verification_key: Option<String>,
}
@@ -0,0 +1,19 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use super::CoinSchema;
use cosmwasm_std::Coin;
use serde::{Deserialize, Serialize};
use utoipa::{ToResponse, ToSchema};
#[derive(Clone, Serialize, Deserialize, schemars::JsonSchema, ToSchema, ToResponse)]
pub struct CirculatingSupplyResponse {
#[schema(value_type = CoinSchema)]
pub total_supply: Coin,
#[schema(value_type = CoinSchema)]
pub mixmining_reserve: Coin,
#[schema(value_type = CoinSchema)]
pub vesting_tokens: Coin,
#[schema(value_type = CoinSchema)]
pub circulating_supply: Coin,
}
@@ -0,0 +1,375 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::models::{BinaryBuildInformationOwned, OffsetDateTimeJsonSchemaWrapper};
use crate::nym_nodes::{BasicEntryInformation, NodeRole, SemiSkimmedNode, SkimmedNode};
use nym_crypto::asymmetric::ed25519::serde_helpers::bs58_ed25519_pubkey;
use nym_crypto::asymmetric::x25519::serde_helpers::bs58_x25519_pubkey;
use nym_crypto::asymmetric::{ed25519, x25519};
use nym_mixnet_contract_common::reward_params::Performance;
use nym_mixnet_contract_common::NodeId;
use nym_network_defaults::{DEFAULT_MIX_LISTENING_PORT, DEFAULT_VERLOC_LISTENING_PORT};
use nym_node_requests::api::v1::authenticator::models::Authenticator;
use nym_node_requests::api::v1::gateway::models::Wireguard;
use nym_node_requests::api::v1::ip_packet_router::models::IpPacketRouter;
use nym_node_requests::api::v1::node::models::{AuxiliaryDetails, NodeRoles};
use nym_noise_keys::VersionedNoiseKey;
use serde::{Deserialize, Serialize};
use std::net::IpAddr;
use tracing::warn;
use utoipa::ToSchema;
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub struct HostInformation {
#[schema(value_type = Vec<String>)]
pub ip_address: Vec<IpAddr>,
pub hostname: Option<String>,
pub keys: HostKeys,
}
impl From<nym_node_requests::api::v1::node::models::HostInformation> for HostInformation {
fn from(value: nym_node_requests::api::v1::node::models::HostInformation) -> Self {
HostInformation {
ip_address: value.ip_address,
hostname: value.hostname,
keys: value.keys.into(),
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub struct HostKeys {
#[serde(with = "bs58_ed25519_pubkey")]
#[schemars(with = "String")]
#[schema(value_type = String)]
pub ed25519: ed25519::PublicKey,
#[deprecated(note = "use the current_x25519_sphinx_key with explicit rotation information")]
#[serde(with = "bs58_x25519_pubkey")]
#[schemars(with = "String")]
#[schema(value_type = String)]
pub x25519: x25519::PublicKey,
pub current_x25519_sphinx_key: SphinxKey,
#[serde(default)]
pub pre_announced_x25519_sphinx_key: Option<SphinxKey>,
#[serde(default)]
pub x25519_versioned_noise: Option<VersionedNoiseKey>,
}
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub struct SphinxKey {
pub rotation_id: u32,
#[serde(with = "bs58_x25519_pubkey")]
#[schemars(with = "String")]
#[schema(value_type = String)]
pub public_key: x25519::PublicKey,
}
impl From<nym_node_requests::api::v1::node::models::SphinxKey> for SphinxKey {
fn from(value: nym_node_requests::api::v1::node::models::SphinxKey) -> Self {
SphinxKey {
rotation_id: value.rotation_id,
public_key: value.public_key,
}
}
}
impl From<nym_node_requests::api::v1::node::models::HostKeys> for HostKeys {
fn from(value: nym_node_requests::api::v1::node::models::HostKeys) -> Self {
HostKeys {
ed25519: value.ed25519_identity,
x25519: value.x25519_sphinx,
current_x25519_sphinx_key: value.primary_x25519_sphinx_key.into(),
pre_announced_x25519_sphinx_key: value.pre_announced_x25519_sphinx_key.map(Into::into),
x25519_versioned_noise: value.x25519_versioned_noise,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub struct WebSockets {
pub ws_port: u16,
pub wss_port: Option<u16>,
}
impl From<nym_node_requests::api::v1::gateway::models::WebSockets> for WebSockets {
fn from(value: nym_node_requests::api::v1::gateway::models::WebSockets) -> Self {
WebSockets {
ws_port: value.ws_port,
wss_port: value.wss_port,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub struct NoiseDetails {
pub key: VersionedNoiseKey,
pub mixnet_port: u16,
#[schema(value_type = Vec<String>)]
pub ip_addresses: Vec<IpAddr>,
}
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub struct NymNodeDescription {
#[schema(value_type = u32)]
pub node_id: NodeId,
pub contract_node_type: DescribedNodeType,
pub description: NymNodeData,
}
impl NymNodeDescription {
pub fn version(&self) -> &str {
&self.description.build_information.build_version
}
pub fn entry_information(&self) -> BasicEntryInformation {
BasicEntryInformation {
hostname: self.description.host_information.hostname.clone(),
ws_port: self.description.mixnet_websockets.ws_port,
wss_port: self.description.mixnet_websockets.wss_port,
}
}
pub fn ed25519_identity_key(&self) -> ed25519::PublicKey {
self.description.host_information.keys.ed25519
}
pub fn current_sphinx_key(&self, current_rotation_id: u32) -> x25519::PublicKey {
let keys = &self.description.host_information.keys;
if keys.current_x25519_sphinx_key.rotation_id == u32::MAX {
// legacy case (i.e. node doesn't support rotation)
return keys.current_x25519_sphinx_key.public_key;
}
if current_rotation_id == keys.current_x25519_sphinx_key.rotation_id {
// it's the 'current' key
return keys.current_x25519_sphinx_key.public_key;
}
if let Some(pre_announced) = &keys.pre_announced_x25519_sphinx_key {
if pre_announced.rotation_id == current_rotation_id {
return pre_announced.public_key;
}
}
warn!(
"unexpected key rotation {current_rotation_id} for node {}",
self.node_id
);
// this should never be reached, but just in case, return the fallback option
keys.current_x25519_sphinx_key.public_key
}
pub fn to_skimmed_node(
&self,
current_rotation_id: u32,
role: NodeRole,
performance: Performance,
) -> SkimmedNode {
let keys = &self.description.host_information.keys;
let entry = if self.description.declared_role.entry {
Some(self.entry_information())
} else {
None
};
SkimmedNode {
node_id: self.node_id,
ed25519_identity_pubkey: keys.ed25519,
ip_addresses: self.description.host_information.ip_address.clone(),
mix_port: self.description.mix_port(),
x25519_sphinx_pubkey: self.current_sphinx_key(current_rotation_id),
// we can't use the declared roles, we have to take whatever was provided in the contract.
// why? say this node COULD operate as an exit, but it might be the case the contract decided
// to assign it an ENTRY role only. we have to use that one instead.
role,
supported_roles: self.description.declared_role,
entry,
performance,
}
}
pub fn to_semi_skimmed_node(
&self,
current_rotation_id: u32,
role: NodeRole,
performance: Performance,
) -> SemiSkimmedNode {
let skimmed_node = self.to_skimmed_node(current_rotation_id, role, performance);
SemiSkimmedNode {
basic: skimmed_node,
x25519_noise_versioned_key: self
.description
.host_information
.keys
.x25519_versioned_noise,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
#[serde(rename_all = "snake_case")]
#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))]
#[cfg_attr(
feature = "generate-ts",
ts(
export,
export_to = "ts-packages/types/src/types/rust/DescribedNodeType.ts"
)
)]
pub enum DescribedNodeType {
LegacyMixnode,
LegacyGateway,
NymNode,
}
impl DescribedNodeType {
pub fn is_nym_node(&self) -> bool {
matches!(self, DescribedNodeType::NymNode)
}
}
#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))]
#[cfg_attr(
feature = "generate-ts",
ts(
export,
export_to = "ts-packages/types/src/types/rust/DeclaredRoles.ts"
)
)]
pub struct DeclaredRoles {
pub mixnode: bool,
pub entry: bool,
pub exit_nr: bool,
pub exit_ipr: bool,
}
impl DeclaredRoles {
pub fn can_operate_exit_gateway(&self) -> bool {
self.exit_ipr && self.exit_nr
}
}
impl From<NodeRoles> for DeclaredRoles {
fn from(value: NodeRoles) -> Self {
DeclaredRoles {
mixnode: value.mixnode_enabled,
entry: value.gateway_enabled,
exit_nr: value.gateway_enabled && value.network_requester_enabled,
exit_ipr: value.gateway_enabled && value.ip_packet_router_enabled,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub struct NetworkRequesterDetails {
/// address of the embedded network requester
pub address: String,
/// flag indicating whether this network requester uses the exit policy rather than the deprecated allow list
pub uses_exit_policy: bool,
}
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub struct IpPacketRouterDetails {
/// address of the embedded ip packet router
pub address: String,
}
// works for current simple case.
impl From<IpPacketRouter> for IpPacketRouterDetails {
fn from(value: IpPacketRouter) -> Self {
IpPacketRouterDetails {
address: value.address,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub struct AuthenticatorDetails {
/// address of the embedded authenticator
pub address: String,
}
// works for current simple case.
impl From<Authenticator> for AuthenticatorDetails {
fn from(value: Authenticator) -> Self {
AuthenticatorDetails {
address: value.address,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub struct WireguardDetails {
pub port: u16,
pub public_key: String,
}
// works for current simple case.
impl From<Wireguard> for WireguardDetails {
fn from(value: Wireguard) -> Self {
WireguardDetails {
port: value.port,
public_key: value.public_key,
}
}
}
// this struct is getting quite bloated...
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub struct NymNodeData {
#[serde(default)]
pub last_polled: OffsetDateTimeJsonSchemaWrapper,
pub host_information: HostInformation,
#[serde(default)]
pub declared_role: DeclaredRoles,
#[serde(default)]
pub auxiliary_details: AuxiliaryDetails,
// TODO: do we really care about ALL build info or just the version?
pub build_information: BinaryBuildInformationOwned,
#[serde(default)]
pub network_requester: Option<NetworkRequesterDetails>,
#[serde(default)]
pub ip_packet_router: Option<IpPacketRouterDetails>,
#[serde(default)]
pub authenticator: Option<AuthenticatorDetails>,
#[serde(default)]
pub wireguard: Option<WireguardDetails>,
// for now we only care about their ws/wss situation, nothing more
pub mixnet_websockets: WebSockets,
}
impl NymNodeData {
pub fn mix_port(&self) -> u16 {
self.auxiliary_details
.announce_ports
.mix_port
.unwrap_or(DEFAULT_MIX_LISTENING_PORT)
}
pub fn verloc_port(&self) -> u16 {
self.auxiliary_details
.announce_ports
.verloc_port
.unwrap_or(DEFAULT_VERLOC_LISTENING_PORT)
}
}
@@ -0,0 +1,364 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use super::{CoinSchema, DeclaredRoles};
use crate::legacy::{
LegacyGatewayBondWithId, LegacyMixNodeBondWithLayer, LegacyMixNodeDetailsWithLayer,
};
use crate::models::{NodePerformance, NymNodeData, StakeSaturation};
use crate::nym_nodes::{BasicEntryInformation, NodeRole, SemiSkimmedNode, SkimmedNode};
use cosmwasm_std::{Addr, Coin, Decimal};
use nym_contracts_common::Percent;
use nym_mixnet_contract_common::reward_params::Performance;
use nym_mixnet_contract_common::rewarding::RewardEstimate;
use nym_mixnet_contract_common::{GatewayBond, Interval, MixNode, NodeId, RewardingParams};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::fmt;
use std::net::IpAddr;
use std::time::Duration;
use thiserror::Error;
use utoipa::{IntoParams, ToSchema};
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub struct LegacyDescribedGateway {
pub bond: GatewayBond,
pub self_described: Option<NymNodeData>,
}
impl From<LegacyGatewayBondWithId> for LegacyDescribedGateway {
fn from(bond: LegacyGatewayBondWithId) -> Self {
LegacyDescribedGateway {
bond: bond.bond,
self_described: None,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub struct LegacyDescribedMixNode {
pub bond: LegacyMixNodeBondWithLayer,
pub self_described: Option<NymNodeData>,
}
impl From<LegacyMixNodeBondWithLayer> for LegacyDescribedMixNode {
fn from(bond: LegacyMixNodeBondWithLayer) -> Self {
LegacyDescribedMixNode {
bond,
self_described: None,
}
}
}
#[deprecated]
#[derive(Clone, Serialize, schemars::JsonSchema, ToSchema)]
pub struct InclusionProbability {
#[schema(value_type = u32)]
pub mix_id: NodeId,
pub in_active: f64,
pub in_reserve: f64,
}
#[deprecated]
#[derive(Clone, Serialize, schemars::JsonSchema, ToSchema)]
pub struct AllInclusionProbabilitiesResponse {
pub inclusion_probabilities: Vec<InclusionProbability>,
pub samples: u64,
pub elapsed: Duration,
pub delta_max: f64,
pub delta_l2: f64,
pub as_at: i64,
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, ToSchema)]
#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))]
#[cfg_attr(
feature = "generate-ts",
ts(
export,
export_to = "ts-packages/types/src/types/rust/SelectionChance.ts"
)
)]
#[deprecated]
pub enum SelectionChance {
High,
Good,
Low,
}
impl From<f64> for SelectionChance {
fn from(p: f64) -> SelectionChance {
match p {
p if p >= 0.7 => SelectionChance::High,
p if p >= 0.3 => SelectionChance::Good,
_ => SelectionChance::Low,
}
}
}
impl From<Decimal> for SelectionChance {
fn from(p: Decimal) -> Self {
match p {
p if p >= Decimal::from_ratio(70u32, 100u32) => SelectionChance::High,
p if p >= Decimal::from_ratio(30u32, 100u32) => SelectionChance::Good,
_ => SelectionChance::Low,
}
}
}
impl fmt::Display for SelectionChance {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
SelectionChance::High => write!(f, "High"),
SelectionChance::Good => write!(f, "Good"),
SelectionChance::Low => write!(f, "Low"),
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, ToSchema)]
#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))]
#[cfg_attr(
feature = "generate-ts",
ts(
export,
export_to = "ts-packages/types/src/types/rust/InclusionProbabilityResponse.ts"
)
)]
#[deprecated]
pub struct InclusionProbabilityResponse {
pub in_active: SelectionChance,
pub in_reserve: SelectionChance,
}
impl fmt::Display for InclusionProbabilityResponse {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"in_active: {}, in_reserve: {}",
self.in_active, self.in_reserve
)
}
}
#[derive(Debug, Serialize, Deserialize, JsonSchema, ToSchema, IntoParams)]
pub struct ComputeRewardEstParam {
#[schema(value_type = Option<String>)]
#[param(value_type = Option<String>)]
pub performance: Option<Performance>,
pub active_in_rewarded_set: Option<bool>,
pub pledge_amount: Option<u64>,
pub total_delegation: Option<u64>,
#[schema(value_type = Option<CoinSchema>)]
#[param(value_type = Option<CoinSchema>)]
pub interval_operating_cost: Option<Coin>,
#[schema(value_type = Option<String>)]
#[param(value_type = Option<String>)]
pub profit_margin_percent: Option<Percent>,
}
#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))]
#[cfg_attr(
feature = "generate-ts",
ts(
export,
export_to = "ts-packages/types/src/types/rust/RewardEstimationResponse.ts"
)
)]
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, ToSchema)]
pub struct RewardEstimationResponse {
pub estimation: RewardEstimate,
pub reward_params: RewardingParams,
pub epoch: Interval,
#[cfg_attr(feature = "generate-ts", ts(type = "number"))]
pub as_at: i64,
}
impl MixNodeBondAnnotated {
pub fn mix_node(&self) -> &MixNode {
&self.mixnode_details.bond_information.mix_node
}
pub fn mix_id(&self) -> NodeId {
self.mixnode_details.mix_id()
}
pub fn identity_key(&self) -> &str {
self.mixnode_details.bond_information.identity()
}
pub fn owner(&self) -> &Addr {
self.mixnode_details.bond_information.owner()
}
pub fn version(&self) -> &str {
&self.mixnode_details.bond_information.mix_node.version
}
pub fn try_to_skimmed_node(&self, role: NodeRole) -> Result<SkimmedNode, MalformedNodeBond> {
Ok(SkimmedNode {
node_id: self.mix_id(),
ed25519_identity_pubkey: self
.identity_key()
.parse()
.map_err(|_| MalformedNodeBond::InvalidEd25519Key)?,
ip_addresses: self.ip_addresses.clone(),
mix_port: self.mix_node().mix_port,
x25519_sphinx_pubkey: self
.mix_node()
.sphinx_key
.parse()
.map_err(|_| MalformedNodeBond::InvalidX25519Key)?,
role,
supported_roles: DeclaredRoles {
mixnode: true,
entry: false,
exit_nr: false,
exit_ipr: false,
},
entry: None,
performance: self.node_performance.last_24h,
})
}
pub fn try_to_semi_skimmed_node(
&self,
role: NodeRole,
) -> Result<SemiSkimmedNode, MalformedNodeBond> {
let skimmed_node = self.try_to_skimmed_node(role)?;
Ok(SemiSkimmedNode {
basic: skimmed_node,
x25519_noise_versioned_key: None, // legacy node won't ever support Noise
})
}
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, ToSchema)]
pub struct GatewayBondAnnotated {
pub gateway_bond: LegacyGatewayBondWithId,
#[serde(default)]
pub self_described: Option<GatewayDescription>,
// NOTE: the performance field is deprecated in favour of node_performance
#[schema(value_type = String)]
pub performance: Performance,
pub node_performance: NodePerformance,
pub blacklisted: bool,
#[serde(default)]
#[schema(value_type = Vec<String>)]
pub ip_addresses: Vec<IpAddr>,
}
impl GatewayBondAnnotated {
pub fn version(&self) -> &str {
&self.gateway_bond.gateway.version
}
pub fn identity(&self) -> &String {
self.gateway_bond.bond.identity()
}
pub fn owner(&self) -> &Addr {
self.gateway_bond.bond.owner()
}
pub fn try_to_skimmed_node(&self, role: NodeRole) -> Result<SkimmedNode, MalformedNodeBond> {
Ok(SkimmedNode {
node_id: self.gateway_bond.node_id,
ip_addresses: self.ip_addresses.clone(),
ed25519_identity_pubkey: self
.gateway_bond
.gateway
.identity_key
.parse()
.map_err(|_| MalformedNodeBond::InvalidEd25519Key)?,
mix_port: self.gateway_bond.bond.gateway.mix_port,
x25519_sphinx_pubkey: self
.gateway_bond
.gateway
.sphinx_key
.parse()
.map_err(|_| MalformedNodeBond::InvalidX25519Key)?,
role,
supported_roles: DeclaredRoles {
mixnode: false,
entry: true,
exit_nr: false,
exit_ipr: false,
},
entry: Some(BasicEntryInformation {
hostname: None,
ws_port: self.gateway_bond.bond.gateway.clients_port,
wss_port: None,
}),
performance: self.node_performance.last_24h,
})
}
pub fn try_to_semi_skimmed_node(
&self,
role: NodeRole,
) -> Result<SemiSkimmedNode, MalformedNodeBond> {
let skimmed_node = self.try_to_skimmed_node(role)?;
Ok(SemiSkimmedNode {
basic: skimmed_node,
x25519_noise_versioned_key: None, // legacy node won't ever support Noise
})
}
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, ToSchema)]
pub struct GatewayDescription {
// for now only expose what we need. this struct will evolve in the future (or be incorporated into nym-node properly)
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, ToSchema)]
#[schema(title = "LegacyMixNodeDetailsWithLayer")]
pub struct LegacyMixNodeDetailsWithLayerSchema {
/// Basic bond information of this mixnode, such as owner address, original pledge, etc.
#[schema(example = "unimplemented schema")]
pub bond_information: String,
/// Details used for computation of rewarding related data.
#[schema(example = "unimplemented schema")]
pub rewarding_details: String,
/// Adjustments to the mixnode that are ought to happen during future epoch transitions.
#[schema(example = "unimplemented schema")]
pub pending_changes: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, ToSchema)]
pub struct MixNodeBondAnnotated {
#[schema(value_type = LegacyMixNodeDetailsWithLayerSchema)]
pub mixnode_details: LegacyMixNodeDetailsWithLayer,
#[schema(value_type = String)]
pub stake_saturation: StakeSaturation,
#[schema(value_type = String)]
pub uncapped_stake_saturation: StakeSaturation,
// NOTE: the performance field is deprecated in favour of node_performance
#[schema(value_type = String)]
pub performance: Performance,
pub node_performance: NodePerformance,
#[schema(value_type = String)]
pub estimated_operator_apy: Decimal,
#[schema(value_type = String)]
pub estimated_delegators_apy: Decimal,
pub blacklisted: bool,
// a rather temporary thing until we query self-described endpoints of mixnodes
#[serde(default)]
#[schema(value_type = Vec<String>)]
pub ip_addresses: Vec<IpAddr>,
}
#[derive(Debug, Error)]
pub enum MalformedNodeBond {
#[error("the associated ed25519 identity key is malformed")]
InvalidEd25519Key,
#[error("the associated x25519 sphinx key is malformed")]
InvalidX25519Key,
}
@@ -0,0 +1,242 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use nym_mixnet_contract_common::nym_node::Role;
use nym_mixnet_contract_common::{EpochId, KeyRotationId, KeyRotationState, NodeId};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::cmp::Ordering;
use std::time::Duration;
use time::OffsetDateTime;
use tracing::warn;
use utoipa::ToSchema;
#[derive(Clone, Copy, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub struct KeyRotationInfoResponse {
#[serde(flatten)]
pub details: KeyRotationDetails,
// helper field that holds calculated data based on the `details` field
// this is to expose the information in a format more easily accessible by humans
// without having to do any calculations
pub progress: KeyRotationProgressInfo,
}
impl From<KeyRotationDetails> for KeyRotationInfoResponse {
fn from(details: KeyRotationDetails) -> Self {
KeyRotationInfoResponse {
details,
progress: KeyRotationProgressInfo {
current_key_rotation_id: details.current_key_rotation_id(),
current_rotation_starting_epoch: details.current_rotation_starting_epoch_id(),
current_rotation_ending_epoch: details.current_rotation_starting_epoch_id()
+ details.key_rotation_state.validity_epochs
- 1,
},
}
}
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub struct KeyRotationProgressInfo {
pub current_key_rotation_id: u32,
pub current_rotation_starting_epoch: u32,
pub current_rotation_ending_epoch: u32,
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub struct KeyRotationDetails {
pub key_rotation_state: KeyRotationState,
#[schema(value_type = u32)]
pub current_absolute_epoch_id: EpochId,
#[serde(with = "time::serde::rfc3339")]
#[schemars(with = "String")]
#[schema(value_type = String)]
pub current_epoch_start: OffsetDateTime,
pub epoch_duration: Duration,
}
impl KeyRotationDetails {
pub fn current_key_rotation_id(&self) -> u32 {
self.key_rotation_state
.key_rotation_id(self.current_absolute_epoch_id)
}
pub fn next_rotation_starting_epoch_id(&self) -> EpochId {
self.key_rotation_state
.next_rotation_starting_epoch_id(self.current_absolute_epoch_id)
}
pub fn current_rotation_starting_epoch_id(&self) -> EpochId {
self.key_rotation_state
.current_rotation_starting_epoch_id(self.current_absolute_epoch_id)
}
fn current_epoch_progress(&self, now: OffsetDateTime) -> f32 {
let elapsed = (now - self.current_epoch_start).as_seconds_f32();
elapsed / self.epoch_duration.as_secs_f32()
}
pub fn is_epoch_stuck(&self) -> bool {
let now = OffsetDateTime::now_utc();
let progress = self.current_epoch_progress(now);
if progress > 1. {
let into_next = 1. - progress;
// if epoch hasn't progressed for more than 20% of its duration, mark is as stuck
if into_next > 0.2 {
let diff_time =
Duration::from_secs_f32(into_next * self.epoch_duration.as_secs_f32());
let expected_epoch_end = self.current_epoch_start + self.epoch_duration;
warn!("the current epoch is expected to have been over by {expected_epoch_end}. it's already {} overdue!", humantime_serde::re::humantime::format_duration(diff_time));
return true;
}
}
false
}
// based on the current **TIME**, determine what's the expected current rotation id
pub fn expected_current_rotation_id(&self) -> KeyRotationId {
let now = OffsetDateTime::now_utc();
let current_end = now + self.epoch_duration;
if now < current_end {
return self
.key_rotation_state
.key_rotation_id(self.current_absolute_epoch_id);
}
let diff = now - current_end;
let passed_epochs = diff / self.epoch_duration;
let expected_current_epoch = self.current_absolute_epoch_id + passed_epochs.floor() as u32;
self.key_rotation_state
.key_rotation_id(expected_current_epoch)
}
pub fn until_next_rotation(&self) -> Option<Duration> {
let current_epoch_progress = self.current_epoch_progress(OffsetDateTime::now_utc());
if current_epoch_progress > 1. {
return None;
}
let next_rotation_epoch = self.next_rotation_starting_epoch_id();
let full_remaining =
(next_rotation_epoch - self.current_absolute_epoch_id).checked_add(1)?;
let epochs_until_next_rotation = (1. - current_epoch_progress) + full_remaining as f32;
Some(Duration::from_secs_f32(
epochs_until_next_rotation * self.epoch_duration.as_secs_f32(),
))
}
pub fn epoch_start_time(&self, absolute_epoch_id: EpochId) -> OffsetDateTime {
match absolute_epoch_id.cmp(&self.current_absolute_epoch_id) {
Ordering::Less => {
let diff = self.current_absolute_epoch_id - absolute_epoch_id;
self.current_epoch_start - diff * self.epoch_duration
}
Ordering::Equal => self.current_epoch_start,
Ordering::Greater => {
let diff = absolute_epoch_id - self.current_absolute_epoch_id;
self.current_epoch_start + diff * self.epoch_duration
}
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub struct RewardedSetResponse {
#[serde(default)]
#[schema(value_type = u32)]
pub epoch_id: EpochId,
pub entry_gateways: Vec<NodeId>,
pub exit_gateways: Vec<NodeId>,
pub layer1: Vec<NodeId>,
pub layer2: Vec<NodeId>,
pub layer3: Vec<NodeId>,
pub standby: Vec<NodeId>,
}
impl From<RewardedSetResponse> for nym_mixnet_contract_common::EpochRewardedSet {
fn from(res: RewardedSetResponse) -> Self {
nym_mixnet_contract_common::EpochRewardedSet {
epoch_id: res.epoch_id,
assignment: nym_mixnet_contract_common::RewardedSet {
entry_gateways: res.entry_gateways,
exit_gateways: res.exit_gateways,
layer1: res.layer1,
layer2: res.layer2,
layer3: res.layer3,
standby: res.standby,
},
}
}
}
impl From<nym_mixnet_contract_common::EpochRewardedSet> for RewardedSetResponse {
fn from(r: nym_mixnet_contract_common::EpochRewardedSet) -> Self {
RewardedSetResponse {
epoch_id: r.epoch_id,
entry_gateways: r.assignment.entry_gateways,
exit_gateways: r.assignment.exit_gateways,
layer1: r.assignment.layer1,
layer2: r.assignment.layer2,
layer3: r.assignment.layer3,
standby: r.assignment.standby,
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, JsonSchema, ToSchema)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))]
#[cfg_attr(
feature = "generate-ts",
ts(export, export_to = "ts-packages/types/src/types/rust/DisplayRole.ts")
)]
pub enum DisplayRole {
EntryGateway,
Layer1,
Layer2,
Layer3,
ExitGateway,
Standby,
}
impl From<Role> for DisplayRole {
fn from(role: Role) -> Self {
match role {
Role::EntryGateway => DisplayRole::EntryGateway,
Role::Layer1 => DisplayRole::Layer1,
Role::Layer2 => DisplayRole::Layer2,
Role::Layer3 => DisplayRole::Layer3,
Role::ExitGateway => DisplayRole::ExitGateway,
Role::Standby => DisplayRole::Standby,
}
}
}
impl From<DisplayRole> for Role {
fn from(role: DisplayRole) -> Self {
match role {
DisplayRole::EntryGateway => Role::EntryGateway,
DisplayRole::Layer1 => Role::Layer1,
DisplayRole::Layer2 => Role::Layer2,
DisplayRole::Layer3 => Role::Layer3,
DisplayRole::ExitGateway => Role::ExitGateway,
DisplayRole::Standby => Role::Standby,
}
}
}
@@ -0,0 +1,62 @@
// Copyright 2022-2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
#![allow(deprecated)]
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::fmt;
use std::fmt::{Display, Formatter};
pub mod api_status;
pub mod circulating_supply;
pub mod described;
pub mod legacy;
pub mod mixnet;
pub mod network;
pub mod network_monitor;
pub mod node_status;
pub mod schema_helpers;
// don't break existing imports
pub use api_status::*;
pub use circulating_supply::*;
pub use described::*;
pub use legacy::*;
pub use mixnet::*;
pub use network::*;
pub use network_monitor::*;
pub use node_status::*;
pub use schema_helpers::*;
pub use nym_mixnet_contract_common::{EpochId, KeyRotationId, KeyRotationState};
pub use nym_node_requests::api::v1::node::models::BinaryBuildInformationOwned;
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
pub struct RequestError {
message: String,
}
impl RequestError {
pub fn new<S: Into<String>>(msg: S) -> Self {
RequestError {
message: msg.into(),
}
}
pub fn message(&self) -> &str {
&self.message
}
pub fn empty() -> Self {
Self {
message: String::new(),
}
}
}
impl Display for RequestError {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
Display::fmt(&self.message, f)
}
}
@@ -0,0 +1,562 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::ecash::models::EcashSignerStatusResponse;
use crate::models::tendermint_types::{BlockHeader, BlockId};
use crate::models::{ChainStatus, SignerInformationResponse};
use crate::signable::SignedMessage;
use nym_coconut_dkg_common::types::EpochId;
use nym_crypto::asymmetric::ed25519::PublicKey;
use nym_ecash_signer_check_types::helper_traits::{
ChainResponse, LegacyChainResponse, LegacySignerResponse, SignerResponse, TimestampedResponse,
Verifiable,
};
use nym_ecash_signer_check_types::status::SignerResult;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::time::Duration;
use time::OffsetDateTime;
use utoipa::ToSchema;
pub type ChainBlocksStatusResponse = SignedMessage<ChainBlocksStatusResponseBody>;
pub type SignersStatusResponse = SignedMessage<SignersStatusResponseBody>;
pub type DetailedSignersStatusResponse = SignedMessage<DetailedSignersStatusResponseBody>;
#[derive(Serialize, Deserialize, ToSchema, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct SignersStatusResponseBody {
#[serde(with = "time::serde::rfc3339")]
#[schema(value_type = String)]
pub as_at: OffsetDateTime,
pub overview: SignersStatusOverview,
pub results: Vec<MinimalSignerResult>,
}
pub type TypedSignerResult = SignerResult<
SignerInformationResponse,
EcashSignerStatusResponse,
ChainStatusResponse,
ChainBlocksStatusResponse,
>;
#[derive(Serialize, Deserialize, ToSchema, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct MinimalSignerResult {
pub announce_address: String,
pub owner_address: String,
pub node_index: u64,
pub public_key: String,
pub local_chain_working: bool,
pub credential_issuance_available: bool,
}
impl From<&TypedSignerResult> for MinimalSignerResult {
fn from(result: &TypedSignerResult) -> MinimalSignerResult {
MinimalSignerResult {
announce_address: result.information.announce_address.clone(),
owner_address: result.information.owner_address.clone(),
node_index: result.information.node_index,
public_key: result.information.public_key.clone(),
local_chain_working: result.chain_available(),
credential_issuance_available: result.signing_available(),
}
}
}
#[derive(Serialize, Deserialize, ToSchema, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct DetailedSignersStatusResponseBody {
#[serde(with = "time::serde::rfc3339")]
#[schema(value_type = String)]
pub as_at: OffsetDateTime,
pub overview: SignersStatusOverview,
pub details: Vec<TypedSignerResult>,
}
#[derive(Serialize, Deserialize, ToSchema, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct SignersStatusOverview {
#[schema(value_type = Option<u64>)]
pub epoch_id: Option<EpochId>,
pub signing_threshold: Option<u64>,
pub threshold_available: Option<bool>,
pub total_signers: usize,
pub unreachable_signers: usize,
pub malformed_signers: usize,
// unreachable or outdated
pub unknown_local_chain_status: usize,
pub working_local_chain: usize,
// i.e. provided signature
pub provably_stalled_local_chain: usize,
pub unprovably_stalled_local_chain: usize,
// unreachable or outdated
pub unknown_credential_issuance_status: usize,
pub working_credential_issuance: usize,
// i.e. provided signature
pub provably_unavailable_credential_issuance: usize,
pub unprovably_unavailable_credential_issuance: usize,
}
impl SignersStatusOverview {
pub fn new(results: &[TypedSignerResult], signing_threshold: Option<u64>) -> Self {
let epoch_id = results.first().map(|r| r.dkg_epoch_id);
let mut unreachable_signers = 0;
let mut malformed_signers = 0;
let mut unknown_local_chain_status = 0;
let mut working_local_chain = 0;
let mut provably_stalled_local_chain = 0;
let mut unprovably_stalled_local_chain = 0;
let mut unknown_credential_issuance_status = 0;
let mut working_credential_issuance = 0;
let mut provably_unavailable_credential_issuance = 0;
let mut unprovably_unavailable_credential_issuance = 0;
for result in results {
if result.signer_unreachable() {
unreachable_signers += 1;
}
if result.malformed_details() {
malformed_signers += 1;
}
if result.unknown_chain_status() {
unknown_local_chain_status += 1;
}
if result.chain_available() {
working_local_chain += 1;
}
if result.chain_provably_stalled() {
provably_stalled_local_chain += 1;
}
if result.chain_unprovably_stalled() {
unprovably_stalled_local_chain += 1;
}
if result.unknown_signing_status() {
unknown_credential_issuance_status += 1;
}
if result.signing_available() {
working_credential_issuance += 1;
}
if result.signing_provably_unavailable() {
provably_unavailable_credential_issuance += 1;
}
if result.signing_unprovably_unavailable() {
unprovably_unavailable_credential_issuance += 1;
}
}
SignersStatusOverview {
epoch_id,
signing_threshold,
threshold_available: signing_threshold.map(|threshold| {
(working_local_chain as u64) >= threshold
&& (working_credential_issuance as u64) >= threshold
}),
total_signers: results.len(),
unreachable_signers,
malformed_signers,
unknown_local_chain_status,
working_local_chain,
provably_stalled_local_chain,
unprovably_stalled_local_chain,
unknown_credential_issuance_status,
working_credential_issuance,
provably_unavailable_credential_issuance,
unprovably_unavailable_credential_issuance,
}
}
}
#[derive(Serialize, Deserialize, ToSchema, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct ChainBlocksStatusResponseBody {
#[serde(with = "time::serde::rfc3339")]
#[schema(value_type = String)]
pub current_time: OffsetDateTime,
pub latest_cached_block: Option<DetailedChainStatus>,
// explicit indication of THIS signer whether it thinks the chain is stalled
pub chain_status: ChainStatus,
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, ToSchema)]
pub struct ChainStatusResponse {
pub connected_nyxd: String,
pub status: DetailedChainStatus,
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, ToSchema)]
pub struct DetailedChainStatus {
pub abci: crate::models::tendermint_types::AbciInfo,
pub latest_block: BlockInfo,
}
impl DetailedChainStatus {
pub fn stall_status(&self, now: OffsetDateTime, threshold: Duration) -> ChainStatus {
let block_time: OffsetDateTime = self.latest_block.block.header.time.into();
let diff = now - block_time;
if diff > threshold {
ChainStatus::Stalled {
approximate_amount: diff.unsigned_abs(),
}
} else {
ChainStatus::Synced
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, ToSchema)]
pub struct BlockInfo {
pub block_id: BlockId,
pub block: FullBlockInfo,
// if necessary we might put block data here later too
}
impl From<tendermint_rpc::endpoint::block::Response> for BlockInfo {
fn from(value: tendermint_rpc::endpoint::block::Response) -> Self {
BlockInfo {
block_id: value.block_id.into(),
block: FullBlockInfo {
header: value.block.header.into(),
},
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, ToSchema)]
pub struct FullBlockInfo {
pub header: BlockHeader,
}
// copy tendermint types definitions whilst deriving schema types on them and dropping unwanted fields
pub mod tendermint_types {
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use tendermint::abci::response::Info;
use tendermint::block::header::Version;
use tendermint::{block, Hash};
use utoipa::ToSchema;
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, ToSchema)]
pub struct AbciInfo {
/// Some arbitrary information.
pub data: String,
/// The application software semantic version.
pub version: String,
/// The application protocol version.
pub app_version: u64,
/// The latest block for which the app has called [`Commit`].
pub last_block_height: u64,
/// The latest result of [`Commit`].
pub last_block_app_hash: String,
}
impl From<Info> for AbciInfo {
fn from(value: Info) -> Self {
AbciInfo {
data: value.data,
version: value.version,
app_version: value.app_version,
last_block_height: value.last_block_height.value(),
last_block_app_hash: value.last_block_app_hash.to_string(),
}
}
}
/// `Version` contains the protocol version for the blockchain and the
/// application.
///
/// <https://github.com/tendermint/spec/blob/d46cd7f573a2c6a2399fcab2cde981330aa63f37/spec/core/data_structures.md#version>
#[derive(
Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema, ToSchema,
)]
pub struct HeaderVersion {
/// Block version
pub block: u64,
/// App version
pub app: u64,
}
impl From<tendermint::block::header::Version> for HeaderVersion {
fn from(value: Version) -> Self {
HeaderVersion {
block: value.block,
app: value.app,
}
}
}
/// Block identifiers which contain two distinct Merkle roots of the block,
/// as well as the number of parts in the block.
///
/// <https://github.com/tendermint/spec/blob/d46cd7f573a2c6a2399fcab2cde981330aa63f37/spec/core/data_structures.md#blockid>
///
/// Default implementation is an empty Id as defined by the Go implementation in
/// <https://github.com/tendermint/tendermint/blob/1635d1339c73ae6a82e062cd2dc7191b029efa14/types/block.go#L1204>.
///
/// If the Hash is empty in BlockId, the BlockId should be empty (encoded to None).
/// This is implemented outside of this struct. Use the Default trait to check for an empty BlockId.
/// See: <https://github.com/informalsystems/tendermint-rs/issues/663>
#[derive(
Serialize,
Deserialize,
Copy,
Clone,
Debug,
Default,
Hash,
Eq,
PartialEq,
PartialOrd,
Ord,
JsonSchema,
ToSchema,
)]
pub struct BlockId {
/// The block's main hash is the Merkle root of all the fields in the
/// block header.
#[schemars(with = "String")]
#[schema(value_type = String)]
pub hash: Hash,
/// Parts header (if available) is used for secure gossipping of the block
/// during consensus. It is the Merkle root of the complete serialized block
/// cut into parts.
///
/// PartSet is used to split a byteslice of data into parts (pieces) for
/// transmission. By splitting data into smaller parts and computing a
/// Merkle root hash on the list, you can verify that a part is
/// legitimately part of the complete data, and the part can be forwarded
/// to other peers before all the parts are known. In short, it's a fast
/// way to propagate a large file over a gossip network.
///
/// <https://github.com/tendermint/tendermint/wiki/Block-Structure#partset>
///
/// PartSetHeader in protobuf is defined as never nil using the gogoproto
/// annotations. This does not translate to Rust, but we can indicate this
/// in the domain type.
pub part_set_header: PartSetHeader,
}
impl From<block::Id> for BlockId {
fn from(value: block::Id) -> Self {
BlockId {
hash: value.hash,
part_set_header: value.part_set_header.into(),
}
}
}
/// Block parts header
#[derive(
Clone,
Copy,
Debug,
Default,
Hash,
Eq,
PartialEq,
PartialOrd,
Ord,
Deserialize,
Serialize,
JsonSchema,
ToSchema,
)]
#[non_exhaustive]
pub struct PartSetHeader {
/// Number of parts in this block
pub total: u32,
/// Hash of the parts set header,
#[schemars(with = "String")]
#[schema(value_type = String)]
pub hash: Hash,
}
impl From<tendermint::block::parts::Header> for PartSetHeader {
fn from(value: block::parts::Header) -> Self {
PartSetHeader {
total: value.total,
hash: value.hash,
}
}
}
/// Block `Header` values contain metadata about the block and about the
/// consensus, as well as commitments to the data in the current block, the
/// previous block, and the results returned by the application.
///
/// <https://github.com/tendermint/spec/blob/d46cd7f573a2c6a2399fcab2cde981330aa63f37/spec/core/data_structures.md#header>
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
pub struct BlockHeader {
/// Header version
pub version: HeaderVersion,
/// Chain ID
pub chain_id: String,
/// Current block height
pub height: u64,
/// Current timestamp
#[schemars(with = "String")]
#[schema(value_type = String)]
pub time: tendermint::Time,
/// Previous block info
pub last_block_id: Option<BlockId>,
/// Commit from validators from the last block
#[schemars(with = "Option<String>")]
#[schema(value_type = Option<String>)]
pub last_commit_hash: Option<Hash>,
/// Merkle root of transaction hashes
#[schemars(with = "Option<String>")]
#[schema(value_type = Option<String>)]
pub data_hash: Option<Hash>,
/// Validators for the current block
#[schemars(with = "String")]
#[schema(value_type = String)]
pub validators_hash: Hash,
/// Validators for the next block
#[schemars(with = "String")]
#[schema(value_type = String)]
pub next_validators_hash: Hash,
/// Consensus params for the current block
#[schemars(with = "String")]
#[schema(value_type = String)]
pub consensus_hash: Hash,
/// State after txs from the previous block
#[schemars(with = "String")]
#[schema(value_type = String)]
pub app_hash: Hash,
/// Root hash of all results from the txs from the previous block
#[schemars(with = "Option<String>")]
#[schema(value_type = Option<String>)]
pub last_results_hash: Option<Hash>,
/// Hash of evidence included in the block
#[schemars(with = "Option<String>")]
#[schema(value_type = Option<String>)]
pub evidence_hash: Option<Hash>,
/// Original proposer of the block
#[serde(with = "nym_serde_helpers::hex")]
#[schemars(with = "String")]
#[schema(value_type = String)]
pub proposer_address: Vec<u8>,
}
impl From<block::Header> for BlockHeader {
fn from(value: block::Header) -> Self {
BlockHeader {
version: value.version.into(),
chain_id: value.chain_id.to_string(),
height: value.height.value(),
time: value.time,
last_block_id: value.last_block_id.map(Into::into),
last_commit_hash: value.last_commit_hash,
data_hash: value.data_hash,
validators_hash: value.validators_hash,
next_validators_hash: value.next_validators_hash,
consensus_hash: value.consensus_hash,
app_hash: Hash::try_from(value.app_hash.as_bytes().to_vec()).unwrap_or_default(),
last_results_hash: value.last_results_hash,
evidence_hash: value.evidence_hash,
proposer_address: value.proposer_address.as_bytes().to_vec(),
}
}
}
}
// implement required traits for the signer responses
impl LegacyChainResponse for ChainStatusResponse {
fn chain_synced(&self, now: OffsetDateTime, stall_threshold: Duration) -> bool {
self.status.stall_status(now, stall_threshold).is_synced()
}
}
impl Verifiable for ChainBlocksStatusResponse {
fn verify_signature(&self, pub_key: &PublicKey) -> bool {
self.verify_signature(pub_key)
}
}
impl TimestampedResponse for ChainBlocksStatusResponse {
fn timestamp(&self) -> OffsetDateTime {
self.body.current_time
}
}
impl ChainResponse for ChainBlocksStatusResponse {
fn chain_synced(&self) -> bool {
self.body.chain_status.is_synced()
}
}
impl LegacySignerResponse for SignerInformationResponse {
fn signer_identity(&self) -> &str {
&self.identity
}
fn signer_verification_key(&self) -> &Option<String> {
&self.verification_key
}
}
impl Verifiable for EcashSignerStatusResponse {
fn verify_signature(&self, pub_key: &PublicKey) -> bool {
self.verify_signature(pub_key)
}
}
impl TimestampedResponse for EcashSignerStatusResponse {
fn timestamp(&self) -> OffsetDateTime {
self.body.current_time
}
}
impl SignerResponse for EcashSignerStatusResponse {
fn has_signing_keys(&self) -> bool {
self.body.has_signing_keys
}
fn signer_disabled(&self) -> bool {
self.body.signer_disabled
}
fn is_ecash_signer(&self) -> bool {
self.body.is_ecash_signer
}
fn dkg_ecash_epoch_id(&self) -> EpochId {
self.body.dkg_ecash_epoch_id
}
}
@@ -0,0 +1,74 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::pagination::PaginatedResponse;
use nym_mixnet_contract_common::NodeId;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use utoipa::ToSchema;
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, Default, ToSchema)]
pub struct TestNode {
pub node_id: Option<u32>,
pub identity_key: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub struct TestRoute {
pub gateway: TestNode,
pub layer1: TestNode,
pub layer2: TestNode,
pub layer3: TestNode,
}
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub struct PartialTestResult {
pub monitor_run_id: i64,
pub timestamp: i64,
pub overall_reliability_for_all_routes_in_monitor_run: Option<u8>,
pub test_routes: TestRoute,
}
pub type MixnodeTestResultResponse = PaginatedResponse<PartialTestResult>;
pub type GatewayTestResultResponse = PaginatedResponse<PartialTestResult>;
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub struct NetworkMonitorRunDetailsResponse {
pub monitor_run_id: i64,
pub network_reliability: f64,
pub total_sent: usize,
pub total_received: usize,
// integer score to number of nodes with that score
pub mixnode_results: BTreeMap<u8, usize>,
pub gateway_results: BTreeMap<u8, usize>,
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, ToSchema)]
#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))]
#[cfg_attr(
feature = "generate-ts",
ts(
export,
export_to = "ts-packages/types/src/types/rust/MixnodeCoreStatusResponse.ts"
)
)]
pub struct MixnodeCoreStatusResponse {
pub mix_id: NodeId,
pub count: i64,
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, ToSchema)]
#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))]
#[cfg_attr(
feature = "generate-ts",
ts(
export,
export_to = "ts-packages/types/src/types/rust/GatewayCoreStatusResponse.ts"
)
)]
pub struct GatewayCoreStatusResponse {
pub identity: String,
pub count: i64,
}
@@ -0,0 +1,587 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::helpers::PlaceholderJsonSchemaImpl;
use crate::pagination::PaginatedResponse;
use cosmwasm_std::Decimal;
use nym_contracts_common::{IdentityKey, NaiveFloat};
use nym_crypto::asymmetric::ed25519;
use nym_crypto::asymmetric::ed25519::serde_helpers::bs58_ed25519_pubkey;
use nym_mixnet_contract_common::reward_params::Performance;
use nym_mixnet_contract_common::NodeId;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::time::Duration;
use time::{Date, OffsetDateTime};
use utoipa::ToSchema;
use crate::models::DisplayRole;
pub use config_score::*;
pub type StakeSaturation = Decimal;
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, ToSchema)]
#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))]
#[cfg_attr(
feature = "generate-ts",
ts(
export,
export_to = "ts-packages/types/src/types/rust/StakeSaturationResponse.ts"
)
)]
pub struct StakeSaturationResponse {
#[cfg_attr(feature = "generate-ts", ts(type = "string"))]
#[schema(value_type = String)]
pub saturation: StakeSaturation,
#[cfg_attr(feature = "generate-ts", ts(type = "string"))]
#[schema(value_type = String)]
pub uncapped_saturation: StakeSaturation,
pub as_at: i64,
}
pub mod config_score {
use nym_contracts_common::NaiveFloat;
use serde::{Deserialize, Serialize};
use std::cmp::Ordering;
use utoipa::ToSchema;
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub struct ConfigScoreDataResponse {
pub parameters: ConfigScoreParams,
pub version_history: Vec<HistoricalNymNodeVersionEntry>,
}
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema, PartialEq)]
pub struct HistoricalNymNodeVersionEntry {
/// The unique, ordered, id of this particular entry
pub id: u32,
/// Data associated with this particular version
pub version_information: HistoricalNymNodeVersion,
}
impl PartialOrd for HistoricalNymNodeVersionEntry {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
// we only care about id for the purposes of ordering as they should have unique data
self.id.partial_cmp(&other.id)
}
}
impl From<nym_mixnet_contract_common::HistoricalNymNodeVersionEntry>
for HistoricalNymNodeVersionEntry
{
fn from(value: nym_mixnet_contract_common::HistoricalNymNodeVersionEntry) -> Self {
HistoricalNymNodeVersionEntry {
id: value.id,
version_information: value.version_information.into(),
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema, PartialEq)]
pub struct HistoricalNymNodeVersion {
/// Version of the nym node that is going to be used for determining the version score of a node.
/// note: value stored here is pre-validated `semver::Version`
pub semver: String,
/// Block height of when this version has been added to the contract
pub introduced_at_height: u64,
// for now ignore that field. it will give nothing useful to the users
// pub difference_since_genesis: TotalVersionDifference,
}
impl From<nym_mixnet_contract_common::HistoricalNymNodeVersion> for HistoricalNymNodeVersion {
fn from(value: nym_mixnet_contract_common::HistoricalNymNodeVersion) -> Self {
HistoricalNymNodeVersion {
semver: value.semver,
introduced_at_height: value.introduced_at_height,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub struct ConfigScoreParams {
/// Defines weights for calculating numbers of versions behind the current release.
pub version_weights: OutdatedVersionWeights,
/// Defines the parameters of the formula for calculating the version score
pub version_score_formula_params: VersionScoreFormulaParams,
}
/// Defines weights for calculating numbers of versions behind the current release.
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub struct OutdatedVersionWeights {
pub major: u32,
pub minor: u32,
pub patch: u32,
pub prerelease: u32,
}
/// Given the formula of version_score = penalty ^ (versions_behind_factor ^ penalty_scaling)
/// define the relevant parameters
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub struct VersionScoreFormulaParams {
pub penalty: f64,
pub penalty_scaling: f64,
}
impl From<nym_mixnet_contract_common::ConfigScoreParams> for ConfigScoreParams {
fn from(value: nym_mixnet_contract_common::ConfigScoreParams) -> Self {
ConfigScoreParams {
version_weights: value.version_weights.into(),
version_score_formula_params: value.version_score_formula_params.into(),
}
}
}
impl From<nym_mixnet_contract_common::OutdatedVersionWeights> for OutdatedVersionWeights {
fn from(value: nym_mixnet_contract_common::OutdatedVersionWeights) -> Self {
OutdatedVersionWeights {
major: value.major,
minor: value.minor,
patch: value.patch,
prerelease: value.prerelease,
}
}
}
impl From<nym_mixnet_contract_common::VersionScoreFormulaParams> for VersionScoreFormulaParams {
fn from(value: nym_mixnet_contract_common::VersionScoreFormulaParams) -> Self {
VersionScoreFormulaParams {
penalty: value.penalty.naive_to_f64(),
penalty_scaling: value.penalty_scaling.naive_to_f64(),
}
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub struct NodeRefreshBody {
#[serde(with = "bs58_ed25519_pubkey")]
#[schemars(with = "String")]
#[schema(value_type = String)]
pub node_identity: ed25519::PublicKey,
// a poor man's nonce
pub request_timestamp: i64,
#[schemars(with = "PlaceholderJsonSchemaImpl")]
#[schema(value_type = String)]
pub signature: ed25519::Signature,
}
impl NodeRefreshBody {
pub fn plaintext(node_identity: ed25519::PublicKey, request_timestamp: i64) -> Vec<u8> {
node_identity
.to_bytes()
.into_iter()
.chain(request_timestamp.to_be_bytes())
.chain(b"describe-cache-refresh-request".iter().copied())
.collect()
}
pub fn new(private_key: &ed25519::PrivateKey) -> Self {
let node_identity = private_key.public_key();
let request_timestamp = OffsetDateTime::now_utc().unix_timestamp();
let signature = private_key.sign(Self::plaintext(node_identity, request_timestamp));
NodeRefreshBody {
node_identity,
request_timestamp,
signature,
}
}
pub fn verify_signature(&self) -> bool {
self.node_identity
.verify(
Self::plaintext(self.node_identity, self.request_timestamp),
&self.signature,
)
.is_ok()
}
pub fn is_stale(&self) -> bool {
let Ok(encoded) = OffsetDateTime::from_unix_timestamp(self.request_timestamp) else {
return true;
};
let now = OffsetDateTime::now_utc();
if encoded > now {
return true;
}
if (encoded + Duration::from_secs(30)) < now {
return true;
}
false
}
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, ToSchema)]
pub struct UptimeResponse {
#[schema(value_type = u32)]
pub mix_id: NodeId,
// The same as node_performance.last_24h. Legacy
pub avg_uptime: u8,
pub node_performance: NodePerformance,
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, ToSchema)]
pub struct GatewayUptimeResponse {
pub identity: String,
// The same as node_performance.last_24h. Legacy
pub avg_uptime: u8,
pub node_performance: NodePerformance,
}
type Uptime = u8;
#[derive(Clone, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub struct MixnodeStatusReportResponse {
pub mix_id: NodeId,
pub identity: IdentityKey,
pub owner: String,
#[schema(value_type = u8)]
pub most_recent: Uptime,
#[schema(value_type = u8)]
pub last_hour: Uptime,
#[schema(value_type = u8)]
pub last_day: Uptime,
}
#[derive(Clone, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub struct GatewayStatusReportResponse {
pub identity: String,
pub owner: String,
#[schema(value_type = u8)]
pub most_recent: Uptime,
#[schema(value_type = u8)]
pub last_hour: Uptime,
#[schema(value_type = u8)]
pub last_day: Uptime,
}
#[derive(Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))]
#[cfg_attr(
feature = "generate-ts",
ts(
export,
export_to = "ts-packages/types/src/types/rust/PerformanceHistoryResponse.ts"
)
)]
pub struct PerformanceHistoryResponse {
#[schema(value_type = u32)]
pub node_id: NodeId,
pub history: PaginatedResponse<HistoricalPerformanceResponse>,
}
#[derive(Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))]
#[cfg_attr(
feature = "generate-ts",
ts(
export,
export_to = "ts-packages/types/src/types/rust/UptimeHistoryResponse.ts"
)
)]
pub struct UptimeHistoryResponse {
#[schema(value_type = u32)]
pub node_id: NodeId,
pub history: PaginatedResponse<HistoricalUptimeResponse>,
}
#[derive(Clone, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))]
#[cfg_attr(
feature = "generate-ts",
ts(
export,
export_to = "ts-packages/types/src/types/rust/HistoricalUptimeResponse.ts"
)
)]
pub struct HistoricalUptimeResponse {
#[schema(value_type = String, example = "1970-01-01")]
#[schemars(with = "String")]
#[cfg_attr(feature = "generate-ts", ts(type = "string"))]
pub date: Date,
pub uptime: Uptime,
}
#[derive(Clone, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))]
#[cfg_attr(
feature = "generate-ts",
ts(
export,
export_to = "ts-packages/types/src/types/rust/HistoricalPerformanceResponse.ts"
)
)]
pub struct HistoricalPerformanceResponse {
#[schema(value_type = String, example = "1970-01-01")]
#[schemars(with = "String")]
#[cfg_attr(feature = "generate-ts", ts(type = "string"))]
pub date: Date,
pub performance: f64,
}
#[derive(Clone, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub struct OldHistoricalUptimeResponse {
pub date: String,
#[schema(value_type = u8)]
pub uptime: Uptime,
}
#[derive(Clone, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub struct MixnodeUptimeHistoryResponse {
pub mix_id: NodeId,
pub identity: String,
pub owner: String,
pub history: Vec<OldHistoricalUptimeResponse>,
}
#[derive(Clone, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub struct GatewayUptimeHistoryResponse {
pub identity: String,
pub owner: String,
pub history: Vec<OldHistoricalUptimeResponse>,
}
#[derive(
Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, ToSchema, Default,
)]
#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))]
#[cfg_attr(
feature = "generate-ts",
ts(
export,
export_to = "ts-packages/types/src/types/rust/MixnodeStatus.ts"
)
)]
#[serde(rename_all = "snake_case")]
pub enum MixnodeStatus {
Active, // in both the active set and the rewarded set
Standby, // only in the rewarded set
Inactive, // in neither the rewarded set nor the active set, but is bonded
#[default]
NotFound, // doesn't even exist in the bonded set
}
impl MixnodeStatus {
pub fn is_active(&self) -> bool {
*self == MixnodeStatus::Active
}
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, ToSchema)]
#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))]
#[cfg_attr(
feature = "generate-ts",
ts(
export,
export_to = "ts-packages/types/src/types/rust/MixnodeStatusResponse.ts"
)
)]
pub struct MixnodeStatusResponse {
pub status: MixnodeStatus,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, ToSchema)]
pub struct NodePerformance {
#[schema(value_type = String)]
pub most_recent: Performance,
#[schema(value_type = String)]
pub last_hour: Performance,
#[schema(value_type = String)]
pub last_24h: Performance,
}
// imo for now there's no point in exposing more than that,
// nym-api shouldn't be calculating apy or stake saturation for you.
// it should just return its own metrics (performance) and then you can do with it as you wish
#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema, ToSchema)]
#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))]
#[cfg_attr(
feature = "generate-ts",
ts(
export,
export_to = "ts-packages/types/src/types/rust/NodeAnnotation.ts"
)
)]
pub struct NodeAnnotation {
#[cfg_attr(feature = "generate-ts", ts(type = "string"))]
// legacy
#[schema(value_type = String)]
pub last_24h_performance: Performance,
pub current_role: Option<DisplayRole>,
pub detailed_performance: DetailedNodePerformance,
}
#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema, ToSchema)]
#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))]
#[cfg_attr(
feature = "generate-ts",
ts(
export,
export_to = "ts-packages/types/src/types/rust/DetailedNodePerformance.ts"
)
)]
#[non_exhaustive]
pub struct DetailedNodePerformance {
/// routing_score * config_score
pub performance_score: f64,
pub routing_score: RoutingScore,
pub config_score: ConfigScore,
}
impl DetailedNodePerformance {
pub fn new(
performance_score: f64,
routing_score: RoutingScore,
config_score: ConfigScore,
) -> DetailedNodePerformance {
Self {
performance_score,
routing_score,
config_score,
}
}
pub fn to_rewarding_performance(&self) -> Performance {
Performance::naive_try_from_f64(self.performance_score).unwrap_or_default()
}
}
#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema, ToSchema)]
#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))]
#[cfg_attr(
feature = "generate-ts",
ts(export, export_to = "ts-packages/types/src/types/rust/RoutingScore.ts")
)]
#[non_exhaustive]
pub struct RoutingScore {
/// Total score after taking all the criteria into consideration
pub score: f64,
}
impl RoutingScore {
pub fn new(score: f64) -> RoutingScore {
Self { score }
}
pub const fn zero() -> RoutingScore {
RoutingScore { score: 0.0 }
}
pub fn legacy_performance(&self) -> Performance {
Performance::naive_try_from_f64(self.score).unwrap_or_default()
}
}
#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema, ToSchema)]
#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))]
#[cfg_attr(
feature = "generate-ts",
ts(export, export_to = "ts-packages/types/src/types/rust/ConfigScore.ts")
)]
#[non_exhaustive]
pub struct ConfigScore {
/// Total score after taking all the criteria into consideration
pub score: f64,
pub versions_behind: Option<u32>,
pub self_described_api_available: bool,
pub accepted_terms_and_conditions: bool,
pub runs_nym_node_binary: bool,
}
impl ConfigScore {
pub fn new(
score: f64,
versions_behind: u32,
accepted_terms_and_conditions: bool,
runs_nym_node_binary: bool,
) -> ConfigScore {
Self {
score,
versions_behind: Some(versions_behind),
self_described_api_available: true,
accepted_terms_and_conditions,
runs_nym_node_binary,
}
}
pub fn bad_semver() -> ConfigScore {
ConfigScore {
score: 0.0,
versions_behind: None,
self_described_api_available: true,
accepted_terms_and_conditions: false,
runs_nym_node_binary: false,
}
}
pub fn unavailable() -> ConfigScore {
ConfigScore {
score: 0.0,
versions_behind: None,
self_described_api_available: false,
accepted_terms_and_conditions: false,
runs_nym_node_binary: false,
}
}
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, ToSchema)]
#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))]
#[cfg_attr(
feature = "generate-ts",
ts(
export,
export_to = "ts-packages/types/src/types/rust/AnnotationResponse.ts"
)
)]
pub struct AnnotationResponse {
#[schema(value_type = u32)]
pub node_id: NodeId,
pub annotation: Option<NodeAnnotation>,
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, ToSchema)]
#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))]
#[cfg_attr(
feature = "generate-ts",
ts(
export,
export_to = "ts-packages/types/src/types/rust/NodePerformanceResponse.ts"
)
)]
pub struct NodePerformanceResponse {
#[schema(value_type = u32)]
pub node_id: NodeId,
pub performance: Option<f64>,
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, ToSchema)]
#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))]
#[cfg_attr(
feature = "generate-ts",
ts(
export,
export_to = "ts-packages/types/src/types/rust/NodeDatePerformanceResponse.ts"
)
)]
pub struct NodeDatePerformanceResponse {
#[schema(value_type = u32)]
pub node_id: NodeId,
#[schema(value_type = String, example = "1970-01-01")]
#[schemars(with = "String")]
#[cfg_attr(feature = "generate-ts", ts(type = "string"))]
pub date: Date,
pub performance: Option<f64>,
}
@@ -0,0 +1,128 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::helpers::unix_epoch;
use cosmwasm_std::Uint128;
use schemars::schema::{InstanceType, Schema, SchemaObject};
use schemars::{JsonSchema, SchemaGenerator};
use serde::{Deserialize, Deserializer, Serialize};
use std::fmt;
use std::fmt::{Display, Formatter};
use std::ops::{Deref, DerefMut};
use time::OffsetDateTime;
use utoipa::ToSchema;
#[derive(ToSchema)]
#[schema(title = "Coin")]
pub struct CoinSchema {
pub denom: String,
#[schema(value_type = String)]
pub amount: Uint128,
}
pub fn de_rfc3339_or_default<'de, D>(deserializer: D) -> Result<OffsetDateTime, D::Error>
where
D: Deserializer<'de>,
{
Ok(time::serde::rfc3339::deserialize(deserializer).unwrap_or_else(|_| unix_epoch()))
}
// for all intents and purposes it's just OffsetDateTime, but we need JsonSchema...
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, ToSchema)]
pub struct OffsetDateTimeJsonSchemaWrapper(
#[serde(
default = "unix_epoch",
with = "crate::helpers::overengineered_offset_date_time_serde"
)]
#[schema(inline)]
pub OffsetDateTime,
);
impl Default for OffsetDateTimeJsonSchemaWrapper {
fn default() -> Self {
OffsetDateTimeJsonSchemaWrapper(unix_epoch())
}
}
impl Display for OffsetDateTimeJsonSchemaWrapper {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
Display::fmt(&self.0, f)
}
}
impl From<OffsetDateTimeJsonSchemaWrapper> for OffsetDateTime {
fn from(value: OffsetDateTimeJsonSchemaWrapper) -> Self {
value.0
}
}
impl From<OffsetDateTime> for OffsetDateTimeJsonSchemaWrapper {
fn from(value: OffsetDateTime) -> Self {
OffsetDateTimeJsonSchemaWrapper(value)
}
}
impl Deref for OffsetDateTimeJsonSchemaWrapper {
type Target = OffsetDateTime;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for OffsetDateTimeJsonSchemaWrapper {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
// implementation taken from: https://github.com/GREsau/schemars/pull/207
impl JsonSchema for OffsetDateTimeJsonSchemaWrapper {
fn is_referenceable() -> bool {
false
}
fn schema_name() -> String {
"DateTime".into()
}
fn json_schema(_: &mut SchemaGenerator) -> Schema {
SchemaObject {
instance_type: Some(InstanceType::String.into()),
format: Some("date-time".into()),
..Default::default()
}
.into()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
#[test]
fn offset_date_time_json_schema_wrapper_serde_backwards_compat() {
let mut dummy = OffsetDateTimeJsonSchemaWrapper::default();
dummy.0 += Duration::from_millis(1);
let ser = serde_json::to_string(&dummy).unwrap();
assert_eq!("\"1970-01-01 00:00:00.001 +00:00:00\"", ser);
let human_readable = "\"2024-05-23 07:41:02.756283766 +00:00:00\"";
let rfc3339 = "\"2002-10-02T15:00:00Z\"";
let rfc3339_offset = "\"2002-10-02T10:00:00-05:00\"";
let de = serde_json::from_str::<OffsetDateTimeJsonSchemaWrapper>(human_readable).unwrap();
assert_eq!(de.0.unix_timestamp(), 1716450062);
let de = serde_json::from_str::<OffsetDateTimeJsonSchemaWrapper>(rfc3339).unwrap();
assert_eq!(de.0.unix_timestamp(), 1033570800);
let de = serde_json::from_str::<OffsetDateTimeJsonSchemaWrapper>(rfc3339_offset).unwrap();
assert_eq!(de.0.unix_timestamp(), 1033570800);
let de = serde_json::from_str::<OffsetDateTimeJsonSchemaWrapper>("\"nonsense\"").unwrap();
assert_eq!(de.0.unix_timestamp(), 0);
}
}
+75
View File
@@ -0,0 +1,75 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use nym_crypto::asymmetric::ed25519;
use nym_crypto::asymmetric::ed25519::serde_helpers::bs58_ed25519_signature;
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
// the trait is not public as it's only defined on types that are guaranteed to not panic when serialised
pub trait SignableMessageBody: Serialize + sealed::Sealed {
fn sign(self, key: &ed25519::PrivateKey) -> SignedMessage<Self>
where
Self: Sized,
{
let signature = key.sign(self.plaintext());
SignedMessage {
body: self,
signature,
}
}
fn plaintext(&self) -> Vec<u8> {
#[allow(clippy::unwrap_used)]
// SAFETY: all types that implement this trait have valid serialisations
serde_json::to_vec(&self).unwrap()
}
}
impl<T> SignableMessageBody for T where T: Serialize + sealed::Sealed {}
#[derive(Clone, Serialize, Deserialize, Debug, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct SignedMessage<T> {
pub body: T,
#[schema(value_type = String)]
#[serde(with = "bs58_ed25519_signature")]
pub signature: ed25519::Signature,
}
impl<T> SignedMessage<T> {
pub fn verify_signature(&self, pub_key: &ed25519::PublicKey) -> bool
where
T: SignableMessageBody,
{
let plaintext = self.body.plaintext();
if plaintext.is_empty() {
return false;
}
pub_key.verify(&plaintext, &self.signature).is_ok()
}
}
// make sure only our types can implement this trait (to ensure infallible serialisation)
pub(crate) mod sealed {
use crate::ecash::models::*;
use crate::models::{
ChainBlocksStatusResponseBody, DetailedSignersStatusResponseBody, SignersStatusResponseBody,
};
pub trait Sealed {}
// requests
impl Sealed for IssuedTicketbooksChallengeCommitmentRequestBody {}
impl Sealed for IssuedTicketbooksDataRequestBody {}
// responses
impl Sealed for IssuedTicketbooksChallengeCommitmentResponseBody {}
impl Sealed for IssuedTicketbooksForResponseBody {}
impl Sealed for IssuedTicketbooksDataResponseBody {}
impl Sealed for EcashSignerStatusResponseBody {}
impl Sealed for ChainBlocksStatusResponseBody {}
impl Sealed for SignersStatusResponseBody {}
impl Sealed for DetailedSignersStatusResponseBody {}
}
+3
View File
@@ -4,8 +4,10 @@
use crate::ecash::api_routes::aggregation::aggregation_routes;
use crate::ecash::api_routes::issued::issued_routes;
use crate::ecash::api_routes::partial_signing::partial_signing_routes;
use crate::ecash::api_routes::signer_status::signer_status;
use crate::ecash::api_routes::spending::spending_routes;
use crate::support::http::state::AppState;
use axum::routing::get;
use axum::Router;
pub(crate) fn ecash_routes() -> Router<AppState> {
@@ -14,4 +16,5 @@ pub(crate) fn ecash_routes() -> Router<AppState> {
.merge(issued_routes())
.merge(partial_signing_routes())
.merge(spending_routes())
.route("/signer-status", get(signer_status))
}
+2 -1
View File
@@ -11,8 +11,9 @@ use nym_api_requests::ecash::models::{
IssuedTicketbooksChallengeCommitmentRequest, IssuedTicketbooksChallengeCommitmentResponse,
IssuedTicketbooksCountResponse, IssuedTicketbooksDataRequest, IssuedTicketbooksDataResponse,
IssuedTicketbooksForCountResponse, IssuedTicketbooksForResponse,
IssuedTicketbooksOnCountResponse, SignableMessageBody,
IssuedTicketbooksOnCountResponse,
};
use nym_api_requests::signable::SignableMessageBody;
use nym_http_api_common::{FormattedResponse, OutputParams};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
+1
View File
@@ -6,4 +6,5 @@ pub(crate) mod handlers;
mod helpers;
pub(crate) mod issued;
pub(crate) mod partial_signing;
pub(crate) mod signer_status;
pub(crate) mod spending;
@@ -0,0 +1,45 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use crate::ecash::state::EcashState;
use crate::node_status_api::models::ApiResult;
use axum::extract::{Query, State};
use nym_api_requests::ecash::models::{EcashSignerStatusResponse, EcashSignerStatusResponseBody};
use nym_api_requests::signable::SignableMessageBody;
use nym_http_api_common::{FormattedResponse, OutputParams};
use std::sync::Arc;
use time::OffsetDateTime;
#[utoipa::path(
tag = "Ecash",
get,
path = "/signer-status",
context_path = "/v1/ecash",
responses(
(status = 200, content(
(EcashSignerStatusResponse = "application/json"),
(EcashSignerStatusResponse = "application/yaml"),
(EcashSignerStatusResponse = "application/bincode")
)),
),
params(OutputParams)
)]
pub(crate) async fn signer_status(
Query(params): Query<OutputParams>,
State(state): State<Arc<EcashState>>,
) -> ApiResult<FormattedResponse<EcashSignerStatusResponse>> {
let output = params.get_output();
let dkg_ecash_epoch_id = state.current_dkg_epoch().await?;
Ok(output.to_response(
EcashSignerStatusResponseBody {
current_time: OffsetDateTime::now_utc(),
dkg_ecash_epoch_id,
signer_disabled: state.local.explicitly_disabled,
is_ecash_signer: state.is_dkg_signer(dkg_ecash_epoch_id).await?,
has_signing_keys: state.ecash_signing_key().await.is_ok(),
}
.sign(state.local.identity_keypair.private_key()),
))
}
+16 -9
View File
@@ -50,7 +50,6 @@ use nym_validator_client::nyxd::AccountId;
use nym_validator_client::EcashApiClient;
use rand::{thread_rng, RngCore};
use std::collections::HashMap;
use std::ops::Deref;
use time::{Date, OffsetDateTime};
use tokio::sync::{RwLockReadGuard, RwLockWriteGuard};
use tokio::task::JoinHandle;
@@ -162,14 +161,11 @@ impl EcashState {
}
}
/// Ensures that this nym-api is one of ecash signers for the current epoch
pub(crate) async fn ensure_signer(&self) -> Result<()> {
if self.local.explicitly_disabled {
return Err(EcashError::NotASigner);
}
let epoch_id = self.aux.current_epoch().await?;
pub(crate) async fn current_dkg_epoch(&self) -> Result<EpochId> {
self.aux.current_epoch().await
}
pub(crate) async fn is_dkg_signer(&self, epoch_id: EpochId) -> Result<bool> {
let is_epoch_signer = self
.local
.active_signer
@@ -183,8 +179,19 @@ impl EcashState {
Ok(ecash_signers.iter().any(|c| c.cosmos_address == address))
})
.await?;
Ok(*is_epoch_signer)
}
if !is_epoch_signer.deref() {
/// Ensures that this nym-api is one of ecash signers for the current epoch
pub(crate) async fn ensure_signer(&self) -> Result<()> {
if self.local.explicitly_disabled {
return Err(EcashError::NotASigner);
}
let epoch_id = self.current_dkg_epoch().await?;
let is_epoch_signer = self.is_dkg_signer(epoch_id).await?;
if !is_epoch_signer {
return Err(EcashError::NotASigner);
}
+3 -1
View File
@@ -32,9 +32,10 @@ use cw3::{Proposal, ProposalResponse, Vote, VoteInfo, VoteResponse, Votes};
use cw4::{Cw4Contract, MemberResponse};
use nym_api_requests::ecash::models::{
IssuedTicketbooksChallengeCommitmentRequestBody, IssuedTicketbooksChallengeCommitmentResponse,
IssuedTicketbooksForResponse, SignableMessageBody,
IssuedTicketbooksForResponse,
};
use nym_api_requests::ecash::{BlindSignRequestBody, BlindedSignatureResponse};
use nym_api_requests::signable::SignableMessageBody;
use nym_coconut_dkg_common::dealer::{
DealerDetails, DealerDetailsResponse, DealerType, RegisteredDealerDetails,
};
@@ -1277,6 +1278,7 @@ impl TestFixture {
AppState {
nyxd_client,
chain_status_cache: ChainStatusCache::new(Duration::from_secs(42)),
ecash_signers_cache: Default::default(),
address_info_cache: AddressInfoCache::new(Duration::from_secs(42), 1000),
forced_refresh: ForcedRefresh::new(true),
mixnet_contract_cache: MixnetContractCache::new(),
+3 -3
View File
@@ -8,7 +8,6 @@ use ::nym_config::defaults::setup_env;
use clap::Parser;
use mixnet_contract_cache::cache::MixnetContractCache;
use node_status_api::NodeStatusCache;
use nym_bin_common::logging::setup_tracing_logger;
use support::nyxd;
use tracing::{info, trace};
@@ -23,6 +22,7 @@ pub(crate) mod node_describe_cache;
mod node_performance;
pub(crate) mod node_status_api;
pub(crate) mod nym_nodes;
mod signers_cache;
mod status;
pub(crate) mod support;
mod unstable_routes;
@@ -32,10 +32,10 @@ async fn main() -> Result<(), anyhow::Error> {
cfg_if::cfg_if! {if #[cfg(feature = "console-subscriber")] {
// instrument tokio console subscriber needs RUSTFLAGS="--cfg tokio_unstable" at build time
console_subscriber::init();
} else {
nym_bin_common::logging::setup_tracing_logger();
}}
setup_tracing_logger();
info!("Starting nym api...");
let args = cli::Cli::parse();
+62 -6
View File
@@ -3,13 +3,19 @@
use crate::network::models::{ContractInformation, NetworkDetails};
use crate::node_status_api::models::AxumResult;
use crate::signers_cache::handlers::signers_routes;
use crate::support::config::CHAIN_STALL_THRESHOLD;
use crate::support::http::state::AppState;
use axum::extract::{Query, State};
use axum::Router;
use nym_api_requests::models::ChainStatusResponse;
use nym_api_requests::models::{
ChainBlocksStatusResponse, ChainBlocksStatusResponseBody, ChainStatus, ChainStatusResponse,
};
use nym_api_requests::signable::SignableMessageBody;
use nym_contracts_common::ContractBuildInformation;
use nym_http_api_common::{FormattedResponse, OutputParams};
use std::collections::HashMap;
use time::OffsetDateTime;
use tower_http::compression::CompressionLayer;
use utoipa::ToSchema;
@@ -17,18 +23,24 @@ pub(crate) fn nym_network_routes() -> Router<AppState> {
Router::new()
.route("/details", axum::routing::get(network_details))
.route("/chain-status", axum::routing::get(chain_status))
.route(
"/chain-blocks-status",
axum::routing::get(chain_blocks_status),
)
.route("/nym-contracts", axum::routing::get(nym_contracts))
.route(
"/nym-contracts-detailed",
axum::routing::get(nym_contracts_detailed),
)
.nest("/signers", signers_routes())
.layer(CompressionLayer::new())
}
#[utoipa::path(
tag = "network",
get,
path = "/v1/network/details",
context_path = "/v1/network",
path = "/details",
responses(
(status = 200, content(
(NetworkDetails = "application/json"),
@@ -50,7 +62,8 @@ async fn network_details(
#[utoipa::path(
tag = "network",
get,
path = "/v1/network/chain-status",
context_path = "/v1/network",
path = "/chain-status",
responses(
(status = 200, content(
(ChainStatusResponse = "application/json"),
@@ -79,6 +92,47 @@ async fn chain_status(
}))
}
#[utoipa::path(
tag = "network",
get,
context_path = "/v1/network",
path = "/chain-blocks-status",
responses(
(status = 200, content(
(ChainBlocksStatusResponse = "application/json"),
(ChainBlocksStatusResponse = "application/yaml"),
(ChainBlocksStatusResponse = "application/bincode")
))
),
params(OutputParams)
)]
async fn chain_blocks_status(
Query(params): Query<OutputParams>,
State(state): State<AppState>,
) -> FormattedResponse<ChainBlocksStatusResponse> {
let output = params.get_output();
let current_time = OffsetDateTime::now_utc();
let latest_cached_block = state
.chain_status_cache
.get_or_refresh(&state.nyxd_client)
.await
.ok();
let chain_status = latest_cached_block
.as_ref()
.map(|detailed| detailed.stall_status(current_time, CHAIN_STALL_THRESHOLD))
.unwrap_or(ChainStatus::Unknown);
output.to_response(
ChainBlocksStatusResponseBody {
current_time,
latest_cached_block,
chain_status,
}
.sign(state.private_signing_key()),
)
}
// it's used for schema generation so dead_code is fine
#[allow(dead_code)]
#[derive(ToSchema)]
@@ -90,7 +144,7 @@ pub(crate) struct ContractVersionSchemaResponse {
/// version is any string that this implementation knows. It may be simple counter "1", "2".
/// or semantic version on release tags "v0.7.0", or some custom feature flag list.
/// the only code that needs to understand the version parsing is code that knows how to
/// migrate from the given contract (and is tied to it's implementation somehow)
/// migrate from the given contract (and is tied to its implementation somehow)
pub version: String,
}
@@ -104,7 +158,8 @@ pub struct ContractInformationContractVersion {
#[utoipa::path(
tag = "network",
get,
path = "/v1/network/nym-contracts",
context_path = "/v1/network",
path = "/nym-contracts",
responses(
(status = 200, content(
(HashMap<String, ContractInformationContractVersion> = "application/json"),
@@ -151,7 +206,8 @@ pub struct ContractInformationBuildInformation {
#[utoipa::path(
tag = "network",
get,
path = "/v1/network/nym-contracts-detailed",
context_path = "/v1/network",
path = "/nym-contracts-detailed",
responses(
(status = 200, content(
(HashMap<String, ContractInformationBuildInformation> = "application/json"),
+2
View File
@@ -0,0 +1,2 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
+11
View File
@@ -0,0 +1,11 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use nym_ecash_signer_check::SignersTestResult;
pub(crate) mod data;
pub(crate) mod refresher;
pub(crate) struct SignersCacheData {
pub(crate) signers_results: SignersTestResult,
}
+33
View File
@@ -0,0 +1,33 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use crate::signers_cache::cache::SignersCacheData;
use crate::support::caching::refresher::CacheItemProvider;
use crate::support::nyxd::Client;
use async_trait::async_trait;
use nym_ecash_signer_check::{check_signers_with_client, SignerCheckError};
pub(crate) struct SignersCacheDataProvider {
nyxd_client: Client,
}
#[async_trait]
impl CacheItemProvider for SignersCacheDataProvider {
type Item = SignersCacheData;
type Error = SignerCheckError;
async fn try_refresh(&mut self) -> Result<Option<Self::Item>, Self::Error> {
self.refresh().await.map(Some)
}
}
impl SignersCacheDataProvider {
pub(crate) fn new(nyxd_client: Client) -> Self {
SignersCacheDataProvider { nyxd_client }
}
async fn refresh(&self) -> Result<SignersCacheData, SignerCheckError> {
let signers_results = check_signers_with_client(&self.nyxd_client).await?;
Ok(SignersCacheData { signers_results })
}
}
+95
View File
@@ -0,0 +1,95 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use crate::node_status_api::models::ApiResult;
use crate::support::http::state::AppState;
use axum::extract::{Query, State};
use axum::routing::get;
use axum::Router;
use nym_api_requests::models::{
DetailedSignersStatusResponse, DetailedSignersStatusResponseBody, SignersStatusOverview,
SignersStatusResponse, SignersStatusResponseBody,
};
use nym_api_requests::signable::SignableMessageBody;
use nym_http_api_common::{FormattedResponse, OutputParams};
pub(crate) fn signers_routes() -> Router<AppState> {
Router::new()
.route("/status", get(signers_status))
.route("/status-detailed", get(signers_status_detailed))
}
#[utoipa::path(
tag = "network",
get,
context_path = "/v1/network/signers",
path = "/status",
responses(
(status = 200, content(
(SignersStatusResponse = "application/json"),
(SignersStatusResponse = "application/yaml"),
(SignersStatusResponse = "application/bincode")
))
),
params(OutputParams)
)]
pub(crate) async fn signers_status(
Query(params): Query<OutputParams>,
State(state): State<AppState>,
) -> ApiResult<FormattedResponse<SignersStatusResponse>> {
let output = params.get_output();
let cached = state.ecash_signers_cache.get().await?;
let as_at = cached.timestamp();
Ok(output.to_response(
SignersStatusResponseBody {
as_at,
overview: SignersStatusOverview::new(
&cached.signers_results.results,
cached.signers_results.threshold,
),
results: cached
.signers_results
.results
.iter()
.map(Into::into)
.collect(),
}
.sign(state.private_signing_key()),
))
}
#[utoipa::path(
tag = "network",
get,
context_path = "/v1/network/signers",
path = "/status-detailed",
responses(
(status = 200, content(
(DetailedSignersStatusResponse = "application/json"),
(DetailedSignersStatusResponse = "application/yaml"),
(DetailedSignersStatusResponse = "application/bincode")
))
),
params(OutputParams)
)]
pub(crate) async fn signers_status_detailed(
Query(params): Query<OutputParams>,
State(state): State<AppState>,
) -> ApiResult<FormattedResponse<DetailedSignersStatusResponse>> {
let output = params.get_output();
let cached = state.ecash_signers_cache.get().await?;
let as_at = cached.timestamp();
Ok(output.to_response(
DetailedSignersStatusResponseBody {
as_at,
overview: SignersStatusOverview::new(
&cached.signers_results.results,
cached.signers_results.threshold,
),
details: cached.signers_results.results.clone(),
}
.sign(state.private_signing_key()),
))
}
+30
View File
@@ -0,0 +1,30 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use crate::signers_cache::cache::refresher::SignersCacheDataProvider;
use crate::signers_cache::cache::SignersCacheData;
use crate::support::caching::cache::SharedCache;
use crate::support::caching::refresher::CacheRefresher;
use crate::support::{config, nyxd};
use nym_task::TaskManager;
pub(crate) mod cache;
pub(crate) mod handlers;
pub(crate) fn start_refresher(
config: &config::SignersCache,
nyxd_client: nyxd::Client,
task_manager: &TaskManager,
) -> SharedCache<SignersCacheData> {
let refresher = CacheRefresher::new(
SignersCacheDataProvider::new(nyxd_client),
config.debug.refresh_interval,
)
.named("signers-cache-refresher");
let shared_cache = refresher.get_shared_cache();
refresher.start_with_delay(
task_manager.subscribe_named("signers-cache-refresher"),
config.debug.refresher_start_delay,
);
shared_cache
}
+2 -12
View File
@@ -3,6 +3,7 @@
use crate::node_status_api::models::{AxumErrorResponse, AxumResult};
use crate::status::ApiStatusState;
use crate::support::config::CHAIN_STALL_THRESHOLD;
use crate::support::http::state::AppState;
use axum::extract::{Query, State};
use axum::Router;
@@ -12,7 +13,6 @@ use nym_api_requests::models::{
use nym_bin_common::build_information::BinaryBuildInformationOwned;
use nym_compact_ecash::Base58;
use nym_http_api_common::{FormattedResponse, OutputParams};
use std::time::Duration;
use time::OffsetDateTime;
pub(crate) fn api_status_routes() -> Router<AppState> {
@@ -42,8 +42,6 @@ async fn health(
Query(output): Query<OutputParams>,
State(state): State<AppState>,
) -> FormattedResponse<ApiHealthResponse> {
const CHAIN_STALL_THRESHOLD: Duration = Duration::from_secs(5 * 60);
let output = output.output.unwrap_or_default();
let uptime = state.api_status.startup_time.elapsed();
@@ -54,15 +52,7 @@ async fn health(
{
Ok(res) => {
let now = OffsetDateTime::now_utc();
let block_time: OffsetDateTime = res.latest_block.block.header.time.into();
let diff = now - block_time;
if diff > CHAIN_STALL_THRESHOLD {
ChainStatus::Stalled {
approximate_amount: diff.unsigned_abs(),
}
} else {
ChainStatus::Synced
}
res.stall_status(now, CHAIN_STALL_THRESHOLD)
}
Err(_) => ChainStatus::Unknown,
};
+27
View File
@@ -60,6 +60,11 @@ pub(crate) trait CacheItemProvider {
async fn try_refresh(&mut self) -> Result<Option<Self::Item>, Self::Error>;
}
// Generics explanation:
// T: the actual type held in the cache
// E: Error type associated with refresh failure
// S: data type retrieved during update operation. it must be convertible into T
// (so that initial state could be established or when no `custom_fn` is set)
impl<T, E, S> CacheRefresher<T, E, S>
where
E: std::error::Error,
@@ -107,6 +112,8 @@ where
}
}
/// Rather than performing default behaviour of overwriting all existing values in the cache,
/// provide a custom update function that will define the update behaviour.
#[must_use]
pub(crate) fn with_update_fn(
mut self,
@@ -259,6 +266,26 @@ where
tokio::spawn(async move { self.run(task_client).await });
}
pub fn start_with_delay(mut self, mut task_client: TaskClient, delay: Duration)
where
T: Send + Sync + 'static,
E: Send + Sync + 'static,
S: Send + Sync + 'static,
{
tokio::spawn(async move {
let sleep = tokio::time::sleep(delay);
tokio::select! {
biased;
_ = task_client.recv() => {
trace!("{}: Received shutdown", self.name);
return
}
_ = sleep => {},
}
self.run(task_client).await
});
}
pub fn start_with_watcher(self, task_client: TaskClient) -> CacheUpdateWatcher
where
T: Send + Sync + 'static,
+9 -1
View File
@@ -34,7 +34,7 @@ use crate::support::storage::NymApiStorage;
use crate::unstable_routes::v1::account::cache::AddressInfoCache;
use crate::{
ecash, epoch_operations, mixnet_contract_cache, network_monitor, node_describe_cache,
node_performance, node_status_api,
node_performance, node_status_api, signers_cache,
};
use anyhow::{bail, Context};
use nym_config::defaults::NymNetworkDetails;
@@ -202,10 +202,18 @@ async fn start_nym_api_tasks(config: &Config) -> anyhow::Result<ShutdownHandles>
None
};
// check if signers cache is enabled, and if so, start the refresher
let ecash_signers_cache = if config.signers_cache.enabled {
signers_cache::start_refresher(&config.signers_cache, nyxd_client.clone(), &task_manager)
} else {
SharedCache::new()
};
ecash_state.spawn_background_cleaner();
let router = router.with_state(AppState {
nyxd_client: nyxd_client.clone(),
chain_status_cache: ChainStatusCache::new(DEFAULT_CHAIN_STATUS_CACHE_TTL),
ecash_signers_cache,
address_info_cache: AddressInfoCache::new(
config.address_cache.time_to_live,
config.address_cache.capacity,

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