Compare commits

...

53 Commits

Author SHA1 Message Date
Bogdan-Ștefan Neacşu fb3ec1e02e Connection fd callback before actual connection (#5494) 2025-02-27 10:53:04 +02:00
Jędrzej Stuczyński 26f97d3c34 dont query for ecash apis unless necessary (#5508) 2025-02-24 10:59:06 +00:00
benedettadavico 63a8f96ea5 bump versions 2025-02-19 12:13:24 +01:00
Jack Wampler 50b044a100 Support static routes for HTTP requests (#5487)
allow static dns override
2025-02-18 11:53:32 -07:00
Jack Wampler ba645694d4 Provide Interval context with node descriptor endpoints (#5456)
send interval with paginated cached node responses - if epoch_id is in params and current send noupdates
2025-02-18 09:02:34 -07:00
Jack Wampler be44811a65 centralize API request interface and add preffered compression in responses (#5450) 2025-02-18 08:58:35 -07:00
import this 62e1d32e4f [DOCs:/operators]: Update sgp locations (#5486) 2025-02-18 11:39:45 +00:00
benedetta davico 9a4bbe1d67 Merge pull request #5484 from nymtech/release/2025.3-ruta
Release/2025.3 ruta to develop
2025-02-18 09:54:04 +01:00
dependabot[bot] 98090d18b4 build(deps): bump the patch-updates group across 1 directory with 3 updates (#5482) 2025-02-18 01:21:46 +01:00
dependabot[bot] 79f8066c13 build(deps): bump http from 1.1.0 to 1.2.0 (#5472)
Bumps [http](https://github.com/hyperium/http) from 1.1.0 to 1.2.0.
- [Release notes](https://github.com/hyperium/http/releases)
- [Changelog](https://github.com/hyperium/http/blob/master/CHANGELOG.md)
- [Commits](https://github.com/hyperium/http/compare/v1.1.0...v1.2.0)

---
updated-dependencies:
- dependency-name: http
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-18 00:45:21 +01:00
dependabot[bot] d0209766a3 build(deps): bump celes from 2.4.0 to 2.5.0 (#5469)
Bumps [celes](https://github.com/mikelodder7/celes) from 2.4.0 to 2.5.0.
- [Commits](https://github.com/mikelodder7/celes/commits/2.5.0)

---
updated-dependencies:
- dependency-name: celes
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-18 00:24:50 +01:00
dependabot[bot] 844030091f build(deps): bump colored from 2.1.0 to 2.2.0 (#5470)
Bumps [colored](https://github.com/mackwic/colored) from 2.1.0 to 2.2.0.
- [Release notes](https://github.com/mackwic/colored/releases)
- [Changelog](https://github.com/colored-rs/colored/blob/master/CHANGELOG.md)
- [Commits](https://github.com/mackwic/colored/compare/v2.1.0...v2.2.0)

---
updated-dependencies:
- dependency-name: colored
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-18 00:24:36 +01:00
dependabot[bot] a7a421b006 build(deps): bump utoipa-swagger-ui from 8.0.3 to 8.1.0 (#5471)
Bumps [utoipa-swagger-ui](https://github.com/juhaku/utoipa) from 8.0.3 to 8.1.0.
- [Release notes](https://github.com/juhaku/utoipa/releases)
- [Changelog](https://github.com/juhaku/utoipa/blob/master/utoipa-rapidoc/CHANGELOG.md)
- [Commits](https://github.com/juhaku/utoipa/compare/utoipa-swagger-ui-8.0.3...utoipa-swagger-ui-8.1.0)

---
updated-dependencies:
- dependency-name: utoipa-swagger-ui
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-18 00:24:24 +01:00
import this 6680fbd61a [DOCs/operators]: Relase notes for v2025.3-ruta & SGPv2 form for public (#5481)
* new relase notess + SG2 rules

* PR ready to review

* PR ready to review

* fix review comments
2025-02-17 16:48:44 +00:00
Jack Wampler fe2d21cf88 Add a middleware layer to the nym api allowing for data compression (#5451) 2025-02-17 09:05:24 -07:00
Jon Häggblad eeaca9fc10 Run cargo autoinherit (#5460)
* cargo autoinherit

* sort
2025-02-17 15:05:27 +00:00
Jon Häggblad 7255f79b9c Merge pull request #5435 from nymtech/jon/task-all-stop
Remove all recv_with_delay and add shutdown condition to loops in client-core
2025-02-17 15:54:34 +01:00
Tommy Verrall 589069504a Merge pull request #5463 from nymtech/dependabot/npm_and_yarn/docker/typescript_client/upload_contract/elliptic-6.6.1
build(deps): bump elliptic from 6.5.4 to 6.6.1 in /docker/typescript_client/upload_contract
2025-02-17 14:48:09 +01:00
Jon Häggblad 4da7bc7442 Fix wasm client stats sender task client 2025-02-17 14:37:34 +01:00
Jon Häggblad 35be8de9f1 Update task fork names to be consistent 2025-02-17 14:37:34 +01:00
Jon Häggblad 2b14a9e6f8 Fix unexpected drop: 2025-02-17 14:37:34 +01:00
Jon Häggblad e9269da897 Fix using is_shutdown_poll 2025-02-17 14:37:34 +01:00
Jon Häggblad 7bceeadf16 Include MessageHandler 2025-02-17 14:37:34 +01:00
Jon Häggblad e72ce8fa92 Fix bug with ack control task client 2025-02-17 14:37:34 +01:00
Jon Häggblad 1ccdd5d660 Also remove a bunch of panics in the native client 2025-02-17 14:37:34 +01:00
Jon Häggblad c6d38d3c4f Also include topology refresher and mix traffic controller 2025-02-17 14:37:34 +01:00
Jon Häggblad e8e2bf107f Wrap more send errors in shutdown check 2025-02-17 14:37:34 +01:00
Jon Häggblad efe4e5c1c1 Move TaskClient to Self in few tasks 2025-02-17 14:37:34 +01:00
Jon Häggblad 2230609a72 Use a TaskClient in client stats sender 2025-02-17 14:37:34 +01:00
Jon Häggblad 6d80c37b21 Tweak logging 2025-02-17 14:37:34 +01:00
Jon Häggblad cb8b4c56af Remove a bunch of unwraps from client-core 2025-02-17 14:37:34 +01:00
Jon Häggblad 4d486abfef Remove all recv_with_delay and add shutdown condition to loops in client-core
Inside client-core we want to prepare the ground for moving a behaviour
close to what we have in the vpn client.

Remove all the recv_with_delay since we want to just stop

Add shutdown condition to all select loops to guard against the shutdown
listener being polled inside the select blocks.
2025-02-17 14:37:34 +01:00
Jędrzej Stuczyński b694845e4c added missing import to doctest (#5480) 2025-02-17 13:27:47 +00:00
Jon Häggblad 5cb2800d15 Trigger contracts CI on main workspace Cargo changes (#5477)
Since the contracts workspace depends on the common code in the main
workspace, and since the contracts are critical to not have regressions
in, trigger contracts CI on any changes to the workspace
Cargo.toml and lock files.
2025-02-17 13:00:40 +01:00
Jędrzej Stuczyński fd14394958 adjusted TestSetup::new_complex to ensure bonded node's existence (#5478) 2025-02-17 11:52:53 +00:00
Drazen Urch 134883522d Seedable clients (#5440)
* Seedable clients

* Finalize seedable PR

* Address PR comments

* More generic DerivationMaterials init

* Fix xoring the wrong index

* Tests
2025-02-17 00:00:17 +01:00
dependabot[bot] 221e01e9b8 build(deps): bump elliptic in /docker/typescript_client/upload_contract
Bumps [elliptic](https://github.com/indutny/elliptic) from 6.5.4 to 6.6.1.
- [Commits](https://github.com/indutny/elliptic/compare/v6.5.4...v6.6.1)

---
updated-dependencies:
- dependency-name: elliptic
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-14 05:30:38 +00:00
Jon Häggblad dcc48db301 Fix clippy::precedence (#5457)
* Fix clippy::precedence

* Fix clippy::useless_conversion
2025-02-13 11:05:39 +00:00
dainius-nym 7528109693 fix: update fx average rate calcs to ignore 0 values (#5454)
* fix: update fx average rate calcs to ignore 0 values

* chore: bump version and format the code
2025-02-13 09:50:32 +00:00
Jon Häggblad 203d682f2c Upgrade tower to 0.5.2 (#5446) 2025-02-13 10:43:39 +01:00
dependabot[bot] 589575eed8 build(deps): bump publicsuffix from 2.2.3 to 2.3.0 (#5367)
Bumps [publicsuffix](https://github.com/rushmorem/publicsuffix) from 2.2.3 to 2.3.0.
- [Release notes](https://github.com/rushmorem/publicsuffix/releases)
- [Commits](https://github.com/rushmorem/publicsuffix/compare/v2.2.3...v2.3.0)

---
updated-dependencies:
- dependency-name: publicsuffix
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-13 00:50:34 +01:00
Jon Häggblad 35bf1cc717 Disable debug in wasm and wallet workflows too (#5459) 2025-02-13 00:16:32 +01:00
dependabot[bot] f5e02d5652 build(deps): bump hickory-proto from 0.24.2 to 0.24.3 (#5444)
* build(deps): bump hickory-proto from 0.24.2 to 0.24.3

Bumps [hickory-proto](https://github.com/hickory-dns/hickory-dns) from 0.24.2 to 0.24.3.
- [Release notes](https://github.com/hickory-dns/hickory-dns/releases)
- [Changelog](https://github.com/hickory-dns/hickory-dns/blob/v0.24.3/CHANGELOG.md)
- [Commits](https://github.com/hickory-dns/hickory-dns/compare/v0.24.2...v0.24.3)

---
updated-dependencies:
- dependency-name: hickory-proto
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

* Don't downgrade rand_core

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jon Häggblad <jon.haggblad@gmail.com>
2025-02-13 00:09:03 +01:00
dependabot[bot] 2fc641a7ff build(deps): bump hyper from 1.4.1 to 1.6.0 (#5416)
Bumps [hyper](https://github.com/hyperium/hyper) from 1.4.1 to 1.6.0.
- [Release notes](https://github.com/hyperium/hyper/releases)
- [Changelog](https://github.com/hyperium/hyper/blob/master/CHANGELOG.md)
- [Commits](https://github.com/hyperium/hyper/compare/v1.4.1...v1.6.0)

---
updated-dependencies:
- dependency-name: hyper
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-13 00:05:39 +01:00
dependabot[bot] 0ccca19cc2 build(deps): bump uniffi_build from 0.25.3 to 0.29.0 (#5448)
* build(deps): bump uniffi_build from 0.25.3 to 0.29.0

Bumps [uniffi_build](https://github.com/mozilla/uniffi-rs) from 0.25.3 to 0.29.0.
- [Changelog](https://github.com/mozilla/uniffi-rs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/mozilla/uniffi-rs/compare/v0.25.3...v0.29.0)

---
updated-dependencies:
- dependency-name: uniffi_build
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* Also update uniffi to match uniffi_build

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jon Häggblad <jon.haggblad@gmail.com>
2025-02-12 23:56:02 +01:00
Jon Häggblad a07e567eb2 Set debug to false in ci-build.yml (#5458) 2025-02-12 23:08:44 +01:00
Jon Häggblad f3400a0aa5 Add helper to extract a list of sqlite files with journal files wal/shm (#5452)
Co-authored-by: Andrej Mihajlov <andrej@nymtech.net>
2025-02-12 17:29:06 +01:00
dainius-nym bf8614a545 Feature/add gbp currency (#5453)
* features: add gbp currency to the fx price scrapper

* regenerated sqlx queries

* nump cargo version

---------

Co-authored-by: Jędrzej Stuczyński <jedrzej.stuczynski@gmail.com>
2025-02-12 13:16:34 +00:00
dynco-nym b7e3687757 Dz nym node stats (#5418)
* Remove blacklisted, inactive, reserve fields

* Remove gw.blacklisted

* Remove blacklisted and bonded count

* DB operations

* Improve logging

* Remove unused functions

* get_nym_nodes for scraping WIP

* Separate nym_nodes from mixnode stats
- fixes FOREIGN_KEY_CONSTRAINT error when storing
  stats for nym_nodes which aren't in mixnodes table

* Daily aggregation works

* mixnodes/stats exposes correct info

* Undo unnecessary tidbits

* Replace obsolete stats

* Add total_stake

* Bump cargo.toml version

* Rename MixingNodeKind for better clarity
2025-02-11 12:07:15 +01:00
windy-ux b9b969b7d3 + specify worker-src (#5443)
+ CSP from main website

Co-authored-by: benedetta davico <46782255+benedettadavico@users.noreply.github.com>
2025-02-11 10:19:12 +00:00
dependabot[bot] 47303e5b3b build(deps): bump openssl from 0.10.56 to 0.10.70 in /nym-wallet (#5422)
Bumps [openssl](https://github.com/sfackler/rust-openssl) from 0.10.56 to 0.10.70.
- [Release notes](https://github.com/sfackler/rust-openssl/releases)
- [Commits](https://github.com/sfackler/rust-openssl/compare/openssl-v0.10.56...openssl-v0.10.70)

---
updated-dependencies:
- dependency-name: openssl
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-10 22:27:42 +01:00
benedettadavico a3e19b4563 update changelog 2025-02-10 18:14:47 +01:00
benedettadavico 54aef7c242 bump binary versions 2025-02-07 10:21:16 +01:00
123 changed files with 3087 additions and 1592 deletions
+14
View File
@@ -54,6 +54,20 @@ jobs:
override: true
components: rustfmt, clippy
# To avoid running out of disk space, skip generating debug symbols
- name: Set debug to false (unix)
if: contains(matrix.os, 'ubuntu') || contains(matrix.os, 'mac')
run: |
sed -i.bak 's/\[profile.dev\]/\[profile.dev\]\ndebug = false/' Cargo.toml
git diff
- name: Set debug to false (win)
if: contains(matrix.os, 'windows')
shell: pwsh
run: |
(Get-Content Cargo.toml) -replace '\[profile.dev\]', "`$&`ndebug = false" | Set-Content Cargo.toml
git diff
- name: Check formatting
uses: actions-rs/cargo@v1
with:
+2
View File
@@ -9,6 +9,8 @@ on:
paths:
- 'contracts/**'
- 'common/**'
- 'Cargo.lock'
- 'Cargo.toml'
- '.github/workflows/ci-contracts.yml'
jobs:
+6
View File
@@ -30,6 +30,12 @@ jobs:
override: true
components: rustfmt, clippy
- name: Set debug to false
working-directory: nym-wallet
run: |
sed -i.bak '1s/^/\[profile.dev\]\ndebug = false\n\n/' Cargo.toml
git diff
- name: Build all binaries
uses: actions-rs/cargo@v1
with:
+5
View File
@@ -45,6 +45,11 @@ jobs:
- name: Install wasm-bindgen-cli
run: cargo install wasm-bindgen-cli
- name: Set debug to false
run: |
sed -i.bak 's/\[profile.dev\]/\[profile.dev\]\ndebug = false/' Cargo.toml
git diff
- name: "Build"
run: make sdk-wasm-build
+36
View File
@@ -4,6 +4,42 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
## [Unreleased]
## [2025.3-ruta] (2025-02-10)
- Push down forget me to client configs ([#5431])
- Fix statistics shutdown ([#5426])
- Make wait_for_graceful_shutdown to be pub ([#5424])
- Upgrade to thiserror 2.0 ([#5414])
- build(deps): bump the patch-updates group across 1 directory with 9 updates ([#5406])
- Relocate a validator api function ([#5401])
- Send shutdown instead of panic when reaching max fail ([#5398])
- Change Explorer URL to new smooshed nodes ([#5396])
- reduce log severity for checking topology validity ([#5395])
- MixnetClient can send ClientRequests ([#5381])
- Fix missing path triggers for CI ([#5380])
- Uncouple storage reference for bandwidth client ([#5372])
- build(deps): bump tokio from 1.40.0 to 1.43.0 ([#5370])
- DNS resolver configuration for internal HTTP client lookups ([#5355])
- Update README.md ([#5328])
- Update README.md ([#5327])
[#5431]: https://github.com/nymtech/nym/pull/5431
[#5426]: https://github.com/nymtech/nym/pull/5426
[#5424]: https://github.com/nymtech/nym/pull/5424
[#5414]: https://github.com/nymtech/nym/pull/5414
[#5406]: https://github.com/nymtech/nym/pull/5406
[#5401]: https://github.com/nymtech/nym/pull/5401
[#5398]: https://github.com/nymtech/nym/pull/5398
[#5396]: https://github.com/nymtech/nym/pull/5396
[#5395]: https://github.com/nymtech/nym/pull/5395
[#5381]: https://github.com/nymtech/nym/pull/5381
[#5380]: https://github.com/nymtech/nym/pull/5380
[#5372]: https://github.com/nymtech/nym/pull/5372
[#5370]: https://github.com/nymtech/nym/pull/5370
[#5355]: https://github.com/nymtech/nym/pull/5355
[#5328]: https://github.com/nymtech/nym/pull/5328
[#5327]: https://github.com/nymtech/nym/pull/5327
## [2025.2-hu] (2025-02-04)
- Feature/remove double spending bloomfilter ([#5417])
Generated
+274 -193
View File
File diff suppressed because it is too large Load Diff
+20 -13
View File
@@ -187,18 +187,21 @@ readme = "README.md"
[workspace.dependencies]
addr = "0.15.6"
aead = "0.5.2"
aes = "0.8.1"
aes-gcm = "0.10.1"
aes-gcm-siv = "0.11.1"
aead = "0.5.2"
ammonia = "4"
anyhow = "1.0.95"
arc-swap = "1.7.1"
argon2 = "0.5.0"
async-trait = "0.1.86"
axum-client-ip = "0.6.1"
axum = "0.7.5"
axum-client-ip = "0.6.1"
axum-extra = "0.9.4"
axum-test = "16.2.0"
base64 = "0.22.1"
base85rs = "0.1.3"
bincode = "1.3.3"
bip39 = { version = "2.0.0", features = ["zeroize"] }
bit-vec = "0.7.0" # can we unify those?
@@ -209,16 +212,16 @@ bs58 = "0.5.1"
bytecodec = "0.4.15"
bytes = "1.7.2"
cargo_metadata = "0.18.1"
celes = "2.4.0"
celes = "2.5.0"
cfg-if = "1.0.0"
chacha20 = "0.9.0"
chacha20poly1305 = "0.10.1"
chrono = "0.4.39"
cipher = "0.4.3"
clap = "4.5.28"
clap = "4.5.30"
clap_complete = "4.5"
clap_complete_fig = "4.5"
colored = "2.0"
colored = "2.2"
comfy-table = "7.1.4"
console = "0.15.10"
console-subscriber = "0.1.1"
@@ -239,8 +242,9 @@ doc-comment = "0.3"
dotenvy = "0.15.6"
ecdsa = "0.16"
ed25519-dalek = "2.1"
etherparse = "0.13.0"
env_logger = "0.11.6"
envy = "0.4"
etherparse = "0.13.0"
eyre = "0.6.9"
fastrand = "2.1.1"
flate2 = "1.0.35"
@@ -259,10 +263,10 @@ hmac = "0.12.1"
http = "1"
http-body-util = "0.1"
httpcodec = "0.2.3"
human-repr = "1.1.0"
humantime = "2.1.0"
humantime-serde = "1.1.1"
human-repr = "1.1.0"
hyper = "1.4.1"
hyper = "1.6.0"
hyper-util = "0.1"
indicatif = "0.17.11"
inquire = "0.6.2"
@@ -276,7 +280,6 @@ ledger-transport = "0.10.0"
ledger-transport-hid = "0.10.0"
log = "0.4"
maxminddb = "0.23.0"
rs_merkle = "1.4.2"
mime = "0.3.17"
moka = { version = "0.12", features = ["future"] }
nix = "0.27.1"
@@ -291,7 +294,7 @@ petgraph = "0.6.5"
pin-project = "1.1"
pin-project-lite = "0.2.16"
pretty_env_logger = "0.4.0"
publicsuffix = "2.2.3"
publicsuffix = "2.3.0"
quote = "1"
rand = "0.8.5"
rand_chacha = "0.3"
@@ -305,6 +308,7 @@ reqwest = { version = "0.12.4", default-features = false }
rocket = "0.5.0"
rocket_cors = "0.6.0"
rocket_okapi = "0.8.0"
rs_merkle = "1.4.2"
safer-ffi = "0.1.13"
schemars = "0.8.21"
semver = "1.0.25"
@@ -331,24 +335,27 @@ tempfile = "3.15"
thiserror = "2.0"
time = "0.3.37"
tokio = "1.43"
tokio-postgres = "0.7"
tokio-stream = "0.1.17"
tokio-test = "0.4.4"
tokio-tun = "0.11.5"
tokio-tungstenite = { version = "0.20.1" }
tokio-util = "0.7.13"
toml = "0.8.20"
tower = "0.4.13"
tower = "0.5.2"
tower-http = "0.5.2"
tracing = "0.1.41"
tracing-log = "0.2"
tracing-opentelemetry = "0.19.0"
tracing-subscriber = "0.3.19"
tracing-tree = "0.2.2"
tracing-log = "0.2"
ts-rs = "10.1.0"
tungstenite = { version = "0.20.1", default-features = false }
uniffi = "0.29.0"
uniffi_build = "0.29.0"
url = "2.5"
utoipa = "5.2"
utoipa-swagger-ui = "8.0"
utoipa-swagger-ui = "8.1"
utoipauto = "0.2"
uuid = "*"
vergen = { version = "=8.3.1", default-features = false }
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "nym-client"
version = "1.1.47"
version = "1.1.49"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>", "Jędrzej Stuczyński <andrew@nymtech.net>"]
description = "Implementation of the Nym Client"
edition = "2021"
+8 -3
View File
@@ -49,7 +49,7 @@ impl SocketClient {
client_output: ClientOutput,
client_state: ClientState,
self_address: &Recipient,
shutdown: nym_task::TaskClient,
task_client: nym_task::TaskClient,
packet_type: PacketType,
) {
info!("Starting websocket listener...");
@@ -77,10 +77,15 @@ impl SocketClient {
shared_lane_queue_lengths,
reply_controller_sender,
Some(packet_type),
task_client.fork("websocket_handler"),
);
websocket::Listener::new(config.socket.host, config.socket.listening_port)
.start(websocket_handler, shutdown);
websocket::Listener::new(
config.socket.host,
config.socket.listening_port,
task_client.with_suffix("websocket_listener"),
)
.start(websocket_handler);
}
/// blocking version of `start_socket` method. Will run forever (or until SIGINT is sent)
+66 -40
View File
@@ -19,6 +19,7 @@ use nym_sphinx::receiver::ReconstructedMessage;
use nym_task::connections::{
ConnectionCommand, ConnectionCommandSender, ConnectionId, LaneQueueLengths, TransmissionLane,
};
use nym_task::TaskClient;
use std::time::Duration;
use tokio::net::TcpStream;
use tokio::time::Instant;
@@ -43,9 +44,11 @@ pub(crate) struct HandlerBuilder {
lane_queue_lengths: LaneQueueLengths,
reply_controller_sender: ReplyControllerSender,
packet_type: Option<PacketType>,
task_client: TaskClient,
}
impl HandlerBuilder {
#[allow(clippy::too_many_arguments)]
pub(crate) fn new(
msg_input: InputMessageSender,
client_connection_tx: ConnectionCommandSender,
@@ -54,6 +57,7 @@ impl HandlerBuilder {
lane_queue_lengths: LaneQueueLengths,
reply_controller_sender: ReplyControllerSender,
packet_type: Option<PacketType>,
task_client: TaskClient,
) -> Self {
Self {
msg_input,
@@ -63,11 +67,14 @@ impl HandlerBuilder {
lane_queue_lengths,
reply_controller_sender,
packet_type,
task_client,
}
}
// TODO: make sure we only ever have one active handler
pub fn create_active_handler(&self) -> Handler {
let mut task_client = self.task_client.fork("active_handler");
task_client.disarm();
Handler {
msg_input: self.msg_input.clone(),
client_connection_tx: self.client_connection_tx.clone(),
@@ -78,6 +85,7 @@ impl HandlerBuilder {
lane_queue_lengths: self.lane_queue_lengths.clone(),
reply_controller_sender: self.reply_controller_sender.clone(),
packet_type: self.packet_type,
task_client,
}
}
}
@@ -92,16 +100,18 @@ pub(crate) struct Handler {
lane_queue_lengths: LaneQueueLengths,
reply_controller_sender: ReplyControllerSender,
packet_type: Option<PacketType>,
task_client: TaskClient,
}
impl Drop for Handler {
fn drop(&mut self) {
if self
if let Err(err) = self
.buffer_requester
.unbounded_send(ReceivedBufferMessage::ReceiverDisconnect)
.is_err()
{
error!("we failed to disconnect the receiver from the buffer! presumably the shutdown procedure has been initiated!")
if !self.task_client.is_shutdown_poll() {
error!("failed to disconnect the receiver from the buffer: {err}");
}
}
}
}
@@ -125,10 +135,23 @@ impl Handler {
};
// get the number of pending replies waiting for reply surbs
let reply_queue_length = self
let reply_queue_length = match self
.reply_controller_sender
.get_lane_queue_length(connection_id)
.await;
.await
{
Ok(length) => length,
Err(err) => {
if !self.task_client.is_shutdown_poll() {
error!(
"Failed to get reply queue length for connection {connection_id}: {err}"
);
}
// We're just going to assume that the queue is empty, and I think that's okay
// during shutdown.
0
}
};
let queue_length = base_length + reply_queue_length;
@@ -168,10 +191,11 @@ impl Handler {
// the ack control is now responsible for chunking, etc.
let input_msg = InputMessage::new_regular(recipient, message, lane, self.packet_type);
self.msg_input
.send(input_msg)
.await
.expect("InputMessageReceiver has stopped receiving!");
if let Err(err) = self.msg_input.send(input_msg).await {
if !self.task_client.is_shutdown_poll() {
error!("Failed to send message to the input buffer: {err}");
}
}
// Only reply back with a `LaneQueueLength` if the sender providided a connection id
let TransmissionLane::ConnectionId(connection_id) = lane else {
@@ -200,10 +224,11 @@ impl Handler {
let input_msg =
InputMessage::new_anonymous(recipient, message, reply_surbs, lane, self.packet_type);
self.msg_input
.send(input_msg)
.await
.expect("InputMessageReceiver has stopped receiving!");
if let Err(err) = self.msg_input.send(input_msg).await {
if !self.task_client.is_shutdown_poll() {
error!("Failed to send anonymous message to the input buffer: {err}");
}
}
// Only reply back with a `LaneQueueLength` if the sender providided a connection id
let TransmissionLane::ConnectionId(connection_id) = lane else {
@@ -227,10 +252,11 @@ impl Handler {
});
let input_msg = InputMessage::new_reply(recipient_tag, message, lane, self.packet_type);
self.msg_input
.send(input_msg)
.await
.expect("InputMessageReceiver has stopped receiving!");
if let Err(err) = self.msg_input.send(input_msg).await {
if !self.task_client.is_shutdown_poll() {
error!("Failed to send reply message to the input buffer: {err}");
}
}
// Only reply back with a `LaneQueueLength` if the sender providided a connection id
let TransmissionLane::ConnectionId(connection_id) = lane else {
@@ -245,9 +271,14 @@ impl Handler {
}
fn handle_closed_connection(&self, connection_id: u64) -> Option<ServerResponse> {
self.client_connection_tx
if let Err(err) = self
.client_connection_tx
.unbounded_send(ConnectionCommand::Close(connection_id))
.unwrap();
{
if !self.task_client.is_shutdown_poll() {
error!("Failed to send close connection command: {err}");
}
}
None
}
@@ -362,11 +393,10 @@ impl Handler {
}
}
async fn listen_for_requests(
&mut self,
mut msg_receiver: ReconstructedMessagesReceiver,
mut task_client: nym_task::TaskClient,
) {
async fn listen_for_requests(&mut self, mut msg_receiver: ReconstructedMessagesReceiver) {
let mut task_client = self.task_client.fork("select");
task_client.disarm();
while !task_client.is_shutdown() {
tokio::select! {
// we can either get a client request from the websocket
@@ -415,15 +445,7 @@ impl Handler {
}
// consume self to make sure `drop` is called after this is done
pub(crate) async fn handle_connection(
mut self,
socket: TcpStream,
mut task_client: nym_task::TaskClient,
) {
// We don't want a crash in the connection handler to trigger a shutdown of the whole
// process.
task_client.disarm();
pub(crate) async fn handle_connection(mut self, socket: TcpStream) {
let ws_stream = match accept_async(socket).await {
Ok(ws_stream) => ws_stream,
Err(err) => {
@@ -436,14 +458,18 @@ impl Handler {
let (reconstructed_sender, reconstructed_receiver) = mpsc::unbounded();
// tell the buffer to start sending stuff to us
self.buffer_requester
.unbounded_send(ReceivedBufferMessage::ReceiverAnnounce(
reconstructed_sender,
))
.expect("the buffer request failed!");
if let Err(err) =
self.buffer_requester
.unbounded_send(ReceivedBufferMessage::ReceiverAnnounce(
reconstructed_sender,
))
{
if !self.task_client.is_shutdown_poll() {
error!("failed to announce the receiver to the buffer: {err}");
}
}
self.listen_for_requests(reconstructed_receiver, task_client)
.await;
self.listen_for_requests(reconstructed_receiver).await;
}
}
+11 -17
View File
@@ -3,6 +3,7 @@
use super::handler::HandlerBuilder;
use log::*;
use nym_task::TaskClient;
use std::net::IpAddr;
use std::{net::SocketAddr, process, sync::Arc};
use tokio::io::AsyncWriteExt;
@@ -22,21 +23,19 @@ impl State {
pub(crate) struct Listener {
address: SocketAddr,
state: State,
task_client: TaskClient,
}
impl Listener {
pub(crate) fn new(host: IpAddr, port: u16) -> Self {
pub(crate) fn new(host: IpAddr, port: u16, task_client: TaskClient) -> Self {
Listener {
address: SocketAddr::new(host, port),
state: State::AwaitingConnection,
task_client,
}
}
pub(crate) async fn run(
&mut self,
handler: HandlerBuilder,
mut task_client: nym_task::TaskClient,
) {
pub(crate) async fn run(&mut self, handler: HandlerBuilder) {
let tcp_listener = match tokio::net::TcpListener::bind(self.address).await {
Ok(listener) => listener,
Err(err) => {
@@ -47,11 +46,11 @@ impl Listener {
let notify = Arc::new(Notify::new());
loop {
while !self.task_client.is_shutdown() {
tokio::select! {
// When the handler finishes we check if shutdown is signalled
_ = notify.notified() => {
if task_client.is_shutdown() {
if self.task_client.is_shutdown() {
log::trace!("Websocket listener: detected shutdown after connection closed");
break;
}
@@ -60,7 +59,7 @@ impl Listener {
}
// ... but when there is no connected client at the time of shutdown being
// signalled, we handle it here.
_ = task_client.recv() => {
_ = self.task_client.recv() => {
if !self.state.is_connected() {
log::trace!("Not connected: shutting down");
break;
@@ -88,9 +87,8 @@ impl Listener {
// hanging because the executor doesn't come back here
let notify_clone = Arc::clone(&notify);
let fresh_handler = handler.create_active_handler();
let task_client_handler = task_client.clone();
tokio::spawn(async move {
fresh_handler.handle_connection(socket, task_client_handler).await;
fresh_handler.handle_connection(socket).await;
notify_clone.notify_one();
});
self.state = State::Connected;
@@ -104,13 +102,9 @@ impl Listener {
log::debug!("Websocket listener: Exiting");
}
pub(crate) fn start(
mut self,
handler: HandlerBuilder,
shutdown: nym_task::TaskClient,
) -> JoinHandle<()> {
pub(crate) fn start(mut self, handler: HandlerBuilder) -> JoinHandle<()> {
info!("Running websocket on {:?}", self.address.to_string());
tokio::spawn(async move { self.run(handler, shutdown).await })
tokio::spawn(async move { self.run(handler).await })
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "nym-socks5-client"
version = "1.1.47"
version = "1.1.49"
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"
@@ -60,7 +60,7 @@ impl From<IpAddr> for IpPair {
std::net::IpAddr::V4(ipv4_addr) => (ipv4_addr.octets()[2], ipv4_addr.octets()[3]),
std::net::IpAddr::V6(ipv6_addr) => (ipv6_addr.octets()[14], ipv6_addr.octets()[15]),
};
let last_bytes = (before_last_byte as u16) << 8 | last_byte as u16;
let last_bytes = ((before_last_byte as u16) << 8) | last_byte as u16;
let ipv4 = Ipv4Addr::new(
WG_TUN_DEVICE_IP_ADDRESS_V4.octets()[0],
WG_TUN_DEVICE_IP_ADDRESS_V4.octets()[1],
+4 -7
View File
@@ -105,26 +105,24 @@ impl<C, St: Storage> BandwidthController<C, St> {
async fn get_aggregate_verification_key(
&self,
epoch_id: EpochId,
apis: &mut ApiClientsWrapper,
ecash_apis: &mut ApiClientsWrapper<'_, C>,
) -> Result<VerificationKeyAuth, BandwidthControllerError>
where
C: DkgQueryClient + Sync + Send,
<St as Storage>::StorageError: Send + Sync + 'static,
{
let ecash_apis = apis.get_or_init(epoch_id, &self.client).await?;
get_aggregate_verification_key(&self.storage, epoch_id, ecash_apis).await
}
async fn get_coin_index_signatures(
&self,
epoch_id: EpochId,
apis: &mut ApiClientsWrapper,
ecash_apis: &mut ApiClientsWrapper<'_, C>,
) -> Result<Vec<AnnotatedCoinIndexSignature>, BandwidthControllerError>
where
C: DkgQueryClient + Sync + Send,
<St as Storage>::StorageError: Send + Sync + 'static,
{
let ecash_apis = apis.get_or_init(epoch_id, &self.client).await?;
get_coin_index_signatures(&self.storage, epoch_id, ecash_apis).await
}
@@ -132,13 +130,12 @@ impl<C, St: Storage> BandwidthController<C, St> {
&self,
epoch_id: EpochId,
expiration_date: Date,
apis: &mut ApiClientsWrapper,
ecash_apis: &mut ApiClientsWrapper<'_, C>,
) -> Result<Vec<AnnotatedExpirationDateSignature>, BandwidthControllerError>
where
C: DkgQueryClient + Sync + Send,
<St as Storage>::StorageError: Send + Sync + 'static,
{
let ecash_apis = apis.get_or_init(epoch_id, &self.client).await?;
get_expiration_date_signatures(&self.storage, epoch_id, expiration_date, ecash_apis).await
}
@@ -154,7 +151,7 @@ impl<C, St: Storage> BandwidthController<C, St> {
{
let epoch_id = retrieved_ticketbook.ticketbook.epoch_id();
let expiration_date = retrieved_ticketbook.ticketbook.expiration_date();
let mut api_clients = Default::default();
let mut api_clients = ApiClientsWrapper::new(&self.client, epoch_id);
let verification_key = self
.get_aggregate_verification_key(epoch_id, &mut api_clients)
+64 -21
View File
@@ -21,30 +21,67 @@ use rand::thread_rng;
use std::fmt::Display;
use std::future::Future;
// it really doesn't need the RwLock because it's never moved across tasks,
// but we need all the Send/Sync action
#[derive(Default)]
pub(crate) struct ApiClientsWrapper(Option<Vec<EcashApiClient>>);
impl ApiClientsWrapper {
pub(crate) async fn get_or_init<C>(
pub(crate) trait EcashClientsProvider {
async fn try_get_ecash_clients(
&mut self,
) -> Result<Vec<EcashApiClient>, BandwidthControllerError>;
}
impl EcashClientsProvider for Vec<EcashApiClient> {
async fn try_get_ecash_clients(
&mut self,
) -> Result<Vec<EcashApiClient>, BandwidthControllerError> {
Ok(self.clone())
}
}
impl<C> EcashClientsProvider for &mut ApiClientsWrapper<'_, C>
where
C: DkgQueryClient + Sync + Send,
{
async fn try_get_ecash_clients(
&mut self,
) -> Result<Vec<EcashApiClient>, BandwidthControllerError> {
self.clients().await
}
}
pub(crate) enum ApiClientsWrapper<'a, C> {
Uninitialised {
query_client: &'a C,
epoch_id: EpochId,
dkg_client: &C,
) -> Result<Vec<EcashApiClient>, BandwidthControllerError>
},
Cached {
clients: Vec<EcashApiClient>,
},
}
impl<'a, C> ApiClientsWrapper<'a, C> {
pub(crate) fn new(query_client: &'a C, epoch_id: EpochId) -> Self {
ApiClientsWrapper::Uninitialised {
query_client,
epoch_id,
}
}
async fn clients(&mut self) -> Result<Vec<EcashApiClient>, BandwidthControllerError>
where
C: DkgQueryClient + Sync + Send,
{
if let Some(cached) = &self.0 {
return Ok(cached.clone());
match self {
ApiClientsWrapper::Uninitialised {
query_client,
epoch_id,
} => {
let clients = all_ecash_api_clients(*query_client, *epoch_id).await?;
*self = ApiClientsWrapper::Cached {
clients: clients.clone(),
};
Ok(clients)
}
ApiClientsWrapper::Cached { clients } => Ok(clients.clone()),
}
let clients = all_ecash_api_clients(dkg_client, epoch_id).await?;
// technically we don't have to be cloning all the clients here, but it's way simpler than
// dealing with locking and whatnot given the performance penalty is negligible
self.0 = Some(clients.clone());
Ok(clients)
}
}
@@ -76,7 +113,7 @@ where
pub(crate) async fn get_aggregate_verification_key<St>(
storage: &St,
epoch_id: EpochId,
ecash_apis: Vec<EcashApiClient>,
mut ecash_apis: impl EcashClientsProvider,
) -> Result<VerificationKeyAuth, BandwidthControllerError>
where
St: Storage,
@@ -90,6 +127,8 @@ where
return Ok(stored);
};
let ecash_apis = ecash_apis.try_get_ecash_clients().await?;
let master_vk = query_random_apis_until_success(
ecash_apis,
|api| async move { api.api_client.master_verification_key(Some(epoch_id)).await },
@@ -115,7 +154,7 @@ where
pub(crate) async fn get_coin_index_signatures<St>(
storage: &St,
epoch_id: EpochId,
ecash_apis: Vec<EcashApiClient>,
mut ecash_apis: impl EcashClientsProvider,
) -> Result<Vec<AnnotatedCoinIndexSignature>, BandwidthControllerError>
where
St: Storage,
@@ -129,6 +168,8 @@ where
return Ok(stored);
};
let ecash_apis = ecash_apis.try_get_ecash_clients().await?;
let index_sigs = query_random_apis_until_success(
ecash_apis,
|api| async move {
@@ -159,7 +200,7 @@ pub(crate) async fn get_expiration_date_signatures<St>(
storage: &St,
epoch_id: EpochId,
expiration_date: Date,
ecash_apis: Vec<EcashApiClient>,
mut ecash_apis: impl EcashClientsProvider,
) -> Result<Vec<AnnotatedExpirationDateSignature>, BandwidthControllerError>
where
St: Storage,
@@ -173,6 +214,8 @@ where
return Ok(stored);
};
let ecash_apis = ecash_apis.try_get_ecash_clients().await?;
let expiration_sigs = query_random_apis_until_success(
ecash_apis,
|api| async move {
@@ -40,6 +40,7 @@ use nym_client_core_config_types::ForgetMe;
use nym_client_core_gateways_storage::{GatewayDetails, GatewaysDetailsStore};
use nym_credential_storage::storage::Storage as CredentialStorage;
use nym_crypto::asymmetric::{encryption, identity};
use nym_crypto::hkdf::DerivationMaterial;
use nym_gateway_client::client::config::GatewayClientConfig;
use nym_gateway_client::{
AcknowledgementReceiver, GatewayClient, GatewayConfig, MixnetMessageReceiver, PacketRouter,
@@ -192,6 +193,8 @@ pub struct BaseClientBuilder<C, S: MixnetClientStorage> {
#[cfg(unix)]
connection_fd_callback: Option<Arc<dyn Fn(RawFd) + Send + Sync>>,
derivation_material: Option<DerivationMaterial>,
}
impl<C, S> BaseClientBuilder<C, S>
@@ -216,9 +219,19 @@ where
setup_method: GatewaySetup::MustLoad { gateway_id: None },
#[cfg(unix)]
connection_fd_callback: None,
derivation_material: None,
}
}
#[must_use]
pub fn with_derivation_material(
mut self,
derivation_material: Option<DerivationMaterial>,
) -> Self {
self.derivation_material = derivation_material;
self
}
#[must_use]
pub fn with_forget_me(mut self, forget_me: &ForgetMe) -> Self {
self.config.debug.forget_me = *forget_me;
@@ -297,7 +310,7 @@ where
topology_accessor: TopologyAccessor,
mix_tx: BatchMixMessageSender,
stats_tx: ClientStatsSender,
shutdown: TaskClient,
task_client: TaskClient,
) {
info!("Starting loop cover traffic stream...");
@@ -310,9 +323,10 @@ where
debug_config.traffic,
debug_config.cover_traffic,
stats_tx,
task_client,
);
stream.start_with_shutdown(shutdown);
stream.start();
}
#[allow(clippy::too_many_arguments)]
@@ -327,7 +341,7 @@ where
reply_controller_receiver: ReplyControllerReceiver,
lane_queue_lengths: LaneQueueLengths,
client_connection_rx: ConnectionCommandReceiver,
shutdown: TaskClient,
task_client: TaskClient,
packet_type: PacketType,
stats_tx: ClientStatsSender,
) {
@@ -345,8 +359,9 @@ where
lane_queue_lengths,
client_connection_rx,
stats_tx,
task_client,
)
.start_with_shutdown(shutdown, packet_type);
.start(packet_type);
}
// buffer controlling all messages fetched from provider
@@ -369,8 +384,9 @@ where
reply_key_storage,
reply_controller_sender,
metrics_reporter,
shutdown,
);
controller.start_with_shutdown(shutdown)
controller.start()
}
#[allow(clippy::too_many_arguments)]
@@ -559,15 +575,22 @@ where
topology_accessor: TopologyAccessor,
local_gateway: NodeIdentity,
wait_for_gateway: bool,
mut shutdown: TaskClient,
mut task_client: TaskClient,
) -> Result<(), ClientCoreError> {
let topology_refresher_config =
TopologyRefresherConfig::new(topology_config.topology_refresh_rate);
if topology_config.disable_refreshing {
// if we're not spawning the refresher, don't cause shutdown immediately
info!("The background topology refesher is not going to be started");
task_client.disarm();
}
let mut topology_refresher = TopologyRefresher::new(
topology_refresher_config,
topology_accessor,
topology_provider,
task_client,
);
// before returning, block entire runtime to refresh the current network view so that any
// components depending on topology would see a non-empty view
@@ -608,15 +631,11 @@ where
}
}
if topology_config.disable_refreshing {
// if we're not spawning the refresher, don't cause shutdown immediately
info!("The topology refesher is not going to be started");
shutdown.disarm();
} else {
if !topology_config.disable_refreshing {
// don't spawn the refresher if we don't want to be refreshing the topology.
// only use the initial values obtained
info!("Starting topology refresher...");
topology_refresher.start_with_shutdown(shutdown);
topology_refresher.start();
}
Ok(())
@@ -627,17 +646,17 @@ where
user_agent: Option<UserAgent>,
client_stats_id: String,
input_sender: Sender<InputMessage>,
shutdown: TaskClient,
task_client: TaskClient,
) -> ClientStatsSender {
info!("Starting statistics control...");
StatisticsControl::create_and_start_with_shutdown(
StatisticsControl::create_and_start(
config.debug.stats_reporting,
user_agent
.map(|u| u.application)
.unwrap_or("unknown".to_string()),
client_stats_id,
input_sender.clone(),
shutdown.with_suffix("controller"),
task_client,
)
}
@@ -647,8 +666,8 @@ where
) -> (BatchMixMessageSender, ClientRequestSender) {
info!("Starting mix traffic controller...");
let (mix_traffic_controller, mix_tx, client_tx) =
MixTrafficController::new(gateway_transceiver);
mix_traffic_controller.start_with_shutdown(shutdown);
MixTrafficController::new(gateway_transceiver, shutdown);
mix_traffic_controller.start();
(mix_tx, client_tx)
}
@@ -684,6 +703,7 @@ where
setup_method: GatewaySetup,
key_store: &S::KeyStore,
details_store: &S::GatewaysDetailsStore,
derivation_material: Option<DerivationMaterial>,
) -> Result<InitialisationResult, ClientCoreError>
where
<S::KeyStore as KeyStore>::StorageError: Sync + Send,
@@ -693,7 +713,12 @@ where
if key_store.load_keys().await.is_err() {
info!("could not find valid client keys - a new set will be generated");
let mut rng = OsRng;
let keys = ClientKeys::generate_new(&mut rng);
let keys = if let Some(derivation_material) = derivation_material {
ClientKeys::from_master_key(&mut rng, &derivation_material)
.map_err(|_| ClientCoreError::HkdfDerivationError {})?
} else {
ClientKeys::generate_new(&mut rng)
};
store_client_keys(keys, key_store).await?;
}
@@ -715,6 +740,7 @@ where
self.setup_method,
self.client_store.key_store(),
self.client_store.gateway_details_store(),
self.derivation_material,
)
.await?;
@@ -13,6 +13,7 @@ use nym_sphinx::cover::generate_loop_cover_packet;
use nym_sphinx::params::{PacketSize, PacketType};
use nym_sphinx::utils::sample_poisson_duration;
use nym_statistics_common::clients::{packet_statistics::PacketStatisticsEvent, ClientStatsSender};
use nym_task::TaskClient;
use rand::{rngs::OsRng, CryptoRng, Rng};
use std::pin::Pin;
use std::sync::Arc;
@@ -64,6 +65,8 @@ where
packet_type: PacketType,
stats_tx: ClientStatsSender,
task_client: TaskClient,
}
impl<R> Stream for LoopCoverTrafficStream<R>
@@ -110,6 +113,7 @@ impl LoopCoverTrafficStream<OsRng> {
traffic_config: config::Traffic,
cover_config: config::CoverTraffic,
stats_tx: ClientStatsSender,
task_client: TaskClient,
) -> Self {
let rng = OsRng;
@@ -128,6 +132,7 @@ impl LoopCoverTrafficStream<OsRng> {
secondary_packet_size: traffic_config.secondary_packet_size,
packet_type: traffic_config.packet_type,
stats_tx,
task_client,
}
}
@@ -175,7 +180,7 @@ impl LoopCoverTrafficStream<OsRng> {
}
};
let cover_message = generate_loop_cover_packet(
let cover_message = match generate_loop_cover_packet(
&mut self.rng,
topology_ref,
&self.ack_key,
@@ -184,8 +189,15 @@ impl LoopCoverTrafficStream<OsRng> {
self.cover_traffic.loop_cover_traffic_average_delay,
cover_traffic_packet_size,
self.packet_type,
)
.expect("Somehow failed to generate a loop cover message with a valid topology");
) {
Ok(cover_message) => cover_message,
Err(err) => {
warn!(
"Somehow failed to generate a loop cover message with a valid topology: {err}"
);
return;
}
};
if let Err(err) = self.mix_tx.try_send(vec![cover_message]) {
match err {
@@ -217,7 +229,7 @@ impl LoopCoverTrafficStream<OsRng> {
tokio::task::yield_now().await;
}
pub fn start_with_shutdown(mut self, mut shutdown: nym_task::TaskClient) {
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
// so panic and review the code that lead to this branch
@@ -231,6 +243,8 @@ impl LoopCoverTrafficStream<OsRng> {
);
self.set_next_delay(sampled);
let mut shutdown = self.task_client.fork("select");
spawn_future(async move {
debug!("Started LoopCoverTrafficStream with graceful shutdown support");
@@ -2,7 +2,10 @@
// SPDX-License-Identifier: Apache-2.0
use crate::client::key_manager::persistence::KeyStore;
use nym_crypto::asymmetric::{encryption, identity};
use nym_crypto::{
asymmetric::{encryption, identity},
hkdf::{DerivationMaterial, InvalidLength},
};
use nym_gateway_requests::shared_key::{LegacySharedKeys, SharedGatewayKey, SharedSymmetricKey};
use nym_sphinx::acknowledgements::AckKey;
use rand::{CryptoRng, RngCore};
@@ -10,6 +13,7 @@ use std::sync::Arc;
use zeroize::ZeroizeOnDrop;
pub mod persistence;
mod test;
// Note: to support key rotation in the future, all keys will require adding an extra smart pointer,
// most likely an AtomicCell, or if it doesn't work as I think it does, a Mutex. Although I think
@@ -43,6 +47,24 @@ impl ClientKeys {
}
}
pub fn from_master_key<R>(
rng: &mut R,
derivation_material: &DerivationMaterial,
) -> Result<Self, InvalidLength>
where
R: RngCore + CryptoRng,
{
let secret = derivation_material.derive_secret()?;
Ok(ClientKeys {
identity_keypair: Arc::new(identity::KeyPair::from_secret(
secret,
derivation_material.index(),
)),
encryption_keypair: Arc::new(encryption::KeyPair::new(rng)),
ack_key: Arc::new(AckKey::new(rng)),
})
}
pub fn from_keys(
id_keypair: identity::KeyPair,
enc_keypair: encryption::KeyPair,
@@ -0,0 +1,89 @@
#[cfg(test)]
mod tests {
use crate::client::key_manager::ClientKeys;
use nym_crypto::hkdf::DerivationMaterial;
use rand::SeedableRng;
use rand_chacha::ChaCha20Rng;
#[test]
fn test_from_master_key_success() {
// Set up a deterministic RNG.
let seed = [33u8; 32];
let mut rng = ChaCha20Rng::from_seed(seed);
// Set up the derivation material.
let master_key = b"this is a secret master key";
let salt = b"unique-salt";
let derivation_material = DerivationMaterial::new(master_key, 0, salt);
// Generate ClientKeys from the master key.
let client_keys = ClientKeys::from_master_key(&mut rng, &derivation_material)
.expect("Failed to create client keys");
assert_eq!(
client_keys.identity_keypair().public_key().to_string(),
String::from("FX4Undr5LPPBA7zThWWpAKXKQTXSbW1C28PnxbCqUkU4")
);
assert_eq!(
client_keys.identity_keypair().private_key().to_string(),
String::from("6S3uMi2rU5SwyUUYCiMrF5qqdcYnEDMYLggBSvavVzEt")
);
}
#[test]
fn test_from_master_key_deterministic_identity() {
// Using identical derivation material should result in the exactly same identity keypair.
let seed = [1u8; 32];
let mut rng1 = ChaCha20Rng::from_seed(seed);
let mut rng2 = ChaCha20Rng::from_seed(seed);
let master_key = b"another secret master key";
let salt = b"deterministic-salt";
let index = 7u32;
let derivation_material = DerivationMaterial::new(master_key, index, salt);
let client_keys1 = ClientKeys::from_master_key(&mut rng1, &derivation_material)
.expect("Failed to create client keys (first instance)");
let client_keys2 = ClientKeys::from_master_key(&mut rng2, &derivation_material)
.expect("Failed to create client keys (second instance)");
assert_eq!(
client_keys1.identity_keypair().public_key().to_string(),
client_keys2.identity_keypair().public_key().to_string()
);
assert_eq!(
client_keys1.identity_keypair().private_key().to_string(),
client_keys2.identity_keypair().private_key().to_string()
);
}
#[test]
fn test_from_master_key_different_indices() {
// Changing the index should yield a different identity key.
let seed = [5u8; 32];
let mut rng = ChaCha20Rng::from_seed(seed);
let master_key = b"same secret key";
let salt = b"same-salt";
let derivation_material1 = DerivationMaterial::new(master_key, 1, salt);
let derivation_material2 = DerivationMaterial::new(master_key, 2, salt);
let client_keys1 = ClientKeys::from_master_key(&mut rng, &derivation_material1)
.expect("Failed to create client keys for index 1");
let client_keys2 = ClientKeys::from_master_key(&mut rng, &derivation_material2)
.expect("Failed to create client keys for index 2");
assert_ne!(
client_keys1.identity_keypair().public_key().to_string(),
client_keys2.identity_keypair().public_key().to_string()
);
assert_ne!(
client_keys1.identity_keypair().private_key().to_string(),
client_keys2.identity_keypair().private_key().to_string()
);
}
}
@@ -7,6 +7,7 @@ use crate::spawn_future;
use log::*;
use nym_gateway_requests::ClientRequest;
use nym_sphinx::forwarding::packet::MixPacket;
use nym_task::TaskClient;
use transceiver::ErasedGatewayError;
pub type BatchMixMessageSender = tokio::sync::mpsc::Sender<Vec<MixPacket>>;
@@ -32,11 +33,14 @@ pub struct MixTrafficController {
// TODO: this is temporary work-around.
// in long run `gateway_client` will be moved away from `MixTrafficController` anyway.
consecutive_gateway_failure_count: usize,
task_client: TaskClient,
}
impl MixTrafficController {
pub fn new<T>(
gateway_transceiver: T,
task_client: TaskClient,
) -> (
MixTrafficController,
BatchMixMessageSender,
@@ -56,6 +60,7 @@ impl MixTrafficController {
mix_rx: message_receiver,
client_rx: client_receiver,
consecutive_gateway_failure_count: 0,
task_client,
},
message_sender,
client_sender,
@@ -64,6 +69,7 @@ impl MixTrafficController {
pub fn new_dynamic(
gateway_transceiver: Box<dyn GatewayTransceiver + Send>,
task_client: TaskClient,
) -> (
MixTrafficController,
BatchMixMessageSender,
@@ -78,6 +84,7 @@ impl MixTrafficController {
mix_rx: message_receiver,
client_rx: client_receiver,
consecutive_gateway_failure_count: 0,
task_client,
},
message_sender,
client_sender,
@@ -109,11 +116,11 @@ impl MixTrafficController {
result
}
pub fn start_with_shutdown(mut self, mut shutdown: nym_task::TaskClient) {
pub fn start(mut self) {
spawn_future(async move {
debug!("Started MixTrafficController with graceful shutdown support");
loop {
while !self.task_client.is_shutdown() {
tokio::select! {
mix_packets = self.mix_rx.recv() => match mix_packets {
Some(mix_packets) => {
@@ -125,7 +132,7 @@ impl MixTrafficController {
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?
shutdown.send_we_stopped(Box::new(ClientCoreError::GatewayFailedToForwardMessages));
self.task_client.send_we_stopped(Box::new(ClientCoreError::GatewayFailedToForwardMessages));
break;
}
}
@@ -146,13 +153,13 @@ impl MixTrafficController {
log::trace!("MixTrafficController, client request channel closed");
}
},
_ = shutdown.recv_with_delay() => {
_ = self.task_client.recv() => {
log::trace!("MixTrafficController: Received shutdown");
break;
}
}
}
shutdown.recv_timeout().await;
self.task_client.recv_timeout().await;
log::debug!("MixTrafficController: Exiting");
});
@@ -11,6 +11,7 @@ use nym_sphinx::{
acknowledgements::{identifier::recover_identifier, AckKey},
chunking::fragment::{FragmentIdentifier, COVER_FRAG_ID},
};
use nym_task::TaskClient;
use std::sync::Arc;
/// Module responsible for listening for any data resembling acknowledgements from the network
@@ -20,6 +21,7 @@ pub(super) struct AcknowledgementListener {
ack_receiver: AcknowledgementReceiver,
action_sender: AckActionSender,
stats_tx: ClientStatsSender,
task_client: TaskClient,
}
impl AcknowledgementListener {
@@ -28,12 +30,14 @@ impl AcknowledgementListener {
ack_receiver: AcknowledgementReceiver,
action_sender: AckActionSender,
stats_tx: ClientStatsSender,
task_client: TaskClient,
) -> Self {
AcknowledgementListener {
ack_key,
ack_receiver,
action_sender,
stats_tx,
task_client,
}
}
@@ -64,9 +68,14 @@ impl AcknowledgementListener {
trace!("Received {} from the mix network", frag_id);
self.stats_tx
.report(PacketStatisticsEvent::RealAckReceived(ack_content.len()).into());
self.action_sender
if let Err(err) = self
.action_sender
.unbounded_send(Action::new_remove(frag_id))
.unwrap();
{
if !self.task_client.is_shutdown_poll() {
error!("Failed to send remove action to action controller: {err}");
}
}
}
async fn handle_ack_receiver_item(&mut self, item: Vec<Vec<u8>>) {
@@ -76,10 +85,10 @@ impl AcknowledgementListener {
}
}
pub(super) async fn run_with_shutdown(&mut self, mut shutdown: nym_task::TaskClient) {
pub(super) async fn run(&mut self) {
debug!("Started AcknowledgementListener with graceful shutdown support");
while !shutdown.is_shutdown() {
while !self.task_client.is_shutdown() {
tokio::select! {
acks = self.ack_receiver.next() => match acks {
Some(acks) => self.handle_ack_receiver_item(acks).await,
@@ -88,12 +97,12 @@ impl AcknowledgementListener {
break;
}
},
_ = shutdown.recv_with_delay() => {
_ = self.task_client.recv() => {
log::trace!("AcknowledgementListener: Received shutdown");
}
}
}
shutdown.recv_timeout().await;
self.task_client.recv_timeout().await;
log::debug!("AcknowledgementListener: Exiting");
}
}
@@ -9,6 +9,7 @@ use log::*;
use nym_nonexhaustive_delayqueue::{Expired, NonExhaustiveDelayQueue, QueueKey};
use nym_sphinx::chunking::fragment::FragmentIdentifier;
use nym_sphinx::Delay as SphinxDelay;
use nym_task::TaskClient;
use std::collections::HashMap;
use std::sync::Arc;
use std::time::Duration;
@@ -101,6 +102,8 @@ pub(super) struct ActionController {
/// Channel for notifying `RetransmissionRequestListener` about expired acknowledgements.
retransmission_sender: RetransmissionRequestSender,
task_client: TaskClient,
}
impl ActionController {
@@ -108,6 +111,7 @@ impl ActionController {
config: Config,
retransmission_sender: RetransmissionRequestSender,
incoming_actions: AckActionReceiver,
task_client: TaskClient,
) -> Self {
ActionController {
config,
@@ -115,6 +119,7 @@ impl ActionController {
pending_acks_timers: NonExhaustiveDelayQueue::new(),
incoming_actions,
retransmission_sender,
task_client,
}
}
@@ -216,11 +221,7 @@ impl ActionController {
}
// note: when the entry expires it's automatically removed from pending_acks_timers
fn handle_expired_ack_timer(
&mut self,
expired_ack: Expired<FragmentIdentifier>,
task_client: &mut nym_task::TaskClient,
) {
fn handle_expired_ack_timer(&mut self, expired_ack: Expired<FragmentIdentifier>) {
// I'm honestly not sure how to handle it, because getting it means other things in our
// system are already misbehaving. If we ever see this panic, then I guess we should worry
// about it. Perhaps just reschedule it at later point?
@@ -238,15 +239,13 @@ impl ActionController {
// downgrading an arc and then upgrading vs cloning is difference of 30ns vs 15ns
// so it's literally a NO difference while it might prevent us from unnecessarily
// resending data (in maybe 1 in 1 million cases, but it's something)
if self
if let Err(err) = self
.retransmission_sender
.unbounded_send(Arc::downgrade(pending_ack_data))
.is_err()
{
assert!(
task_client.is_shutdown_poll(),
"Failed to send pending ack for retransmission"
);
if !self.task_client.is_shutdown_poll() {
log::error!("Failed to send pending ack for retransmission: {err}");
}
}
} else {
// this shouldn't cause any issues but shouldn't have happened to begin with!
@@ -265,10 +264,10 @@ impl ActionController {
}
}
pub(super) async fn run_with_shutdown(&mut self, mut shutdown: nym_task::TaskClient) {
pub(super) async fn run(&mut self) {
debug!("Started ActionController with graceful shutdown support");
loop {
while !self.task_client.is_shutdown() {
tokio::select! {
action = self.incoming_actions.next() => match action {
Some(action) => self.process_action(action),
@@ -280,19 +279,19 @@ impl ActionController {
}
},
expired_ack = self.pending_acks_timers.next() => match expired_ack {
Some(expired_ack) => self.handle_expired_ack_timer(expired_ack, &mut shutdown),
Some(expired_ack) => self.handle_expired_ack_timer(expired_ack),
None => {
log::trace!("ActionController: Stopping since ack channel closed");
break;
}
},
_ = shutdown.recv_with_delay() => {
_ = self.task_client.recv() => {
log::trace!("ActionController: Received shutdown");
break;
}
}
}
shutdown.recv_timeout().await;
self.task_client.recv_timeout().await;
log::debug!("ActionController: Exiting");
}
}
@@ -11,6 +11,7 @@ use nym_sphinx::anonymous_replies::requests::AnonymousSenderTag;
use nym_sphinx::forwarding::packet::MixPacket;
use nym_sphinx::params::PacketType;
use nym_task::connections::TransmissionLane;
use nym_task::TaskClient;
use rand::{CryptoRng, Rng};
/// Module responsible for dealing with the received messages: splitting them, creating acknowledgements,
@@ -23,6 +24,7 @@ where
input_receiver: InputMessageReceiver,
message_handler: MessageHandler<R>,
reply_controller_sender: ReplyControllerSender,
task_client: TaskClient,
}
impl<R> InputMessageListener<R>
@@ -36,11 +38,13 @@ where
input_receiver: InputMessageReceiver,
message_handler: MessageHandler<R>,
reply_controller_sender: ReplyControllerSender,
task_client: TaskClient,
) -> Self {
InputMessageListener {
input_receiver,
message_handler,
reply_controller_sender,
task_client,
}
}
@@ -63,8 +67,14 @@ where
lane: TransmissionLane,
) {
// offload reply handling to the dedicated task
self.reply_controller_sender
if let Err(err) = self
.reply_controller_sender
.send_reply(recipient_tag, data, lane)
{
if !self.task_client.is_shutdown_poll() {
error!("failed to send a reply - {err}");
}
}
}
async fn handle_plain_message(
@@ -164,10 +174,10 @@ where
};
}
pub(super) async fn run_with_shutdown(&mut self, mut shutdown: nym_task::TaskClient) {
pub(super) async fn run(&mut self) {
debug!("Started InputMessageListener with graceful shutdown support");
while !shutdown.is_shutdown() {
while !self.task_client.is_shutdown() {
tokio::select! {
input_msg = self.input_receiver.recv() => match input_msg {
Some(input_msg) => {
@@ -178,12 +188,12 @@ where
break;
}
},
_ = shutdown.recv_with_delay() => {
_ = self.task_client.recv() => {
log::trace!("InputMessageListener: Received shutdown");
}
}
}
shutdown.recv_timeout().await;
self.task_client.recv_timeout().await;
log::debug!("InputMessageListener: Exiting");
}
}
@@ -24,6 +24,7 @@ use nym_sphinx::{
Delay as SphinxDelay,
};
use nym_statistics_common::clients::ClientStatsSender;
use nym_task::TaskClient;
use rand::{CryptoRng, Rng};
use std::{
sync::{Arc, Weak},
@@ -66,7 +67,7 @@ pub(crate) enum PacketDestination {
/// Structure representing a data `Fragment` that is on-route to the specified `Recipient`
#[derive(Debug)]
pub(crate) struct PendingAcknowledgement {
pub struct PendingAcknowledgement {
message_chunk: Fragment,
delay: SphinxDelay,
destination: PacketDestination,
@@ -216,6 +217,7 @@ where
message_handler: MessageHandler<R>,
reply_controller_sender: ReplyControllerSender,
stats_tx: ClientStatsSender,
task_client: TaskClient,
) -> Self {
let (retransmission_tx, retransmission_rx) = mpsc::unbounded();
@@ -225,6 +227,7 @@ where
action_config,
retransmission_tx,
connectors.ack_action_receiver,
task_client.fork("action_controller"),
);
// will listen for any acks coming from the network
@@ -233,6 +236,7 @@ where
connectors.ack_receiver,
connectors.ack_action_sender.clone(),
stats_tx,
task_client.fork("acknowledgement_listener"),
);
// will listen for any new messages from the client
@@ -240,6 +244,7 @@ where
connectors.input_receiver,
message_handler.clone(),
reply_controller_sender.clone(),
task_client.fork("input_message_listener"),
);
// will listen for any ack timeouts and trigger retransmission
@@ -249,12 +254,16 @@ where
message_handler,
retransmission_rx,
reply_controller_sender,
task_client.fork("retransmission_request_listener"),
);
// will listen for events indicating the packet was sent through the network so that
// the retransmission timer should be started.
let sent_notification_listener =
SentNotificationListener::new(connectors.sent_notifier, connectors.ack_action_sender);
let sent_notification_listener = SentNotificationListener::new(
connectors.sent_notifier,
connectors.ack_action_sender,
task_client.with_suffix("sent_notification_listener"),
);
AcknowledgementController {
acknowledgement_listener,
@@ -265,53 +274,35 @@ where
}
}
pub(super) fn start_with_shutdown(
self,
shutdown: nym_task::TaskClient,
packet_type: PacketType,
) {
pub(super) fn start(self, packet_type: PacketType) {
let mut acknowledgement_listener = self.acknowledgement_listener;
let mut input_message_listener = self.input_message_listener;
let mut retransmission_request_listener = self.retransmission_request_listener;
let mut sent_notification_listener = self.sent_notification_listener;
let mut action_controller = self.action_controller;
let shutdown_handle = shutdown.fork("acknowledgement_listener");
spawn_future(async move {
acknowledgement_listener
.run_with_shutdown(shutdown_handle)
.await;
acknowledgement_listener.run().await;
debug!("The acknowledgement listener has finished execution!");
});
let shutdown_handle = shutdown.fork("input_message_listener");
spawn_future(async move {
input_message_listener
.run_with_shutdown(shutdown_handle)
.await;
input_message_listener.run().await;
debug!("The input listener has finished execution!");
});
let shutdown_handle = shutdown.fork("retransmission_request_listener");
spawn_future(async move {
retransmission_request_listener
.run_with_shutdown(shutdown_handle, packet_type)
.await;
retransmission_request_listener.run(packet_type).await;
debug!("The retransmission request listener has finished execution!");
});
let shutdown_handle = shutdown.fork("sent_notification_listener");
spawn_future(async move {
sent_notification_listener
.run_with_shutdown(shutdown_handle)
.await;
sent_notification_listener.run().await;
debug!("The sent notification listener has finished execution!");
});
spawn_future(async move {
action_controller
.run_with_shutdown(shutdown.with_suffix("action_controller"))
.await;
action_controller.run().await;
debug!("The controller has finished execution!");
});
}
@@ -14,7 +14,7 @@ use log::*;
use nym_sphinx::chunking::fragment::Fragment;
use nym_sphinx::preparer::PreparedFragment;
use nym_sphinx::{addressing::clients::Recipient, params::PacketType};
use nym_task::connections::TransmissionLane;
use nym_task::{connections::TransmissionLane, TaskClient};
use rand::{CryptoRng, Rng};
use std::sync::{Arc, Weak};
@@ -25,6 +25,7 @@ pub(super) struct RetransmissionRequestListener<R> {
message_handler: MessageHandler<R>,
request_receiver: RetransmissionRequestReceiver,
reply_controller_sender: ReplyControllerSender,
task_client: TaskClient,
}
impl<R> RetransmissionRequestListener<R>
@@ -37,6 +38,7 @@ where
message_handler: MessageHandler<R>,
request_receiver: RetransmissionRequestReceiver,
reply_controller_sender: ReplyControllerSender,
task_client: TaskClient,
) -> Self {
RetransmissionRequestListener {
maximum_retransmissions,
@@ -44,6 +46,7 @@ where
message_handler,
request_receiver,
reply_controller_sender,
task_client,
}
}
@@ -79,9 +82,12 @@ where
if let Some(limit) = self.maximum_retransmissions {
if timed_out_ack.retransmissions >= limit {
warn!("reached maximum number of allowed retransmissions for the packet");
self.action_sender
if let Err(err) = self
.action_sender
.unbounded_send(Action::new_remove(frag_id))
.unwrap();
{
error!("Failed to send remove action to the controller: {err}");
}
return;
}
}
@@ -93,11 +99,16 @@ where
} => {
// if this is retransmission for reply, offload it to the dedicated task
// that deals with all the surbs
return self.reply_controller_sender.send_retransmission_data(
if let Err(err) = self.reply_controller_sender.send_retransmission_data(
*recipient_tag,
weak_timed_out_ack,
*extra_surb_request,
);
) {
if !self.task_client.is_shutdown_poll() {
error!("Failed to send retransmission data to the reply controller: {err}");
}
}
return;
}
PacketDestination::KnownRecipient(recipient) => {
self.prepare_normal_retransmission_chunk(
@@ -114,9 +125,12 @@ where
Err(err) => {
warn!("Could not retransmit the packet - {err}");
// we NEED to start timer here otherwise we will have this guy permanently stuck in memory
self.action_sender
if let Err(err) = self
.action_sender
.unbounded_send(Action::new_start_timer(frag_id))
.unwrap();
{
error!("Failed to send start timer action to the controller: {err}");
}
return;
}
};
@@ -141,9 +155,14 @@ where
// is sent to the `OutQueueControl` and has gone through its internal queue
// with the additional poisson delay.
// And since Actions are executed in order `UpdateTimer` will HAVE TO be executed before `StartTimer`
self.action_sender
if let Err(err) = self
.action_sender
.unbounded_send(Action::new_update_pending_ack(frag_id, new_delay))
.unwrap();
{
if !self.task_client.is_shutdown_poll() {
error!("Failed to send update pending ack action to the controller: {err}");
}
}
// send to `OutQueueControl` to eventually send to the mix network
self.message_handler
@@ -157,14 +176,10 @@ where
.await
}
pub(super) async fn run_with_shutdown(
&mut self,
mut shutdown: nym_task::TaskClient,
packet_type: PacketType,
) {
pub(super) async fn run(&mut self, packet_type: PacketType) {
debug!("Started RetransmissionRequestListener with graceful shutdown support");
while !shutdown.is_shutdown() {
while !self.task_client.is_shutdown() {
tokio::select! {
timed_out_ack = self.request_receiver.next() => match timed_out_ack {
Some(timed_out_ack) => self.on_retransmission_request(timed_out_ack, packet_type).await,
@@ -173,12 +188,12 @@ where
break;
}
},
_ = shutdown.recv() => {
_ = self.task_client.recv() => {
log::trace!("RetransmissionRequestListener: Received shutdown");
}
}
}
shutdown.recv_timeout().await;
self.task_client.recv_timeout().await;
log::debug!("RetransmissionRequestListener: Exiting");
}
}
@@ -6,6 +6,7 @@ use super::SentPacketNotificationReceiver;
use futures::StreamExt;
use log::*;
use nym_sphinx::chunking::fragment::{FragmentIdentifier, COVER_FRAG_ID};
use nym_task::TaskClient;
/// Module responsible for starting up retransmission timers.
/// It is required because when we send our packet to the `real traffic stream` controlled
@@ -14,16 +15,19 @@ use nym_sphinx::chunking::fragment::{FragmentIdentifier, COVER_FRAG_ID};
pub(super) struct SentNotificationListener {
sent_notifier: SentPacketNotificationReceiver,
action_sender: AckActionSender,
task_client: TaskClient,
}
impl SentNotificationListener {
pub(super) fn new(
sent_notifier: SentPacketNotificationReceiver,
action_sender: AckActionSender,
task_client: TaskClient,
) -> Self {
SentNotificationListener {
sent_notifier,
action_sender,
task_client,
}
}
@@ -32,15 +36,20 @@ impl SentNotificationListener {
trace!("sent off a cover message - no need to start retransmission timer!");
return;
}
self.action_sender
if let Err(err) = self
.action_sender
.unbounded_send(Action::new_start_timer(frag_id))
.unwrap();
{
if !self.task_client.is_shutdown_poll() {
error!("Failed to send start timer action to action controller: {err}");
}
}
}
pub(super) async fn run_with_shutdown(&mut self, mut shutdown: nym_task::TaskClient) {
pub(super) async fn run(&mut self) {
debug!("Started SentNotificationListener with graceful shutdown support");
loop {
while !self.task_client.is_shutdown() {
tokio::select! {
frag_id = self.sent_notifier.next() => match frag_id {
Some(frag_id) => {
@@ -51,13 +60,13 @@ impl SentNotificationListener {
break;
}
},
_ = shutdown.recv_with_delay() => {
_ = self.task_client.recv() => {
log::trace!("SentNotificationListener: Received shutdown");
break;
}
}
}
assert!(shutdown.is_shutdown_poll());
assert!(self.task_client.is_shutdown_poll());
log::debug!("SentNotificationListener: Exiting");
}
}
@@ -19,6 +19,7 @@ use nym_sphinx::params::{PacketSize, PacketType};
use nym_sphinx::preparer::{MessagePreparer, PreparedFragment};
use nym_sphinx::Delay;
use nym_task::connections::TransmissionLane;
use nym_task::TaskClient;
use nym_topology::{NymRouteProvider, NymTopologyError};
use rand::{CryptoRng, Rng};
use std::collections::HashMap;
@@ -149,12 +150,14 @@ pub(crate) struct MessageHandler<R> {
topology_access: TopologyAccessor,
reply_key_storage: SentReplyKeys,
tag_storage: UsedSenderTags,
task_client: TaskClient,
}
impl<R> MessageHandler<R>
where
R: CryptoRng + Rng,
{
#[allow(clippy::too_many_arguments)]
pub(crate) fn new(
config: Config,
rng: R,
@@ -163,6 +166,7 @@ where
topology_access: TopologyAccessor,
reply_key_storage: SentReplyKeys,
tag_storage: UsedSenderTags,
task_client: TaskClient,
) -> Self
where
R: Copy,
@@ -183,6 +187,7 @@ where
topology_access,
reply_key_storage,
tag_storage,
task_client,
}
}
@@ -609,15 +614,25 @@ where
}
pub(crate) fn update_ack_delay(&self, id: FragmentIdentifier, new_delay: Delay) {
self.action_sender
if let Err(err) = self
.action_sender
.unbounded_send(Action::UpdatePendingAck(id, new_delay))
.expect("action control task has died")
{
if !self.task_client.is_shutdown_poll() {
error!("Failed to send update action to the controller: {err}");
}
}
}
pub(crate) fn insert_pending_acks(&self, pending_acks: Vec<PendingAcknowledgement>) {
self.action_sender
if let Err(err) = self
.action_sender
.unbounded_send(Action::new_insert(pending_acks))
.expect("action control task has died")
{
if !self.task_client.is_shutdown_poll() {
error!("Failed to send insert action to the controller: {err}");
}
}
}
// tells real message sender (with the poisson timer) to send this to the mix network
@@ -631,9 +646,9 @@ where
.send((messages, transmission_lane))
.await
{
error!(
"Failed to forward messages to the real message sender (OutQueueControl): {err}"
);
if !self.task_client.is_shutdown_poll() {
error!("Failed to forward messages to the real message sender: {err}");
}
}
}
}
@@ -31,6 +31,7 @@ use nym_sphinx::addressing::clients::Recipient;
use nym_sphinx::params::PacketType;
use nym_statistics_common::clients::ClientStatsSender;
use nym_task::connections::{ConnectionCommandReceiver, LaneQueueLengths};
use nym_task::TaskClient;
use rand::{rngs::OsRng, CryptoRng, Rng};
use std::sync::Arc;
@@ -147,6 +148,7 @@ impl RealMessagesController<OsRng> {
lane_queue_lengths: LaneQueueLengths,
client_connection_rx: ConnectionCommandReceiver,
stats_tx: ClientStatsSender,
task_client: TaskClient,
) -> Self {
let rng = OsRng;
@@ -177,6 +179,7 @@ impl RealMessagesController<OsRng> {
topology_access.clone(),
reply_storage.key_storage(),
reply_storage.tags_storage(),
task_client.fork("message_handler"),
);
let ack_control = AcknowledgementController::new(
@@ -186,6 +189,7 @@ impl RealMessagesController<OsRng> {
message_handler.clone(),
reply_controller_sender,
stats_tx.clone(),
task_client.fork("ack_control"),
);
let reply_control = ReplyController::new(
@@ -193,6 +197,7 @@ impl RealMessagesController<OsRng> {
message_handler,
reply_storage,
reply_controller_receiver,
task_client.fork("reply_controller"),
);
let out_queue_control = OutQueueControl::new(
@@ -205,6 +210,7 @@ impl RealMessagesController<OsRng> {
lane_queue_lengths,
client_connection_rx,
stats_tx,
task_client.with_suffix("out_queue_control"),
);
RealMessagesController {
@@ -214,22 +220,20 @@ impl RealMessagesController<OsRng> {
}
}
pub fn start_with_shutdown(self, shutdown: nym_task::TaskClient, packet_type: PacketType) {
pub fn start(self, packet_type: PacketType) {
let mut out_queue_control = self.out_queue_control;
let ack_control = self.ack_control;
let mut reply_control = self.reply_control;
let shutdown_handle = shutdown.fork("out_queue_control");
spawn_future(async move {
out_queue_control.run_with_shutdown(shutdown_handle).await;
out_queue_control.run().await;
debug!("The out queue controller has finished execution!");
});
let shutdown_handle = shutdown.fork("reply_control");
spawn_future(async move {
reply_control.run_with_shutdown(shutdown_handle).await;
reply_control.run().await;
debug!("The reply controller has finished execution!");
});
ack_control.start_with_shutdown(shutdown.with_suffix("ack_control"), packet_type);
ack_control.start(packet_type);
}
}
@@ -22,6 +22,7 @@ use nym_statistics_common::clients::{packet_statistics::PacketStatisticsEvent, C
use nym_task::connections::{
ConnectionCommand, ConnectionCommandReceiver, ConnectionId, LaneQueueLengths, TransmissionLane,
};
use nym_task::TaskClient;
use rand::{CryptoRng, Rng};
use std::pin::Pin;
use std::sync::Arc;
@@ -117,6 +118,8 @@ where
/// Channel used for sending metrics events (specifically `PacketStatistics` events) to the metrics tracker.
stats_tx: ClientStatsSender,
task_client: TaskClient,
}
#[derive(Debug)]
@@ -176,6 +179,7 @@ where
lane_queue_lengths: LaneQueueLengths,
client_connection_rx: ConnectionCommandReceiver,
stats_tx: ClientStatsSender,
task_client: TaskClient,
) -> Self {
OutQueueControl {
config,
@@ -190,6 +194,7 @@ where
client_connection_rx,
lane_queue_lengths,
stats_tx,
task_client,
}
}
@@ -198,7 +203,9 @@ where
// queues and client load rather than the required delay. So realistically we can treat
// whatever is about to happen as negligible additional delay.
trace!("{} is about to get sent to the mixnet", frag_id);
self.sent_notifier.unbounded_send(frag_id).unwrap();
if let Err(err) = self.sent_notifier.unbounded_send(frag_id) {
error!("Failed to notify about sent message: {err}");
}
}
fn loop_cover_message_size(&mut self) -> PacketSize {
@@ -271,7 +278,9 @@ where
};
if let Err(err) = self.mix_tx.send(vec![next_message]).await {
log::error!("Failed to send: {err}");
if !self.task_client.is_shutdown_poll() {
log::error!("Failed to send: {err}");
}
} else {
let event = if fragment_id.is_some() {
PacketStatisticsEvent::RealPacketSent(packet_size)
@@ -504,7 +513,7 @@ where
}
#[cfg(not(target_arch = "wasm32"))]
fn log_status(&self, shutdown: &mut nym_task::TaskClient) {
fn log_status(&self, shutdown: &mut TaskClient) {
use crate::error::ClientCoreStatusMessage;
let packets = self.transmission_buffer.total_size();
@@ -535,14 +544,16 @@ where
}
}
pub(super) async fn run_with_shutdown(&mut self, mut shutdown: nym_task::TaskClient) {
pub(super) async fn run(&mut self) {
debug!("Started OutQueueControl with graceful shutdown support");
let mut shutdown = self.task_client.fork("select");
#[cfg(not(target_arch = "wasm32"))]
{
let mut status_timer = tokio::time::interval(Duration::from_secs(5));
loop {
while !shutdown.is_shutdown() {
tokio::select! {
biased;
_ = shutdown.recv() => {
@@ -20,6 +20,7 @@ use nym_sphinx::message::{NymMessage, PlainMessage};
use nym_sphinx::params::ReplySurbKeyDigestAlgorithm;
use nym_sphinx::receiver::{MessageReceiver, MessageRecoveryError, ReconstructedMessage};
use nym_statistics_common::clients::{packet_statistics::PacketStatisticsEvent, ClientStatsSender};
use nym_task::TaskClient;
use std::collections::HashSet;
use std::sync::Arc;
@@ -152,6 +153,7 @@ struct ReceivedMessagesBuffer<R: MessageReceiver> {
inner: Arc<Mutex<ReceivedMessagesBufferInner<R>>>,
reply_key_storage: SentReplyKeys,
reply_controller_sender: ReplyControllerSender,
task_client: TaskClient,
}
impl<R: MessageReceiver> ReceivedMessagesBuffer<R> {
@@ -160,6 +162,7 @@ impl<R: MessageReceiver> ReceivedMessagesBuffer<R> {
reply_key_storage: SentReplyKeys,
reply_controller_sender: ReplyControllerSender,
stats_tx: ClientStatsSender,
task_client: TaskClient,
) -> Self {
ReceivedMessagesBuffer {
inner: Arc::new(Mutex::new(ReceivedMessagesBufferInner {
@@ -172,6 +175,7 @@ impl<R: MessageReceiver> ReceivedMessagesBuffer<R> {
})),
reply_key_storage,
reply_controller_sender,
task_client,
}
}
@@ -257,11 +261,15 @@ impl<R: MessageReceiver> ReceivedMessagesBuffer<R> {
}
};
self.reply_controller_sender.send_additional_surbs(
if let Err(err) = self.reply_controller_sender.send_additional_surbs(
msg.sender_tag,
reply_surbs,
from_surb_request,
)
) {
if !self.task_client.is_shutdown_poll() {
error!("{err}");
}
}
}
reconstructed
}
@@ -276,8 +284,14 @@ impl<R: MessageReceiver> ReceivedMessagesBuffer<R> {
ReplyMessageContent::Data { message } => reconstructed.push(message.into()),
ReplyMessageContent::SurbRequest { recipient, amount } => {
debug!("received request for {amount} additional reply SURBs from {recipient}");
self.reply_controller_sender
.send_additional_surbs_request(*recipient, amount);
if let Err(err) = self
.reply_controller_sender
.send_additional_surbs_request(*recipient, amount)
{
if !self.task_client.is_shutdown_poll() {
error!("{err}");
}
}
}
}
}
@@ -399,16 +413,19 @@ pub enum ReceivedBufferMessage {
struct RequestReceiver<R: MessageReceiver> {
received_buffer: ReceivedMessagesBuffer<R>,
query_receiver: ReceivedBufferRequestReceiver,
task_client: TaskClient,
}
impl<R: MessageReceiver> RequestReceiver<R> {
fn new(
received_buffer: ReceivedMessagesBuffer<R>,
query_receiver: ReceivedBufferRequestReceiver,
task_client: TaskClient,
) -> Self {
RequestReceiver {
received_buffer,
query_receiver,
task_client,
}
}
@@ -423,12 +440,12 @@ impl<R: MessageReceiver> RequestReceiver<R> {
}
}
async fn run_with_shutdown(&mut self, mut shutdown: nym_task::TaskClient) {
async fn run(&mut self) {
debug!("Started RequestReceiver with graceful shutdown support");
while !shutdown.is_shutdown() {
while !self.task_client.is_shutdown() {
tokio::select! {
biased;
_ = shutdown.recv_with_delay() => {
_ = self.task_client.recv() => {
log::trace!("RequestReceiver: Received shutdown");
}
request = self.query_receiver.next() => {
@@ -441,7 +458,7 @@ impl<R: MessageReceiver> RequestReceiver<R> {
},
}
}
shutdown.recv_timeout().await;
self.task_client.recv().await;
log::debug!("RequestReceiver: Exiting");
}
}
@@ -449,25 +466,25 @@ impl<R: MessageReceiver> RequestReceiver<R> {
struct FragmentedMessageReceiver<R: MessageReceiver> {
received_buffer: ReceivedMessagesBuffer<R>,
mixnet_packet_receiver: MixnetMessageReceiver,
task_client: TaskClient,
}
impl<R: MessageReceiver> FragmentedMessageReceiver<R> {
fn new(
received_buffer: ReceivedMessagesBuffer<R>,
mixnet_packet_receiver: MixnetMessageReceiver,
task_client: TaskClient,
) -> Self {
FragmentedMessageReceiver {
received_buffer,
mixnet_packet_receiver,
task_client,
}
}
async fn run_with_shutdown(
&mut self,
mut shutdown: nym_task::TaskClient,
) -> Result<(), MessageRecoveryError> {
async fn run(&mut self) -> Result<(), MessageRecoveryError> {
debug!("Started FragmentedMessageReceiver with graceful shutdown support");
while !shutdown.is_shutdown() {
while !self.task_client.is_shutdown() {
tokio::select! {
new_messages = self.mixnet_packet_receiver.next() => {
if let Some(new_messages) = new_messages {
@@ -477,12 +494,12 @@ impl<R: MessageReceiver> FragmentedMessageReceiver<R> {
break;
}
},
_ = shutdown.recv_with_delay() => {
_ = self.task_client.recv_with_delay() => {
log::trace!("FragmentedMessageReceiver: Received shutdown");
}
}
}
shutdown.recv_timeout().await;
self.task_client.recv_timeout().await;
log::debug!("FragmentedMessageReceiver: Exiting");
Ok(())
}
@@ -501,41 +518,42 @@ impl<R: MessageReceiver + Clone + Send + 'static> ReceivedMessagesBufferControll
reply_key_storage: SentReplyKeys,
reply_controller_sender: ReplyControllerSender,
metrics_reporter: ClientStatsSender,
task_client: TaskClient,
) -> Self {
let received_buffer = ReceivedMessagesBuffer::new(
local_encryption_keypair,
reply_key_storage,
reply_controller_sender,
metrics_reporter,
task_client.fork("received_messages_buffer"),
);
ReceivedMessagesBufferController {
fragmented_message_receiver: FragmentedMessageReceiver::new(
received_buffer.clone(),
mixnet_packet_receiver,
task_client.fork("fragmented_message_receiver"),
),
request_receiver: RequestReceiver::new(
received_buffer,
query_receiver,
task_client.with_suffix("request_receiver"),
),
request_receiver: RequestReceiver::new(received_buffer, query_receiver),
}
}
pub fn start_with_shutdown(self, shutdown: nym_task::TaskClient) {
pub fn start(self) {
let mut fragmented_message_receiver = self.fragmented_message_receiver;
let mut request_receiver = self.request_receiver;
let shutdown_handle = shutdown.fork("fragmented_message_receiver");
spawn_future(async move {
match fragmented_message_receiver
.run_with_shutdown(shutdown_handle)
.await
{
match fragmented_message_receiver.run().await {
Ok(_) => {}
Err(e) => error!("{e}"),
}
});
spawn_future(async move {
request_receiver
.run_with_shutdown(shutdown.with_suffix("request_receiver"))
.await;
request_receiver.run().await;
});
}
}
@@ -12,6 +12,7 @@ use nym_sphinx::anonymous_replies::requests::AnonymousSenderTag;
use nym_sphinx::anonymous_replies::ReplySurb;
use nym_sphinx::chunking::fragment::{Fragment, FragmentIdentifier};
use nym_task::connections::{ConnectionId, TransmissionLane};
use nym_task::TaskClient;
use rand::{CryptoRng, Rng};
use std::cmp::{max, min};
use std::collections::btree_map::Entry;
@@ -68,6 +69,9 @@ pub struct ReplyController<R> {
message_handler: MessageHandler<R>,
full_reply_storage: CombinedReplyStorage,
// Listen for shutdown signals
task_client: TaskClient,
}
impl<R> ReplyController<R>
@@ -79,6 +83,7 @@ where
message_handler: MessageHandler<R>,
full_reply_storage: CombinedReplyStorage,
request_receiver: ReplyControllerReceiver,
task_client: TaskClient,
) -> Self {
ReplyController {
config,
@@ -87,6 +92,7 @@ where
pending_retransmissions: HashMap::new(),
message_handler,
full_reply_storage,
task_client,
}
}
@@ -846,9 +852,11 @@ where
// todo!()
// }
pub(crate) async fn run_with_shutdown(&mut self, mut shutdown: nym_task::TaskClient) {
pub(crate) async fn run(&mut self) {
debug!("Started ReplyController with graceful shutdown support");
let mut shutdown = self.task_client.fork("select");
let polling_rate = Duration::from_secs(5);
let mut stale_inspection = new_interval_stream(polling_rate);
@@ -860,7 +868,7 @@ where
while !shutdown.is_shutdown() {
tokio::select! {
biased;
_ = shutdown.recv_with_delay() => {
_ = shutdown.recv() => {
log::trace!("ReplyController: Received shutdown");
},
req = self.request_receiver.next() => match req {
@@ -15,6 +15,27 @@ pub(crate) fn new_control_channels() -> (ReplyControllerSender, ReplyControllerR
(tx.into(), rx)
}
#[derive(Debug, thiserror::Error)]
pub enum ReplyControllerSenderError {
#[error("failed to send retransmission data to reply controller")]
SendRetransmissionData(#[source] mpsc::TrySendError<ReplyControllerMessage>),
#[error("failed to send reply to reply controller")]
SendReply(#[source] mpsc::TrySendError<ReplyControllerMessage>),
#[error("failed to send additional surbs to reply controller")]
AdditionalSurbs(#[source] mpsc::TrySendError<ReplyControllerMessage>),
#[error("failed to send additional surbs request to reply controller")]
AdditionalSurbsRequest(#[source] mpsc::TrySendError<ReplyControllerMessage>),
#[error("failed to request lane queue length from reply controller")]
LaneQueueLength(#[source] mpsc::TrySendError<ReplyControllerMessage>),
#[error("response channel was dropped before we could receive the response")]
ResponseChannelDropped(#[source] oneshot::Canceled),
}
#[derive(Debug, Clone)]
pub struct ReplyControllerSender(mpsc::UnboundedSender<ReplyControllerMessage>);
@@ -30,14 +51,14 @@ impl ReplyControllerSender {
recipient: AnonymousSenderTag,
timed_out_ack: Weak<PendingAcknowledgement>,
extra_surb_request: bool,
) {
) -> Result<(), ReplyControllerSenderError> {
self.0
.unbounded_send(ReplyControllerMessage::RetransmitReply {
recipient,
timed_out_ack,
extra_surb_request,
})
.expect("ReplyControllerReceiver has died!")
.map_err(ReplyControllerSenderError::SendRetransmissionData)
}
pub(crate) fn send_reply(
@@ -45,14 +66,14 @@ impl ReplyControllerSender {
recipient: AnonymousSenderTag,
message: Vec<u8>,
lane: TransmissionLane,
) {
) -> Result<(), ReplyControllerSenderError> {
self.0
.unbounded_send(ReplyControllerMessage::SendReply {
recipient,
message,
lane,
})
.expect("ReplyControllerReceiver has died!")
.map_err(ReplyControllerSenderError::SendReply)
}
pub(crate) fn send_additional_surbs(
@@ -60,42 +81,47 @@ impl ReplyControllerSender {
sender_tag: AnonymousSenderTag,
reply_surbs: Vec<ReplySurb>,
from_surb_request: bool,
) {
) -> Result<(), ReplyControllerSenderError> {
self.0
.unbounded_send(ReplyControllerMessage::AdditionalSurbs {
sender_tag,
reply_surbs,
from_surb_request,
})
.expect("ReplyControllerReceiver has died!")
.map_err(ReplyControllerSenderError::AdditionalSurbs)
}
pub(crate) fn send_additional_surbs_request(&self, recipient: Recipient, amount: u32) {
pub(crate) fn send_additional_surbs_request(
&self,
recipient: Recipient,
amount: u32,
) -> Result<(), ReplyControllerSenderError> {
self.0
.unbounded_send(ReplyControllerMessage::AdditionalSurbsRequest {
recipient: Box::new(recipient),
amount,
})
.expect("ReplyControllerReceiver has died!")
.map_err(ReplyControllerSenderError::AdditionalSurbsRequest)
}
pub async fn get_lane_queue_length(&self, connection_id: ConnectionId) -> usize {
pub async fn get_lane_queue_length(
&self,
connection_id: ConnectionId,
) -> Result<usize, ReplyControllerSenderError> {
let (response_tx, response_rx) = oneshot::channel();
self.0
if let Err(err) = self
.0
.unbounded_send(ReplyControllerMessage::LaneQueueLength {
connection_id,
response_channel: response_tx,
})
.expect("ReplyControllerReceiver has died!");
match response_rx.await {
Ok(length) => length,
Err(_) => {
error!("The reply controller has dropped our response channel!");
// TODO: should we panic here instead? this message implies something weird and unrecoverable has happened
0
}
{
return Err(ReplyControllerSenderError::LaneQueueLength(err));
}
response_rx
.await
.map_err(ReplyControllerSenderError::ResponseChannelDropped)
}
}
@@ -110,7 +136,10 @@ impl ReplyQueueLengths {
}
}
pub async fn get_lane_queue_length(&self, connection_id: ConnectionId) -> usize {
pub async fn get_lane_queue_length(
&self,
connection_id: ConnectionId,
) -> Result<usize, ReplyControllerSenderError> {
self.reply_controller_sender
.get_lane_queue_length(connection_id)
.await
@@ -120,7 +149,7 @@ impl ReplyQueueLengths {
pub(crate) type ReplyControllerReceiver = mpsc::UnboundedReceiver<ReplyControllerMessage>;
#[derive(Debug)]
pub(crate) enum ReplyControllerMessage {
pub enum ReplyControllerMessage {
RetransmitReply {
recipient: AnonymousSenderTag,
timed_out_ack: Weak<PendingAcknowledgement>,
@@ -22,7 +22,7 @@ use nym_sphinx::addressing::Recipient;
use nym_statistics_common::clients::{
ClientStatsController, ClientStatsReceiver, ClientStatsSender,
};
use nym_task::connections::TransmissionLane;
use nym_task::{connections::TransmissionLane, TaskClient};
use std::time::Duration;
use crate::{
@@ -51,6 +51,9 @@ pub(crate) struct StatisticsControl {
/// Config for stats reporting (enabled, address, interval)
reporting_config: StatsReporting,
/// Task client for listening for shutdown
task_client: TaskClient,
}
impl StatisticsControl {
@@ -59,19 +62,24 @@ impl StatisticsControl {
client_type: String,
client_stats_id: String,
report_tx: InputMessageSender,
task_client: TaskClient,
) -> (Self, ClientStatsSender) {
let (stats_tx, stats_rx) = tokio::sync::mpsc::unbounded_channel();
let stats = ClientStatsController::new(client_stats_id, client_type);
let mut task_client_stats_sender = task_client.fork("stats_sender");
task_client_stats_sender.disarm();
(
StatisticsControl {
stats,
stats_rx,
report_tx,
reporting_config,
task_client,
},
ClientStatsSender::new(Some(stats_tx)),
ClientStatsSender::new(Some(stats_tx), task_client_stats_sender),
)
}
@@ -91,7 +99,7 @@ impl StatisticsControl {
}
}
async fn run_with_shutdown(&mut self, mut task_client: nym_task::TaskClient) {
async fn run(&mut self) {
log::debug!("Started StatisticsControl with graceful shutdown support");
#[cfg(not(target_arch = "wasm32"))]
@@ -121,10 +129,10 @@ impl StatisticsControl {
let mut snapshot_interval =
gloo_timers::future::IntervalStream::new(SNAPSHOT_INTERVAL.as_millis() as u32);
loop {
while !self.task_client.is_shutdown() {
tokio::select! {
biased;
_ = task_client.recv() => {
_ = self.task_client.recv() => {
log::trace!("StatisticsControl: Received shutdown");
break;
},
@@ -149,29 +157,34 @@ impl StatisticsControl {
}
_ = local_report_interval.next() => {
self.stats.local_report(&mut task_client);
self.stats.local_report(&mut self.task_client);
}
}
}
log::debug!("StatisticsControl: Exiting");
}
pub(crate) fn start_with_shutdown(mut self, task_client: nym_task::TaskClient) {
pub(crate) fn start(mut self) {
spawn_future(async move {
self.run_with_shutdown(task_client).await;
self.run().await;
})
}
pub(crate) fn create_and_start_with_shutdown(
pub(crate) fn create_and_start(
reporting_config: StatsReporting,
client_type: String,
client_stats_id: String,
report_tx: InputMessageSender,
task_client: nym_task::TaskClient,
task_client: TaskClient,
) -> ClientStatsSender {
let (controller, sender) =
Self::create(reporting_config, client_type, client_stats_id, report_tx);
controller.start_with_shutdown(task_client);
let (controller, sender) = Self::create(
reporting_config,
client_type,
client_stats_id,
report_tx,
task_client,
);
controller.start();
sender
}
}
@@ -6,6 +6,7 @@ pub(crate) use accessor::{TopologyAccessor, TopologyReadPermit};
use futures::StreamExt;
use log::*;
use nym_sphinx::addressing::nodes::NodeIdentity;
use nym_task::TaskClient;
use nym_topology::NymTopologyError;
use std::time::Duration;
@@ -43,6 +44,8 @@ pub struct TopologyRefresher {
refresh_rate: Duration,
consecutive_failure_count: usize,
task_client: TaskClient,
}
impl TopologyRefresher {
@@ -50,12 +53,14 @@ impl TopologyRefresher {
cfg: TopologyRefresherConfig,
topology_accessor: TopologyAccessor,
topology_provider: Box<dyn TopologyProvider + Send + Sync>,
task_client: TaskClient,
) -> Self {
TopologyRefresher {
topology_provider,
topology_accessor,
refresh_rate: cfg.refresh_rate,
consecutive_failure_count: 0,
task_client,
}
}
@@ -142,7 +147,7 @@ impl TopologyRefresher {
}
}
pub fn start_with_shutdown(mut self, mut shutdown: nym_task::TaskClient) {
pub fn start(mut self) {
spawn_future(async move {
debug!("Started TopologyRefresher with graceful shutdown support");
@@ -155,17 +160,17 @@ impl TopologyRefresher {
let mut interval =
gloo_timers::future::IntervalStream::new(self.refresh_rate.as_millis() as u32);
while !shutdown.is_shutdown() {
while !self.task_client.is_shutdown() {
tokio::select! {
_ = interval.next() => {
self.try_refresh().await;
},
_ = shutdown.recv() => {
_ = self.task_client.recv() => {
log::trace!("TopologyRefresher: Received shutdown");
},
}
}
shutdown.recv_timeout().await;
self.task_client.recv_timeout().await;
log::debug!("TopologyRefresher: Exiting");
})
}
+3
View File
@@ -222,6 +222,9 @@ pub enum ClientCoreError {
"fresh registration with gateway {gateway_id} somehow requires an additional key upgrade!"
)]
UnexpectedKeyUpgrade { gateway_id: String },
#[error("failed to derive keys from master key")]
HkdfDerivationError {},
}
/// Set of messages that the client can send to listeners via the task manager
@@ -204,15 +204,15 @@ impl<C, St> GatewayClient<C, St> {
"Attemting to establish connection to gateway at: {}",
self.gateway_address
);
let (ws_stream, _) = connect_async(&self.gateway_address).await?;
let (ws_stream, _) = connect_async(
&self.gateway_address,
#[cfg(unix)]
self.connection_fd_callback.clone(),
)
.await?;
self.connection = SocketState::Available(Box::new(ws_stream));
#[cfg(unix)]
if let (Some(callback), Some(fd)) = (self.connection_fd_callback.as_ref(), self.ws_fd()) {
callback.as_ref()(fd);
}
Ok(())
}
@@ -1053,7 +1053,7 @@ impl GatewayClient<InitOnly, EphemeralCredentialStorage> {
connection: SocketState::NotConnected,
packet_router,
bandwidth_controller: None,
stats_reporter: ClientStatsSender::new(None),
stats_reporter: ClientStatsSender::new(None, task_client.clone()),
negotiated_protocol: None,
#[cfg(unix)]
connection_fd_callback: None,
@@ -1,6 +1,11 @@
use crate::error::GatewayClientError;
use nym_http_api_client::HickoryDnsResolver;
#[cfg(unix)]
use std::{
os::fd::{AsRawFd, RawFd},
sync::Arc,
};
use tokio::net::TcpStream;
use tokio_tungstenite::{MaybeTlsStream, WebSocketStream};
use tungstenite::handshake::client::Response;
@@ -11,7 +16,10 @@ use std::net::SocketAddr;
#[cfg(not(target_arch = "wasm32"))]
pub(crate) async fn connect_async(
endpoint: &str,
#[cfg(unix)] connection_fd_callback: Option<Arc<dyn Fn(RawFd) + Send + Sync>>,
) -> Result<(WebSocketStream<MaybeTlsStream<TcpStream>>, Response), GatewayClientError> {
use tokio::net::TcpSocket;
let resolver = HickoryDnsResolver::default();
let uri =
Url::parse(endpoint).map_err(|_| GatewayClientError::InvalidUrl(endpoint.to_owned()))?;
@@ -37,14 +45,41 @@ pub(crate) async fn connect_async(
}
};
let stream = TcpStream::connect(&sock_addrs[..]).await.map_err(|error| {
GatewayClientError::NetworkConnectionFailed {
address: endpoint.to_owned(),
source: error.into(),
let mut stream = Err(GatewayClientError::NoEndpointForConnection {
address: endpoint.to_owned(),
});
for sock_addr in sock_addrs {
let socket = if sock_addr.is_ipv4() {
TcpSocket::new_v4()
} else {
TcpSocket::new_v6()
}
})?;
.map_err(|err| GatewayClientError::NetworkConnectionFailed {
address: endpoint.to_owned(),
source: err.into(),
})?;
tokio_tungstenite::client_async_tls(endpoint, stream)
#[cfg(unix)]
if let Some(callback) = connection_fd_callback.as_ref() {
callback.as_ref()(socket.as_raw_fd());
}
match socket.connect(sock_addr).await {
Ok(s) => {
stream = Ok(s);
break;
}
Err(err) => {
stream = Err(GatewayClientError::NetworkConnectionFailed {
address: endpoint.to_owned(),
source: err.into(),
});
continue;
}
}
}
tokio_tungstenite::client_async_tls(endpoint, stream?)
.await
.map_err(|error| GatewayClientError::NetworkConnectionFailed {
address: endpoint.to_owned(),
@@ -43,6 +43,9 @@ pub enum GatewayClientError {
#[error("connection failed: {address}: {source}")]
NetworkConnectionFailed { address: String, source: WsError },
#[error("no socket address for endpoint: {address}")]
NoEndpointForConnection { address: String },
#[error("Invalid URL: {0}")]
InvalidUrl(String),
+2 -1
View File
@@ -24,6 +24,7 @@ ed25519-dalek = { workspace = true, features = ["rand_core"], optional = true }
rand = { workspace = true, optional = true }
serde_bytes = { workspace = true, optional = true }
serde = { workspace = true, features = ["derive"], optional = true }
sha2 = { workspace = true, optional = true }
subtle-encoding = { workspace = true, features = ["bech32-preview"] }
thiserror = { workspace = true }
zeroize = { workspace = true, optional = true, features = ["zeroize_derive"] }
@@ -40,7 +41,7 @@ default = ["sphinx"]
aead = ["dep:aead", "aead/std", "aes-gcm-siv", "generic-array"]
serde = ["dep:serde", "serde_bytes", "ed25519-dalek/serde", "x25519-dalek/serde"]
asymmetric = ["x25519-dalek", "ed25519-dalek", "zeroize"]
hashing = ["blake3", "digest", "hkdf", "hmac", "generic-array"]
hashing = ["blake3", "digest", "hkdf", "hmac", "generic-array", "sha2"]
stream_cipher = ["aes", "ctr", "cipher", "generic-array"]
sphinx = ["nym-sphinx-types/sphinx"]
outfox = ["nym-sphinx-types/outfox"]
+37 -3
View File
@@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
pub use ed25519_dalek::SignatureError;
use ed25519_dalek::{Signer, SigningKey};
use ed25519_dalek::{SecretKey, Signer, SigningKey};
pub use ed25519_dalek::{Verifier, PUBLIC_KEY_LENGTH, SECRET_KEY_LENGTH, SIGNATURE_LENGTH};
use nym_pemstore::traits::{PemStorableKey, PemStorableKeyPair};
use std::fmt::{self, Debug, Display, Formatter};
@@ -18,7 +18,7 @@ pub mod serde_helpers;
use nym_sphinx_types::{DestinationAddressBytes, DESTINATION_ADDRESS_LENGTH};
#[cfg(feature = "rand")]
use rand::{CryptoRng, RngCore};
use rand::{CryptoRng, Rng, RngCore};
#[cfg(feature = "serde")]
use serde::de::Error as SerdeError;
#[cfg(feature = "serde")]
@@ -62,16 +62,33 @@ pub struct KeyPair {
// nothing secret about public key
#[zeroize(skip)]
public_key: PublicKey,
#[zeroize(skip)]
index: u32,
}
/// All keys will always have an index field populated this is to prevent anyone from figuring out if
/// the keys are derived or random, and alter their behaviour based on that.
impl KeyPair {
#[cfg(feature = "rand")]
pub fn new<R: RngCore + CryptoRng>(rng: &mut R) -> Self {
let index = rng.gen();
let ed25519_signing_key = ed25519_dalek::SigningKey::generate(rng);
KeyPair {
private_key: PrivateKey(ed25519_signing_key.to_bytes()),
public_key: PublicKey(ed25519_signing_key.verifying_key()),
index,
}
}
pub fn from_secret(secret: SecretKey, index: u32) -> Self {
let ed25519_signing_key = SigningKey::from(secret);
KeyPair {
private_key: PrivateKey(ed25519_signing_key.to_bytes()),
public_key: PublicKey(ed25519_signing_key.verifying_key()),
index,
}
}
@@ -87,15 +104,31 @@ impl KeyPair {
Ok(KeyPair {
private_key: PrivateKey::from_bytes(priv_bytes)?,
public_key: PublicKey::from_bytes(pub_bytes)?,
index: fake_index(pub_bytes),
})
}
}
/// Reduces a byte slice into a u32 value by XOR-ing all its bytes into a 4-byte accumulator.
/// The process iterates over every byte in the input slice, XOR-ing each one into a slot based on its index modulo 4.
/// If the input slice contains fewer than 4 bytes, the remaining positions in the accumulator remain zero.
/// Finally, the accumulator is interpreted in big-endian order to produce the resulting u32.
/// Index is used to verify deterministic identity key, master key and salt are also requried for verification.
fn fake_index(input: &[u8]) -> u32 {
let mut accumulator = [0u8; 4];
for (i, &byte) in input.iter().enumerate() {
accumulator[i % 4] ^= byte;
}
u32::from_be_bytes(accumulator)
}
impl From<PrivateKey> for KeyPair {
fn from(private_key: PrivateKey) -> Self {
let public_key = (&private_key).into();
KeyPair {
public_key: (&private_key).into(),
public_key,
private_key,
index: fake_index(public_key.to_bytes().as_ref()),
}
}
}
@@ -115,6 +148,7 @@ impl PemStorableKeyPair for KeyPair {
KeyPair {
private_key,
public_key,
index: fake_index(public_key.to_bytes().as_ref()),
}
}
}
+81
View File
@@ -8,6 +8,10 @@ use hkdf::{
},
Hkdf,
};
use sha2::{Sha256, Sha512};
pub use hkdf::InvalidLength;
use zeroize::ZeroizeOnDrop;
/// Perform HKDF `extract` then `expand` as a single step.
pub fn extract_then_expand<D>(
@@ -28,3 +32,80 @@ where
Ok(okm)
}
/// `DerivationMaterial` encapsulates parameters for deterministic key derivation using
/// HKDF (SHA-512).
///
/// It consists of:
/// - A master key (`master_key`): the base secret.
/// - An index (`index`): ensures unique derivations.
/// - A salt (`salt`): adds additional uniqueness, should be application specific.
///
/// Use the `derive_secret()` method to generate a 32-byte secret. To prepare for a new derivation,
/// call the `next()` method, which increments the index. **It is the caller's responsibility to
/// track and persist the derivation index if keys need to be rederived.**
///
/// # Example
///
/// ```rust
/// use nym_crypto::hkdf::DerivationMaterial;
///
/// let master_key = [0u8; 32]; // your secret master key
/// let salt = b"unique-salt-value";
/// let material = DerivationMaterial::new(master_key, 0, salt);
///
/// // Derive a secret
/// let secret = material.derive_secret().expect("Failed to derive secret");
///
/// // Prepare for the next derivation
/// let next_material = material.next();
/// ```
#[derive(ZeroizeOnDrop)]
pub struct DerivationMaterial {
master_key: [u8; 32],
index: u32,
salt: Vec<u8>,
}
impl DerivationMaterial {
pub fn index(&self) -> u32 {
self.index
}
pub fn salt(&self) -> &[u8] {
&self.salt
}
/// Derives a 32-byte seed from a master seed and an index using HKDF (with SHA-512).
///
/// The `salt` and the use of the index (as info) bind this derivation to an application/client.
pub fn derive_secret(&self) -> Result<[u8; 32], hkdf::InvalidLength> {
let salt = &self.salt;
let info = self.index.to_be_bytes(); // Use the index as info
let hk = Hkdf::<Sha512>::new(Some(salt), &self.master_key);
let mut okm = [0u8; 32];
hk.expand(&info, &mut okm)?;
Ok(okm)
}
pub fn new<T: AsRef<[u8]>>(master_key: T, index: u32, salt: &[u8]) -> Self {
// Coerce master_key to [u8; 32]
let mut hasher = Sha256::new();
hasher.update(master_key.as_ref());
let master_key = hasher.finalize().into();
Self {
master_key,
index,
salt: salt.to_vec(),
}
}
pub fn next(&self) -> Self {
Self {
master_key: self.master_key,
index: self.index + 1,
salt: self.salt.clone(),
}
}
}
+1 -1
View File
@@ -12,7 +12,7 @@ license.workspace = true
[dependencies]
async-trait = { workspace = true }
reqwest = { workspace = true, features = ["json"] }
reqwest = { workspace = true, features = ["json", "gzip"] }
http.workspace = true
url = { workspace = true }
once_cell = { workspace = true }
File diff suppressed because it is too large Load Diff
+5
View File
@@ -7,11 +7,16 @@ use http::HeaderValue;
use nym_bin_common::build_information::{BinaryBuildInformation, BinaryBuildInformationOwned};
use serde::{Deserialize, Serialize};
/// Characteristic elements sent to the API providing basic context information of the requesting client.
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct UserAgent {
/// The internal crate / application / subsystem making use of API client
pub application: String,
/// version of the calling crate / application / subsystem
pub version: String,
/// client platform
pub platform: String,
/// source commit version for the calling calling crate / subsystem
pub git_commit: String,
}
+1 -1
View File
@@ -26,4 +26,4 @@ utoipa = [ "dep:utoipa" ]
[build-dependencies]
regex = { workspace = true }
cargo_metadata = { version = "0.18" }
cargo_metadata = { workspace = true }
+1 -1
View File
@@ -31,7 +31,7 @@ nym-pemstore = { path = "../pemstore" }
nym-network-defaults = { path = "../network-defaults", default-features = false }
[dev-dependencies]
criterion = { version = "0.5.1", features = ["html_reports"] }
criterion = { workspace = true, features = ["html_reports"] }
[[bench]]
+12 -3
View File
@@ -25,19 +25,28 @@ pub type ClientStatsReceiver = tokio::sync::mpsc::UnboundedReceiver<ClientStatsE
#[derive(Clone)]
pub struct ClientStatsSender {
stats_tx: Option<UnboundedSender<ClientStatsEvents>>,
task_client: TaskClient,
}
impl ClientStatsSender {
/// Create a new statistics Sender
pub fn new(stats_tx: Option<UnboundedSender<ClientStatsEvents>>) -> Self {
ClientStatsSender { stats_tx }
pub fn new(
stats_tx: Option<UnboundedSender<ClientStatsEvents>>,
task_client: TaskClient,
) -> Self {
ClientStatsSender {
stats_tx,
task_client,
}
}
/// Report a statistics event using the sender.
pub fn report(&self, event: ClientStatsEvents) {
if let Some(tx) = &self.stats_tx {
if let Err(err) = tx.send(event) {
log::error!("Failed to send stats event: {:?}", err);
if !self.task_client.is_shutdown_poll() {
log::error!("Failed to send stats event: {err}");
}
}
}
}
+4
View File
@@ -299,6 +299,8 @@ impl Clone for TaskClient {
None
};
log::debug!("Cloned task client: {name:?}");
TaskClient {
name,
shutdown: AtomicBool::new(self.shutdown.load(Ordering::Relaxed)),
@@ -344,6 +346,7 @@ impl TaskClient {
format!("unknown-{suffix}")
};
log::debug!("Forked task client: {child_name}");
child.name = Some(child_name);
child
}
@@ -377,6 +380,7 @@ impl TaskClient {
} else {
format!("unknown-{suffix}")
};
log::debug!("Renamed task client: {name}");
self.named(name)
}
+1 -1
View File
@@ -43,7 +43,7 @@ impl PeerHandle {
let timeout_check_interval = tokio_stream::wrappers::IntervalStream::new(
tokio::time::interval(DEFAULT_PEER_TIMEOUT_CHECK),
);
let mut task_client = task_client.fork(format!("peer-{public_key}"));
let mut task_client = task_client.fork(format!("peer_{public_key}"));
task_client.disarm();
PeerHandle {
public_key,
+8 -8
View File
@@ -198,8 +198,8 @@ pub mod test_helpers {
let choices = [true, false];
// every epoch there's a 2% chance of somebody bonding a node
let bonding_weights = [2, 98];
// every epoch there's a 3% chance of somebody bonding a node
let bonding_weights = [3, 97];
// and 15% of making a delegation
let delegation_weights = [15, 85];
@@ -246,28 +246,28 @@ pub mod test_helpers {
// make sure we cover our edge case of somebody having both liquid and vested delegation towards the same node
if epoch_id == 123 {
test.add_immediate_delegation(problematic_delegator, stake, 4);
test.add_immediate_delegation(problematic_delegator_twin, stake, 4);
test.add_immediate_delegation(problematic_delegator, stake, 3);
test.add_immediate_delegation(problematic_delegator_twin, stake, 3);
}
if epoch_id == 666 {
test.add_immediate_delegation_with_legal_proxy(problematic_delegator, stake, 4);
test.add_immediate_delegation_with_legal_proxy(problematic_delegator, stake, 3);
test.add_immediate_delegation_with_legal_proxy(
problematic_delegator_twin,
stake,
4,
3,
);
}
if epoch_id == 234 {
test.add_immediate_delegation(problematic_delegator_alt_twin, stake, 4);
test.add_immediate_delegation(problematic_delegator_alt_twin, stake, 3);
}
if epoch_id == 420 {
test.add_immediate_delegation_with_legal_proxy(
problematic_delegator_alt_twin,
stake,
4,
3,
);
}
+1 -1
View File
@@ -400,7 +400,7 @@ mod tests {
let problematic_delegator = "n1foomp";
let problematic_delegator_twin = "n1bar";
let mix_id = 4;
let mix_id = 3;
let liquid_storage_key = Delegation::generate_storage_key(
mix_id,
+7 -6
View File
@@ -535,9 +535,10 @@
}
},
"node_modules/elliptic": {
"version": "6.5.4",
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz",
"integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==",
"version": "6.6.1",
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.6.1.tgz",
"integrity": "sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==",
"license": "MIT",
"dependencies": {
"bn.js": "^4.11.9",
"brorand": "^1.1.0",
@@ -1635,9 +1636,9 @@
"integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A=="
},
"elliptic": {
"version": "6.5.4",
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz",
"integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==",
"version": "6.6.1",
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.6.1.tgz",
"integrity": "sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==",
"requires": {
"bn.js": "^4.11.9",
"brorand": "^1.1.0",
+1 -1
View File
@@ -9,5 +9,5 @@ edition.workspace = true
license.workspace = true
[dependencies]
env_logger = "0.11.6"
env_logger = { workspace = true }
log.workspace = true
+45
View File
@@ -1067,6 +1067,51 @@ const config = {
unoptimized: true,
},
transpilePackages: ["@nymproject/contract-clients"],
async headers() {
const isDev = process.env.NODE_ENV === "development";
const csp = isDev
? `
default-src 'self' 'unsafe-inline' 'unsafe-eval' data: blob: *;
script-src 'self' 'unsafe-inline' 'unsafe-eval' data: blob: *;
font-src 'self' data: blob: *;
style-src 'self' 'unsafe-inline' data: blob: *;
img-src 'self' data: blob: *;
object-src 'self' data: blob: *;
base-uri 'self';
form-action 'self';
frame-ancestors 'self';
upgrade-insecure-requests;
connect-src 'self' data: blob: *;
frame-src 'self' data: blob: *;
worker-src 'self' blob: *;
`
: `
default-src 'self';
script-src 'self' 'unsafe-inline' 'unsafe-eval' https://vercel.live *.nymtech.net *.nymvpn.com *.vercel.app *.nymte.ch *.nyx.network *.nym.com https://nym.com nymvpn.com https://nymvpn.com *.nymtech.cc;
font-src 'self' data:;
style-src 'self' 'unsafe-inline';
img-src 'self';
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
upgrade-insecure-requests;
connect-src 'self' https://github.com *.vercel.app *.nymtech.net *.nymvpn.com *.nymte.ch *.nyx.network *.nym.com https://nym.com nymvpn.com https://nymvpn.com *.nymtech.cc;
frame-src 'self' https://vercel.live *.vercel.app *.nym.com https://nym.com;
worker-src 'self' blob: https://vercel.live *.vercel.app *.nym.com https://nym.com;
`;
return [
{
source: '/(.*)',
headers: [
{
key: 'Content-Security-Policy',
value: csp.replace(/\s{2,}/g, " ").trim(),
}
]
}
]
}
};
module.exports = config;
@@ -47,6 +47,128 @@ This page displays a full list of all the changes during our release cycle from
<VarInfo />
## `v2025.3-ruta`
- [Release Binaries](https://github.com/nymtech/nym/releases/tag/nym-binaries-v2025.3-ruta)
- [`nym-node`](nodes/nym-node.mdx) version `1.5.0`
```
nym-node
Binary Name: nym-node
Build Timestamp: 2025-02-13T11:49:34.670488195Z
Build Version: 1.5.0
Commit SHA: a3e19b4563843055b305ea9a397eb1ad84b5c378
Commit Date: 2025-02-10T18:14:47.000000000+01:00
Commit Branch: HEAD
rustc Version: 1.84.1
rustc Channel: stable
cargo Profile: release
```
### Operators Updates & Tools
- **Operators who use Debian based distributions must run their `nym-node` on Debian bookworm (Debian 12)/ Ubuntu 22.04 (and newer).** In case of running older operation system make sure to upgrade your server.
#### Service Grant Program v2 (SGPv2)
As we announced in [`hu` release notes](#service-grant-program-v2), we are opened a second iteration Service Grant Program to bootstrap node operations in key target geographies for Nym and ensures the network can provide a good quality of service.
##### Conditions to join of SGPv2
<Callout type="warning">
**If you are interested to become a part of SGPv2, read the point below. Do *not* buy new machines and do *not* migrate nodes just yet!**
</Callout>
- [Read specs & rules first](#service-grant-program-v2)
- The program will start from March 1st.
- Accepted participants are expected to run their nodes in assigned location and with [new specs](#service-grant-program-v2) by February 28th the latest.
- Squads orchestrating multiple nodes will be prioritized in locations with more slots.
- There is no final budget, neither a size per node as we expect that the next round of Service Grants program will be individual grants of varying sizes, depending on how many nodes, location, specs etc.
- Every participant of SGPv1 can submit for SGPv2 as many nodes as they currently have grant for, more slots may be offered later on.
- New submissions can only do one node per operator unless people come in squads (or DAOs) and can orchestrate multiple nodes on larger dedicated servers.
- Actual locations and approximate slots are listed below. If you want to have a node in a location which is not listed but it's near by some of the listed ones, it's not a problem, be honest in the form so we know where you planning to run nodes.
- February is the last month we proceed with grants under SGPv1.
##### How to Join
1. Have a look at the remaining [locations](#locations), specs and providers. You can contact them directly and make a calculation how much running a node on their service would cost and what size of a grant you expect to receive.
2. To apply for a service grant, the first step is to fill in [this form](https://nymtech2.typeform.com/to/LyPJer49) per each node.
3. Be responsive on Element for follow up communication.
##### Locations
These locations and slots are approximate and constantly change based on new operators submissions. Please keep in mind that we are going to chose people with respect to empty slots in the given location at the time of their submission. In case of competing submissions in the same location we will take the one with better specs vs price ratio.
| LOCATION | SLOTS |
| :-- | --: |
| India | 3 |
| Austria | 1 |
| Poland | 1 |
| Russia | 2 |
| Canada | 1 |
| South Africa | 2 |
| Sweden | 2 |
| Latvia | 2 |
| Lithuania | 1 |
| Bahrain | 1 |
| Czech Republic | 1 |
| Denmark | 1 |
| Norway | 1 |
| South Korea | 1 |
| Ukraine | 1 |
| Vietnam | 1 |
| Argentina | 1 |
| Belarus | 1 |
| Croatia | 1 |
| Cyprus | 1 |
| Ecuador | 1 |
| Hungary | 1 |
| Malta | 1 |
| New Zealand | 1 |
##### ISPs
- Have a look into our [ISP list](community-counsel/isp-list) page
- Further you can check out Tor community [Good Bad ISPs](https://community.torproject.org/relay/community-resources/good-bad-isps/) page
- With any findings or feedback, please add it to this [csv](https://github.com/nymtech/nym/blob/develop/documentation/docs/data/csv/isp-sheet.csv), issue a [Pull Request](community-counsel/add-content) and ping me for review.
- We will add much more providers in the coming days
### Features
- [DNS resolver configuration for internal HTTP client lookups](https://github.com/nymtech/nym/pull/5355): The resolver itself is the set combination of the google, cloudflare, and quad9 endpoints supporting DoH, DoT, and for google DoH3 as well. This resolver implements a fallback mechanism where, should the DNS-over-X resolution fail, a followup resolution will be done using the hosts configured default (e.g. `/etc/resolve.conf` on linux).
- [Bump tokio from `1.40.0` to `1.43.0`](https://github.com/nymtech/nym/pull/5370)
- [Uncouple storage reference for bandwidth client](https://github.com/nymtech/nym/pull/5372): Because of the `Storage` reference in the bandwidth client, the client can't be separated and used in a different place, as it will be linked to the `DisconnectedClient`. Cloning it ensures we uncouple this unnecessary dependence.
- [`MixnetClient` can send `ClientRequests`](https://github.com/nymtech/nym/pull/5381): Adding a channel to the `MixnetClient`, so that it can signal to the `MixTrafficController` that can send a `ClientRequest` via its `GatewayTransciver`. Additionaly adds a sleep to the `disconnect` call as the socket would close before the channel message got processed.
- [Use secure DNS for websocket connection establishment](https://github.com/nymtech/nym/pull/5386): Adjust the websocket connection establishment (using `tokio_tungstenite`) to use a custom DNS resolver that tries DoH and DoT. Instead of relying on the `tokio_tungstenite` to open the `tokio::net::TcpStream` ([which it does here](https://github.com/snapview/tokio-tungstenite/blob/aafb2f9e036162f7bffa002cfea502376a690724/src/connect.rs#L91)) which would kick in the default network resolver we do the resolution and open the `TcpStream` ourselves before handing off to the websocket libary.
- [Reduce log severity for checking topology validity](https://github.com/nymtech/nym/pull/5395)
- [Change Explorer URL to new smooshed nodes](https://github.com/nymtech/nym/pull/5396)
- [Send shutdown instead of panic when reaching max fail](https://github.com/nymtech/nym/pull/5398): Remove a panic and an unwrap inside `client-core` that is hit occasionally in the vpn client. This is a change that can have wide ranging impact since it changes the task handling and it's inside `client-core`, which is used in many components and services, so preventing regressions is important.
- [Relocate a validator api function](https://github.com/nymtech/nym/pull/5401): Adds a function to the `nym-validator-client` crate that hits the network details endpoint and returns an object parsed from `json`. This was floating loose in the `nym-vpn-client` repo.
- [Bump the patch-updates group across 1 directory with 9 updates](https://github.com/nymtech/nym/pull/5406)
- [Upgrade to `thiserror` 2.0](https://github.com/nymtech/nym/pull/5414)
- [Make `wait_for_graceful_shutdown` to be pub](https://github.com/nymtech/nym/pull/5424)
- [Fix statistics shutdown](https://github.com/nymtech/nym/pull/5426)
- [Push down forget me to client configs](https://github.com/nymtech/nym/pull/5431)
### Bugfix
- [Fix missing path triggers for CI](https://github.com/nymtech/nym/pull/5380): Fix missing path triggers for CI and sort crate list alphabetically to make it easier to maintain.
## `v2025.2-hu`
- [Release Binaries](https://github.com/nymtech/nym/releases/tag/nym-binaries-v2025.2-hu)
@@ -20,10 +20,10 @@ This documentation page provides a guide on how to set up and run a [NYM NODE](.
```sh
nym-node
Binary Name: nym-node
Build Timestamp: 2025-02-04T09:35:42.399220545Z
Build Version: 1.4.0
Commit SHA: 4c2bf3642e8eec0d55c7753e14429d73ac2d0e99
Commit Date: 2025-02-04T10:29:48.000000000+01:00
Build Timestamp: 2025-02-13T11:49:34.670488195Z
Build Version: 1.5.0
Commit SHA: a3e19b4563843055b305ea9a397eb1ad84b5c378
Commit Date: 2025-02-10T18:14:47.000000000+01:00
Commit Branch: HEAD
rustc Version: 1.84.1
rustc Channel: stable
@@ -62,7 +62,7 @@ Exit Gateway is the only mode routing data directly to the open internet. Theref
***dVPN routing (2-hop) is the Fast option of NymVPN. It runs a wireguard tunnel inside another wireguard tunnel. dVPN uses 2 Gateway layers (entry and exit) and no Mixnode layers. If an operator announces and [correctly configure](configuration#connectivity-test-and-configuration) their node to route wireguard, the node can be chosen as entry or exit by any client at any time.***
To activate wireguard routing, operators need to add `--wireguard-enabled true` alongside `nym-node` command `run`.
To activate wireguard routing, operators need to add `--wireguard-enabled true` alongside `nym-node` command `run`.
<Callout type="warning">
Wireguard nodes route data directly to the open internet. Therefore it exposes IP of operators server (VPS) to abuse complains. Before you decide to run a node with active wireguard routing, please read our [Community Counsel pages](../../community-counsel/exit-gateway) containing more information and some legal content.
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "explorer-api"
version = "1.1.45"
version = "1.1.47"
edition = "2021"
license.workspace = true
@@ -95,7 +95,7 @@ where
S: AsyncRead + AsyncWrite + Unpin + Send,
{
// don't accept any new requests if we have already received shutdown
if handle.shutdown.is_shutdown() {
if handle.shutdown.is_shutdown_poll() {
debug!("stopping the handle as we have received a shutdown");
return;
}
@@ -50,7 +50,7 @@ impl Listener {
connection = tcp_listener.accept() => {
match connection {
Ok((socket, remote_addr)) => {
let shutdown = self.shutdown.fork(format!("websocket-handler-{remote_addr}"));
let shutdown = self.shutdown.fork(format!("websocket_handler_{remote_addr}"));
trace!("received a socket connection from {remote_addr}");
// TODO: I think we *REALLY* need a mechanism for having a maximum number of connected
// clients or spawned tokio tasks -> perhaps a worker system?
+8 -8
View File
@@ -225,7 +225,7 @@ impl GatewayTasksBuilder {
handler_config,
nyxd_client,
self.identity_keypair.public_key().to_bytes(),
self.shutdown.fork("ecash-manager"),
self.shutdown.fork("ecash_manager"),
self.storage.clone(),
)
.await?,
@@ -279,13 +279,13 @@ impl GatewayTasksBuilder {
let mut message_router_builder = SpMessageRouterBuilder::new(
*self.identity_keypair.public_key(),
self.mix_packet_sender.clone(),
self.shutdown.fork("network-requester-message-router"),
self.shutdown.fork("network_requester_message_router"),
);
let transceiver = message_router_builder.gateway_transceiver();
let (on_start_tx, on_start_rx) = oneshot::channel();
let mut nr_builder = NRServiceProviderBuilder::new(nr_opts.config.clone())
.with_shutdown(self.shutdown.fork("network-requester-sp"))
.with_shutdown(self.shutdown.fork("network_requester_sp"))
.with_custom_gateway_transceiver(transceiver)
.with_wait_for_gateway(true)
.with_minimum_gateway_performance(0)
@@ -314,13 +314,13 @@ impl GatewayTasksBuilder {
let mut message_router_builder = SpMessageRouterBuilder::new(
*self.identity_keypair.public_key(),
self.mix_packet_sender.clone(),
self.shutdown.fork("ipr-message-router"),
self.shutdown.fork("ipr_message_router"),
);
let transceiver = message_router_builder.gateway_transceiver();
let (on_start_tx, on_start_rx) = oneshot::channel();
let mut ip_packet_router = IpPacketRouter::new(ip_opts.config.clone())
.with_shutdown(self.shutdown.fork("ipr-sp"))
.with_shutdown(self.shutdown.fork("ipr_sp"))
.with_custom_gateway_transceiver(Box::new(transceiver))
.with_wait_for_gateway(true)
.with_minimum_gateway_performance(0)
@@ -418,7 +418,7 @@ impl GatewayTasksBuilder {
let mut message_router_builder = SpMessageRouterBuilder::new(
*self.identity_keypair.public_key(),
self.mix_packet_sender.clone(),
self.shutdown.fork("authenticator-message-router"),
self.shutdown.fork("authenticator_message_router"),
);
let transceiver = message_router_builder.gateway_transceiver();
@@ -431,7 +431,7 @@ impl GatewayTasksBuilder {
)
.with_ecash_verifier(ecash_manager)
.with_custom_gateway_transceiver(transceiver)
.with_shutdown(self.shutdown.fork("authenticator-sp"))
.with_shutdown(self.shutdown.fork("authenticator_sp"))
.with_wait_for_gateway(true)
.with_minimum_gateway_performance(0)
.with_custom_topology_provider(topology_provider)
@@ -451,7 +451,7 @@ impl GatewayTasksBuilder {
pub fn build_stale_messages_cleaner(&self) -> StaleMessagesCleaner {
StaleMessagesCleaner::new(
&self.storage,
self.shutdown.fork("stale-messages-cleaner"),
self.shutdown.fork("stale_messages_cleaner"),
self.config.debug.stale_messages_max_age,
self.config.debug.stale_messages_cleaner_run_interval,
)
+3 -3
View File
@@ -4,7 +4,7 @@
[package]
name = "nym-api"
license = "GPL-3.0"
version = "1.1.49"
version = "1.1.51"
authors.workspace = true
edition = "2021"
rust-version.workspace = true
@@ -65,7 +65,7 @@ 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"] }
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"] }
@@ -136,7 +136,7 @@ sqlx = { workspace = true, features = [
] }
[dev-dependencies]
axum-test = "16.2.0"
axum-test = { workspace = true }
tempfile = { workspace = true }
cw3 = { workspace = true }
cw-utils = { workspace = true }
+31 -1
View File
@@ -8,12 +8,19 @@ use nym_crypto::asymmetric::x25519::serde_helpers::bs58_x25519_pubkey;
use nym_crypto::asymmetric::{ed25519, x25519};
use nym_mixnet_contract_common::nym_node::Role;
use nym_mixnet_contract_common::reward_params::Performance;
use nym_mixnet_contract_common::NodeId;
use nym_mixnet_contract_common::{Interval, NodeId};
use serde::{Deserialize, Serialize};
use std::net::IpAddr;
use time::OffsetDateTime;
use utoipa::ToSchema;
#[derive(Clone, Copy, Debug, Serialize, Deserialize, schemars::JsonSchema, utoipa::ToSchema)]
#[serde(rename_all = "kebab-case")]
pub enum TopologyRequestStatus {
NoUpdates,
Fresh(Interval),
}
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub struct CachedNodesResponse<T: ToSchema> {
pub refreshed_at: OffsetDateTimeJsonSchemaWrapper,
@@ -37,6 +44,7 @@ impl<T: ToSchema> CachedNodesResponse<T> {
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema)]
pub struct PaginatedCachedNodesResponse<T> {
pub status: Option<TopologyRequestStatus>,
pub refreshed_at: OffsetDateTimeJsonSchemaWrapper,
pub nodes: PaginatedResponse<T>,
}
@@ -56,6 +64,28 @@ impl<T> PaginatedCachedNodesResponse<T> {
},
data: nodes,
},
status: None,
}
}
pub fn fresh(mut self, interval: Option<Interval>) -> Self {
let iv = interval.map(TopologyRequestStatus::Fresh);
self.status = iv;
self
}
pub fn no_updates() -> Self {
PaginatedCachedNodesResponse {
refreshed_at: OffsetDateTime::now_utc().into(),
nodes: PaginatedResponse {
pagination: Pagination {
total: 0,
page: 0,
size: 0,
},
data: Vec::new(),
},
status: Some(TopologyRequestStatus::NoUpdates),
}
}
}
+2
View File
@@ -6,6 +6,7 @@ use crate::support::http::state::AppState;
use axum::{extract, Router};
use nym_contracts_common::ContractBuildInformation;
use std::collections::HashMap;
use tower_http::compression::CompressionLayer;
use utoipa::ToSchema;
pub(crate) fn nym_network_routes() -> Router<AppState> {
@@ -16,6 +17,7 @@ pub(crate) fn nym_network_routes() -> Router<AppState> {
"/nym-contracts-detailed",
axum::routing::get(nym_contracts_detailed),
)
.layer(CompressionLayer::new())
}
#[utoipa::path(
@@ -167,7 +167,7 @@ impl PacketSender {
let gateway_packet_router = PacketRouter::new(
ack_sender,
message_sender,
task_client.fork("packet-router"),
task_client.fork("packet_router"),
);
let shared_keys = fresh_gateway_client_data
@@ -184,7 +184,10 @@ impl PacketSender {
shared_keys,
gateway_packet_router,
Some(fresh_gateway_client_data.bandwidth_controller.clone()),
nym_statistics_common::clients::ClientStatsSender::new(None),
nym_statistics_common::clients::ClientStatsSender::new(
None,
task_client.fork("client_stats_sender"),
),
#[cfg(unix)]
None,
task_client,
+2
View File
@@ -7,6 +7,7 @@ use axum::extract::State;
use axum::{Json, Router};
use nym_api_requests::legacy::LegacyMixNodeBondWithLayer;
use nym_api_requests::models::{LegacyDescribedGateway, LegacyDescribedMixNode};
use tower_http::compression::CompressionLayer;
// we want to mark the routes as deprecated in swagger, but still expose them
#[allow(deprecated)]
@@ -20,6 +21,7 @@ pub(crate) fn legacy_nym_node_routes() -> Router<AppState> {
"/mixnodes/described",
axum::routing::get(get_mixnodes_described),
)
.layer(CompressionLayer::new())
}
#[utoipa::path(
+2
View File
@@ -21,6 +21,7 @@ use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::time::Duration;
use time::{Date, OffsetDateTime};
use tower_http::compression::CompressionLayer;
use utoipa::{IntoParams, ToSchema};
pub(crate) mod legacy;
@@ -45,6 +46,7 @@ pub(crate) fn nym_node_routes() -> Router<AppState> {
// to make it compatible with all the explorers that were used to using 0-100 values
.route("/uptime-history/:node_id", get(get_node_uptime_history))
.route("/rewarded-set", get(rewarded_set))
.layer(CompressionLayer::new())
}
#[utoipa::path(
@@ -33,6 +33,7 @@ use axum::routing::get;
use axum::Router;
use nym_api_requests::nym_nodes::NodeRoleQueryParam;
use serde::Deserialize;
use tower_http::compression::CompressionLayer;
pub(crate) mod full_fat;
mod helpers;
@@ -73,6 +74,7 @@ pub(crate) fn nym_node_routes_unstable() -> Router<AppState> {
.nest("/full-fat", Router::new().route("/", get(nodes_detailed)))
.route("/gateways/skimmed", get(skimmed::deprecated_gateways_basic))
.route("/mixnodes/skimmed", get(skimmed::deprecated_mixnodes_basic))
.layer(CompressionLayer::new())
}
#[derive(Debug, Deserialize, utoipa::IntoParams)]
@@ -85,6 +87,11 @@ struct NodesParamsWithRole {
no_legacy: Option<bool>,
page: Option<u32>,
per_page: Option<u32>,
// Identifier for the current epoch of the topology state. When sent by a client we can check if
// the client already knows about the latest topology state, allowing a `no-updates` response
// instead of wasting bandwidth serving an unchanged topology.
epoch_id: Option<u32>,
}
#[derive(Debug, Deserialize, utoipa::IntoParams)]
@@ -95,6 +102,11 @@ struct NodesParams {
no_legacy: Option<bool>,
page: Option<u32>,
per_page: Option<u32>,
// Identifier for the current epoch of the topology state. When sent by a client we can check if
// the client already knows about the latest topology state, allowing a `no-updates` response
// instead of wasting bandwidth serving an unchanged topology.
epoch_id: Option<u32>,
}
impl From<NodesParamsWithRole> for NodesParams {
@@ -104,6 +116,7 @@ impl From<NodesParamsWithRole> for NodesParams {
no_legacy: params.no_legacy,
page: params.page,
per_page: params.per_page,
epoch_id: params.epoch_id,
}
}
}
@@ -124,6 +124,21 @@ where
// (ideally it'd be tied directly to the NI iterator, but I couldn't defeat the compiler)
let describe_cache = state.describe_nodes_cache_data().await?;
let maybe_interval = state
.nym_contract_cache()
.current_interval()
.await
.to_owned();
// 4.0 If the client indicates that they already know about the current topology send empty response
if let Some(client_known_epoch) = query_params.epoch_id {
if let Some(ref interval) = maybe_interval {
if client_known_epoch == interval.current_epoch_id() {
return Ok(Json(PaginatedCachedNodesResponse::no_updates()));
}
}
}
// 4. start building the response
let mut nodes =
build_nym_nodes_response(&rewarded_set, nym_nodes_subset, &annotations, active_only);
@@ -137,10 +152,9 @@ where
describe_cache.timestamp(),
]);
return Ok(Json(PaginatedCachedNodesResponse::new_full(
refreshed_at,
nodes,
)));
return Ok(Json(
PaginatedCachedNodesResponse::new_full(refreshed_at, nodes).fresh(maybe_interval),
));
}
// 6. grab relevant legacy nodes
@@ -162,10 +176,9 @@ where
annotated_legacy_nodes.timestamp(),
]);
Ok(Json(PaginatedCachedNodesResponse::new_full(
refreshed_at,
nodes,
)))
Ok(Json(
PaginatedCachedNodesResponse::new_full(refreshed_at, nodes).fresh(maybe_interval),
))
}
/// Deprecated query that gets ALL gateways
@@ -6,7 +6,9 @@ use crate::api::v1::ticketbook::models::{
TicketbookWalletSharesResponse,
};
use async_trait::async_trait;
use nym_http_api_client::{parse_response, HttpClientError, Params, PathSegments, NO_PARAMS};
use nym_http_api_client::{
parse_response, ApiClient, HttpClientError, Params, PathSegments, NO_PARAMS,
};
use reqwest::IntoUrl;
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
@@ -20,11 +20,11 @@ thiserror.workspace = true
zeroize = { workspace = true }
# wasm-specific deps
getrandom = { version = "0.2", features = ["js"] }
getrandom = { workspace = true, features = ["js"] }
wasm-bindgen = { workspace = true }
js-sys = "0.3.76"
js-sys = { workspace = true }
tsify = { workspace = true, features = ["js"] }
serde-wasm-bindgen = "0.6.5"
serde-wasm-bindgen = { workspace = true }
# NYM:
nym-bin-common = { path = "../../common/bin-common" }
+1 -1
View File
@@ -27,7 +27,7 @@ tokio = { workspace = true, features = ["macros", "time"] }
tokio-util = { workspace = true }
utoipa = { workspace = true, features = ["axum_extras"] }
utoipa-swagger-ui = { workspace = true, features = ["axum"] }
tokio-postgres = "0.7"
tokio-postgres = { workspace = true }
# internal
nym-bin-common = { path = "../common/bin-common" }
+1
View File
@@ -1,6 +1,7 @@
nym-node-status-agent/nym-gateway-probe
nym-node-status-agent/keys/
data/
settings.sql
enter_db.sh
nym-gateway-probe
*.sqlite
@@ -3,7 +3,7 @@
[package]
name = "nym-node-status-api"
version = "1.0.0-rc.7"
version = "1.0.0-rc.8"
authors.workspace = true
repository.workspace = true
homepage.workspace = true
@@ -13,7 +13,7 @@ license.workspace = true
rust-version.workspace = true
[dependencies]
ammonia = "4"
ammonia = { workspace = true }
anyhow = { workspace = true }
axum = { workspace = true, features = ["tokio", "macros"] }
chrono = { workspace = true }
@@ -22,6 +22,7 @@ cosmwasm-std = { workspace = true }
envy = { workspace = true }
futures-util = { workspace = true }
moka = { workspace = true, features = ["future"] }
nym-contracts-common = { path = "../../common/cosmwasm-smart-contracts/contracts-common" }
nym-bin-common = { path = "../../common/bin-common", features = ["models"] }
nym-node-status-client = { path = "../nym-node-status-client" }
nym-crypto = { path = "../../common/crypto", features = ["asymmetric", "serde"] }
@@ -50,10 +51,6 @@ tracing-log = { workspace = true }
tower-http = { workspace = true, features = ["cors", "trace"] }
utoipa = { workspace = true, features = ["axum_extras", "time"] }
utoipa-swagger-ui = { workspace = true, features = ["axum"] }
# TODO dz `cargo update async-trait`
# for automatic schema detection, which was merged, but not released yet
# https://github.com/ProbablyClem/utoipauto/pull/38
# utoipauto = { git = "https://github.com/ProbablyClem/utoipauto", rev = "eb04cba" }
utoipauto = { workspace = true }
nym-node-metrics = { path = "../../nym-node/nym-node-metrics" }
@@ -36,10 +36,18 @@ fn read_env_var(var: &str) -> Result<String> {
/// use `./enter_db.sh` to inspect DB
async fn write_db_path_to_file(out_dir: &str, db_filename: &str) -> anyhow::Result<()> {
let mut file = File::create("settings.sql").await?;
let settings = ".mode columns
.headers on";
file.write_all(settings.as_bytes()).await?;
let mut file = File::create("enter_db.sh").await?;
let _ = file.write(b"#!/bin/bash\n").await?;
file.write_all(format!("sqlite3 {}/{}", out_dir, db_filename).as_bytes())
.await?;
let contents = format!(
"#!/bin/bash\n\
sqlite3 -init settings.sql {}/{}",
out_dir, db_filename,
);
file.write_all(contents.as_bytes()).await?;
#[cfg(target_family = "unix")]
file.set_permissions(Permissions::from_mode(0o755))
@@ -3,16 +3,19 @@
set -e
user_rust_log_preference=$RUST_LOG
export ENVIRONMENT=${ENVIRONMENT:-"sandbox"}
export NYM_API_CLIENT_TIMEOUT=60
export EXPLORER_CLIENT_TIMEOUT=60
export NODE_STATUS_API_TESTRUN_REFRESH_INTERVAL=120
if [ "$ENVIRONMENT" = "mainnet" ]; then
export NYM_NODE_STATUS_API_HM_URL="https://harbourmaster.nymtech.net"
fi
# public counterpart of the agent's private key.
# For TESTING only. NOT used in any other environment
export NODE_STATUS_API_AGENT_KEY_LIST="H4z8kx5Kkf5JMQHhxaW1MwYndjKCDHC7HsVhHTFfBZ4J"
export ENVIRONMENT=${ENVIRONMENT:-"sandbox"}
script_dir=$(dirname $(realpath "$0"))
monorepo_root=$(realpath "${script_dir}/../..")
@@ -0,0 +1,54 @@
ALTER TABLE mixnodes DROP COLUMN blacklisted;
ALTER TABLE gateways DROP COLUMN blacklisted;
CREATE TABLE nym_nodes (
node_id INTEGER PRIMARY KEY,
ed25519_identity_pubkey VARCHAR NOT NULL UNIQUE,
total_stake INTEGER NOT NULL,
ip_addresses TEXT NOT NULL,
mix_port INTEGER NOT NULL,
x25519_sphinx_pubkey VARCHAR NOT NULL UNIQUE,
node_role TEXT NOT NULL,
supported_roles TEXT NOT NULL,
performance VARCHAR NOT NULL,
entry TEXT,
last_updated_utc INTEGER NOT NULL
);
CREATE INDEX idx_nym_nodes_node_id ON nym_nodes (node_id);
CREATE INDEX idx_nym_nodes_ed25519_identity_pubkey ON nym_nodes (ed25519_identity_pubkey);
CREATE TABLE nym_node_descriptions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
node_id INTEGER UNIQUE NOT NULL,
moniker VARCHAR,
website VARCHAR,
security_contact VARCHAR,
details VARCHAR,
last_updated_utc INTEGER NOT NULL,
FOREIGN KEY (node_id) REFERENCES nym_nodes (node_id)
);
CREATE TABLE nym_nodes_packet_stats_raw (
id INTEGER PRIMARY KEY AUTOINCREMENT,
node_id INTEGER NOT NULL,
timestamp_utc INTEGER NOT NULL,
packets_received INTEGER,
packets_sent INTEGER,
packets_dropped INTEGER,
FOREIGN KEY (node_id) REFERENCES nym_nodes (node_id)
);
CREATE INDEX idx_nym_nodes_packet_stats_raw_node_id_timestamp_utc ON nym_nodes_packet_stats_raw (node_id, timestamp_utc);
CREATE TABLE nym_node_daily_mixing_stats (
id INTEGER PRIMARY KEY AUTOINCREMENT,
node_id INTEGER NOT NULL,
total_stake BIGINT NOT NULL,
date_utc VARCHAR NOT NULL,
packets_received INTEGER DEFAULT 0,
packets_sent INTEGER DEFAULT 0,
packets_dropped INTEGER DEFAULT 0,
FOREIGN KEY (node_id) REFERENCES nym_nodes (node_id),
UNIQUE (node_id, date_utc) -- This constraint automatically creates an index
);
@@ -83,6 +83,9 @@ pub(crate) struct Cli {
env = "NYM_NODE_STATUS_API_MAX_AGENT_COUNT"
)]
pub(crate) max_agent_count: i64,
#[clap(long, default_value = "", env = "NYM_NODE_STATUS_API_HM_URL")]
pub(crate) hm_url: String,
}
fn parse_duration(arg: &str) -> Result<std::time::Duration, std::num::ParseIntError> {
@@ -1,18 +1,24 @@
use std::str::FromStr;
use crate::{
http::{self, models::SummaryHistory},
monitor::NumericalCheckedCast,
};
use anyhow::Context;
use nym_contracts_common::Percent;
use nym_crypto::asymmetric::{ed25519, x25519};
use nym_network_defaults::DEFAULT_NYM_NODE_HTTP_PORT;
use nym_node_requests::api::v1::node::models::NodeDescription;
use nym_validator_client::nym_api::SkimmedNode;
use serde::{Deserialize, Serialize};
use sqlx::FromRow;
use strum_macros::{EnumString, FromRepr};
use time::Date;
use time::{Date, OffsetDateTime};
use utoipa::ToSchema;
pub(crate) struct GatewayRecord {
pub(crate) identity_key: String,
pub(crate) bonded: bool,
pub(crate) blacklisted: bool,
pub(crate) self_described: String,
// TODO dz shouldn't be an option
pub(crate) explorer_pretty_bond: Option<String>,
@@ -24,7 +30,6 @@ pub(crate) struct GatewayRecord {
pub(crate) struct GatewayDto {
pub(crate) gateway_identity_key: String,
pub(crate) bonded: bool,
pub(crate) blacklisted: bool,
pub(crate) performance: i64,
pub(crate) self_described: Option<String>,
pub(crate) explorer_pretty_bond: Option<String>,
@@ -69,7 +74,6 @@ impl TryFrom<GatewayDto> for http::models::Gateway {
let last_probe_result = serde_json::from_str(&last_probe_result).unwrap_or(None);
let bonded = value.bonded;
let blacklisted = value.blacklisted;
let performance = value.performance as u8;
let description = NodeDescription {
@@ -82,7 +86,6 @@ impl TryFrom<GatewayDto> for http::models::Gateway {
Ok(http::models::Gateway {
gateway_identity_key: value.gateway_identity_key.clone(),
bonded,
blacklisted,
performance,
self_described,
explorer_pretty_bond,
@@ -109,7 +112,6 @@ pub(crate) struct MixnodeRecord {
pub(crate) total_stake: i64,
pub(crate) host: String,
pub(crate) http_port: u16,
pub(crate) blacklisted: bool,
pub(crate) full_details: String,
pub(crate) self_described: Option<String>,
pub(crate) last_updated_utc: i64,
@@ -120,7 +122,6 @@ pub(crate) struct MixnodeRecord {
pub(crate) struct MixnodeDto {
pub(crate) mix_id: i64,
pub(crate) bonded: bool,
pub(crate) blacklisted: bool,
pub(crate) is_dp_delegatee: bool,
pub(crate) total_stake: i64,
pub(crate) full_details: String,
@@ -147,7 +148,6 @@ impl TryFrom<MixnodeDto> for http::models::Mixnode {
let last_updated_utc =
timestamp_as_utc(value.last_updated_utc.cast_checked()?).to_rfc3339();
let blacklisted = value.blacklisted;
let is_dp_delegatee = value.is_dp_delegatee;
let moniker = value.moniker.clone();
let website = value.website.clone();
@@ -157,7 +157,6 @@ impl TryFrom<MixnodeDto> for http::models::Mixnode {
Ok(http::models::Mixnode {
mix_id,
bonded: value.bonded,
blacklisted,
is_dp_delegatee,
total_stake: value.total_stake,
full_details,
@@ -211,25 +210,27 @@ impl TryFrom<SummaryHistoryDto> for SummaryHistory {
}
}
pub(crate) const MIXNODES_BONDED_COUNT: &str = "mixnodes.bonded.count";
pub(crate) const MIXNODES_BONDED_ACTIVE: &str = "mixnodes.bonded.active";
pub(crate) const MIXNODES_BONDED_INACTIVE: &str = "mixnodes.bonded.inactive";
pub(crate) const MIXNODES_BONDED_RESERVE: &str = "mixnodes.bonded.reserve";
pub(crate) const MIXNODES_BLACKLISTED_COUNT: &str = "mixnodes.blacklisted.count";
pub(crate) const MIXNODES_LEGACY_COUNT: &str = "mixnodes.legacy.count";
pub(crate) const NYMNODES_DESCRIBED_COUNT: &str = "nymnode.described.count";
pub(crate) const NYMNODE_COUNT: &str = "nymnode.total.count";
pub(crate) const ASSIGNED_ENTRY_COUNT: &str = "assigned.entry.count";
pub(crate) const ASSIGNED_EXIT_COUNT: &str = "assigned.exit.count";
pub(crate) const ASSIGNED_MIXING_COUNT: &str = "assigned.mixing.count";
pub(crate) const GATEWAYS_BONDED_COUNT: &str = "gateways.bonded.count";
pub(crate) const GATEWAYS_BLACKLISTED_COUNT: &str = "gateways.blacklisted.count";
pub(crate) const MIXNODES_HISTORICAL_COUNT: &str = "mixnodes.historical.count";
pub(crate) const GATEWAYS_HISTORICAL_COUNT: &str = "gateways.historical.count";
// `utoipa`` goes crazy if you use module-qualified prefix as field type so we
// `utoipa` goes crazy if you use module-qualified prefix as field type so we
// have to import it
use gateway::GatewaySummary;
use mixnode::MixnodeSummary;
#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)]
pub(crate) struct NetworkSummary {
pub(crate) total_nodes: i32,
pub(crate) mixnodes: MixnodeSummary,
pub(crate) gateways: GatewaySummary,
}
@@ -239,23 +240,15 @@ pub(crate) mod mixnode {
#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)]
pub(crate) struct MixnodeSummary {
pub(crate) bonded: MixnodeSummaryBonded,
pub(crate) blacklisted: MixnodeSummaryBlacklisted,
pub(crate) bonded: MixingNodesSummary,
pub(crate) historical: MixnodeSummaryHistorical,
}
#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)]
pub(crate) struct MixnodeSummaryBonded {
pub(crate) count: i32,
pub(crate) active: i32,
pub(crate) inactive: i32,
pub(crate) reserve: i32,
pub(crate) last_updated_utc: String,
}
#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)]
pub(crate) struct MixnodeSummaryBlacklisted {
pub(crate) struct MixingNodesSummary {
pub(crate) count: i32,
pub(crate) self_described: i32,
pub(crate) legacy: i32,
pub(crate) last_updated_utc: String,
}
@@ -272,19 +265,15 @@ pub(crate) mod gateway {
#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)]
pub(crate) struct GatewaySummary {
pub(crate) bonded: GatewaySummaryBonded,
pub(crate) blacklisted: GatewaySummaryBlacklisted,
pub(crate) historical: GatewaySummaryHistorical,
}
#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)]
pub(crate) struct GatewaySummaryExplorer {
pub(crate) count: i32,
pub(crate) last_updated_utc: String,
pub(crate) historical: GatewaySummaryHistorical,
}
#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)]
pub(crate) struct GatewaySummaryBonded {
pub(crate) count: i32,
pub(crate) entry: i32,
pub(crate) exit: i32,
pub(crate) last_updated_utc: String,
}
@@ -293,12 +282,6 @@ pub(crate) mod gateway {
pub(crate) count: i32,
pub(crate) last_updated_utc: String,
}
#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)]
pub(crate) struct GatewaySummaryBlacklisted {
pub(crate) count: i32,
pub(crate) last_updated_utc: String,
}
}
#[allow(dead_code)] // not dead code, this is SQL data model
@@ -377,8 +360,130 @@ impl TryFrom<GatewaySessionsRecord> for http::models::SessionStats {
}
}
pub(crate) enum MixingNodeKind {
LegacyMixnode,
NymNode,
}
pub(crate) struct ScraperNodeInfo {
pub node_id: i64,
pub host: String,
pub node_kind: MixingNodeKind,
pub hosts: Vec<String>,
pub http_api_port: i64,
}
impl ScraperNodeInfo {
pub(crate) fn contact_addresses(&self) -> Vec<String> {
let mut urls = Vec::new();
for host in &self.hosts {
urls.append(&mut vec![
format!("http://{}:{}", host, DEFAULT_NYM_NODE_HTTP_PORT),
format!("http://{}:8000", host),
format!("https://{}", host),
format!("http://{}", host),
]);
if self.http_api_port != DEFAULT_NYM_NODE_HTTP_PORT as i64 {
urls.insert(0, format!("http://{}:{}", host, self.http_api_port));
}
}
urls
}
}
#[derive(sqlx::Decode, Debug)]
pub(crate) struct NymNodeDto {
pub node_id: i64,
pub ed25519_identity_pubkey: String,
#[allow(dead_code)] // it's not dead code but clippy doesn't detect usage in sqlx macros
pub total_stake: i64,
pub ip_addresses: serde_json::Value,
pub mix_port: i64,
pub x25519_sphinx_pubkey: String,
pub node_role: serde_json::Value,
pub supported_roles: serde_json::Value,
pub entry: Option<serde_json::Value>,
pub performance: String,
}
#[derive(Debug)]
pub(crate) struct NymNodeInsertRecord {
pub node_id: i64,
pub ed25519_identity_pubkey: String,
pub total_stake: i64,
pub ip_addresses: serde_json::Value,
pub mix_port: i64,
pub x25519_sphinx_pubkey: String,
pub node_role: serde_json::Value,
pub supported_roles: serde_json::Value,
pub performance: String,
pub entry: Option<serde_json::Value>,
pub last_updated_utc: String,
}
impl NymNodeInsertRecord {
pub fn new(skimmed_node: SkimmedNode, total_stake: i64) -> anyhow::Result<Self> {
let now = OffsetDateTime::now_utc().to_string();
let record = Self {
node_id: skimmed_node.node_id.into(),
ed25519_identity_pubkey: skimmed_node.ed25519_identity_pubkey.to_base58_string(),
total_stake,
ip_addresses: serde_json::to_value(&skimmed_node.ip_addresses)?,
mix_port: skimmed_node.mix_port as i64,
x25519_sphinx_pubkey: skimmed_node.x25519_sphinx_pubkey.to_base58_string(),
node_role: serde_json::to_value(&skimmed_node.role)?,
supported_roles: serde_json::to_value(skimmed_node.supported_roles)?,
performance: skimmed_node.performance.value().to_string(),
entry: match skimmed_node.entry {
Some(entry) => Some(serde_json::to_value(entry)?),
None => None,
},
last_updated_utc: now,
};
Ok(record)
}
}
impl TryFrom<NymNodeDto> for SkimmedNode {
type Error = anyhow::Error;
fn try_from(other: NymNodeDto) -> Result<Self, Self::Error> {
let node_id = u32::try_from(other.node_id).context("Invalid node_id in DB")?;
let supported_roles =
serde_json::from_value(other.supported_roles).context("supported_roles")?;
let node_role = serde_json::from_value(other.node_role).context("node_role")?;
let ip_addresses = serde_json::from_value(other.ip_addresses).context("ip_addresses")?;
let entry = match other.entry {
Some(raw) => Some(serde_json::from_value(raw).context("entry")?),
None => None,
};
let skimmed_node = SkimmedNode {
node_id,
ed25519_identity_pubkey: ed25519::PublicKey::from_base58_string(
other.ed25519_identity_pubkey,
)
.context("ed25519_identity_pubkey")?,
ip_addresses,
mix_port: other.mix_port.try_into()?,
x25519_sphinx_pubkey: x25519::PublicKey::from_base58_string(other.x25519_sphinx_pubkey)
.context("x25519_sphinx_pubkey")?,
role: node_role,
supported_roles,
entry,
performance: Percent::from_str(&other.performance).context("can't parse Percent")?,
};
Ok(skimmed_node)
}
}
#[derive(Debug, Serialize, Deserialize, sqlx::Decode)]
pub struct NodeStats {
pub packets_received: i64,
pub packets_sent: i64,
pub packets_dropped: i64,
}
@@ -1,12 +1,11 @@
use crate::{
db::{
models::{BondedStatusDto, GatewayDto, GatewayRecord},
models::{GatewayDto, GatewayRecord},
DbPool,
},
http::models::Gateway,
};
use futures_util::TryStreamExt;
use nym_validator_client::models::NymNodeDescription;
use sqlx::{pool::PoolConnection, Sqlite};
use tracing::error;
@@ -37,20 +36,18 @@ pub(crate) async fn insert_gateways(
for record in gateways {
sqlx::query!(
"INSERT INTO gateways
(gateway_identity_key, bonded, blacklisted,
(gateway_identity_key, bonded,
self_described, explorer_pretty_bond,
last_updated_utc, performance)
VALUES (?, ?, ?, ?, ?, ?, ?)
VALUES (?, ?, ?, ?, ?, ?)
ON CONFLICT(gateway_identity_key) DO UPDATE SET
bonded=excluded.bonded,
blacklisted=excluded.blacklisted,
self_described=excluded.self_described,
explorer_pretty_bond=excluded.explorer_pretty_bond,
last_updated_utc=excluded.last_updated_utc,
performance = excluded.performance;",
record.identity_key,
record.bonded,
record.blacklisted,
record.self_described,
record.explorer_pretty_bond,
record.last_updated_utc,
@@ -63,82 +60,6 @@ pub(crate) async fn insert_gateways(
Ok(())
}
pub(crate) async fn write_blacklisted_gateways_to_db<'a, I>(
pool: &DbPool,
gateways: I,
) -> anyhow::Result<()>
where
I: Iterator<Item = &'a String>,
{
let mut conn = pool.acquire().await?;
for gateway_identity_key in gateways {
sqlx::query!(
"UPDATE gateways
SET blacklisted = true
WHERE gateway_identity_key = ?;",
gateway_identity_key,
)
.execute(&mut *conn)
.await?;
}
Ok(())
}
/// Ensure all gateways that are set as bonded, are still bonded
pub(crate) async fn ensure_gateways_still_bonded(
pool: &DbPool,
gateways: &[&NymNodeDescription],
) -> anyhow::Result<usize> {
let bonded_gateways_rows = get_all_bonded_gateways_row_ids_by_status(pool, true).await?;
let unbonded_gateways_rows = bonded_gateways_rows.iter().filter(|v| {
!gateways
.iter()
.any(|bonded| *bonded.ed25519_identity_key().to_base58_string() == v.identity_key)
});
let recently_unbonded_gateways = unbonded_gateways_rows.to_owned().count();
let last_updated_utc = chrono::offset::Utc::now().timestamp();
let mut transaction = pool.begin().await?;
for row in unbonded_gateways_rows {
sqlx::query!(
"UPDATE gateways
SET bonded = ?, last_updated_utc = ?
WHERE id = ?;",
false,
last_updated_utc,
row.id,
)
.execute(&mut *transaction)
.await?;
}
transaction.commit().await?;
Ok(recently_unbonded_gateways)
}
async fn get_all_bonded_gateways_row_ids_by_status(
pool: &DbPool,
status: bool,
) -> anyhow::Result<Vec<BondedStatusDto>> {
let mut conn = pool.acquire().await?;
let items = sqlx::query_as!(
BondedStatusDto,
r#"SELECT
id as "id!",
gateway_identity_key as "identity_key!",
bonded as "bonded: bool"
FROM gateways
WHERE bonded = ?"#,
status,
)
.fetch(&mut *conn)
.try_collect::<Vec<_>>()
.await?;
Ok(items)
}
pub(crate) async fn get_all_gateways(pool: &DbPool) -> anyhow::Result<Vec<Gateway>> {
let mut conn = pool.acquire().await?;
let items = sqlx::query_as!(
@@ -146,9 +67,8 @@ pub(crate) async fn get_all_gateways(pool: &DbPool) -> anyhow::Result<Vec<Gatewa
r#"SELECT
gw.gateway_identity_key as "gateway_identity_key!",
gw.bonded as "bonded: bool",
gw.blacklisted as "blacklisted: bool",
gw.performance as "performance!",
gw.self_described as "self_described?",
gw.self_described as "self_described?",
gw.explorer_pretty_bond as "explorer_pretty_bond?",
gw.last_probe_result as "last_probe_result?",
gw.last_probe_log as "last_probe_log?",
@@ -5,7 +5,7 @@ use chrono::{DateTime, Utc};
/// `daily_summary`
pub(crate) async fn insert_summaries(
pool: &DbPool,
summaries: &[(&str, &usize)],
summaries: &[(&str, usize)],
summary: &NetworkSummary,
last_updated: DateTime<Utc>,
) -> anyhow::Result<()> {
@@ -18,7 +18,7 @@ pub(crate) async fn insert_summaries(
async fn insert_summary(
pool: &DbPool,
summaries: &[(&str, &usize)],
summaries: &[(&str, usize)],
last_updated: DateTime<Utc>,
) -> anyhow::Result<()> {
let timestamp = last_updated.timestamp();
@@ -1,10 +1,9 @@
use futures_util::TryStreamExt;
use nym_validator_client::models::MixNodeBondAnnotated;
use tracing::error;
use crate::{
db::{
models::{BondedStatusDto, MixnodeDto, MixnodeRecord},
models::{MixnodeDto, MixnodeRecord},
DbPool,
},
http::models::{DailyStats, Mixnode},
@@ -21,13 +20,13 @@ pub(crate) async fn insert_mixnodes(
sqlx::query!(
"INSERT INTO mixnodes
(mix_id, identity_key, bonded, total_stake,
host, http_api_port, blacklisted, full_details,
host, http_api_port, full_details,
self_described, last_updated_utc, is_dp_delegatee)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(mix_id) DO UPDATE SET
bonded=excluded.bonded,
total_stake=excluded.total_stake, host=excluded.host,
http_api_port=excluded.http_api_port,blacklisted=excluded.blacklisted,
http_api_port=excluded.http_api_port,
full_details=excluded.full_details,self_described=excluded.self_described,
last_updated_utc=excluded.last_updated_utc,
is_dp_delegatee = excluded.is_dp_delegatee;",
@@ -37,7 +36,6 @@ pub(crate) async fn insert_mixnodes(
record.total_stake,
record.host,
record.http_port,
record.blacklisted,
record.full_details,
record.self_described,
record.last_updated_utc,
@@ -57,7 +55,6 @@ pub(crate) async fn get_all_mixnodes(pool: &DbPool) -> anyhow::Result<Vec<Mixnod
r#"SELECT
mn.mix_id as "mix_id!",
mn.bonded as "bonded: bool",
mn.blacklisted as "blacklisted: bool",
mn.is_dp_delegatee as "is_dp_delegatee: bool",
mn.total_stake as "total_stake!",
mn.full_details as "full_details!",
@@ -86,34 +83,43 @@ pub(crate) async fn get_all_mixnodes(pool: &DbPool) -> anyhow::Result<Vec<Mixnod
Ok(items)
}
/// We fetch the latest 30 days of data as a subquery and then
/// return it in ascending order, so we don't break existing UI
pub(crate) async fn get_daily_stats(pool: &DbPool) -> anyhow::Result<Vec<DailyStats>> {
/// `offset` = slides our fixed-day period further into the past by N days
pub(crate) async fn get_daily_stats(pool: &DbPool, offset: i64) -> anyhow::Result<Vec<DailyStats>> {
let mut conn = pool.acquire().await?;
let items = sqlx::query_as!(
DailyStats,
r#"
SELECT
date_utc as "date_utc!",
packets_received as "total_packets_received!: i64",
packets_sent as "total_packets_sent!: i64",
packets_dropped as "total_packets_dropped!: i64",
total_stake as "total_stake!: i64"
SUM(total_stake) as "total_stake!: i64",
SUM(packets_received) as "total_packets_received!: i64",
SUM(packets_sent) as "total_packets_sent!: i64",
SUM(packets_dropped) as "total_packets_dropped!: i64"
FROM (
SELECT
date_utc,
SUM(packets_received) as packets_received,
SUM(packets_sent) as packets_sent,
SUM(packets_dropped) as packets_dropped,
SUM(total_stake) as total_stake
FROM mixnode_daily_stats
GROUP BY date_utc
ORDER BY date_utc DESC
LIMIT 30
n.total_stake,
n.packets_received,
n.packets_sent,
n.packets_dropped
FROM nym_node_daily_mixing_stats n
UNION ALL
SELECT
m.date_utc,
m.total_stake,
m.packets_received,
m.packets_sent,
m.packets_dropped
FROM mixnode_daily_stats m
LEFT JOIN nym_node_daily_mixing_stats ON m.mix_id = nym_node_daily_mixing_stats.node_id
WHERE nym_node_daily_mixing_stats.node_id IS NULL
)
GROUP BY date_utc
ORDER BY date_utc
"#
ORDER BY date_utc DESC
LIMIT 30
OFFSET ?
"#,
offset
)
.fetch(&mut *conn)
.try_collect::<Vec<DailyStats>>()
@@ -121,57 +127,3 @@ pub(crate) async fn get_daily_stats(pool: &DbPool) -> anyhow::Result<Vec<DailySt
Ok(items)
}
/// Ensure all mixnodes that are set as bonded, are still bonded
pub(crate) async fn ensure_mixnodes_still_bonded(
pool: &DbPool,
mixnodes: &[MixNodeBondAnnotated],
) -> anyhow::Result<usize> {
let bonded_mixnodes_rows = get_all_bonded_mixnodes_row_ids_by_status(pool, true).await?;
let unbonded_mixnodes_rows = bonded_mixnodes_rows.iter().filter(|v| {
!mixnodes
.iter()
.any(|bonded| *bonded.mixnode_details.bond_information.identity() == v.identity_key)
});
let recently_unbonded_mixnodes = unbonded_mixnodes_rows.to_owned().count();
let last_updated_utc = chrono::offset::Utc::now().timestamp();
let mut transaction = pool.begin().await?;
for row in unbonded_mixnodes_rows {
sqlx::query!(
"UPDATE mixnodes
SET bonded = ?, last_updated_utc = ?
WHERE id = ?;",
false,
last_updated_utc,
row.id,
)
.execute(&mut *transaction)
.await?;
}
transaction.commit().await?;
Ok(recently_unbonded_mixnodes)
}
async fn get_all_bonded_mixnodes_row_ids_by_status(
pool: &DbPool,
status: bool,
) -> anyhow::Result<Vec<BondedStatusDto>> {
let mut conn = pool.acquire().await?;
let items = sqlx::query_as!(
BondedStatusDto,
r#"SELECT
id as "id!",
identity_key as "identity_key!",
bonded as "bonded: bool"
FROM mixnodes
WHERE bonded = ?"#,
status,
)
.fetch(&mut *conn)
.try_collect::<Vec<_>>()
.await?;
Ok(items)
}
@@ -2,19 +2,19 @@ mod gateways;
mod gateways_stats;
mod misc;
mod mixnodes;
mod nym_nodes;
mod packet_stats;
pub(crate) mod scraper;
mod summary;
pub(crate) mod testruns;
pub(crate) use gateways::{
ensure_gateways_still_bonded, get_all_gateways, insert_gateways, select_gateway_identity,
write_blacklisted_gateways_to_db,
};
pub(crate) use misc::insert_summaries;
pub(crate) use mixnodes::{
ensure_mixnodes_still_bonded, get_all_mixnodes, get_daily_stats, insert_mixnodes,
};
pub(crate) use scraper::fetch_active_nodes;
pub(crate) use summary::{get_summary, get_summary_history};
pub(crate) use gateways::{get_all_gateways, insert_gateways, select_gateway_identity};
pub(crate) use gateways_stats::{delete_old_records, get_sessions_stats, insert_session_records};
pub(crate) use misc::insert_summaries;
pub(crate) use mixnodes::{get_all_mixnodes, get_daily_stats, insert_mixnodes};
pub(crate) use nym_nodes::{get_nym_nodes, insert_nym_nodes};
pub(crate) use packet_stats::{
get_raw_node_stats, insert_daily_node_stats, insert_node_packet_stats,
};
pub(crate) use scraper::{get_mixing_nodes_for_scraping, insert_scraped_node_description};
pub(crate) use summary::{get_summary, get_summary_history};
@@ -0,0 +1,107 @@
use std::collections::HashMap;
use futures_util::TryStreamExt;
use nym_validator_client::{client::NymNodeDetails, nym_api::SkimmedNode};
use tracing::instrument;
use crate::{
db::{
models::{NymNodeDto, NymNodeInsertRecord},
DbPool,
},
monitor::decimal_to_i64,
};
pub(crate) async fn get_nym_nodes(pool: &DbPool) -> anyhow::Result<Vec<SkimmedNode>> {
let mut conn = pool.acquire().await?;
let items = sqlx::query_as!(
NymNodeDto,
r#"SELECT
node_id,
ed25519_identity_pubkey,
total_stake,
ip_addresses as "ip_addresses!: serde_json::Value",
mix_port,
x25519_sphinx_pubkey,
node_role as "node_role: serde_json::Value",
supported_roles as "supported_roles: serde_json::Value",
entry as "entry: serde_json::Value",
performance
FROM
nym_nodes
"#,
)
.fetch(&mut *conn)
.try_collect::<Vec<NymNodeDto>>()
.await?;
let mut skimmed_nodes = Vec::new();
for item in items {
let node_id = item.node_id;
match SkimmedNode::try_from(item) {
Ok(node) => skimmed_nodes.push(node),
Err(e) => {
tracing::warn!("Failed to decode node_id={}: {}", node_id, e);
}
}
}
Ok(skimmed_nodes)
}
#[instrument(level = "debug", skip_all)]
pub(crate) async fn insert_nym_nodes(
pool: &DbPool,
nym_nodes: Vec<SkimmedNode>,
bonded_node_info: &HashMap<u32, NymNodeDetails>,
) -> anyhow::Result<()> {
let mut conn = pool.acquire().await?;
for nym_node in nym_nodes.into_iter() {
let total_stake = bonded_node_info
.get(&nym_node.node_id)
.map(|details| decimal_to_i64(details.total_stake()))
.unwrap_or(0);
let record = NymNodeInsertRecord::new(nym_node, total_stake)?;
// https://www.sqlite.org/lang_upsert.html
sqlx::query!(
"INSERT INTO nym_nodes
(node_id, ed25519_identity_pubkey,
total_stake,
ip_addresses, mix_port,
x25519_sphinx_pubkey, node_role,
supported_roles, entry,
performance, last_updated_utc
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(node_id) DO UPDATE SET
ed25519_identity_pubkey=excluded.ed25519_identity_pubkey,
ip_addresses=excluded.ip_addresses,
mix_port=excluded.mix_port,
x25519_sphinx_pubkey=excluded.x25519_sphinx_pubkey,
node_role=excluded.node_role,
supported_roles=excluded.supported_roles,
entry=excluded.entry,
performance=excluded.performance,
last_updated_utc=excluded.last_updated_utc
;",
record.node_id,
record.ed25519_identity_pubkey,
record.total_stake,
record.ip_addresses,
record.mix_port,
record.x25519_sphinx_pubkey,
record.node_role,
record.supported_roles,
record.entry,
record.performance,
record.last_updated_utc,
)
.execute(&mut *conn)
.await?;
}
Ok(())
}
@@ -0,0 +1,188 @@
use crate::db::{
models::{MixingNodeKind, NodeStats, ScraperNodeInfo},
DbPool,
};
use anyhow::Result;
pub(crate) async fn insert_node_packet_stats(
pool: &DbPool,
node_id: i64,
node_kind: &MixingNodeKind,
stats: &NodeStats,
timestamp_utc: i64,
) -> Result<()> {
let mut conn = pool.acquire().await?;
match node_kind {
MixingNodeKind::LegacyMixnode => {
sqlx::query!(
r#"
INSERT INTO mixnode_packet_stats_raw (
mix_id, timestamp_utc, packets_received, packets_sent, packets_dropped
) VALUES (?, ?, ?, ?, ?)
"#,
node_id,
timestamp_utc,
stats.packets_received,
stats.packets_sent,
stats.packets_dropped,
)
.execute(&mut *conn)
.await?;
}
MixingNodeKind::NymNode => {
sqlx::query!(
r#"
INSERT INTO nym_nodes_packet_stats_raw (
node_id, timestamp_utc, packets_received, packets_sent, packets_dropped
) VALUES (?, ?, ?, ?, ?)
"#,
node_id,
timestamp_utc,
stats.packets_received,
stats.packets_sent,
stats.packets_dropped,
)
.execute(&mut *conn)
.await?;
}
}
Ok(())
}
pub(crate) async fn get_raw_node_stats(
pool: &DbPool,
node: &ScraperNodeInfo,
) -> Result<Option<NodeStats>> {
let mut conn = pool.acquire().await?;
let packets = match node.node_kind {
// if no packets are found, it's fine to assume 0 because that's also
// SQL default value if none provided
MixingNodeKind::LegacyMixnode => {
sqlx::query_as!(
NodeStats,
r#"
SELECT
COALESCE(packets_received, 0) as packets_received,
COALESCE(packets_sent, 0) as packets_sent,
COALESCE(packets_dropped, 0) as packets_dropped
FROM mixnode_packet_stats_raw
WHERE mix_id = ?
ORDER BY timestamp_utc DESC
LIMIT 1 OFFSET 1
"#,
node.node_id
)
.fetch_optional(&mut *conn)
.await?
}
MixingNodeKind::NymNode => {
sqlx::query_as!(
NodeStats,
r#"
SELECT
COALESCE(packets_received, 0) as packets_received,
COALESCE(packets_sent, 0) as packets_sent,
COALESCE(packets_dropped, 0) as packets_dropped
FROM nym_nodes_packet_stats_raw
WHERE node_id = ?
ORDER BY timestamp_utc DESC
LIMIT 1 OFFSET 1
"#,
node.node_id
)
.fetch_optional(&mut *conn)
.await?
}
};
Ok(packets)
}
pub(crate) async fn insert_daily_node_stats(
pool: &DbPool,
node: &ScraperNodeInfo,
date_utc: &str,
packets: NodeStats,
) -> Result<()> {
let mut conn = pool.acquire().await?;
match node.node_kind {
MixingNodeKind::LegacyMixnode => {
let total_stake = sqlx::query_scalar!(
r#"
SELECT
total_stake
FROM mixnodes
WHERE mix_id = ?
"#,
node.node_id
)
.fetch_one(&mut *conn)
.await?;
sqlx::query!(
r#"
INSERT INTO mixnode_daily_stats (
mix_id, date_utc,
total_stake, packets_received,
packets_sent, packets_dropped
) VALUES (?, ?, ?, ?, ?, ?)
ON CONFLICT(mix_id, date_utc) DO UPDATE SET
total_stake = excluded.total_stake,
packets_received = mixnode_daily_stats.packets_received + excluded.packets_received,
packets_sent = mixnode_daily_stats.packets_sent + excluded.packets_sent,
packets_dropped = mixnode_daily_stats.packets_dropped + excluded.packets_dropped
"#,
node.node_id,
date_utc,
total_stake,
packets.packets_received,
packets.packets_sent,
packets.packets_dropped,
)
.execute(&mut *conn)
.await?;
}
MixingNodeKind::NymNode => {
let total_stake = sqlx::query_scalar!(
r#"
SELECT
total_stake
FROM nym_nodes
WHERE node_id = ?
"#,
node.node_id
)
.fetch_one(&mut *conn)
.await?;
sqlx::query!(
r#"
INSERT INTO nym_node_daily_mixing_stats (
node_id, date_utc,
total_stake, packets_received,
packets_sent, packets_dropped
) VALUES (?, ?, ?, ?, ?, ?)
ON CONFLICT(node_id, date_utc) DO UPDATE SET
total_stake = nym_node_daily_mixing_stats.total_stake + excluded.total_stake,
packets_received = nym_node_daily_mixing_stats.packets_received + excluded.packets_received,
packets_sent = nym_node_daily_mixing_stats.packets_sent + excluded.packets_sent,
packets_dropped = nym_node_daily_mixing_stats.packets_dropped + excluded.packets_dropped
"#,
node.node_id,
date_utc,
total_stake,
packets.packets_received,
packets.packets_sent,
packets.packets_dropped,
)
.execute(&mut *conn)
.await?;
}
}
Ok(())
}
@@ -1,20 +1,149 @@
use crate::db::{models::ScraperNodeInfo, DbPool};
use crate::{
db::{
models::{MixingNodeKind, ScraperNodeInfo},
queries, DbPool,
},
mixnet_scraper::helpers::NodeDescriptionResponse,
};
use anyhow::Result;
use chrono::Utc;
pub(crate) async fn get_mixing_nodes_for_scraping(pool: &DbPool) -> Result<Vec<ScraperNodeInfo>> {
let mut nodes_to_scrape = Vec::new();
queries::get_nym_nodes(pool)
.await?
.into_iter()
.for_each(|node| {
nodes_to_scrape.push(ScraperNodeInfo {
node_id: node.node_id.into(),
node_kind: MixingNodeKind::NymNode,
hosts: node
.ip_addresses
.into_iter()
.map(|ip| ip.to_string())
.collect::<Vec<_>>(),
http_api_port: node.mix_port.into(),
})
});
tracing::debug!("Fetched {} 🌟 nym nodes", nodes_to_scrape.len());
pub(crate) async fn fetch_active_nodes(pool: &DbPool) -> Result<Vec<ScraperNodeInfo>> {
let mut conn = pool.acquire().await?;
let nodes = sqlx::query_as!(
ScraperNodeInfo,
let mixnodes = sqlx::query!(
r#"
SELECT mix_id as node_id, host, http_api_port
FROM mixnodes
SELECT mix_id as node_id, host, http_api_port
FROM mixnodes
WHERE bonded = true
"#
)
.fetch_all(&mut *conn)
.await?;
drop(conn);
Ok(nodes)
tracing::debug!("Fetched {} 🦖 mixnodes", nodes_to_scrape.len());
let mut duplicates = 0;
let mut legacy_not_in_nym_node_list = 0;
let total_legacy_mixnodes = mixnodes.len();
for mixnode in mixnodes {
if nodes_to_scrape
.iter()
.all(|node| node.node_id != mixnode.node_id)
{
legacy_not_in_nym_node_list += 1;
} else {
duplicates += 1;
}
// technically, mixnodes shouldn't be in nym_nodes table, but it's
// possible due to polyfilling on Nym API
if nodes_to_scrape
.iter()
.all(|node| node.node_id != mixnode.node_id)
{
nodes_to_scrape.push(ScraperNodeInfo {
node_id: mixnode.node_id,
node_kind: MixingNodeKind::LegacyMixnode,
hosts: vec![mixnode.host],
http_api_port: mixnode.http_api_port,
})
}
}
tracing::debug!(
"{}/{} legacy mixnodes already included in nym_node list",
duplicates,
total_legacy_mixnodes
);
tracing::debug!(
"{}/{} legacy mixnodes NOT included in nym_node list",
legacy_not_in_nym_node_list,
total_legacy_mixnodes
);
tracing::debug!("In total: {} 🌟+🦖 mixing nodes", nodes_to_scrape.len());
Ok(nodes_to_scrape)
}
// TODO: add stuff for gateways
pub(crate) async fn insert_scraped_node_description(
pool: &DbPool,
node_kind: &MixingNodeKind,
node_id: i64,
description: &NodeDescriptionResponse,
) -> Result<()> {
let timestamp = Utc::now().timestamp();
let mut conn = pool.acquire().await?;
match node_kind {
MixingNodeKind::LegacyMixnode => {
sqlx::query!(
r#"
INSERT INTO mixnode_description (
mix_id, moniker, website, security_contact, details, last_updated_utc
) VALUES (?, ?, ?, ?, ?, ?)
ON CONFLICT (mix_id) DO UPDATE SET
moniker = excluded.moniker,
website = excluded.website,
security_contact = excluded.security_contact,
details = excluded.details,
last_updated_utc = excluded.last_updated_utc
"#,
node_id,
description.moniker,
description.website,
description.security_contact,
description.details,
timestamp,
)
.execute(&mut *conn)
.await?;
}
MixingNodeKind::NymNode => {
sqlx::query!(
r#"
INSERT INTO nym_node_descriptions (
node_id, moniker, website, security_contact, details, last_updated_utc
) VALUES (?, ?, ?, ?, ?, ?)
ON CONFLICT (node_id) DO UPDATE SET
moniker = excluded.moniker,
website = excluded.website,
security_contact = excluded.security_contact,
details = excluded.details,
last_updated_utc = excluded.last_updated_utc
"#,
node_id,
description.moniker,
description.website,
description.security_contact,
description.details,
timestamp,
)
.execute(&mut *conn)
.await?;
}
}
Ok(())
}
@@ -6,15 +6,12 @@ use tracing::error;
use crate::{
db::{
models::{
gateway::{
GatewaySummary, GatewaySummaryBlacklisted, GatewaySummaryBonded,
GatewaySummaryHistorical,
},
mixnode::{
MixnodeSummary, MixnodeSummaryBlacklisted, MixnodeSummaryBonded,
MixnodeSummaryHistorical,
},
NetworkSummary, SummaryDto, SummaryHistoryDto,
gateway::{GatewaySummary, GatewaySummaryBonded, GatewaySummaryHistorical},
mixnode::{MixingNodesSummary, MixnodeSummary, MixnodeSummaryHistorical},
NetworkSummary, SummaryDto, SummaryHistoryDto, ASSIGNED_ENTRY_COUNT,
ASSIGNED_EXIT_COUNT, ASSIGNED_MIXING_COUNT, GATEWAYS_BONDED_COUNT,
GATEWAYS_HISTORICAL_COUNT, MIXNODES_HISTORICAL_COUNT, MIXNODES_LEGACY_COUNT,
NYMNODES_DESCRIBED_COUNT, NYMNODE_COUNT,
},
DbPool,
},
@@ -76,16 +73,6 @@ pub(crate) async fn get_summary(pool: &DbPool) -> HttpResult<NetworkSummary> {
}
async fn from_summary_dto(items: Vec<SummaryDto>) -> HttpResult<NetworkSummary> {
const MIXNODES_BONDED_COUNT: &str = "mixnodes.bonded.count";
const MIXNODES_BONDED_ACTIVE: &str = "mixnodes.bonded.active";
const MIXNODES_BONDED_INACTIVE: &str = "mixnodes.bonded.inactive";
const MIXNODES_BONDED_RESERVE: &str = "mixnodes.bonded.reserve";
const MIXNODES_BLACKLISTED_COUNT: &str = "mixnodes.blacklisted.count";
const GATEWAYS_BONDED_COUNT: &str = "gateways.bonded.count";
const GATEWAYS_BLACKLISTED_COUNT: &str = "gateways.blacklisted.count";
const MIXNODES_HISTORICAL_COUNT: &str = "mixnodes.historical.count";
const GATEWAYS_HISTORICAL_COUNT: &str = "gateways.historical.count";
// convert database rows into a map by key
let mut map = HashMap::new();
for item in items {
@@ -94,15 +81,15 @@ async fn from_summary_dto(items: Vec<SummaryDto>) -> HttpResult<NetworkSummary>
// check we have all the keys we are expecting, and build up a map of errors for missing one
let keys = [
NYMNODE_COUNT,
ASSIGNED_MIXING_COUNT,
MIXNODES_LEGACY_COUNT,
NYMNODES_DESCRIBED_COUNT,
GATEWAYS_BONDED_COUNT,
GATEWAYS_HISTORICAL_COUNT,
GATEWAYS_BLACKLISTED_COUNT,
MIXNODES_BLACKLISTED_COUNT,
MIXNODES_BONDED_ACTIVE,
MIXNODES_BONDED_COUNT,
MIXNODES_BONDED_INACTIVE,
MIXNODES_BONDED_RESERVE,
ASSIGNED_ENTRY_COUNT,
ASSIGNED_EXIT_COUNT,
MIXNODES_HISTORICAL_COUNT,
GATEWAYS_HISTORICAL_COUNT,
];
let mut errors: Vec<&str> = vec![];
@@ -119,22 +106,17 @@ async fn from_summary_dto(items: Vec<SummaryDto>) -> HttpResult<NetworkSummary>
}
// strip the options and use default values (anything missing is trapped above)
let mixnodes_bonded_count: SummaryDto =
map.get(MIXNODES_BONDED_COUNT).cloned().unwrap_or_default();
let mixnodes_bonded_active: SummaryDto =
map.get(MIXNODES_BONDED_ACTIVE).cloned().unwrap_or_default();
let mixnodes_bonded_inactive: SummaryDto = map
.get(MIXNODES_BONDED_INACTIVE)
.cloned()
.unwrap_or_default();
let mixnodes_bonded_reserve: SummaryDto = map
.get(MIXNODES_BONDED_RESERVE)
.cloned()
.unwrap_or_default();
let mixnodes_blacklisted_count: SummaryDto = map
.get(MIXNODES_BLACKLISTED_COUNT)
let total_nodes: SummaryDto = map.get(NYMNODE_COUNT).cloned().unwrap_or_default();
let assigned_mixing_count: SummaryDto =
map.get(ASSIGNED_MIXING_COUNT).cloned().unwrap_or_default();
let assigned_entry: SummaryDto = map.get(ASSIGNED_ENTRY_COUNT).cloned().unwrap_or_default();
let assigned_exit: SummaryDto = map.get(ASSIGNED_EXIT_COUNT).cloned().unwrap_or_default();
let self_described: SummaryDto = map
.get(NYMNODES_DESCRIBED_COUNT)
.cloned()
.unwrap_or_default();
let legacy_mixnodes_count: SummaryDto =
map.get(MIXNODES_LEGACY_COUNT).cloned().unwrap_or_default();
let gateways_bonded_count: SummaryDto =
map.get(GATEWAYS_BONDED_COUNT).cloned().unwrap_or_default();
let mixnodes_historical_count: SummaryDto = map
@@ -145,23 +127,15 @@ async fn from_summary_dto(items: Vec<SummaryDto>) -> HttpResult<NetworkSummary>
.get(GATEWAYS_HISTORICAL_COUNT)
.cloned()
.unwrap_or_default();
let gateways_blacklisted_count: SummaryDto = map
.get(GATEWAYS_BLACKLISTED_COUNT)
.cloned()
.unwrap_or_default();
Ok(NetworkSummary {
total_nodes: to_count_i32(&total_nodes),
mixnodes: MixnodeSummary {
bonded: MixnodeSummaryBonded {
count: to_count_i32(&mixnodes_bonded_count),
active: to_count_i32(&mixnodes_bonded_active),
reserve: to_count_i32(&mixnodes_bonded_reserve),
inactive: to_count_i32(&mixnodes_bonded_inactive),
last_updated_utc: to_timestamp(&mixnodes_bonded_count),
},
blacklisted: MixnodeSummaryBlacklisted {
count: to_count_i32(&mixnodes_blacklisted_count),
last_updated_utc: to_timestamp(&mixnodes_blacklisted_count),
bonded: MixingNodesSummary {
count: to_count_i32(&assigned_mixing_count),
self_described: to_count_i32(&self_described),
legacy: to_count_i32(&legacy_mixnodes_count),
last_updated_utc: to_timestamp(&assigned_mixing_count),
},
historical: MixnodeSummaryHistorical {
count: to_count_i32(&mixnodes_historical_count),
@@ -171,12 +145,10 @@ async fn from_summary_dto(items: Vec<SummaryDto>) -> HttpResult<NetworkSummary>
gateways: GatewaySummary {
bonded: GatewaySummaryBonded {
count: to_count_i32(&gateways_bonded_count),
entry: to_count_i32(&assigned_entry),
exit: to_count_i32(&assigned_exit),
last_updated_utc: to_timestamp(&gateways_bonded_count),
},
blacklisted: GatewaySummaryBlacklisted {
count: to_count_i32(&gateways_blacklisted_count),
last_updated_utc: to_timestamp(&gateways_blacklisted_count),
},
historical: GatewaySummaryHistorical {
count: to_count_i32(&gateways_historical_count),
last_updated_utc: to_timestamp(&gateways_historical_count),
@@ -77,15 +77,33 @@ async fn get_mixnodes(
}
}
#[derive(Deserialize, IntoParams)]
#[into_params(parameter_in = Query)]
struct MixStatsQueryParams {
offset: Option<i64>,
}
#[utoipa::path(
tag = "Mixnodes",
get,
path = "/v2/mixnodes/stats",
params(
MixStatsQueryParams
),
responses(
(status = 200, body = Vec<DailyStats>)
)
)]
async fn get_stats(State(state): State<AppState>) -> HttpResult<Json<Vec<DailyStats>>> {
let stats = state.cache().get_mixnode_stats(state.db_pool()).await;
Ok(Json(stats))
#[instrument(level = "debug", skip(state))]
async fn get_stats(
Query(MixStatsQueryParams { offset }): Query<MixStatsQueryParams>,
State(state): State<AppState>,
) -> HttpResult<Json<Vec<DailyStats>>> {
let offset = offset.unwrap_or(0);
let last_30_days = state
.cache()
.get_mixnode_stats(state.db_pool(), offset)
.await;
Ok(Json(last_30_days))
}
@@ -8,7 +8,6 @@ pub(crate) use nym_node_status_client::models::TestrunAssignment;
pub struct Gateway {
pub gateway_identity_key: String,
pub bonded: bool,
pub blacklisted: bool,
pub performance: u8,
pub self_described: Option<serde_json::Value>,
pub explorer_pretty_bond: Option<serde_json::Value>,
@@ -38,7 +37,6 @@ pub struct GatewaySkinny {
pub struct Mixnode {
pub mix_id: u32,
pub bonded: bool,
pub blacklisted: bool,
pub is_dp_delegatee: bool,
pub total_stake: i64,
pub full_details: Option<serde_json::Value>,
@@ -17,13 +17,20 @@ pub(crate) async fn start_http_api(
nym_http_cache_ttl: u64,
agent_key_list: Vec<PublicKey>,
agent_max_count: i64,
hm_url: String,
) -> anyhow::Result<ShutdownHandles> {
let router_builder = RouterBuilder::with_default_routes();
let state = AppState::new(db_pool, nym_http_cache_ttl, agent_key_list, agent_max_count);
let state = AppState::new(
db_pool,
nym_http_cache_ttl,
agent_key_list,
agent_max_count,
hm_url,
)
.await;
let router = router_builder.with_state(state);
// TODO dz do we need this to be configurable?
let bind_addr = format!("0.0.0.0:{}", http_port);
tracing::info!("Binding server to {bind_addr}");
let server = router.build_server(bind_addr).await?;
@@ -20,15 +20,16 @@ pub(crate) struct AppState {
}
impl AppState {
pub(crate) fn new(
pub(crate) async fn new(
db_pool: DbPool,
cache_ttl: u64,
agent_key_list: Vec<PublicKey>,
agent_max_count: i64,
hm_url: String,
) -> Self {
Self {
db_pool,
cache: HttpCache::new(cache_ttl),
cache: HttpCache::new(cache_ttl, hm_url).await,
agent_key_list,
agent_max_count,
}
@@ -51,6 +52,90 @@ impl AppState {
}
}
#[derive(Debug, Clone)]
struct HistoricMixingStats {
historic_stats: Vec<DailyStats>,
}
impl HistoricMixingStats {
/// Collect historic stats only on initialization. From this point onwards,
/// service will collect its own stats
async fn init(hm_url: String) -> Self {
tracing::info!("Fetching historic mixnode stats from {}", hm_url);
let target_url = format!("{}/v2/mixnodes/stats", hm_url);
if let Ok(response) = reqwest::get(&target_url)
.await
.and_then(|res| res.error_for_status())
.inspect_err(|err| tracing::error!("Failed to fetch cache from HM: {}", err))
{
if let Ok(mut daily_stats) = response.json::<Vec<DailyStats>>().await {
// sorting required for seamless comparison later (descending, newest first)
daily_stats.sort_by(|left, right| right.date_utc.cmp(&left.date_utc));
tracing::info!(
"Successfully fetched {} historic entries from {}",
daily_stats.len(),
hm_url
);
return Self {
historic_stats: daily_stats,
};
}
};
tracing::warn!("Failed to get historic daily stats from {}", hm_url);
Self {
historic_stats: Vec::new(),
}
}
/// polyfill with historical data obtained from Harbour Master
fn merge_with_historic_stats(&self, mut new_stats: Vec<DailyStats>) -> Vec<DailyStats> {
// newest first
new_stats.sort_by(|left, right| right.date_utc.cmp(&left.date_utc));
// historic stats are only used for dates when we don't have new data
let oldest_date_in_new_stats = new_stats
.last()
.map(|day| day.date_utc.to_owned())
.unwrap_or(String::from("1900-01-01"));
// given 2 arrays
// index historic_stats new_stats
// 0 30-01 31-01
// 1 29-01 30-01
// 2 28-01
// ...
// N 01-01
// cutoff point would be at historic_stats[1]
// (first date smaller than oldest we've already got)
if let Some(cutoff) = self
.historic_stats
.iter()
.position(|elem| elem.date_utc < oldest_date_in_new_stats)
{
// missing data = (all historic data) - (however many days we already have)
let missing_data = self.historic_stats.iter().skip(cutoff).cloned();
// extend new data with missing days
tracing::debug!(
"Polyfilled with {} historic records from {:?} to {:?}",
missing_data.len(),
self.historic_stats.last(),
self.historic_stats.get(cutoff)
);
new_stats.extend(missing_data);
// oldest first
new_stats.into_iter().rev().collect::<Vec<_>>()
} else {
// if all historic data is older than what we've got, don't use it
new_stats
}
}
}
static GATEWAYS_LIST_KEY: &str = "gateways";
static MIXNODES_LIST_KEY: &str = "mixnodes";
static MIXSTATS_LIST_KEY: &str = "mixstats";
@@ -64,10 +149,11 @@ pub(crate) struct HttpCache {
mixstats: Cache<String, Arc<RwLock<Vec<DailyStats>>>>,
history: Cache<String, Arc<RwLock<Vec<SummaryHistory>>>>,
session_stats: Cache<String, Arc<RwLock<Vec<SessionStats>>>>,
mixnode_historic_daily_stats: HistoricMixingStats,
}
impl HttpCache {
pub fn new(ttl_seconds: u64) -> Self {
pub async fn new(ttl_seconds: u64, hm_url: String) -> Self {
HttpCache {
gateways: Cache::builder()
.max_capacity(2)
@@ -89,6 +175,7 @@ impl HttpCache {
.max_capacity(2)
.time_to_live(Duration::from_secs(ttl_seconds))
.build(),
mixnode_historic_daily_stats: HistoricMixingStats::init(hm_url).await,
}
}
@@ -114,12 +201,13 @@ impl HttpCache {
pub async fn get_gateway_list(&self, db: &DbPool) -> Vec<Gateway> {
match self.gateways.get(GATEWAYS_LIST_KEY).await {
Some(guard) => {
tracing::trace!("Fetching from cache...");
let read_lock = guard.read().await;
read_lock.clone()
}
None => {
// the key is missing so populate it
tracing::warn!("No gateways in cache, refreshing cache from DB...");
tracing::trace!("No gateways in cache, refreshing cache from DB...");
let gateways = crate::db::queries::get_all_gateways(db)
.await
@@ -157,11 +245,12 @@ impl HttpCache {
pub async fn get_mixnodes_list(&self, db: &DbPool) -> Vec<Mixnode> {
match self.mixnodes.get(MIXNODES_LIST_KEY).await {
Some(guard) => {
tracing::trace!("Fetching from cache...");
let read_lock = guard.read().await;
read_lock.clone()
}
None => {
tracing::warn!("No mixnodes in cache, refreshing cache from DB...");
tracing::trace!("No mixnodes in cache, refreshing cache from DB...");
let mixnodes = crate::db::queries::get_all_mixnodes(db)
.await
@@ -196,16 +285,22 @@ impl HttpCache {
.await
}
pub async fn get_mixnode_stats(&self, db: &DbPool) -> Vec<DailyStats> {
pub async fn get_mixnode_stats(&self, db: &DbPool, offset: i64) -> Vec<DailyStats> {
match self.mixstats.get(MIXSTATS_LIST_KEY).await {
Some(guard) => {
let read_lock = guard.read().await;
read_lock.to_vec()
}
None => {
let mixnode_stats = crate::db::queries::get_daily_stats(db)
let new_node_stats = crate::db::queries::get_daily_stats(db, offset)
.await
.unwrap_or_default();
// for every day that's missing, fill it with cached historic data
let mut mixnode_stats = self
.mixnode_historic_daily_stats
.merge_with_historic_stats(new_node_stats);
mixnode_stats.truncate(30);
self.upsert_mixnode_stats(mixnode_stats.clone()).await;
mixnode_stats
}
@@ -33,6 +33,7 @@ pub(crate) fn setup_tracing_logger() -> anyhow::Result<()> {
"tendermint_rpc",
"tower_http",
"axum",
"html5ever",
];
for crate_name in warn_crates {
filter = filter.add_directive(directive_checked(format!("{}=warn", crate_name))?);
@@ -40,28 +41,6 @@ pub(crate) fn setup_tracing_logger() -> anyhow::Result<()> {
let log_level_hint = filter.max_level_hint();
// debug or higher granularity (e.g. trace)
let debug_or_higher = std::cmp::max(
log_level_hint.unwrap_or(LevelFilter::DEBUG),
LevelFilter::DEBUG,
);
filter = filter.add_directive(directive_checked(format!(
"nym_bin_common={}",
debug_or_higher
))?);
filter = filter.add_directive(directive_checked(format!(
"nym_explorer_client={}",
debug_or_higher
))?);
filter = filter.add_directive(directive_checked(format!(
"nym_network_defaults={}",
debug_or_higher
))?);
filter = filter.add_directive(directive_checked(format!(
"nym_validator_client={}",
debug_or_higher
))?);
log_builder.with_env_filter(filter).init();
tracing::info!("Log level: {:?}", log_level_hint);
@@ -35,7 +35,6 @@ async fn main() -> anyhow::Result<()> {
tokio::spawn(async move {
scraper.start().await;
});
tracing::info!("Started node scraper task");
// Start the monitor
let args_clone = args.clone();
@@ -67,6 +66,7 @@ async fn main() -> anyhow::Result<()> {
args.nym_http_cache_ttl,
agent_key_list.to_owned(),
args.max_agent_count,
args.hm_url,
)
.await
.expect("Failed to start server");
@@ -1,20 +1,18 @@
use crate::db::models::ScraperNodeInfo;
use crate::db::{
models::{NodeStats, ScraperNodeInfo},
queries::{
get_raw_node_stats, insert_daily_node_stats, insert_node_packet_stats,
insert_scraped_node_description,
},
};
use ammonia::Builder;
use anyhow::Result;
use chrono::{DateTime, Datelike, Utc};
use nym_network_defaults::DEFAULT_NYM_NODE_HTTP_PORT;
use reqwest;
use serde::{Deserialize, Serialize};
use sqlx::SqlitePool;
use std::time::Duration;
#[derive(Debug, Serialize, Deserialize)]
pub struct NodeStats {
pub packets_received: i64,
pub packets_sent: i64,
pub packets_dropped: i64,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct NodeDescriptionResponse {
pub moniker: Option<String>,
@@ -106,16 +104,7 @@ pub fn sanitize_description(description: NodeDescriptionResponse) -> NodeDescrip
pub async fn scrape_and_store_description(pool: &SqlitePool, node: &ScraperNodeInfo) -> Result<()> {
let client = build_client()?;
let mut urls = vec![
format!("http://{}:{DEFAULT_NYM_NODE_HTTP_PORT}", node.host),
format!("http://{}:8000", node.host),
format!("https://{}", node.host),
format!("http://{}", node.host),
];
if node.http_api_port != DEFAULT_NYM_NODE_HTTP_PORT as i64 {
urls.insert(0, format!("http://{}:{}", node.host, node.http_api_port));
}
let urls = node.contact_addresses();
let mut description = None;
let mut error = None;
@@ -140,31 +129,8 @@ pub async fn scrape_and_store_description(pool: &SqlitePool, node: &ScraperNodeI
})?;
let sanitized_description = sanitize_description(description);
let mut conn = pool.acquire().await?;
let timestamp = Utc::now().timestamp();
sqlx::query!(
r#"
INSERT INTO mixnode_description (
mix_id, moniker, website, security_contact, details, last_updated_utc
) VALUES (?, ?, ?, ?, ?, ?)
ON CONFLICT (mix_id) DO UPDATE SET
moniker = excluded.moniker,
website = excluded.website,
security_contact = excluded.security_contact,
details = excluded.details,
last_updated_utc = excluded.last_updated_utc
"#,
node.node_id,
sanitized_description.moniker,
sanitized_description.website,
sanitized_description.security_contact,
sanitized_description.details,
timestamp,
)
.execute(&mut *conn)
.await?;
insert_scraped_node_description(pool, &node.node_kind, node.node_id, &sanitized_description)
.await?;
Ok(())
}
@@ -174,16 +140,7 @@ pub async fn scrape_and_store_packet_stats(
node: &ScraperNodeInfo,
) -> Result<()> {
let client = build_client()?;
let mut urls = vec![
format!("http://{}:{DEFAULT_NYM_NODE_HTTP_PORT}", node.host),
format!("http://{}:8000", node.host),
format!("https://{}", node.host),
format!("http://{}", node.host),
];
if node.http_api_port != DEFAULT_NYM_NODE_HTTP_PORT as i64 {
urls.insert(0, format!("http://{}:{}", node.host, node.http_api_port));
}
let urls = node.contact_addresses();
let mut stats = None;
let mut error = None;
@@ -209,38 +166,20 @@ pub async fn scrape_and_store_packet_stats(
let timestamp = Utc::now();
let timestamp_utc = timestamp.timestamp();
let mut conn = pool.acquire().await?;
// Store raw stats
sqlx::query!(
r#"
INSERT INTO mixnode_packet_stats_raw (
mix_id, timestamp_utc, packets_received, packets_sent, packets_dropped
) VALUES (?, ?, ?, ?, ?)
"#,
node.node_id,
timestamp_utc,
stats.packets_received,
stats.packets_sent,
stats.packets_dropped,
)
.execute(&mut *conn)
.await?;
insert_node_packet_stats(pool, node.node_id, &node.node_kind, &stats, timestamp_utc).await?;
// Update daily stats
update_daily_stats(pool, node.node_id, timestamp, &stats).await?;
update_daily_stats(pool, node, timestamp, &stats).await?;
Ok(())
}
pub async fn update_daily_stats(
pool: &SqlitePool,
node_id: i64,
node: &ScraperNodeInfo,
timestamp: DateTime<Utc>,
current_stats: &NodeStats,
) -> Result<()> {
let mut conn = pool.acquire().await?;
let date_utc = format!(
"{:04}-{:02}-{:02}",
timestamp.year(),
@@ -248,67 +187,29 @@ pub async fn update_daily_stats(
timestamp.day()
);
let total_stake = sqlx::query_scalar!(
r#"
SELECT
total_stake
FROM mixnodes
WHERE mix_id = ?
"#,
node_id
)
.fetch_one(&mut *conn)
.await?;
// Get previous stats
let previous_stats = sqlx::query!(
r#"
SELECT packets_received, packets_sent, packets_dropped
FROM mixnode_packet_stats_raw
WHERE mix_id = ?
ORDER BY timestamp_utc DESC
LIMIT 1 OFFSET 1
"#,
node_id
)
.fetch_optional(&mut *conn)
.await?;
let previous_stats = get_raw_node_stats(pool, node).await?;
let (diff_received, diff_sent, diff_dropped) = if let Some(prev) = previous_stats {
(
calculate_packet_difference(
current_stats.packets_received,
prev.packets_received.unwrap_or(0),
),
calculate_packet_difference(current_stats.packets_sent, prev.packets_sent.unwrap_or(0)),
calculate_packet_difference(
current_stats.packets_dropped,
prev.packets_dropped.unwrap_or(0),
),
calculate_packet_difference(current_stats.packets_received, prev.packets_received),
calculate_packet_difference(current_stats.packets_sent, prev.packets_sent),
calculate_packet_difference(current_stats.packets_dropped, prev.packets_dropped),
)
} else {
(0, 0, 0) // No previous stats available
};
sqlx::query!(
r#"
INSERT INTO mixnode_daily_stats (
mix_id, date_utc, total_stake, packets_received, packets_sent, packets_dropped
) VALUES (?, ?, ?, ?, ?, ?)
ON CONFLICT(mix_id, date_utc) DO UPDATE SET
total_stake = excluded.total_stake,
packets_received = mixnode_daily_stats.packets_received + excluded.packets_received,
packets_sent = mixnode_daily_stats.packets_sent + excluded.packets_sent,
packets_dropped = mixnode_daily_stats.packets_dropped + excluded.packets_dropped
"#,
node_id,
date_utc,
total_stake,
diff_received,
diff_sent,
diff_dropped,
insert_daily_node_stats(
pool,
node,
&date_utc,
NodeStats {
packets_received: diff_received,
packets_sent: diff_sent,
packets_dropped: diff_dropped,
},
)
.execute(&mut *conn)
.await?;
Ok(())
@@ -5,13 +5,13 @@ pub mod helpers;
use anyhow::Result;
use helpers::{scrape_and_store_description, scrape_and_store_packet_stats};
use sqlx::SqlitePool;
use tracing::{debug, error, warn};
use tracing::{debug, error, instrument, warn};
use crate::db::models::ScraperNodeInfo;
use crate::db::queries::fetch_active_nodes;
use crate::db::queries::get_mixing_nodes_for_scraping;
const DESCRIPTION_SCRAPE_INTERVAL: Duration = Duration::from_secs(60 * 60 * 4); // 4 hours
const PACKET_SCRAPE_INTERVAL: Duration = Duration::from_secs(60 * 60); // 1 hour
const DESCRIPTION_SCRAPE_INTERVAL: Duration = Duration::from_secs(60 * 60 * 4);
const PACKET_SCRAPE_INTERVAL: Duration = Duration::from_secs(60 * 60);
const QUEUE_CHECK_INTERVAL: Duration = Duration::from_millis(250);
const MAX_CONCURRENT_TASKS: usize = 5;
@@ -45,8 +45,9 @@ impl Scraper {
tokio::spawn(async move {
loop {
if let Err(e) = Self::run_description_scraper(&pool, queue.clone()).await {
error!("Description scraper failed: {}", e);
error!(name: "description_scraper", "Description scraper failed: {}", e);
}
debug!(name: "description_scraper", "Sleeping for {}s", DESCRIPTION_SCRAPE_INTERVAL.as_secs());
tokio::time::sleep(DESCRIPTION_SCRAPE_INTERVAL).await;
}
});
@@ -56,21 +57,24 @@ impl Scraper {
let pool = self.pool.clone();
let queue = self.packet_queue.clone();
tracing::info!("Starting packet scraper");
tokio::spawn(async move {
loop {
if let Err(e) = Self::run_packet_scraper(&pool, queue.clone()).await {
error!("Packet scraper failed: {}", e);
error!(name: "packet_scraper", "Packet scraper failed: {}", e);
}
debug!(name: "packet_scraper", "Sleeping for {}s", PACKET_SCRAPE_INTERVAL.as_secs());
tokio::time::sleep(PACKET_SCRAPE_INTERVAL).await;
}
});
}
#[instrument(level = "info", name = "description_scraper", skip_all)]
async fn run_description_scraper(
pool: &SqlitePool,
queue: Arc<Mutex<Vec<ScraperNodeInfo>>>,
) -> Result<()> {
let nodes = fetch_active_nodes(pool).await?;
let nodes = get_mixing_nodes_for_scraping(pool).await?;
if let Ok(mut queue_lock) = queue.lock() {
queue_lock.extend(nodes);
} else {
@@ -82,12 +86,13 @@ impl Scraper {
Ok(())
}
#[instrument(level = "info", name = "packet_scraper", skip_all)]
async fn run_packet_scraper(
pool: &SqlitePool,
queue: Arc<Mutex<Vec<ScraperNodeInfo>>>,
) -> Result<()> {
let nodes = fetch_active_nodes(pool).await?;
tracing::info!("Found {} active nodes", nodes.len());
let nodes = get_mixing_nodes_for_scraping(pool).await?;
tracing::info!("Querying {} mixing nodes", nodes.len());
if let Ok(mut queue_lock) = queue.lock() {
queue_lock.extend(nodes);
} else {
@@ -125,7 +130,7 @@ impl Scraper {
let pool = pool.clone();
tokio::spawn(async move {
match Self::scrape_and_store_description(&pool, &node).await {
match scrape_and_store_description(&pool, &node).await {
Ok(_) => debug!(
"✅ Description task #{} for node {} complete",
task_id, node.node_id
@@ -170,7 +175,7 @@ impl Scraper {
let pool = pool.clone();
tokio::spawn(async move {
match Self::scrape_and_store_packet_stats(&pool, &node).await {
match scrape_and_store_packet_stats(&pool, &node).await {
Ok(_) => debug!(
"✅ Packet stats task #{} for node {} complete",
task_id, node.node_id
@@ -188,15 +193,4 @@ impl Scraper {
}
Ok(())
}
async fn scrape_and_store_description(pool: &SqlitePool, node: &ScraperNodeInfo) -> Result<()> {
scrape_and_store_description(pool, node).await
}
async fn scrape_and_store_packet_stats(
pool: &SqlitePool,
node: &ScraperNodeInfo,
) -> Result<()> {
scrape_and_store_packet_stats(pool, node).await
}
}
@@ -1,10 +1,9 @@
#![allow(deprecated)]
use crate::db::models::{
gateway, mixnode, GatewayRecord, MixnodeRecord, NetworkSummary, GATEWAYS_BLACKLISTED_COUNT,
GATEWAYS_BONDED_COUNT, GATEWAYS_HISTORICAL_COUNT, MIXNODES_BLACKLISTED_COUNT,
MIXNODES_BONDED_ACTIVE, MIXNODES_BONDED_COUNT, MIXNODES_BONDED_INACTIVE,
MIXNODES_BONDED_RESERVE, MIXNODES_HISTORICAL_COUNT,
gateway, mixnode, GatewayRecord, MixnodeRecord, NetworkSummary, ASSIGNED_ENTRY_COUNT,
ASSIGNED_EXIT_COUNT, ASSIGNED_MIXING_COUNT, GATEWAYS_BONDED_COUNT, GATEWAYS_HISTORICAL_COUNT,
MIXNODES_HISTORICAL_COUNT, MIXNODES_LEGACY_COUNT, NYMNODES_DESCRIBED_COUNT, NYMNODE_COUNT,
};
use crate::db::{queries, DbPool};
use crate::monitor::geodata::{Location, NodeGeoData};
@@ -16,7 +15,7 @@ use nym_validator_client::client::{NodeId, NymApiClientExt};
use nym_validator_client::models::{
LegacyDescribedMixNode, MixNodeBondAnnotated, NymNodeDescription,
};
use nym_validator_client::nym_nodes::SkimmedNode;
use nym_validator_client::nym_nodes::{NodeRole, SkimmedNode};
use nym_validator_client::nyxd::contract_traits::PagedMixnetQueryClient;
use nym_validator_client::nyxd::{AccountId, NyxdClient};
use nym_validator_client::NymApiClient;
@@ -102,104 +101,101 @@ impl Monitor {
let api_client =
NymApiClient::new_with_timeout(default_api_url, self.nym_api_client_timeout);
let all_nodes = api_client
let described_nodes = api_client
.get_all_described_nodes()
.await
.log_error("get_all_described_nodes")?;
tracing::debug!("Fetched {} total nodes", all_nodes.len());
tracing::info!("🟣 described nodes: {}", described_nodes.len());
let gateways = all_nodes
let gateways = described_nodes
.iter()
.filter(|node| node.description.declared_role.entry)
.collect::<Vec<_>>();
tracing::debug!(
"{}/{} with declared entry gateway capability",
gateways.len(),
all_nodes.len()
);
let mixnodes = all_nodes
.iter()
.filter(|node| node.description.declared_role.mixnode)
.collect::<Vec<_>>();
tracing::debug!(
"{}/{} with declared mixnode capability",
mixnodes.len(),
all_nodes.len()
);
let bonded_node_info = api_client
.get_all_bonded_nym_nodes()
.await?
.into_iter()
.map(|node| (node.bond_information.node_id, node.bond_information))
.map(|node| (node.bond_information.node_id, node))
// for faster reads
.collect::<HashMap<_, _>>();
let nym_nodes = api_client
.get_all_basic_nodes()
.await
.log_error("get_all_basic_nodes")?;
queries::insert_nym_nodes(&self.db_pool, nym_nodes.clone(), &bonded_node_info).await?;
let mut gateway_geodata = Vec::new();
for gateway in gateways.iter() {
if let Some(node_info) = bonded_node_info.get(&gateway.node_id) {
if let Some(node_details) = bonded_node_info.get(&gateway.node_id) {
let bond_info = &node_details.bond_information;
let gw_geodata = NodeGeoData {
identity_key: node_info.node.identity_key.to_owned(),
owner: node_info.owner.to_owned(),
pledge_amount: node_info.original_pledge.to_owned(),
identity_key: bond_info.node.identity_key.to_owned(),
owner: bond_info.owner.to_owned(),
pledge_amount: bond_info.original_pledge.to_owned(),
location: self.location_cached(gateway).await,
};
gateway_geodata.push(gw_geodata);
}
}
// contains performance data
let all_skimmed_nodes = api_client
.get_all_basic_nodes()
.await
.log_error("get_all_basic_nodes")?;
let gateways_blacklisted = all_skimmed_nodes
.iter()
.filter_map(|node| {
if node.performance.round_to_integer() <= 50 && node.supported_roles.entry {
Some(node.ed25519_identity_pubkey.to_base58_string())
} else {
None
}
})
.collect::<HashSet<_>>();
// Cached mixnodes don't include blacklisted nodes
// We need that to calculate the total locked tokens later
// TODO dz deprecated API, remove
let legacy_mixnodes = api_client
let mixnodes_detailed = api_client
.nym_api
.get_mixnodes_detailed_unfiltered()
.await
.log_error("get_mixnodes_detailed_unfiltered")?;
tracing::info!(
"🟣 mixnodes_detailed_unfiltered: {}",
mixnodes_detailed.len()
);
let mixnodes_detailed_set = mixnodes_detailed
.iter()
.map(|elem| elem.identity_key().to_owned())
.collect::<HashSet<_>>();
let mixnodes_legacy = nym_nodes
.iter()
.filter(|node| {
mixnodes_detailed_set.contains(&node.ed25519_identity_pubkey.to_base58_string())
})
.collect::<Vec<_>>();
let mixnodes_described = api_client
.nym_api
.get_mixnodes_described()
.await
.log_error("get_mixnodes_described")?;
let mixnodes_active = api_client
tracing::info!("🟣 mixnodes_described: {}", mixnodes_described.len());
let mixing_assigned_nodes = api_client
.nym_api
.get_basic_active_mixing_assigned_nodes(false, None, None)
.await
.log_error("get_active_mixnodes")?
.log_error("get_basic_active_mixing_assigned_nodes")?
.nodes
.data;
let delegation_program_members =
get_delegation_program_details(&self.network_details, &self.nyxd_addr).await?;
// keep stats for later
let count_bonded_mixnodes = mixnodes.len();
let assigned_entry_count = nym_nodes
.iter()
.filter(|elem| matches!(elem.role, NodeRole::EntryGateway))
.count();
let assigned_exit_count = nym_nodes
.iter()
.filter(|elem| matches!(elem.role, NodeRole::ExitGateway))
.count();
let count_bonded_gateways = gateways.len();
let count_bonded_mixnodes_active = mixnodes_active.len();
let assigned_mixing_count = mixing_assigned_nodes.len();
let count_legacy_mixnodes = mixnodes_legacy.len();
let gateway_records = self.prepare_gateway_data(
&gateways,
&gateways_blacklisted,
gateway_geodata,
all_skimmed_nodes,
)?;
let gateway_records = self.prepare_gateway_data(&gateways, gateway_geodata, &nym_nodes)?;
let pool = self.db_pool.clone();
queries::insert_gateways(&pool, gateway_records)
@@ -208,27 +204,8 @@ impl Monitor {
tracing::debug!("Gateway info written to DB!");
})?;
let count_gateways_blacklisted = gateways
.iter()
.filter(|gw| {
let gw_identity = gw.ed25519_identity_key().to_base58_string();
gateways_blacklisted.contains(&gw_identity)
})
.count();
if count_gateways_blacklisted > 0 {
queries::write_blacklisted_gateways_to_db(&pool, gateways_blacklisted.iter())
.await
.map(|_| {
tracing::debug!(
"Gateway blacklist info written to DB! {} blacklisted by Nym API",
count_gateways_blacklisted
)
})?;
}
let mixnode_records = self.prepare_mixnode_data(
&legacy_mixnodes,
&mixnodes_detailed,
mixnodes_described,
delegation_program_members,
)?;
@@ -238,20 +215,6 @@ impl Monitor {
tracing::debug!("Mixnode info written to DB!");
})?;
let count_mixnodes_blacklisted = legacy_mixnodes
.iter()
.filter(|elem| elem.blacklisted)
.count();
let recently_unbonded_gateways =
queries::ensure_gateways_still_bonded(&pool, &gateways).await?;
let recently_unbonded_mixnodes =
queries::ensure_mixnodes_still_bonded(&pool, &legacy_mixnodes).await?;
let count_bonded_mixnodes_reserve = 0; // TODO: NymAPI doesn't report the reserve set size
let count_bonded_mixnodes_inactive =
count_bonded_mixnodes.saturating_sub(count_bonded_mixnodes_active);
let (all_historical_gateways, all_historical_mixnodes) = calculate_stats(&pool).await?;
//
@@ -259,30 +222,28 @@ impl Monitor {
//
let nodes_summary = vec![
(MIXNODES_BONDED_COUNT, &count_bonded_mixnodes),
(MIXNODES_BONDED_ACTIVE, &count_bonded_mixnodes_active),
(MIXNODES_BONDED_INACTIVE, &count_bonded_mixnodes_inactive),
(MIXNODES_BONDED_RESERVE, &count_bonded_mixnodes_reserve),
(MIXNODES_BLACKLISTED_COUNT, &count_mixnodes_blacklisted),
(GATEWAYS_BONDED_COUNT, &count_bonded_gateways),
(MIXNODES_HISTORICAL_COUNT, &all_historical_mixnodes),
(GATEWAYS_HISTORICAL_COUNT, &all_historical_gateways),
(GATEWAYS_BLACKLISTED_COUNT, &count_gateways_blacklisted),
(NYMNODE_COUNT, nym_nodes.len()),
(ASSIGNED_MIXING_COUNT, assigned_mixing_count),
(MIXNODES_LEGACY_COUNT, count_legacy_mixnodes),
(NYMNODES_DESCRIBED_COUNT, described_nodes.len()),
(GATEWAYS_BONDED_COUNT, count_bonded_gateways),
(ASSIGNED_ENTRY_COUNT, assigned_entry_count),
(ASSIGNED_EXIT_COUNT, assigned_exit_count),
// TODO dz doesn't make sense, could make sense with historical Nym
// Nodes if we really need this data
(MIXNODES_HISTORICAL_COUNT, all_historical_mixnodes),
(GATEWAYS_HISTORICAL_COUNT, all_historical_gateways),
];
let last_updated = chrono::offset::Utc::now();
let last_updated_utc = last_updated.timestamp().to_string();
let network_summary = NetworkSummary {
total_nodes: nym_nodes.len().cast_checked()?,
mixnodes: mixnode::MixnodeSummary {
bonded: mixnode::MixnodeSummaryBonded {
count: count_bonded_mixnodes.cast_checked()?,
active: count_bonded_mixnodes_active.cast_checked()?,
inactive: count_bonded_mixnodes_inactive.cast_checked()?,
reserve: count_bonded_mixnodes_reserve.cast_checked()?,
last_updated_utc: last_updated_utc.to_owned(),
},
blacklisted: mixnode::MixnodeSummaryBlacklisted {
count: count_mixnodes_blacklisted.cast_checked()?,
bonded: mixnode::MixingNodesSummary {
count: assigned_mixing_count.cast_checked()?,
self_described: described_nodes.len().cast_checked()?,
legacy: count_legacy_mixnodes.cast_checked()?,
last_updated_utc: last_updated_utc.to_owned(),
},
historical: mixnode::MixnodeSummaryHistorical {
@@ -293,10 +254,8 @@ impl Monitor {
gateways: gateway::GatewaySummary {
bonded: gateway::GatewaySummaryBonded {
count: count_bonded_gateways.cast_checked()?,
last_updated_utc: last_updated_utc.to_owned(),
},
blacklisted: gateway::GatewaySummaryBlacklisted {
count: count_gateways_blacklisted.cast_checked()?,
entry: assigned_entry_count.cast_checked()?,
exit: assigned_exit_count.cast_checked()?,
last_updated_utc: last_updated_utc.to_owned(),
},
historical: gateway::GatewaySummaryHistorical {
@@ -312,14 +271,6 @@ impl Monitor {
for (key, value) in nodes_summary.iter() {
log_lines.push(format!("{} = {}", key, value));
}
log_lines.push(format!(
"recently_unbonded_mixnodes = {}",
recently_unbonded_mixnodes
));
log_lines.push(format!(
"recently_unbonded_gateways = {}",
recently_unbonded_gateways
));
tracing::info!("Directory summary: \n{}", log_lines.join("\n"));
@@ -349,9 +300,8 @@ impl Monitor {
fn prepare_gateway_data(
&self,
gateways: &[&NymNodeDescription],
gateways_blacklisted: &HashSet<String>,
gateway_geodata: Vec<NodeGeoData>,
skimmed_gateways: Vec<SkimmedNode>,
skimmed_gateways: &[SkimmedNode],
) -> anyhow::Result<Vec<GatewayRecord>> {
let mut gateway_records = Vec::new();
@@ -359,7 +309,6 @@ impl Monitor {
let identity_key = gateway.ed25519_identity_key().to_base58_string();
let bonded = true;
let last_updated_utc = chrono::offset::Utc::now().timestamp();
let blacklisted = gateways_blacklisted.contains(&identity_key);
let self_described = serde_json::to_string(&gateway.description)?;
@@ -383,7 +332,6 @@ impl Monitor {
gateway_records.push(GatewayRecord {
identity_key: identity_key.to_owned(),
bonded,
blacklisted,
self_described,
explorer_pretty_bond,
last_updated_utc,
@@ -407,7 +355,6 @@ impl Monitor {
let identity_key = mixnode.identity_key();
let bonded = true;
let total_stake = decimal_to_i64(mixnode.mixnode_details.total_stake());
let blacklisted = mixnode.blacklisted;
let node_info = mixnode.mix_node();
let host = node_info.host.clone();
let http_port = node_info.http_api_port;
@@ -427,7 +374,6 @@ impl Monitor {
total_stake,
host,
http_port,
blacklisted,
full_details,
self_described,
last_updated_utc,
@@ -519,7 +465,7 @@ async fn get_delegation_program_details(
Ok(mix_ids)
}
fn decimal_to_i64(decimal: Decimal) -> i64 {
pub(crate) fn decimal_to_i64(decimal: Decimal) -> i64 {
// Convert the underlying Uint128 to a u128
let atomics = decimal.atomics().u128();
let precision = 1_000_000_000_000_000_000u128;
@@ -123,6 +123,7 @@ impl MetricsScrapingData {
}
}
#[instrument(level = "debug", name = "metrics_scraper", skip_all)]
async fn try_scrape_metrics(&self) -> Option<SessionStats> {
match self.try_get_client().await {
Ok(client) => {
@@ -136,13 +137,13 @@ impl MetricsScrapingData {
}
}
Err(e) => {
tracing::error!("[metrics scraper]: {e}");
tracing::warn!("{e}");
None
}
}
}
Err(e) => {
tracing::error!("[metrics scraper]: {e}");
tracing::warn!("{e}");
None
}
}
@@ -109,7 +109,6 @@ pub(crate) async fn try_queue_testrun(
})
}
// TODO dz do we need these?
pub fn now_utc() -> DateTime<chrono::Utc> {
SystemTime::now().into()
}
+1 -1
View File
@@ -3,7 +3,7 @@
[package]
name = "nym-node"
version = "1.4.0"
version = "1.6.0"
authors.workspace = true
repository.workspace = true
homepage.workspace = true
+27 -13
View File
@@ -165,6 +165,19 @@ version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545"
[[package]]
name = "async-compression"
version = "0.4.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df895a515f70646414f4b45c0b79082783b80552b373a68283012928df56f522"
dependencies = [
"flate2",
"futures-core",
"memchr",
"pin-project-lite",
"tokio",
]
[[package]]
name = "async-trait"
version = "0.1.86"
@@ -560,9 +573,9 @@ dependencies = [
[[package]]
name = "celes"
version = "2.4.0"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39b9a21273925d7cc9e8a9a5f068122341336813c607014f5ef64f82b6acba58"
checksum = "54441489dce7026efc8f01d1aa996c23fa39dd615a953d0e934433a42f61dd30"
dependencies = [
"serde",
]
@@ -724,13 +737,12 @@ dependencies = [
[[package]]
name = "colored"
version = "2.0.4"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2674ec482fbc38012cf31e6c42ba0177b431a0cb6f15fe40efa5aab1bda516f6"
checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c"
dependencies = [
"is-terminal",
"lazy_static",
"windows-sys 0.48.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -3715,7 +3727,7 @@ dependencies = [
"base64 0.22.1",
"bip32",
"bip39",
"colored 2.0.4",
"colored 2.2.0",
"cosmrs 0.21.1",
"cosmwasm-std",
"cw-controllers",
@@ -3823,7 +3835,7 @@ dependencies = [
"base64 0.13.1",
"bip39",
"cfg-if",
"colored 2.0.4",
"colored 2.2.0",
"cosmrs 0.21.1",
"cosmwasm-std",
"dirs 4.0.0",
@@ -3945,11 +3957,11 @@ dependencies = [
[[package]]
name = "openssl"
version = "0.10.56"
version = "0.10.70"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "729b745ad4a5575dd06a3e1af1414bd330ee561c01b3899eb584baeaa8def17e"
checksum = "61cfb4e166a8bb8c9b55c500bc2308550148ece889be90f609377e58140f42c6"
dependencies = [
"bitflags 1.3.2",
"bitflags 2.4.0",
"cfg-if",
"foreign-types",
"libc",
@@ -3977,9 +3989,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
[[package]]
name = "openssl-sys"
version = "0.9.91"
version = "0.9.105"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "866b5f16f90776b9bb8dc1e1802ac6f0513de3a7a7465867bfbc563dc737faac"
checksum = "8b22d5b84be05a8d6947c7cb71f7c849aa0f112acd4bf51c2a7c1c988ac0a9dc"
dependencies = [
"cc",
"libc",
@@ -4758,6 +4770,7 @@ version = "0.12.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "566cafdd92868e0939d3fb961bd0dc25fcfaaed179291093b3d43e6b3150ea10"
dependencies = [
"async-compression",
"base64 0.22.1",
"bytes",
"encoding_rs",
@@ -4790,6 +4803,7 @@ dependencies = [
"tokio",
"tokio-native-tls",
"tokio-rustls 0.25.0",
"tokio-util",
"tower-service",
"url",
"wasm-bindgen",
+1 -1
View File
@@ -70,7 +70,7 @@ pub struct NetworkConfig {
impl Default for Base {
fn default() -> Self {
let networks = WalletNetwork::iter().map(Into::into).collect();
let networks = WalletNetwork::iter().collect();
Base {
networks: SupportedNetworks::new(networks),
}

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