diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index d7a76f2673c..56fd0d15eb0 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -34,6 +34,7 @@ jobs: runs-on: ${{ matrix.os }} env: CARGO_TERM_COLOR: always + IPINFO_API_TOKEN: ${{ secrets.IPINFO_API_TOKEN }} steps: - name: Install Dependencies (Linux) run: sudo apt-get update && sudo apt-get -y install libwebkit2gtk-4.0-dev build-essential curl wget libssl-dev libgtk-3-dev libudev-dev squashfs-tools protobuf-compiler diff --git a/.github/workflows/push-credential-proxy.yaml b/.github/workflows/push-credential-proxy.yaml index ff3b02f728c..6eb29e361ca 100644 --- a/.github/workflows/push-credential-proxy.yaml +++ b/.github/workflows/push-credential-proxy.yaml @@ -26,7 +26,7 @@ jobs: git config --global user.name "Lawrence Stalder" - name: Get version from cargo.toml - uses: mikefarah/yq@v4.44.5 + uses: mikefarah/yq@v4.44.6 id: get_version with: cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/nym-credential-proxy/Cargo.toml diff --git a/.github/workflows/push-data-observatory.yaml b/.github/workflows/push-data-observatory.yaml index 8b5250995d3..16026ee589a 100644 --- a/.github/workflows/push-data-observatory.yaml +++ b/.github/workflows/push-data-observatory.yaml @@ -26,7 +26,7 @@ jobs: git config --global user.name "Lawrence Stalder" - name: Get version from cargo.toml - uses: mikefarah/yq@v4.44.5 + uses: mikefarah/yq@v4.44.6 id: get_version with: cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml diff --git a/.github/workflows/push-network-monitor.yaml b/.github/workflows/push-network-monitor.yaml index 6856ec88d18..a63ae96be8f 100644 --- a/.github/workflows/push-network-monitor.yaml +++ b/.github/workflows/push-network-monitor.yaml @@ -26,7 +26,7 @@ jobs: git config --global user.name "Lawrence Stalder" - name: Get version from cargo.toml - uses: mikefarah/yq@v4.44.5 + uses: mikefarah/yq@v4.44.6 id: get_version with: cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/nym-network-monitor/Cargo.toml diff --git a/.github/workflows/push-node-status-agent.yaml b/.github/workflows/push-node-status-agent.yaml index a30ef58f771..8400b7e7e16 100644 --- a/.github/workflows/push-node-status-agent.yaml +++ b/.github/workflows/push-node-status-agent.yaml @@ -31,7 +31,7 @@ jobs: git config --global user.name "Lawrence Stalder" - name: Get version from cargo.toml - uses: mikefarah/yq@v4.44.5 + uses: mikefarah/yq@v4.44.6 id: get_version with: cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml diff --git a/.github/workflows/push-node-status-api.yaml b/.github/workflows/push-node-status-api.yaml index acf857109d8..bd19db3bc84 100644 --- a/.github/workflows/push-node-status-api.yaml +++ b/.github/workflows/push-node-status-api.yaml @@ -26,7 +26,7 @@ jobs: git config --global user.name "Lawrence Stalder" - name: Get version from cargo.toml - uses: mikefarah/yq@v4.44.5 + uses: mikefarah/yq@v4.44.6 id: get_version with: cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml diff --git a/.github/workflows/push-nym-node.yaml b/.github/workflows/push-nym-node.yaml index 52dfeae0e7a..79c25e022c7 100644 --- a/.github/workflows/push-nym-node.yaml +++ b/.github/workflows/push-nym-node.yaml @@ -26,7 +26,7 @@ jobs: git config --global user.name "Lawrence Stalder" - name: Get version from cargo.toml - uses: mikefarah/yq@v4.44.5 + uses: mikefarah/yq@v4.44.6 id: get_version with: cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml diff --git a/.github/workflows/push-nyx-chain-watcher.yaml b/.github/workflows/push-nyx-chain-watcher.yaml new file mode 100644 index 00000000000..084f4d11d76 --- /dev/null +++ b/.github/workflows/push-nyx-chain-watcher.yaml @@ -0,0 +1,55 @@ +name: Build and upload Nyx Chain Watcher container to harbor.nymte.ch +on: + workflow_dispatch: + +env: + WORKING_DIRECTORY: "nyx-chain-watcher" + CONTAINER_NAME: "nyx-chain-watcher" + +jobs: + build-container: + runs-on: arc-ubuntu-22.04-dind + steps: + - name: Login to Harbor + uses: docker/login-action@v3 + with: + registry: harbor.nymte.ch + username: ${{ secrets.HARBOR_ROBOT_USERNAME }} + password: ${{ secrets.HARBOR_ROBOT_SECRET }} + + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Configure git identity + run: | + git config --global user.email "lawrence@nymtech.net" + git config --global user.name "Lawrence Stalder" + + - name: Get version from cargo.toml + uses: mikefarah/yq@v4.44.6 + id: get_version + with: + cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml + + - name: Check if tag exists + run: | + if git rev-parse ${{ steps.get_version.outputs.value }} >/dev/null 2>&1; then + echo "Tag ${{ steps.get_version.outputs.value }} already exists" + fi + + - name: Remove existing tag if exists + run: | + if git rev-parse ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }} >/dev/null 2>&1; then + git push --delete origin ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }} + git tag -d ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }} + fi + + - name: Create tag + run: | + git tag -a ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }} -m "Version ${{ steps.get_version.outputs.result }}" + git push origin ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }} + + - name: BuildAndPushImageOnHarbor + run: | + docker build -f ${{ env.WORKING_DIRECTORY }}/Dockerfile . -t harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }}:${{ steps.get_version.outputs.result }} -t harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }}:latest + docker push harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }} --all-tags diff --git a/.github/workflows/push-validator-rewarder.yaml b/.github/workflows/push-validator-rewarder.yaml index 8676520f825..db27ca4d649 100644 --- a/.github/workflows/push-validator-rewarder.yaml +++ b/.github/workflows/push-validator-rewarder.yaml @@ -26,10 +26,10 @@ jobs: git config --global user.name "Lawrence Stalder" - name: Get version from cargo.toml - uses: mikefarah/yq@v4.44.5 + uses: mikefarah/yq@v4.44.6 id: get_version with: - cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/nym-credential-proxy/Cargo.toml + cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml - name: Remove existing tag if exists run: | diff --git a/CHANGELOG.md b/CHANGELOG.md index 46cb35531ee..bda75be82ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https:// ## [Unreleased] +## [2024.13-magura-drift] (2024-11-29) + +- Optimised syncing bandwidth information to storage + ## [2024.13-magura-patched] (2024-11-22) - [experimental] allow clients to change between deterministic route selection based on packet headers and a pseudorandom distribution diff --git a/Cargo.lock b/Cargo.lock index ebc4a8efa31..4b693241204 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,7 +11,7 @@ dependencies = [ "macroific", "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.90", ] [[package]] @@ -283,7 +283,7 @@ dependencies = [ "proc-macro2", "quote", "serde", - "syn 2.0.87", + "syn 2.0.90", ] [[package]] @@ -352,7 +352,7 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.90", ] [[package]] @@ -363,7 +363,7 @@ checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.90", ] [[package]] @@ -581,7 +581,7 @@ checksum = "57d123550fa8d071b7255cb0cc04dc302baa6c8c4a79f55701552684d8399bce" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.90", ] [[package]] @@ -1148,7 +1148,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.90", ] [[package]] @@ -1798,7 +1798,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.90", ] [[package]] @@ -1956,7 +1956,7 @@ dependencies = [ "proc-macro2", "quote", "strsim 0.11.1", - "syn 2.0.87", + "syn 2.0.90", ] [[package]] @@ -1978,7 +1978,7 @@ checksum = "733cabb43482b1a1b53eee8583c2b9e8684d592215ea83efd305dd31bc2f0178" dependencies = [ "darling_core 0.20.9", "quote", - "syn 2.0.87", + "syn 2.0.90", ] [[package]] @@ -2029,7 +2029,7 @@ dependencies = [ "macroific", "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.90", ] [[package]] @@ -2072,7 +2072,7 @@ checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.90", ] [[package]] @@ -2105,7 +2105,7 @@ dependencies = [ "proc-macro2", "proc-macro2-diagnostics", "quote", - "syn 2.0.87", + "syn 2.0.90", ] [[package]] @@ -2173,7 +2173,7 @@ checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.90", ] [[package]] @@ -2449,7 +2449,7 @@ dependencies = [ [[package]] name = "explorer-api" -version = "1.1.42" +version = "1.1.43" dependencies = [ "chrono", "clap 4.5.20", @@ -2552,7 +2552,7 @@ dependencies = [ "macroific", "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.90", ] [[package]] @@ -2786,7 +2786,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.90", ] [[package]] @@ -2881,7 +2881,7 @@ dependencies = [ "proc-macro-error2", "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.90", ] [[package]] @@ -3269,6 +3269,12 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "human-repr" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f58b778a5761513caf593693f8951c97a5b610841e754788400f32102eefdff1" + [[package]] name = "humantime" version = "1.3.0" @@ -3843,9 +3849,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.162" +version = "0.2.167" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18d287de67fe55fd7e1581fe933d965a5a9477b38e949cfa9f8574ef01506398" +checksum = "09d6582e104315a817dff97f75133544b2e094ee22447d2acf4a74e189ba06fc" [[package]] name = "libm" @@ -3971,7 +3977,7 @@ dependencies = [ "cfg-if", "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.90", ] [[package]] @@ -3982,7 +3988,7 @@ checksum = "13198c120864097a565ccb3ff947672d969932b7975ebd4085732c9f09435e55" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.90", ] [[package]] @@ -3995,7 +4001,7 @@ dependencies = [ "macroific_core", "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.90", ] [[package]] @@ -4149,6 +4155,21 @@ dependencies = [ "wasm-utils", ] +[[package]] +name = "mixnet-connectivity-check" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap 4.5.20", + "futures", + "nym-bin-common", + "nym-crypto", + "nym-network-defaults", + "nym-sdk", + "tokio", + "tracing", +] + [[package]] name = "moka" version = "0.12.8" @@ -4452,7 +4473,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.90", ] [[package]] @@ -4472,7 +4493,7 @@ checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" [[package]] name = "nym-api" -version = "1.1.46" +version = "1.1.47" dependencies = [ "anyhow", "async-trait", @@ -4695,7 +4716,6 @@ dependencies = [ "opentelemetry-jaeger", "pretty_env_logger", "schemars", - "semver 1.0.23", "serde", "serde_json", "tracing-opentelemetry", @@ -4722,7 +4742,7 @@ dependencies = [ [[package]] name = "nym-cli" -version = "1.1.44" +version = "1.1.45" dependencies = [ "anyhow", "base64 0.22.1", @@ -4805,7 +4825,7 @@ dependencies = [ [[package]] name = "nym-client" -version = "1.1.44" +version = "1.1.45" dependencies = [ "bs58", "clap 4.5.20", @@ -4872,6 +4892,7 @@ dependencies = [ "nym-gateway-requests", "nym-id", "nym-metrics", + "nym-mixnet-client", "nym-network-defaults", "nym-nonexhaustive-delayqueue", "nym-pemstore", @@ -5106,7 +5127,7 @@ dependencies = [ [[package]] name = "nym-credential-proxy" -version = "0.1.3" +version = "0.1.6" dependencies = [ "anyhow", "async-trait", @@ -5126,6 +5147,7 @@ dependencies = [ "nym-credentials", "nym-credentials-interface", "nym-crypto", + "nym-ecash-contract-common", "nym-http-api-common", "nym-network-defaults", "nym-validator-client", @@ -5464,7 +5486,7 @@ dependencies = [ "nym-mixnode-common", "nym-network-defaults", "nym-network-requester", - "nym-node-http-api", + "nym-node-metrics", "nym-sdk", "nym-sphinx", "nym-statistics-common", @@ -5556,8 +5578,11 @@ name = "nym-gateway-stats-storage" version = "0.1.0" dependencies = [ "nym-credentials-interface", + "nym-node-metrics", "nym-sphinx", + "nym-statistics-common", "sqlx", + "strum 0.26.3", "thiserror", "time", "tokio", @@ -5568,7 +5593,6 @@ dependencies = [ name = "nym-gateway-storage" version = "0.1.0" dependencies = [ - "async-trait", "bincode", "defguard_wireguard_rs", "log", @@ -5633,12 +5657,15 @@ dependencies = [ "axum-client-ip", "bytes", "colored", + "futures", "mime", "serde", "serde_json", "serde_yaml", + "tower 0.4.13", "tracing", "utoipa", + "zeroize", ] [[package]] @@ -5761,11 +5788,11 @@ name = "nym-mixnet-client" version = "0.1.0" dependencies = [ "futures", - "log", "nym-sphinx", "nym-task", "tokio", "tokio-util", + "tracing", ] [[package]] @@ -5792,35 +5819,6 @@ dependencies = [ "utoipa", ] -[[package]] -name = "nym-mixnode" -version = "1.1.37" -dependencies = [ - "colored", - "futures", - "nym-contracts-common", - "nym-crypto", - "nym-http-api-common", - "nym-metrics", - "nym-mixnet-client", - "nym-mixnode-common", - "nym-node-http-api", - "nym-nonexhaustive-delayqueue", - "nym-sphinx", - "nym-sphinx-params", - "nym-sphinx-types", - "nym-task", - "nym-topology", - "nym-types", - "nym-validator-client", - "thiserror", - "time", - "tokio", - "tokio-util", - "tracing", - "url", -] - [[package]] name = "nym-mixnode-common" version = "0.1.0" @@ -5829,11 +5827,9 @@ dependencies = [ "futures", "humantime-serde", "log", - "nym-bin-common", "nym-crypto", "nym-metrics", "nym-network-defaults", - "nym-node-http-api", "nym-sphinx-acknowledgements", "nym-sphinx-addressing", "nym-sphinx-forwarding", @@ -5841,7 +5837,6 @@ dependencies = [ "nym-sphinx-params", "nym-sphinx-types", "nym-task", - "nym-validator-client", "rand", "serde", "thiserror", @@ -5891,6 +5886,7 @@ dependencies = [ "futures", "log", "nym-bin-common", + "nym-client-core", "nym-crypto", "nym-network-defaults", "nym-sdk", @@ -5912,7 +5908,7 @@ dependencies = [ [[package]] name = "nym-network-requester" -version = "1.1.45" +version = "1.1.46" dependencies = [ "addr", "anyhow", @@ -5963,9 +5959,12 @@ dependencies = [ [[package]] name = "nym-node" -version = "1.1.11" +version = "1.2.0" dependencies = [ "anyhow", + "async-trait", + "axum 0.7.7", + "axum-extra", "bip39", "bs58", "cargo_metadata 0.18.1", @@ -5973,6 +5972,10 @@ dependencies = [ "clap 4.5.20", "colored", "cupid", + "dashmap", + "futures", + "headers", + "human-repr", "humantime-serde", "ipnetwork 0.20.0", "nym-authenticator", @@ -5981,63 +5984,60 @@ dependencies = [ "nym-config", "nym-crypto", "nym-gateway", + "nym-gateway-stats-storage", + "nym-http-api-common", "nym-ip-packet-router", - "nym-mixnode", + "nym-metrics", + "nym-mixnet-client", "nym-network-requester", - "nym-node-http-api", + "nym-node-metrics", + "nym-node-requests", + "nym-nonexhaustive-delayqueue", "nym-pemstore", "nym-sphinx-acknowledgements", "nym-sphinx-addressing", + "nym-sphinx-forwarding", + "nym-sphinx-framing", + "nym-sphinx-types", "nym-task", + "nym-topology", "nym-types", "nym-validator-client", + "nym-verloc", "nym-wireguard", "nym-wireguard-types", "rand", "semver 1.0.23", "serde", "serde_json", + "si-scale", "sysinfo", "thiserror", + "time", "tokio", + "tokio-util", "toml 0.8.14", + "tower-http", "tracing", "tracing-subscriber", "url", + "utoipa", + "utoipa-swagger-ui", "zeroize", ] [[package]] -name = "nym-node-http-api" +name = "nym-node-metrics" version = "0.1.0" dependencies = [ - "axum 0.7.7", - "axum-extra", - "base64 0.22.1", - "colored", "dashmap", - "fastrand 2.1.1", - "headers", - "hmac", - "hyper 1.4.1", - "ipnetwork 0.20.0", - "nym-crypto", - "nym-http-api-common", + "futures", "nym-metrics", - "nym-node-requests", - "nym-task", - "nym-wireguard", - "rand", - "serde_json", - "thiserror", + "nym-statistics-common", + "strum 0.26.3", "time", "tokio", - "tower 0.4.13", - "tower-http", "tracing", - "utoipa", - "utoipa-swagger-ui", - "x25519-dalek", ] [[package]] @@ -6082,7 +6082,7 @@ dependencies = [ [[package]] name = "nym-node-status-api" -version = "1.0.0-rc.3" +version = "1.0.0-rc.7" dependencies = [ "anyhow", "axum 0.7.7", @@ -6096,8 +6096,11 @@ dependencies = [ "nym-crypto", "nym-explorer-client", "nym-network-defaults", + "nym-node-metrics", "nym-node-requests", "nym-node-status-client", + "nym-serde-helpers", + "nym-statistics-common", "nym-task", "nym-validator-client", "regex", @@ -6109,6 +6112,7 @@ dependencies = [ "strum 0.26.3", "strum_macros 0.26.4", "thiserror", + "time", "tokio", "tokio-util", "tower-http", @@ -6327,7 +6331,7 @@ dependencies = [ [[package]] name = "nym-socks5-client" -version = "1.1.44" +version = "1.1.45" dependencies = [ "bs58", "clap 4.5.20", @@ -6829,6 +6833,25 @@ dependencies = [ "zeroize", ] +[[package]] +name = "nym-verloc" +version = "0.1.0" +dependencies = [ + "bytes", + "futures", + "humantime 2.1.0", + "nym-crypto", + "nym-task", + "nym-validator-client", + "rand", + "thiserror", + "time", + "tokio", + "tokio-util", + "tracing", + "url", +] + [[package]] name = "nym-vesting-contract-common" version = "0.7.0" @@ -6907,8 +6930,10 @@ dependencies = [ "nym-task", "nym-wireguard-types", "thiserror", + "time", "tokio", "tokio-stream", + "tracing", "x25519-dalek", ] @@ -6929,7 +6954,7 @@ dependencies = [ [[package]] name = "nymvisor" -version = "0.1.9" +version = "0.1.10" dependencies = [ "anyhow", "bytes", @@ -7242,7 +7267,7 @@ dependencies = [ "proc-macro2", "proc-macro2-diagnostics", "quote", - "syn 2.0.87", + "syn 2.0.90", ] [[package]] @@ -7329,7 +7354,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.90", ] [[package]] @@ -7370,7 +7395,7 @@ checksum = "a4502d8515ca9f32f1fb543d987f63d95a14934883db45bdb48060b6b69257f8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.90", ] [[package]] @@ -7585,14 +7610,14 @@ dependencies = [ "proc-macro-error-attr2", "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.90", ] [[package]] name = "proc-macro2" -version = "1.0.89" +version = "1.0.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" +checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" dependencies = [ "unicode-ident", ] @@ -7605,7 +7630,7 @@ checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.90", "version_check", "yansi", ] @@ -7668,7 +7693,7 @@ dependencies = [ "itertools 0.12.1", "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.90", ] [[package]] @@ -7901,7 +7926,7 @@ checksum = "bcc303e793d3734489387d205e9b186fac9c6cfacedd98cbb2e8a5943595f3e6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.90", ] [[package]] @@ -8128,7 +8153,7 @@ dependencies = [ "proc-macro2", "quote", "rocket_http", - "syn 2.0.87", + "syn 2.0.90", "unicode-xid", "version_check", ] @@ -8254,7 +8279,7 @@ dependencies = [ "proc-macro2", "quote", "rust-embed-utils", - "syn 2.0.87", + "syn 2.0.90", "walkdir", ] @@ -8310,9 +8335,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.40" +version = "0.38.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99e4ea3e1cdc4b559b8e5650f9c8e5998e3e5c1343b4eaf034565f32318d63c0" +checksum = "d7f649912bc1495e167a6edee79151c84b1bad49748cb4f1f1167f459f6224f6" dependencies = [ "bitflags 2.5.0", "errno", @@ -8503,7 +8528,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals 0.29.1", - "syn 2.0.87", + "syn 2.0.90", ] [[package]] @@ -8535,7 +8560,7 @@ checksum = "1db149f81d46d2deba7cd3c50772474707729550221e69588478ebf9ada425ae" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.90", ] [[package]] @@ -8667,7 +8692,7 @@ checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.90", ] [[package]] @@ -8678,7 +8703,7 @@ checksum = "e578a843d40b4189a4d66bba51d7684f57da5bd7c304c64e14bd63efbef49509" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.90", ] [[package]] @@ -8689,7 +8714,7 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.90", ] [[package]] @@ -8751,7 +8776,7 @@ checksum = "aafbefbe175fa9bf03ca83ef89beecff7d2a95aaacd5732325b90ac8c3bd7b90" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.90", ] [[package]] @@ -8772,7 +8797,7 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.90", ] [[package]] @@ -8823,7 +8848,7 @@ dependencies = [ "darling 0.20.9", "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.90", ] [[package]] @@ -9422,7 +9447,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.87", + "syn 2.0.90", ] [[package]] @@ -9465,9 +9490,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.87" +version = "2.0.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" +checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31" dependencies = [ "proc-macro2", "quote", @@ -9773,7 +9798,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.90", ] [[package]] @@ -9904,7 +9929,7 @@ checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.90", ] [[package]] @@ -10183,7 +10208,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.90", ] [[package]] @@ -10347,7 +10372,7 @@ checksum = "0ea0b99e8ec44abd6f94a18f28f7934437809dd062820797c52401298116f70e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.90", "termcolor", ] @@ -10374,7 +10399,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals 0.28.0", - "syn 2.0.87", + "syn 2.0.90", ] [[package]] @@ -10577,7 +10602,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55137c122f712d9330fd985d66fa61bdc381752e89c35708c13ce63049a3002c" dependencies = [ "quote", - "syn 2.0.87", + "syn 2.0.90", ] [[package]] @@ -10609,7 +10634,7 @@ dependencies = [ "proc-macro2", "quote", "serde", - "syn 2.0.87", + "syn 2.0.90", "toml 0.5.11", "uniffi_build", "uniffi_meta", @@ -10741,7 +10766,7 @@ dependencies = [ "proc-macro2", "quote", "regex", - "syn 2.0.87", + "syn 2.0.90", "uuid", ] @@ -10780,7 +10805,7 @@ checksum = "17e82ab96c5a55263b5bed151b8426410d93aa909a453acdbd4b6792b5af7d64" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.90", ] [[package]] @@ -10791,7 +10816,7 @@ checksum = "86b8338dc3c9526011ffaa2aa6bd60ddfda9d49d2123108690755c6e34844212" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.90", "utoipauto-core", ] @@ -10898,7 +10923,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.90", "wasm-bindgen-shared", ] @@ -10932,7 +10957,7 @@ checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.90", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -10966,7 +10991,7 @@ checksum = "4b8220be1fa9e4c889b30fd207d4906657e7e90b12e0e6b0c8b8d8709f5de021" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.90", ] [[package]] @@ -11494,7 +11519,7 @@ checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.90", ] [[package]] @@ -11514,7 +11539,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.90", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index bad5383babc..d833eb8fe39 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -104,7 +104,6 @@ members = [ "explorer-api/explorer-client", "gateway", "integrations/bity", - "mixnode", "sdk/ffi/cpp", "sdk/ffi/go", "sdk/ffi/shared", @@ -123,8 +122,8 @@ members = [ "nym-data-observatory", "nym-network-monitor", "nym-node", - "nym-node/nym-node-http-api", "nym-node/nym-node-requests", + "nym-node/nym-node-metrics", "nym-node-status-api/nym-node-status-agent", "nym-node-status-api/nym-node-status-api", "nym-node-status-api/nym-node-status-client", @@ -149,15 +148,13 @@ members = [ "tools/internal/contract-state-importer/importer-cli", "tools/internal/contract-state-importer/importer-contract", "tools/internal/testnet-manager", - "tools/internal/testnet-manager/dkg-bypass-contract", + "tools/internal/testnet-manager/dkg-bypass-contract", "common/verloc", "tools/internal/mixnet-connectivity-check", ] default-members = [ "clients/native", "clients/socks5", "explorer-api", - "gateway", - "mixnode", "nym-api", "nym-credential-proxy/nym-credential-proxy", "nym-data-observatory", @@ -263,6 +260,7 @@ http-body-util = "0.1" httpcodec = "0.2.3" humantime = "2.1.0" humantime-serde = "1.1.1" +human-repr = "1.1.0" hyper = "1.4.1" hyper-util = "0.1" indicatif = "0.17.8" diff --git a/SECURITY.md b/SECURITY.md index c3b9bb9748e..23091d1cbc1 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -3,37 +3,23 @@ Critical bug or security issue 💥 If you're here because you're trying to figure out how to notify us of a security issue, send us a PGP encrypted email to: ``` -security@nymte.ch +security@nym.com ``` Encrypted with our public key which is available below in plain text and also on keyservers: ``` -pub rsa4096 2023-10-30 [SC] [expire : 2026-10-29] +sec rsa4096/7C3C727F05090550 2023-10-30 [SC] [expire : 2026-10-29] 24B2592E801A5AAA8666C8BA7C3C727F05090550 -uid [ ultime ] Security Nym Technologies -sub rsa4096 2023-10-30 [E] [expire : 2026-10-29] +uid [ ultime ] Security Nym Technologies +ssb rsa4096/ACD0FBD79DC70ACC 2023-10-30 [E] [expire : 2026-10-29] ``` The fingerprint of the key is on the second line above. -If you need to chat __urgently__ to our team for a __critical__ security issue: - -go to Matrix, and alert the core engineers with a private direct message: - - Jedrzej Stuczynski @jstuczyn:nymtech.chat - Mark Sinclair @mark:nymtech.chat - Raphaël Walther @raphael:nymtech.chat - Please avoid opening public issues on GitHub that contain information about a potential security vulnerability as this makes it difficult to reduce the impact and harm of valid security issues. -If you don't know what Matrix is, you can follow this documentation to create an account on this federation of instant messaging servers: - -[Matrix for Instant Messaging](https://matrix.org/docs/chat_basics/matrix-for-im/) - - - ``` -----BEGIN PGP PUBLIC KEY BLOCK----- @@ -48,43 +34,69 @@ vMFUIzBMHOPXH16036zGyFMC1esRd2qqil4b9KtLgCOkrD1VgpjcveoA0VyMJCN6 LmKTrVjwjjDMxby+d49BolRWGnCofXozXwvNQx+CYv8M2WPErTpyYoofYFtpqr7A fIufc/e0+um3zoGIbHejrhsbuH9Qf+MKsI+Ng93bdDtjeHz6MEgAlsTm0qeizYpj IyKZIObPmfvrAm08hFZ8JnGk+XuooF36XWbJYjCCy0bOyMw1r7ZG99TcSwARAQAB -tC1TZWN1cml0eSBOeW0gVGVjaG5vbG9naWVzIDxzZWN1cml0eUBueW10ZS5jaD6J -AlQEEwEKAD4WIQQkslkugBpaqoZmyLp8PHJ/BQkFUAUCZT9elwIbAwUJBaOagAUL -CQgHAwUVCgkICwUWAgMBAAIeAQIXgAAKCRB8PHJ/BQkFUL7dD/9zO73uI5VR+SWx -PFmJW+9QsPiQbVRvGwNZurctmQ2s2Pe0vHRELFeqD5oYvSx2Lequ3Ir+zn/C3kDM -kNs40obSL6jCBiLPkxEY0JqzPM9jZr7EjvlibWV3f6DxooRIqEyfN57I3OBGlqZE -0Mx7sQuCcgau8C70DF952QhKUwXC2cmpmDKHVEEoio1xGSD4dQhGapCB32RQGtna -OGfAO9celNMvSq0Lp+aJxeACmWFY5T4/y79JPcT5vSs/yEIRmaH/fn2piwaFBsIq -gHJJMxO3740P1hF8j7KWUoUofuFaEALHBpEpjWTOj8ej1wmFlu+5F+jSVoc781Wb -ZZXu04cOBXnGTogzSxMpBe9TtLb28zd6WzFotC25KTI3pngMzXsQGLJLOwvoZKiS -LFjPRjg1rwobmB3Q3J2W5GYSveia0CDsZGP+g87GVVf/oD2Djpa68xyVYwIYeA6T -3DNdS77qHiRuGiS4kWXyVjDqOICboR4uCvt09zlkBuLDdTWqWYARUvZjtjs4w/Ol -rdrBI3A88ti8fRldYaNpu17ME1ilpN44yKoJtqiWc3Tisk8eYLfx6c7FQF3PrRva -mr7FZvhFsYML5CeNFHTEzN6Y3jjKN/60DvCfodWnWFK47Txkl8UAXGY2W9B0fWqQ -wUVr8uLuMyyMiKbeoufi7rGOj6AMErkCDQRlP16XARAA8FGmD5J3tM1BOM1niJxZ -JTdCauzEtxEoBL0RuqGBkR8U29sRM6DwuzjU7PwscFnBaGyU+eU73GwGkH3ozFfF -tllYhQrhP/kkN+0rEO5Xi+nR+4JCFRqrf3nJXAAPfiksURMp8er1dUOY2/e1ZSoL -tS+nzUivV8CfE+pgj/5YtGwPC+KYHLATkKkMELCrbW4UO06VWOqQsvr6kivXuJQQ -LdEAMpBlADmXFG45DmPKQzsBWUgvTwyGy3LX0nys8cgpex9BH8hhr01QmGyP469s -N3cNrtFuu8U6RAsiCD/8mlBuD3EQEU5SF0lc7kCICAZk+wElmXnimEi0TOYsbz6k -90lteicX70rA9GNeyI76H+VSOYvWpkRwaJAgUdzrAM1o9SHASq+cZ6nD85OZioQk -DWM6+Q+sf2oen0qJnnGmUr93kJIC0PIdgrXRrtiNfeRa1Z/H0LmREyyEMoFiVivn -z1vVk85Oq6Sf3ltUwvmDzuuJOtsp2Qp6+x6Snn/yKauI4uf4Cf/wKUch4r6Bwgg5 -Dw49ky7lwlnALio4GIVoGLpLef93wWoDmp4Klyh3ZPf2nB0U91u3bHRUo7m+D7QJ -98cyKtqLLzjg7szGf60pIWNWRsadYQT3bSncynqknAjOV3BCvx6/ivsnpj//QjYR -HtviUAcQ1DBB6UC6q23FIs0AEQEAAYkCPAQYAQoAJhYhBCSyWS6AGlqqhmbIunw8 -cn8FCQVQBQJlP16XAhsMBQkFo5qAAAoJEHw8cn8FCQVQzukP/iLxjOxT+UpPR//c -prDVSLkP4pF5bmw36U07jvqpS+/KTXsxiiQleffRabOpNLcd+K1ueavyt9nnIwHH -tHS9kM9A7DBw3LnpEbXki46QDCCI6niGijlLOEeAWqnocwMNTT05wVVgCtO3DQP2 -MoSCcqHpXDChvOyr5d5xjYLVJhlctIMSomcVzGryjknPu0Yj/TkC/4c+m86ZWQUD -HqMHQIuiEenvb62/F4c5OJIRZPEn70wdddkgJuJU3eHdHrnuhCkjCC93GQGbGj03 -Zqos6699y6hmPeD3U5IUv8ujwZYVCCuDm8gJfrp3R6WLfeZeK9WmTVBpCzsDg3fV -hSwmOk6pp8DAq1/Dev3yRkFggCEyGK6c9b+a0CRBncl8e5Q0QQIzNiS/uExQP3h+ -ELJs3P0MLP+6FWhNUry09n3lnWkr1hY+v1M0GAxbfdv/tsCN1Pq/VQEz+CTqXqya -ftWldOHWw6Hh+gtwxcHjG4MBOrO5oICQ3lh2hGwQ58cDgZYSK/OGgJ9BggFl1CcM -0uGC0/TRCI1zt/4y+7efSZQMZkHo7VC/3MFbp2hcNejpW+BxVuwKTunFvWK3TLhq -sSlQ5yyhqchooepsFHq9bosKFjLJC01uprBv1rinoNduOy43FbyS7JPRRspANN0R -iC2pMbWdE0ZTQaFq6tPIg058pjqi -=nqgX +tCxTZWN1cml0eSBOeW0gVGVjaG5vbG9naWVzIDxzZWN1cml0eUBueW0uY29tPokC +VAQTAQoAPhYhBCSyWS6AGlqqhmbIunw8cn8FCQVQBQJnSd5VAhsDBQkFo5qABQsJ +CAcDBRUKCQgLBRYCAwEAAh4BAheAAAoJEHw8cn8FCQVQPPIP/ipGz2zLAjE2dSE3 +VcqOvras0DfqIL9HDm26Dg6QO2D/4YRntw0RqVyuy+zFnRUm+RZCKLPLUzbQ9Wjb +G/Og5ttQVYQMu5eKu7OMvXkrbRo3teZFU+8IL08zIW6pyf9haxO6YMhLRy6cLYwW +0EYC6Qzn5gz3kI7VkI8fWfs2Dk4XEV3D+SVtBoF6KRxMXT6HZvpzoMSEJZBoNj8S +jw0TF8TFUQf49jUQbIHumukMswolrHi8a5ej8DSfNwSgz+Tt8oh5lu01kyUJiHn7 +nuHaY4Y9cHUVAOSwq/hovG52+ZE1r3aiswvle/B19o9pKeWWVvacSptGxDQagBtQ +igoNLdRvY0XN2TEyX9pOHR0AoVOxtIW11CpkKuDbQG9vPwovqJ2L6+Fh3pzHYzcI +2GIShNm/Z2SZBiUqbljJe9H4UAT/aHgMINkEG8qzUKwO42MA5HJT7YbHTR17/QSF +Il5dhneRzmSbNcW2rdRwx/BmzrcsFJfqCt4JG/WDF293xSOjhFqQYvU4gCO+OB7o +KXjX907XXDjS2KEJ71OGqVfk/P7BqEfQNfrLtb02TyXJAPQXHhybv23c4E7zUs9V +lMjNizzxYB96uwJb0LAB2ijzEwoP91uGT2tFjk6F08x2QiArmXUdgrv44b39Stia +gJS0GYKqSzyr10xHhUuDA+GKYtcitC1TZWN1cml0eSBOeW0gVGVjaG5vbG9naWVz +IDxzZWN1cml0eUBueW10ZS5jaD6JAjYEMAEKACAWIQQkslkugBpaqoZmyLp8PHJ/ +BQkFUAUCZ0nftQIdIAAKCRB8PHJ/BQkFUFHDEACtyNuUEjKCLAT5mSfow85PjFgo +o8kHjQr/IIQ7ZbBOHeJJcrxDuypssiLh5XUjF3x5BiBfZ6vCxSb81RRwsDMp0mA1 +qzv9G8sgW0HTQUnZ9oH6CYut2NgzAnQpmuacrunm9Zy0FJ3ejbmwUY/NqK6gJkle +66duHKhAy7DWjj7amd0C8bPDR+PA44fI3MezDHkQNaauKZTRqd1TqH8Qk5PAl4cB +o5gVzeZh/U7/usvtGhazAIUF5BqK6bTmDnYopg+2x8jjwrG4+08GrttZkNjBLXeA +Y/2U064yMz12LPv01qqAFdZ+coRy/ps/gOQTz34/VeW0CFy7TMqs4t3vSBWTqU7w +hnw/qj6cM33fdxctj6KDgJSCkZdx2fvwXgxiPqUa5+j9FlFBeD5RDAl6g6t8N1/K +Xca+zNYuSZgc297q1D+mtSD1C7uJNPxoAl+Bv5KNKpsjfQ+m04++CIFtGyX22aCA +h2/tHwQZIXhOiMAKOoupidDVDhgxtCJ3Ps416xL0sTZfsPfg+j1Uv/Em9pzPClEl +fX6+1O4DdSyZUQ4VsjMu/H5W/NQdbHgmqFrxQ6WX/0s5GMwO6GMDiPe8sOrwz9wD +WYtyjafxXOHEZ1OjYX5gr7bGaG4oKc2btTJN0B3Phg4dStnHCNjEYccxuV3507fj +HnNotkpXF2nGLxy+PYkCVAQTAQoAPhYhBCSyWS6AGlqqhmbIunw8cn8FCQVQBQJl +P16XAhsDBQkFo5qABQsJCAcDBRUKCQgLBRYCAwEAAh4BAheAAAoJEHw8cn8FCQVQ +vt0P/3M7ve4jlVH5JbE8WYlb71Cw+JBtVG8bA1m6ty2ZDazY97S8dEQsV6oPmhi9 +LHYt6q7civ7Of8LeQMyQ2zjShtIvqMIGIs+TERjQmrM8z2NmvsSO+WJtZXd/oPGi +hEioTJ83nsjc4EaWpkTQzHuxC4JyBq7wLvQMX3nZCEpTBcLZyamYModUQSiKjXEZ +IPh1CEZqkIHfZFAa2do4Z8A71x6U0y9KrQun5onF4AKZYVjlPj/Lv0k9xPm9Kz/I +QhGZof9+famLBoUGwiqAckkzE7fvjQ/WEXyPspZShSh+4VoQAscGkSmNZM6Px6PX +CYWW77kX6NJWhzvzVZtlle7Thw4FecZOiDNLEykF71O0tvbzN3pbMWi0LbkpMjem +eAzNexAYsks7C+hkqJIsWM9GODWvChuYHdDcnZbkZhK96JrQIOxkY/6DzsZVV/+g +PYOOlrrzHJVjAhh4DpPcM11LvuoeJG4aJLiRZfJWMOo4gJuhHi4K+3T3OWQG4sN1 +NapZgBFS9mO2OzjD86Wt2sEjcDzy2Lx9GV1ho2m7XswTWKWk3jjIqgm2qJZzdOKy +Tx5gt/HpzsVAXc+tG9qavsVm+EWxgwvkJ40UdMTM3pjeOMo3/rQO8J+h1adYUrjt +PGSXxQBcZjZb0HR9apDBRWvy4u4zLIyIpt6i5+LusY6PoAwSuQINBGU/XpcBEADw +UaYPkne0zUE4zWeInFklN0Jq7MS3ESgEvRG6oYGRHxTb2xEzoPC7ONTs/CxwWcFo +bJT55TvcbAaQfejMV8W2WViFCuE/+SQ37SsQ7leL6dH7gkIVGqt/eclcAA9+KSxR +Eynx6vV1Q5jb97VlKgu1L6fNSK9XwJ8T6mCP/li0bA8L4pgcsBOQqQwQsKttbhQ7 +TpVY6pCy+vqSK9e4lBAt0QAykGUAOZcUbjkOY8pDOwFZSC9PDIbLctfSfKzxyCl7 +H0EfyGGvTVCYbI/jr2w3dw2u0W67xTpECyIIP/yaUG4PcRARTlIXSVzuQIgIBmT7 +ASWZeeKYSLRM5ixvPqT3SW16JxfvSsD0Y17Ijvof5VI5i9amRHBokCBR3OsAzWj1 +IcBKr5xnqcPzk5mKhCQNYzr5D6x/ah6fSomecaZSv3eQkgLQ8h2CtdGu2I195FrV +n8fQuZETLIQygWJWK+fPW9WTzk6rpJ/eW1TC+YPO64k62ynZCnr7HpKef/Ipq4ji +5/gJ//ApRyHivoHCCDkPDj2TLuXCWcAuKjgYhWgYukt5/3fBagOangqXKHdk9/ac +HRT3W7dsdFSjub4PtAn3xzIq2osvOODuzMZ/rSkhY1ZGxp1hBPdtKdzKeqScCM5X +cEK/Hr+K+yemP/9CNhEe2+JQBxDUMEHpQLqrbcUizQARAQABiQI8BBgBCgAmFiEE +JLJZLoAaWqqGZsi6fDxyfwUJBVAFAmU/XpcCGwwFCQWjmoAACgkQfDxyfwUJBVDO +6Q/+IvGM7FP5Sk9H/9ymsNVIuQ/ikXlubDfpTTuO+qlL78pNezGKJCV599Fps6k0 +tx34rW55q/K32ecjAce0dL2Qz0DsMHDcuekRteSLjpAMIIjqeIaKOUs4R4Baqehz +Aw1NPTnBVWAK07cNA/YyhIJyoelcMKG87Kvl3nGNgtUmGVy0gxKiZxXMavKOSc+7 +RiP9OQL/hz6bzplZBQMeowdAi6IR6e9vrb8Xhzk4khFk8SfvTB112SAm4lTd4d0e +ue6EKSMIL3cZAZsaPTdmqizrr33LqGY94PdTkhS/y6PBlhUIK4ObyAl+undHpYt9 +5l4r1aZNUGkLOwODd9WFLCY6TqmnwMCrX8N6/fJGQWCAITIYrpz1v5rQJEGdyXx7 +lDRBAjM2JL+4TFA/eH4Qsmzc/Qws/7oVaE1SvLT2feWdaSvWFj6/UzQYDFt92/+2 +wI3U+r9VATP4JOperJp+1aV04dbDoeH6C3DFweMbgwE6s7mggJDeWHaEbBDnxwOB +lhIr84aAn0GCAWXUJwzS4YLT9NEIjXO3/jL7t59JlAxmQejtUL/cwVunaFw16Olb +4HFW7ApO6cW9YrdMuGqxKVDnLKGpyGih6mwUer1uiwoWMskLTW6msG/WuKeg1247 +LjcVvJLsk9FGykA03RGILakxtZ0TRlNBoWrq08iDTnymOqI= +=QPTf -----END PGP PUBLIC KEY BLOCK----- + ``` diff --git a/clients/native/Cargo.toml b/clients/native/Cargo.toml index c5679d02deb..03eb55186e8 100644 --- a/clients/native/Cargo.toml +++ b/clients/native/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nym-client" -version = "1.1.44" +version = "1.1.45" authors = ["Dave Hrycyszyn ", "Jędrzej Stuczyński "] description = "Implementation of the Nym Client" edition = "2021" diff --git a/clients/native/src/commands/run.rs b/clients/native/src/commands/run.rs index eac8271a95b..c2e5c22a0fc 100644 --- a/clients/native/src/commands/run.rs +++ b/clients/native/src/commands/run.rs @@ -3,13 +3,10 @@ use crate::commands::try_load_current_config; use crate::{ - client::{config::Config, SocketClient}, + client::SocketClient, commands::{override_config, OverrideConfig}, - error::ClientError, }; use clap::Args; -use log::*; -use nym_bin_common::version_checker::is_minor_version_compatible; use nym_client_core::cli_helpers::client_run::CommonClientRunArgs; use std::error::Error; use std::net::IpAddr; @@ -48,36 +45,12 @@ impl From for OverrideConfig { } } -// this only checks compatibility between config the binary. It does not take into consideration -// network version. It might do so in the future. -fn version_check(cfg: &Config) -> bool { - let binary_version = env!("CARGO_PKG_VERSION"); - let config_version = &cfg.base.client.version; - if binary_version == config_version { - true - } else { - warn!("The native-client binary has different version than what is specified in config file! {} and {}", binary_version, config_version); - if is_minor_version_compatible(binary_version, config_version) { - info!("but they are still semver compatible. However, consider running the `upgrade` command"); - true - } else { - error!("and they are semver incompatible! - please run the `upgrade` command before attempting `run` again"); - false - } - } -} - pub(crate) async fn execute(args: Run) -> Result<(), Box> { eprintln!("Starting client {}...", args.common_args.id); let mut config = try_load_current_config(&args.common_args.id).await?; config = override_config(config, OverrideConfig::from(args.clone())); - if !version_check(&config) { - error!("failed the local version check"); - return Err(Box::new(ClientError::FailedLocalVersionCheck)); - } - SocketClient::new(config, args.common_args.custom_mixnet) .run_socket_forever() .await diff --git a/clients/native/src/error.rs b/clients/native/src/error.rs index 23e30121f55..d2e559f2ea7 100644 --- a/clients/native/src/error.rs +++ b/clients/native/src/error.rs @@ -17,9 +17,6 @@ pub enum ClientError { #[error("Failed to validate the loaded config")] ConfigValidationFailure, - #[error("Failed local version check, client and config mismatch")] - FailedLocalVersionCheck, - #[error("Attempted to start the client in invalid socket mode")] InvalidSocketMode, diff --git a/clients/socks5/Cargo.toml b/clients/socks5/Cargo.toml index f5d3cd8967b..4363c56387c 100644 --- a/clients/socks5/Cargo.toml +++ b/clients/socks5/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nym-socks5-client" -version = "1.1.44" +version = "1.1.45" authors = ["Dave Hrycyszyn "] description = "A SOCKS5 localhost proxy that converts incoming messages to Sphinx and sends them to a Nym address" edition = "2021" diff --git a/clients/socks5/src/commands/run.rs b/clients/socks5/src/commands/run.rs index 15857d986ec..3dcbe1dfd43 100644 --- a/clients/socks5/src/commands/run.rs +++ b/clients/socks5/src/commands/run.rs @@ -2,14 +2,8 @@ // SPDX-License-Identifier: Apache-2.0 use crate::commands::try_load_current_config; -use crate::config::Config; -use crate::{ - commands::{override_config, OverrideConfig}, - error::Socks5ClientError, -}; +use crate::commands::{override_config, OverrideConfig}; use clap::Args; -use log::*; -use nym_bin_common::version_checker::is_minor_version_compatible; use nym_client_core::cli_helpers::client_run::CommonClientRunArgs; use nym_client_core::client::base_client::storage::OnDiskPersistent; use nym_client_core::client::topology_control::geo_aware_provider::CountryGroup; @@ -82,38 +76,12 @@ fn validate_country_group(s: &str) -> Result { } } -// this only checks compatibility between config the binary. It does not take into consideration -// network version. It might do so in the future. -fn version_check(cfg: &Config) -> bool { - let binary_version = env!("CARGO_PKG_VERSION"); - let config_version = &cfg.core.base.client.version; - if binary_version == config_version { - true - } else { - warn!( - "The socks5-client binary has different version than what is specified in config file! {binary_version} and {config_version}", - ); - if is_minor_version_compatible(binary_version, config_version) { - info!("but they are still semver compatible. However, consider running the `upgrade` command"); - true - } else { - error!("and they are semver incompatible! - please run the `upgrade` command before attempting `run` again"); - false - } - } -} - pub(crate) async fn execute(args: Run) -> Result<(), Box> { eprintln!("Starting client {}...", args.common_args.id); let mut config = try_load_current_config(&args.common_args.id).await?; config = override_config(config, OverrideConfig::from(args.clone())); - if !version_check(&config) { - error!("failed the local version check"); - return Err(Box::new(Socks5ClientError::FailedLocalVersionCheck)); - } - let storage = OnDiskPersistent::from_paths(config.storage_paths.common_paths, &config.core.base.debug) .await?; diff --git a/clients/socks5/src/error.rs b/clients/socks5/src/error.rs index a2f4d8379a5..3255e694007 100644 --- a/clients/socks5/src/error.rs +++ b/clients/socks5/src/error.rs @@ -14,9 +14,6 @@ pub enum Socks5ClientError { #[error("Failed to validate the loaded config")] ConfigValidationFailure, - #[error("Failed local version check, client and config mismatch")] - FailedLocalVersionCheck, - #[error("Fail to bind address")] FailToBindAddress, diff --git a/common/bandwidth-controller/src/acquire/mod.rs b/common/bandwidth-controller/src/acquire/mod.rs index e2d18d55458..450c9e338ea 100644 --- a/common/bandwidth-controller/src/acquire/mod.rs +++ b/common/bandwidth-controller/src/acquire/mod.rs @@ -17,7 +17,7 @@ use nym_validator_client::coconut::all_ecash_api_clients; use nym_validator_client::nym_api::EpochId; use nym_validator_client::nyxd::contract_traits::EcashSigningClient; use nym_validator_client::nyxd::contract_traits::{DkgQueryClient, EcashQueryClient}; -use nym_validator_client::nyxd::cosmwasm_client::ToSingletonContractData; +use nym_validator_client::nyxd::cosmwasm_client::ContractResponseData; use nym_validator_client::EcashApiClient; use rand::rngs::OsRng; diff --git a/common/bin-common/Cargo.toml b/common/bin-common/Cargo.toml index 11a2c76f2b9..78e8e6ca937 100644 --- a/common/bin-common/Cargo.toml +++ b/common/bin-common/Cargo.toml @@ -15,7 +15,6 @@ const-str = { workspace = true } log = { workspace = true } pretty_env_logger = { workspace = true } schemars = { workspace = true, features = ["preserve_order"], optional = true } -semver.workspace = true serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true, optional = true } @@ -44,5 +43,5 @@ tracing = [ "tracing-opentelemetry", "opentelemetry", ] -clap = [ "dep:clap", "dep:clap_complete", "dep:clap_complete_fig" ] +clap = ["dep:clap", "dep:clap_complete", "dep:clap_complete_fig"] models = [] diff --git a/common/bin-common/src/lib.rs b/common/bin-common/src/lib.rs index 1c6c42e6ac0..9353302b2f8 100644 --- a/common/bin-common/src/lib.rs +++ b/common/bin-common/src/lib.rs @@ -3,7 +3,6 @@ pub mod build_information; pub mod logging; -pub mod version_checker; #[cfg(feature = "clap")] pub mod completions; diff --git a/common/bin-common/src/version_checker/mod.rs b/common/bin-common/src/version_checker/mod.rs deleted file mode 100644 index 921c33b9d96..00000000000 --- a/common/bin-common/src/version_checker/mod.rs +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright 2021 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -pub use semver::Version; - -/// Checks if the version is minor version compatible. -/// -/// Checks whether given `version` is compatible with a given semantic version requirement `req` -/// according to major-minor semver rules. The semantic version requirement can be passed as a full, -/// concrete version number, because that's what we'll have in our Cargo.toml files (e.g. 0.3.2). -/// The patch number in the requirement gets dropped and replaced with a wildcard (0.3.*) as all -/// minor versions should be compatible with each other. -pub fn is_minor_version_compatible(version: &str, req: &str) -> bool { - let expected_version = match Version::parse(version) { - Ok(v) => v, - Err(_) => return false, - }; - let req_version = match Version::parse(req) { - Ok(v) => v, - Err(_) => return false, - }; - - expected_version.major == req_version.major && expected_version.minor == req_version.minor -} - -pub fn parse_version(raw_version: &str) -> Result { - Version::parse(raw_version) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn version_0_3_0_is_compatible_with_requirement_0_3_x() { - assert!(is_minor_version_compatible("0.3.0", "0.3.2")); - } - - #[test] - fn version_0_3_1_is_compatible_with_minimum_requirement_0_3_x() { - assert!(is_minor_version_compatible("0.3.1", "0.3.2")); - } - - #[test] - fn version_0_3_2_is_compatible_with_minimum_requirement_0_3_x() { - assert!(is_minor_version_compatible("0.3.2", "0.3.0")); - } - - #[test] - fn version_0_2_0_is_not_compatible_with_requirement_0_3_x() { - assert!(!is_minor_version_compatible("0.2.0", "0.3.2")); - } - - #[test] - fn version_0_4_0_is_not_compatible_with_requirement_0_3_x() { - assert!(!is_minor_version_compatible("0.4.0", "0.3.2")); - } - - #[test] - fn version_1_3_2_is_not_compatible_with_requirement_0_3_x() { - assert!(!is_minor_version_compatible("1.3.2", "0.3.2")); - } - - #[test] - fn version_0_4_0_rc_1_is_compatible_with_version_0_4_0_rc_1() { - assert!(is_minor_version_compatible("0.4.0-rc.1", "0.4.0-rc.1")); - } - - #[test] - fn returns_false_on_foo_version() { - assert!(!is_minor_version_compatible("foo", "0.3.2")); - } - - #[test] - fn returns_false_on_bar_version() { - assert!(!is_minor_version_compatible("0.3.2", "bar")); - } -} diff --git a/common/client-core/Cargo.toml b/common/client-core/Cargo.toml index 754eb61440a..b5191eef802 100644 --- a/common/client-core/Cargo.toml +++ b/common/client-core/Cargo.toml @@ -46,6 +46,7 @@ nym-sphinx = { path = "../nymsphinx" } nym-statistics-common = { path = "../statistics" } nym-pemstore = { path = "../pemstore" } nym-topology = { path = "../topology", features = ["serializable"] } +nym-mixnet-client = { path = "../client-libs/mixnet-client", default-features = false } nym-validator-client = { path = "../client-libs/validator-client", default-features = false } nym-task = { path = "../task" } nym-credentials-interface = { path = "../credentials-interface" } diff --git a/common/client-core/src/client/base_client/mod.rs b/common/client-core/src/client/base_client/mod.rs index b91e5323769..856ddd94973 100644 --- a/common/client-core/src/client/base_client/mod.rs +++ b/common/client-core/src/client/base_client/mod.rs @@ -514,15 +514,10 @@ where min_gateway_performance: config_topology.minimum_gateway_performance, }, nym_api_urls, - env!("CARGO_PKG_VERSION").to_string(), user_agent, )), config::TopologyStructure::GeoAware(group_by) => { - Box::new(GeoAwareTopologyProvider::new( - nym_api_urls, - env!("CARGO_PKG_VERSION").to_string(), - group_by, - )) + Box::new(GeoAwareTopologyProvider::new(nym_api_urls, group_by)) } }) } diff --git a/common/client-core/src/client/mix_traffic/transceiver.rs b/common/client-core/src/client/mix_traffic/transceiver.rs index 0862911d935..6d9b4fa6ded 100644 --- a/common/client-core/src/client/mix_traffic/transceiver.rs +++ b/common/client-core/src/client/mix_traffic/transceiver.rs @@ -14,7 +14,7 @@ use std::os::raw::c_int as RawFd; use thiserror::Error; #[cfg(not(target_arch = "wasm32"))] -use futures::channel::{mpsc, oneshot}; +use futures::channel::oneshot; // we need to type erase the error type since we can't have dynamic associated types alongside dynamic dispatch #[derive(Debug, Error)] @@ -170,7 +170,7 @@ pub struct LocalGateway { // 'sender' part /// Channel responsible for taking mix packets and forwarding them further into the further mixnet layers. - packet_forwarder: mpsc::UnboundedSender, + packet_forwarder: nym_mixnet_client::forwarder::MixForwardingSender, // 'receiver' part packet_router_tx: Option>, @@ -180,7 +180,7 @@ pub struct LocalGateway { impl LocalGateway { pub fn new( local_identity: identity::PublicKey, - packet_forwarder: mpsc::UnboundedSender, + packet_forwarder: nym_mixnet_client::forwarder::MixForwardingSender, packet_router_tx: oneshot::Sender, ) -> Self { LocalGateway { @@ -208,8 +208,7 @@ mod nonwasm_sealed { impl GatewaySender for LocalGateway { async fn send_mix_packet(&mut self, packet: MixPacket) -> Result<(), ErasedGatewayError> { self.packet_forwarder - .unbounded_send(packet) - .map_err(|err| err.into_send_error()) + .forward_packet(packet) .map_err(erase_err) } } diff --git a/common/client-core/src/client/topology_control/accessor.rs b/common/client-core/src/client/topology_control/accessor.rs index fcb272c9343..6b12d64562c 100644 --- a/common/client-core/src/client/topology_control/accessor.rs +++ b/common/client-core/src/client/topology_control/accessor.rs @@ -38,7 +38,7 @@ pub struct TopologyReadPermit<'a> { permit: RwLockReadGuard<'a, Option>, } -impl<'a> Deref for TopologyReadPermit<'a> { +impl Deref for TopologyReadPermit<'_> { type Target = Option; fn deref(&self) -> &Self::Target { diff --git a/common/client-core/src/client/topology_control/geo_aware_provider.rs b/common/client-core/src/client/topology_control/geo_aware_provider.rs index 7e961bb8d23..d3fabd9a938 100644 --- a/common/client-core/src/client/topology_control/geo_aware_provider.rs +++ b/common/client-core/src/client/topology_control/geo_aware_provider.rs @@ -85,15 +85,10 @@ fn check_layer_integrity(topology: NymTopology) -> Result<(), ()> { pub struct GeoAwareTopologyProvider { validator_client: nym_validator_client::client::NymApiClient, filter_on: GroupBy, - client_version: String, } impl GeoAwareTopologyProvider { - pub fn new( - mut nym_api_urls: Vec, - client_version: String, - filter_on: GroupBy, - ) -> GeoAwareTopologyProvider { + pub fn new(mut nym_api_urls: Vec, filter_on: GroupBy) -> GeoAwareTopologyProvider { log::info!( "Creating geo-aware topology provider with filter on {}", filter_on @@ -105,14 +100,13 @@ impl GeoAwareTopologyProvider { nym_api_urls[0].clone(), ), filter_on, - client_version, } } async fn get_topology(&self) -> Option { let mixnodes = match self .validator_client - .get_all_basic_active_mixing_assigned_nodes(Some(self.client_version.clone())) + .get_all_basic_active_mixing_assigned_nodes() .await { Err(err) => { @@ -124,7 +118,7 @@ impl GeoAwareTopologyProvider { let gateways = match self .validator_client - .get_all_basic_entry_assigned_nodes(Some(self.client_version.clone())) + .get_all_basic_entry_assigned_nodes() .await { Err(err) => { diff --git a/common/client-core/src/client/topology_control/nym_api_provider.rs b/common/client-core/src/client/topology_control/nym_api_provider.rs index 7734ea74610..3b87086f59a 100644 --- a/common/client-core/src/client/topology_control/nym_api_provider.rs +++ b/common/client-core/src/client/topology_control/nym_api_provider.rs @@ -35,18 +35,11 @@ pub struct NymApiTopologyProvider { validator_client: nym_validator_client::client::NymApiClient, nym_api_urls: Vec, - - client_version: String, currently_used_api: usize, } impl NymApiTopologyProvider { - pub fn new( - config: Config, - mut nym_api_urls: Vec, - client_version: String, - user_agent: Option, - ) -> Self { + pub fn new(config: Config, mut nym_api_urls: Vec, user_agent: Option) -> Self { nym_api_urls.shuffle(&mut thread_rng()); let validator_client = if let Some(user_agent) = user_agent { @@ -62,7 +55,6 @@ impl NymApiTopologyProvider { config, validator_client, nym_api_urls, - client_version, currently_used_api: 0, } } @@ -99,7 +91,7 @@ impl NymApiTopologyProvider { async fn get_current_compatible_topology(&mut self) -> Option { let mixnodes = match self .validator_client - .get_all_basic_active_mixing_assigned_nodes(Some(self.client_version.clone())) + .get_all_basic_active_mixing_assigned_nodes() .await { Err(err) => { @@ -111,7 +103,7 @@ impl NymApiTopologyProvider { let gateways = match self .validator_client - .get_all_basic_entry_assigned_nodes(Some(self.client_version.clone())) + .get_all_basic_entry_assigned_nodes() .await { Err(err) => { diff --git a/common/client-core/src/init/helpers.rs b/common/client-core/src/init/helpers.rs index 3f6a390bd0c..60c692df9f7 100644 --- a/common/client-core/src/init/helpers.rs +++ b/common/client-core/src/init/helpers.rs @@ -94,7 +94,7 @@ pub async fn current_gateways( log::debug!("Fetching list of gateways from: {nym_api}"); - let gateways = client.get_all_basic_entry_assigned_nodes(None).await?; + let gateways = client.get_all_basic_entry_assigned_nodes().await?; log::debug!("Found {} gateways", gateways.len()); log::trace!("Gateways: {:#?}", gateways); @@ -121,9 +121,7 @@ pub async fn current_mixnodes( log::trace!("Fetching list of mixnodes from: {nym_api}"); - let mixnodes = client - .get_all_basic_active_mixing_assigned_nodes(None) - .await?; + let mixnodes = client.get_all_basic_active_mixing_assigned_nodes().await?; let valid_mixnodes = mixnodes .iter() .filter_map(|mixnode| mixnode.try_into().ok()) diff --git a/common/client-libs/gateway-client/src/bandwidth.rs b/common/client-libs/gateway-client/src/bandwidth.rs index 25e9a44394d..9fd43765bdd 100644 --- a/common/client-libs/gateway-client/src/bandwidth.rs +++ b/common/client-libs/gateway-client/src/bandwidth.rs @@ -87,8 +87,10 @@ impl ClientBandwidth { if remaining < 0 { tracing::warn!("OUT OF BANDWIDTH. remaining: {remaining_bi2}"); - } else { + } else if remaining < 1_000_000 { tracing::info!("remaining bandwidth: {remaining_bi2}"); + } else { + tracing::debug!("remaining bandwidth: {remaining_bi2}"); } self.inner diff --git a/common/client-libs/gateway-client/src/client/mod.rs b/common/client-libs/gateway-client/src/client/mod.rs index 6c1d9fa0b28..6cb7b83f020 100644 --- a/common/client-libs/gateway-client/src/client/mod.rs +++ b/common/client-libs/gateway-client/src/client/mod.rs @@ -139,6 +139,10 @@ impl GatewayClient { self.gateway_identity } + pub fn shared_key(&self) -> Option> { + self.shared_key.clone() + } + pub fn ws_fd(&self) -> Option { match &self.connection { SocketState::Available(conn) => ws_fd(conn.as_ref()), @@ -408,7 +412,7 @@ impl GatewayClient { } Some(_) => { - info!("the gateway is using exactly the same (or older) protocol version as we are. We're good to continue!"); + debug!("the gateway is using exactly the same (or older) protocol version as we are. We're good to continue!"); Ok(()) } } @@ -992,24 +996,6 @@ impl GatewayClient { } Ok(()) } - - #[deprecated(note = "this method does not deal with upgraded keys for legacy clients")] - pub async fn authenticate_and_start( - &mut self, - ) -> Result - where - C: DkgQueryClient + Send + Sync, - St: CredentialStorage, - ::StorageError: Send + Sync + 'static, - { - let shared_key = self.perform_initial_authentication().await?; - self.claim_initial_bandwidth().await?; - - // this call is NON-blocking - self.start_listening_for_mixnet_messages()?; - - Ok(shared_key) - } } // type alias for an ease of use diff --git a/common/client-libs/gateway-client/src/socket_state.rs b/common/client-libs/gateway-client/src/socket_state.rs index 942f6506148..319a7bb624d 100644 --- a/common/client-libs/gateway-client/src/socket_state.rs +++ b/common/client-libs/gateway-client/src/socket_state.rs @@ -46,7 +46,8 @@ pub(crate) fn ws_fd(_conn: &WsConn) -> Option { #[cfg(unix)] match _conn.get_ref() { MaybeTlsStream::Plain(stream) => Some(stream.as_raw_fd()), - &_ => None, + MaybeTlsStream::Rustls(tls_stream) => Some(tls_stream.as_raw_fd()), + _ => None, } #[cfg(not(unix))] None @@ -110,6 +111,11 @@ impl PartiallyDelegatedRouter { } }; + if self.stream_return.is_canceled() { + // nothing to do, receiver has been dropped + return; + } + let return_res = match ret { Err(err) => self.stream_return.send(Err(err)), Ok(_) => { diff --git a/common/client-libs/mixnet-client/Cargo.toml b/common/client-libs/mixnet-client/Cargo.toml index 68e048d1f0b..25dd62f7023 100644 --- a/common/client-libs/mixnet-client/Cargo.toml +++ b/common/client-libs/mixnet-client/Cargo.toml @@ -9,10 +9,14 @@ license.workspace = true [dependencies] futures = { workspace = true } -log = { workspace = true } -tokio = { workspace = true, features = ["time", "net", "rt"] } -tokio-util = { workspace = true, features = ["codec"] } +tracing = { workspace = true } +tokio = { workspace = true, features = ["time"] } +tokio-util = { workspace = true, features = ["codec"], optional = true } # internal nym-sphinx = { path = "../../nymsphinx" } -nym-task = { path = "../../task" } +nym-task = { path = "../../task", optional = true } + +[features] +default = ["client"] +client = ["tokio-util", "nym-task", "tokio/net", "tokio/rt"] \ No newline at end of file diff --git a/common/client-libs/mixnet-client/src/client.rs b/common/client-libs/mixnet-client/src/client.rs index 4da308144f1..b8eebcdcc58 100644 --- a/common/client-libs/mixnet-client/src/client.rs +++ b/common/client-libs/mixnet-client/src/client.rs @@ -3,7 +3,6 @@ use futures::channel::mpsc; use futures::StreamExt; -use log::*; use nym_sphinx::addressing::nodes::NymNodeRoutingAddress; use nym_sphinx::framing::codec::NymCodec; use nym_sphinx::framing::packet::FramedNymPacket; @@ -18,13 +17,14 @@ use std::time::Duration; use tokio::net::TcpStream; use tokio::time::sleep; use tokio_util::codec::Framed; +use tracing::*; +#[derive(Clone, Copy)] pub struct Config { initial_reconnection_backoff: Duration, maximum_reconnection_backoff: Duration, initial_connection_timeout: Duration, maximum_connection_buffer_size: usize, - use_legacy_version: bool, } impl Config { @@ -33,14 +33,12 @@ impl Config { maximum_reconnection_backoff: Duration, initial_connection_timeout: Duration, maximum_connection_buffer_size: usize, - use_legacy_version: bool, ) -> Self { Config { initial_reconnection_backoff, maximum_reconnection_backoff, initial_connection_timeout, maximum_connection_buffer_size, - use_legacy_version, } } } @@ -200,9 +198,8 @@ impl SendWithoutResponse for Client { packet: NymPacket, packet_type: PacketType, ) -> io::Result<()> { - trace!("Sending packet to {:?}", address); - let framed_packet = - FramedNymPacket::new(packet, packet_type, self.config.use_legacy_version); + trace!("Sending packet to {address:?}"); + let framed_packet = FramedNymPacket::new(packet, packet_type); if let Some(sender) = self.conn_new.get_mut(&address) { if let Err(err) = sender.channel.try_send(framed_packet) { @@ -260,7 +257,6 @@ mod tests { maximum_reconnection_backoff: Duration::from_millis(300_000), initial_connection_timeout: Duration::from_millis(1_500), maximum_connection_buffer_size: 128, - use_legacy_version: false, }) } diff --git a/common/client-libs/mixnet-client/src/forwarder.rs b/common/client-libs/mixnet-client/src/forwarder.rs index 630cc956632..d7815be92fb 100644 --- a/common/client-libs/mixnet-client/src/forwarder.rs +++ b/common/client-libs/mixnet-client/src/forwarder.rs @@ -1,77 +1,72 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::client::{Client, Config, SendWithoutResponse}; use futures::channel::mpsc; -use futures::StreamExt; -use log::*; +use futures::channel::mpsc::SendError; use nym_sphinx::forwarding::packet::MixPacket; -use std::time::Duration; +use tokio::time::Instant; -pub type MixForwardingSender = mpsc::UnboundedSender; -type MixForwardingReceiver = mpsc::UnboundedReceiver; +pub fn mix_forwarding_channels() -> (MixForwardingSender, MixForwardingReceiver) { + let (tx, rx) = mpsc::unbounded(); + (tx.into(), rx) +} + +#[derive(Clone)] +pub struct MixForwardingSender(mpsc::UnboundedSender); + +impl From> for MixForwardingSender { + fn from(tx: mpsc::UnboundedSender) -> Self { + MixForwardingSender(tx) + } +} + +impl MixForwardingSender { + pub fn forward_packet(&self, packet: impl Into) -> Result<(), SendError> { + self.0 + .unbounded_send(packet.into()) + .map_err(|err| err.into_send_error()) + } -/// A specialisation of client such that it forwards any received packets on the channel into the -/// mix network immediately, i.e. will not try to listen for any responses. -pub struct PacketForwarder { - mixnet_client: Client, - packet_receiver: MixForwardingReceiver, - shutdown: nym_task::TaskClient, + #[allow(clippy::len_without_is_empty)] + pub fn len(&self) -> usize { + self.0.len() + } } -impl PacketForwarder { - pub fn new( - initial_reconnection_backoff: Duration, - maximum_reconnection_backoff: Duration, - initial_connection_timeout: Duration, - maximum_connection_buffer_size: usize, - use_legacy_version: bool, - shutdown: nym_task::TaskClient, - ) -> (PacketForwarder, MixForwardingSender) { - let client_config = Config::new( - initial_reconnection_backoff, - maximum_reconnection_backoff, - initial_connection_timeout, - maximum_connection_buffer_size, - use_legacy_version, - ); +pub type MixForwardingReceiver = mpsc::UnboundedReceiver; - let (packet_sender, packet_receiver) = mpsc::unbounded(); +pub struct PacketToForward { + pub packet: MixPacket, + pub forward_delay_target: Option, +} - ( - PacketForwarder { - mixnet_client: Client::new(client_config), - packet_receiver, - shutdown, - }, - packet_sender, - ) +impl From for PacketToForward { + fn from(packet: MixPacket) -> Self { + PacketToForward::new_no_delay(packet) } +} - pub async fn run(&mut self) { - while !self.shutdown.is_shutdown() { - tokio::select! { - biased; - _ = self.shutdown.recv() => { - log::trace!("PacketForwarder: Received shutdown"); - } - Some(mix_packet) = self.packet_receiver.next() => { - trace!("Going to forward packet to {}", mix_packet.next_hop()); +impl From<(MixPacket, Option)> for PacketToForward { + fn from((packet, delay_until): (MixPacket, Option)) -> Self { + PacketToForward::new(packet, delay_until) + } +} - let next_hop = mix_packet.next_hop(); - let packet_type = mix_packet.packet_type(); - let packet = mix_packet.into_packet(); - // we don't care about responses, we just want to fire packets - // as quickly as possible +impl From<(MixPacket, Instant)> for PacketToForward { + fn from((packet, delay_until): (MixPacket, Instant)) -> Self { + PacketToForward::new(packet, Some(delay_until)) + } +} - if let Err(err) = - self.mixnet_client - .send_without_response(next_hop, packet, packet_type) - { - debug!("failed to forward the packet - {err}") - } - } - } +impl PacketToForward { + pub fn new(packet: MixPacket, forward_delay_target: Option) -> Self { + PacketToForward { + packet, + forward_delay_target, } } + + pub fn new_no_delay(packet: MixPacket) -> Self { + Self::new(packet, None) + } } diff --git a/common/client-libs/mixnet-client/src/lib.rs b/common/client-libs/mixnet-client/src/lib.rs index a63eb5ca03a..5f967e03307 100644 --- a/common/client-libs/mixnet-client/src/lib.rs +++ b/common/client-libs/mixnet-client/src/lib.rs @@ -1,7 +1,9 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +#[cfg(feature = "client")] pub mod client; pub mod forwarder; +#[cfg(feature = "client")] pub use client::{Client, Config, SendWithoutResponse}; diff --git a/common/client-libs/validator-client/src/client.rs b/common/client-libs/validator-client/src/client.rs index 076421bc6ad..cae61b0d07a 100644 --- a/common/client-libs/validator-client/src/client.rs +++ b/common/client-libs/validator-client/src/client.rs @@ -19,8 +19,8 @@ use nym_api_requests::ecash::{ PartialExpirationDateSignatureResponse, VerificationKeyResponse, }; use nym_api_requests::models::{ - ApiHealthResponse, GatewayCoreStatusResponse, MixnodeCoreStatusResponse, MixnodeStatusResponse, - NymNodeDescription, RewardEstimationResponse, StakeSaturationResponse, + ApiHealthResponse, GatewayBondAnnotated, GatewayCoreStatusResponse, MixnodeCoreStatusResponse, + MixnodeStatusResponse, NymNodeDescription, RewardEstimationResponse, StakeSaturationResponse, }; use nym_api_requests::models::{LegacyDescribedGateway, MixNodeBondAnnotated}; use nym_api_requests::nym_nodes::SkimmedNode; @@ -257,6 +257,13 @@ impl Client { Ok(self.nym_api.get_gateways().await?) } + #[deprecated] + pub async fn get_cached_gateways_detailed_unfiltered( + &self, + ) -> Result, ValidatorClientError> { + Ok(self.nym_api.get_gateways_detailed_unfiltered().await?) + } + // TODO: combine with NymApiClient... pub async fn get_all_cached_described_nodes( &self, @@ -351,34 +358,19 @@ impl NymApiClient { } #[deprecated(note = "use get_all_basic_active_mixing_assigned_nodes instead")] - pub async fn get_basic_mixnodes( - &self, - semver_compatibility: Option, - ) -> Result, ValidatorClientError> { - Ok(self - .nym_api - .get_basic_mixnodes(semver_compatibility) - .await? - .nodes) + pub async fn get_basic_mixnodes(&self) -> Result, ValidatorClientError> { + Ok(self.nym_api.get_basic_mixnodes().await?.nodes) } #[deprecated(note = "use get_all_basic_entry_assigned_nodes instead")] - pub async fn get_basic_gateways( - &self, - semver_compatibility: Option, - ) -> Result, ValidatorClientError> { - Ok(self - .nym_api - .get_basic_gateways(semver_compatibility) - .await? - .nodes) + pub async fn get_basic_gateways(&self) -> Result, ValidatorClientError> { + Ok(self.nym_api.get_basic_gateways().await?.nodes) } /// retrieve basic information for nodes are capable of operating as an entry gateway /// this includes legacy gateways and nym-nodes pub async fn get_all_basic_entry_assigned_nodes( &self, - semver_compatibility: Option, ) -> Result, ValidatorClientError> { // TODO: deal with paging in macro or some helper function or something, because it's the same pattern everywhere let mut page = 0; @@ -387,12 +379,7 @@ impl NymApiClient { loop { let mut res = self .nym_api - .get_basic_entry_assigned_nodes( - semver_compatibility.clone(), - false, - Some(page), - None, - ) + .get_basic_entry_assigned_nodes(false, Some(page), None) .await?; nodes.append(&mut res.nodes.data); @@ -410,7 +397,6 @@ impl NymApiClient { /// this includes legacy mixnodes and nym-nodes pub async fn get_all_basic_active_mixing_assigned_nodes( &self, - semver_compatibility: Option, ) -> Result, ValidatorClientError> { // TODO: deal with paging in macro or some helper function or something, because it's the same pattern everywhere let mut page = 0; @@ -419,12 +405,7 @@ impl NymApiClient { loop { let mut res = self .nym_api - .get_basic_active_mixing_assigned_nodes( - semver_compatibility.clone(), - false, - Some(page), - None, - ) + .get_basic_active_mixing_assigned_nodes(false, Some(page), None) .await?; nodes.append(&mut res.nodes.data); @@ -442,7 +423,6 @@ impl NymApiClient { /// this includes legacy mixnodes and nym-nodes pub async fn get_all_basic_mixing_capable_nodes( &self, - semver_compatibility: Option, ) -> Result, ValidatorClientError> { // TODO: deal with paging in macro or some helper function or something, because it's the same pattern everywhere let mut page = 0; @@ -451,12 +431,7 @@ impl NymApiClient { loop { let mut res = self .nym_api - .get_basic_mixing_capable_nodes( - semver_compatibility.clone(), - false, - Some(page), - None, - ) + .get_basic_mixing_capable_nodes(false, Some(page), None) .await?; nodes.append(&mut res.nodes.data); @@ -471,10 +446,7 @@ impl NymApiClient { } /// retrieve basic information for all bonded nodes on the network - pub async fn get_all_basic_nodes( - &self, - semver_compatibility: Option, - ) -> Result, ValidatorClientError> { + pub async fn get_all_basic_nodes(&self) -> Result, ValidatorClientError> { // TODO: deal with paging in macro or some helper function or something, because it's the same pattern everywhere let mut page = 0; let mut nodes = Vec::new(); @@ -482,7 +454,7 @@ impl NymApiClient { loop { let mut res = self .nym_api - .get_basic_nodes(semver_compatibility.clone(), false, Some(page), None) + .get_basic_nodes(false, Some(page), None) .await?; nodes.append(&mut res.nodes.data); diff --git a/common/client-libs/validator-client/src/nym_api/mod.rs b/common/client-libs/validator-client/src/nym_api/mod.rs index 7e54b3f9ec8..ebfb85ba97c 100644 --- a/common/client-libs/validator-client/src/nym_api/mod.rs +++ b/common/client-libs/validator-client/src/nym_api/mod.rs @@ -102,6 +102,23 @@ pub trait NymApiClientExt: ApiClient { .await } + #[deprecated] + #[instrument(level = "debug", skip(self))] + async fn get_gateways_detailed_unfiltered( + &self, + ) -> Result, NymAPIError> { + self.get_json( + &[ + routes::API_VERSION, + routes::STATUS, + routes::GATEWAYS, + routes::DETAILED_UNFILTERED, + ], + NO_PARAMS, + ) + .await + } + #[deprecated] #[instrument(level = "debug", skip(self))] async fn get_mixnodes_detailed_unfiltered( @@ -188,16 +205,7 @@ pub trait NymApiClientExt: ApiClient { #[deprecated] #[tracing::instrument(level = "debug", skip_all)] - async fn get_basic_mixnodes( - &self, - semver_compatibility: Option, - ) -> Result, NymAPIError> { - let params = if let Some(semver_compatibility) = &semver_compatibility { - vec![("semver_compatibility", semver_compatibility.as_str())] - } else { - vec![] - }; - + async fn get_basic_mixnodes(&self) -> Result, NymAPIError> { self.get_json( &[ routes::API_VERSION, @@ -206,23 +214,14 @@ pub trait NymApiClientExt: ApiClient { "mixnodes", "skimmed", ], - ¶ms, + NO_PARAMS, ) .await } #[deprecated] #[instrument(level = "debug", skip(self))] - async fn get_basic_gateways( - &self, - semver_compatibility: Option, - ) -> Result, NymAPIError> { - let params = if let Some(semver_compatibility) = &semver_compatibility { - vec![("semver_compatibility", semver_compatibility.as_str())] - } else { - vec![] - }; - + async fn get_basic_gateways(&self) -> Result, NymAPIError> { self.get_json( &[ routes::API_VERSION, @@ -231,7 +230,7 @@ pub trait NymApiClientExt: ApiClient { "gateways", "skimmed", ], - ¶ms, + NO_PARAMS, ) .await } @@ -241,17 +240,12 @@ pub trait NymApiClientExt: ApiClient { #[instrument(level = "debug", skip(self))] async fn get_basic_entry_assigned_nodes( &self, - semver_compatibility: Option, no_legacy: bool, page: Option, per_page: Option, ) -> Result, NymAPIError> { let mut params = Vec::new(); - if let Some(arg) = &semver_compatibility { - params.push(("semver_compatibility", arg.clone())) - } - if no_legacy { params.push(("no_legacy", "true".to_string())) } @@ -283,17 +277,12 @@ pub trait NymApiClientExt: ApiClient { #[instrument(level = "debug", skip(self))] async fn get_basic_active_mixing_assigned_nodes( &self, - semver_compatibility: Option, no_legacy: bool, page: Option, per_page: Option, ) -> Result, NymAPIError> { let mut params = Vec::new(); - if let Some(arg) = &semver_compatibility { - params.push(("semver_compatibility", arg.clone())) - } - if no_legacy { params.push(("no_legacy", "true".to_string())) } @@ -325,17 +314,12 @@ pub trait NymApiClientExt: ApiClient { #[instrument(level = "debug", skip(self))] async fn get_basic_mixing_capable_nodes( &self, - semver_compatibility: Option, no_legacy: bool, page: Option, per_page: Option, ) -> Result, NymAPIError> { let mut params = Vec::new(); - if let Some(arg) = &semver_compatibility { - params.push(("semver_compatibility", arg.clone())) - } - if no_legacy { params.push(("no_legacy", "true".to_string())) } @@ -365,17 +349,12 @@ pub trait NymApiClientExt: ApiClient { #[instrument(level = "debug", skip(self))] async fn get_basic_nodes( &self, - semver_compatibility: Option, no_legacy: bool, page: Option, per_page: Option, ) -> Result, NymAPIError> { let mut params = Vec::new(); - if let Some(arg) = &semver_compatibility { - params.push(("semver_compatibility", arg.clone())) - } - if no_legacy { params.push(("no_legacy", "true".to_string())) } diff --git a/common/client-libs/validator-client/src/nyxd/coin.rs b/common/client-libs/validator-client/src/nyxd/coin.rs index 0fa175a2436..8050273a51d 100644 --- a/common/client-libs/validator-client/src/nyxd/coin.rs +++ b/common/client-libs/validator-client/src/nyxd/coin.rs @@ -32,7 +32,7 @@ impl Div for Coin { } } -impl<'a> Div for &'a Coin { +impl Div for &Coin { type Output = Gas; fn div(self, rhs: GasPrice) -> Self::Output { diff --git a/common/client-libs/validator-client/src/nyxd/contract_traits/mixnet_query_client.rs b/common/client-libs/validator-client/src/nyxd/contract_traits/mixnet_query_client.rs index 433112e1575..2e1095b3b8b 100644 --- a/common/client-libs/validator-client/src/nyxd/contract_traits/mixnet_query_client.rs +++ b/common/client-libs/validator-client/src/nyxd/contract_traits/mixnet_query_client.rs @@ -26,10 +26,11 @@ use nym_mixnet_contract_common::{ reward_params::{Performance, RewardingParams}, rewarding::{EstimatedCurrentEpochRewardResponse, PendingRewardResponse}, ContractBuildInformation, ContractState, ContractStateParams, CurrentIntervalResponse, - Delegation, EpochEventId, EpochStatus, GatewayBond, GatewayBondResponse, - GatewayOwnershipResponse, IdentityKey, IdentityKeyRef, IntervalEventId, MixNodeBond, - MixNodeDetails, MixOwnershipResponse, MixnodeDetailsByIdentityResponse, MixnodeDetailsResponse, - NodeId, NumberOfPendingEventsResponse, NymNodeBond, NymNodeDetails, + CurrentNymNodeVersionResponse, Delegation, EpochEventId, EpochStatus, GatewayBond, + GatewayBondResponse, GatewayOwnershipResponse, HistoricalNymNodeVersionEntry, IdentityKey, + IdentityKeyRef, IntervalEventId, MixNodeBond, MixNodeDetails, MixOwnershipResponse, + MixnodeDetailsByIdentityResponse, MixnodeDetailsResponse, NodeId, + NumberOfPendingEventsResponse, NymNodeBond, NymNodeDetails, NymNodeVersionHistoryResponse, PagedAllDelegationsResponse, PagedDelegatorDelegationsResponse, PagedGatewayResponse, PagedMixnodeBondsResponse, PagedNodeDelegationsResponse, PendingEpochEvent, PendingEpochEventResponse, PendingEpochEventsResponse, PendingIntervalEvent, @@ -71,6 +72,22 @@ pub trait MixnetQueryClient { .await } + async fn get_nym_node_version_history_paged( + &self, + start_after: Option, + limit: Option, + ) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetNymNodeVersionHistory { limit, start_after }) + .await + } + + async fn get_current_nym_node_version( + &self, + ) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetCurrentNymNodeVersion {}) + .await + } + async fn get_mixnet_contract_state(&self) -> Result { self.query_mixnet_contract(MixnetQueryMsg::GetState {}) .await @@ -638,6 +655,12 @@ pub trait PagedMixnetQueryClient: MixnetQueryClient { ) -> Result, NyxdError> { collect_paged!(self, get_pending_interval_events_paged, events) } + + async fn get_full_nym_node_version_history( + &self, + ) -> Result, NyxdError> { + collect_paged!(self, get_nym_node_version_history_paged, history) + } } #[async_trait] @@ -724,6 +747,7 @@ where mod tests { use super::*; use crate::nyxd::contract_traits::tests::IgnoreValue; + use nym_mixnet_contract_common::QueryMsg; // it's enough that this compiles and clippy is happy about it #[allow(dead_code)] @@ -924,6 +948,10 @@ mod tests { MixnetQueryMsg::GetRewardedSetMetadata {} => { client.get_rewarded_set_metadata().ignore() } + QueryMsg::GetCurrentNymNodeVersion {} => client.get_current_nym_node_version().ignore(), + QueryMsg::GetNymNodeVersionHistory { limit, start_after } => client + .get_nym_node_version_history_paged(start_after, limit) + .ignore(), } } } diff --git a/common/client-libs/validator-client/src/nyxd/cosmwasm_client/helpers.rs b/common/client-libs/validator-client/src/nyxd/cosmwasm_client/helpers.rs index 559ad434a23..0c4c6c8dcb3 100644 --- a/common/client-libs/validator-client/src/nyxd/cosmwasm_client/helpers.rs +++ b/common/client-libs/validator-client/src/nyxd/cosmwasm_client/helpers.rs @@ -13,6 +13,44 @@ use tracing::error; pub use cosmrs::abci::MsgResponse; +pub fn parse_singleton_u32_from_contract_response(b: Vec) -> Result { + if b.len() != 4 { + return Err(NyxdError::MalformedResponseData { + got: b.len(), + expected: 4, + }); + } + Ok(u32::from_be_bytes([b[0], b[1], b[2], b[3]])) +} + +pub fn parse_singleton_u64_from_contract_response(b: Vec) -> Result { + if b.len() != 8 { + return Err(NyxdError::MalformedResponseData { + got: b.len(), + expected: 8, + }); + } + Ok(u64::from_be_bytes([ + b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7], + ])) +} + +#[derive(Debug, Clone)] +pub struct ParsedContractResponse { + pub message_index: usize, + pub response: Vec, +} + +impl ParsedContractResponse { + pub fn parse_singleton_u32_contract_data(self) -> Result { + parse_singleton_u32_from_contract_response(self.response) + } + + pub fn parse_singleton_u64_contract_data(self) -> Result { + parse_singleton_u64_from_contract_response(self.response) + } +} + pub fn parse_msg_responses(data: Bytes) -> Vec { // it seems that currently, on wasmd 0.43 + tendermint-rs 0.37 + cosmrs 0.17.0-pre // the data is left in undecoded base64 form, but I'd imagine this might change so if the decoding fails, @@ -34,35 +72,25 @@ pub fn parse_msg_responses(data: Bytes) -> Vec { } // requires there's a single response message -pub trait ToSingletonContractData: Sized { +pub trait ContractResponseData: Sized { fn parse_singleton_u32_contract_data(&self) -> Result { let b = self.to_singleton_contract_data()?; - if b.len() != 4 { - return Err(NyxdError::MalformedResponseData { - got: b.len(), - expected: 4, - }); - } - Ok(u32::from_be_bytes([b[0], b[1], b[2], b[3]])) + parse_singleton_u32_from_contract_response(b) } fn parse_singleton_u64_contract_data(&self) -> Result { let b = self.to_singleton_contract_data()?; - if b.len() != 8 { - return Err(NyxdError::MalformedResponseData { - got: b.len(), - expected: 8, - }); - } - Ok(u64::from_be_bytes([ - b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7], - ])) + parse_singleton_u64_from_contract_response(b) } fn to_singleton_contract_data(&self) -> Result, NyxdError>; + + fn to_unchecked_contract_data(&self) -> Result>, NyxdError>; + + fn to_contract_data(&self) -> Result, NyxdError>; } -impl ToSingletonContractData for ExecuteResult { +impl ContractResponseData for ExecuteResult { fn to_singleton_contract_data(&self) -> Result, NyxdError> { if self.msg_responses.len() != 1 { return Err(NyxdError::UnexpectedNumberOfMsgResponses { @@ -72,6 +100,30 @@ impl ToSingletonContractData for ExecuteResult { self.msg_responses[0].to_contract_response_data() } + + fn to_unchecked_contract_data(&self) -> Result>, NyxdError> { + self.msg_responses + .iter() + .map(ToContractResponseData::to_contract_response_data) + .collect() + } + + fn to_contract_data(&self) -> Result, NyxdError> { + let mut response = Vec::new(); + + for (message_index, msg) in self.msg_responses.iter().enumerate() { + // unfortunately `Name` trait has not been derived for `MsgExecuteContractResponse`, + // so we have to make an explicit string comparison instead + if msg.type_url == "/cosmwasm.wasm.v1.MsgExecuteContractResponse" { + response.push(ParsedContractResponse { + message_index, + response: msg.to_contract_response_data()?, + }) + } + } + + Ok(response) + } } pub trait ToContractResponseData: Sized { diff --git a/common/client-libs/validator-client/src/nyxd/cosmwasm_client/mod.rs b/common/client-libs/validator-client/src/nyxd/cosmwasm_client/mod.rs index a0b65bfdd93..65165883396 100644 --- a/common/client-libs/validator-client/src/nyxd/cosmwasm_client/mod.rs +++ b/common/client-libs/validator-client/src/nyxd/cosmwasm_client/mod.rs @@ -23,7 +23,7 @@ use tendermint_rpc::endpoint::*; use tendermint_rpc::query::Query; use tendermint_rpc::{Error as TendermintRpcError, Order, Paging, SimpleRequest}; -pub use helpers::{ToContractResponseData, ToSingletonContractData}; +pub use helpers::{ContractResponseData, ToContractResponseData}; #[cfg(feature = "http-client")] use crate::http_client; diff --git a/common/client-libs/validator-client/src/nyxd/fee/gas_price.rs b/common/client-libs/validator-client/src/nyxd/fee/gas_price.rs index 4f0f2fd0a8e..3679cc7500b 100644 --- a/common/client-libs/validator-client/src/nyxd/fee/gas_price.rs +++ b/common/client-libs/validator-client/src/nyxd/fee/gas_price.rs @@ -22,7 +22,7 @@ pub struct GasPrice { pub denom: String, } -impl<'a> Mul for &'a GasPrice { +impl Mul for &GasPrice { type Output = Coin; fn mul(self, gas_limit: Gas) -> Self::Output { diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/config_score.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/config_score.rs new file mode 100644 index 00000000000..a5d8ed44f44 --- /dev/null +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/config_score.rs @@ -0,0 +1,673 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use cosmwasm_schema::cw_serde; +use cosmwasm_std::Decimal; +use std::cmp::Ordering; +use std::ops::{Add, Sub}; + +#[cw_serde] +pub struct HistoricalNymNodeVersion { + /// Version of the nym node that is going to be used for determining the version score of a node. + /// note: value stored here is pre-validated `semver::Version` + pub semver: String, + + /// Block height of when this version has been added to the contract + pub introduced_at_height: u64, + + /// The absolute version difference as compared against the first version introduced into the contract. + pub difference_since_genesis: TotalVersionDifference, +} + +impl HistoricalNymNodeVersion { + pub fn genesis(semver: String, height: u64) -> HistoricalNymNodeVersion { + HistoricalNymNodeVersion { + semver, + introduced_at_height: height, + difference_since_genesis: Default::default(), + } + } + + // SAFETY: the value stored in the contract is always valid + // if you manually construct that struct with invalid value, it's on you. + #[allow(clippy::unwrap_used)] + pub fn semver_unchecked(&self) -> semver::Version { + self.semver.parse().unwrap() + } + + /// Return [`TotalVersionDifference`] for a new release version that is going to be pushed right after this one + /// this function cannot be called against 2 arbitrary versions + #[inline] + pub fn cumulative_difference_since_genesis( + &self, + new_version: &semver::Version, + ) -> TotalVersionDifference { + let self_semver = self.semver_unchecked(); + let mut new_absolute = self.difference_since_genesis; + if new_version.major > self_semver.major { + new_absolute.major += (new_version.major - self_semver.major) as u32 + } else if new_version.minor > self_semver.minor { + new_absolute.minor += (new_version.minor - self_semver.minor) as u32 + } else if new_version.patch > self_semver.patch { + new_absolute.patch += (new_version.patch - self_semver.patch) as u32 + } else if new_version.pre != self_semver.pre { + new_absolute.prerelease += 1 + } + new_absolute + } + + pub fn relative_difference(&self, other: &Self) -> TotalVersionDifference { + if self.difference_since_genesis > other.difference_since_genesis { + self.difference_since_genesis - other.difference_since_genesis + } else { + other.difference_since_genesis - self.difference_since_genesis + } + } + + pub fn difference_against_legacy( + &self, + legacy_version: &semver::Version, + ) -> TotalVersionDifference { + let current = self.semver_unchecked(); + let major_diff = (current.major as i64 - legacy_version.major as i64).unsigned_abs() as u32; + let minor_diff = (current.minor as i64 - legacy_version.minor as i64).unsigned_abs() as u32; + let patch_diff = (current.patch as i64 - legacy_version.patch as i64).unsigned_abs() as u32; + let prerelease_diff = if current.pre == legacy_version.pre { + 0 + } else { + 1 + }; + + let mut diff = TotalVersionDifference::default(); + // if there's a major increase, ignore minor and patch and treat it as 0 + if major_diff != 0 { + diff.major += major_diff; + return diff; + } + + // if there's a minor increase, ignore patch and treat is as 0 + if minor_diff != 0 { + diff.minor += minor_diff; + return diff; + } + + diff.patch = patch_diff; + diff.prerelease = prerelease_diff; + diff + } +} + +#[cw_serde] +#[derive(Default, Copy, PartialOrd, Ord, Eq)] +pub struct TotalVersionDifference { + pub major: u32, + pub minor: u32, + pub patch: u32, + pub prerelease: u32, +} + +impl Add for TotalVersionDifference { + type Output = TotalVersionDifference; + fn add(self, rhs: TotalVersionDifference) -> Self::Output { + TotalVersionDifference { + major: self.major.add(rhs.major), + minor: self.minor.add(rhs.minor), + patch: self.patch.add(rhs.patch), + prerelease: self.prerelease.add(rhs.prerelease), + } + } +} + +impl Sub for TotalVersionDifference { + type Output = TotalVersionDifference; + fn sub(self, rhs: TotalVersionDifference) -> Self::Output { + TotalVersionDifference { + major: self.major.saturating_sub(rhs.major), + minor: self.minor.saturating_sub(rhs.minor), + patch: self.patch.saturating_sub(rhs.patch), + prerelease: self.prerelease.saturating_sub(rhs.prerelease), + } + } +} + +#[cw_serde] +pub struct HistoricalNymNodeVersionEntry { + /// The unique, ordered, id of this particular entry + pub id: u32, + + /// Data associated with this particular version + pub version_information: HistoricalNymNodeVersion, +} + +impl From<(u32, HistoricalNymNodeVersion)> for HistoricalNymNodeVersionEntry { + fn from((id, version_information): (u32, HistoricalNymNodeVersion)) -> Self { + HistoricalNymNodeVersionEntry { + id, + version_information, + } + } +} + +impl PartialOrd for HistoricalNymNodeVersionEntry { + fn partial_cmp(&self, other: &Self) -> Option { + // we only care about id for the purposes of ordering as they should have unique data + self.id.partial_cmp(&other.id) + } +} + +#[cw_serde] +pub struct NymNodeVersionHistoryResponse { + pub history: Vec, + + /// Field indicating paging information for the following queries if the caller wishes to get further entries. + pub start_next_after: Option, +} + +#[cw_serde] +pub struct CurrentNymNodeVersionResponse { + pub version: Option, +} + +#[cw_serde] +pub struct ConfigScoreParams { + /// Defines weights for calculating numbers of versions behind the current release. + pub version_weights: OutdatedVersionWeights, + + /// Defines the parameters of the formula for calculating the version score + pub version_score_formula_params: VersionScoreFormulaParams, +} + +/// Defines weights for calculating numbers of versions behind the current release. +#[cw_serde] +#[derive(Copy)] +pub struct OutdatedVersionWeights { + pub major: u32, + pub minor: u32, + pub patch: u32, + pub prerelease: u32, +} + +fn is_one_semver_difference(this: &semver::Version, other: &semver::Version) -> bool { + let major_diff = (this.major as i64 - other.major as i64).unsigned_abs() as u32; + let minor_diff = (this.minor as i64 - other.minor as i64).unsigned_abs() as u32; + let patch_diff = (this.patch as i64 - other.patch as i64).unsigned_abs() as u32; + let prerelease_diff = if this.pre == other.pre { 0 } else { 1 }; + + if major_diff == 1 { + return true; + } + + if major_diff == 0 && minor_diff == 1 { + return true; + } + + if major_diff == 0 && minor_diff == 0 && patch_diff == 1 { + return true; + } + + prerelease_diff == 1 +} + +impl OutdatedVersionWeights { + pub fn difference_to_versions_behind_factor(&self, diff: TotalVersionDifference) -> u32 { + diff.major * self.major + + diff.minor * self.minor + + diff.patch * self.patch + + diff.prerelease * self.prerelease + } + + // INVARIANT: release chain is sorted + // do NOT call this method directly from inside the contract. it's too inefficient + // it relies on some external caching. + pub fn versions_behind_factor( + &self, + node_version: &semver::Version, + release_chain: &[HistoricalNymNodeVersionEntry], + ) -> u32 { + let Some(latest) = release_chain.last() else { + return 0; + }; + + let latest_semver = latest.version_information.semver_unchecked(); + + // if you're more recent than the latest, you get the benefit of the doubt, the release might have not yet been commited to the chain + // but only if you're only a single semver ahead, otherwise you get penalty equivalent of being major version behind for cheating + if node_version > &latest_semver { + return if is_one_semver_difference(node_version, &latest_semver) { + 0 + } else { + self.major + }; + } + + // find your position in the release chain, if we fail, we assume that the node comes from before the changes were introduced + // in which case we simply calculate the absolute difference between the genesis entry and add up the total difference + let version_diff = match release_chain + .iter() + .rfind(|h| &h.version_information.semver_unchecked() <= node_version) + { + Some(h) => { + // first chain entry that is smaller (or equal) to the provided node version + // now, calculate the difference to the genesis version and ultimately against the current head + let diff_since_genesis = if h.version_information.semver == node_version.to_string() + { + h.version_information.difference_since_genesis + } else { + h.version_information + .cumulative_difference_since_genesis(node_version) + }; + latest.version_information.difference_since_genesis - diff_since_genesis + } + None => { + // SAFETY: since we managed to get 'last' entry, it means the release chain is not empty, + // so we must be able to obtain the first entry + #[allow(clippy::unwrap_used)] + let genesis = release_chain.first().unwrap(); + + let difference_from_genesis = genesis + .version_information + .difference_against_legacy(node_version); + difference_from_genesis + latest.version_information.difference_since_genesis + } + }; + + self.difference_to_versions_behind_factor(version_diff) + } +} + +impl Default for OutdatedVersionWeights { + fn default() -> Self { + OutdatedVersionWeights { + major: 100, + minor: 10, + patch: 1, + prerelease: 1, + } + } +} + +/// Given the formula of version_score = penalty ^ (versions_behind_factor ^ penalty_scaling) +/// define the relevant parameters +#[cw_serde] +#[derive(Copy)] +pub struct VersionScoreFormulaParams { + pub penalty: Decimal, + pub penalty_scaling: Decimal, +} + +impl Default for VersionScoreFormulaParams { + fn default() -> Self { + #[allow(clippy::unwrap_used)] + VersionScoreFormulaParams { + penalty: "0.995".parse().unwrap(), + penalty_scaling: "1.65".parse().unwrap(), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::ops::Deref; + + // simple wrapper for tests + struct ReleaseChain { + inner: Vec, + } + + impl Deref for ReleaseChain { + type Target = [HistoricalNymNodeVersionEntry]; + fn deref(&self) -> &Self::Target { + self.inner.deref() + } + } + + impl ReleaseChain { + fn new(initial: &str) -> Self { + ReleaseChain { + inner: vec![HistoricalNymNodeVersionEntry { + id: 0, + version_information: HistoricalNymNodeVersion { + semver: initial.to_string(), + introduced_at_height: 123, + difference_since_genesis: TotalVersionDifference::default(), + }, + }], + } + } + + fn with_release(mut self, raw: &str) -> Self { + self.push_new(raw); + self + } + + fn push_new(&mut self, raw: &str) { + let latest = self.inner.last().unwrap(); + let new_version: semver::Version = raw.parse().unwrap(); + + let new_absolute = latest + .version_information + .cumulative_difference_since_genesis(&new_version); + + self.inner.push(HistoricalNymNodeVersionEntry { + id: latest.id + 1, + version_information: HistoricalNymNodeVersion { + semver: new_version.to_string(), + introduced_at_height: latest.version_information.introduced_at_height + 1, + difference_since_genesis: new_absolute, + }, + }) + } + } + + #[test] + fn versions_behind_factor() { + // helper to compact the parsing + fn s(raw: &str) -> semver::Version { + raw.parse().unwrap() + } + + let weights = OutdatedVersionWeights::default(); + + // no releases: + let res = weights.versions_behind_factor(&s("1.1.13"), &[]); + assert_eq!(0, res); + + // ############################### + // single released version (1.1.13) + // ############################### + let mut release_chain = ReleaseChain::new("1.1.13"); + + // "legacy" versions + let res = weights.versions_behind_factor(&s("1.0.12"), &release_chain); + assert_eq!(10, res); + let res = weights.versions_behind_factor(&s("1.0.4"), &release_chain); + assert_eq!(10, res); + let res = weights.versions_behind_factor(&s("1.0.1"), &release_chain); + assert_eq!(10, res); + let res = weights.versions_behind_factor(&s("0.1.12"), &release_chain); + assert_eq!(100, res); + + let res = weights.versions_behind_factor(&s("1.1.12"), &release_chain); + assert_eq!(1, res); + let res = weights.versions_behind_factor(&s("1.1.11"), &release_chain); + assert_eq!(2, res); + let res = weights.versions_behind_factor(&s("1.1.9"), &release_chain); + assert_eq!(4, res); + + // current version + let res = weights.versions_behind_factor(&s("1.1.13"), &release_chain); + assert_eq!(0, res); + + // "ahead" versions + let res = weights.versions_behind_factor(&s("1.1.14"), &release_chain); + assert_eq!(0, res); + let res = weights.versions_behind_factor(&s("1.2.0"), &release_chain); + assert_eq!(0, res); + let res = weights.versions_behind_factor(&s("2.0.0"), &release_chain); + assert_eq!(0, res); + + // cheating ahead: + let res = weights.versions_behind_factor(&s("1.1.15"), &release_chain); + assert_eq!(100, res); + let res = weights.versions_behind_factor(&s("1.3.0"), &release_chain); + assert_eq!(100, res); + let res = weights.versions_behind_factor(&s("3.0.0"), &release_chain); + assert_eq!(100, res); + + // ############################### + // small patch release chain (1.1.13 => 1.1.14 => 1.1.15 => 1.1.16) + // ############################### + release_chain.push_new("1.1.14"); + release_chain.push_new("1.1.15"); + release_chain.push_new("1.1.16"); + + // "legacy" versions + let res = weights.versions_behind_factor(&s("1.0.12"), &release_chain); + assert_eq!(13, res); + let res = weights.versions_behind_factor(&s("1.0.4"), &release_chain); + assert_eq!(13, res); + let res = weights.versions_behind_factor(&s("1.0.1"), &release_chain); + assert_eq!(13, res); + let res = weights.versions_behind_factor(&s("0.1.12"), &release_chain); + assert_eq!(103, res); + + let res = weights.versions_behind_factor(&s("1.1.12"), &release_chain); + assert_eq!(4, res); + let res = weights.versions_behind_factor(&s("1.1.11"), &release_chain); + assert_eq!(5, res); + let res = weights.versions_behind_factor(&s("1.1.9"), &release_chain); + assert_eq!(7, res); + + // current version + let res = weights.versions_behind_factor(&s("1.1.16"), &release_chain); + assert_eq!(0, res); + + // present in the chain + let res = weights.versions_behind_factor(&s("1.1.15"), &release_chain); + assert_eq!(1, res); + let res = weights.versions_behind_factor(&s("1.1.14"), &release_chain); + assert_eq!(2, res); + let res = weights.versions_behind_factor(&s("1.1.13"), &release_chain); + assert_eq!(3, res); + + // "ahead" versions + let res = weights.versions_behind_factor(&s("1.1.17"), &release_chain); + assert_eq!(0, res); + let res = weights.versions_behind_factor(&s("1.2.0"), &release_chain); + assert_eq!(0, res); + let res = weights.versions_behind_factor(&s("2.0.0"), &release_chain); + assert_eq!(0, res); + + // cheating ahead: + let res = weights.versions_behind_factor(&s("1.1.18"), &release_chain); + assert_eq!(100, res); + let res = weights.versions_behind_factor(&s("1.3.0"), &release_chain); + assert_eq!(100, res); + let res = weights.versions_behind_factor(&s("3.0.0"), &release_chain); + assert_eq!(100, res); + + // ############################### + // small minor release chain (1.2.0 => 1.3.0 => 1.4.0) + // ############################### + let release_chain = ReleaseChain::new("1.2.0") + .with_release("1.3.0") + .with_release("1.4.0"); + + // "legacy" versions + let res = weights.versions_behind_factor(&s("1.0.12"), &release_chain); + assert_eq!(40, res); + let res = weights.versions_behind_factor(&s("1.0.4"), &release_chain); + assert_eq!(40, res); + let res = weights.versions_behind_factor(&s("1.0.1"), &release_chain); + assert_eq!(40, res); + let res = weights.versions_behind_factor(&s("0.1.12"), &release_chain); + assert_eq!(120, res); + + let res = weights.versions_behind_factor(&s("1.1.12"), &release_chain); + assert_eq!(30, res); + let res = weights.versions_behind_factor(&s("1.1.11"), &release_chain); + assert_eq!(30, res); + let res = weights.versions_behind_factor(&s("1.1.9"), &release_chain); + assert_eq!(30, res); + + // current version + let res = weights.versions_behind_factor(&s("1.4.0"), &release_chain); + assert_eq!(0, res); + + // present in the chain + let res = weights.versions_behind_factor(&s("1.2.0"), &release_chain); + assert_eq!(20, res); + let res = weights.versions_behind_factor(&s("1.3.0"), &release_chain); + assert_eq!(10, res); + + // weird in between + let res = weights.versions_behind_factor(&s("1.2.1"), &release_chain); + assert_eq!(20, res); + let res = weights.versions_behind_factor(&s("1.3.3"), &release_chain); + assert_eq!(10, res); + + // "ahead" versions + let res = weights.versions_behind_factor(&s("1.4.1"), &release_chain); + assert_eq!(0, res); + let res = weights.versions_behind_factor(&s("1.5.0"), &release_chain); + assert_eq!(0, res); + let res = weights.versions_behind_factor(&s("2.0.0"), &release_chain); + assert_eq!(0, res); + + // cheating ahead: + let res = weights.versions_behind_factor(&s("1.4.2"), &release_chain); + assert_eq!(100, res); + let res = weights.versions_behind_factor(&s("1.6.0"), &release_chain); + assert_eq!(100, res); + let res = weights.versions_behind_factor(&s("3.0.0"), &release_chain); + assert_eq!(100, res); + + // ############################### + // mixed release chain (1.1.13 => 1.2.0 => 1.2.1 => 1.3.0 => 1.3.1 => 1.3.2 => 1.4.0) + // ############################### + let release_chain = ReleaseChain::new("1.1.13") + .with_release("1.2.0") + .with_release("1.2.1") + .with_release("1.3.0") + .with_release("1.3.1-importantpre") + .with_release("1.3.1") + .with_release("1.3.2") + .with_release("1.4.0"); + + // "legacy" versions + let res = weights.versions_behind_factor(&s("1.0.12"), &release_chain); + assert_eq!(44, res); + let res = weights.versions_behind_factor(&s("1.0.4"), &release_chain); + assert_eq!(44, res); + let res = weights.versions_behind_factor(&s("1.0.1"), &release_chain); + assert_eq!(44, res); + let res = weights.versions_behind_factor(&s("0.1.12"), &release_chain); + assert_eq!(134, res); + + let res = weights.versions_behind_factor(&s("1.1.12"), &release_chain); + assert_eq!(35, res); + let res = weights.versions_behind_factor(&s("1.1.11"), &release_chain); + assert_eq!(36, res); + let res = weights.versions_behind_factor(&s("1.1.9"), &release_chain); + assert_eq!(38, res); + + // current version + let res = weights.versions_behind_factor(&s("1.4.0"), &release_chain); + assert_eq!(0, res); + + // present in the chain + let res = weights.versions_behind_factor(&s("1.1.13"), &release_chain); + assert_eq!(34, res); + let res = weights.versions_behind_factor(&s("1.2.0"), &release_chain); + assert_eq!(24, res); + let res = weights.versions_behind_factor(&s("1.2.1"), &release_chain); + assert_eq!(23, res); + let res = weights.versions_behind_factor(&s("1.3.0"), &release_chain); + assert_eq!(13, res); + let res = weights.versions_behind_factor(&s("1.3.1-importantpre"), &release_chain); + assert_eq!(12, res); + let res = weights.versions_behind_factor(&s("1.3.1"), &release_chain); + assert_eq!(11, res); + let res = weights.versions_behind_factor(&s("1.3.2"), &release_chain); + assert_eq!(10, res); + + // weird in between + let res = weights.versions_behind_factor(&s("1.2.3"), &release_chain); + assert_eq!(21, res); + let res = weights.versions_behind_factor(&s("1.3.69"), &release_chain); + assert_eq!(10, res); + + // "ahead" versions + let res = weights.versions_behind_factor(&s("1.4.1"), &release_chain); + assert_eq!(0, res); + let res = weights.versions_behind_factor(&s("1.5.0"), &release_chain); + assert_eq!(0, res); + let res = weights.versions_behind_factor(&s("2.0.0"), &release_chain); + assert_eq!(0, res); + + // cheating ahead: + let res = weights.versions_behind_factor(&s("1.4.2"), &release_chain); + assert_eq!(100, res); + let res = weights.versions_behind_factor(&s("1.6.0"), &release_chain); + assert_eq!(100, res); + let res = weights.versions_behind_factor(&s("3.0.0"), &release_chain); + assert_eq!(100, res); + + // ############################### + // skipped patch chain (1.1.13 => 1.2.0 => 1.2.1 => 1.2.4 => [1.3.0]) + // ############################### + let mut release_chain = ReleaseChain::new("1.1.13") + .with_release("1.2.0") + .with_release("1.2.1") + .with_release("1.2.4"); + + // current + let res = weights.versions_behind_factor(&s("1.2.4"), &release_chain); + assert_eq!(0, res); + + // on 'skipped' version + let res = weights.versions_behind_factor(&s("1.2.2"), &release_chain); + assert_eq!(2, res); + + // on version before the skip + let res = weights.versions_behind_factor(&s("1.2.1"), &release_chain); + assert_eq!(3, res); + + release_chain.push_new("1.3.0"); + // current + let res = weights.versions_behind_factor(&s("1.3.0"), &release_chain); + assert_eq!(0, res); + + // on 'skipped' version + let res = weights.versions_behind_factor(&s("1.2.2"), &release_chain); + assert_eq!(12, res); + + // on version before the skip + let res = weights.versions_behind_factor(&s("1.2.1"), &release_chain); + assert_eq!(13, res); + + // ############################### + // skipped minor chain (1.1.13 => 1.2.0 => 1.2.1 => 1.4.0 => [1.5.0]) + // ############################### + let mut release_chain = ReleaseChain::new("1.1.13") + .with_release("1.2.0") + .with_release("1.2.1") + .with_release("1.4.0"); + + // current + let res = weights.versions_behind_factor(&s("1.4.0"), &release_chain); + assert_eq!(0, res); + + // on 'skipped' version + let res = weights.versions_behind_factor(&s("1.3.0"), &release_chain); + assert_eq!(10, res); + + // on version before the skip + let res = weights.versions_behind_factor(&s("1.2.1"), &release_chain); + assert_eq!(20, res); + + let res = weights.versions_behind_factor(&s("1.2.0"), &release_chain); + assert_eq!(21, res); + + release_chain.push_new("1.5.0"); + + // current + let res = weights.versions_behind_factor(&s("1.5.0"), &release_chain); + assert_eq!(0, res); + + let res = weights.versions_behind_factor(&s("1.4.0"), &release_chain); + assert_eq!(10, res); + + // on 'skipped' version + let res = weights.versions_behind_factor(&s("1.3.0"), &release_chain); + assert_eq!(20, res); + + // on version before the skip + let res = weights.versions_behind_factor(&s("1.2.1"), &release_chain); + assert_eq!(30, res); + + let res = weights.versions_behind_factor(&s("1.2.0"), &release_chain); + assert_eq!(31, res); + } +} diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/error.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/error.rs index c34686151d2..e8349d4eb0e 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/error.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/error.rs @@ -275,6 +275,9 @@ pub enum MixnetContractError { #[error("the provided nym-node version is not a valid semver. got: {provided}")] InvalidNymNodeSemver { provided: String }, + + #[error("the provided nym-node version is not greater than the current one. got: {provided}. current: {current}")] + NonIncreasingSemver { provided: String, current: String }, } impl MixnetContractError { diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/events.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/events.rs index e45f7a38fef..b29fba4bd06 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/events.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/events.rs @@ -141,6 +141,7 @@ pub const NEW_INTERVAL_OPERATING_COST_RANGE_KEY: &str = "new_interval_operating_ pub const NEW_VERSION_WEIGHTS_RANGE_KEY: &str = "new_version_weights_range"; pub const NEW_VERSION_SCORE_FORMULA_PARAMS_KEY: &str = "new_version_score_formula_params"; pub const NYM_NODE_CURRENT_SEMVER_KEY: &str = "new_current_semver"; +pub const NYM_NODE_CURRENT_SEMVER_ID_KEY: &str = "new_current_semver_id"; pub const OLD_REWARDING_VALIDATOR_ADDRESS_KEY: &str = "old_rewarding_validator_address"; pub const NEW_REWARDING_VALIDATOR_ADDRESS_KEY: &str = "new_rewarding_validator_address"; @@ -481,12 +482,6 @@ pub fn new_settings_update_event(update: &ContractStateParamsUpdate) -> Event { // check for config score params updates if let Some(config_score_update) = &update.config_score_params { - if let Some(current_nym_node_semver) = &config_score_update.current_nym_node_semver { - event.attributes.push(attr( - NYM_NODE_CURRENT_SEMVER_KEY, - current_nym_node_semver.to_string(), - )) - } if let Some(version_weights) = &config_score_update.version_weights { event.attributes.push(attr( NEW_VERSION_WEIGHTS_RANGE_KEY, @@ -506,9 +501,10 @@ pub fn new_settings_update_event(update: &ContractStateParamsUpdate) -> Event { event } -pub fn new_update_nym_node_semver_event(new_version: &str) -> Event { +pub fn new_update_nym_node_semver_event(new_version: &str, new_id: u32) -> Event { Event::new(MixnetEventType::NymNodeSemverUpdate) .add_attribute(NYM_NODE_CURRENT_SEMVER_KEY, new_version) + .add_attribute(NYM_NODE_CURRENT_SEMVER_ID_KEY, new_id.to_string()) } pub fn new_not_found_node_operator_rewarding_event(interval: Interval, node_id: NodeId) -> Event { diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/interval.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/interval.rs index e36935d66cf..2259af4c7a9 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/interval.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/interval.rs @@ -32,7 +32,7 @@ pub(crate) mod string_rfc3339_offset_date_time { struct Rfc3339OffsetDateTimeVisitor; - impl<'de> Visitor<'de> for Rfc3339OffsetDateTimeVisitor { + impl Visitor<'_> for Rfc3339OffsetDateTimeVisitor { type Value = OffsetDateTime; fn expecting(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result { diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/lib.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/lib.rs index fca9f6bfcf8..4f9d7a088fb 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/lib.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/lib.rs @@ -5,6 +5,7 @@ #![warn(clippy::unwrap_used)] #![warn(clippy::todo)] +mod config_score; pub mod constants; pub mod delegation; pub mod error; @@ -21,6 +22,7 @@ pub mod rewarding; pub mod signing_types; pub mod types; +pub use config_score::*; pub use constants::*; pub use contracts_common::types::*; pub use cosmwasm_std::{Addr, Coin, Decimal, Fraction}; diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs index 529bf53cc2f..214e7e95dfa 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs @@ -25,6 +25,7 @@ use std::time::Duration; #[cfg(feature = "schema")] use crate::{ + config_score::{CurrentNymNodeVersionResponse, NymNodeVersionHistoryResponse}, delegation::{ NodeDelegationResponse, PagedAllDelegationsResponse, PagedDelegatorDelegationsResponse, PagedNodeDelegationsResponse, @@ -423,6 +424,20 @@ pub enum QueryMsg { #[cfg_attr(feature = "schema", returns(ContractState))] GetState {}, + /// Get the current expected version of a Nym Node. + #[cfg_attr(feature = "schema", returns(CurrentNymNodeVersionResponse))] + GetCurrentNymNodeVersion {}, + + /// Get the version history of Nym Node. + #[cfg_attr(feature = "schema", returns(NymNodeVersionHistoryResponse))] + GetNymNodeVersionHistory { + /// Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default. + limit: Option, + + /// Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response. + start_after: Option, + }, + /// Gets the current parameters used for reward calculation. #[cfg_attr(feature = "schema", returns(RewardingParams))] GetRewardingParams {}, diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/types.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/types.rs index 802754fd5a6..eb5f972e4b5 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/types.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/types.rs @@ -1,11 +1,12 @@ // Copyright 2021-2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use crate::config_score::{ConfigScoreParams, OutdatedVersionWeights, VersionScoreFormulaParams}; use crate::nym_node::Role; use contracts_common::Percent; use cosmwasm_schema::cw_serde; +use cosmwasm_std::Coin; use cosmwasm_std::{Addr, Uint128}; -use cosmwasm_std::{Coin, Decimal}; use std::fmt::{Display, Formatter}; // type aliases for better reasoning about available data @@ -221,96 +222,14 @@ impl OperatorsParamsUpdate { } } -#[cw_serde] -pub struct ConfigScoreParams { - /// Current version of the nym node that is going to be used for determining the version score of a node. - /// note: value stored here is pre-validated `semver::Version` - pub current_nym_node_semver: String, - - /// Defines weights for calculating numbers of versions behind the current release. - pub version_weights: OutdatedVersionWeights, - - /// Defines the parameters of the formula for calculating the version score - pub version_score_formula_params: VersionScoreFormulaParams, -} - -impl ConfigScoreParams { - // SAFETY: the value stored in the contract is always valid - #[allow(clippy::unwrap_used)] - pub fn unchecked_nym_node_version(&self) -> semver::Version { - self.current_nym_node_semver.parse().unwrap() - } - - pub fn versions_behind(&self, node_semver: &semver::Version) -> u32 { - let expected = self.unchecked_nym_node_version(); - - let major_diff = (node_semver.major as i64 - expected.major as i64).unsigned_abs() as u32; - let minor_diff = (node_semver.minor as i64 - expected.minor as i64).unsigned_abs() as u32; - let patch_diff = (node_semver.patch as i64 - expected.patch as i64).unsigned_abs() as u32; - let prerelease_diff = if node_semver.pre == expected.pre { - 0 - } else { - 1 - }; - - major_diff * self.version_weights.major - + minor_diff * self.version_weights.minor - + patch_diff * self.version_weights.patch - + prerelease_diff * self.version_weights.prerelease - } -} - -/// Defines weights for calculating numbers of versions behind the current release. -#[cw_serde] -#[derive(Copy)] -pub struct OutdatedVersionWeights { - pub major: u32, - pub minor: u32, - pub patch: u32, - pub prerelease: u32, -} - -impl Default for OutdatedVersionWeights { - fn default() -> Self { - OutdatedVersionWeights { - major: 100, - minor: 10, - patch: 1, - prerelease: 1, - } - } -} - -/// Given the formula of version_score = penalty ^ (num_versions_behind ^ penalty_scaling) -/// define the relevant parameters -#[cw_serde] -#[derive(Copy)] -pub struct VersionScoreFormulaParams { - pub penalty: Decimal, - pub penalty_scaling: Decimal, -} - -impl Default for VersionScoreFormulaParams { - fn default() -> Self { - #[allow(clippy::unwrap_used)] - VersionScoreFormulaParams { - penalty: "0.8".parse().unwrap(), - penalty_scaling: "2.0".parse().unwrap(), - } - } -} - #[cw_serde] pub struct ConfigScoreParamsUpdate { - pub current_nym_node_semver: Option, pub version_weights: Option, pub version_score_formula_params: Option, } impl ConfigScoreParamsUpdate { pub fn contains_updates(&self) -> bool { - self.current_nym_node_semver.is_some() - || self.version_weights.is_some() - || self.version_score_formula_params.is_some() + self.version_weights.is_some() || self.version_score_formula_params.is_some() } } diff --git a/common/credential-verification/src/bandwidth_storage_manager.rs b/common/credential-verification/src/bandwidth_storage_manager.rs index 3e35fd9eb2b..19df1dba6f7 100644 --- a/common/credential-verification/src/bandwidth_storage_manager.rs +++ b/common/credential-verification/src/bandwidth_storage_manager.rs @@ -7,7 +7,7 @@ use crate::ClientBandwidth; use nym_credentials::ecash::utils::ecash_today; use nym_credentials_interface::Bandwidth; use nym_gateway_requests::ServerResponse; -use nym_gateway_storage::Storage; +use nym_gateway_storage::GatewayStorage; use si_scale::helpers::bibytes2; use time::OffsetDateTime; use tracing::*; @@ -15,17 +15,17 @@ use tracing::*; const FREE_TESTNET_BANDWIDTH_VALUE: Bandwidth = Bandwidth::new_unchecked(64 * 1024 * 1024 * 1024); // 64GB #[derive(Clone)] -pub struct BandwidthStorageManager { - pub(crate) storage: S, +pub struct BandwidthStorageManager { + pub(crate) storage: GatewayStorage, pub(crate) client_bandwidth: ClientBandwidth, pub(crate) client_id: i64, pub(crate) bandwidth_cfg: BandwidthFlushingBehaviourConfig, pub(crate) only_coconut_credentials: bool, } -impl BandwidthStorageManager { +impl BandwidthStorageManager { pub fn new( - storage: S, + storage: GatewayStorage, client_bandwidth: ClientBandwidth, client_id: i64, bandwidth_cfg: BandwidthFlushingBehaviourConfig, @@ -111,7 +111,7 @@ impl BandwidthStorageManager { } #[instrument(level = "trace", skip_all)] - async fn sync_storage_bandwidth(&mut self) -> Result<()> { + pub async fn sync_storage_bandwidth(&mut self) -> Result<()> { trace!("syncing client bandwidth with the underlying storage"); let updated = self .storage diff --git a/common/credential-verification/src/client_bandwidth.rs b/common/credential-verification/src/client_bandwidth.rs index 9b764714e88..d98f89b5117 100644 --- a/common/credential-verification/src/client_bandwidth.rs +++ b/common/credential-verification/src/client_bandwidth.rs @@ -8,8 +8,8 @@ use std::time::Duration; use time::OffsetDateTime; use tokio::sync::RwLock; -const DEFAULT_CLIENT_BANDWIDTH_MAX_FLUSHING_RATE: Duration = Duration::from_millis(5); -const DEFAULT_CLIENT_BANDWIDTH_MAX_DELTA_FLUSHING_AMOUNT: i64 = 512 * 1024; // 512kB +const DEFAULT_CLIENT_BANDWIDTH_MAX_FLUSHING_RATE: Duration = Duration::from_secs(5 * 60); // 5 minutes +const DEFAULT_CLIENT_BANDWIDTH_MAX_DELTA_FLUSHING_AMOUNT: i64 = 5 * 1024 * 1024; // 5MB #[derive(Debug, Clone, Copy)] pub struct BandwidthFlushingBehaviourConfig { diff --git a/common/credential-verification/src/ecash/credential_sender.rs b/common/credential-verification/src/ecash/credential_sender.rs index 4da2095f43e..140e3e4b4d0 100644 --- a/common/credential-verification/src/ecash/credential_sender.rs +++ b/common/credential-verification/src/ecash/credential_sender.rs @@ -13,12 +13,11 @@ use nym_api_requests::constants::MIN_BATCH_REDEMPTION_DELAY; use nym_api_requests::ecash::models::{BatchRedeemTicketsBody, VerifyEcashTicketBody}; use nym_credentials_interface::Bandwidth; use nym_credentials_interface::{ClientTicket, TicketType}; -use nym_gateway_storage::Storage; use nym_validator_client::nym_api::EpochId; use nym_validator_client::nyxd::contract_traits::{ EcashSigningClient, MultisigQueryClient, MultisigSigningClient, PagedMultisigQueryClient, }; -use nym_validator_client::nyxd::cosmwasm_client::ToSingletonContractData; +use nym_validator_client::nyxd::cosmwasm_client::ContractResponseData; use nym_validator_client::nyxd::cw3::Status; use nym_validator_client::nyxd::AccountId; use nym_validator_client::EcashApiClient; @@ -126,21 +125,18 @@ pub struct CredentialHandlerConfig { pub maximum_time_between_redemption: Duration, } -pub(crate) struct CredentialHandler { +pub(crate) struct CredentialHandler { config: CredentialHandlerConfig, multisig_threshold: f32, ticket_receiver: UnboundedReceiver, - shared_state: SharedState, + shared_state: SharedState, pending_tickets: Vec, pending_redemptions: Vec, } -impl CredentialHandler -where - St: Storage + Clone + 'static, -{ +impl CredentialHandler { async fn rebuild_pending_tickets( - shared_state: &SharedState, + shared_state: &SharedState, ) -> Result, EcashTicketError> { // 1. get all tickets that were not fully verified let unverified = shared_state.storage.get_all_unverified_tickets().await?; @@ -188,7 +184,7 @@ where } async fn rebuild_pending_votes( - shared_state: &SharedState, + shared_state: &SharedState, ) -> Result, EcashTicketError> { // 1. get all tickets that were not fully verified let unverified = shared_state.storage.get_all_unresolved_proposals().await?; @@ -259,7 +255,7 @@ where pub(crate) async fn new( config: CredentialHandlerConfig, ticket_receiver: UnboundedReceiver, - shared_state: SharedState, + shared_state: SharedState, ) -> Result { let multisig_threshold = shared_state .nyxd_client diff --git a/common/credential-verification/src/ecash/error.rs b/common/credential-verification/src/ecash/error.rs index 9e025316a71..ff85a32c3e1 100644 --- a/common/credential-verification/src/ecash/error.rs +++ b/common/credential-verification/src/ecash/error.rs @@ -1,7 +1,7 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use nym_gateway_storage::error::StorageError; +use nym_gateway_storage::error::GatewayStorageError; use nym_validator_client::coconut::EcashApiError; use nym_validator_client::nym_api::EpochId; use nym_validator_client::nyxd::error::NyxdError; @@ -37,7 +37,7 @@ pub enum EcashTicketError { #[error("could not handle the ecash ticket due to internal storage failure: {source}")] InternalStorageFailure { #[from] - source: StorageError, + source: GatewayStorageError, }, #[error("failed to create ticket redemption proposal: {source}")] diff --git a/common/credential-verification/src/ecash/mod.rs b/common/credential-verification/src/ecash/mod.rs index 7a2a7b133a3..71d0d993ffe 100644 --- a/common/credential-verification/src/ecash/mod.rs +++ b/common/credential-verification/src/ecash/mod.rs @@ -8,7 +8,7 @@ use error::EcashTicketError; use futures::channel::mpsc::{self, UnboundedSender}; use nym_credentials::CredentialSpendingData; use nym_credentials_interface::{ClientTicket, CompactEcashError, NymPayInfo, VerificationKeyAuth}; -use nym_gateway_storage::Storage; +use nym_gateway_storage::GatewayStorage; use nym_validator_client::nym_api::EpochId; use nym_validator_client::DirectSigningHttpRpcNyxdClient; use state::SharedState; @@ -23,24 +23,21 @@ mod state; pub const TIME_RANGE_SEC: i64 = 30; -pub struct EcashManager { - shared_state: SharedState, +pub struct EcashManager { + shared_state: SharedState, pk_bytes: [u8; 32], // bytes representation of a pub key representing the verifier pay_infos: Mutex>, cred_sender: UnboundedSender, } -impl EcashManager -where - S: Storage + Clone + 'static, -{ +impl EcashManager { pub async fn new( credential_handler_cfg: CredentialHandlerConfig, nyxd_client: DirectSigningHttpRpcNyxdClient, pk_bytes: [u8; 32], shutdown: nym_task::TaskClient, - storage: S, + storage: GatewayStorage, ) -> Result { let shared_state = SharedState::new(nyxd_client, storage).await?; @@ -66,7 +63,7 @@ where self.shared_state.verification_key(epoch_id).await } - pub fn storage(&self) -> &S { + pub fn storage(&self) -> &GatewayStorage { &self.shared_state.storage } diff --git a/common/credential-verification/src/ecash/state.rs b/common/credential-verification/src/ecash/state.rs index 7f5a718759b..4c718c3c30d 100644 --- a/common/credential-verification/src/ecash/state.rs +++ b/common/credential-verification/src/ecash/state.rs @@ -6,7 +6,7 @@ use crate::Error; use cosmwasm_std::{from_binary, CosmosMsg, WasmMsg}; use nym_credentials_interface::VerificationKeyAuth; use nym_ecash_contract_common::msg::ExecuteMsg; -use nym_gateway_storage::Storage; +use nym_gateway_storage::GatewayStorage; use nym_validator_client::coconut::all_ecash_api_clients; use nym_validator_client::nym_api::EpochId; use nym_validator_client::nyxd::contract_traits::{ @@ -23,20 +23,17 @@ use tracing::{error, trace, warn}; // state shared by different subtasks dealing with credentials #[derive(Clone)] -pub(crate) struct SharedState { +pub(crate) struct SharedState { pub(crate) nyxd_client: Arc>, pub(crate) address: AccountId, pub(crate) epoch_data: Arc>>, - pub(crate) storage: S, + pub(crate) storage: GatewayStorage, } -impl SharedState -where - S: Storage + Clone, -{ +impl SharedState { pub(crate) async fn new( nyxd_client: DirectSigningHttpRpcNyxdClient, - storage: S, + storage: GatewayStorage, ) -> Result { let address = nyxd_client.address(); diff --git a/common/credential-verification/src/error.rs b/common/credential-verification/src/error.rs index 0f6b0e27e56..34dbdb53b6f 100644 --- a/common/credential-verification/src/error.rs +++ b/common/credential-verification/src/error.rs @@ -39,7 +39,7 @@ pub enum Error { OutOfBandwidth { required: i64, available: i64 }, #[error("Internal gateway storage error")] - StorageError(#[from] nym_gateway_storage::error::StorageError), + StorageError(#[from] nym_gateway_storage::error::GatewayStorageError), #[error("{0}")] UnknownTicketType(#[from] nym_credentials_interface::UnknownTicketType), diff --git a/common/credential-verification/src/lib.rs b/common/credential-verification/src/lib.rs index 066953fc551..50d6db1e05d 100644 --- a/common/credential-verification/src/lib.rs +++ b/common/credential-verification/src/lib.rs @@ -2,17 +2,15 @@ // SPDX-License-Identifier: Apache-2.0 use bandwidth_storage_manager::BandwidthStorageManager; -use std::sync::Arc; -use time::{Date, OffsetDateTime}; -use tracing::*; - +use ecash::EcashManager; use nym_credentials::ecash::utils::{cred_exp_date, ecash_today, EcashTime}; use nym_credentials_interface::{Bandwidth, ClientTicket, TicketType}; use nym_gateway_requests::models::CredentialSpendingRequest; -use nym_gateway_storage::Storage; +use std::sync::Arc; +use time::{Date, OffsetDateTime}; +use tracing::*; pub use client_bandwidth::*; -use ecash::EcashManager; pub use error::*; pub mod bandwidth_storage_manager; @@ -20,17 +18,17 @@ mod client_bandwidth; pub mod ecash; pub mod error; -pub struct CredentialVerifier { +pub struct CredentialVerifier { credential: CredentialSpendingRequest, - ecash_verifier: Arc>, - bandwidth_storage_manager: BandwidthStorageManager, + ecash_verifier: Arc, + bandwidth_storage_manager: BandwidthStorageManager, } -impl CredentialVerifier { +impl CredentialVerifier { pub fn new( credential: CredentialSpendingRequest, - ecash_verifier: Arc>, - bandwidth_storage_manager: BandwidthStorageManager, + ecash_verifier: Arc, + bandwidth_storage_manager: BandwidthStorageManager, ) -> Self { CredentialVerifier { credential, diff --git a/common/credentials-interface/src/lib.rs b/common/credentials-interface/src/lib.rs index 651da25ed65..cf1b138677e 100644 --- a/common/credentials-interface/src/lib.rs +++ b/common/credentials-interface/src/lib.rs @@ -225,8 +225,10 @@ impl From for NymPayInfo { Clone, Debug, PartialEq, + Eq, Serialize, Deserialize, + Hash, strum::Display, strum::EnumString, strum::EnumIter, diff --git a/common/crypto/src/asymmetric/identity/mod.rs b/common/crypto/src/asymmetric/identity/mod.rs index 4b51aa2f641..a432b0c806d 100644 --- a/common/crypto/src/asymmetric/identity/mod.rs +++ b/common/crypto/src/asymmetric/identity/mod.rs @@ -6,6 +6,7 @@ use ed25519_dalek::{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}; +use std::hash::{Hash, Hasher}; use std::str::FromStr; use thiserror::Error; use zeroize::{Zeroize, ZeroizeOnDrop}; @@ -122,6 +123,14 @@ impl PemStorableKeyPair for KeyPair { #[derive(Copy, Clone, Eq, PartialEq)] pub struct PublicKey(ed25519_dalek::VerifyingKey); +impl Hash for PublicKey { + fn hash(&self, state: &mut H) { + // each public key has unique bytes representation which can be used + // for the hash implementation + self.to_bytes().hash(state) + } +} + impl Display for PublicKey { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { Display::fmt(&self.to_base58_string(), f) diff --git a/common/dkg/src/bte/proof_chunking.rs b/common/dkg/src/bte/proof_chunking.rs index 6829a536605..1365aecbd78 100644 --- a/common/dkg/src/bte/proof_chunking.rs +++ b/common/dkg/src/bte/proof_chunking.rs @@ -26,9 +26,8 @@ const PARALLEL_RUNS: usize = 32; /// `lambda` ($\lambda$) in the DKG paper const SECURITY_PARAMETER: usize = 256; -// note: ceiling in integer division can be achieved via q = (x + y - 1) / y; /// ceil(SECURITY_PARAMETER / PARALLEL_RUNS) in the paper -const NUM_CHALLENGE_BITS: usize = (SECURITY_PARAMETER + PARALLEL_RUNS - 1) / PARALLEL_RUNS; +const NUM_CHALLENGE_BITS: usize = SECURITY_PARAMETER.div_ceil(PARALLEL_RUNS); // type alias for ease of use type FirstChallenge = Vec>>; diff --git a/common/dkg/src/interpolation/polynomial.rs b/common/dkg/src/interpolation/polynomial.rs index 33c7486a99e..671b8e68328 100644 --- a/common/dkg/src/interpolation/polynomial.rs +++ b/common/dkg/src/interpolation/polynomial.rs @@ -196,7 +196,7 @@ impl<'b> Add<&'b Polynomial> for Polynomial { } } -impl<'a> Add for &'a Polynomial { +impl Add for &Polynomial { type Output = Polynomial; fn add(self, rhs: Polynomial) -> Polynomial { @@ -212,10 +212,10 @@ impl Add for Polynomial { } } -impl<'a, 'b> Add<&'b Polynomial> for &'a Polynomial { +impl<'a> Add<&'a Polynomial> for &Polynomial { type Output = Polynomial; - fn add(self, rhs: &'b Polynomial) -> Self::Output { + fn add(self, rhs: &'a Polynomial) -> Self::Output { let len = self.coefficients.len(); let rhs_len = rhs.coefficients.len(); diff --git a/common/gateway-requests/src/registration/handshake/mod.rs b/common/gateway-requests/src/registration/handshake/mod.rs index e5dc1dc59fa..4a6f44b7d7d 100644 --- a/common/gateway-requests/src/registration/handshake/mod.rs +++ b/common/gateway-requests/src/registration/handshake/mod.rs @@ -37,7 +37,7 @@ pub struct GatewayHandshake<'a> { handshake_future: BoxFuture<'a, Result>, } -impl<'a> Future for GatewayHandshake<'a> { +impl Future for GatewayHandshake<'_> { type Output = Result; fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { diff --git a/common/gateway-stats-storage/Cargo.toml b/common/gateway-stats-storage/Cargo.toml index d439b34a162..0a36798e529 100644 --- a/common/gateway-stats-storage/Cargo.toml +++ b/common/gateway-stats-storage/Cargo.toml @@ -16,12 +16,15 @@ sqlx = { workspace = true, features = [ "migrate", "time", ] } +strum = { workspace = true } time = { workspace = true } thiserror = { workspace = true } tracing = { workspace = true } nym-sphinx = { path = "../nymsphinx" } nym-credentials-interface = { path = "../credentials-interface" } +nym-node-metrics = { path = "../../nym-node/nym-node-metrics" } +nym-statistics-common = { path = "../statistics" } [build-dependencies] diff --git a/common/gateway-stats-storage/src/lib.rs b/common/gateway-stats-storage/src/lib.rs index 453258b606b..74b45e9e7a3 100644 --- a/common/gateway-stats-storage/src/lib.rs +++ b/common/gateway-stats-storage/src/lib.rs @@ -2,7 +2,8 @@ // SPDX-License-Identifier: GPL-3.0-only use error::StatsStorageError; -use models::{ActiveSession, FinishedSession, SessionType, StoredFinishedSession}; +use models::StoredFinishedSession; +use nym_node_metrics::entry::{ActiveSession, FinishedSession, SessionType}; use nym_sphinx::DestinationAddressBytes; use sessions::SessionManager; use sqlx::ConnectOptions; @@ -70,8 +71,8 @@ impl PersistentStatsStorage { .session_manager .insert_finished_session( date, - session.duration.whole_milliseconds() as i64, - session.typ.to_string().into(), + session.duration.as_millis() as i64, + session.typ.to_string(), ) .await?) } @@ -125,7 +126,7 @@ impl PersistentStatsStorage { .insert_active_session( client_address.as_base58_string(), session.start, - session.typ.to_string().into(), + session.typ.to_string(), ) .await?) } @@ -137,10 +138,7 @@ impl PersistentStatsStorage { ) -> Result<(), StatsStorageError> { Ok(self .session_manager - .update_active_session_type( - client_address.as_base58_string(), - session_type.to_string().into(), - ) + .update_active_session_type(client_address.as_base58_string(), session_type.to_string()) .await?) } diff --git a/common/gateway-stats-storage/src/models.rs b/common/gateway-stats-storage/src/models.rs index 6f53cf429ab..5553875fd45 100644 --- a/common/gateway-stats-storage/src/models.rs +++ b/common/gateway-stats-storage/src/models.rs @@ -1,9 +1,11 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use nym_credentials_interface::TicketType; +use nym_node_metrics::entry::{ActiveSession, FinishedSession, SessionType}; use sqlx::prelude::FromRow; -use time::{Duration, OffsetDateTime}; +use time::OffsetDateTime; + +pub use nym_credentials_interface::TicketType; #[derive(FromRow)] pub struct StoredFinishedSession { @@ -11,52 +13,26 @@ pub struct StoredFinishedSession { typ: String, } -impl StoredFinishedSession { - pub fn serialize(&self) -> (u64, String) { - ( - self.duration_ms as u64, //we are sure that it fits in a u64, see `fn end_at` - self.typ.clone(), - ) +impl From for FinishedSession { + fn from(value: StoredFinishedSession) -> Self { + FinishedSession { + duration: std::time::Duration::from_millis(value.duration_ms as u64), + typ: SessionType::from_string(value.typ), + } } } -pub struct FinishedSession { - pub duration: Duration, - pub typ: SessionType, -} - -#[derive(PartialEq)] -pub enum SessionType { - Vpn, - Mixnet, - Unknown, +pub trait ToSessionType { + fn to_session_type(&self) -> SessionType; } -impl SessionType { - pub fn to_string(&self) -> &str { +impl ToSessionType for TicketType { + fn to_session_type(&self) -> SessionType { match self { - Self::Vpn => "vpn", - Self::Mixnet => "mixnet", - Self::Unknown => "unknown", - } - } - - pub fn from_string(s: &str) -> Self { - match s { - "vpn" => Self::Vpn, - "mixnet" => Self::Mixnet, - _ => Self::Unknown, - } - } -} - -impl From for SessionType { - fn from(value: TicketType) -> Self { - match value { - TicketType::V1MixnetEntry => Self::Mixnet, - TicketType::V1MixnetExit => Self::Mixnet, - TicketType::V1WireguardEntry => Self::Vpn, - TicketType::V1WireguardExit => Self::Vpn, + TicketType::V1MixnetEntry => SessionType::Mixnet, + TicketType::V1MixnetExit => SessionType::Mixnet, + TicketType::V1WireguardEntry => SessionType::Vpn, + TicketType::V1WireguardExit => SessionType::Vpn, } } } @@ -67,38 +43,6 @@ pub(crate) struct StoredActiveSession { typ: String, } -pub struct ActiveSession { - pub start: OffsetDateTime, - pub typ: SessionType, -} - -impl ActiveSession { - pub fn new(start_time: OffsetDateTime) -> Self { - ActiveSession { - start: start_time, - typ: SessionType::Unknown, - } - } - - pub fn set_type(&mut self, ticket_type: TicketType) { - self.typ = ticket_type.into(); - } - - pub fn end_at(self, stop_time: OffsetDateTime) -> Option { - let session_duration = stop_time - self.start; - //ensure duration is positive to fit in a u64 - //u64::max milliseconds is 500k millenia so no overflow issue - if session_duration > Duration::ZERO { - Some(FinishedSession { - duration: session_duration, - typ: self.typ, - }) - } else { - None - } - } -} - impl From for ActiveSession { fn from(value: StoredActiveSession) -> Self { ActiveSession { diff --git a/common/gateway-storage/Cargo.toml b/common/gateway-storage/Cargo.toml index d89bac76397..bd4d8b7256b 100644 --- a/common/gateway-storage/Cargo.toml +++ b/common/gateway-storage/Cargo.toml @@ -9,7 +9,6 @@ edition.workspace = true license.workspace = true [dependencies] -async-trait = { workspace = true } bincode = { workspace = true } defguard_wireguard_rs = { workspace = true } log = { workspace = true } diff --git a/common/gateway-storage/src/error.rs b/common/gateway-storage/src/error.rs index 408ec245d66..272d86b5571 100644 --- a/common/gateway-storage/src/error.rs +++ b/common/gateway-storage/src/error.rs @@ -4,7 +4,7 @@ use thiserror::Error; #[derive(Error, Debug)] -pub enum StorageError { +pub enum GatewayStorageError { #[error("Database experienced an internal error: {0}")] InternalDatabaseError(#[from] sqlx::Error), diff --git a/common/gateway-storage/src/lib.rs b/common/gateway-storage/src/lib.rs index 8d5fda0912e..7b0f70a5b35 100644 --- a/common/gateway-storage/src/lib.rs +++ b/common/gateway-storage/src/lib.rs @@ -1,10 +1,8 @@ // Copyright 2020 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use async_trait::async_trait; use bandwidth::BandwidthManager; use clients::{ClientManager, ClientType}; -use error::StorageError; use inboxes::InboxManager; use models::{ Client, PersistedBandwidth, PersistedSharedKeys, RedemptionProposal, StoredMessage, @@ -29,237 +27,11 @@ mod shared_keys; mod tickets; mod wireguard_peers; -#[async_trait] -pub trait Storage: Send + Sync { - async fn get_mixnet_client_id( - &self, - client_address: DestinationAddressBytes, - ) -> Result; - - /// Inserts provided derived shared keys into the database. - /// If keys previously existed for the provided client, they are overwritten with the new data. - /// - /// # Arguments - /// - /// * `client_address`: base58-encoded address of the client - /// * `shared_keys`: - /// - legacy: shared encryption (AES128CTR) and mac (hmac-blake3) derived shared keys to store. - /// - current: shared AES256-GCM-SIV keys - async fn insert_shared_keys( - &self, - client_address: DestinationAddressBytes, - shared_keys: &SharedGatewayKey, - ) -> Result; - - /// Tries to retrieve shared keys stored for the particular client. - /// - /// # Arguments - /// - /// * `client_address`: address of the client - async fn get_shared_keys( - &self, - client_address: DestinationAddressBytes, - ) -> Result, StorageError>; - - /// Removes from the database shared keys derived with the particular client. - /// - /// # Arguments - /// - /// * `client_address`: address of the client - // currently there is no code flow that causes removal (not overwriting) - // of the stored keys. However, retain the function for consistency and completion sake - #[allow(dead_code)] - async fn remove_shared_keys( - &self, - client_address: DestinationAddressBytes, - ) -> Result<(), StorageError>; - - /// Tries to retrieve a particular client. - /// - /// # Arguments - /// - /// * `client_id`: id of the client - #[allow(dead_code)] - async fn get_client(&self, client_id: i64) -> Result, StorageError>; - - /// Inserts new message to the storage for an offline client for future retrieval. - /// - /// # Arguments - /// - /// * `client_address`: address of the client - /// * `message`: raw message to store. - async fn store_message( - &self, - client_address: DestinationAddressBytes, - message: Vec, - ) -> Result<(), StorageError>; - - /// Retrieves messages stored for the particular client specified by the provided address. - /// - /// # Arguments - /// - /// * `client_address`: address of the client - /// * `start_after`: optional starting id of the messages to grab - /// - /// returns the retrieved messages alongside optional id of the last message retrieved if - /// there are more messages to retrieve. - async fn retrieve_messages( - &self, - client_address: DestinationAddressBytes, - start_after: Option, - ) -> Result<(Vec, Option), StorageError>; - - /// Removes messages with the specified ids - /// - /// # Arguments - /// - /// * `ids`: ids of the messages to remove - async fn remove_messages(&self, ids: Vec) -> Result<(), StorageError>; - - /// Creates a new bandwidth entry for the particular client. - async fn create_bandwidth_entry(&self, client_id: i64) -> Result<(), StorageError>; - - /// Set the freepass expiration date of the particular client to the provided date. - /// - /// # Arguments - /// - /// * `client_address`: address of the client - /// * `expiration`: the expiration date of the associated free pass. - async fn set_expiration( - &self, - client_id: i64, - expiration: OffsetDateTime, - ) -> Result<(), StorageError>; - - /// Reset all the bandwidth - /// - /// # Arguments - /// - /// * `client_address`: address of the client - async fn reset_bandwidth(&self, client_id: i64) -> Result<(), StorageError>; - - /// Tries to retrieve available bandwidth for the particular client. - async fn get_available_bandwidth( - &self, - client_id: i64, - ) -> Result, StorageError>; - - /// Increases specified client's bandwidth by the provided amount and returns the current value. - async fn increase_bandwidth(&self, client_id: i64, amount: i64) -> Result; - - async fn revoke_ticket_bandwidth( - &self, - ticket_id: i64, - amount: i64, - ) -> Result<(), StorageError>; - - #[allow(dead_code)] - /// Decreases specified client's bandwidth by the provided amount and returns the current value. - async fn decrease_bandwidth(&self, client_id: i64, amount: i64) -> Result; - - async fn insert_epoch_signers( - &self, - epoch_id: i64, - signer_ids: Vec, - ) -> Result<(), StorageError>; - - async fn insert_received_ticket( - &self, - client_id: i64, - received_at: OffsetDateTime, - serial_number: Vec, - data: Vec, - ) -> Result; - - // note: this only checks very recent tickets that haven't yet been redeemed - // (but it's better than nothing) - /// Check if the ticket with the provided serial number if already present in the storage. - /// - /// # Arguments - /// - /// * `serial_number`: the unique serial number embedded in the ticket - async fn contains_ticket(&self, serial_number: &[u8]) -> Result; - - async fn insert_ticket_verification( - &self, - ticket_id: i64, - signer_id: i64, - verified_at: OffsetDateTime, - accepted: bool, - ) -> Result<(), StorageError>; - - async fn update_rejected_ticket(&self, ticket_id: i64) -> Result<(), StorageError>; - - async fn update_verified_ticket(&self, ticket_id: i64) -> Result<(), StorageError>; - - async fn remove_verified_ticket_binary_data(&self, ticket_id: i64) -> Result<(), StorageError>; - - async fn get_all_verified_tickets_with_sn(&self) -> Result, StorageError>; - async fn get_all_proposed_tickets_with_sn( - &self, - proposal_id: u32, - ) -> Result, StorageError>; - - async fn insert_redemption_proposal( - &self, - tickets: &[VerifiedTicket], - proposal_id: u32, - created_at: OffsetDateTime, - ) -> Result<(), StorageError>; - - async fn clear_post_proposal_data( - &self, - proposal_id: u32, - resolved_at: OffsetDateTime, - rejected: bool, - ) -> Result<(), StorageError>; - - async fn latest_proposal(&self) -> Result, StorageError>; - - async fn get_all_unverified_tickets(&self) -> Result, StorageError>; - async fn get_all_unresolved_proposals(&self) -> Result, StorageError>; - async fn get_votes(&self, ticket_id: i64) -> Result, StorageError>; - - async fn get_signers(&self, epoch_id: i64) -> Result, StorageError>; - - /// Insert a wireguard peer in the storage. - /// - /// # Arguments - /// - /// * `peer`: wireguard peer data to be stored - /// * `with_client_id`: if the peer should have a corresponding client_id - /// (created with entry wireguard ticket) or live without one (or with an - /// exiting one), for temporary backwards compatibility. - async fn insert_wireguard_peer( - &self, - peer: &defguard_wireguard_rs::host::Peer, - with_client_id: bool, - ) -> Result, StorageError>; - - /// Tries to retrieve available bandwidth for the particular peer. - /// - /// # Arguments - /// - /// * `peer_public_key`: wireguard public key of the peer to be retrieved. - async fn get_wireguard_peer( - &self, - peer_public_key: &str, - ) -> Result, StorageError>; - - /// Retrieves all wireguard peers. - async fn get_all_wireguard_peers(&self) -> Result, StorageError>; - - /// Remove a wireguard peer from the storage. - /// - /// # Arguments - /// - /// * `peer_public_key`: wireguard public key of the peer to be removed. - async fn remove_wireguard_peer(&self, peer_public_key: &str) -> Result<(), StorageError>; -} +pub use error::GatewayStorageError; // note that clone here is fine as upon cloning the same underlying pool will be used #[derive(Clone)] -pub struct PersistentStorage { +pub struct GatewayStorage { client_manager: ClientManager, shared_key_manager: SharedKeysManager, inbox_manager: InboxManager, @@ -268,7 +40,7 @@ pub struct PersistentStorage { wireguard_peer_manager: wireguard_peers::WgPeerManager, } -impl PersistentStorage { +impl GatewayStorage { /// Initialises `PersistentStorage` using the provided path. /// /// # Arguments @@ -278,7 +50,7 @@ impl PersistentStorage { pub async fn init + Send>( database_path: P, message_retrieval_limit: i64, - ) -> Result { + ) -> Result { debug!( "Attempting to connect to database {:?}", database_path.as_ref().as_os_str() @@ -307,7 +79,7 @@ impl PersistentStorage { } // the cloning here are cheap as connection pool is stored behind an Arc - Ok(PersistentStorage { + Ok(GatewayStorage { client_manager: clients::ClientManager::new(connection_pool.clone()), wireguard_peer_manager: wireguard_peers::WgPeerManager::new(connection_pool.clone()), shared_key_manager: SharedKeysManager::new(connection_pool.clone()), @@ -318,23 +90,22 @@ impl PersistentStorage { } } -#[async_trait] -impl Storage for PersistentStorage { - async fn get_mixnet_client_id( +impl GatewayStorage { + pub async fn get_mixnet_client_id( &self, client_address: DestinationAddressBytes, - ) -> Result { + ) -> Result { Ok(self .shared_key_manager .client_id(&client_address.as_base58_string()) .await?) } - async fn insert_shared_keys( + pub async fn insert_shared_keys( &self, client_address: DestinationAddressBytes, shared_keys: &SharedGatewayKey, - ) -> Result { + ) -> Result { let client_address_bs58 = client_address.as_base58_string(); let client_id = match self .shared_key_manager @@ -359,10 +130,10 @@ impl Storage for PersistentStorage { Ok(client_id) } - async fn get_shared_keys( + pub async fn get_shared_keys( &self, client_address: DestinationAddressBytes, - ) -> Result, StorageError> { + ) -> Result, GatewayStorageError> { let keys = self .shared_key_manager .get_shared_keys(&client_address.as_base58_string()) @@ -371,37 +142,37 @@ impl Storage for PersistentStorage { } #[allow(dead_code)] - async fn remove_shared_keys( + pub async fn remove_shared_keys( &self, client_address: DestinationAddressBytes, - ) -> Result<(), StorageError> { + ) -> Result<(), GatewayStorageError> { self.shared_key_manager .remove_shared_keys(&client_address.as_base58_string()) .await?; Ok(()) } - async fn get_client(&self, client_id: i64) -> Result, StorageError> { + pub async fn get_client(&self, client_id: i64) -> Result, GatewayStorageError> { let client = self.client_manager.get_client(client_id).await?; Ok(client) } - async fn store_message( + pub async fn store_message( &self, client_address: DestinationAddressBytes, message: Vec, - ) -> Result<(), StorageError> { + ) -> Result<(), GatewayStorageError> { self.inbox_manager .insert_message(&client_address.as_base58_string(), message) .await?; Ok(()) } - async fn retrieve_messages( + pub async fn retrieve_messages( &self, client_address: DestinationAddressBytes, start_after: Option, - ) -> Result<(Vec, Option), StorageError> { + ) -> Result<(Vec, Option), GatewayStorageError> { let messages = self .inbox_manager .get_messages(&client_address.as_base58_string(), start_after) @@ -409,87 +180,95 @@ impl Storage for PersistentStorage { Ok(messages) } - async fn remove_messages(&self, ids: Vec) -> Result<(), StorageError> { + pub async fn remove_messages(&self, ids: Vec) -> Result<(), GatewayStorageError> { for id in ids { self.inbox_manager.remove_message(id).await?; } Ok(()) } - async fn create_bandwidth_entry(&self, client_id: i64) -> Result<(), StorageError> { + pub async fn create_bandwidth_entry(&self, client_id: i64) -> Result<(), GatewayStorageError> { self.bandwidth_manager.insert_new_client(client_id).await?; Ok(()) } - async fn set_expiration( + pub async fn set_expiration( &self, client_id: i64, expiration: OffsetDateTime, - ) -> Result<(), StorageError> { + ) -> Result<(), GatewayStorageError> { self.bandwidth_manager .set_expiration(client_id, expiration) .await?; Ok(()) } - async fn reset_bandwidth(&self, client_id: i64) -> Result<(), StorageError> { + pub async fn reset_bandwidth(&self, client_id: i64) -> Result<(), GatewayStorageError> { self.bandwidth_manager.reset_bandwidth(client_id).await?; Ok(()) } - async fn get_available_bandwidth( + pub async fn get_available_bandwidth( &self, client_id: i64, - ) -> Result, StorageError> { + ) -> Result, GatewayStorageError> { Ok(self .bandwidth_manager .get_available_bandwidth(client_id) .await?) } - async fn increase_bandwidth(&self, client_id: i64, amount: i64) -> Result { + pub async fn increase_bandwidth( + &self, + client_id: i64, + amount: i64, + ) -> Result { Ok(self .bandwidth_manager .increase_bandwidth(client_id, amount) .await?) } - async fn revoke_ticket_bandwidth( + pub async fn revoke_ticket_bandwidth( &self, ticket_id: i64, amount: i64, - ) -> Result<(), StorageError> { + ) -> Result<(), GatewayStorageError> { Ok(self .bandwidth_manager .revoke_ticket_bandwidth(ticket_id, amount) .await?) } - async fn decrease_bandwidth(&self, client_id: i64, amount: i64) -> Result { + pub async fn decrease_bandwidth( + &self, + client_id: i64, + amount: i64, + ) -> Result { Ok(self .bandwidth_manager .decrease_bandwidth(client_id, amount) .await?) } - async fn insert_epoch_signers( + pub async fn insert_epoch_signers( &self, epoch_id: i64, signer_ids: Vec, - ) -> Result<(), StorageError> { + ) -> Result<(), GatewayStorageError> { self.ticket_manager .insert_ecash_signers(epoch_id, signer_ids) .await?; Ok(()) } - async fn insert_received_ticket( + pub async fn insert_received_ticket( &self, client_id: i64, received_at: OffsetDateTime, serial_number: Vec, data: Vec, - ) -> Result { + ) -> Result { // technically if we crash between those 2 calls we'll have a bit of data inconsistency, // but nothing too tragic. we just won't get paid for a single ticket let ticket_id = self @@ -503,24 +282,24 @@ impl Storage for PersistentStorage { Ok(ticket_id) } - async fn contains_ticket(&self, serial_number: &[u8]) -> Result { + pub async fn contains_ticket(&self, serial_number: &[u8]) -> Result { Ok(self.ticket_manager.has_ticket_data(serial_number).await?) } - async fn insert_ticket_verification( + pub async fn insert_ticket_verification( &self, ticket_id: i64, signer_id: i64, verified_at: OffsetDateTime, accepted: bool, - ) -> Result<(), StorageError> { + ) -> Result<(), GatewayStorageError> { self.ticket_manager .insert_ticket_verification(ticket_id, signer_id, verified_at, accepted) .await?; Ok(()) } - async fn update_rejected_ticket(&self, ticket_id: i64) -> Result<(), StorageError> { + pub async fn update_rejected_ticket(&self, ticket_id: i64) -> Result<(), GatewayStorageError> { // set the ticket as rejected self.ticket_manager.set_rejected_ticket(ticket_id).await?; @@ -531,7 +310,7 @@ impl Storage for PersistentStorage { Ok(()) } - async fn update_verified_ticket(&self, ticket_id: i64) -> Result<(), StorageError> { + pub async fn update_verified_ticket(&self, ticket_id: i64) -> Result<(), GatewayStorageError> { // 1. insert into verified table self.ticket_manager .insert_verified_ticket(ticket_id) @@ -545,36 +324,41 @@ impl Storage for PersistentStorage { Ok(()) } - async fn remove_verified_ticket_binary_data(&self, ticket_id: i64) -> Result<(), StorageError> { + pub async fn remove_verified_ticket_binary_data( + &self, + ticket_id: i64, + ) -> Result<(), GatewayStorageError> { self.ticket_manager .remove_binary_ticket_data(ticket_id) .await?; Ok(()) } - async fn get_all_verified_tickets_with_sn(&self) -> Result, StorageError> { + pub async fn get_all_verified_tickets_with_sn( + &self, + ) -> Result, GatewayStorageError> { Ok(self .ticket_manager .get_all_verified_tickets_with_sn() .await?) } - async fn get_all_proposed_tickets_with_sn( + pub async fn get_all_proposed_tickets_with_sn( &self, proposal_id: u32, - ) -> Result, StorageError> { + ) -> Result, GatewayStorageError> { Ok(self .ticket_manager .get_all_proposed_tickets_with_sn(proposal_id as i64) .await?) } - async fn insert_redemption_proposal( + pub async fn insert_redemption_proposal( &self, tickets: &[VerifiedTicket], proposal_id: u32, created_at: OffsetDateTime, - ) -> Result<(), StorageError> { + ) -> Result<(), GatewayStorageError> { // if we crash between those, there might a bit of an issue. we should revisit it later // 1. insert the actual proposal @@ -592,12 +376,12 @@ impl Storage for PersistentStorage { Ok(()) } - async fn clear_post_proposal_data( + pub async fn clear_post_proposal_data( &self, proposal_id: u32, resolved_at: OffsetDateTime, rejected: bool, - ) -> Result<(), StorageError> { + ) -> Result<(), GatewayStorageError> { // 1. update proposal metadata self.ticket_manager .update_redemption_proposal(proposal_id as i64, resolved_at, rejected) @@ -616,11 +400,13 @@ impl Storage for PersistentStorage { Ok(()) } - async fn latest_proposal(&self) -> Result, StorageError> { + pub async fn latest_proposal(&self) -> Result, GatewayStorageError> { Ok(self.ticket_manager.get_latest_redemption_proposal().await?) } - async fn get_all_unverified_tickets(&self) -> Result, StorageError> { + pub async fn get_all_unverified_tickets( + &self, + ) -> Result, GatewayStorageError> { self.ticket_manager .get_unverified_tickets() .await? @@ -629,29 +415,37 @@ impl Storage for PersistentStorage { .collect() } - async fn get_all_unresolved_proposals(&self) -> Result, StorageError> { + pub async fn get_all_unresolved_proposals(&self) -> Result, GatewayStorageError> { Ok(self .ticket_manager .get_all_unresolved_redemption_proposal_ids() .await?) } - async fn get_votes(&self, ticket_id: i64) -> Result, StorageError> { + pub async fn get_votes(&self, ticket_id: i64) -> Result, GatewayStorageError> { Ok(self .ticket_manager .get_verification_votes(ticket_id) .await?) } - async fn get_signers(&self, epoch_id: i64) -> Result, StorageError> { + pub async fn get_signers(&self, epoch_id: i64) -> Result, GatewayStorageError> { Ok(self.ticket_manager.get_epoch_signers(epoch_id).await?) } - async fn insert_wireguard_peer( + /// Insert a wireguard peer in the storage. + /// + /// # Arguments + /// + /// * `peer`: wireguard peer data to be stored + /// * `with_client_id`: if the peer should have a corresponding client_id + /// (created with entry wireguard ticket) or live without one (or with an + /// exiting one), for temporary backwards compatibility. + pub async fn insert_wireguard_peer( &self, peer: &defguard_wireguard_rs::host::Peer, with_client_id: bool, - ) -> Result, StorageError> { + ) -> Result, GatewayStorageError> { let client_id = match self .wireguard_peer_manager .retrieve_peer(&peer.public_key.to_string()) @@ -676,10 +470,15 @@ impl Storage for PersistentStorage { Ok(client_id) } - async fn get_wireguard_peer( + /// Tries to retrieve available bandwidth for the particular peer. + /// + /// # Arguments + /// + /// * `peer_public_key`: wireguard public key of the peer to be retrieved. + pub async fn get_wireguard_peer( &self, peer_public_key: &str, - ) -> Result, StorageError> { + ) -> Result, GatewayStorageError> { let peer = self .wireguard_peer_manager .retrieve_peer(peer_public_key) @@ -687,12 +486,21 @@ impl Storage for PersistentStorage { Ok(peer) } - async fn get_all_wireguard_peers(&self) -> Result, StorageError> { + /// Retrieves all wireguard peers. + pub async fn get_all_wireguard_peers(&self) -> Result, GatewayStorageError> { let ret = self.wireguard_peer_manager.retrieve_all_peers().await?; Ok(ret) } - async fn remove_wireguard_peer(&self, peer_public_key: &str) -> Result<(), StorageError> { + /// Remove a wireguard peer from the storage. + /// + /// # Arguments + /// + /// * `peer_public_key`: wireguard public key of the peer to be removed. + pub async fn remove_wireguard_peer( + &self, + peer_public_key: &str, + ) -> Result<(), GatewayStorageError> { self.wireguard_peer_manager .remove_peer(peer_public_key) .await?; diff --git a/common/gateway-storage/src/models.rs b/common/gateway-storage/src/models.rs index 74a2e5162d6..f1bacdcaea3 100644 --- a/common/gateway-storage/src/models.rs +++ b/common/gateway-storage/src/models.rs @@ -1,7 +1,7 @@ // Copyright 2021-2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::error::StorageError; +use crate::error::GatewayStorageError; use nym_credentials_interface::{AvailableBandwidth, ClientTicket, CredentialSpendingData}; use nym_gateway_requests::shared_key::{LegacySharedKeys, SharedGatewayKey, SharedSymmetricKey}; use sqlx::FromRow; @@ -24,24 +24,24 @@ pub struct PersistedSharedKeys { } impl TryFrom for SharedGatewayKey { - type Error = StorageError; + type Error = GatewayStorageError; fn try_from(value: PersistedSharedKeys) -> Result { match ( &value.derived_aes256_gcm_siv_key, &value.derived_aes128_ctr_blake3_hmac_keys_bs58, ) { - (None, None) => Err(StorageError::MissingSharedKey { + (None, None) => Err(GatewayStorageError::MissingSharedKey { id: value.client_id, }), (Some(aes256gcm_siv), _) => { let current_key = SharedSymmetricKey::try_from_bytes(aes256gcm_siv) - .map_err(|source| StorageError::DataCorruption(source.to_string()))?; + .map_err(|source| GatewayStorageError::DataCorruption(source.to_string()))?; Ok(SharedGatewayKey::Current(current_key)) } (None, Some(aes128ctr_hmac)) => { let legacy_key = LegacySharedKeys::try_from_base58_string(aes128ctr_hmac) - .map_err(|source| StorageError::DataCorruption(source.to_string()))?; + .map_err(|source| GatewayStorageError::DataCorruption(source.to_string()))?; Ok(SharedGatewayKey::Legacy(legacy_key)) } } @@ -91,12 +91,12 @@ pub struct UnverifiedTicketData { } impl TryFrom for ClientTicket { - type Error = StorageError; + type Error = GatewayStorageError; fn try_from(value: UnverifiedTicketData) -> Result { Ok(ClientTicket { spending_data: CredentialSpendingData::try_from_bytes(&value.data).map_err(|_| { - StorageError::MalformedStoredTicketData { + GatewayStorageError::MalformedStoredTicketData { ticket_id: value.ticket_id, } })?, @@ -152,7 +152,7 @@ impl From for WireguardPeer { } impl TryFrom for defguard_wireguard_rs::host::Peer { - type Error = crate::error::StorageError; + type Error = crate::error::GatewayStorageError; fn try_from(value: WireguardPeer) -> Result { Ok(Self { diff --git a/common/http-api-client/src/user_agent.rs b/common/http-api-client/src/user_agent.rs index 1cdbf458257..eeec96e4248 100644 --- a/common/http-api-client/src/user_agent.rs +++ b/common/http-api-client/src/user_agent.rs @@ -1,12 +1,13 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use std::fmt; +use std::{fmt, str::FromStr}; use http::HeaderValue; use nym_bin_common::build_information::{BinaryBuildInformation, BinaryBuildInformationOwned}; +use serde::{Deserialize, Serialize}; -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] pub struct UserAgent { pub application: String, pub version: String, @@ -14,6 +15,36 @@ pub struct UserAgent { pub git_commit: String, } +#[derive(Clone, Debug, thiserror::Error)] +#[error("invalid user agent string: {0}")] +pub struct UserAgentError(String); + +impl FromStr for UserAgent { + type Err = UserAgentError; + + fn from_str(s: &str) -> Result { + let parts: Vec<&str> = s.split('/').collect(); + if parts.len() != 4 { + return Err(UserAgentError(s.to_string())); + } + + Ok(UserAgent { + application: parts[0].to_string(), + version: parts[1].to_string(), + platform: parts[2].to_string(), + git_commit: parts[3].to_string(), + }) + } +} + +impl TryFrom<&str> for UserAgent { + type Error = UserAgentError; + + fn try_from(s: &str) -> Result { + UserAgent::from_str(s) + } +} + impl fmt::Display for UserAgent { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let abbreviated_commit = self.git_commit.chars().take(7).collect::(); @@ -54,3 +85,85 @@ impl From for UserAgent { } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parsing_valid_user_agent() { + let user_agent = "nym-mixnode/0.11.0/x86_64-unknown-linux-gnu/abcdefg"; + let parsed = UserAgent::from_str(user_agent).unwrap(); + assert_eq!( + parsed, + UserAgent { + application: "nym-mixnode".to_string(), + version: "0.11.0".to_string(), + platform: "x86_64-unknown-linux-gnu".to_string(), + git_commit: "abcdefg".to_string() + } + ); + } + + #[test] + fn parsing_invalid_user_agent() { + let user_agent = "nym-mixnode/0.11.0/x86_64-unknown-linux-gnu"; + assert!(UserAgent::from_str(user_agent).is_err()); + } + + #[test] + fn converting_user_agent_to_string() { + let user_agent = UserAgent { + application: "nym-mixnode".to_string(), + version: "0.11.0".to_string(), + platform: "x86_64-unknown-linux-gnu".to_string(), + git_commit: "abcdefg".to_string(), + }; + + assert_eq!( + user_agent.to_string(), + "nym-mixnode/0.11.0/x86_64-unknown-linux-gnu/abcdefg" + ); + } + + #[test] + fn converting_user_agent_to_display() { + let user_agent = UserAgent { + application: "nym-mixnode".to_string(), + version: "0.11.0".to_string(), + platform: "x86_64-unknown-linux-gnu".to_string(), + git_commit: "abcdefg".to_string(), + }; + + assert_eq!( + format!("{}", user_agent), + "nym-mixnode/0.11.0/x86_64-unknown-linux-gnu/abcdefg" + ); + } + + #[test] + fn converting_user_agent_to_header_value_fails() { + let user_agent = UserAgent { + application: "nym-mixnode".to_string(), + version: "0.11.0".to_string(), + platform: "x86_64-unknown-linux-gnu".to_string(), + git_commit: "abcdefg".to_string(), + }; + + let header_value: Result = user_agent.clone().try_into(); + assert!(header_value.is_ok()); + } + + #[test] + fn converting_user_agent_to_header_value_has_same_string_representation() { + let user_agent = UserAgent { + application: "nym-mixnode".to_string(), + version: "0.11.0".to_string(), + platform: "x86_64-unknown-linux-gnu".to_string(), + git_commit: "abcdefg".to_string(), + }; + + let header_value: HeaderValue = user_agent.clone().try_into().unwrap(); + assert_eq!(header_value.to_str().unwrap(), user_agent.to_string()); + } +} diff --git a/common/http-api-common/Cargo.toml b/common/http-api-common/Cargo.toml index c7b6db2ff6a..e00af2e4efe 100644 --- a/common/http-api-common/Cargo.toml +++ b/common/http-api-common/Cargo.toml @@ -15,12 +15,15 @@ axum-client-ip.workspace = true axum.workspace = true bytes = { workspace = true } colored.workspace = true +futures = { workspace = true } mime = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json.workspace = true serde_yaml = { workspace = true } +tower = { workspace = true } tracing.workspace = true utoipa = { workspace = true, optional = true } +zeroize = { workspace = true } [features] utoipa = ["dep:utoipa"] diff --git a/common/http-api-common/src/lib.rs b/common/http-api-common/src/lib.rs index f84e6e2ecc2..dbc3bc0125e 100644 --- a/common/http-api-common/src/lib.rs +++ b/common/http-api-common/src/lib.rs @@ -7,7 +7,7 @@ use axum::Json; use bytes::{BufMut, BytesMut}; use serde::{Deserialize, Serialize}; -pub mod logging; +pub mod middleware; #[derive(Debug, Clone)] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] diff --git a/nym-credential-proxy/nym-credential-proxy/src/http/middleware/auth.rs b/common/http-api-common/src/middleware/bearer_auth.rs similarity index 98% rename from nym-credential-proxy/nym-credential-proxy/src/http/middleware/auth.rs rename to common/http-api-common/src/middleware/bearer_auth.rs index ce9c4e09120..68867f32a34 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/http/middleware/auth.rs +++ b/common/http-api-common/src/middleware/bearer_auth.rs @@ -1,5 +1,5 @@ // Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only +// SPDX-License-Identifier: Apache-2.0 use axum::http::{header, HeaderValue, StatusCode}; use axum::response::IntoResponse; diff --git a/common/http-api-common/src/logging.rs b/common/http-api-common/src/middleware/logging.rs similarity index 96% rename from common/http-api-common/src/logging.rs rename to common/http-api-common/src/middleware/logging.rs index 8de60b338f0..fd60ca30b18 100644 --- a/common/http-api-common/src/logging.rs +++ b/common/http-api-common/src/middleware/logging.rs @@ -1,5 +1,5 @@ // Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only +// SPDX-License-Identifier: Apache-2.0 use axum::extract::Request; use axum::http::header::{HOST, USER_AGENT}; @@ -11,6 +11,7 @@ use colored::Colorize; use std::time::Instant; use tracing::info; +/// Simple logger for requests pub async fn logger( InsecureClientIp(addr): InsecureClientIp, request: Request, diff --git a/common/http-api-common/src/middleware/mod.rs b/common/http-api-common/src/middleware/mod.rs new file mode 100644 index 00000000000..d81923bce55 --- /dev/null +++ b/common/http-api-common/src/middleware/mod.rs @@ -0,0 +1,5 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod bearer_auth; +pub mod logging; diff --git a/common/mixnode-common/Cargo.toml b/common/mixnode-common/Cargo.toml index a955aba65d7..8fbab39525c 100644 --- a/common/mixnode-common/Cargo.toml +++ b/common/mixnode-common/Cargo.toml @@ -26,7 +26,7 @@ url = { workspace = true } time.workspace = true thiserror = { workspace = true } -nym-crypto = { path = "../crypto" } +nym-crypto = { path = "../crypto", features = ["asymmetric"] } nym-network-defaults = { path = "../network-defaults" } nym-sphinx-acknowledgements = { path = "../nymsphinx/acknowledgements" } nym-sphinx-addressing = { path = "../nymsphinx/addressing" } @@ -35,7 +35,5 @@ nym-sphinx-framing = { path = "../nymsphinx/framing" } nym-sphinx-params = { path = "../nymsphinx/params" } nym-sphinx-types = { path = "../nymsphinx/types" } nym-task = { path = "../task" } -nym-validator-client = { path = "../client-libs/validator-client" } -nym-bin-common = { path = "../bin-common" } nym-metrics = { path = "../nym-metrics" } -nym-node-http-api = { path = "../../nym-node/nym-node-http-api" } + diff --git a/common/mixnode-common/src/lib.rs b/common/mixnode-common/src/lib.rs index 4ab41c6040b..ffd6ff8c802 100644 --- a/common/mixnode-common/src/lib.rs +++ b/common/mixnode-common/src/lib.rs @@ -1,4 +1,4 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 + pub mod packet_processor; -pub mod verloc; diff --git a/common/mixnode-common/src/verloc/error.rs b/common/mixnode-common/src/verloc/error.rs deleted file mode 100644 index 4bfe0da48e0..00000000000 --- a/common/mixnode-common/src/verloc/error.rs +++ /dev/null @@ -1,81 +0,0 @@ -// Copyright 2021 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use std::fmt::{self, Display, Formatter}; -use std::io; - -#[derive(Debug)] -pub enum RttError { - UnexpectedEchoPacketSize, - UnexpectedReplyPacketSize, - - MalformedSenderIdentity, - - MalformedEchoSignature, - MalformedReplySignature, - - InvalidEchoSignature, - InvalidReplySignature, - - UnreachableNode(String, io::Error), - UnexpectedConnectionFailureWrite(String, io::Error), - UnexpectedConnectionFailureRead(String, io::Error), - ConnectionReadTimeout(String), - ConnectionWriteTimeout(String), - - UnexpectedReplySequence, - - ShutdownReceived, -} - -impl Display for RttError { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - match self { - RttError::UnexpectedEchoPacketSize => { - write!(f, "The received echo packet had unexpected size") - } - RttError::UnexpectedReplyPacketSize => { - write!(f, "The received reply packet had unexpected size") - } - RttError::MalformedSenderIdentity => { - write!(f, "The received echo packet had malformed sender") - } - RttError::MalformedEchoSignature => { - write!(f, "The received echo packet had malformed signature") - } - RttError::MalformedReplySignature => { - write!(f, "The received reply packet had malformed signature") - } - RttError::InvalidEchoSignature => { - write!(f, "The received echo packet had invalid signature") - } - RttError::InvalidReplySignature => { - write!(f, "The received reply packet had invalid signature") - } - RttError::UnreachableNode(id, err) => { - write!(f, "Could not establish connection to {id} - {err}") - } - RttError::UnexpectedConnectionFailureWrite(id, err) => { - write!(f, "Failed to write echo packet to {id} - {err}") - } - RttError::UnexpectedConnectionFailureRead(id, err) => { - write!(f, "Failed to read reply packet from {id} - {err}") - } - RttError::ConnectionReadTimeout(id) => { - write!(f, "Timed out while trying to read reply packet from {id}") - } - RttError::ConnectionWriteTimeout(id) => { - write!(f, "Timed out while trying to write echo packet to {id}") - } - RttError::UnexpectedReplySequence => write!( - f, - "The received reply packet had an unexpected sequence number" - ), - RttError::ShutdownReceived => { - write!(f, "Shutdown signal received") - } - } - } -} - -impl std::error::Error for RttError {} diff --git a/common/mixnode-common/src/verloc/measurement.rs b/common/mixnode-common/src/verloc/measurement.rs deleted file mode 100644 index cacf72080a7..00000000000 --- a/common/mixnode-common/src/verloc/measurement.rs +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright 2021-2024 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use nym_node_http_api::state::metrics::{SharedVerlocStats, VerlocNodeResult}; -use std::mem; -use time::OffsetDateTime; - -pub(crate) trait VerlocStatsUpdateExt { - async fn start_new_measurements(&self, nodes_to_test: usize); - - async fn append_measurement_results(&self, new_data: Vec); - - async fn finish_measurements(&self); -} - -impl VerlocStatsUpdateExt for SharedVerlocStats { - async fn start_new_measurements(&self, nodes_to_test: usize) { - let mut guard = self.write().await; - guard.previous_run_data = mem::take(&mut guard.current_run_data); - guard.current_run_data.nodes_tested = nodes_to_test; - } - - async fn append_measurement_results(&self, mut new_data: Vec) { - let mut write_permit = self.write().await; - write_permit.current_run_data.results.append(&mut new_data); - // make sure the data always stays in order. - // TODO: considering the front of the results is guaranteed to be sorted, should perhaps - // a non-default sorting algorithm be used? - write_permit.current_run_data.results.sort() - } - - async fn finish_measurements(&self) { - self.write().await.current_run_data.run_finished = Some(OffsetDateTime::now_utc()) - } -} diff --git a/common/mixnode-common/src/verloc/mod.rs b/common/mixnode-common/src/verloc/mod.rs deleted file mode 100644 index 7bcea71c9d8..00000000000 --- a/common/mixnode-common/src/verloc/mod.rs +++ /dev/null @@ -1,383 +0,0 @@ -// Copyright 2021-2024 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::verloc::listener::PacketListener; -use crate::verloc::sender::{PacketSender, TestedNode}; -use futures::stream::FuturesUnordered; -use futures::StreamExt; -use log::*; -use nym_bin_common::version_checker::{self, parse_version}; -use nym_crypto::asymmetric::identity; -use nym_network_defaults::mainnet::NYM_API; -use nym_node_http_api::state::metrics::{SharedVerlocStats, VerlocNodeResult}; -use nym_task::TaskClient; -use rand::seq::SliceRandom; -use rand::thread_rng; -use std::net::SocketAddr; -use std::sync::Arc; -use std::time::Duration; -use tokio::task::JoinHandle; -use tokio::time::sleep; -use url::Url; - -use measurement::VerlocStatsUpdateExt; - -// pub use crate::verloc::measurement::{AtomicVerlocResult, Verloc, VerlocResult}; - -pub mod error; -pub(crate) mod listener; -pub(crate) mod measurement; -pub(crate) mod packet; -pub(crate) mod sender; - -// TODO: MUST BE UPDATED BEFORE ACTUAL RELEASE!! -pub const MINIMUM_NODE_VERSION: &str = "0.10.1"; - -// by default all of those are overwritten by config data from mixnodes directly -const DEFAULT_VERLOC_PORT: u16 = 1790; -const DEFAULT_PACKETS_PER_NODE: usize = 100; -const DEFAULT_PACKET_TIMEOUT: Duration = Duration::from_millis(1500); -const DEFAULT_CONNECTION_TIMEOUT: Duration = Duration::from_millis(5000); -const DEFAULT_DELAY_BETWEEN_PACKETS: Duration = Duration::from_millis(50); -const DEFAULT_BATCH_SIZE: usize = 50; -const DEFAULT_TESTING_INTERVAL: Duration = Duration::from_secs(60 * 60 * 12); -const DEFAULT_RETRY_TIMEOUT: Duration = Duration::from_secs(60 * 30); - -#[derive(Clone, Debug)] -pub struct Config { - /// Minimum semver version of a node (gateway or mixnode) that is capable of replying to echo packets. - minimum_compatible_node_version: version_checker::Version, - - /// Socket address of this node on which it will be listening for the measurement packets. - listening_address: SocketAddr, - - /// Specifies number of echo packets sent to each node during a measurement run. - packets_per_node: usize, - - /// Specifies maximum amount of time to wait for the reply packet to arrive before abandoning the test. - packet_timeout: Duration, - - /// Specifies maximum amount of time to wait for the connection to get established. - connection_timeout: Duration, - - /// Specifies delay between subsequent test packets being sent (after receiving a reply). - delay_between_packets: Duration, - - /// Specifies number of nodes being tested at once. - tested_nodes_batch_size: usize, - - /// Specifies delay between subsequent test runs. - testing_interval: Duration, - - /// Specifies delay between attempting to run the measurement again if the previous run failed - /// due to being unable to get the list of nodes. - retry_timeout: Duration, - - /// URLs to the nym apis for obtaining network topology. - nym_api_urls: Vec, -} - -impl Config { - pub fn build() -> ConfigBuilder { - ConfigBuilder::new() - } -} - -#[must_use] -pub struct ConfigBuilder(Config); - -impl ConfigBuilder { - pub fn new() -> ConfigBuilder { - Self::default() - } - - pub fn minimum_compatible_node_version(mut self, version: version_checker::Version) -> Self { - self.0.minimum_compatible_node_version = version; - self - } - - pub fn listening_address(mut self, listening_address: SocketAddr) -> Self { - self.0.listening_address = listening_address; - self - } - - pub fn packets_per_node(mut self, packets_per_node: usize) -> Self { - self.0.packets_per_node = packets_per_node; - self - } - - pub fn packet_timeout(mut self, packet_timeout: Duration) -> Self { - self.0.packet_timeout = packet_timeout; - self - } - - pub fn connection_timeout(mut self, connection_timeout: Duration) -> Self { - self.0.connection_timeout = connection_timeout; - self - } - - pub fn delay_between_packets(mut self, delay_between_packets: Duration) -> Self { - self.0.delay_between_packets = delay_between_packets; - self - } - - pub fn tested_nodes_batch_size(mut self, tested_nodes_batch_size: usize) -> Self { - self.0.tested_nodes_batch_size = tested_nodes_batch_size; - self - } - - pub fn testing_interval(mut self, testing_interval: Duration) -> Self { - self.0.testing_interval = testing_interval; - self - } - - pub fn retry_timeout(mut self, retry_timeout: Duration) -> Self { - self.0.retry_timeout = retry_timeout; - self - } - - pub fn nym_api_urls(mut self, nym_api_urls: Vec) -> Self { - self.0.nym_api_urls = nym_api_urls; - self - } - - pub fn build(self) -> Config { - // panics here are fine as those are only ever constructed at the initial setup - assert!( - !self.0.nym_api_urls.is_empty(), - "at least one validator endpoint must be provided", - ); - self.0 - } -} - -impl Default for ConfigBuilder { - fn default() -> Self { - ConfigBuilder(Config { - minimum_compatible_node_version: parse_version(MINIMUM_NODE_VERSION).unwrap(), - listening_address: format!("[::]:{DEFAULT_VERLOC_PORT}").parse().unwrap(), - packets_per_node: DEFAULT_PACKETS_PER_NODE, - packet_timeout: DEFAULT_PACKET_TIMEOUT, - connection_timeout: DEFAULT_CONNECTION_TIMEOUT, - delay_between_packets: DEFAULT_DELAY_BETWEEN_PACKETS, - tested_nodes_batch_size: DEFAULT_BATCH_SIZE, - testing_interval: DEFAULT_TESTING_INTERVAL, - retry_timeout: DEFAULT_RETRY_TIMEOUT, - nym_api_urls: vec![NYM_API.parse().expect("Invalid default API URL")], - }) - } -} - -pub struct VerlocMeasurer { - config: Config, - packet_sender: Arc, - packet_listener: Arc, - shutdown_listener: TaskClient, - - currently_used_api: usize, - - // Note: this client is only fine here as it does not maintain constant connection to the validator. - // It only does bunch of REST queries. If we update it at some point to a more sophisticated (maybe signing) client, - // then it definitely cannot be constructed here and probably will need to be passed from outside, - // as mixnodes/gateways would already be using an instance of said client. - validator_client: nym_validator_client::NymApiClient, - state: SharedVerlocStats, -} - -impl VerlocMeasurer { - pub fn new( - mut config: Config, - identity: Arc, - shutdown_listener: TaskClient, - ) -> Self { - config.nym_api_urls.shuffle(&mut thread_rng()); - - VerlocMeasurer { - packet_sender: Arc::new(PacketSender::new( - Arc::clone(&identity), - config.packets_per_node, - config.packet_timeout, - config.connection_timeout, - config.delay_between_packets, - shutdown_listener.clone().named("VerlocPacketSender"), - )), - packet_listener: Arc::new(PacketListener::new( - config.listening_address, - Arc::clone(&identity), - shutdown_listener.clone().named("VerlocPacketListener"), - )), - shutdown_listener, - currently_used_api: 0, - validator_client: nym_validator_client::NymApiClient::new( - config.nym_api_urls[0].clone(), - ), - config, - state: SharedVerlocStats::default(), - } - } - - pub fn set_shared_state(&mut self, state: SharedVerlocStats) { - self.state = state; - } - - fn use_next_nym_api(&mut self) { - if self.config.nym_api_urls.len() == 1 { - warn!("There's only a single validator API available - it won't be possible to use a different one"); - return; - } - - self.currently_used_api = (self.currently_used_api + 1) % self.config.nym_api_urls.len(); - self.validator_client - .change_nym_api(self.config.nym_api_urls[self.currently_used_api].clone()) - } - - fn start_listening(&self) -> JoinHandle<()> { - let packet_listener = Arc::clone(&self.packet_listener); - tokio::spawn(packet_listener.run()) - } - - async fn perform_measurement(&self, nodes_to_test: Vec) -> MeasurementOutcome { - log::trace!("Performing measurements"); - if nodes_to_test.is_empty() { - log::debug!("there are no nodes to measure"); - return MeasurementOutcome::Done; - } - - let mut shutdown_listener = self.shutdown_listener.clone().named("VerlocMeasurement"); - shutdown_listener.disarm(); - - for chunk in nodes_to_test.chunks(self.config.tested_nodes_batch_size) { - let mut chunk_results = Vec::with_capacity(chunk.len()); - - let mut measurement_chunk = chunk - .iter() - .map(|node| { - let node = *node; - let packet_sender = Arc::clone(&self.packet_sender); - // TODO: there's a potential issue here. if we make the measurement go into separate - // task, we risk biasing results with the bunch of context switches overhead - // but if we don't do it, it will take ages to complete - - // TODO: check performance difference when it's not spawned as a separate task - tokio::spawn(async move { - ( - packet_sender.send_packets_to_node(node).await, - node.identity, - ) - }) - }) - .collect::>(); - - // exhaust the results - while !shutdown_listener.is_shutdown() { - tokio::select! { - measurement_result = measurement_chunk.next() => { - let Some(result) = measurement_result else { - // if the stream has finished, it means we got everything we could have gotten - break - }; - - // if we receive JoinError it means the task failed to get executed, so either there's a bigger issue with tokio - // or there was a panic inside the task itself. In either case, we should just terminate ourselves. - let execution_result = result.expect("the measurement task panicked!"); - let identity = execution_result.1; - - let measurement_result = match execution_result.0 { - Err(err) => { - debug!("Failed to perform measurement for {identity}: {err}"); - None - } - Ok(result) => Some(result), - }; - chunk_results.push(VerlocNodeResult::new(identity, measurement_result)); - }, - _ = shutdown_listener.recv() => { - trace!("Shutdown received while measuring"); - return MeasurementOutcome::Shutdown; - } - } - } - - // update the results vector with chunks as they become available (by default every 50 nodes) - self.state.append_measurement_results(chunk_results).await; - } - - MeasurementOutcome::Done - } - - pub async fn run(&mut self) { - self.start_listening(); - - while !self.shutdown_listener.is_shutdown() { - info!("Starting verloc measurements"); - // TODO: should we also measure gateways? - - let all_mixes = match self.validator_client.get_all_described_nodes().await { - Ok(nodes) => nodes, - Err(err) => { - error!( - "failed to obtain list of mixnodes from the validator - {}. Going to attempt to use another validator API in the next run", - err - ); - self.use_next_nym_api(); - sleep(self.config.retry_timeout).await; - continue; - } - }; - if all_mixes.is_empty() { - warn!("There does not seem there are any nodes to measure...") - } - - // we only care about address and identity - let tested_nodes = all_mixes - .into_iter() - .filter(|n| n.description.declared_role.mixnode) - .filter_map(|node| { - // try to parse the identity and host - let node_identity = node.ed25519_identity_key(); - - let ip = node.description.host_information.ip_address.first()?; - let verloc_port = node.description.verloc_port(); - let verloc_host = SocketAddr::new(*ip, verloc_port); - - // TODO: possible problem in the future, this does name resolution and theoretically - // if a lot of nodes maliciously mis-configured themselves, it might take a while to resolve them all - // However, maybe it's not a problem as if they are misconfigured, they will eventually be - // pushed out of the network and on top of that, verloc is done in separate task that runs - // only every few hours. - Some(TestedNode::new(verloc_host, node_identity)) - }) - .collect::>(); - - // on start of each run remove old results - self.state.start_new_measurements(tested_nodes.len()).await; - - if let MeasurementOutcome::Shutdown = self.perform_measurement(tested_nodes).await { - log::trace!("Shutting down after aborting measurements"); - break; - } - - // write current time to "run finished" field - self.state.finish_measurements().await; - - info!( - "Finished performing verloc measurements. The next one will happen in {:?}", - self.config.testing_interval - ); - - tokio::select! { - _ = sleep(self.config.testing_interval) => {}, - _ = self.shutdown_listener.recv() => { - log::trace!("Shutdown received while sleeping"); - } - } - } - - log::trace!("Verloc: Exiting"); - } -} - -enum MeasurementOutcome { - Done, - Shutdown, -} diff --git a/common/nym_offline_compact_ecash/src/scheme/coin_indices_signatures.rs b/common/nym_offline_compact_ecash/src/scheme/coin_indices_signatures.rs index b08ab368dcb..30c7fe5523c 100644 --- a/common/nym_offline_compact_ecash/src/scheme/coin_indices_signatures.rs +++ b/common/nym_offline_compact_ecash/src/scheme/coin_indices_signatures.rs @@ -324,18 +324,6 @@ pub fn unchecked_aggregate_indices_signatures( _aggregate_indices_signatures(params, vk, signatures_shares, false) } -/// Generates parameters for the scheme setup. -/// -/// # Arguments -/// -/// * `total_coins` - it is the number of coins in a freshly generated wallet. It is the public parameter of the scheme. -/// -/// # Returns -/// -/// A `Parameters` struct containing group parameters, public key, the number of signatures (`total_coins`), -/// and a map of signatures for each index `l`. -/// - #[cfg(test)] mod tests { use super::*; diff --git a/common/nym_offline_compact_ecash/src/scheme/keygen.rs b/common/nym_offline_compact_ecash/src/scheme/keygen.rs index e49c6309df8..8db1597e617 100644 --- a/common/nym_offline_compact_ecash/src/scheme/keygen.rs +++ b/common/nym_offline_compact_ecash/src/scheme/keygen.rs @@ -264,7 +264,7 @@ impl<'b> Add<&'b VerificationKeyAuth> for VerificationKeyAuth { } } -impl<'a> Mul for &'a VerificationKeyAuth { +impl Mul for &VerificationKeyAuth { type Output = VerificationKeyAuth; #[inline] diff --git a/common/nym_offline_compact_ecash/src/scheme/mod.rs b/common/nym_offline_compact_ecash/src/scheme/mod.rs index 8912bf91eb1..7f9687a380a 100644 --- a/common/nym_offline_compact_ecash/src/scheme/mod.rs +++ b/common/nym_offline_compact_ecash/src/scheme/mod.rs @@ -984,7 +984,7 @@ pub struct SerialNumberRef<'a> { pub(crate) inner: &'a [G1Projective], } -impl<'a> SerialNumberRef<'a> { +impl SerialNumberRef<'_> { pub fn to_bytes(&self) -> Vec { let ss_len = self.inner.len(); let mut bytes: Vec = Vec::with_capacity(ss_len * 48); diff --git a/common/nymcoconut/src/elgamal.rs b/common/nymcoconut/src/elgamal.rs index 55db7dcdd27..1b814f0bc57 100644 --- a/common/nymcoconut/src/elgamal.rs +++ b/common/nymcoconut/src/elgamal.rs @@ -206,10 +206,10 @@ impl Deref for PublicKey { } } -impl<'a, 'b> Mul<&'b Scalar> for &'a PublicKey { +impl<'a> Mul<&'a Scalar> for &PublicKey { type Output = G1Projective; - fn mul(self, rhs: &'b Scalar) -> Self::Output { + fn mul(self, rhs: &'a Scalar) -> Self::Output { self.0 * rhs } } diff --git a/common/nymcoconut/src/scheme/keygen.rs b/common/nymcoconut/src/scheme/keygen.rs index 85e057793da..f80c1311437 100644 --- a/common/nymcoconut/src/scheme/keygen.rs +++ b/common/nymcoconut/src/scheme/keygen.rs @@ -305,7 +305,7 @@ impl<'b> Add<&'b VerificationKey> for VerificationKey { } } -impl<'a> Mul for &'a VerificationKey { +impl Mul for &VerificationKey { type Output = VerificationKey; #[inline] diff --git a/common/nymsphinx/addressing/src/clients.rs b/common/nymsphinx/addressing/src/clients.rs index a739e7369e6..743302edbe1 100644 --- a/common/nymsphinx/addressing/src/clients.rs +++ b/common/nymsphinx/addressing/src/clients.rs @@ -64,7 +64,7 @@ impl<'de> Deserialize<'de> for Recipient { { struct RecipientVisitor; - impl<'de> Visitor<'de> for RecipientVisitor { + impl Visitor<'_> for RecipientVisitor { type Value = Recipient; fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result { diff --git a/common/nymsphinx/addressing/src/nodes.rs b/common/nymsphinx/addressing/src/nodes.rs index e0cd745ae99..c748a72dcd1 100644 --- a/common/nymsphinx/addressing/src/nodes.rs +++ b/common/nymsphinx/addressing/src/nodes.rs @@ -1,6 +1,13 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +//! Encoding and decoding node routing information. +//! +//! This module is responsible for encoding and decoding node routing information, so that +//! they could be later put into an appropriate field in a sphinx header. +//! Currently, that routing information is an IP address, but in principle it can be anything +//! for as long as it's going to fit in the field. + use nym_crypto::asymmetric::identity; use nym_sphinx_types::{NodeAddressBytes, NODE_ADDRESS_LENGTH}; @@ -12,13 +19,6 @@ use thiserror::Error; pub type NodeIdentity = identity::PublicKey; pub const NODE_IDENTITY_SIZE: usize = identity::PUBLIC_KEY_LENGTH; -/// Encodoing and decoding node routing information. -/// -/// This module is responsible for encoding and decoding node routing information, so that -/// they could be later put into an appropriate field in a sphinx header. -/// Currently, that routing information is an IP address, but in principle it can be anything -/// for as long as it's going to fit in the field. - /// MAX_UNPADDED_LEN represents maximum length an unpadded address could have. /// In this case it's an ipv6 socket address (with version prefix) pub const MAX_NODE_ADDRESS_UNPADDED_LEN: usize = 19; diff --git a/common/nymsphinx/anonymous-replies/src/reply_surb.rs b/common/nymsphinx/anonymous-replies/src/reply_surb.rs index deac5f532d7..f25871ba0be 100644 --- a/common/nymsphinx/anonymous-replies/src/reply_surb.rs +++ b/common/nymsphinx/anonymous-replies/src/reply_surb.rs @@ -56,7 +56,7 @@ impl<'de> Deserialize<'de> for ReplySurb { { struct ReplySurbVisitor; - impl<'de> Visitor<'de> for ReplySurbVisitor { + impl Visitor<'_> for ReplySurbVisitor { type Value = ReplySurb; fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result { diff --git a/common/nymsphinx/framing/src/packet.rs b/common/nymsphinx/framing/src/packet.rs index 7d6dfcfa01c..fec9d5285f0 100644 --- a/common/nymsphinx/framing/src/packet.rs +++ b/common/nymsphinx/framing/src/packet.rs @@ -18,19 +18,13 @@ pub struct FramedNymPacket { } impl FramedNymPacket { - pub fn new(packet: NymPacket, packet_type: PacketType, use_legacy_version: bool) -> Self { + pub fn new(packet: NymPacket, packet_type: PacketType) -> Self { // If this fails somebody is using the library in a super incorrect way, because they // already managed to somehow create a sphinx packet let packet_size = PacketSize::get_type(packet.len()).unwrap(); - let use_legacy = if packet_type == PacketType::Outfox { - false - } else { - use_legacy_version - }; - let header = Header { - packet_version: PacketVersion::new(use_legacy), + packet_version: PacketVersion::new(), packet_size, packet_type, }; diff --git a/common/nymsphinx/params/src/packet_version.rs b/common/nymsphinx/params/src/packet_version.rs index c8c6ce3fc6d..8639dea4998 100644 --- a/common/nymsphinx/params/src/packet_version.rs +++ b/common/nymsphinx/params/src/packet_version.rs @@ -13,12 +13,8 @@ pub enum PacketVersion { } impl PacketVersion { - pub fn new(use_legacy: bool) -> Self { - if use_legacy { - Self::new_legacy() - } else { - Self::new_versioned(CURRENT_PACKET_VERSION_NUMBER) - } + pub fn new() -> Self { + Self::new_versioned(CURRENT_PACKET_VERSION_NUMBER) } pub fn new_legacy() -> Self { diff --git a/common/socks5/requests/src/request.rs b/common/socks5/requests/src/request.rs index 23e844db877..28a5182d001 100644 --- a/common/socks5/requests/src/request.rs +++ b/common/socks5/requests/src/request.rs @@ -253,25 +253,15 @@ impl Socks5RequestContent { /// Deserialize the request type, connection id, destination address and port, /// and the request body from bytes. /// - // TODO: this was already inaccurate - // /// Serialized bytes looks like this: - // /// - // /// -------------------------------------------------------------------------------------- - // /// request_flag | connection_id | address_length | remote_address_bytes | request_data | - // /// 1 | 8 | 2 | address_length | ... | - // /// -------------------------------------------------------------------------------------- - /// /// The request_flag tells us whether this is a new connection request (`new_connect`), /// an already-established connection we should send up (`new_send`), or /// a request to close an established connection (`new_close`). - // connect: // RequestFlag::Connect || CONN_ID || ADDR_LEN || ADDR || // // send: // RequestFlag::Send || CONN_ID || LOCAL_CLOSED || DATA // where DATA: SEQ || TRUE_DATA - pub fn try_from_bytes(b: &[u8]) -> Result { // each request needs to at least contain flag and ConnectionId if b.is_empty() { diff --git a/common/statistics/Cargo.toml b/common/statistics/Cargo.toml index 9f8b8db1776..230cf62a2f3 100644 --- a/common/statistics/Cargo.toml +++ b/common/statistics/Cargo.toml @@ -26,3 +26,4 @@ nym-sphinx = { path = "../nymsphinx" } nym-credentials-interface = { path = "../credentials-interface" } nym-metrics = { path = "../nym-metrics" } nym-task = { path = "../task" } + diff --git a/common/statistics/src/gateways.rs b/common/statistics/src/gateways.rs index 398fda559ad..cc617ae0274 100644 --- a/common/statistics/src/gateways.rs +++ b/common/statistics/src/gateways.rs @@ -31,40 +31,12 @@ impl GatewayStatsReporter { /// Gateway Statistics events pub enum GatewayStatsEvent { /// Events in the lifecycle of an established client tunnel - SessionStatsEvent(SessionEvent), -} - -impl GatewayStatsEvent { - /// A new session between this gateway and the client remote has successfully opened - pub fn new_session_start(client: DestinationAddressBytes) -> GatewayStatsEvent { - GatewayStatsEvent::SessionStatsEvent(SessionEvent::SessionStart { - start_time: OffsetDateTime::now_utc(), - client, - }) - } - - /// An existing session with the client remote has ended - pub fn new_session_stop(client: DestinationAddressBytes) -> GatewayStatsEvent { - GatewayStatsEvent::SessionStatsEvent(SessionEvent::SessionStop { - stop_time: OffsetDateTime::now_utc(), - client, - }) - } - - /// A new ecash ticket has been added / requested - pub fn new_ecash_ticket( - client: DestinationAddressBytes, - ticket_type: TicketType, - ) -> GatewayStatsEvent { - GatewayStatsEvent::SessionStatsEvent(SessionEvent::EcashTicket { - ticket_type, - client, - }) - } + SessionStatsEvent(GatewaySessionEvent), } /// Events in the lifecycle of an established client tunnel -pub enum SessionEvent { +#[derive(Debug, Clone, Copy)] +pub enum GatewaySessionEvent { /// A new session between this gateway and the client remote has successfully opened SessionStart { /// The timestamp of the session open event @@ -87,3 +59,32 @@ pub enum SessionEvent { client: DestinationAddressBytes, }, } + +impl GatewaySessionEvent { + /// A new session between this gateway and the client remote has successfully opened + pub fn new_session_start(client: DestinationAddressBytes) -> GatewaySessionEvent { + GatewaySessionEvent::SessionStart { + start_time: OffsetDateTime::now_utc(), + client, + } + } + + /// An existing session with the client remote has ended + pub fn new_session_stop(client: DestinationAddressBytes) -> GatewaySessionEvent { + GatewaySessionEvent::SessionStop { + stop_time: OffsetDateTime::now_utc(), + client, + } + } + + /// A new ecash ticket has been added / requested + pub fn new_ecash_ticket( + client: DestinationAddressBytes, + ticket_type: TicketType, + ) -> GatewaySessionEvent { + GatewaySessionEvent::EcashTicket { + ticket_type, + client, + } + } +} diff --git a/common/statistics/src/lib.rs b/common/statistics/src/lib.rs index eccf217d777..c0ee03dddcf 100644 --- a/common/statistics/src/lib.rs +++ b/common/statistics/src/lib.rs @@ -12,7 +12,7 @@ #![warn(clippy::dbg_macro)] use nym_crypto::asymmetric::ed25519; -use sha2::Digest; +use sha2::{Digest, Sha256}; /// Client specific statistics interfaces and events. pub mod clients; @@ -36,3 +36,7 @@ fn generate_stats_id>(prefix: &str, id_seed: M) -> String { let output = hasher.finalize(); format!("{:x}", output) } + +pub fn hash_identifier>(identifier: M) -> String { + format!("{:x}", Sha256::digest(identifier)) +} diff --git a/common/task/src/manager.rs b/common/task/src/manager.rs index b3bc6e02323..7d8ba3a780e 100644 --- a/common/task/src/manager.rs +++ b/common/task/src/manager.rs @@ -455,6 +455,10 @@ impl TaskClient { self.mode.set_should_not_signal_on_drop(); } + pub fn rearm(&mut self) { + self.mode.set_should_signal_on_drop(); + } + pub fn send_we_stopped(&mut self, err: SentError) { if self.mode.is_dummy() { return; @@ -482,7 +486,7 @@ impl Drop for TaskClient { if !self.mode.should_signal_on_drop() { self.log( Level::Trace, - "the task client is getting dropped but inststructed to not signal: this is expected during client shutdown", + "the task client is getting dropped but instructed to not signal: this is expected during client shutdown", ); return; } else { @@ -527,6 +531,14 @@ impl ClientOperatingMode { } } + fn set_should_signal_on_drop(&mut self) { + use ClientOperatingMode::{Dummy, Listening, ListeningButDontReportHalt}; + *self = match &self { + ListeningButDontReportHalt | Listening => Listening, + Dummy => Dummy, + }; + } + fn set_should_not_signal_on_drop(&mut self) { use ClientOperatingMode::{Dummy, Listening, ListeningButDontReportHalt}; *self = match &self { diff --git a/common/topology/src/filter.rs b/common/topology/src/filter.rs deleted file mode 100644 index baf76225f8d..00000000000 --- a/common/topology/src/filter.rs +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright 2021 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use nym_bin_common::version_checker; -use std::collections::{BTreeMap, HashMap}; -use std::hash::Hash; - -pub trait Versioned: Clone { - fn version(&self) -> String; -} - -pub trait VersionFilterable { - #[must_use] - fn filter_by_version(&self, expected_version: &str) -> Self; -} - -impl VersionFilterable for Vec -where - T: Versioned, -{ - fn filter_by_version(&self, expected_version: &str) -> Self { - self.iter() - .filter(|node| { - version_checker::is_minor_version_compatible(&node.version(), expected_version) - }) - .cloned() - .collect() - } -} - -impl VersionFilterable for HashMap -where - K: Eq + Hash + Clone, - V: VersionFilterable, - T: Versioned, -{ - fn filter_by_version(&self, expected_version: &str) -> Self { - self.iter() - .map(|(k, v)| (k.clone(), v.filter_by_version(expected_version))) - .collect() - } -} - -impl VersionFilterable for BTreeMap -where - K: Eq + Ord + Clone, - V: VersionFilterable, - T: Versioned, -{ - fn filter_by_version(&self, expected_version: &str) -> Self { - self.iter() - .map(|(k, v)| (k.clone(), v.filter_by_version(expected_version))) - .collect() - } -} diff --git a/common/topology/src/gateway.rs b/common/topology/src/gateway.rs index e6f4981560f..545f47dd211 100644 --- a/common/topology/src/gateway.rs +++ b/common/topology/src/gateway.rs @@ -1,7 +1,7 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::{filter, NetworkAddress, NodeVersion}; +use crate::{NetworkAddress, NodeVersion}; use nym_api_requests::nym_nodes::SkimmedNode; use nym_crypto::asymmetric::{encryption, identity}; use nym_mixnet_contract_common::NodeId; @@ -126,13 +126,6 @@ impl fmt::Display for LegacyNode { } } -impl filter::Versioned for LegacyNode { - fn version(&self) -> String { - // TODO: return semver instead - self.version.to_string() - } -} - impl<'a> From<&'a LegacyNode> for SphinxNode { fn from(node: &'a LegacyNode) -> Self { let node_address_bytes = NymNodeRoutingAddress::from(node.mix_host) diff --git a/common/topology/src/lib.rs b/common/topology/src/lib.rs index 6e89cf27535..4133a8ae0e3 100644 --- a/common/topology/src/lib.rs +++ b/common/topology/src/lib.rs @@ -4,7 +4,6 @@ #![allow(unknown_lints)] // clippy::to_string_trait_impl is not on stable as of 1.77 -use crate::filter::VersionFilterable; pub use error::NymTopologyError; use log::{debug, info, warn}; use nym_api_requests::nym_nodes::{CachedNodesResponse, SkimmedNode}; @@ -25,7 +24,6 @@ use std::str::FromStr; use ::serde::{Deserialize, Deserializer, Serialize, Serializer}; pub mod error; -pub mod filter; pub mod gateway; pub mod mix; pub mod random_route_provider; @@ -465,19 +463,6 @@ impl NymTopology { Ok(()) } - - #[must_use] - pub fn filter_system_version(&self, expected_version: &str) -> Self { - self.filter_node_versions(expected_version) - } - - #[must_use] - pub fn filter_node_versions(&self, expected_mix_version: &str) -> Self { - NymTopology { - mixes: self.mixes.filter_by_version(expected_mix_version), - gateways: self.gateways.clone(), - } - } } #[cfg(feature = "serializable")] diff --git a/common/topology/src/mix.rs b/common/topology/src/mix.rs index 170f1000a55..40c61cff4b4 100644 --- a/common/topology/src/mix.rs +++ b/common/topology/src/mix.rs @@ -1,7 +1,7 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::{filter, NetworkAddress, NodeVersion}; +use crate::{NetworkAddress, NodeVersion}; use nym_api_requests::nym_nodes::{NodeRole, SkimmedNode}; use nym_crypto::asymmetric::{encryption, identity}; pub use nym_mixnet_contract_common::LegacyMixLayer; @@ -89,13 +89,6 @@ impl LegacyNode { } } -impl filter::Versioned for LegacyNode { - fn version(&self) -> String { - // TODO: return semver instead - self.version.to_string() - } -} - impl<'a> From<&'a LegacyNode> for SphinxNode { fn from(node: &'a LegacyNode) -> Self { let node_address_bytes = NymNodeRoutingAddress::from(node.mix_host) diff --git a/common/verloc/Cargo.toml b/common/verloc/Cargo.toml new file mode 100644 index 00000000000..e906f72dde8 --- /dev/null +++ b/common/verloc/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "nym-verloc" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true + +[dependencies] +bytes = { workspace = true } +futures = { workspace = true } +humantime = { workspace = true } +tracing = { workspace = true } +time = { workspace = true } +tokio = { workspace = true, features = ["sync", "net", "rt-multi-thread", "io-util"] } +tokio-util = { workspace = true, features = ["codec"] } +thiserror = { workspace = true } +rand = { workspace = true } +url = { workspace = true } + +nym-crypto = { path = "../crypto", features = ["asymmetric"] } +nym-task = { path = "../task" } +nym-validator-client = { path = "../client-libs/validator-client" } diff --git a/common/verloc/src/error.rs b/common/verloc/src/error.rs new file mode 100644 index 00000000000..836177d62f9 --- /dev/null +++ b/common/verloc/src/error.rs @@ -0,0 +1,72 @@ +// Copyright 2021 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use std::io; +use std::net::SocketAddr; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum VerlocError { + #[error("the received echo packet had unexpected size")] + UnexpectedEchoPacketSize, + + #[error("the received reply packet had unexpected size")] + UnexpectedReplyPacketSize, + + #[error("the received echo packet had malformed sender")] + MalformedSenderIdentity, + + #[error("the received echo packet had malformed signature")] + MalformedEchoSignature, + + #[error("the received reply packet had malformed signature")] + MalformedReplySignature, + + #[error("the received echo packet had invalid signature")] + InvalidEchoSignature, + + #[error("the received reply packet had invalid signature")] + InvalidReplySignature, + + #[error("could not establish connection to {identity} on {address}: {err}")] + UnreachableNode { + identity: String, + address: SocketAddr, + #[source] + err: io::Error, + }, + + #[error("failed to write echo packet to {identity} on {address}: {err}")] + UnexpectedConnectionFailureWrite { + identity: String, + address: SocketAddr, + #[source] + err: io::Error, + }, + + #[error("failed to read reply packet from {identity} on {address}: {err}")] + UnexpectedConnectionFailureRead { + identity: String, + address: SocketAddr, + #[source] + err: io::Error, + }, + + #[error("timed out while trying to read reply packet from {identity} on {address}")] + ConnectionReadTimeout { + identity: String, + address: SocketAddr, + }, + + #[error("timed out while trying to write echo packet to {identity} on {address}")] + ConnectionWriteTimeout { + identity: String, + address: SocketAddr, + }, + + #[error("the received reply packet had an unexpected sequence number")] + UnexpectedReplySequence, + + #[error("shutdown signal received")] + ShutdownReceived, +} diff --git a/common/verloc/src/lib.rs b/common/verloc/src/lib.rs new file mode 100644 index 00000000000..2f976e06fb9 --- /dev/null +++ b/common/verloc/src/lib.rs @@ -0,0 +1,11 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +#![warn(clippy::expect_used)] +#![warn(clippy::unwrap_used)] +#![warn(clippy::todo)] +#![warn(clippy::dbg_macro)] + +pub mod error; +pub mod measurements; +pub mod models; diff --git a/common/verloc/src/measurements/config.rs b/common/verloc/src/measurements/config.rs new file mode 100644 index 00000000000..d6a15ef3d2b --- /dev/null +++ b/common/verloc/src/measurements/config.rs @@ -0,0 +1,135 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use nym_validator_client::UserAgent; +use std::net::{IpAddr, Ipv6Addr, SocketAddr}; +use std::time::Duration; +use url::Url; + +// by default all of those are overwritten by config data from nym-node directly +const DEFAULT_VERLOC_PORT: u16 = 1790; +const DEFAULT_PACKETS_PER_NODE: usize = 100; +const DEFAULT_PACKET_TIMEOUT: Duration = Duration::from_millis(1500); +const DEFAULT_CONNECTION_TIMEOUT: Duration = Duration::from_millis(5000); +const DEFAULT_DELAY_BETWEEN_PACKETS: Duration = Duration::from_millis(50); +const DEFAULT_BATCH_SIZE: usize = 50; +const DEFAULT_TESTING_INTERVAL: Duration = Duration::from_secs(60 * 60 * 12); +const DEFAULT_RETRY_TIMEOUT: Duration = Duration::from_secs(60 * 30); + +#[derive(Clone, Debug)] +pub struct Config { + /// Socket address of this node on which it will be listening for the measurement packets. + pub listening_address: SocketAddr, + + /// Specifies number of echo packets sent to each node during a measurement run. + pub packets_per_node: usize, + + /// Specifies maximum amount of time to wait for the reply packet to arrive before abandoning the test. + pub packet_timeout: Duration, + + /// Specifies maximum amount of time to wait for the connection to get established. + pub connection_timeout: Duration, + + /// Specifies delay between subsequent test packets being sent (after receiving a reply). + pub delay_between_packets: Duration, + + /// Specifies number of nodes being tested at once. + pub tested_nodes_batch_size: usize, + + /// Specifies delay between subsequent test runs. + pub testing_interval: Duration, + + /// Specifies delay between attempting to run the measurement again if the previous run failed + /// due to being unable to get the list of nodes. + pub retry_timeout: Duration, + + /// URLs to the nym apis for obtaining network topology. + pub nym_api_urls: Vec, + + /// User agent used for querying the nym-api + pub user_agent: UserAgent, +} + +impl Config { + pub fn build(nym_api_urls: Vec, user_agent: impl Into) -> ConfigBuilder { + ConfigBuilder::new(nym_api_urls, user_agent) + } +} + +#[must_use] +pub struct ConfigBuilder(Config); + +impl ConfigBuilder { + pub fn new(nym_api_urls: Vec, user_agent: impl Into) -> ConfigBuilder { + ConfigBuilder(Config { + // '[::]:port' + listening_address: SocketAddr::new( + IpAddr::V6(Ipv6Addr::UNSPECIFIED), + DEFAULT_VERLOC_PORT, + ), + packets_per_node: DEFAULT_PACKETS_PER_NODE, + packet_timeout: DEFAULT_PACKET_TIMEOUT, + connection_timeout: DEFAULT_CONNECTION_TIMEOUT, + delay_between_packets: DEFAULT_DELAY_BETWEEN_PACKETS, + tested_nodes_batch_size: DEFAULT_BATCH_SIZE, + testing_interval: DEFAULT_TESTING_INTERVAL, + retry_timeout: DEFAULT_RETRY_TIMEOUT, + nym_api_urls, + user_agent: user_agent.into(), + }) + } + + pub fn listening_address(mut self, listening_address: SocketAddr) -> Self { + self.0.listening_address = listening_address; + self + } + + pub fn packets_per_node(mut self, packets_per_node: usize) -> Self { + self.0.packets_per_node = packets_per_node; + self + } + + pub fn packet_timeout(mut self, packet_timeout: Duration) -> Self { + self.0.packet_timeout = packet_timeout; + self + } + + pub fn connection_timeout(mut self, connection_timeout: Duration) -> Self { + self.0.connection_timeout = connection_timeout; + self + } + + pub fn delay_between_packets(mut self, delay_between_packets: Duration) -> Self { + self.0.delay_between_packets = delay_between_packets; + self + } + + pub fn tested_nodes_batch_size(mut self, tested_nodes_batch_size: usize) -> Self { + self.0.tested_nodes_batch_size = tested_nodes_batch_size; + self + } + + pub fn testing_interval(mut self, testing_interval: Duration) -> Self { + self.0.testing_interval = testing_interval; + self + } + + pub fn retry_timeout(mut self, retry_timeout: Duration) -> Self { + self.0.retry_timeout = retry_timeout; + self + } + + pub fn nym_api_urls(mut self, nym_api_urls: Vec) -> Self { + self.0.nym_api_urls = nym_api_urls; + self + } + + pub fn build(self) -> Config { + // panics here are fine as those are only ever constructed at the initial setup + assert!( + !self.0.nym_api_urls.is_empty(), + "at least one validator endpoint must be provided", + ); + self.0 + } +} diff --git a/common/mixnode-common/src/verloc/listener.rs b/common/verloc/src/measurements/listener.rs similarity index 78% rename from common/mixnode-common/src/verloc/listener.rs rename to common/verloc/src/measurements/listener.rs index aa58b56b8e7..77c17390687 100644 --- a/common/mixnode-common/src/verloc/listener.rs +++ b/common/verloc/src/measurements/listener.rs @@ -1,29 +1,29 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::verloc::error::RttError; -use crate::verloc::packet::{EchoPacket, ReplyPacket}; +use crate::error::VerlocError; +use crate::measurements::packet::{EchoPacket, ReplyPacket}; use bytes::{BufMut, BytesMut}; use futures::StreamExt; -use log::*; use nym_crypto::asymmetric::identity; use nym_task::TaskClient; -use std::fmt::{Display, Formatter}; use std::net::SocketAddr; use std::sync::Arc; -use std::{fmt, io, process}; +use std::{io, process}; +use thiserror::Error; use tokio::io::AsyncWriteExt; use tokio::net::{TcpListener, TcpStream}; use tokio_util::codec::{Decoder, Encoder, Framed}; +use tracing::{debug, error, info, trace, warn}; -pub(crate) struct PacketListener { +pub struct PacketListener { address: SocketAddr, connection_handler: Arc, shutdown: TaskClient, } impl PacketListener { - pub(crate) fn new( + pub fn new( address: SocketAddr, identity: Arc, shutdown: TaskClient, @@ -37,13 +37,13 @@ impl PacketListener { } impl PacketListener { - pub(super) async fn run(self: Arc) { + pub async fn run(self: Arc) { let listener = match TcpListener::bind(self.address).await { Ok(listener) => listener, Err(err) => { error!( - "Failed to bind to {} - {}. Are you sure nothing else is running on the specified port and your user has sufficient permission to bind to the requested address?", - self.address, err + "Failed to bind to {}: {err}. Are you sure nothing else is running on the specified port and your user has sufficient permission to bind to the requested address?", + self.address ); process::exit(1); } @@ -71,7 +71,7 @@ impl PacketListener { } }, _ = shutdown_listener.recv() => { - log::trace!("PacketListener: Received shutdown"); + trace!("PacketListener: Received shutdown"); } } } @@ -137,28 +137,18 @@ impl ConnectionHandler { } } -#[derive(Debug)] +#[derive(Debug, Error)] enum EchoPacketCodecError { - IoError(io::Error), - PacketRecoveryError(RttError), -} + #[error("encountered io error {0}")] + IoError(#[from] io::Error), -impl Display for EchoPacketCodecError { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - match self { - EchoPacketCodecError::IoError(err) => write!(f, "encountered io error - {err}"), - EchoPacketCodecError::PacketRecoveryError(err) => { - write!(f, "failed to correctly decode an echo packet - {err}") - } - } - } + #[error("failed to correctly decode an echo packet: {0}")] + PacketRecoveryError(Box), } -impl std::error::Error for EchoPacketCodecError {} - -impl From for EchoPacketCodecError { - fn from(err: io::Error) -> Self { - EchoPacketCodecError::IoError(err) +impl From for EchoPacketCodecError { + fn from(value: VerlocError) -> Self { + EchoPacketCodecError::PacketRecoveryError(Box::new(value)) } } @@ -188,10 +178,7 @@ impl Decoder for EchoPacketCodec { let packet_bytes = src.split_to(EchoPacket::SIZE); - let echo_packet = match EchoPacket::try_from_bytes(&packet_bytes) { - Ok(packet) => packet, - Err(err) => return Err(EchoPacketCodecError::PacketRecoveryError(err)), - }; + let echo_packet = EchoPacket::try_from_bytes(&packet_bytes)?; // reserve enough bytes for the next frame src.reserve(EchoPacket::SIZE); diff --git a/common/verloc/src/measurements/measurer.rs b/common/verloc/src/measurements/measurer.rs new file mode 100644 index 00000000000..e56f35d0868 --- /dev/null +++ b/common/verloc/src/measurements/measurer.rs @@ -0,0 +1,225 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::measurements::metrics::SharedVerlocStats; +use crate::measurements::sender::TestedNode; +use crate::measurements::{Config, PacketListener, PacketSender}; +use crate::models::VerlocNodeResult; +use futures::stream::FuturesUnordered; +use futures::StreamExt; +use nym_crypto::asymmetric::identity; +use nym_task::TaskClient; +use nym_validator_client::models::NymNodeDescription; +use nym_validator_client::NymApiClient; +use rand::prelude::SliceRandom; +use rand::thread_rng; +use std::net::SocketAddr; +use std::sync::Arc; +use tokio::task::JoinHandle; +use tokio::time::sleep; +use tracing::{debug, error, info, trace, warn}; + +pub struct VerlocMeasurer { + config: Config, + packet_sender: Arc, + packet_listener: Arc, + shutdown_listener: TaskClient, + state: SharedVerlocStats, +} + +impl VerlocMeasurer { + pub fn new( + config: Config, + identity: Arc, + shutdown_listener: TaskClient, + ) -> Self { + VerlocMeasurer { + packet_sender: Arc::new(PacketSender::new( + Arc::clone(&identity), + config.packets_per_node, + config.packet_timeout, + config.connection_timeout, + config.delay_between_packets, + shutdown_listener.clone().named("VerlocPacketSender"), + )), + packet_listener: Arc::new(PacketListener::new( + config.listening_address, + Arc::clone(&identity), + shutdown_listener.clone().named("VerlocPacketListener"), + )), + shutdown_listener, + config, + state: SharedVerlocStats::default(), + } + } + + pub fn set_shared_state(&mut self, state: SharedVerlocStats) { + self.state = state; + } + + fn start_listening(&self) -> JoinHandle<()> { + let packet_listener = Arc::clone(&self.packet_listener); + tokio::spawn(packet_listener.run()) + } + + async fn perform_measurement(&self, nodes_to_test: Vec) -> MeasurementOutcome { + trace!("Performing measurements"); + if nodes_to_test.is_empty() { + debug!("there are no nodes to measure"); + return MeasurementOutcome::Done; + } + + let mut shutdown_listener = self.shutdown_listener.clone().named("VerlocMeasurement"); + shutdown_listener.disarm(); + + for chunk in nodes_to_test.chunks(self.config.tested_nodes_batch_size) { + let mut chunk_results = Vec::with_capacity(chunk.len()); + + let mut measurement_chunk = chunk + .iter() + .map(|node| { + let node = *node; + let packet_sender = Arc::clone(&self.packet_sender); + // TODO: there's a potential issue here. if we make the measurement go into separate + // task, we risk biasing results with the bunch of context switches overhead + // but if we don't do it, it will take ages to complete + + // TODO: check performance difference when it's not spawned as a separate task + tokio::spawn(async move { + ( + packet_sender.send_packets_to_node(node).await, + node.identity, + ) + }) + }) + .collect::>(); + + // exhaust the results + while !shutdown_listener.is_shutdown() { + tokio::select! { + measurement_result = measurement_chunk.next() => { + let Some(result) = measurement_result else { + // if the stream has finished, it means we got everything we could have gotten + break + }; + + // if we receive JoinError it means the task failed to get executed, so either there's a bigger issue with tokio + // or there was a panic inside the task itself. In either case, we should just terminate ourselves. + let Ok(execution_result) = result else { + error!("the verloc measurer has panicked!"); + continue + }; + let identity = execution_result.1; + + let measurement_result = match execution_result.0 { + Err(err) => { + debug!("Failed to perform measurement for {identity}: {err}"); + None + } + Ok(result) => Some(result), + }; + chunk_results.push(VerlocNodeResult::new(identity, measurement_result)); + }, + _ = shutdown_listener.recv() => { + trace!("Shutdown received while measuring"); + return MeasurementOutcome::Shutdown; + } + } + } + + // update the results vector with chunks as they become available (by default every 50 nodes) + self.state.append_measurement_results(chunk_results).await; + } + + MeasurementOutcome::Done + } + + async fn get_list_of_nodes(&self) -> Option> { + let mut api_endpoints = self.config.nym_api_urls.clone(); + api_endpoints.shuffle(&mut thread_rng()); + for api_endpoint in api_endpoints { + let client = NymApiClient::new_with_user_agent( + api_endpoint.clone(), + self.config.user_agent.clone(), + ); + match client.get_all_described_nodes().await { + Ok(res) => return Some(res), + Err(err) => { + warn!("failed to get described nodes from {api_endpoint}: {err}") + } + } + } + None + } + + pub async fn run(&mut self) { + self.start_listening(); + + while !self.shutdown_listener.is_shutdown() { + info!("Starting verloc measurements"); + // TODO: should we also measure gateways? + + let Some(all_nodes) = self.get_list_of_nodes().await else { + error!("failed to obtain list of all nodes from any available api endpoint"); + sleep(self.config.retry_timeout).await; + continue; + }; + + if all_nodes.is_empty() { + warn!("it does not seem there are any nodes to measure..."); + sleep(self.config.retry_timeout).await; + continue; + } + + // we only care about address and identity + let tested_nodes = all_nodes + .into_iter() + .filter_map(|node| { + // try to parse the identity and host + let node_identity = node.ed25519_identity_key(); + + let ip = node.description.host_information.ip_address.first()?; + let verloc_port = node.description.verloc_port(); + let verloc_host = SocketAddr::new(*ip, verloc_port); + + // TODO: possible problem in the future, this does name resolution and theoretically + // if a lot of nodes maliciously mis-configured themselves, it might take a while to resolve them all + // However, maybe it's not a problem as if they are misconfigured, they will eventually be + // pushed out of the network and on top of that, verloc is done in separate task that runs + // only every few hours. + Some(TestedNode::new(verloc_host, node_identity)) + }) + .collect::>(); + + // on start of each run remove old results + self.state.start_new_measurements(tested_nodes.len()).await; + + if let MeasurementOutcome::Shutdown = self.perform_measurement(tested_nodes).await { + trace!("Shutting down after aborting measurements"); + break; + } + + // write current time to "run finished" field + self.state.finish_measurements().await; + + info!( + "Finished performing verloc measurements. The next one will happen in {:?}", + self.config.testing_interval + ); + + tokio::select! { + _ = sleep(self.config.testing_interval) => {}, + _ = self.shutdown_listener.recv() => { + trace!("Shutdown received while sleeping"); + } + } + } + + trace!("Verloc: Exiting"); + } +} + +enum MeasurementOutcome { + Done, + Shutdown, +} diff --git a/common/verloc/src/measurements/metrics.rs b/common/verloc/src/measurements/metrics.rs new file mode 100644 index 00000000000..210607696e5 --- /dev/null +++ b/common/verloc/src/measurements/metrics.rs @@ -0,0 +1,50 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::models::{VerlocNodeResult, VerlocResultData}; +use std::mem; +use std::sync::Arc; +use time::OffsetDateTime; +use tokio::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard}; + +#[derive(Clone, Debug, Default)] +pub struct SharedVerlocStats { + inner: Arc>, +} + +impl SharedVerlocStats { + pub(crate) async fn start_new_measurements(&self, nodes_to_test: usize) { + let mut guard = self.write().await; + guard.previous_run_data = mem::take(&mut guard.current_run_data); + guard.current_run_data.nodes_tested = nodes_to_test; + } + + pub(crate) async fn append_measurement_results(&self, mut new_data: Vec) { + let mut write_permit = self.write().await; + write_permit.current_run_data.results.append(&mut new_data); + // make sure the data always stays in order. + // TODO: considering the front of the results is guaranteed to be sorted, should perhaps + // a non-default sorting algorithm be used? + write_permit.current_run_data.results.sort() + } + + pub(crate) async fn finish_measurements(&self) { + self.write().await.current_run_data.run_finished = Some(OffsetDateTime::now_utc()) + } +} + +#[derive(Clone, Debug, Default)] +pub struct VerlocStatsState { + pub current_run_data: VerlocResultData, + pub previous_run_data: VerlocResultData, +} + +impl SharedVerlocStats { + pub async fn read(&self) -> RwLockReadGuard<'_, VerlocStatsState> { + self.inner.read().await + } + + pub async fn write(&self) -> RwLockWriteGuard<'_, VerlocStatsState> { + self.inner.write().await + } +} diff --git a/common/verloc/src/measurements/mod.rs b/common/verloc/src/measurements/mod.rs new file mode 100644 index 00000000000..303fab22408 --- /dev/null +++ b/common/verloc/src/measurements/mod.rs @@ -0,0 +1,16 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod config; +pub mod listener; +pub mod measurer; +pub mod metrics; +pub mod packet; +pub mod sender; + +pub use config::{Config, ConfigBuilder}; +pub use listener::PacketListener; +pub use measurer::VerlocMeasurer; +pub use metrics::{SharedVerlocStats, VerlocStatsState}; +pub use packet::{EchoPacket, ReplyPacket}; +pub use sender::PacketSender; diff --git a/common/mixnode-common/src/verloc/packet.rs b/common/verloc/src/measurements/packet.rs similarity index 61% rename from common/mixnode-common/src/verloc/packet.rs rename to common/verloc/src/measurements/packet.rs index c5729904b78..2a918b007a9 100644 --- a/common/mixnode-common/src/verloc/packet.rs +++ b/common/verloc/src/measurements/packet.rs @@ -1,20 +1,20 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::verloc::error::RttError; -use nym_crypto::asymmetric::identity::{self, PUBLIC_KEY_LENGTH, SIGNATURE_LENGTH}; +use crate::error::VerlocError; +use nym_crypto::asymmetric::ed25519::{self, PUBLIC_KEY_LENGTH, SIGNATURE_LENGTH}; -pub(crate) struct EchoPacket { +pub struct EchoPacket { sequence_number: u64, - sender: identity::PublicKey, + sender: ed25519::PublicKey, - signature: identity::Signature, + signature: ed25519::Signature, } impl EchoPacket { pub(crate) const SIZE: usize = 8 + PUBLIC_KEY_LENGTH + SIGNATURE_LENGTH; - pub(crate) fn new(sequence_number: u64, keys: &identity::KeyPair) -> Self { + pub(crate) fn new(sequence_number: u64, keys: &ed25519::KeyPair) -> Self { let bytes_to_sign = sequence_number .to_be_bytes() .iter() @@ -42,20 +42,22 @@ impl EchoPacket { .collect() } - pub(crate) fn try_from_bytes(bytes: &[u8]) -> Result { + pub(crate) fn try_from_bytes(bytes: &[u8]) -> Result { if bytes.len() != Self::SIZE { - return Err(RttError::UnexpectedEchoPacketSize); + return Err(VerlocError::UnexpectedEchoPacketSize); } + // SAFETY: we have ensured our packet has correct size + #[allow(clippy::unwrap_used)] let sequence_number = u64::from_be_bytes(bytes[..8].try_into().unwrap()); - let sender = identity::PublicKey::from_bytes(&bytes[8..8 + PUBLIC_KEY_LENGTH]) - .map_err(|_| RttError::MalformedSenderIdentity)?; - let signature = identity::Signature::from_bytes(&bytes[8 + PUBLIC_KEY_LENGTH..]) - .map_err(|_| RttError::MalformedEchoSignature)?; + let sender = ed25519::PublicKey::from_bytes(&bytes[8..8 + PUBLIC_KEY_LENGTH]) + .map_err(|_| VerlocError::MalformedSenderIdentity)?; + let signature = ed25519::Signature::from_bytes(&bytes[8 + PUBLIC_KEY_LENGTH..]) + .map_err(|_| VerlocError::MalformedEchoSignature)?; sender .verify(&bytes[..Self::SIZE - SIGNATURE_LENGTH], &signature) - .map_err(|_| RttError::InvalidEchoSignature)?; + .map_err(|_| VerlocError::InvalidEchoSignature)?; Ok(EchoPacket { sequence_number, @@ -64,7 +66,7 @@ impl EchoPacket { }) } - pub(crate) fn construct_reply(self, private_key: &identity::PrivateKey) -> ReplyPacket { + pub(crate) fn construct_reply(self, private_key: &ed25519::PrivateKey) -> ReplyPacket { let bytes = self.to_bytes(); let signature = private_key.sign(bytes); ReplyPacket { @@ -74,9 +76,9 @@ impl EchoPacket { } } -pub(crate) struct ReplyPacket { +pub struct ReplyPacket { base_packet: EchoPacket, - signature: identity::Signature, + signature: ed25519::Signature, } impl ReplyPacket { @@ -96,21 +98,21 @@ impl ReplyPacket { pub(crate) fn try_from_bytes( bytes: &[u8], - remote_identity: &identity::PublicKey, - ) -> Result { + remote_ed25519: &ed25519::PublicKey, + ) -> Result { if bytes.len() != Self::SIZE { - return Err(RttError::UnexpectedReplyPacketSize); + return Err(VerlocError::UnexpectedReplyPacketSize); } let base_packet = EchoPacket::try_from_bytes(&bytes[..8 + PUBLIC_KEY_LENGTH + SIGNATURE_LENGTH])?; let signature = - identity::Signature::from_bytes(&bytes[8 + PUBLIC_KEY_LENGTH + SIGNATURE_LENGTH..]) - .map_err(|_| RttError::MalformedReplySignature)?; + ed25519::Signature::from_bytes(&bytes[8 + PUBLIC_KEY_LENGTH + SIGNATURE_LENGTH..]) + .map_err(|_| VerlocError::MalformedReplySignature)?; - remote_identity + remote_ed25519 .verify(&bytes[..Self::SIZE - SIGNATURE_LENGTH], &signature) - .map_err(|_| RttError::InvalidReplySignature)?; + .map_err(|_| VerlocError::InvalidReplySignature)?; Ok(ReplyPacket { base_packet, diff --git a/common/mixnode-common/src/verloc/sender.rs b/common/verloc/src/measurements/sender.rs similarity index 68% rename from common/mixnode-common/src/verloc/sender.rs rename to common/verloc/src/measurements/sender.rs index 5887a22a6ef..292cd8d8c0b 100644 --- a/common/mixnode-common/src/verloc/sender.rs +++ b/common/verloc/src/measurements/sender.rs @@ -1,11 +1,10 @@ // Copyright 2021-2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::verloc::error::RttError; -use crate::verloc::packet::{EchoPacket, ReplyPacket}; -use log::*; -use nym_crypto::asymmetric::identity; -use nym_node_http_api::state::metrics::VerlocMeasurement; +use crate::error::VerlocError; +use crate::measurements::packet::{EchoPacket, ReplyPacket}; +use crate::models::VerlocMeasurement; +use nym_crypto::asymmetric::ed25519; use nym_task::TaskClient; use rand::{thread_rng, Rng}; use std::net::SocketAddr; @@ -15,15 +14,16 @@ use std::{fmt, io}; use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::net::TcpStream; use tokio::time::sleep; +use tracing::{debug, trace}; #[derive(Copy, Clone)] pub(crate) struct TestedNode { pub(crate) address: SocketAddr, - pub(crate) identity: identity::PublicKey, + pub(crate) identity: ed25519::PublicKey, } impl TestedNode { - pub(crate) fn new(address: SocketAddr, identity: identity::PublicKey) -> Self { + pub(crate) fn new(address: SocketAddr, identity: ed25519::PublicKey) -> Self { TestedNode { address, identity } } } @@ -38,8 +38,8 @@ impl fmt::Display for TestedNode { } } -pub(crate) struct PacketSender { - identity: Arc, +pub struct PacketSender { + identity: Arc, // timeout for receiving before sending new one packets_per_node: usize, packet_timeout: Duration, @@ -49,8 +49,8 @@ pub(crate) struct PacketSender { } impl PacketSender { - pub(super) fn new( - identity: Arc, + pub fn new( + identity: Arc, packets_per_node: usize, packet_timeout: Duration, connection_timeout: Duration, @@ -82,7 +82,7 @@ impl PacketSender { pub(super) async fn send_packets_to_node( self: Arc, tested_node: TestedNode, - ) -> Result { + ) -> Result { let mut shutdown_listener = self.shutdown_listener.fork(tested_node.address.to_string()); shutdown_listener.disarm(); @@ -93,16 +93,18 @@ impl PacketSender { .await { Err(_timeout) => { - return Err(RttError::UnreachableNode( - tested_node.identity.to_base58_string(), - io::ErrorKind::TimedOut.into(), - )) + return Err(VerlocError::UnreachableNode { + identity: tested_node.identity.to_string(), + err: io::ErrorKind::TimedOut.into(), + address: tested_node.address, + }) } Ok(Err(err)) => { - return Err(RttError::UnreachableNode( - tested_node.identity.to_base58_string(), + return Err(VerlocError::UnreachableNode { + identity: tested_node.identity.to_string(), err, - )) + address: tested_node.address, + }) } Ok(Ok(conn)) => conn, }; @@ -121,33 +123,34 @@ impl PacketSender { write = tokio::time::timeout(self.packet_timeout, conn.write_all(packet_bytes.as_ref())) => { match write { Err(_timeout) => { - let identity_string = tested_node.identity.to_base58_string(); + let identity = tested_node.identity; debug!( - "failed to write echo packet to {} within {:?}. Stopping the test.", - identity_string, self.packet_timeout + "failed to write echo packet to {identity} within {:?}. Stopping the test.", + self.packet_timeout ); - return Err(RttError::UnexpectedConnectionFailureWrite( - identity_string, - io::ErrorKind::TimedOut.into(), - )); + return Err(VerlocError::UnexpectedConnectionFailureWrite{ + identity: identity.to_string(), + err:io::ErrorKind::TimedOut.into(), + address: tested_node.address + }); } Ok(Err(err)) => { - let identity_string = tested_node.identity.to_base58_string(); + let identity = tested_node.identity; debug!( - "failed to write echo packet to {} - {}. Stopping the test.", - identity_string, err + "failed to write echo packet to {identity}: {err}. Stopping the test.", ); - return Err(RttError::UnexpectedConnectionFailureWrite( - identity_string, + return Err(VerlocError::UnexpectedConnectionFailureWrite{ + identity: identity.to_string(), err, - )); + address: tested_node.address + }); } Ok(Ok(_)) => {} } }, _ = shutdown_listener.recv() => { - log::trace!("PacketSender: Received shutdown while sending"); - return Err(RttError::ShutdownReceived); + trace!("PacketSender: Received shutdown while sending"); + return Err(VerlocError::ShutdownReceived); }, } @@ -156,15 +159,15 @@ impl PacketSender { let reply_packet_future = async { let mut buf = [0u8; ReplyPacket::SIZE]; if let Err(err) = conn.read_exact(&mut buf).await { + let identity = tested_node.identity; debug!( - "failed to read reply packet from {} - {}. Stopping the test.", - tested_node.identity.to_base58_string(), - err + "failed to read reply packet from {identity}: {err}. Stopping the test.", ); - return Err(RttError::UnexpectedConnectionFailureRead( - tested_node.identity.to_base58_string(), + return Err(VerlocError::UnexpectedConnectionFailureRead { + identity: identity.to_string(), err, - )); + address: tested_node.address, + }); } ReplyPacket::try_from_bytes(&buf, &tested_node.identity) }; @@ -180,15 +183,16 @@ impl PacketSender { "failed to receive reply to our echo packet within {:?}. Stopping the test", self.packet_timeout ); - return Err(RttError::ConnectionReadTimeout( - tested_node.identity.to_base58_string(), - )); + return Err(VerlocError::ConnectionReadTimeout{ + identity: tested_node.identity.to_string(), + address: tested_node.address + }); } } }, _ = shutdown_listener.recv() => { - log::trace!("PacketSender: Received shutdown while waiting for reply"); - return Err(RttError::ShutdownReceived); + trace!("PacketSender: Received shutdown while waiting for reply"); + return Err(VerlocError::ShutdownReceived); } }; @@ -198,7 +202,7 @@ impl PacketSender { // we have received the previous one if reply_packet.base_sequence_number() != seq { debug!("Received reply packet with invalid sequence number! Got {} expected {}. Stopping the test", reply_packet.base_sequence_number(), seq); - return Err(RttError::UnexpectedReplySequence); + return Err(VerlocError::UnexpectedReplySequence); } let time_taken = tokio::time::Instant::now().duration_since(start); diff --git a/common/verloc/src/models.rs b/common/verloc/src/models.rs new file mode 100644 index 00000000000..0bedab812c3 --- /dev/null +++ b/common/verloc/src/models.rs @@ -0,0 +1,225 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use nym_crypto::asymmetric::ed25519::{self}; +use std::cmp::Ordering; +use std::fmt; +use std::fmt::{Display, Formatter}; +use std::time::Duration; +use time::OffsetDateTime; + +#[derive(Debug, Clone)] +pub struct VerlocResultData { + pub nodes_tested: usize, + + pub run_started: OffsetDateTime, + + pub run_finished: Option, + + pub results: Vec, +} + +impl Default for VerlocResultData { + fn default() -> Self { + VerlocResultData { + nodes_tested: 0, + run_started: OffsetDateTime::now_utc(), + run_finished: None, + results: vec![], + } + } +} + +impl VerlocResultData { + pub fn run_finished(&self) -> bool { + self.run_finished.is_some() + } +} + +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub struct VerlocNodeResult { + pub node_identity: ed25519::PublicKey, + + pub latest_measurement: Option, +} + +impl VerlocNodeResult { + pub fn new( + node_identity: ed25519::PublicKey, + latest_measurement: Option, + ) -> Self { + VerlocNodeResult { + node_identity, + latest_measurement, + } + } +} + +impl PartialOrd for VerlocNodeResult { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for VerlocNodeResult { + fn cmp(&self, other: &Self) -> Ordering { + // if both have measurement, compare measurements + // then if only one have measurement, prefer that one + // completely ignore identity as it makes no sense to order by it + if let Some(self_measurement) = &self.latest_measurement { + if let Some(other_measurement) = &other.latest_measurement { + self_measurement.cmp(other_measurement) + } else { + Ordering::Less + } + } else if other.latest_measurement.is_some() { + Ordering::Greater + } else { + Ordering::Equal + } + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct VerlocMeasurement { + /// Minimum RTT duration it took to receive an echo packet. + pub minimum: Duration, + + /// Average RTT duration it took to receive the echo packets. + pub mean: Duration, + + /// Maximum RTT duration it took to receive an echo packet. + pub maximum: Duration, + + /// The standard deviation of the RTT duration it took to receive the echo packets. + pub standard_deviation: Duration, +} + +impl VerlocMeasurement { + pub fn new(raw_results: &[Duration]) -> Self { + let minimum = raw_results.iter().min().copied().unwrap_or_default(); + let maximum = raw_results.iter().max().copied().unwrap_or_default(); + + let mean = Self::duration_mean(raw_results); + let standard_deviation = Self::duration_standard_deviation(raw_results, mean); + + VerlocMeasurement { + minimum, + mean, + maximum, + standard_deviation, + } + } + + fn duration_mean(data: &[Duration]) -> Duration { + if data.is_empty() { + return Default::default(); + } + + let sum = data.iter().sum::(); + let count = data.len() as u32; + + sum / count + } + + fn duration_standard_deviation(data: &[Duration], mean: Duration) -> Duration { + if data.is_empty() { + return Default::default(); + } + + let variance_micros = data + .iter() + .map(|&value| { + // make sure we don't underflow + let diff = if mean > value { + mean - value + } else { + value - mean + }; + // we don't need nanos precision + let diff_micros = diff.as_micros(); + diff_micros * diff_micros + }) + .sum::() + / data.len() as u128; + + // we shouldn't really overflow as our differences shouldn't be larger than couple seconds at the worst possible case scenario + let std_deviation_micros = (variance_micros as f64).sqrt() as u64; + Duration::from_micros(std_deviation_micros) + } +} + +impl Display for VerlocMeasurement { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!( + f, + "rtt min/avg/max/mdev = {} / {} / {} / {}", + humantime::format_duration(self.minimum), + humantime::format_duration(self.mean), + humantime::format_duration(self.maximum), + humantime::format_duration(self.standard_deviation) + ) + } +} + +impl PartialOrd for VerlocMeasurement { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for VerlocMeasurement { + fn cmp(&self, other: &Self) -> Ordering { + // minimum value is most important, then look at standard deviation, then mean and finally maximum + let min_cmp = self.minimum.cmp(&other.minimum); + if min_cmp != Ordering::Equal { + return min_cmp; + } + let std_dev_cmp = self.standard_deviation.cmp(&other.standard_deviation); + if std_dev_cmp != Ordering::Equal { + return std_dev_cmp; + } + let std_dev_cmp = self.mean.cmp(&other.mean); + if std_dev_cmp != Ordering::Equal { + return std_dev_cmp; + } + self.maximum.cmp(&other.maximum) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn sorting_vec_of_verlocs() { + let some_identity = + ed25519::PublicKey::from_base58_string("Be9wH7xuXBRJAuV1pC7MALZv6a61RvWQ3SypsNarqTt") + .unwrap(); + let no_measurement = VerlocNodeResult::new(some_identity, None); + let low_min = VerlocNodeResult::new( + some_identity, + Some(VerlocMeasurement { + minimum: Duration::from_millis(42), + mean: Duration::from_millis(43), + maximum: Duration::from_millis(44), + standard_deviation: Duration::from_millis(45), + }), + ); + let higher_min = VerlocNodeResult::new( + some_identity, + Some(VerlocMeasurement { + minimum: Duration::from_millis(420), + mean: Duration::from_millis(430), + maximum: Duration::from_millis(440), + standard_deviation: Duration::from_millis(450), + }), + ); + + let mut vec_verloc = vec![no_measurement, low_min, no_measurement, higher_min]; + vec_verloc.sort(); + + let expected_sorted = vec![low_min, higher_min, no_measurement, no_measurement]; + assert_eq!(expected_sorted, vec_verloc); + } +} diff --git a/common/wasm/client-core/src/helpers.rs b/common/wasm/client-core/src/helpers.rs index c403e9a8dea..eee064e2cc2 100644 --- a/common/wasm/client-core/src/helpers.rs +++ b/common/wasm/client-core/src/helpers.rs @@ -68,9 +68,9 @@ pub async fn current_network_topology_async( let api_client = NymApiClient::new(url); let mixnodes = api_client - .get_all_basic_active_mixing_assigned_nodes(None) + .get_all_basic_active_mixing_assigned_nodes() .await?; - let gateways = api_client.get_all_basic_entry_assigned_nodes(None).await?; + let gateways = api_client.get_all_basic_entry_assigned_nodes().await?; Ok(NymTopology::from_basic(&mixnodes, &gateways).into()) } diff --git a/common/wireguard/Cargo.toml b/common/wireguard/Cargo.toml index f20651acbd6..df333cab828 100644 --- a/common/wireguard/Cargo.toml +++ b/common/wireguard/Cargo.toml @@ -26,6 +26,8 @@ log.workspace = true thiserror = { workspace = true } tokio = { workspace = true, features = ["rt-multi-thread", "net", "io-util"] } tokio-stream = { workspace = true } +time = { workspace = true } +tracing = { workspace = true } nym-authenticator-requests = { path = "../authenticator-requests" } nym-credential-verification = { path = "../credential-verification" } diff --git a/common/wireguard/src/error.rs b/common/wireguard/src/error.rs index 65a7269952c..d2fd0e79562 100644 --- a/common/wireguard/src/error.rs +++ b/common/wireguard/src/error.rs @@ -16,7 +16,7 @@ pub enum Error { MissingClientBandwidthEntry, #[error("{0}")] - GatewayStorage(#[from] nym_gateway_storage::error::StorageError), + GatewayStorage(#[from] nym_gateway_storage::error::GatewayStorageError), #[error("{0}")] SystemTime(#[from] std::time::SystemTimeError), diff --git a/common/wireguard/src/lib.rs b/common/wireguard/src/lib.rs index a6ae2841700..7b5f1901938 100644 --- a/common/wireguard/src/lib.rs +++ b/common/wireguard/src/lib.rs @@ -7,19 +7,22 @@ // #![warn(clippy::unwrap_used)] use defguard_wireguard_rs::WGApi; -#[cfg(target_os = "linux")] -use defguard_wireguard_rs::{host::Peer, key::Key, net::IpAddrMask}; use nym_crypto::asymmetric::encryption::KeyPair; -#[cfg(target_os = "linux")] -use nym_network_defaults::constants::WG_TUN_BASE_NAME; use nym_wireguard_types::Config; use peer_controller::PeerControlRequest; use std::sync::Arc; use tokio::sync::mpsc::{self, Receiver, Sender}; +#[cfg(target_os = "linux")] +use defguard_wireguard_rs::{host::Peer, key::Key, net::IpAddrMask}; + +#[cfg(target_os = "linux")] +use nym_network_defaults::constants::WG_TUN_BASE_NAME; + pub(crate) mod error; pub mod peer_controller; pub mod peer_handle; +pub mod peer_storage_manager; pub struct WgApiWrapper { inner: WGApi, @@ -80,8 +83,8 @@ pub struct WireguardData { /// Start wireguard device #[cfg(target_os = "linux")] -pub async fn start_wireguard( - storage: St, +pub async fn start_wireguard( + storage: nym_gateway_storage::GatewayStorage, all_peers: Vec, task_client: nym_task::TaskClient, wireguard_data: WireguardData, @@ -92,6 +95,7 @@ pub async fn start_wireguard use peer_controller::PeerController; use std::collections::HashMap; use tokio::sync::RwLock; + use tracing::info; let ifname = String::from(WG_TUN_BASE_NAME); let wg_api = defguard_wireguard_rs::WGApi::new(ifname.clone(), false)?; @@ -118,8 +122,9 @@ pub async fn start_wireguard storage .insert_wireguard_peer(peer, bandwidth_manager.is_some()) .await?; - peer_bandwidth_managers.insert(peer.public_key.clone(), bandwidth_manager); + peer_bandwidth_managers.insert(peer.public_key.clone(), (bandwidth_manager, peer.clone())); } + wg_api.create_interface()?; let interface_config = InterfaceConfiguration { name: ifname.clone(), @@ -129,6 +134,11 @@ pub async fn start_wireguard peers, mtu: None, }; + info!( + "attempting to configure wireguard interface '{ifname}': address={}, port={}", + interface_config.address, interface_config.port + ); + wg_api.configure_interface(&interface_config)?; std::process::Command::new("ip") .args([ diff --git a/common/wireguard/src/peer_controller.rs b/common/wireguard/src/peer_controller.rs index 8c7d94784ae..5f2cf6399fe 100644 --- a/common/wireguard/src/peer_controller.rs +++ b/common/wireguard/src/peer_controller.rs @@ -7,6 +7,7 @@ use defguard_wireguard_rs::{ WireguardInterfaceApi, }; use futures::channel::oneshot; +use log::info; use nym_authenticator_requests::latest::registration::{ RemainingBandwidthData, BANDWIDTH_CAP_PER_DAY, }; @@ -14,15 +15,15 @@ use nym_credential_verification::{ bandwidth_storage_manager::BandwidthStorageManager, BandwidthFlushingBehaviourConfig, ClientBandwidth, }; -use nym_gateway_storage::Storage; +use nym_gateway_storage::GatewayStorage; use nym_wireguard_types::DEFAULT_PEER_TIMEOUT_CHECK; use std::{collections::HashMap, sync::Arc}; use tokio::sync::{mpsc, RwLock}; use tokio_stream::{wrappers::IntervalStream, StreamExt}; -use crate::peer_handle::PeerHandle; use crate::WgApiWrapper; use crate::{error::Error, peer_handle::SharedBandwidthStorageManager}; +use crate::{peer_handle::PeerHandle, peer_storage_manager::PeerStorageManager}; pub enum PeerControlRequest { AddPeer { @@ -62,24 +63,24 @@ pub struct QueryBandwidthControlResponse { pub bandwidth_data: Option, } -pub struct PeerController { - storage: St, +pub struct PeerController { + storage: GatewayStorage, // used to receive commands from individual handles too request_tx: mpsc::Sender, request_rx: mpsc::Receiver, wg_api: Arc, host_information: Arc>, - bw_storage_managers: HashMap>>, + bw_storage_managers: HashMap>, timeout_check_interval: IntervalStream, task_client: nym_task::TaskClient, } -impl PeerController { +impl PeerController { pub fn new( - storage: St, + storage: GatewayStorage, wg_api: Arc, initial_host_information: Host, - bw_storage_managers: HashMap>>, + bw_storage_managers: HashMap, Peer)>, request_tx: mpsc::Sender, request_rx: mpsc::Receiver, task_client: nym_task::TaskClient, @@ -88,11 +89,16 @@ impl PeerController { tokio::time::interval(DEFAULT_PEER_TIMEOUT_CHECK), ); let host_information = Arc::new(RwLock::new(initial_host_information)); - for (public_key, bandwidth_storage_manager) in bw_storage_managers.iter() { - let mut handle = PeerHandle::new( + for (public_key, (bandwidth_storage_manager, peer)) in bw_storage_managers.iter() { + let peer_storage_manager = PeerStorageManager::new( storage.clone(), + peer.clone(), + bandwidth_storage_manager.is_some(), + ); + let mut handle = PeerHandle::new( public_key.clone(), host_information.clone(), + peer_storage_manager, bandwidth_storage_manager.clone(), request_tx.clone(), &task_client, @@ -103,6 +109,10 @@ impl PeerController { } }); } + let bw_storage_managers = bw_storage_managers + .into_iter() + .map(|(k, (m, _))| (k, m)) + .collect(); PeerController { storage, @@ -149,9 +159,9 @@ impl PeerController { } pub async fn generate_bandwidth_manager( - storage: St, + storage: GatewayStorage, public_key: &Key, - ) -> Result>, Error> { + ) -> Result, Error> { if let Some(client_id) = storage .get_wireguard_peer(&public_key.to_string()) .await? @@ -184,10 +194,15 @@ impl PeerController { Self::generate_bandwidth_manager(self.storage.clone(), &peer.public_key) .await? .map(|bw_m| Arc::new(RwLock::new(bw_m))); - let mut handle = PeerHandle::new( + let peer_storage_manager = PeerStorageManager::new( self.storage.clone(), + peer.clone(), + bandwidth_storage_manager.is_some(), + ); + let mut handle = PeerHandle::new( peer.public_key.clone(), self.host_information.clone(), + peer_storage_manager, bandwidth_storage_manager.clone(), self.request_tx.clone(), &self.task_client, @@ -243,6 +258,7 @@ impl PeerController { } pub async fn run(&mut self) { + info!("started wireguard peer controller"); loop { tokio::select! { _ = self.timeout_check_interval.next() => { diff --git a/common/wireguard/src/peer_handle.rs b/common/wireguard/src/peer_handle.rs index 71fa06f8474..c7c8709bbc2 100644 --- a/common/wireguard/src/peer_handle.rs +++ b/common/wireguard/src/peer_handle.rs @@ -3,13 +3,13 @@ use crate::error::Error; use crate::peer_controller::PeerControlRequest; +use crate::peer_storage_manager::PeerStorageManager; use defguard_wireguard_rs::host::Peer; use defguard_wireguard_rs::{host::Host, key::Key}; use futures::channel::oneshot; use nym_authenticator_requests::latest::registration::BANDWIDTH_CAP_PER_DAY; use nym_credential_verification::bandwidth_storage_manager::BandwidthStorageManager; use nym_gateway_storage::models::WireguardPeer; -use nym_gateway_storage::Storage; use nym_task::TaskClient; use nym_wireguard_types::DEFAULT_PEER_TIMEOUT_CHECK; use std::sync::Arc; @@ -17,26 +17,26 @@ use std::time::{Duration, SystemTime}; use tokio::sync::{mpsc, RwLock}; use tokio_stream::{wrappers::IntervalStream, StreamExt}; -pub(crate) type SharedBandwidthStorageManager = Arc>>; +pub(crate) type SharedBandwidthStorageManager = Arc>; const AUTO_REMOVE_AFTER: Duration = Duration::from_secs(60 * 60 * 24 * 30); // 30 days -pub struct PeerHandle { - storage: St, +pub struct PeerHandle { public_key: Key, host_information: Arc>, - bandwidth_storage_manager: Option>, + peer_storage_manager: PeerStorageManager, + bandwidth_storage_manager: Option, request_tx: mpsc::Sender, timeout_check_interval: IntervalStream, task_client: TaskClient, startup_timestamp: SystemTime, } -impl PeerHandle { +impl PeerHandle { pub fn new( - storage: St, public_key: Key, host_information: Arc>, - bandwidth_storage_manager: Option>, + peer_storage_manager: PeerStorageManager, + bandwidth_storage_manager: Option, request_tx: mpsc::Sender, task_client: &TaskClient, ) -> Self { @@ -46,9 +46,9 @@ impl PeerHandle { let mut task_client = task_client.fork(format!("peer-{public_key}")); task_client.disarm(); PeerHandle { - storage, public_key, host_information, + peer_storage_manager, bandwidth_storage_manager, request_tx, timeout_check_interval, @@ -84,16 +84,19 @@ impl PeerHandle { .ok_or(Error::InconsistentConsumedBytes)? .try_into() .map_err(|_| Error::InconsistentConsumedBytes)?; - if spent_bandwidth > 0 - && bandwidth_manager + if spent_bandwidth > 0 { + self.peer_storage_manager.update_trx(kernel_peer); + if bandwidth_manager .write() .await .try_use_bandwidth(spent_bandwidth) .await .is_err() - { - let success = self.remove_peer().await?; - return Ok(!success); + { + let success = self.remove_peer().await?; + self.peer_storage_manager.remove_peer(); + return Ok(!success); + } } } else { if SystemTime::now().duration_since(self.startup_timestamp)? >= AUTO_REMOVE_AFTER { @@ -132,7 +135,7 @@ impl PeerHandle { // the host information hasn't beed updated yet continue; }; - let Some(storage_peer) = self.storage.get_wireguard_peer(&self.public_key.to_string()).await? else { + let Some(storage_peer) = self.peer_storage_manager.get_peer() else { log::debug!("Peer {:?} not in storage anymore, shutting down handle", self.public_key); return Ok(()); }; @@ -141,12 +144,18 @@ impl PeerHandle { return Ok(()); } else { // Update storage values - self.storage.insert_wireguard_peer(&kernel_peer, self.bandwidth_storage_manager.is_some()).await?; + self.peer_storage_manager.sync_storage_peer().await?; } } _ = self.task_client.recv() => { log::trace!("PeerHandle: Received shutdown"); + if let Some(bandwidth_manager) = &self.bandwidth_storage_manager { + if let Err(e) = bandwidth_manager.write().await.sync_storage_bandwidth().await { + log::error!("Storage sync failed - {e}, unaccounted bandwidth might have been consumed"); + } + } + log::trace!("PeerHandle: Finished shutdown"); } } } diff --git a/common/wireguard/src/peer_storage_manager.rs b/common/wireguard/src/peer_storage_manager.rs new file mode 100644 index 00000000000..c2567ce83a9 --- /dev/null +++ b/common/wireguard/src/peer_storage_manager.rs @@ -0,0 +1,138 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::error::Error; +use defguard_wireguard_rs::host::Peer; +use nym_gateway_storage::models::WireguardPeer; +use nym_gateway_storage::GatewayStorage; +use std::time::Duration; +use time::OffsetDateTime; + +const DEFAULT_PEER_MAX_FLUSHING_RATE: Duration = Duration::from_secs(60 * 60 * 24); // 24h +const DEFAULT_PEER_MAX_DELTA_FLUSHING_AMOUNT: u64 = 512 * 1024 * 1024; // 512MB + +#[derive(Debug, Clone, Copy)] +pub struct PeerFlushingBehaviourConfig { + /// Defines maximum delay between peer information being flushed to the persistent storage. + pub peer_max_flushing_rate: Duration, + + /// Defines a maximum change in peer before it gets flushed to the persistent storage. + pub peer_max_delta_flushing_amount: u64, +} + +impl Default for PeerFlushingBehaviourConfig { + fn default() -> Self { + Self { + peer_max_flushing_rate: DEFAULT_PEER_MAX_FLUSHING_RATE, + peer_max_delta_flushing_amount: DEFAULT_PEER_MAX_DELTA_FLUSHING_AMOUNT, + } + } +} + +pub struct PeerStorageManager { + pub(crate) storage: GatewayStorage, + pub(crate) peer_information: Option, + pub(crate) cfg: PeerFlushingBehaviourConfig, + pub(crate) with_client_id: bool, +} + +impl PeerStorageManager { + pub(crate) fn new(storage: GatewayStorage, peer: Peer, with_client_id: bool) -> Self { + let peer_information = Some(PeerInformation::new(peer)); + Self { + storage, + peer_information, + cfg: PeerFlushingBehaviourConfig::default(), + with_client_id, + } + } + + pub(crate) fn get_peer(&self) -> Option { + self.peer_information + .as_ref() + .map(|p| p.peer.clone().into()) + } + + pub(crate) fn remove_peer(&mut self) { + self.peer_information = None; + } + + pub(crate) fn update_trx(&mut self, kernel_peer: &Peer) { + if let Some(peer_information) = self.peer_information.as_mut() { + peer_information.update_trx_bytes(kernel_peer.tx_bytes, kernel_peer.rx_bytes); + } + } + + pub(crate) async fn sync_storage_peer(&mut self) -> Result<(), Error> { + let Some(peer_information) = self.peer_information.as_mut() else { + return Ok(()); + }; + if !peer_information.should_sync(self.cfg) { + return Ok(()); + } + if self + .storage + .get_wireguard_peer(&peer_information.peer().public_key.to_string()) + .await? + .is_none() + { + self.peer_information = None; + return Ok(()); + } + self.storage + .insert_wireguard_peer(peer_information.peer(), self.with_client_id) + .await?; + + peer_information.resync_peer_with_storage(); + + Ok(()) + } +} + +#[derive(Clone, Debug)] +pub(crate) struct PeerInformation { + pub(crate) peer: Peer, + pub(crate) last_synced: OffsetDateTime, + + pub(crate) bytes_delta_since_sync: u64, +} + +impl PeerInformation { + pub fn new(peer: Peer) -> PeerInformation { + PeerInformation { + peer, + last_synced: OffsetDateTime::now_utc(), + bytes_delta_since_sync: 0, + } + } + + pub(crate) fn should_sync(&self, cfg: PeerFlushingBehaviourConfig) -> bool { + if self.bytes_delta_since_sync >= cfg.peer_max_delta_flushing_amount { + return true; + } + + if self.last_synced + cfg.peer_max_flushing_rate < OffsetDateTime::now_utc() + && self.bytes_delta_since_sync != 0 + { + return true; + } + + false + } + + pub(crate) fn peer(&self) -> &Peer { + &self.peer + } + + pub(crate) fn update_trx_bytes(&mut self, tx_bytes: u64, rx_bytes: u64) { + self.bytes_delta_since_sync += tx_bytes.saturating_sub(self.peer.tx_bytes) + + rx_bytes.saturating_sub(self.peer.rx_bytes); + self.peer.tx_bytes = tx_bytes; + self.peer.rx_bytes = rx_bytes; + } + + pub(crate) fn resync_peer_with_storage(&mut self) { + self.bytes_delta_since_sync = 0; + self.last_synced = OffsetDateTime::now_utc(); + } +} diff --git a/contracts/mixnet/schema/nym-mixnet-contract.json b/contracts/mixnet/schema/nym-mixnet-contract.json index b164efb55f8..3549011058f 100644 --- a/contracts/mixnet/schema/nym-mixnet-contract.json +++ b/contracts/mixnet/schema/nym-mixnet-contract.json @@ -60,8 +60,8 @@ }, "version_score_params": { "default": { - "penalty": "0.8", - "penalty_scaling": "2" + "penalty": "0.995", + "penalty_scaling": "1.65" }, "allOf": [ { @@ -261,7 +261,7 @@ "type": "string" }, "VersionScoreFormulaParams": { - "description": "Given the formula of version_score = penalty ^ (num_versions_behind ^ penalty_scaling) define the relevant parameters", + "description": "Given the formula of version_score = penalty ^ (versions_behind_factor ^ penalty_scaling) define the relevant parameters", "type": "object", "required": [ "penalty", @@ -1308,12 +1308,6 @@ "ConfigScoreParamsUpdate": { "type": "object", "properties": { - "current_nym_node_semver": { - "type": [ - "string", - "null" - ] - }, "version_score_formula_params": { "anyOf": [ { @@ -1938,7 +1932,7 @@ "type": "string" }, "VersionScoreFormulaParams": { - "description": "Given the formula of version_score = penalty ^ (num_versions_behind ^ penalty_scaling) define the relevant parameters", + "description": "Given the formula of version_score = penalty ^ (versions_behind_factor ^ penalty_scaling) define the relevant parameters", "type": "object", "required": [ "penalty", @@ -2043,6 +2037,54 @@ }, "additionalProperties": false }, + { + "description": "Get the current expected version of a Nym Node.", + "type": "object", + "required": [ + "get_current_nym_node_version" + ], + "properties": { + "get_current_nym_node_version": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Get the version history of Nym Node.", + "type": "object", + "required": [ + "get_nym_node_version_history" + ], + "properties": { + "get_nym_node_version_history": { + "type": "object", + "properties": { + "limit": { + "description": "Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "description": "Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, { "description": "Gets the current parameters used for reward calculation.", "type": "object", @@ -3443,8 +3485,8 @@ }, "version_score_params": { "default": { - "penalty": "0.8", - "penalty_scaling": "2" + "penalty": "0.995", + "penalty_scaling": "1.65" }, "allOf": [ { @@ -3512,7 +3554,7 @@ "additionalProperties": false }, "VersionScoreFormulaParams": { - "description": "Given the formula of version_score = penalty ^ (num_versions_behind ^ penalty_scaling) define the relevant parameters", + "description": "Given the formula of version_score = penalty ^ (versions_behind_factor ^ penalty_scaling) define the relevant parameters", "type": "object", "required": [ "penalty", @@ -4180,6 +4222,111 @@ } } }, + "get_current_nym_node_version": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "CurrentNymNodeVersionResponse", + "type": "object", + "properties": { + "version": { + "anyOf": [ + { + "$ref": "#/definitions/HistoricalNymNodeVersionEntry" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "definitions": { + "HistoricalNymNodeVersion": { + "type": "object", + "required": [ + "difference_since_genesis", + "introduced_at_height", + "semver" + ], + "properties": { + "difference_since_genesis": { + "description": "The absolute version difference as compared against the first version introduced into the contract.", + "allOf": [ + { + "$ref": "#/definitions/TotalVersionDifference" + } + ] + }, + "introduced_at_height": { + "description": "Block height of when this version has been added to the contract", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "semver": { + "description": "Version of the nym node that is going to be used for determining the version score of a node. note: value stored here is pre-validated `semver::Version`", + "type": "string" + } + }, + "additionalProperties": false + }, + "HistoricalNymNodeVersionEntry": { + "type": "object", + "required": [ + "id", + "version_information" + ], + "properties": { + "id": { + "description": "The unique, ordered, id of this particular entry", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "version_information": { + "description": "Data associated with this particular version", + "allOf": [ + { + "$ref": "#/definitions/HistoricalNymNodeVersion" + } + ] + } + }, + "additionalProperties": false + }, + "TotalVersionDifference": { + "type": "object", + "required": [ + "major", + "minor", + "patch", + "prerelease" + ], + "properties": { + "major": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "minor": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "patch": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "prerelease": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + } + }, "get_delegation_details": { "$schema": "http://json-schema.org/draft-07/schema#", "title": "NodeDelegationResponse", @@ -7119,6 +7266,119 @@ } } }, + "get_nym_node_version_history": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "NymNodeVersionHistoryResponse", + "type": "object", + "required": [ + "history" + ], + "properties": { + "history": { + "type": "array", + "items": { + "$ref": "#/definitions/HistoricalNymNodeVersionEntry" + } + }, + "start_next_after": { + "description": "Field indicating paging information for the following queries if the caller wishes to get further entries.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false, + "definitions": { + "HistoricalNymNodeVersion": { + "type": "object", + "required": [ + "difference_since_genesis", + "introduced_at_height", + "semver" + ], + "properties": { + "difference_since_genesis": { + "description": "The absolute version difference as compared against the first version introduced into the contract.", + "allOf": [ + { + "$ref": "#/definitions/TotalVersionDifference" + } + ] + }, + "introduced_at_height": { + "description": "Block height of when this version has been added to the contract", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "semver": { + "description": "Version of the nym node that is going to be used for determining the version score of a node. note: value stored here is pre-validated `semver::Version`", + "type": "string" + } + }, + "additionalProperties": false + }, + "HistoricalNymNodeVersionEntry": { + "type": "object", + "required": [ + "id", + "version_information" + ], + "properties": { + "id": { + "description": "The unique, ordered, id of this particular entry", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "version_information": { + "description": "Data associated with this particular version", + "allOf": [ + { + "$ref": "#/definitions/HistoricalNymNodeVersion" + } + ] + } + }, + "additionalProperties": false + }, + "TotalVersionDifference": { + "type": "object", + "required": [ + "major", + "minor", + "patch", + "prerelease" + ], + "properties": { + "major": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "minor": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "patch": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "prerelease": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + } + }, "get_nym_nodes_detailed_paged": { "$schema": "http://json-schema.org/draft-07/schema#", "title": "PagedNymNodeDetailsResponse", @@ -10554,15 +10814,10 @@ "ConfigScoreParams": { "type": "object", "required": [ - "current_nym_node_semver", "version_score_formula_params", "version_weights" ], "properties": { - "current_nym_node_semver": { - "description": "Current version of the nym node that is going to be used for determining the version score of a node. note: value stored here is pre-validated `semver::Version`", - "type": "string" - }, "version_score_formula_params": { "description": "Defines the parameters of the formula for calculating the version score", "allOf": [ @@ -10752,7 +11007,7 @@ "type": "string" }, "VersionScoreFormulaParams": { - "description": "Given the formula of version_score = penalty ^ (num_versions_behind ^ penalty_scaling) define the relevant parameters", + "description": "Given the formula of version_score = penalty ^ (versions_behind_factor ^ penalty_scaling) define the relevant parameters", "type": "object", "required": [ "penalty", @@ -10826,15 +11081,10 @@ "ConfigScoreParams": { "type": "object", "required": [ - "current_nym_node_semver", "version_score_formula_params", "version_weights" ], "properties": { - "current_nym_node_semver": { - "description": "Current version of the nym node that is going to be used for determining the version score of a node. note: value stored here is pre-validated `semver::Version`", - "type": "string" - }, "version_score_formula_params": { "description": "Defines the parameters of the formula for calculating the version score", "allOf": [ @@ -10988,7 +11238,7 @@ "type": "string" }, "VersionScoreFormulaParams": { - "description": "Given the formula of version_score = penalty ^ (num_versions_behind ^ penalty_scaling) define the relevant parameters", + "description": "Given the formula of version_score = penalty ^ (versions_behind_factor ^ penalty_scaling) define the relevant parameters", "type": "object", "required": [ "penalty", diff --git a/contracts/mixnet/schema/raw/execute.json b/contracts/mixnet/schema/raw/execute.json index 2c042ee7054..367c6483b97 100644 --- a/contracts/mixnet/schema/raw/execute.json +++ b/contracts/mixnet/schema/raw/execute.json @@ -1027,12 +1027,6 @@ "ConfigScoreParamsUpdate": { "type": "object", "properties": { - "current_nym_node_semver": { - "type": [ - "string", - "null" - ] - }, "version_score_formula_params": { "anyOf": [ { @@ -1657,7 +1651,7 @@ "type": "string" }, "VersionScoreFormulaParams": { - "description": "Given the formula of version_score = penalty ^ (num_versions_behind ^ penalty_scaling) define the relevant parameters", + "description": "Given the formula of version_score = penalty ^ (versions_behind_factor ^ penalty_scaling) define the relevant parameters", "type": "object", "required": [ "penalty", diff --git a/contracts/mixnet/schema/raw/instantiate.json b/contracts/mixnet/schema/raw/instantiate.json index c856dce3129..7ab76571c79 100644 --- a/contracts/mixnet/schema/raw/instantiate.json +++ b/contracts/mixnet/schema/raw/instantiate.json @@ -56,8 +56,8 @@ }, "version_score_params": { "default": { - "penalty": "0.8", - "penalty_scaling": "2" + "penalty": "0.995", + "penalty_scaling": "1.65" }, "allOf": [ { @@ -257,7 +257,7 @@ "type": "string" }, "VersionScoreFormulaParams": { - "description": "Given the formula of version_score = penalty ^ (num_versions_behind ^ penalty_scaling) define the relevant parameters", + "description": "Given the formula of version_score = penalty ^ (versions_behind_factor ^ penalty_scaling) define the relevant parameters", "type": "object", "required": [ "penalty", diff --git a/contracts/mixnet/schema/raw/migrate.json b/contracts/mixnet/schema/raw/migrate.json index 36e279f35e5..3844bce3087 100644 --- a/contracts/mixnet/schema/raw/migrate.json +++ b/contracts/mixnet/schema/raw/migrate.json @@ -17,8 +17,8 @@ }, "version_score_params": { "default": { - "penalty": "0.8", - "penalty_scaling": "2" + "penalty": "0.995", + "penalty_scaling": "1.65" }, "allOf": [ { @@ -86,7 +86,7 @@ "additionalProperties": false }, "VersionScoreFormulaParams": { - "description": "Given the formula of version_score = penalty ^ (num_versions_behind ^ penalty_scaling) define the relevant parameters", + "description": "Given the formula of version_score = penalty ^ (versions_behind_factor ^ penalty_scaling) define the relevant parameters", "type": "object", "required": [ "penalty", diff --git a/contracts/mixnet/schema/raw/query.json b/contracts/mixnet/schema/raw/query.json index 90d0477a303..fdf631ebce2 100644 --- a/contracts/mixnet/schema/raw/query.json +++ b/contracts/mixnet/schema/raw/query.json @@ -85,6 +85,54 @@ }, "additionalProperties": false }, + { + "description": "Get the current expected version of a Nym Node.", + "type": "object", + "required": [ + "get_current_nym_node_version" + ], + "properties": { + "get_current_nym_node_version": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Get the version history of Nym Node.", + "type": "object", + "required": [ + "get_nym_node_version_history" + ], + "properties": { + "get_nym_node_version_history": { + "type": "object", + "properties": { + "limit": { + "description": "Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "description": "Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, { "description": "Gets the current parameters used for reward calculation.", "type": "object", diff --git a/contracts/mixnet/schema/raw/response_to_get_current_nym_node_version.json b/contracts/mixnet/schema/raw/response_to_get_current_nym_node_version.json new file mode 100644 index 00000000000..c075132d9e4 --- /dev/null +++ b/contracts/mixnet/schema/raw/response_to_get_current_nym_node_version.json @@ -0,0 +1,105 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "CurrentNymNodeVersionResponse", + "type": "object", + "properties": { + "version": { + "anyOf": [ + { + "$ref": "#/definitions/HistoricalNymNodeVersionEntry" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "definitions": { + "HistoricalNymNodeVersion": { + "type": "object", + "required": [ + "difference_since_genesis", + "introduced_at_height", + "semver" + ], + "properties": { + "difference_since_genesis": { + "description": "The absolute version difference as compared against the first version introduced into the contract.", + "allOf": [ + { + "$ref": "#/definitions/TotalVersionDifference" + } + ] + }, + "introduced_at_height": { + "description": "Block height of when this version has been added to the contract", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "semver": { + "description": "Version of the nym node that is going to be used for determining the version score of a node. note: value stored here is pre-validated `semver::Version`", + "type": "string" + } + }, + "additionalProperties": false + }, + "HistoricalNymNodeVersionEntry": { + "type": "object", + "required": [ + "id", + "version_information" + ], + "properties": { + "id": { + "description": "The unique, ordered, id of this particular entry", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "version_information": { + "description": "Data associated with this particular version", + "allOf": [ + { + "$ref": "#/definitions/HistoricalNymNodeVersion" + } + ] + } + }, + "additionalProperties": false + }, + "TotalVersionDifference": { + "type": "object", + "required": [ + "major", + "minor", + "patch", + "prerelease" + ], + "properties": { + "major": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "minor": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "patch": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "prerelease": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + } +} diff --git a/contracts/mixnet/schema/raw/response_to_get_nym_node_version_history.json b/contracts/mixnet/schema/raw/response_to_get_nym_node_version_history.json new file mode 100644 index 00000000000..d85073662bc --- /dev/null +++ b/contracts/mixnet/schema/raw/response_to_get_nym_node_version_history.json @@ -0,0 +1,113 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "NymNodeVersionHistoryResponse", + "type": "object", + "required": [ + "history" + ], + "properties": { + "history": { + "type": "array", + "items": { + "$ref": "#/definitions/HistoricalNymNodeVersionEntry" + } + }, + "start_next_after": { + "description": "Field indicating paging information for the following queries if the caller wishes to get further entries.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false, + "definitions": { + "HistoricalNymNodeVersion": { + "type": "object", + "required": [ + "difference_since_genesis", + "introduced_at_height", + "semver" + ], + "properties": { + "difference_since_genesis": { + "description": "The absolute version difference as compared against the first version introduced into the contract.", + "allOf": [ + { + "$ref": "#/definitions/TotalVersionDifference" + } + ] + }, + "introduced_at_height": { + "description": "Block height of when this version has been added to the contract", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "semver": { + "description": "Version of the nym node that is going to be used for determining the version score of a node. note: value stored here is pre-validated `semver::Version`", + "type": "string" + } + }, + "additionalProperties": false + }, + "HistoricalNymNodeVersionEntry": { + "type": "object", + "required": [ + "id", + "version_information" + ], + "properties": { + "id": { + "description": "The unique, ordered, id of this particular entry", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "version_information": { + "description": "Data associated with this particular version", + "allOf": [ + { + "$ref": "#/definitions/HistoricalNymNodeVersion" + } + ] + } + }, + "additionalProperties": false + }, + "TotalVersionDifference": { + "type": "object", + "required": [ + "major", + "minor", + "patch", + "prerelease" + ], + "properties": { + "major": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "minor": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "patch": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "prerelease": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + } +} diff --git a/contracts/mixnet/schema/raw/response_to_get_state.json b/contracts/mixnet/schema/raw/response_to_get_state.json index 1161f1b6ae3..8d688360f71 100644 --- a/contracts/mixnet/schema/raw/response_to_get_state.json +++ b/contracts/mixnet/schema/raw/response_to_get_state.json @@ -76,15 +76,10 @@ "ConfigScoreParams": { "type": "object", "required": [ - "current_nym_node_semver", "version_score_formula_params", "version_weights" ], "properties": { - "current_nym_node_semver": { - "description": "Current version of the nym node that is going to be used for determining the version score of a node. note: value stored here is pre-validated `semver::Version`", - "type": "string" - }, "version_score_formula_params": { "description": "Defines the parameters of the formula for calculating the version score", "allOf": [ @@ -274,7 +269,7 @@ "type": "string" }, "VersionScoreFormulaParams": { - "description": "Given the formula of version_score = penalty ^ (num_versions_behind ^ penalty_scaling) define the relevant parameters", + "description": "Given the formula of version_score = penalty ^ (versions_behind_factor ^ penalty_scaling) define the relevant parameters", "type": "object", "required": [ "penalty", diff --git a/contracts/mixnet/schema/raw/response_to_get_state_params.json b/contracts/mixnet/schema/raw/response_to_get_state_params.json index 4a5bf9d2dcd..47da7893a61 100644 --- a/contracts/mixnet/schema/raw/response_to_get_state_params.json +++ b/contracts/mixnet/schema/raw/response_to_get_state_params.json @@ -54,15 +54,10 @@ "ConfigScoreParams": { "type": "object", "required": [ - "current_nym_node_semver", "version_score_formula_params", "version_weights" ], "properties": { - "current_nym_node_semver": { - "description": "Current version of the nym node that is going to be used for determining the version score of a node. note: value stored here is pre-validated `semver::Version`", - "type": "string" - }, "version_score_formula_params": { "description": "Defines the parameters of the formula for calculating the version score", "allOf": [ @@ -216,7 +211,7 @@ "type": "string" }, "VersionScoreFormulaParams": { - "description": "Given the formula of version_score = penalty ^ (num_versions_behind ^ penalty_scaling) define the relevant parameters", + "description": "Given the formula of version_score = penalty ^ (versions_behind_factor ^ penalty_scaling) define the relevant parameters", "type": "object", "required": [ "penalty", diff --git a/contracts/mixnet/src/constants.rs b/contracts/mixnet/src/constants.rs index 3d2d1651138..50cf7c2541f 100644 --- a/contracts/mixnet/src/constants.rs +++ b/contracts/mixnet/src/constants.rs @@ -71,6 +71,9 @@ pub const LAST_INTERVAL_EVENT_ID_KEY: &str = "lie"; pub const ADMIN_STORAGE_KEY: &str = "admin"; pub const CONTRACT_STATE_KEY: &str = "state"; +pub const VERSION_HISTORY_ID_COUNTER_KEY: &str = "vhid"; +pub const VERSION_HISTORY_NAMESPACE: &str = "vh"; + pub const NYMNODE_ROLES_ASSIGNMENT_NAMESPACE: &str = "roles"; pub const NYMNODE_REWARDED_SET_METADATA_NAMESPACE: &str = "roles_metadata"; pub const NYMNODE_ACTIVE_ROLE_ASSIGNMENT_KEY: &str = "active_roles"; diff --git a/contracts/mixnet/src/contract.rs b/contracts/mixnet/src/contract.rs index 86a701978b0..3526e586368 100644 --- a/contracts/mixnet/src/contract.rs +++ b/contracts/mixnet/src/contract.rs @@ -48,7 +48,6 @@ fn default_initial_state( }, config_score_params: ConfigScoreParams { - current_nym_node_semver: msg.current_nym_node_version.clone(), version_weights: msg.version_score_weights, version_score_formula_params: msg.version_score_params, }, @@ -101,7 +100,13 @@ pub fn instantiate( starting_interval, rewarding_validator_address, )?; - mixnet_params_storage::initialise_storage(deps.branch(), state, info.sender)?; + mixnet_params_storage::initialise_storage( + deps.branch(), + &env, + state, + info.sender, + msg.current_nym_node_version, + )?; RewardingStorage::new().initialise(deps.storage, reward_params)?; nymnodes_storage::initialise_storage(deps.storage)?; cw2::set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; @@ -138,6 +143,7 @@ pub fn execute( ExecuteMsg::UpdateCurrentNymNodeSemver { current_version } => { crate::mixnet_contract_settings::transactions::try_update_current_nym_node_semver( deps, + env, info, current_version, ) @@ -334,6 +340,16 @@ pub fn query( QueryMsg::GetState {} => { to_binary(&crate::mixnet_contract_settings::queries::query_contract_state(deps)?) } + QueryMsg::GetCurrentNymNodeVersion {} => to_binary( + &crate::mixnet_contract_settings::queries::query_current_nym_node_version(deps)?, + ), + QueryMsg::GetNymNodeVersionHistory { limit, start_after } => to_binary( + &crate::mixnet_contract_settings::queries::query_nym_node_version_history_paged( + deps, + start_after, + limit, + )?, + ), QueryMsg::Admin {} => to_binary(&crate::mixnet_contract_settings::queries::query_admin( deps, )?), @@ -587,7 +603,7 @@ pub fn query( #[entry_point] pub fn migrate( mut deps: DepsMut<'_>, - _env: Env, + env: Env, msg: MigrateMsg, ) -> Result { set_build_information!(deps.storage)?; @@ -596,7 +612,7 @@ pub fn migrate( let skip_state_updates = msg.unsafe_skip_state_updates.unwrap_or(false); if !skip_state_updates { - crate::queued_migrations::add_config_score_params(deps.branch(), &msg)?; + crate::queued_migrations::add_config_score_params(deps.branch(), env, &msg)?; } // due to circular dependency on contract addresses (i.e. mixnet contract requiring vesting contract address @@ -693,7 +709,6 @@ mod tests { }, }, config_score_params: ConfigScoreParams { - current_nym_node_semver: "1.1.10".to_string(), version_weights: Default::default(), version_score_formula_params: Default::default(), }, diff --git a/contracts/mixnet/src/mixnet_contract_settings/queries.rs b/contracts/mixnet/src/mixnet_contract_settings/queries.rs index 4ece720fcd2..5963dbf0a2b 100644 --- a/contracts/mixnet/src/mixnet_contract_settings/queries.rs +++ b/contracts/mixnet/src/mixnet_contract_settings/queries.rs @@ -3,9 +3,14 @@ use super::storage; use crate::mixnet_contract_settings::storage::ADMIN; -use cosmwasm_std::{Deps, StdResult}; +use cosmwasm_std::{Deps, Order, StdResult}; use cw_controllers::AdminResponse; -use mixnet_contract_common::{ContractBuildInformation, ContractState, ContractStateParams}; +use cw_storage_plus::Bound; +use mixnet_contract_common::error::MixnetContractError; +use mixnet_contract_common::{ + ContractBuildInformation, ContractState, ContractStateParams, CurrentNymNodeVersionResponse, + HistoricalNymNodeVersionEntry, NymNodeVersionHistoryResponse, +}; use nym_contracts_common::get_build_information; pub(crate) fn query_admin(deps: Deps<'_>) -> StdResult { @@ -32,6 +37,36 @@ pub(crate) fn query_contract_version() -> ContractBuildInformation { get_build_information!() } +pub(crate) fn query_nym_node_version_history_paged( + deps: Deps<'_>, + start_after: Option, + limit: Option, +) -> StdResult { + let limit = limit.unwrap_or(100).min(200) as usize; + let start = start_after.map(Bound::exclusive); + + let history = storage::NymNodeVersionHistory::new() + .version_history + .range(deps.storage, start, None, Order::Ascending) + .take(limit) + .map(|r| r.map(Into::::into)) + .collect::>>()?; + + let start_next_after = history.last().map(|entry| entry.id); + + Ok(NymNodeVersionHistoryResponse { + history, + start_next_after, + }) +} + +pub(crate) fn query_current_nym_node_version( + deps: Deps<'_>, +) -> Result { + let version = storage::NymNodeVersionHistory::new().current_version(deps.storage)?; + Ok(CurrentNymNodeVersionResponse { version }) +} + #[cfg(test)] pub(crate) mod tests { use super::*; @@ -59,7 +94,6 @@ pub(crate) mod tests { interval_operating_cost: Default::default(), }, config_score_params: ConfigScoreParams { - current_nym_node_semver: "1.1.10".to_string(), version_weights: Default::default(), version_score_formula_params: Default::default(), }, diff --git a/contracts/mixnet/src/mixnet_contract_settings/storage.rs b/contracts/mixnet/src/mixnet_contract_settings/storage.rs index 09bab3490cc..6201972e51a 100644 --- a/contracts/mixnet/src/mixnet_contract_settings/storage.rs +++ b/contracts/mixnet/src/mixnet_contract_settings/storage.rs @@ -1,19 +1,108 @@ // Copyright 2021-2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::constants::{ADMIN_STORAGE_KEY, CONTRACT_STATE_KEY}; -use cosmwasm_std::{Addr, DepsMut, Storage}; -use cosmwasm_std::{Coin, StdResult}; +use crate::constants::{ + ADMIN_STORAGE_KEY, CONTRACT_STATE_KEY, VERSION_HISTORY_ID_COUNTER_KEY, + VERSION_HISTORY_NAMESPACE, +}; +use cosmwasm_std::Coin; +use cosmwasm_std::{Addr, DepsMut, Env, Storage}; use cw_controllers::Admin; -use cw_storage_plus::Item; +use cw_storage_plus::{Item, Map}; use mixnet_contract_common::error::MixnetContractError; use mixnet_contract_common::{ - ContractState, ContractStateParams, OperatingCostRange, ProfitMarginRange, + ContractState, ContractStateParams, HistoricalNymNodeVersion, HistoricalNymNodeVersionEntry, + OperatingCostRange, ProfitMarginRange, }; +use std::str::FromStr; pub(crate) const CONTRACT_STATE: Item<'_, ContractState> = Item::new(CONTRACT_STATE_KEY); pub(crate) const ADMIN: Admin = Admin::new(ADMIN_STORAGE_KEY); +pub(crate) struct NymNodeVersionHistory<'a> { + pub(crate) id_counter: Item<'a, u32>, + pub(crate) version_history: Map<'a, u32, HistoricalNymNodeVersion>, +} + +impl NymNodeVersionHistory<'_> { + #[allow(clippy::new_without_default)] + pub const fn new() -> Self { + Self { + id_counter: Item::new(VERSION_HISTORY_ID_COUNTER_KEY), + version_history: Map::new(VERSION_HISTORY_NAMESPACE), + } + } + + fn next_id(&self, storage: &mut dyn Storage) -> Result { + let next = self.id_counter.may_load(storage)?.unwrap_or_default(); + self.id_counter.save(storage, &next)?; + Ok(next) + } + + pub fn current_version( + &self, + storage: &dyn Storage, + ) -> Result, MixnetContractError> { + let Some(current_id) = self.id_counter.may_load(storage)? else { + return Ok(None); + }; + let version_information = self.version_history.load(storage, current_id)?; + Ok(Some(HistoricalNymNodeVersionEntry { + id: current_id, + version_information, + })) + } + + pub fn insert_new( + &self, + storage: &mut dyn Storage, + entry: HistoricalNymNodeVersion, + ) -> Result { + let next_id = self.next_id(storage)?; + self.version_history.save(storage, next_id, &entry)?; + Ok(next_id) + } + + pub fn try_insert_new( + &self, + storage: &mut dyn Storage, + env: &Env, + raw_semver: &str, + ) -> Result { + let Ok(new_semver) = semver::Version::from_str(raw_semver) else { + return Err(MixnetContractError::InvalidNymNodeSemver { + provided: raw_semver.to_string(), + }); + }; + + let Some(current) = self.current_version(storage)? else { + // treat this as genesis + let genesis = + HistoricalNymNodeVersion::genesis(raw_semver.to_string(), env.block.height); + return self.insert_new(storage, genesis); + }; + + let current_semver = current.version_information.semver_unchecked(); + if new_semver <= current_semver { + // make sure the new semver is strictly more recent than the current head + return Err(MixnetContractError::NonIncreasingSemver { + provided: raw_semver.to_string(), + current: current.version_information.semver, + }); + } + + let diff = current + .version_information + .cumulative_difference_since_genesis(&new_semver); + let entry = HistoricalNymNodeVersion { + semver: raw_semver.to_string(), + introduced_at_height: env.block.height, + difference_since_genesis: diff, + }; + self.insert_new(storage, entry) + } +} + pub fn rewarding_validator_address(storage: &dyn Storage) -> Result { Ok(CONTRACT_STATE .load(storage) @@ -71,9 +160,13 @@ pub(crate) fn state_params( pub(crate) fn initialise_storage( deps: DepsMut<'_>, + env: &Env, initial_state: ContractState, initial_admin: Addr, -) -> StdResult<()> { + initial_nymnode_version: String, +) -> Result<(), MixnetContractError> { CONTRACT_STATE.save(deps.storage, &initial_state)?; - ADMIN.set(deps, Some(initial_admin)) + NymNodeVersionHistory::new().try_insert_new(deps.storage, env, &initial_nymnode_version)?; + ADMIN.set(deps, Some(initial_admin))?; + Ok(()) } diff --git a/contracts/mixnet/src/mixnet_contract_settings/transactions.rs b/contracts/mixnet/src/mixnet_contract_settings/transactions.rs index 78ddab97b9c..d3c3a211f1a 100644 --- a/contracts/mixnet/src/mixnet_contract_settings/transactions.rs +++ b/contracts/mixnet/src/mixnet_contract_settings/transactions.rs @@ -3,16 +3,15 @@ use super::storage; use crate::mixnet_contract_settings::storage::ADMIN; -use cosmwasm_std::MessageInfo; use cosmwasm_std::Response; use cosmwasm_std::{DepsMut, StdResult}; +use cosmwasm_std::{Env, MessageInfo}; use mixnet_contract_common::error::MixnetContractError; use mixnet_contract_common::events::{ new_rewarding_validator_address_update_event, new_settings_update_event, new_update_nym_node_semver_event, }; use mixnet_contract_common::ContractStateParamsUpdate; -use std::str::FromStr; pub fn try_update_contract_admin( mut deps: DepsMut<'_>, @@ -92,15 +91,6 @@ pub(crate) fn try_update_contract_settings( // check for config score params updates if let Some(config_score_update) = update.config_score_params { - // if semver is to be updated - validate the provided value - if let Some(current_nym_node_semver) = config_score_update.current_nym_node_semver { - if semver::Version::from_str(¤t_nym_node_semver).is_err() { - return Err(MixnetContractError::InvalidNymNodeSemver { - provided: current_nym_node_semver, - }); - } - state.params.config_score_params.current_nym_node_semver = current_nym_node_semver - } if let Some(version_weights) = config_score_update.version_weights { state.params.config_score_params.version_weights = version_weights } @@ -119,23 +109,19 @@ pub(crate) fn try_update_contract_settings( pub(crate) fn try_update_current_nym_node_semver( deps: DepsMut<'_>, + env: Env, info: MessageInfo, current_version: String, ) -> Result { - let mut state = storage::CONTRACT_STATE.load(deps.storage)?; ADMIN.assert_admin(deps.as_ref(), &info.sender)?; - let response = Response::new().add_event(new_update_nym_node_semver_event(¤t_version)); - - if semver::Version::from_str(¤t_version).is_err() { - return Err(MixnetContractError::InvalidNymNodeSemver { - provided: current_version, - }); - } + let new_id = storage::NymNodeVersionHistory::new().try_insert_new( + deps.storage, + &env, + ¤t_version, + )?; - state.params.config_score_params.current_nym_node_semver = current_version; - storage::CONTRACT_STATE.save(deps.storage, &state)?; - Ok(response) + Ok(Response::new().add_event(new_update_nym_node_semver_event(¤t_version, new_id))) } #[cfg(test)] diff --git a/contracts/mixnet/src/queued_migrations.rs b/contracts/mixnet/src/queued_migrations.rs index 3381cac47e6..476d484d809 100644 --- a/contracts/mixnet/src/queued_migrations.rs +++ b/contracts/mixnet/src/queued_migrations.rs @@ -4,7 +4,8 @@ mod config_score_params { use crate::constants::CONTRACT_STATE_KEY; use crate::mixnet_contract_settings::storage as mixnet_params_storage; - use cosmwasm_std::{Addr, Coin, DepsMut}; + use crate::mixnet_contract_settings::storage::NymNodeVersionHistory; + use cosmwasm_std::{Addr, Coin, DepsMut, Env}; use cw_storage_plus::Item; use mixnet_contract_common::error::MixnetContractError; use mixnet_contract_common::{ @@ -16,6 +17,7 @@ mod config_score_params { pub(crate) fn add_config_score_params( deps: DepsMut<'_>, + env: Env, msg: &MigrateMsg, ) -> Result<(), MixnetContractError> { if semver::Version::from_str(&msg.current_nym_node_semver).is_err() { @@ -62,7 +64,6 @@ mod config_score_params { interval_operating_cost: old_state.params.interval_operating_cost, }, config_score_params: ConfigScoreParams { - current_nym_node_semver: msg.current_nym_node_semver.to_string(), version_weights: msg.version_score_weights, version_score_formula_params: msg.version_score_params, }, @@ -70,6 +71,14 @@ mod config_score_params { }; mixnet_params_storage::CONTRACT_STATE.save(deps.storage, &new_state)?; + + // initialise the version chain + NymNodeVersionHistory::new().try_insert_new( + deps.storage, + &env, + &msg.current_nym_node_semver, + )?; + Ok(()) } } diff --git a/documentation/docs/pages/developers/_meta.json b/documentation/docs/pages/developers/_meta.json index d0103c08a59..b3e5134275f 100644 --- a/documentation/docs/pages/developers/_meta.json +++ b/documentation/docs/pages/developers/_meta.json @@ -5,6 +5,7 @@ "integrations": "Integration Options", "clients": "Clients", "tools": "Tools", + "nymvpncli": "Nym VPN CLI", "chain": "Interacting with Nyx", "--": { "type": "separator" diff --git a/documentation/docs/pages/developers/nymvpncli.mdx b/documentation/docs/pages/developers/nymvpncli.mdx new file mode 100644 index 00000000000..d8687cc5b94 --- /dev/null +++ b/documentation/docs/pages/developers/nymvpncli.mdx @@ -0,0 +1,278 @@ +import { Callout } from 'nextra/components' + +# Nym VPN CLI + +This is a short guide to setting up and using the `nym-vpnc` tool, which is used in conjunction with the `nym-vpnd` daemon. + + + These binaries have superceded the older `nym-vpn-cli` binary. This still operates for the moment as it is being used in testing scenarios but will go out of date quickly. + + +Download and run instructions for the GUIs can be found [here](https://nymvpn.com/en/download/linux). + +## Download & Extract Binary +Check the [release page](https://github.com/nymtech/nym-vpn-client/releases/) page for the latest release version and modify the instructions accordingly. These instructions use the latest as of the time of writing. +```sh +wget -q https://github.com/nymtech/nym-vpn-client/releases/download/nym-vpn-core-v1.1.0-beta.3/nym-vpn-core-v1.1.0-beta.3_.tar.gz && +tar -xzf nym-vpn-core-v1.1.0-beta.3_.tar.gz && +cd nym-vpn-core-v1.1.0-beta.3_/ && +chmod u+x * +``` + +## Build from Source +### Prerequisites +All operating systems require both [Rust](https://www.rust-lang.org/tools/install) and [Go](https://go.dev/doc/install). + +**Arch specific packages:** +```sh +yay -S gcc make protobuf base-devel clang +``` + +**Ubuntu24 specific packages:** +```sh +apt install gcc make protobuf-compiler pkconfig libdbus-1-dev build-essential clang +``` + + + Older Debian/Ubuntu versions need to manually install `protobuf-compiler` >= v3.21.12 + + +### Clone & `make` +```sh +git clone https://github.com/nymtech/nym-vpn-client.git +cd nym-vpn-client/ +make +``` + +## Mnemonic Generation +Create an account at [nymvpn.com](nymvpn.com) to obtain your mnemonic. + +## Start the daemon +```sh +sudo ./PATH/TO/nym-vpnd +``` + +If you are running for the first time you will see the following: + +```sh +2024-12-11T11:03:58.202159Z INFO nym_vpnd::environment: Setting up environment by discovering the network: mainnet +2024-12-11T11:03:58.202205Z INFO nym_vpn_network_config::discovery: No discovery file found, writing default discovery file +2024-12-11T11:03:59.905505Z INFO nym_vpnd::command_interface::start: Starting command interface +2024-12-11T11:03:59.905660Z INFO nym_vpnd::service::vpn_service: Starting VPN service +2024-12-11T11:03:59.905879Z INFO nym_vpnd::command_interface::start: Starting socket listener on: /var/run/nym-vpn.sock +2024-12-11T11:03:59.906227Z INFO nym_vpn_account_controller::controller: Starting account controller +2024-12-11T11:03:59.906285Z INFO nym_vpn_account_controller::controller: Account controller: data directory: "/var/lib/nym-vpnd/mainnet" +2024-12-11T11:03:59.906313Z INFO nym_vpn_account_controller::controller: Account controller: credential mode: false +2024-12-11T11:03:59.913215Z INFO nym_vpnd::command_interface::listener: Removed previous command interface socket: "/var/run/nym-vpn.sock" +2024-12-11T11:03:59.977206Z INFO nym_vpnd::service::vpn_service: VPN service initialized successfully +2024-12-11T11:03:59.979246Z INFO nym_vpn_account_controller::controller: Account id: (unset) +2024-12-11T11:03:59.979265Z INFO nym_vpn_account_controller::controller: Device id: BZWA5MRnEvRYD8WWrH9KULdj2Q1uTssu6idjgWFae9dv +2024-12-11T11:03:59.979762Z INFO nym_vpn_account_controller::storage: Ticketbooks stored: 0 +2024-12-11T11:03:59.982125Z INFO nym_vpn_account_controller::controller: Received command: UpdateAccountState +2024-12-11T11:03:59.982181Z INFO nym_vpn_account_controller::shared_state: Setting mnemonic state to NotStored +2024-12-11T11:03:59.982200Z WARN nym_vpn_account_controller::commands: Returning error: NoAccountStored +2024-12-11T11:03:59.982218Z INFO nym_vpn_account_controller::controller: Received command: UpdateDeviceState +2024-12-11T11:03:59.982230Z INFO nym_vpn_account_controller::shared_state: Setting mnemonic state to NotStored +2024-12-11T11:03:59.982240Z WARN nym_vpn_account_controller::commands: Returning error: NoAccountStored +``` + +Ignore the `NoAccountStored` errors: these will disappear after the next step. **Leave the daemon running and run the following commands in another terminal window** or create an init file for `nym-vpnd`. + +## Run VPN +We have to first store the account we have created online: +```sh +./PATH/TO/nym-vpn-cli store-account --mnemonic "" +``` + +You will see this registration in the daemon logs: + +```sh +2024-12-11T11:04:31.918455Z INFO grpc_vpnd: ← StoreAccount () +2024-12-11T11:04:31.919296Z INFO nym_vpnd::service::vpn_service: Storing account +2024-12-11T11:04:31.919531Z INFO nym_vpn_store::mnemonic::on_disk: Storing mnemonic to: /var/lib/nym-vpnd/mainnet/mnemonic.json +2024-12-11T11:04:31.920327Z INFO nym_vpn_account_controller::controller: Received command: UpdateAccountState +2024-12-11T11:04:31.950720Z INFO nym_vpn_account_controller::shared_state: Setting mnemonic state to Stored { id: "n1nghj6qnmfww22tq6wyntnf709lr90qjem0uezz" } +2024-12-11T11:04:34.616249Z INFO nym_vpn_account_controller::shared_state: Setting account to Registered +2024-12-11T11:04:34.616363Z INFO nym_vpn_account_controller::shared_state: Setting account summary to AccountSummary { account: Active, subscription: Active, device_summary: DeviceSummary { active: 0, max: 10, remaining: 10 }, fair_usage: FairUsage { used_gb: None, limit_gb: None, resets_on_utc: Some("2025-01-09 15:43:37.223Z") } } +2024-12-11T11:04:34.981875Z INFO nym_vpn_account_controller::controller: Received command: RegisterDevice +2024-12-11T11:04:35.008575Z INFO register_device: nym_vpn_account_controller::shared_state: Setting device registration result to InProgress id=09876a3a +2024-12-11T11:04:35.008611Z INFO register_device: nym_vpn_account_controller::commands::register_device: Registering device: Device { identity_key: BZWA5MRnEvRYD8WWrH9KULdj2Q1uTssu6idjgWFae9dv } id=09876a3a +2024-12-11T11:04:36.765850Z INFO register_device: nym_vpn_account_controller::commands::register_device: Response: NymVpnDevice { + created_on_utc: "2024-12-11 11:04:36.432Z", + last_updated_utc: "2024-12-11 11:04:36.432Z", + device_identity_key: "BZWA5MRnEvRYD8WWrH9KULdj2Q1uTssu6idjgWFae9dv", + status: Active, +} id=09876a3a +2024-12-11T11:04:36.765998Z INFO register_device: nym_vpn_account_controller::commands::register_device: Device registered: BZWA5MRnEvRYD8WWrH9KULdj2Q1uTssu6idjgWFae9dv id=09876a3a +``` + +You can then connect `nym-vpnc` (in this case, with 2 hop wireguard mode enabled): +```sh +./PATH/TO/nym-vpn-cli nym-vpnc connect --enable-two-hop +``` + +Which shows as such in the daemon logs: +```sh +2024-12-11T11:05:25.727784Z INFO grpc_vpnd: ← VpnConnect () +2024-12-11T11:05:25.728107Z INFO grpc_vpnd: nym_vpnd::command_interface::listener: Got connect request: Request { metadata: MetadataMap { headers: {"te": "trailers", "content-type": "application/grpc", "user-agent": "tonic/0.11.0"} }, message: ConnectRequest { entry: None, exit: None, dns: None, disable_routing: false, enable_two_hop: true, netstack: false, disable_poisson_rate: false, disable_background_cover_traffic: false, enable_credentials_mode: false, user_agent: Some(UserAgent { application: "nym-vpnc", version: "1.1.0-beta.3 (1.1.0-beta.3)", platform: "Manjaro Linux; Linux 24.2.0 Manjaro Linux; x86_64", git_commit: "59c0714f1dac1a2d8bf77f3d2705a5c9bb57a5be (59c0714f1dac1a2d8bf77f3d2705a5c9bb57a5be)" }), min_mixnode_performance: None, min_gateway_mixnet_performance: None, min_gateway_vpn_performance: None }, extensions: Extensions } +2024-12-11T11:05:25.728225Z INFO grpc_vpnd: nym_vpnd::command_interface::connection_handler: Starting VPN +2024-12-11T11:05:25.728430Z INFO nym_vpnd::service::vpn_service: Using entry point: None +2024-12-11T11:05:25.728450Z INFO nym_vpnd::service::vpn_service: Using exit point: None +2024-12-11T11:05:25.728468Z INFO nym_vpnd::service::vpn_service: Using options: ConnectOptions { dns: None, disable_routing: false, enable_two_hop: true, netstack: false, disable_poisson_rate: false, disable_background_cover_traffic: true, enable_credentials_mode: false, min_mixnode_performance: None, min_gateway_mixnet_performance: None, min_gateway_vpn_performance: None } +2024-12-11T11:05:25.729112Z INFO nym_vpnd::service::config: Config file updated at "/etc/nym/mainnet/nym-vpnd.toml" +2024-12-11T11:05:25.729161Z INFO nym_vpnd::service::vpn_service: Using config: entry point: Random, exit point: Random +2024-12-11T11:05:25.729611Z INFO nym_vpnd::service::vpn_service: Tunnel event: Connecting +2024-12-11T11:05:25.730108Z INFO nym_gateway_directory::gateway_client: Fetching gateways from nym-vpn-api... +2024-12-11T11:05:26.387699Z INFO nym_vpn_lib::tunnel_state_machine::tunnel::gateway_selector: Found 113 entry gateways +2024-12-11T11:05:26.387744Z INFO nym_vpn_lib::tunnel_state_machine::tunnel::gateway_selector: Found 113 exit gateways +2024-12-11T11:05:26.387752Z INFO nym_gateway_directory::entries::exit_point: Selecting a random exit gateway +2024-12-11T11:05:26.387794Z INFO nym_vpn_lib::tunnel_state_machine::tunnel::gateway_selector: Using entry gateway: CcYinhLeFU8n6xs78FG6Rz3wvosGTCU2hLB1CZyfkMVe, location: IN, performance: 96% +2024-12-11T11:05:26.387814Z INFO nym_vpn_lib::tunnel_state_machine::tunnel::gateway_se +lector: Using exit gateway: Atcji22Wnfwi6nEkGC5BmgbqNPLYdhx5r4NxTqXAzFeq, location: GB, performance: 99% +2024-12-11T11:05:26.387852Z INFO nym_vpn_lib::tunnel_state_machine::tunnel::gateway_selector: Using exit router address 3MJSnmUeH54a7DJ8C4C8oZPkCjtENSfwcMLJ39zUk9Ys.59h9HKGTM4MPXVJRDfaJYFg1aoAdeBGjLHMFxQ6fBsfF@Atcji22Wnfwi6nEkGC5BmgbqNPLYdhx5r4NxTqXAzFeq +2024-12-11T11:05:26.388144Z INFO nym_vpn_lib::mixnet::connect: mixnet client poisson rate limiting: disabled +2024-12-11T11:05:26.388154Z INFO nym_vpn_lib::mixnet::connect: mixnet client background loop cover traffic stream: disabled +2024-12-11T11:05:26.388158Z INFO nym_vpn_lib::mixnet::connect: mixnet client minimum mixnode performance: 50 +2024-12-11T11:05:26.388163Z INFO nym_vpn_lib::mixnet::connect: mixnet client minimum gateway performance: 50 +2024-12-11T11:05:26.388434Z INFO nym_client_core::client::base_client::non_wasm_helpers: loading existing surb database +2024-12-11T11:05:26.391135Z INFO nym_client_core_surb_storage::backend::fs_backend::manager: Database migration finished! +2024-12-11T11:05:26.503966Z INFO nym_client_core::init::helpers: nym-api reports 195 valid gateways +2024-12-11T11:05:27.888231Z INFO nym_client_core::client::base_client: Starting nym client +2024-12-11T11:05:27.894924Z INFO nym_client_core::client::base_client: Starting statistics control... +2024-12-11T11:05:27.895105Z INFO nym_client_core::client::base_client: Obtaining initial network topology +2024-12-11T11:05:28.047235Z INFO nym_client_core::client::base_client: Starting topology refresher... +2024-12-11T11:05:29.024114Z INFO perform_initial_authentication: nym_gateway_client::bandwidth: remaining bandwidth: 0.00 B gateway=CcYinhLeFU8n6xs78FG6Rz3wvosGTCU2hLB1CZyfkMVe gateway_address=wss://gateway4.lunardao.net:9001/ +2024-12-11T11:05:29.024375Z INFO nym_gateway_client::client: Claiming more bandwidth with existing credentials. Stop the process now if you don't want that to happen. +2024-12-11T11:05:29.024413Z WARN nym_gateway_client::client: Not enough bandwidth. Trying to get more bandwidth, this might take a while +2024-12-11T11:05:29.024430Z INFO nym_gateway_client::client: The client is running in disabled credentials mode - attempting to claim bandwidth without a credential +2024-12-11T11:05:29.171514Z INFO nym_gateway_client::client: managed to claim testnet bandwidth +2024-12-11T11:05:29.175555Z INFO nym_client_core::client::base_client: Starting received messages buffer controller... +2024-12-11T11:05:29.175648Z INFO nym_client_core::client::base_client: Starting mix traffic controller... +2024-12-11T11:05:29.175698Z INFO nym_client_core::client::base_client: Starting real traffic stream... +2024-12-11T11:05:29.176182Z INFO nym_task::manager: Starting status message listener +2024-12-11T11:05:29.189388Z INFO nym_vpn_lib::bandwidth_controller: Registering with wireguard gateway +2024-12-11T11:05:29.189449Z INFO nym_gateway_directory::gateway_client: Fetching gateway ip from nym-vpn-api... +2024-12-11T11:05:32.373749Z INFO nym_vpn_lib::bandwidth_controller: Registering with wireguard gateway +2024-12-11T11:05:32.373860Z INFO nym_gateway_directory::gateway_client: Fetching gateway ip from nym-vpn-api... +2024-12-11T11:05:36.675679Z INFO nym_vpn_lib::tunnel_state_machine::tunnel_monitor: Created entry tun device: tun0 +2024-12-11T11:05:36.679774Z INFO nym_vpn_lib::tunnel_state_machine::tunnel_monitor: Created exit tun device: tun1 +2024-12-11T11:05:36.681435Z INFO nym_dns: Setting DNS servers: Tunnel DNS: {1.1.1.1, 1.0.0.1, 2606:4700:4700::1111, 2606:4700:4700::1001} Non-tunnel DNS: {} +2024-12-11T11:05:36.690464Z INFO nym_vpnd::service::vpn_service: Tunnel event: Connecting WireGuard tunnel with entry 213.210.21.111:51822 and exit 45.140.167.83:51822 +2024-12-11T11:05:36.690577Z INFO nym_vpnd::service::vpn_service: Tunnel event: Connected WireGuard tunnel with entry 213.210.21.111:51822 and exit 45.140.167.83:51822 +2024-12-11T11:05:37.734615Z INFO nym_wg_gateway_client: Remaining wireguard bandwidth with gateway CcYinhLeFU8n6xs78FG6Rz3wvosGTCU2hLB1CZyfkMVe for today: 256000.00 MB +2024-12-11T11:05:39.240143Z INFO nym_wg_gateway_client: Remaining wireguard bandwidth with gateway Atcji22Wnfwi6nEkGC5BmgbqNPLYdhx5r4NxTqXAzFeq for today: 256000.00 MB +``` + +You should see the `Remaining wireguard bandwidth` decrease as you use your allowance. + +There are a lot of configuration options available to you regarding how to connect: + +```sh +❯ ./nym-vpn-core/target/debug/nym-vpnc connect --help +Connect to the Nym network + +Usage: nym-vpnc connect [OPTIONS] + +Options: + --entry-gateway-id + Mixnet public ID of the entry gateway + --entry-gateway-country + Auto-select entry gateway by country ISO + --entry-gateway-low-latency + Auto-select entry gateway by latency + --entry-gateway-random + Auto-select entry gateway randomly + --exit-router-address + Mixnet recipient address + --exit-gateway-id + Mixnet public ID of the exit gateway + --exit-gateway-country + Auto-select exit gateway by country ISO + --exit-gateway-random + Auto-select exit gateway randomly + --dns + Set the IP address of the DNS server to use + --disable-routing + Disable routing all traffic through the nym TUN device. When the flag is set, + the nym TUN device will be created, but to route traffic through it you will + need to do it manually, e.g. ping -Itun0 + --enable-two-hop + Enable two-hop wireguard traffic. This means that traffic jumps directly from + entry gateway to exit gateway using Wireguard protocol + -w, --wait-until-connected + Blocks until the connection is established or failed + --netstack + Use netstack based implementation for two-hop wireguard + --enable-credentials-mode + Enable credentials mode + --min-gateway-mixnet-performance + An integer between 0 and 100 representing the minimum gateway performance + required to consider a gateway for routing traffic + --min-gateway-vpn-performance + An integer between 0 and 100 representing the minimum gateway performance + required to consider a gateway for routing traffic + -h, --help + Print help +``` + +## Command Reference +```sh +❯ ./nym-vpn-core/target/debug/nym-vpnc --help +NymVPN commandline client + +Usage: nym-vpnc [OPTIONS] + +Commands: + connect Connect to the Nym network + disconnect Disconnect from the Nym network + status Get the current status of the connection + info Get info about the current client. Things like version and + network details + set-network Set the network to be used. This requires a restart of the + daemon (`nym-vpnd`) + store-account Store the account recovery phrase + is-account-stored Check if the account is stored + forget-account Forget the stored account. This removes the stores recovery + phrase, device and mixnet keys, stored local credentials, etc + get-account-id Get the account ID + get-account-state Get the current account state + get-account-links Get URLs for managing your nym-vpn account + get-device-id Get the device ID + list-entry-gateways List the set of entry gateways for mixnet mode + list-exit-gateways List the set of exit gateways for mixnet mode + list-vpn-gateways List the set of entry and exit gateways for dVPN mode + list-entry-countries List the set of countries with available entry gateways for + mixnet mode + list-exit-countries List the set of countries with available exit gateways for + mixnet mode + list-vpn-countries List the set of countries with available entry and exit + gateways for dVPN mode + help Print this message or the help of the given subcommand(s) + +Options: + --http Use HTTP instead of socket file for IPC with the daemon + --verbose + -h, --help Print help + -V, --version Print version +``` + +```sh +❯ ./nym-vpn-core/target/debug/nym-vpnd --help +NymVPN daemon + +Usage: nym-vpnd [OPTIONS] + +Options: + -c, --config-env-file + Path pointing to an env file describing the network + --enable-http-listener + + --disable-socket-listener + + --run-as-service + + -h, --help + Print help + -V, --version + Print version +``` diff --git a/documentation/docs/pages/operators/changelog.mdx b/documentation/docs/pages/operators/changelog.mdx index b498b5156c6..6c156b5f229 100644 --- a/documentation/docs/pages/operators/changelog.mdx +++ b/documentation/docs/pages/operators/changelog.mdx @@ -25,14 +25,307 @@ export const CiConfig = () => ( ); +export const TunnelManagerCommands = () => ( +
+ Commands to update IP tables rules with a new network_tunnel_manager.sh +
+); + # Changelog -This page displays a full list of all the changes during our release cycle from `v2024.3-eclipse` onwards. Operators can find here the newest updates together with links to relevant documentation. The list is sorted so that the newest changes appear first. +This page displays a full list of all the changes during our release cycle from `v2024.3-eclipse` onward. Operators can find here the newest updates together with links to relevant documentation. The list is sorted so that the newest changes appear first. +## `v2024.14-crunch` + +- [Release binaries](https://github.com/nymtech/nym/releases/tag/nym-binaries-v2024.14-crunch) +- [`nym-node`](nodes/nym-node.mdx) version `1.2.0` +- [Releae operators updates and tools](changelog#operators-updates--tools) +- [Github CHANGELOG.md](https://github.com/nymtech/nym/blob/nym-binaries-v2024.14-crunch/CHANGELOG.md) + +```sh +nym-node +Binary Name: nym-node +Build Timestamp: 2024-12-11T13:49:11.974104790Z +Build Version: 1.2.0 +Commit SHA: a491e6a71a8cf862d77defd740a4ee8d65d8292a +Commit Date: 2024-12-11T10:28:47.000000000+01:00 +Commit Branch: HEAD +rustc Version: 1.83.0 +rustc Channel: stable +cargo Profile: release +``` + +### Features + +- [Bump elliptic from `6.5.4` to `6.5.7` in /testnet-faucet](https://github.com/nymtech/nym/pull/4768): Bumps [elliptic](https://github.com/indutny/elliptic) from `6.5.4` to `6.5.7`. + +- [build(deps): bump micromatch from `4.0.4` to `4.0.8` in /nym-wallet/webdriver](https://github.com/nymtech/nym/pull/4789): Bumps [micromatch](https://github.com/micromatch/micromatch) from `4.0.4` to `4.0.8`. + +- [build(deps): bump axios from 1.6.0 to 1.7.5 in /nym-api/tests](https://github.com/nymtech/nym/pull/4790) Bumps [axios](https://github.com/axios/axios) from 1.6.0 to 1.7.5. + +- [Sync code with `.env` in build.rs](https://github.com/nymtech/nym/pull/4876): Keep `dotenv` file always up to date + +- [build(deps): bump lazy_static from `1.4.0` to `1.5.0`](https://github.com/nymtech/nym/pull/4913): Bumps [lazy_static](https://github.com/rust-lang-nursery/lazy-static.rs) from `1.4.0` to `1.5.0`. + +- [Create TaskStatusEvent trait instead of piggybacking on Error](https://github.com/nymtech/nym/pull/4919) + +- [build(deps): bump once_cell from `1.19.0` to `1.20.2`](https://github.com/nymtech/nym/pull/4952): Bumps [`once_cell`](https://github.com/matklad/once_cell) from `1.19.0` to `1.20.2` + +- [Bump the patch-updates group across 1 directory with 10 updates](https://github.com/nymtech/nym/pull/5011): Bumps the patch-updates group with 9 updates in the / directory: + +| Package | From | To | +| --- | --- | --- | +| [anyhow](https://github.com/dtolnay/anyhow) | `1.0.89` | `1.0.90` | +| [clap](https://github.com/clap-rs/clap) | `4.5.18` | `4.5.20` | +| [clap_complete](https://github.com/clap-rs/clap) | `4.5.29` | `4.5.33` | +| [pin-project](https://github.com/taiki-e/pin-project) | `1.1.5` | `1.1.6` | +| [serde](https://github.com/serde-rs/serde) | `1.0.210` | `1.0.211` | +| [serde_json](https://github.com/serde-rs/json) | `1.0.128` | `1.0.132` | +| [wasm-bindgen](https://github.com/rustwasm/wasm-bindgen) | `0.2.93` | `0.2.95` | +| [wasm-bindgen-futures](https://github.com/rustwasm/wasm-bindgen) | `0.4.43` | `0.4.45` | +| [web-sys](https://github.com/rustwasm/wasm-bindgen) | `0.3.70` | `0.3.72` | +| Updates `anyhow` | `1.0.89` | `1.0.90` | + +- [[Product Data] Introduce data persistence on gateways](https://github.com/nymtech/nym/pull/5022): This PR builds on top of [\#4974](https://github.com/nymtech/nym/pull/4974), not changing the behavior of the data collection, but persisting them in a sqlite database so they can be kept across restarts and crashes. It also leave the door open for other stats module to use that storage if needed. Here are some points of interest: + - New [`gateway_stats_storage`](https://github.com/nymtech/nym/tree/simon/gateway_stats_persistence/common/gateway-stats-storage) crate + - [Config migration](https://github.com/nymtech/nym/blob/simon/gateway_stats_persistence/nym-node/src/config/old_configs/old_config_v4.rs) resulting from the added database. + - Resulting changes in the [`statistics`](https://github.com/nymtech/nym/tree/simon/gateway_stats_persistence/gateway/src/node/statistics) module to account the new storage system + +- [Integrate nym-credential-proxy into workspace](https://github.com/nymtech/nym/pull/5027): Integrate `nym-credential-proxy` into the main workspace + +- [Authenticator CLI client mode](https://github.com/nymtech/nym/pull/5044) + +- [Node Status API](https://github.com/nymtech/nym/pull/5050): merging a long-diverged feature branch - all commits here were their own merge requests + +- [IPv6 support for wireguard](https://github.com/nymtech/nym/pull/5059) + +- [Add nym node GH workflow](https://github.com/nymtech/nym/pull/5080) + +- [[Product Data] Better unique user count on gateways](https://github.com/nymtech/nym/pull/5084): To avoid double counting clients across gateways, we add a user ID to the gateway session data. + +- [chore: ecash contract migration to remove unused 'redemption_gateway_share'](https://github.com/nymtech/nym/pull/5104) + +- [[Product Data] Client-side stats collection ](https://github.com/nymtech/nym/pull/5107): The goal is to anonymously gather stats from nym-clients. These stats will be sent through the mixnet to a Nym run service provider that will gather them. This PR sets the scene to send stats in a mixnet message to an address. The address can be set when the client is created. Current stats include some infos on sent packets along with platform information. If a receiving address is set, the client will send a mixnet packet every 5min to this address. Otherwise, nothing happens and the client runs as usual. + +- [Send mixnet packet stats using task client](https://github.com/nymtech/nym/pull/5109) + +- [Add granular log on nym-node](https://github.com/nymtech/nym/pull/5111) and make use of it for `defguard_wireguard_rs` big info logs + +- [Rewarding for ticketbook issuance](https://github.com/nymtech/nym/pull/5112): Revamps the current validator rewarder to allow for rewards for issuing the zk-nym ticketbooks. + +- [[Product Data] Add stats reporting configuration in client config ](https://github.com/nymtech/nym/pull/5115): Adds the stats reporting address to client configs. It can be set in the config file, as a CLI argument and as an env var in a `.env` file. As the stats reporting config in now in the `DebugConfig`, the `StatsReportingConfig` is no longer required, making the propagation of these changes more readable + +- [config score](https://github.com/nymtech/nym/pull/5117): introduces a concept of a `config_score` to a nym node which influences performance and thus rewarding amounts and chances of being in the rewarded set. Currently it's influenced by the following factors: + - Accepting terms and conditions (not accepted: 0) + - Exposing self-described API (not exposed: 0) + - Running "nym-node" binary (legacy binary: 0) + - Number of versions behind the core (`score = 0.995 ^ (X * versions_behind ^ 1.65)`) + - The old performance is now treated as `routing_score` + - the "new" performance = `routing_score * config_score` + +- [Add Dockerfile and add env vars for clap arguments](https://github.com/nymtech/nym/pull/5118) + +- [Aadd GH workflow for nym-validator-rewarder](https://github.com/nymtech/nym/pull/5119) + +- [[Product data] Data consumption with ecash ticket](https://github.com/nymtech/nym/pull/5120): Send an event each time an ecash ticket get successfully spent. This allows to approximate how much data each client is using. + +- [[Product Data] Config deserialization bug fix](https://github.com/nymtech/nym/pull/5126): Fixes a bug where a `None` value was serialized into an empty string, and incorrectly deserialized into a `Some` variant. + +- [NS Agent auth with NS API](https://github.com/nymtech/nym/pull/5127): NS Agent authenticates with key that was registered with NS API + - Added flag to Agent to generate keypairs + - Agent requests are signed by agent + - Server-side requests are checked for authentication + +- [CI: reduce jobs running on cluster](https://github.com/nymtech/nym/pull/5132) + +- [Removed ci-nym-api-tests.yml which was running outdated (and broken) tests](https://github.com/nymtech/nym/pull/5133) + +- [[Product Data] Set up country reporting from vpn-client](https://github.com/nymtech/nym/pull/5134): Add the ability to report exit country, along with a small refactoring of a module. + +- [chore: remove standalone legacy mixnode/gateway binaries](https://github.com/nymtech/nym/pull/5135) + +- [Update `serde_json_path` due to compilation issue](https://github.com/nymtech/nym/pull/5144) + +- [Add version to clientStatsReport](https://github.com/nymtech/nym/pull/5147): Add a `kind` and `api_version` field for `ClientStatsReport` + +- [Start session collection for exit gateways](https://github.com/nymtech/nym/pull/5148): Apparently, exit gateways are also entry gateways so we need to start session stats for them as well + +- [build(deps): bump mikefarah/yq from `4.44.3` to `4.44.5`](https://github.com/nymtech/nym/pull/5149): Bumps [mikefarah/yq](https://github.com/mikefarah/yq) from `4.44.3` to `4.44.5`. + +- [build(deps): bump cross-spawn from `7.0.3` to `7.0.6` in /testnet-faucet](https://github.com/nymtech/nym/pull/5150): Bumps [cross-spawn](https://github.com/moxystudio/node-cross-spawn) from `7.0.3` to `7.0.6`. + +- [Add export_to_env to NymNetworkDetails](https://github.com/nymtech/nym/pull/5162): In `nym-vpn-core` we've started to read the network environment from a json file and then try to pass around `NymNetworkDetails` directly instead of relying on the exported environment. However we still need to bridge with old code so we need to export the network details instance to the environment. + +- [Add strum::EnumIter for TicketType](https://github.com/nymtech/nym/pull/5164) + +- [Fix env var name](https://github.com/nymtech/nym/pull/5165) + +- [Add support for DELETE to nym-http-api-client](https://github.com/nymtech/nym/pull/5166): Add delete support to `http-api-client` + +- [Add derive_extended_private_key to DirectSecp256k1HdWallet](https://github.com/nymtech/nym/pull/5167): Add `derive_extended_private_key` to `DirectSectp256k1HdWallet` to support seeding ecash keys + +- [Move two minor jobs to free tier github hosted runners](https://github.com/nymtech/nym/pull/5169): In an attempt to easy the load on the self-hosted runners, move two minor workflows over to GH hosted free tier runners. + +- [Remove peers with no allowed ip from storage](https://github.com/nymtech/nym/pull/5175) + +- [Add indexes to monitor run and testing route](https://github.com/nymtech/nym/pull/5181) + +- [Add `monitor_run` and testing_route indexes](https://github.com/nymtech/nym/pull/5182) + +- [`explorer-api`: add nym node endpoints + UI to show nym-nodes and account balances](https://github.com/nymtech/nym/pull/5183): Explorer API: + - Existing endpoints stay identical + - Adds new endpoints to get: + - `nym-nodes` (list + by id) + - account balance + delegations + rewarding + vesting + + - Explorer UI (NextJS) + - List of nym-nodes + - Remove service providers routes (Harbour Master shows these) + - Updates summary page to show nym-nodes + - Adds legacy markers to old gateway and mixnode bond lists + +- [Add `monitor_run` and testing_route indexes](https://github.com/nymtech/nym/pull/5182) + +- [Bugfix/credential proxy sequencing](https://github.com/nymtech/nym/pull/5187) + +- [improvement: make internal gateway clients use the same topology cache](https://github.com/nymtech/nym/pull/5191): This should result in 66% reduction in queries for topology within `nym-node` as all the clients should rely on the same cache + +- [chore: apply 1.84 linter suggestions](https://github.com/nymtech/nym/pull/5192) + +- [Guard storage access with cache](https://github.com/nymtech/nym/pull/5193) + +- [Update Security disclosure email, public key and policy](https://github.com/nymtech/nym/pull/5195) + +- [merge crunch into develop](https://github.com/nymtech/nym/pull/5199) + +- [Fix backwards compat mac generation](https://github.com/nymtech/nym/pull/5202) + +- [adjusted config score penalty calculation](https://github.com/nymtech/nym/pull/5206) + +- [`nym-api` NMv1 adjustments](https://github.com/nymtech/nym/pull/5209) + +- [Nmv2 add debug config](https://github.com/nymtech/nym/pull/5212): Adds debug config to disable poisson process, cover traffic and min performance filtering + +- [introduce UNSTABLE endpoints for returning network monitor run details](https://github.com/nymtech/nym/pull/5214) + +- [Don't consider legacy nodes for rewarded set selection](https://github.com/nymtech/nym/pull/5215) + +- [Derive serialize for UserAgent (#5210)](https://github.com/nymtech/nym/pull/5217): Cherry-pick PR [\#5210](https://github.com/nymtech/nym/pull/5210) + +- [Backport \#5218](https://github.com/nymtech/nym/pull/5220) + +- [Remove any filtering on node semver](https://github.com/nymtech/nym/pull/5224): Removed any filtering on version of nodes. however, the parameters can still be passed to `nym-api` queries to not break existing clients, but they will happily ignore them + +- [Further config score adjustments](https://github.com/nymtech/nym/pull/5225): I still want to add helper endpoints on `nym-api` to expose some of this data. but for now, I'll let this PR bake over the weekend. + +### Bugfix + +- [Correct IPv6 address generation](https://github.com/nymtech/nym/pull/5113) + +- [bugfix: don't send empty BankMsg in ecash contract](https://github.com/nymtech/nym/pull/5121): If ticketbook prices were to be set so low the resultant redemption would have created `BankMsg` with value of 0, that message is no longer going to be sent + +- [fix: validator-rewarder GH job](https://github.com/nymtech/nym/pull/5151) + +- [bugfix: correctly expose ecash-related data on nym-api](https://github.com/nymtech/nym/pull/5155): This PR makes fixes to ecash-related endpoints on `nym-api` + - global data (such as aggregated signatures and keys) are actually always available by all apis + - global data (such as aggregated signatures and keys) are actually always available by all apis + +- [bugfix: use default value for verloc config when deserialising missing values](https://github.com/nymtech/nym/pull/5177) + +- [bugfix: fixed nym-node config migrations (again)](https://github.com/nymtech/nym/pull/5179) + +- [bugfix: added explicit openapi servers to account for route prefixes](https://github.com/nymtech/nym/pull/5237) + +### Operators Updates & Tools + + +**Nym Network will now only allow nodes which [migrated](nodes/nym-node/bonding#migrate-to-nym-node-in-mixnet-smart-contract) their node in Nym mixnet smart contract to Nym Node. All nodes which are still bonded as a legacy one (Mixnode or Gateway) in the wallet will have no chance to take part in the [Rewarded set selection](tokenomics/mixnet-rewards#rewarded-set-selection).** + +**Operators taking part in Delegation program or Service Grant program must migrate their nodes latest by December 16th, 08:00 UTC.** + + +#### Updates + +- [Version count as a part of config score](tokenomics/mixnet-rewards#config-score-calculation) has been introduced. To familiarize yourself with Nym Node operator rewards calculation, read [this page](tokenomics/mixnet-rewards). +- Nym nodes running as Exit Gateway in Service Grant program received delegation. Nym team is now delegating total of **64,800,000 NYM on top 241 Nym Nodes** (137 in Mixnode mode and 104 as Gateways). Our delegation aims to incentivise committed operators who support bootstrapping of Nym network before paying users come. + +
+ + +- 250k NYM - Upgrading to magura in time - 2 nodes +- 300k NYM - Upgrading to magura + bonus for a quick patch upgrade - 102 nodes +- No delegation - not upgrading in time - 2 nodes + + +- `nym-node` has now implemented [IPv6 support for wireguard](https://github.com/nymtech/nym/pull/5059) + +- [`network_tunnel_manager.sh` updated](network): run the commands below to make sure + +
+ +}> +These commands can be run one by one or copy-pasted and run as a block. +```sh +mkdir $HOME/nym-binaries; \ + +curl -L https://raw.githubusercontent.com/nymtech/nym/refs/heads/develop/scripts/network_tunnel_manager.sh -o $HOME/nym-binaries/network_tunnel_manager.sh && \ +chmod +x $HOME/nym-binaries/network_tunnel_manager.sh; \ + +$HOME/nym-binaries/network_tunnel_manager.sh check_nymtun_iptables ; \ +$HOME/nym-binaries/network_tunnel_manager.sh remove_duplicate_rules nymtun0 ;\ +$HOME/nym-binaries/network_tunnel_manager.sh remove_duplicate_rules nymwg;\ +$HOME/nym-binaries/network_tunnel_manager.sh check_nymtun_iptables ; \ +$HOME/nym-binaries/network_tunnel_manager.sh adjust_ip_forwarding ; \ +$HOME/nym-binaries/network_tunnel_manager.sh apply_iptables_rules ; \ +$HOME/nym-binaries/network_tunnel_manager.sh check_nymtun_iptables ; \ +$HOME/nym-binaries/network_tunnel_manager.sh apply_iptables_rules_wg ; \ +$HOME/nym-binaries/network_tunnel_manager.sh configure_dns_and_icmp_wg ; \ +$HOME/nym-binaries/network_tunnel_manager.sh adjust_ip_forwarding ; \ +$HOME/nym-binaries/network_tunnel_manager.sh check_ipv6_ipv4_forwarding; \ + +systemctl daemon-reload && service nym-node restart && journalctl -u nym-node -f +``` + +Then run the jokes in a new window for control +```sh +$HOME/nym-binaries/network_tunnel_manager.sh joke_through_the_mixnet +$HOME/nym-binaries/network_tunnel_manager.sh joke_through_wg_tunnel +``` + + +#### Tools + +- **[New APIs documentation](../apis/introduction)** with interactive APIs generated from the OpenAPI specs of various API endpoints offered by bits of Nym infrastructure run both by Nym and community operators for both Mainnet and the Sandbox testnet. +- [Nym Harbourmaster](https://harbourmaster.nymtech.net/) has a new tab called `CONTRACT EXPLORER` querying data from Nym mixnet contract in real time. +- [Nym Explorer](https://explorer.nymtech.net) is updated to read migrated nodes correctly +- [New community explorer by SpectreDAO](https://explorer.nym.spectredao.net/dashboard) offers Nym Network dashboard, Node overview and Account stats view functions for operators and delegators. +- [`nym-vpnc`](../developers/nymvpncli) build and run documentation, for those who don't want to use the Nym VPN GUIs. + +## `magura-drift` + +Second patch to `v2024.13-magura` release version. + +- [Release binaries](https://github.com/nymtech/nym/releases/tag/nym-binaries-v2024.13-magura-drift) +- [`nym-node`](nodes/nym-node.mdx) version `1.1.12` + +```sh +nym-node +Binary Name: nym-node +Build Timestamp: 2024-11-29T13:10:51.813092288Z +Build Version: 1.1.12 +Commit SHA: 4a9a5579c40ad956163ea02e01d7b53aef2ac8ef +Commit Date: 2024-11-29T14:06:32.000000000+01:00 +Commit Branch: HEAD +rustc Version: 1.83.0 +rustc Channel: stable +cargo Profile: release +``` + +- This patch adds a peer storage manager to fix issues causing external clients to be blocked, ensuring they can successfully connect to different nodes. ## `v2024.13-magura-patched` @@ -54,12 +347,11 @@ cargo Profile: release -After changes coming along with `v2024.13-magura` (`nym-node v1.1.10`), Nym Explorer is no longer picking all values correctly. Insstead of fixing this outdated explorer, we are working on a new one, coming out soon. +After changes coming along with `v2024.13-magura` (`nym-node v1.1.10`), Nym Explorer is no longer picking all values correctly. Instead of fixing this outdated explorer, we are working on a new one, coming out soon. [Nym Harbourmaster](https://harbourmaster.nymtech.net) has cache of 90min, expect your values to be updated with delay. We are aware of some issues with Nym Harbourmaster and working hard to resolve them in the upcoming explorer v2. To check your routing values in real time, you can use [`nym-gateway-probe`](nodes/performance-and-testing/gateway-probe). - ### Operators Updates & Tools - Updated [`network_tunnel_manager.sh`](https://github.com/nymtech/nym/blob/develop/scripts/network_tunnel_manager.sh) (moved to our monorepo) helps operators to configure their IP tables rules for `nymtun` and `wireguard` routing. diff --git a/documentation/docs/pages/operators/nodes/maintenance/manual-upgrade.mdx b/documentation/docs/pages/operators/nodes/maintenance/manual-upgrade.mdx index a68d01b1fde..8b3db6b61ba 100644 --- a/documentation/docs/pages/operators/nodes/maintenance/manual-upgrade.mdx +++ b/documentation/docs/pages/operators/nodes/maintenance/manual-upgrade.mdx @@ -53,7 +53,7 @@ journalctl -f -u nym-node.service -After changes coming along with `v2024.13-magura` (`nym-node v1.1.10`), Nym Explorer is no longer picking all values correctly. Insstead of fixing this outdated explorer, we are working on a new one, coming out soon. +After changes coming along with `v2024.13-magura` (`nym-node v1.1.10`), Nym Explorer is no longer picking all values correctly. Instead of fixing this outdated explorer, we are working on a new one, coming out soon. [Nym Harbourmaster](https://harbourmaster.nymtech.net) has cache of 90min, expect your values to be updated with delay. We are aware of some issues with Nym Harbourmaster and working hard to resolve them in the upcoming explorer v2. To check your routing values in real time, you can use [`nym-gateway-probe`](../performance-and-testing/gateway-probe). diff --git a/documentation/docs/pages/operators/nodes/nym-node/bonding.mdx b/documentation/docs/pages/operators/nodes/nym-node/bonding.mdx index 44a46ac714a..3c64fe9b291 100644 --- a/documentation/docs/pages/operators/nodes/nym-node/bonding.mdx +++ b/documentation/docs/pages/operators/nodes/nym-node/bonding.mdx @@ -120,7 +120,7 @@ From `nym-wallet` version `1.2.15` onward the application allows and prompts ope ###### 2. Verify the binary and extract it if needed -- Download [`hashes.json`]https://github.com/nymtech/nym/releases/download/nym-wallet-v1.2.15/hashes.json +- Download [`hashes.json`](https://github.com/nymtech/nym/releases/download/nym-wallet-v1.2.15/hashes.json) - Open it with your text editor or print it's content with `cat hashes.json` - Run `sha256sum ` for example `sha256sum ./nym-wallet_1.2.15_amd64.AppImage` - If your have to extract it (like `.tar.gz`) do it diff --git a/documentation/docs/pages/operators/nodes/nym-node/configuration.mdx b/documentation/docs/pages/operators/nodes/nym-node/configuration.mdx index 63ba5b3f0d6..9a82c0081f6 100644 --- a/documentation/docs/pages/operators/nodes/nym-node/configuration.mdx +++ b/documentation/docs/pages/operators/nodes/nym-node/configuration.mdx @@ -301,8 +301,10 @@ chmod +x network_tunnel_manager.sh && \ ###### 3. Setup IP tables rules -- Apply the rules for IPv4 and IPv6: +- Delete IP tables rules for IPv4 and IPv6 and apply new ones: ```sh +./network_tunnel_manager.sh remove_duplicate_rules nymtun0 + ./network_tunnel_manager.sh apply_iptables_rules ``` @@ -363,9 +365,11 @@ operation check_nymtun_iptables completed successfully. ``` -###### 5. Apply rules for wireguad routing +###### 5. Remove old and apply new rules for wireguad routing ```sh +/network_tunnel_manager.sh remove_duplicate_rules nymwg + ./network_tunnel_manager.sh apply_iptables_rules_wg ``` @@ -374,8 +378,15 @@ operation check_nymtun_iptables completed successfully. ```sh ./network_tunnel_manager.sh configure_dns_and_icmp_wg ``` +###### 7. Adjust and validate IP forwarding + +```sh +./network_tunnel_manager.sh adjust_ip_forwarding + +./network_tunnel_manager.sh check_ipv6_ipv4_forwarding +``` -###### 7. Check `nymtun0` interface and test routing configuration +###### 8. Check `nymtun0` interface and test routing configuration ```sh ip addr show nymtun0 @@ -409,7 +420,7 @@ ip addr show nymtun0 - **Note:** WireGuard will return only IPv4 joke, not IPv6. WG IPv6 is under development. Running IPR joke through the mixnet with `./network_tunnel_manager.sh joke_through_the_mixnet` should work with both IPv4 and IPv6! -###### 8. Enable wireguard +###### 9. Enable wireguard Now you can run your node with the `--wireguard-enabled true` flag or add it to your [systemd service config](#systemd). Restart your `nym-node` or [systemd](#2-following-steps-for-nym-nodes-running-as-systemd-service) service (recommended): diff --git a/documentation/docs/pages/operators/nodes/nym-node/configuration/_meta.json b/documentation/docs/pages/operators/nodes/nym-node/configuration/_meta.json index 3d0ff2819e7..a48b50f426a 100644 --- a/documentation/docs/pages/operators/nodes/nym-node/configuration/_meta.json +++ b/documentation/docs/pages/operators/nodes/nym-node/configuration/_meta.json @@ -1,3 +1,3 @@ { - "proxy-configuration": "WSS & Reverese Proxy" + "proxy-configuration": "WSS & Reverse Proxy" } diff --git a/documentation/docs/pages/operators/nodes/nym-node/setup.mdx b/documentation/docs/pages/operators/nodes/nym-node/setup.mdx index 31eb89308c2..33e7e3f6c34 100644 --- a/documentation/docs/pages/operators/nodes/nym-node/setup.mdx +++ b/documentation/docs/pages/operators/nodes/nym-node/setup.mdx @@ -17,12 +17,12 @@ This documentation page provides a guide on how to set up and run a [NYM NODE](. ```sh nym-node Binary Name: nym-node -Build Timestamp: 2024-11-22T14:30:48.067329245Z -Build Version: 1.1.11 -Commit SHA: 01c7b2819ee3d328deccd303b4113ff415d7e276 -Commit Date: 2024-11-22T10:50:59.000000000+01:00 +Build Timestamp: 2024-12-11T13:49:11.974104790Z +Build Version: 1.2.0 +Commit SHA: a491e6a71a8cf862d77defd740a4ee8d65d8292a +Commit Date: 2024-12-11T10:28:47.000000000+01:00 Commit Branch: HEAD -rustc Version: 1.82.0 +rustc Version: 1.83.0 rustc Channel: stable cargo Profile: release ``` @@ -33,6 +33,7 @@ cargo Profile: release ## Summary + To run a new node, you can simply execute the `nym-node` command without any flags. By default, the node will set necessary configurations. If you later decide to change a setting, you can use the `-w` flag. diff --git a/documentation/docs/pages/operators/tokenomics/mixnet-rewards.mdx b/documentation/docs/pages/operators/tokenomics/mixnet-rewards.mdx index facd3860f30..569cd12e089 100644 --- a/documentation/docs/pages/operators/tokenomics/mixnet-rewards.mdx +++ b/documentation/docs/pages/operators/tokenomics/mixnet-rewards.mdx @@ -12,6 +12,10 @@ import { Clt } from 'components/callout-custom/CalloutCustom.jsx'; # Nym Operators Rewards + +**Nym Network Rewarded set selection had been upgraded recently. Make sure to read the chapter *[Rewarded Set Selection](#rewarded-set-selection)* below carefully to fully understand all requirements to be rewarded!** + + * Nym tokenomics are based on the research paper [*Reward Sharing for Mixnets*](https://nymtech.net/nym-cryptoecon-paper.pdf) @@ -42,6 +46,7 @@ To make it easier for the reader, we use a highlighting line on the left side, w Nodes bonded with vesting tokens are [not allowed to join rewarded set](https://github.com/nymtech/nym/pull/5129) - read more on [Nym operators forum](https://forum.nymtech.net/t/vesting-accounts-are-no-longer-supported/827). + ## Overview This is a quick summary, to understand the full picture, please see detailed [*Rewards Logic & Calculation*](#rewards-logic--calculation) chapter below. @@ -126,34 +131,94 @@ This is a quick summary, to understand the full picture, please see detailed [*R -### Active Set Selection - -*Performance matters!* +### Rewarded Set Selection For a node to be rewarded, the node must be part of a [Rewarded set](https://validator.nymtech.net/api/v1/epoch/reward_params) (which currently = active set) in the first place. The active set is selected in the beginning of each epoch (every 60min) where total of 240 Nym nodes - represented by 120 mixnodes and 120 gateways, are randomly allocated across the layers. -The algorithm choosing nodes into the active set takes into account node's performance and [stake saturation](../tolkenomics.mdx#stake-saturation), both values being between 0 and 1 and config score which is either 0 or 1. +The algorithm choosing nodes into the active set takes into account these parameters: + +1. [Config score](#config-score-calculation) +2. [Performance](#performance-calculation) +3. [Stake saturation](../tokenomics.mdx#stake-saturation) + +Besides these values, the API is also looking whther the node is bonded in Mixnet smart contract as a Nym Node or legacy node (Mixnode or Gateway). **Only nodes bonded as Nym Node in Mixnet smart contract can be selected to the Rewrded set, if you haven't migrated your node yet, please [follow these steps](../nodes/nym-node/bonding#migrate-to-nym-node-in-mixnet-smart-contract)!** + +**The Rewarded set selection probablity formula:** + + +> **active_set_selection_probability = config_score \* ( node_performance ^ 20 ) \* stake_saturation** + + +#### Config Score Calculation + +The nodes selection to the active set has a new parameter - `config_score`. Config score currently looks into three paramteres: -**Config score is introduced:** The nodes selection to the active set has a new parameter - `config_score`. Config score currently looks if the node binary is `nym-node` (not legacy `nym-mixnode` or `nym-gateway`) **AND** if [Terms & Conditions](nodes/nym-node/setup.mdx#terms--conditions) are accepted. Config score has binary values of either 0 or 1, with a following logic: +1. If the node binary is `nym-node` (not legacy `nym-mixnode` or `nym-gateway`) +2. If [Terms & Conditions](../nodes/nym-node/setup.mdx#terms--conditions) are accepted. +3. Version of `nym-node` binary + +**The `config_score` parameter calculation formula:** + + +> **config_score = is_tc_accepted \* is_nym-node_binary \* ( 0.995 ^ ( ( X * versions_behind) ^ 1.65 ) )** + -| **Run `nym-node` binary** | **T&C's accepted** | **`config_score`** | +First two points have binary values of either 0 or 1, with a following logic: + +| **Run `nym-node` binary** | **T&C's accepted** | **Value** | | :-- | :-- | ---: | +| True | True | 1 | | True | False | 0 | | False | True | 0 | | False | False | 0 | -| True | True | 1 | +Only if both conditions above are `True` the node can have any chance to be selected, as otherwise the probability will always be 0. + +**The `version_behind` parameter in `config_score` calculation** -The entire active set selection probablity: +From release `2024.14-crunch` (`nym-node v1.2.0`), the `config_score` parameter takes into account also nodes version. Current version is the +one marked as `Latest` in our repository. From that one we count the parameter `version_behind`, where every version back the number of `versions_behind` increases by 1 in this formula: -> **active_set_selection_probability = config_score \* stake_saturation \* node_performance ^ 20** +> **0.995 ^ ( ( X * versions_behind ) ^ 1.65 )** +> +> where:
+> **X = 1; for patches**
+> **X = 10; for minor versions**
+> **X = 100; for major versions**
-For a comparison we made an example with 5 nodes, where first number is node performance and second stake saturation (assuming all of them `config_score` = 1 and not 0): +> The exact parameters are live accessible on [`/v1/status/config-score-details`](https://validator.nymtech.net/api/swagger/index.html#/Status/config_score_details). + +Our versioning convention is: `major_version . minor_version . patch` + +For example `nym-node` on version `1.2.0` is on 1st major version, 2nd minor and 0 patches. See the the table and graph below: + +| **Version behind** | **Patches (X = 1)** | **Minor versions (X = 10)** | **Major versions (X = 100)** | +| :-- | --: | --: | --: | +| 0 (current version) | 1.0 | 1.0 | 1.0 | +| 1 | 0.995 | 0.7994 | 0.0000 | +| 2 | 0.9844 | 0.4953 | 0.0000 | +| 3 | 0.9698 | 0.2536 | 0.0000 | +| 4 | 0.9518 | 0.1102 | 0.0000 | +| 5 | 0.9311 | 0.0413 | 0.0000 | + + +![](/images/operators/tokenomics/reward_version_graph.png) + +As you can see on above, the algorithm is designed to give maximum probability (`1`) to the latest version and exponentialy dicrease the probability to non-upgraded nodes where the more important version the node is behind, the faster the cliff. This eliminates any older nodes despite their saturation and performance to take place in the Rewarded set and gives a priority to the operators running up-to-date nodes, ensuring as strong network as possible. + + +#### Performance Calculation + +Performance is measured by Nym Network Monitor which sends thousands of packages through different routes every 15 minutes and measures how many were dropped on the way. Test result represents percentage of packets succesfully returned (can be anything between 0 and 1). Performance value is nodes average of these tests in last 24h. + +Good performance is much more essential than [stake saturation](../tokenomics.mdx#stake-saturation), because it's lifted to 20th power in the selection formula. + +For a comparison we made an example with 5 nodes, where first number is node performance and second stake saturation (assuming all of them `config_score` = 1):
- + > node_1 = 1.00 ^ 20 \* 1.0 = 1
> node_2 = 1.00 ^ 20 \* 0.5 = 0.5
> node_3 = 0.99 ^ 20 \* 1.0 = 0.818
@@ -201,7 +266,7 @@ $33\% - 67\%$ ## Roadmap -We are working on the final architecture of [*Fair Mixnet*](#fair-mixnet) tokenomics implementation. The current design is called [*Naive rewarding*](#naive-rewarding). This is an intermediate step, allowing operators to migrate to `nym-node` in Mixnet smart contract and for the first time recieve delegations and earn rewards for any `nym-node` functionality, in opposite to the past system, where only Mixnodes were able to recieve delegations and rewards. +We are working on the final architecture of [*Fair Mixnet*](#fair-mixnet) tokenomics implementation. The current design is called [*Naive rewarding*](#naive-rewarding). This is an intermediate step, expecting operators to migrate to `nym-node` in Mixnet smart contract and be able to recieve delegations and earn rewards for any `nym-node` functionality, in opposite to the past system, where only Mixnodes were able to recieve delegations and rewards. On November 5th, we presented a release roadmap in live [Operators Townhall](https://www.youtube.com/watch?v=3G1pJqvO2VM) where we explained in detail the steps of Nym node and tokenomics development and the effect it will have on node operators and put it into a rough timeline. diff --git a/documentation/docs/public/images/operators/tokenomics/reward_version_graph.png b/documentation/docs/public/images/operators/tokenomics/reward_version_graph.png new file mode 100644 index 00000000000..57948742702 Binary files /dev/null and b/documentation/docs/public/images/operators/tokenomics/reward_version_graph.png differ diff --git a/documentation/scripts/rewards_version_graph.py b/documentation/scripts/rewards_version_graph.py new file mode 100644 index 00000000000..6177c3d7e23 --- /dev/null +++ b/documentation/scripts/rewards_version_graph.py @@ -0,0 +1,67 @@ +import matplotlib.pyplot as plt +import matplotlib.axes as ax +import matplotlib.pylab as pylab +from matplotlib.pyplot import figure +import numpy as np + +plt.style.use('dark_background') + +a = 0.995 +b = 1.65 + +# make data +x1 = [0,1,2,3,4,5] +x2 = x1 +x3 = x1 +x4 = x1 + +y1 = [a**((v*1)**b) for v in x1] +y2 = [a**((v*10)**b) for v in x1] +y3 = [a**((v*100)**b) for v in x1] +# y4 = [a**((11)**b) for v in x1] + +f = plt.figure() +f.set_figwidth(12) +f.set_figheight(9) + +# plot +#fig, ax = plt.subplots() +plt.plot(x1,y1, label=f'Patches behind: config_score_multiplier = {a} ^ ((1 * versions_behind) ^ {b})') +plt.plot(x2,y2, label=f'Minor versions behind: config_score_multiplier = {a} ^ ((10 * versions_behind) ^ {b})') +plt.plot(x3,y3, label=f'Major versions behind: config_score_multiplier = {a} ^ ((100 * versions_behind) ^ {b})') +#ax.plot(x, y, linewidth=2.0) + + +# naming the x axis +plt.xlabel('Nym Node versions behind the current one', fontsize=20) + + +# naming the y axis +plt.ylabel('Config score multiplier', fontsize=20) + +# giving a title to my graph +plt.title('Nym node version config score multiplier', fontsize=28) + + +#ax.Axes.set_xticks([x]) +#ax.Axes.set_yticks([y]) + +plt.legend(fontsize=12) + +#params = {'legend.fontsize': 20, +# 'axes.labelsize': 24, +# 'axes.titlesize':'x-large', +# 'xtick.labelsize':20, +# 'ytick.labelsize':20} +# +#pylab.rcParams.update(params) + +# set the limits +plt.xlim([0, 5]) +plt.ylim([0,1]) + + + +#plt.show() + +plt.savefig('../docs/public/images/operators/tokenomics/reward_version_graph.png') diff --git a/envs/qa.env b/envs/qa.env index 81adaf299c8..0a53e1aa0c4 100644 --- a/envs/qa.env +++ b/envs/qa.env @@ -20,5 +20,6 @@ REWARDING_VALIDATOR_ADDRESS=n1rfvpsynktze6wvn6ldskj8xgwfzzk5v6pnff39 EXPLORER_API=https://qa-network-explorer.qa.nymte.ch/api/ NYXD=https://qa-validator.qa.nymte.ch +NYXD_WS=wss://qa-validator.qa.nymte.ch/websocket/ NYM_API=https://qa-nym-api.qa.nymte.ch/api/ NYM_VPN_API=https://nym-vpn-api-git-deploy-qa-nyx-network-staging.vercel.app/api/ diff --git a/explorer-api/Cargo.toml b/explorer-api/Cargo.toml index 11f8672f982..6fe9830daee 100644 --- a/explorer-api/Cargo.toml +++ b/explorer-api/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "explorer-api" -version = "1.1.42" +version = "1.1.43" edition = "2021" license.workspace = true diff --git a/explorer-api/explorer-api-requests/src/lib.rs b/explorer-api/explorer-api-requests/src/lib.rs index 3ea6ed72009..0eddf354263 100644 --- a/explorer-api/explorer-api-requests/src/lib.rs +++ b/explorer-api/explorer-api-requests/src/lib.rs @@ -1,6 +1,8 @@ -use nym_api_requests::models::NodePerformance; +use nym_api_requests::models::{DescribedNodeType, NodePerformance, NymNodeData}; use nym_contracts_common::Percent; -use nym_mixnet_contract_common::{Addr, Coin, Gateway, LegacyMixLayer, MixNode, NodeId}; +use nym_mixnet_contract_common::{ + Addr, Coin, Delegation, Gateway, LegacyMixLayer, MixNode, NodeId, NodeRewarding, NymNodeBond, +}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -52,3 +54,32 @@ pub struct PrettyDetailedGatewayBond { pub proxy: Option, pub location: Option, } + +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] +pub struct NymNodeWithDescriptionAndLocation { + pub node_id: NodeId, + pub contract_node_type: Option, + pub description: Option, + pub bond_information: NymNodeBond, + pub rewarding_details: NodeRewarding, + pub location: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] +pub struct NymNodeWithDescriptionAndLocationAndDelegations { + pub node_id: NodeId, + pub contract_node_type: Option, + pub description: Option, + pub bond_information: NymNodeBond, + pub rewarding_details: NodeRewarding, + pub location: Option, + pub delegations: Option>, +} + +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] +pub struct NymVestingAccount { + pub locked: Coin, + pub vested: Coin, + pub vesting: Coin, + pub spendable: Coin, +} diff --git a/explorer-api/src/country_statistics/distribution.rs b/explorer-api/src/country_statistics/distribution.rs index f834b831c72..2625952353e 100644 --- a/explorer-api/src/country_statistics/distribution.rs +++ b/explorer-api/src/country_statistics/distribution.rs @@ -38,8 +38,14 @@ impl CountryStatisticsDistributionTask { /// Retrieves the current list of mixnodes from the validators and calculates how many nodes are in each country async fn calculate_nodes_per_country(&mut self) { let cache = self.state.inner.mixnodes.get_locations().await; + let nym_nodes = self + .state + .inner + .nymnodes + .get_bonded_nymnodes_locations() + .await; - let three_letter_iso_country_codes: Vec = cache + let mut three_letter_iso_country_codes: Vec = cache .values() .flat_map(|i| { i.location @@ -48,6 +54,10 @@ impl CountryStatisticsDistributionTask { }) .collect(); + for node in nym_nodes { + three_letter_iso_country_codes.push(node.three_letter_iso_country_code); + } + let mut distribution = CountryNodesDistribution::new(); info!("Calculating country distribution from located mixnodes..."); diff --git a/explorer-api/src/gateways/models.rs b/explorer-api/src/gateways/models.rs index a3ed4060672..d2298b2a0c2 100644 --- a/explorer-api/src/gateways/models.rs +++ b/explorer-api/src/gateways/models.rs @@ -2,8 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 use crate::{cache::Cache, location::LocationCacheItem}; +use nym_contracts_common::IdentityKey; use nym_explorer_api_requests::{Location, PrettyDetailedGatewayBond}; -use nym_mixnet_contract_common::{GatewayBond, IdentityKey}; +use nym_mixnet_contract_common::GatewayBond; +use nym_validator_client::models::GatewayBondAnnotated; use serde::Serialize; use std::{sync::Arc, time::SystemTime}; use tokio::sync::RwLock; @@ -11,7 +13,7 @@ use tokio::sync::RwLock; use super::location::GatewayLocationCache; pub(crate) struct GatewayCache { - pub(crate) gateways: Cache, + pub(crate) gateways: Cache, } #[derive(Clone, Debug, Serialize, JsonSchema)] @@ -22,6 +24,7 @@ pub(crate) struct GatewaySummary { #[derive(Clone)] pub(crate) struct ThreadsafeGatewayCache { gateways: Arc>, + legacy_gateway_bonds: Arc>, locations: Arc>, } @@ -31,6 +34,9 @@ impl ThreadsafeGatewayCache { gateways: Arc::new(RwLock::new(GatewayCache { gateways: Cache::new(), })), + legacy_gateway_bonds: Arc::new(RwLock::new(GatewayCache { + gateways: Cache::new(), + })), locations: Arc::new(RwLock::new(GatewayLocationCache::new())), } } @@ -51,7 +57,14 @@ impl ThreadsafeGatewayCache { } pub(crate) async fn get_gateways(&self) -> Vec { - self.gateways.read().await.gateways.get_all() + self.gateways + .read() + .await + .gateways + .get_all() + .iter() + .map(|g| g.gateway_bond.bond.clone()) + .collect() } pub(crate) async fn get_detailed_gateways(&self) -> Vec { @@ -64,7 +77,22 @@ impl ThreadsafeGatewayCache { .iter() .map(|bond| { let location = location_guard.get(bond.identity()); - self.create_detailed_gateway(bond.to_owned(), location) + self.create_detailed_gateway(bond.gateway_bond.bond.to_owned(), location) + }) + .collect() + } + + pub(crate) async fn get_legacy_detailed_gateways(&self) -> Vec { + let legacy_gateways = self.legacy_gateway_bonds.read().await; + let location_guard = self.locations.read().await; + + legacy_gateways + .gateways + .get_all() + .iter() + .map(|bond| { + let location = location_guard.get(bond.identity()); + self.create_detailed_gateway(bond.gateway_bond.bond.to_owned(), location) }) .collect() } @@ -80,6 +108,9 @@ impl ThreadsafeGatewayCache { gateways: Arc::new(RwLock::new(GatewayCache { gateways: Cache::new(), })), + legacy_gateway_bonds: Arc::new(RwLock::new(GatewayCache { + gateways: Cache::new(), + })), locations: Arc::new(RwLock::new(locations)), } } @@ -106,13 +137,26 @@ impl ThreadsafeGatewayCache { .insert(identy_key, LocationCacheItem::new_from_location(location)); } - pub(crate) async fn update_cache(&self, gateways: Vec) { + pub(crate) async fn update_cache( + &self, + gateways: Vec, + legacy_gateway_bonds: Vec, + ) { let mut guard = self.gateways.write().await; + let mut guard_legacy_gateways = self.legacy_gateway_bonds.write().await; for gateway in gateways { guard .gateways - .set(gateway.gateway.identity_key.clone(), gateway) + .set(gateway.gateway_bond.gateway.identity_key.clone(), gateway) + } + + for legacy_gateway in legacy_gateway_bonds { + if let Some(g) = guard.gateways.get(&legacy_gateway.gateway.identity_key) { + guard_legacy_gateways + .gateways + .set(legacy_gateway.gateway.identity_key, g.clone()); + } } } } diff --git a/explorer-api/src/geo_ip/location.rs b/explorer-api/src/geo_ip/location.rs index 59bbb33ad7f..263764943e3 100644 --- a/explorer-api/src/geo_ip/location.rs +++ b/explorer-api/src/geo_ip/location.rs @@ -166,7 +166,7 @@ impl GeoIp { } } -impl<'a> TryFrom<&City<'a>> for Location { +impl TryFrom<&City<'_>> for Location { type Error = String; fn try_from(city: &City) -> Result { diff --git a/explorer-api/src/guards/location.rs b/explorer-api/src/guards/location.rs index 045b03f5b4e..f5ce0b7ee28 100644 --- a/explorer-api/src/guards/location.rs +++ b/explorer-api/src/guards/location.rs @@ -65,7 +65,7 @@ impl<'r> FromRequest<'r> for Location { } } -impl<'a> OpenApiFromRequest<'a> for Location { +impl OpenApiFromRequest<'_> for Location { fn from_request_input( _gen: &mut OpenApiGenerator, _name: String, diff --git a/explorer-api/src/http/mod.rs b/explorer-api/src/http/mod.rs index 2d4e2d9fabf..19802e5fcca 100644 --- a/explorer-api/src/http/mod.rs +++ b/explorer-api/src/http/mod.rs @@ -10,11 +10,11 @@ use crate::gateways::http::gateways_make_default_routes; use crate::http::swagger::get_docs; use crate::mix_node::http::mix_node_make_default_routes; use crate::mix_nodes::http::mix_nodes_make_default_routes; -use crate::nym_nodes::http::unstable_temp_nymnodes_make_default_routes; use crate::overview::http::overview_make_default_routes; use crate::ping::http::ping_make_default_routes; use crate::service_providers::http::service_providers_make_default_routes; use crate::state::ExplorerApiStateContext; +use crate::unstable::http::unstable_temp_make_default_routes; use crate::validators::http::validators_make_default_routes; mod swagger; @@ -59,7 +59,7 @@ fn configure_rocket(state: ExplorerApiStateContext) -> Rocket { "/ping" => ping_make_default_routes(&openapi_settings), "/validators" => validators_make_default_routes(&openapi_settings), "/service-providers" => service_providers_make_default_routes(&openapi_settings), - "/tmp/unstable" => unstable_temp_nymnodes_make_default_routes(&openapi_settings), + "/tmp/unstable" => unstable_temp_make_default_routes(&openapi_settings), }; building_rocket diff --git a/explorer-api/src/main.rs b/explorer-api/src/main.rs index ecaae3f4922..9811eed41ac 100644 --- a/explorer-api/src/main.rs +++ b/explorer-api/src/main.rs @@ -22,12 +22,12 @@ mod http; mod location; mod mix_node; pub(crate) mod mix_nodes; -mod nym_nodes; mod overview; mod ping; pub(crate) mod service_providers; mod state; mod tasks; +mod unstable; mod validators; const COUNTRY_DATA_REFRESH_INTERVAL: u64 = 60 * 15; // every 15 minutes diff --git a/explorer-api/src/mix_node/http.rs b/explorer-api/src/mix_node/http.rs index 2a44097275c..78263d9b132 100644 --- a/explorer-api/src/mix_node/http.rs +++ b/explorer-api/src/mix_node/http.rs @@ -44,7 +44,7 @@ async fn get_mix_node_stats(host: &str, port: u16) -> Result")] pub(crate) async fn get_by_id( mix_id: NodeId, diff --git a/explorer-api/src/mix_nodes/models.rs b/explorer-api/src/mix_nodes/models.rs index 4ffd99304f7..ce5876c9496 100644 --- a/explorer-api/src/mix_nodes/models.rs +++ b/explorer-api/src/mix_nodes/models.rs @@ -7,7 +7,7 @@ use crate::location::LocationCacheItem; use crate::mix_nodes::CACHE_ENTRY_TTL; use nym_explorer_api_requests::{Location, MixnodeStatus, PrettyDetailedMixNodeBond}; use nym_mixnet_contract_common::rewarding::helpers::truncate_reward; -use nym_mixnet_contract_common::NodeId; +use nym_mixnet_contract_common::{MixNodeBond, NodeId}; use nym_validator_client::models::MixNodeBondAnnotated; use serde::Serialize; use std::collections::{HashMap, HashSet}; @@ -80,6 +80,7 @@ impl MixNodesResult { #[derive(Clone)] pub(crate) struct ThreadsafeMixNodesCache { mixnodes: Arc>, + legacy_mixnode_bonds: Arc>, locations: Arc>, } @@ -87,6 +88,7 @@ impl ThreadsafeMixNodesCache { pub(crate) fn new() -> Self { ThreadsafeMixNodesCache { mixnodes: Arc::new(RwLock::new(MixNodesResult::new())), + legacy_mixnode_bonds: Arc::new(RwLock::new(MixNodesResult::new())), locations: Arc::new(RwLock::new(MixnodeLocationCache::new())), } } @@ -94,6 +96,7 @@ impl ThreadsafeMixNodesCache { pub(crate) fn new_with_location_cache(locations: MixnodeLocationCache) -> Self { ThreadsafeMixNodesCache { mixnodes: Arc::new(RwLock::new(MixNodesResult::new())), + legacy_mixnode_bonds: Arc::new(RwLock::new(MixNodesResult::new())), locations: Arc::new(RwLock::new(locations)), } } @@ -188,13 +191,35 @@ impl ThreadsafeMixNodesCache { .collect() } + pub(crate) async fn get_legacy_detailed_mixnodes(&self) -> Vec { + let legacy_mixnodes = self.legacy_mixnode_bonds.read().await; + let location_guard = self.locations.read().await; + + legacy_mixnodes + .all_mixnodes + .values() + .map(|bond| { + let location = location_guard.get(&bond.mix_id()); + self.create_detailed_mixnode(bond.mix_id(), &legacy_mixnodes, location, bond) + }) + .collect() + } + pub(crate) async fn update_cache( &self, all_bonds: Vec, rewarded_nodes: HashSet, active_nodes: HashSet, + legacy_mixnode_bonds: Vec, ) { let mut guard = self.mixnodes.write().await; + let mut guard_legacy_mixnodes = self.legacy_mixnode_bonds.write().await; + + let legacy_mixnode_bond_ids: Vec<&NodeId> = legacy_mixnode_bonds + .iter() + .map(|bond| &bond.mix_id) + .collect(); + guard.all_mixnodes = all_bonds .into_iter() .map(|bond| (bond.mix_id(), bond)) @@ -202,5 +227,11 @@ impl ThreadsafeMixNodesCache { guard.rewarded_mixnodes = rewarded_nodes; guard.active_mixnodes = active_nodes; guard.valid_until = SystemTime::now() + CACHE_ENTRY_TTL; + guard_legacy_mixnodes.all_mixnodes = guard + .all_mixnodes + .clone() + .into_iter() + .filter(|(node_id, _bond)| legacy_mixnode_bond_ids.iter().any(|i| **i == *node_id)) + .collect(); } } diff --git a/explorer-api/src/nym_nodes/http.rs b/explorer-api/src/nym_nodes/http.rs deleted file mode 100644 index 378fcc78d79..00000000000 --- a/explorer-api/src/nym_nodes/http.rs +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::state::ExplorerApiStateContext; -use nym_explorer_api_requests::PrettyDetailedGatewayBond; -use okapi::openapi3::OpenApi; -use rocket::serde::json::Json; -use rocket::{Route, State}; -use rocket_okapi::settings::OpenApiSettings; - -pub fn unstable_temp_nymnodes_make_default_routes( - settings: &OpenApiSettings, -) -> (Vec, OpenApi) { - openapi_get_routes_spec![settings: all_gateways] -} - -#[openapi(tag = "UNSTABLE")] -#[get("/gateways")] -pub(crate) async fn all_gateways( - state: &State, -) -> Json> { - let mut gateways = state.inner.gateways.get_detailed_gateways().await; - gateways.append(&mut state.inner.nymnodes.pretty_gateways().await); - - Json(gateways) -} diff --git a/explorer-api/src/overview/http.rs b/explorer-api/src/overview/http.rs index a5cfb127c58..1ef904918ab 100644 --- a/explorer-api/src/overview/http.rs +++ b/explorer-api/src/overview/http.rs @@ -1,3 +1,4 @@ +use nym_validator_client::models::NymNodeData; use rocket::serde::json::Json; use rocket::{Route, State}; use rocket_okapi::okapi::openapi3::OpenApi; @@ -5,19 +6,51 @@ use rocket_okapi::openapi_get_routes_spec; use rocket_okapi::settings::OpenApiSettings; use crate::mix_nodes::http::get_mixnode_summary; -use crate::overview::models::OverviewSummary; +use crate::overview::models::{NymNodeSummary, OverviewSummary, RoleSummary}; use crate::state::ExplorerApiStateContext; pub fn overview_make_default_routes(settings: &OpenApiSettings) -> (Vec, OpenApi) { openapi_get_routes_spec![settings: summary] } +fn get_nym_nodes_by_role(nodes: &Vec) -> RoleSummary { + let mut summary = RoleSummary::default(); + + for node in nodes { + if node.declared_role.entry { + summary.entry += 1; + } + if node.declared_role.exit_ipr { + summary.exit_ipr += 1; + } + if node.declared_role.exit_nr { + summary.exit_nr += 1; + } + if node.declared_role.mixnode { + summary.mixnode += 1; + } + } + + summary +} + #[openapi(tag = "overview")] #[get("/summary")] pub(crate) async fn summary(state: &State) -> Json { + let nym_nodes = state + .inner + .nymnodes + .get_bonded_nymnodes_descriptions() + .await; + let roles = get_nym_nodes_by_role(&nym_nodes); + Json(OverviewSummary { mixnodes: get_mixnode_summary(state).await, validators: state.inner.validators.get_validator_summary().await, gateways: state.inner.gateways.get_gateway_summary().await, + nymnodes: NymNodeSummary { + count: nym_nodes.len(), + roles, + }, }) } diff --git a/explorer-api/src/overview/models.rs b/explorer-api/src/overview/models.rs index b396e2e3fb0..2edcffaaf8b 100644 --- a/explorer-api/src/overview/models.rs +++ b/explorer-api/src/overview/models.rs @@ -7,9 +7,24 @@ use crate::gateways::models::GatewaySummary; use crate::mix_nodes::models::MixNodeSummary; use crate::validators::models::ValidatorSummary; +#[derive(Clone, Debug, Serialize, JsonSchema, Default)] +pub(crate) struct RoleSummary { + pub mixnode: usize, + pub entry: usize, + pub exit_nr: usize, + pub exit_ipr: usize, +} + +#[derive(Clone, Debug, Serialize, JsonSchema, Default)] +pub(crate) struct NymNodeSummary { + pub count: usize, + pub roles: RoleSummary, +} + #[derive(Clone, Debug, Serialize, JsonSchema)] pub(crate) struct OverviewSummary { pub mixnodes: MixNodeSummary, pub gateways: GatewaySummary, pub validators: ValidatorSummary, + pub nymnodes: NymNodeSummary, } diff --git a/explorer-api/src/state.rs b/explorer-api/src/state.rs index 28373aef809..729e1e95739 100644 --- a/explorer-api/src/state.rs +++ b/explorer-api/src/state.rs @@ -3,12 +3,18 @@ use std::path::Path; use chrono::{DateTime, Utc}; use log::info; -use nym_mixnet_contract_common::NodeId; +use nym_explorer_api_requests::NymVestingAccount; +use nym_mixnet_contract_common::{Addr, Delegation, NodeId, PendingRewardResponse}; use serde::{Deserialize, Serialize}; use crate::client::ThreadsafeValidatorClient; use crate::geo_ip::location::ThreadsafeGeoIp; +use nym_mixnet_contract_common::Coin as CosmWasmCoin; use nym_validator_client::models::MixNodeBondAnnotated; +use nym_validator_client::nyxd::contract_traits::{ + MixnetQueryClient, PagedMixnetQueryClient, VestingQueryClient, +}; +use nym_validator_client::nyxd::{AccountId, Coin, CosmWasmClient}; use crate::country_statistics::country_nodes_distribution::{ CountryNodesDistribution, ThreadsafeCountryNodesDistribution, @@ -18,9 +24,9 @@ use crate::gateways::models::ThreadsafeGatewayCache; use crate::mix_node::models::ThreadsafeMixNodeCache; use crate::mix_nodes::location::MixnodeLocationCache; use crate::mix_nodes::models::ThreadsafeMixNodesCache; -use crate::nym_nodes::location::NymNodeLocationCache; -use crate::nym_nodes::models::ThreadSafeNymNodesCache; use crate::ping::models::ThreadsafePingCache; +use crate::unstable::location::NymNodeLocationCache; +use crate::unstable::models::ThreadSafeNymNodesCache; use crate::validators::models::ThreadsafeValidatorCache; // TODO: change to an environment variable with a default value @@ -45,6 +51,150 @@ impl ExplorerApiState { pub(crate) async fn get_mix_node(&self, mix_id: NodeId) -> Option { self.mixnodes.get_mixnode(mix_id).await } + + pub(crate) async fn get_delegations_by_node( + &self, + node_id: NodeId, + ) -> Result, rocket::response::status::NotFound> { + match self + .validator_client + .0 + .nyxd + .get_all_single_mixnode_delegations(node_id) + .await + { + Ok(res) => Ok(res), + Err(e) => Err(rocket::response::status::NotFound(format!("{}", e))), + } + } + + pub(crate) async fn get_balance( + &self, + addr: &AccountId, + ) -> Result, rocket::response::status::NotFound> { + match self.validator_client.0.nyxd.get_all_balances(addr).await { + Ok(res) => Ok(res), + Err(e) => Err(rocket::response::status::NotFound(format!("{}", e))), + } + } + + pub(crate) async fn get_vesting_balance( + &self, + addr: &AccountId, + ) -> Result, rocket::response::status::NotFound> { + match nym_validator_client::nyxd::contract_traits::VestingQueryClient::get_account( + &self.validator_client.0.nyxd, + addr.as_ref(), + ) + .await + { + // 1. is there a vesting account? + Ok(_res) => { + // 2. there is vesting account, get all the coins + let mut locked = CosmWasmCoin::default(); + let mut vested = CosmWasmCoin::default(); + let mut vesting = CosmWasmCoin::default(); + let mut spendable = CosmWasmCoin::default(); + + // 3. try to get each coin type + if let Ok(coin) = self + .validator_client + .0 + .nyxd + .locked_coins(addr.as_ref(), None) + .await + { + locked = coin.into(); + } + if let Ok(coin) = self + .validator_client + .0 + .nyxd + .vested_coins(addr.as_ref(), None) + .await + { + vested = coin.into(); + } + if let Ok(coin) = self + .validator_client + .0 + .nyxd + .vesting_coins(addr.as_ref(), None) + .await + { + vesting = coin.into(); + } + if let Ok(coin) = self + .validator_client + .0 + .nyxd + .spendable_coins(addr.as_ref(), None) + .await + { + spendable = coin.into(); + } + + // 4.combine into a response + Ok(Some(NymVestingAccount { + locked, + vested, + vesting, + spendable, + })) + } + Err(e) => Err(rocket::response::status::NotFound(format!("{}", e))), + } + } + + pub(crate) async fn get_delegations( + &self, + addr: &AccountId, + ) -> Result, rocket::response::status::NotFound> { + match self + .validator_client + .0 + .nyxd + .get_all_delegator_delegations(addr) + .await + { + Ok(res) => Ok(res), + Err(e) => Err(rocket::response::status::NotFound(format!("{}", e))), + } + } + + pub(crate) async fn get_delegation_rewards( + &self, + addr: &AccountId, + node_id: &NodeId, + proxy: &Option, + ) -> Result> { + match self + .validator_client + .0 + .nyxd + .get_pending_delegator_reward(addr, *node_id, proxy.clone().map(|d| d.to_string())) + .await + { + Ok(res) => Ok(res), + Err(e) => Err(rocket::response::status::NotFound(format!("{}", e))), + } + } + + pub(crate) async fn get_operator_rewards( + &self, + addr: &AccountId, + ) -> Result> { + match self + .validator_client + .0 + .nyxd + .get_pending_operator_reward(addr) + .await + { + Ok(res) => Ok(res), + Err(e) => Err(rocket::response::status::NotFound(format!("{}", e))), + } + } } #[derive(Debug, Serialize, Deserialize)] diff --git a/explorer-api/src/tasks.rs b/explorer-api/src/tasks.rs index fa43bfc9e46..1fa43d98654 100644 --- a/explorer-api/src/tasks.rs +++ b/explorer-api/src/tasks.rs @@ -3,9 +3,12 @@ use crate::mix_nodes::CACHE_REFRESH_RATE; use crate::state::ExplorerApiStateContext; -use nym_mixnet_contract_common::{GatewayBond, NymNodeDetails}; +use nym_mixnet_contract_common::{GatewayBond, MixNodeBond, NymNodeDetails}; use nym_task::TaskClient; -use nym_validator_client::models::{MixNodeBondAnnotated, NymNodeDescription}; +use nym_validator_client::models::{ + GatewayBondAnnotated, MixNodeBondAnnotated, NymNodeDescription, +}; +use nym_validator_client::nyxd::contract_traits::PagedMixnetQueryClient; use nym_validator_client::nyxd::error::NyxdError; use nym_validator_client::nyxd::{Paging, TendermintRpcClient, ValidatorResponse}; use nym_validator_client::{QueryHttpRpcValidatorClient, ValidatorClientError}; @@ -71,13 +74,15 @@ impl ExplorerApiTasks { .await } - async fn retrieve_all_gateways(&self) -> Result, ValidatorClientError> { + async fn retrieve_all_gateways( + &self, + ) -> Result, ValidatorClientError> { info!("About to retrieve all gateways..."); self.state .inner .validator_client .0 - .get_cached_gateways() + .get_cached_gateways_detailed_unfiltered() .await } @@ -115,7 +120,30 @@ impl ExplorerApiTasks { .await } + async fn retrieve_legacy_gateway_bonds(&self) -> Vec { + self.state + .inner + .validator_client + .0 + .nyxd + .get_all_gateways() + .await + .unwrap_or(vec![]) + } + + async fn retrieve_legacy_mixnode_bonds(&self) -> Vec { + self.state + .inner + .validator_client + .0 + .nyxd + .get_all_mixnode_bonds() + .await + .unwrap_or(vec![]) + } + async fn update_mixnode_cache(&self) { + let legacy_mixnode_bonds = self.retrieve_legacy_mixnode_bonds().await; let all_bonds = self.retrieve_all_mixnodes().await; let rewarded_nodes = self .retrieve_rewarded_mixnodes() @@ -132,7 +160,12 @@ impl ExplorerApiTasks { self.state .inner .mixnodes - .update_cache(all_bonds, rewarded_nodes, active_nodes) + .update_cache( + all_bonds, + rewarded_nodes, + active_nodes, + legacy_mixnode_bonds, + ) .await; } @@ -146,8 +179,15 @@ impl ExplorerApiTasks { } async fn update_gateways_cache(&self) { + let legacy_gateway_bonds = self.retrieve_legacy_gateway_bonds().await; match self.retrieve_all_gateways().await { - Ok(response) => self.state.inner.gateways.update_cache(response).await, + Ok(response) => { + self.state + .inner + .gateways + .update_cache(response, legacy_gateway_bonds) + .await + } Err(err) => { error!("Failed to get gateways: {err}") } diff --git a/explorer-api/src/unstable/http.rs b/explorer-api/src/unstable/http.rs new file mode 100644 index 00000000000..46e3f0aa0b2 --- /dev/null +++ b/explorer-api/src/unstable/http.rs @@ -0,0 +1,258 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::state::ExplorerApiStateContext; +use nym_explorer_api_requests::{ + NymNodeWithDescriptionAndLocation, NymNodeWithDescriptionAndLocationAndDelegations, + NymVestingAccount, PrettyDetailedGatewayBond, PrettyDetailedMixNodeBond, +}; +use nym_mixnet_contract_common::{Addr, Coin, NodeId}; +use nym_validator_client::nyxd::AccountId; +use okapi::openapi3::OpenApi; +use rocket::response::status::NotFound; +use rocket::serde::json::Json; +use rocket::serde::{Deserialize, Serialize}; +use rocket::{Route, State}; +use rocket_okapi::settings::OpenApiSettings; +use std::collections::HashMap; +use std::str::FromStr; + +pub fn unstable_temp_make_default_routes(settings: &OpenApiSettings) -> (Vec, OpenApi) { + openapi_get_routes_spec![settings: all_gateways, all_gateway_bonds, all_mixnode_bonds, all_nym_nodes, get_nym_node_by_id, get_account_by_addr] +} + +#[openapi(tag = "UNSTABLE")] +#[get("/gateways")] +pub(crate) async fn all_gateways( + state: &State, +) -> Json> { + let mut gateways = state.inner.gateways.get_legacy_detailed_gateways().await; + let mut nym_node_gateways: Vec = state + .inner + .nymnodes + .pretty_gateways() + .await + .clone() + .into_iter() + .filter(|g| { + !gateways + .iter() + .any(|g2| g.gateway.identity_key == g2.gateway.identity_key) + }) + .collect(); + gateways.append(&mut nym_node_gateways); + + Json(gateways) +} + +#[openapi(tag = "UNSTABLE")] +#[get("/legacy-gateway-bonds")] +pub(crate) async fn all_gateway_bonds( + state: &State, +) -> Json> { + Json(state.inner.gateways.get_legacy_detailed_gateways().await) +} + +#[openapi(tag = "UNSTABLE")] +#[get("/legacy-mixnode-bonds")] +pub(crate) async fn all_mixnode_bonds( + state: &State, +) -> Json> { + Json(state.inner.mixnodes.get_legacy_detailed_mixnodes().await) +} + +#[openapi(tag = "UNSTABLE")] +#[get("/nym-nodes")] +pub(crate) async fn all_nym_nodes( + state: &State, +) -> Json> { + let nodes = state + .inner + .nymnodes + .get_bonded_nymnodes_with_description_and_location() + .await; + Json(nodes.values().cloned().collect()) +} + +#[openapi(tag = "UNSTABLE")] +#[get("/nym-nodes/")] +pub(crate) async fn get_nym_node_by_id( + node_id: NodeId, + state: &State, +) -> Json> { + let nodes = state + .inner + .nymnodes + .get_bonded_nymnodes_with_description_and_location() + .await; + Json(match nodes.get(&node_id).cloned() { + None => None, + Some(node) => { + let delegations = state.inner.get_delegations_by_node(node_id).await.ok(); + Some(NymNodeWithDescriptionAndLocationAndDelegations { + node_id: node.node_id, + contract_node_type: node.contract_node_type, + description: node.description, + bond_information: node.bond_information, + rewarding_details: node.rewarding_details, + location: node.location, + delegations, + }) + } + }) +} + +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] +pub struct NyxAccountDelegationDetails { + pub node_id: NodeId, + pub delegated: Coin, + pub height: u64, + pub proxy: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] +pub struct NyxAccountDelegationRewardDetails { + pub node_id: NodeId, + pub rewards: Coin, + pub amount_staked: Coin, + pub node_still_fully_bonded: bool, +} + +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] +pub struct NyxAccountDetails { + pub address: String, + pub balances: Vec, + pub total_value: Coin, + pub delegations: Vec, + pub accumulated_rewards: Vec, + pub total_delegations: Coin, + pub claimable_rewards: Coin, + pub vesting_account: Option, + pub operator_rewards: Option, +} + +#[openapi(tag = "UNSTABLE")] +#[get("/account/")] +pub(crate) async fn get_account_by_addr( + addr: String, + state: &State, +) -> Result, NotFound> { + match AccountId::from_str(&addr) { + Ok(address) => { + let mut total_value = 0u128; + + // 1. get balances of chain tokens + let balances: Vec = state + .inner + .get_balance(&address) + .await? + .into_iter() + .map(|c| { + if c.denom == "unym" { + total_value += c.amount; + } + c.into() + }) + .collect(); + + // 2. get list of delegations (history) + let delegations: Vec = state + .inner + .get_delegations(&address) + .await? + .into_iter() + .map(|d| NyxAccountDelegationDetails { + delegated: d.amount, + height: d.height, + node_id: d.node_id, + proxy: d.proxy, + }) + .collect(); + + // 3. get the current reward for each active delegation + let mut rewards_map: HashMap<&NodeId, NyxAccountDelegationRewardDetails> = + HashMap::new(); + for d in &delegations { + if rewards_map.contains_key(&d.node_id) { + continue; + } + + if let Ok(r) = state + .inner + .get_delegation_rewards(&address, &d.node_id, &d.proxy) + .await + { + if let Some(rewards) = r.amount_earned { + rewards_map.insert( + &d.node_id, + NyxAccountDelegationRewardDetails { + node_id: d.node_id, + rewards, + amount_staked: r.amount_staked.unwrap_or_default(), + node_still_fully_bonded: r.node_still_fully_bonded, + }, + ); + } + } + } + + // 4. make the map of rewards into a vec and sum the rewards and delegations + let accumulated_rewards: Vec = + rewards_map.values().cloned().collect(); + + let mut claimable_rewards = 0u128; + let mut total_delegations = 0u128; + for r in &accumulated_rewards { + claimable_rewards += r.rewards.amount.u128(); + total_delegations += r.amount_staked.amount.u128(); + total_value += r.rewards.amount.u128(); + total_value += r.amount_staked.amount.u128(); + } + + // 5. get vesting account details (if present) + let vesting_account = state + .inner + .get_vesting_balance(&address) + .await + .unwrap_or_default(); + + if let Some(vesting_account) = vesting_account.clone() { + total_value += vesting_account.locked.amount.u128(); + total_value += vesting_account.spendable.amount.u128(); + } + + // 6. get operator rewards + + let operator_rewards: Option = if let Ok(operator_rewards_res) = + state.inner.get_operator_rewards(&address).await + { + if let Some(operator_reward_amount) = &operator_rewards_res.amount_earned { + total_value += operator_reward_amount.amount.u128(); + } + + operator_rewards_res.amount_earned + } else { + None + }; + + // 7. convert totals + + let claimable_rewards = Coin::new(claimable_rewards, "unym"); + let total_delegations = Coin::new(total_delegations, "unym"); + let total_value = Coin::new(total_value, "unym"); + + Ok(Json(NyxAccountDetails { + address: address.to_string(), + balances, + delegations, + accumulated_rewards, + claimable_rewards, + total_delegations, + total_value, + vesting_account, + operator_rewards, + })) + } + Err(_e) => Err(NotFound("Account not found".to_string())), + } +} diff --git a/explorer-api/src/nym_nodes/location.rs b/explorer-api/src/unstable/location.rs similarity index 100% rename from explorer-api/src/nym_nodes/location.rs rename to explorer-api/src/unstable/location.rs diff --git a/explorer-api/src/nym_nodes/mod.rs b/explorer-api/src/unstable/mod.rs similarity index 100% rename from explorer-api/src/nym_nodes/mod.rs rename to explorer-api/src/unstable/mod.rs diff --git a/explorer-api/src/nym_nodes/models.rs b/explorer-api/src/unstable/models.rs similarity index 72% rename from explorer-api/src/nym_nodes/models.rs rename to explorer-api/src/unstable/models.rs index 7cdbbbdee37..39254d9c071 100644 --- a/explorer-api/src/nym_nodes/models.rs +++ b/explorer-api/src/unstable/models.rs @@ -2,11 +2,13 @@ // SPDX-License-Identifier: Apache-2.0 use crate::location::{LocationCache, LocationCacheItem}; -use crate::nym_nodes::location::NymNodeLocationCache; -use crate::nym_nodes::CACHE_ENTRY_TTL; -use nym_explorer_api_requests::{Location, PrettyDetailedGatewayBond}; +use crate::unstable::location::NymNodeLocationCache; +use crate::unstable::CACHE_ENTRY_TTL; +use nym_explorer_api_requests::{ + Location, NymNodeWithDescriptionAndLocation, PrettyDetailedGatewayBond, +}; use nym_mixnet_contract_common::{Gateway, NodeId, NymNodeDetails}; -use nym_validator_client::models::NymNodeDescription; +use nym_validator_client::models::{NymNodeData, NymNodeDescription}; use std::collections::HashMap; use std::sync::Arc; use std::time::{Duration, SystemTime}; @@ -70,6 +72,54 @@ impl ThreadSafeNymNodesCache { RwLockReadGuard::map(guard, |n| &n.bonded_nym_nodes) } + pub(crate) async fn get_bonded_nymnodes_descriptions(&self) -> Vec { + let guard = self.nymnodes.read().await; + guard + .described_nodes + .values() + .map(|i| i.description.clone()) + .collect() + } + + pub(crate) async fn get_bonded_nymnodes_locations(&self) -> Vec { + let guard_locations = self.locations.read().await; + let mut locations: Vec = vec![]; + for location in guard_locations.values() { + if let Some(l) = &location.location { + locations.push(l.clone()); + } + } + locations + } + + pub(crate) async fn get_bonded_nymnodes_with_description_and_location( + &self, + ) -> HashMap { + let guard_nodes = self.nymnodes.read().await; + let guard_locations = self.locations.read().await; + + let mut map: HashMap = HashMap::new(); + + for (node_id, node) in guard_nodes.bonded_nym_nodes.clone() { + let description = guard_nodes.described_nodes.get(&node_id); + let location = guard_locations.get(&node_id); + + map.insert( + node_id, + NymNodeWithDescriptionAndLocation { + node_id, + description: description.map(|d| d.description.clone()), + location: location.and_then(|l| l.location.clone()), + contract_node_type: description.map(|d| d.contract_node_type), + bond_information: node.bond_information, + rewarding_details: node.rewarding_details, + }, + ); + } + + map + } + pub(crate) async fn get_locations(&self) -> NymNodeLocationCache { self.locations.read().await.clone() } diff --git a/explorer-nextjs/app/account/[id]/page.tsx b/explorer-nextjs/app/account/[id]/page.tsx new file mode 100644 index 00000000000..c0c482476cd --- /dev/null +++ b/explorer-nextjs/app/account/[id]/page.tsx @@ -0,0 +1,407 @@ +'use client' + +import * as React from 'react' +import {Alert, AlertTitle, Box, Button, Chip, CircularProgress, Grid, Tooltip, Typography} from '@mui/material' +import { useParams } from 'next/navigation' +import { useMainContext } from '@/app/context/main' +import { Title } from '@/app/components/Title' +import { MaterialReactTable, MRT_ColumnDef, useMaterialReactTable } from "material-react-table"; +import { useMemo } from "react"; +import { humanReadableCurrencyToString } from "@/app/utils/currency"; +import Table from '@mui/material/Table'; +import TableBody from '@mui/material/TableBody'; +import TableCell from '@mui/material/TableCell'; +import TableContainer from '@mui/material/TableContainer'; +import TableRow from '@mui/material/TableRow'; +import Paper from '@mui/material/Paper'; +import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline'; +import WarningAmberIcon from '@mui/icons-material/WarningAmber'; +import { PieChart } from '@mui/x-charts/PieChart'; +import { useTheme } from "@mui/material/styles"; +import { useIsMobile } from "@/app/hooks"; +import { StyledLink } from "@/app/components"; + +const AccumulatedRewards = ({account}: { account?: any}) => { + const columns = useMemo< + MRT_ColumnDef[] + >(() => { + return [ + { + id: 'accumulated-rewards-data', + header: 'Accumulated Rewards Data', + columns: [ + { + id: 'node_id', + accessorKey: 'node_id', + header: 'Node ID', + size: 150, + Cell: ({ row }) => ( + {row.original.node_id} + ), + }, + { + id: 'node_still_fully_bonded', + accessorKey: 'node_still_fully_bonded', + header: 'Node still bonded?', + width: 150, + Cell: ({ row }) => ( + <>{row.original.node_still_fully_bonded ? : + theme.palette.warning.main }}> + + Unbonded + } + ) + }, + { + id: 'amount_staked', + accessorKey: 'amount_staked', + header: 'Amount', + width: 150, + Cell: ({ row }) => ( + <>{humanReadableCurrencyToString(row.original.amount_staked)} + ) + }, + { + id: 'rewards', + accessorKey: 'rewards', + header: 'Rewards', + width: 150, + Cell: ({ row }) => ( + {humanReadableCurrencyToString(row.original.rewards)} + ) + }, + ], + }, + ] + }, []) + + const table = useMaterialReactTable({ + columns, + data: account?.accumulated_rewards || [], + enableFullScreenToggle: false, + }) + + return (); +} + +const DelegationHistory = ({account}: { account?: any}) => { + const columns = useMemo< + MRT_ColumnDef[] + >(() => { + return [ + { + id: 'delegation-history-data', + header: 'Delegation History', + columns: [ + { + id: 'node_id', + accessorKey: 'node_id', + header: 'Node ID', + size: 150, + }, + { + id: 'delegated', + accessorKey: 'delegated', + header: 'Amount', + width: 150, + Cell: ({ row }) => ( + <>{humanReadableCurrencyToString(row.original.delegated)} + ) + }, + { + id: 'height', + accessorKey: 'height', + header: 'Delegated at height', + width: 150, + Cell: ({ row }) => ( + <>{row.original.height} + ) + }, + ], + }, + ] + }, []) + + const table = useMaterialReactTable({ + columns, + data: account?.delegations || [], + enableFullScreenToggle: false, + }) + + return (); +} + + +/** + * Shows account details + */ +const PageAccountWithState = ({ account }: { + account?: any; +}) => { + const theme = useTheme(); + const isMobile = useIsMobile(); + + const pieChartData = React.useMemo(() => { + if(!account) { + return []; + } + + const parts = []; + + const nymBalance = Number.parseFloat(account.balances.find((b: any) => b.denom === "unym")?.amount || "0") / 1e6; + + if(nymBalance > 0) { + parts.push({label: "Spendable", value: nymBalance, color: theme.palette.primary.main}); + } + + if(account.vesting_account) { + if (`${account.vesting_account.locked?.amount}` !== "0") { + const value = Number.parseFloat(account.vesting_account.locked.amount) / 1e6; + if(value > 0) { + parts.push({ + label: "Vesting locked", + value, + color: 'red' + }); + } + } + if (`${account.vesting_account.spendable?.amount}` !== "0") { + const value = Number.parseFloat(account.vesting_account.spendable.amount) / 1e6; + if(value > 0) { + parts.push({ + label: "Vesting spendable", + value, + color: theme.palette.primary.light + }); + } + } + } + + if (account.claimable_rewards &&`${account.claimable_rewards.amount}` !== "0") { + const value = Number.parseFloat(account.claimable_rewards.amount) / 1e6; + if(value > 0) { + parts.push({ + label: "Claimable delegation rewards", + value, + color: theme.palette.success.light + }); + } + } + if (account.operator_rewards && `${account.operator_rewards.amount}` !== "0") { + const value = Number.parseFloat(account.operator_rewards.amount) / 1e6; + if(value > 0) { + parts.push({ + label: "Claimable operator rewards", + value, + color: theme.palette.success.dark + }); + } + } + if (account.total_delegations && `${account.total_delegations.amount}` !== "0") { + const value = Number.parseFloat(account.total_delegations.amount) / 1e6; + if(value > 0) { + parts.push({ + label: "Total delegations", + value, + color: '#888' + }); + } + } + + return parts; + }, [account]); + + return ( + + + + </Box> + + <Box mt={4} sx={{ maxWidth: "600px" }}> + <PieChart + series={[ + { + data: pieChartData, + innerRadius: 40, + outerRadius: 80, + cy: isMobile ? 200 : undefined, + }, + ]} + height={300} + slotProps={isMobile ? { + legend: { position: { vertical: "top", horizontal: "right" } } + } : undefined} + /> + </Box> + + <Box mt={4}> + <TableContainer component={Paper} sx={{ maxWidth: "400px" }}> + <Table> + <TableBody> + <TableRow sx={{ color: theme => theme.palette.primary.main }}> + <TableCell component="th" scope="row" sx={{ color: "inherit" }}> + <strong>Spendable Balance</strong> + </TableCell> + <TableCell align="right" sx={{ color: "inherit" }}> + {account.balances.map((b: any) => (<strong key={`balance-${b.denom}`}>{humanReadableCurrencyToString(b)}<br/></strong>))} + </TableCell> + </TableRow> + <TableRow> + <TableCell component="th" scope="row"> + Total delegations + </TableCell> + <TableCell align="right"> + {humanReadableCurrencyToString(account.total_delegations)} + </TableCell> + </TableRow> + {account.claimable_rewards && <TableRow sx={{ color: theme => theme.palette.success.light }}> + <TableCell component="th" scope="row" sx={{ color: "inherit" }}> + Claimable delegation rewards + </TableCell> + <TableCell align="right" sx={{ color: "inherit" }}> + {humanReadableCurrencyToString(account.claimable_rewards)} + </TableCell> + </TableRow>} + {account.operator_rewards && `${account.operator_rewards.amount}` !== "0" && <TableRow sx={{ color: theme => theme.palette.success.light }}> + <TableCell component="th" scope="row" sx={{ color: "inherit" }}> + Claimable operator rewards + </TableCell> + <TableCell align="right" sx={{ color: "inherit" }}> + {humanReadableCurrencyToString(account.operator_rewards)} + </TableCell> + </TableRow>} + {account.vesting_account && ( + <> + <TableRow> + <TableCell component="th" scope="row" colSpan={2}> + Vesting account + </TableCell> + </TableRow> + {`${account.vesting_account.locked.amount}` !== "0" && + <TableRow> + <TableCell component="th" scope="row" sx={{ pl: 4 }}> + Locked + </TableCell> + <TableCell align="right" sx={{ color: "inherit" }}> + {humanReadableCurrencyToString(account.vesting_account.locked)} + </TableCell> + </TableRow> + } + {`${account.vesting_account.vested.amount}` !== "0" && + <TableRow> + <TableCell component="th" scope="row" sx={{ pl: 4 }}> + Vested + </TableCell> + <TableCell align="right" sx={{ color: "inherit" }}> + {humanReadableCurrencyToString(account.vesting_account.vested)} + </TableCell> + </TableRow> + } + {`${account.vesting_account.vesting.amount}` !== "0" && + <TableRow> + <TableCell component="th" scope="row" sx={{ pl: 4 }}> + Vesting + </TableCell> + <TableCell align="right" sx={{ color: "inherit" }}> + {humanReadableCurrencyToString(account.vesting_account.vesting)} + </TableCell> + </TableRow> + } + {`${account.vesting_account.spendable.amount}` !== "0" && + <TableRow> + <TableCell component="th" scope="row" sx={{ pl: 4 }}> + Spendable + </TableCell> + <TableCell align="right" sx={{ color: "inherit" }}> + {humanReadableCurrencyToString(account.vesting_account.spendable)} + </TableCell> + </TableRow> + } + </> + )} + <TableRow> + <TableCell component="th" scope="row" sx={{ color: "inherit" }}> + <h3>Total value</h3> + </TableCell> + <TableCell align="right" sx={{ color: "inherit" }}> + <h3>{humanReadableCurrencyToString(account.total_value)}</h3> + </TableCell> + </TableRow> + </TableBody> + </Table> + </TableContainer> + </Box> + <Box mt={4}> + <AccumulatedRewards account={account}/> + </Box> + <Box mt={4}> + <DelegationHistory account={account}/> + </Box> + </Box> + ) +} + +/** + * Guard component to handle loading and not found states + */ +const PageAccountDetailGuard = ({ account } : { account: string }) => { + const [accountDetails, setAccountDetails] = React.useState<any>(); + const [isLoading, setLoading] = React.useState<boolean>(true); + const [error, setError] = React.useState<string>(); + const { fetchAccountById } = useMainContext() + const { id } = useParams() + + React.useEffect(() => { + setLoading(true); + (async () => { + if(typeof(id) === "string") { + try { + const res = await fetchAccountById(account); + setAccountDetails(res); + } catch(e: any) { + setError(e.message); + } + finally { + setLoading(false); + } + } + })(); + }, [id]) + + if (isLoading) { + return <CircularProgress /> + } + + // loaded, but not found + if (error) { + return ( + <Alert severity="warning"> + <AlertTitle>Account not found</AlertTitle> + Sorry, we could not find the account <code>{id || ''}</code> + </Alert> + ) + } + + return <PageAccountWithState account={accountDetails} /> +} + +/** + * Wrapper component that adds the account details based on the `id` in the address URL + */ +const PageAccountDetail = () => { + const { id } = useParams() + + if (!id || typeof id !== 'string') { + return ( + <Alert severity="error">Oh no! Could not find that account</Alert> + ) + } + + return ( + <PageAccountDetailGuard account={id} /> + ) +} + +export default PageAccountDetail diff --git a/explorer-nextjs/app/api/constants.ts b/explorer-nextjs/app/api/constants.ts index e83aca4d027..515655c1840 100644 --- a/explorer-nextjs/app/api/constants.ts +++ b/explorer-nextjs/app/api/constants.ts @@ -5,28 +5,33 @@ export const NYM_API_BASE_URL = process.env.NEXT_PUBLIC_NYM_API_URL || 'https:// export const NYX_RPC_BASE_URL = process.env.NEXT_PUBLIC_NYX_RPC_BASE_URL || 'https://rpc.nymtech.net'; export const VALIDATOR_BASE_URL = process.env.NEXT_PUBLIC_VALIDATOR_URL || 'https://rpc.nymtech.net'; -export const BIG_DIPPER = process.env.NEXT_PUBLIC_BIG_DIPPER_URL || 'https://nym.explorers.guru'; +export const BLOCK_EXPLORER_BASE_URL = process.env.NEXT_PUBLIC_BIG_DIPPER_URL || 'https://nym.explorers.guru'; // specific API routes export const OVERVIEW_API = `${API_BASE_URL}/overview`; export const MIXNODE_PING = `${API_BASE_URL}/ping`; export const MIXNODES_API = `${API_BASE_URL}/mix-nodes`; export const MIXNODE_API = `${API_BASE_URL}/mix-node`; -export const GATEWAYS_EXPLORER_API = `${API_BASE_URL}/gateways`; -export const GATEWAYS_API = `${NYM_API_BASE_URL}/api/v1/status/gateways/detailed`; export const VALIDATORS_API = `${NYX_RPC_BASE_URL}/validators`; export const BLOCK_API = `${NYX_RPC_BASE_URL}/block`; export const COUNTRY_DATA_API = `${API_BASE_URL}/countries`; export const UPTIME_STORY_API = `${NYM_API_BASE_URL}/api/v1/status/mixnode`; // add ID then '/history' to this. export const UPTIME_STORY_API_GATEWAY = `${NYM_API_BASE_URL}/api/v1/status/gateway`; // add ID then '/history' or '/report' to this export const SERVICE_PROVIDERS = `${API_BASE_URL}/service-providers`; +export const TEMP_UNSTABLE_NYM_NODES = `${API_BASE_URL}/tmp/unstable/nym-nodes`; +export const TEMP_UNSTABLE_ACCOUNT = `${API_BASE_URL}/tmp/unstable/account`; +export const NYM_API_NODE_UPTIME = `${NYM_API_BASE_URL}/api/v1/nym-nodes/uptime-history`; +export const NYM_API_NODE_PERFORMANCE = `${NYM_API_BASE_URL}/api/v1/nym-nodes/performance-history`; + +export const LEGACY_MIXNODES_API = `${API_BASE_URL}/tmp/unstable/legacy-mixnode-bonds`; +export const LEGACY_GATEWAYS_API = `${API_BASE_URL}/tmp/unstable/legacy-gateway-bonds`; // errors export const MIXNODE_API_ERROR = "We're having trouble finding that record, please try again or Contact Us."; export const NYM_WEBSITE = 'https://nymtech.net'; -export const NYM_BIG_DIPPER = 'https://mixnet.explorers.guru'; +export const EXPLORER_FOR_ACCOUNTS = ''; // set to empty to use this Nym Explorer and NOT an external one export const NYM_MIXNET_CONTRACT = process.env.NYM_MIXNET_CONTRACT || 'n17srjznxl9dvzdkpwpw24gg668wc73val88a6m5ajg6ankwvz9wtst0cznr'; diff --git a/explorer-nextjs/app/api/index.ts b/explorer-nextjs/app/api/index.ts index a2b134d4aeb..ae190de9b9d 100644 --- a/explorer-nextjs/app/api/index.ts +++ b/explorer-nextjs/app/api/index.ts @@ -3,7 +3,6 @@ import { API_BASE_URL, BLOCK_API, COUNTRY_DATA_API, - GATEWAYS_API, UPTIME_STORY_API_GATEWAY, MIXNODE_API, MIXNODE_PING, @@ -12,7 +11,11 @@ import { UPTIME_STORY_API, VALIDATORS_API, SERVICE_PROVIDERS, - GATEWAYS_EXPLORER_API, + TEMP_UNSTABLE_NYM_NODES, + NYM_API_NODE_UPTIME, + NYM_API_NODE_PERFORMANCE, + TEMP_UNSTABLE_ACCOUNT, + LEGACY_MIXNODES_API, LEGACY_GATEWAYS_API, } from './constants'; import { @@ -59,7 +62,14 @@ export class Api { return cache; } const res = await fetch(`${OVERVIEW_API}/summary`); - const json = await res.json(); + const json: SummaryOverviewResponse = await res.json(); + + if (json.nymnodes?.roles) { + json.mixnodes.count += json.nymnodes.roles.mixnode; + json.gateways.count += json.nymnodes.roles.entry; + json.gateways.count += Math.max(json.nymnodes.roles.exit_ipr, json.nymnodes.roles.exit_nr); + } + storeInCache('overview-summary', JSON.stringify(json)); return json; }; @@ -70,7 +80,7 @@ export class Api { return cachedMixnodes; } - const res = await fetch(MIXNODES_API); + const res = await fetch(LEGACY_MIXNODES_API); const json = await res.json(); storeInCache('mixnodes', JSON.stringify(json)); return json; @@ -98,17 +108,21 @@ export class Api { return response.json(); }; - static fetchGateways = async (): Promise<GatewayBond[]> => { - const res = await fetch(GATEWAYS_API); - const gatewaysAnnotated: GatewayBondAnnotated[] = await res.json(); - const res2 = await fetch(GATEWAYS_EXPLORER_API); - const locatedGateways: LocatedGateway[] = await res2.json(); - const locatedGatewaysByOwner = keyBy(locatedGateways, 'owner'); - return gatewaysAnnotated.map(({ gateway_bond, node_performance }) => ({ - ...gateway_bond, - node_performance, - location: locatedGatewaysByOwner[gateway_bond.owner]?.location, - })); + static fetchGateways = async (): Promise<LocatedGateway[]> => { + // const res = await fetch(GATEWAYS_API); + // const gatewaysAnnotated: GatewayBondAnnotated[] = await res.json(); + // const res2 = await fetch(GATEWAYS_EXPLORER_API); + // const locatedGateways: LocatedGateway[] = await res2.json(); + // const locatedGatewaysByOwner = keyBy(locatedGateways, 'owner'); + // return gatewaysAnnotated.map(({ gateway_bond, node_performance }) => ({ + // ...gateway_bond, + // node_performance, + // location: locatedGatewaysByOwner[gateway_bond.owner]?.location, + // })); + + const res = await fetch(LEGACY_GATEWAYS_API); + const locatedGateways: LocatedGateway[] = await res.json(); + return locatedGateways; }; static fetchGatewayUptimeStoryById = async (id: string): Promise<UptimeStoryResponse> => @@ -165,6 +179,36 @@ export class Api { const json = await res.json(); return json; }; + + static fetchNodes = async () => { + const res = await fetch(TEMP_UNSTABLE_NYM_NODES); + const json = await res.json(); + return json; + } + + static fetchNodeById = async (id: number) => { + const res = await fetch(`${TEMP_UNSTABLE_NYM_NODES}/${id}`); + const json = await res.json(); + return json; + } + + static fetchNymNodeUptimeHistoryById = async (id: number | string) => { + const res = await fetch(`${NYM_API_NODE_UPTIME}/${id}`) + const json = await res.json(); + return json; + } + + static fetchNymNodePerformanceById = async (id: number | string) => { + const res = await fetch(`${NYM_API_NODE_PERFORMANCE}/${id}`) + const json = await res.json(); + return json; + } + + static fetchAccountById = async (id: string) => { + const res = await fetch(`${TEMP_UNSTABLE_ACCOUNT}/${id}`); + const json = await res.json(); + return json; + } } export const getEnvironment = (): Environment => { diff --git a/explorer-nextjs/app/components/DetailTable.tsx b/explorer-nextjs/app/components/DetailTable.tsx index 73868b8ce9e..b068a5c8524 100644 --- a/explorer-nextjs/app/components/DetailTable.tsx +++ b/explorer-nextjs/app/components/DetailTable.tsx @@ -18,6 +18,7 @@ import { unymToNym } from '@/app/utils/currency' import { GatewayEnrichedRowType } from './Gateways/Gateways' import { MixnodeRowType } from './MixNodes' import { StakeSaturationProgressBar } from './MixNodes/Economics/StakeSaturationProgressBar' +import {EXPLORER_FOR_ACCOUNTS} from "@/app/api/constants"; export type ColumnsType = { field: string @@ -57,7 +58,7 @@ function formatCellValues(val: string | number, field: string) { underline="none" color="inherit" target="_blank" - href={`https://mixnet.explorers.guru/account/${val}`} + href={`${EXPLORER_FOR_ACCOUNTS}/account/${val}`} > {val} </Link> @@ -74,7 +75,7 @@ function formatCellValues(val: string | number, field: string) { export const DetailTable: FCWithChildren<{ tableName: string columnsData: ColumnsType[] - rows: MixnodeRowType[] | GatewayEnrichedRowType[] + rows: MixnodeRowType[] | GatewayEnrichedRowType[] | any[] }> = ({ tableName, columnsData, rows }: UniversalTableProps) => { const theme = useTheme() return ( diff --git a/explorer-nextjs/app/components/Gateways/Gateways.ts b/explorer-nextjs/app/components/Gateways/Gateways.ts index d3aead4f13f..f4dbf2743cb 100644 --- a/explorer-nextjs/app/components/Gateways/Gateways.ts +++ b/explorer-nextjs/app/components/Gateways/Gateways.ts @@ -1,4 +1,4 @@ -import { GatewayResponse, GatewayBond, GatewayReportResponse } from '@/app/typeDefs/explorer-api'; +import {GatewayResponse, GatewayBond, GatewayReportResponse, LocatedGateway} from '@/app/typeDefs/explorer-api'; import { toPercentInteger } from '@/app/utils'; export type GatewayRowType = { @@ -9,7 +9,7 @@ export type GatewayRowType = { host: string; location: string; version: string; - node_performance: number; +// node_performance: number; }; export type GatewayEnrichedRowType = GatewayRowType & { @@ -30,11 +30,11 @@ export function gatewayToGridRow(arrayOfGateways: GatewayResponse): GatewayRowTy bond: gw.pledge_amount.amount || 0, host: gw.gateway.host || '', version: gw.gateway.version || '', - node_performance: toPercentInteger(gw.node_performance.last_24h), +// node_performance: toPercentInteger(gw.node_performance.last_24h), })); } -export function gatewayEnrichedToGridRow(gateway: GatewayBond, report: GatewayReportResponse): GatewayEnrichedRowType { +export function gatewayEnrichedToGridRow(gateway: LocatedGateway, report: GatewayReportResponse): GatewayEnrichedRowType { return { id: gateway.owner, owner: gateway.owner, @@ -47,6 +47,6 @@ export function gatewayEnrichedToGridRow(gateway: GatewayBond, report: GatewayRe mixPort: gateway.gateway.mix_port || 0, routingScore: `${report.most_recent}%`, avgUptime: `${report.last_day || report.last_hour}%`, - node_performance: toPercentInteger(gateway.node_performance.most_recent), +// node_performance: toPercentInteger(gateway.node_performance.most_recent), }; } diff --git a/explorer-nextjs/app/components/Nav/DesktopNav.tsx b/explorer-nextjs/app/components/Nav/DesktopNav.tsx index abc4c0901a1..618037ea040 100644 --- a/explorer-nextjs/app/components/Nav/DesktopNav.tsx +++ b/explorer-nextjs/app/components/Nav/DesktopNav.tsx @@ -24,6 +24,7 @@ import { DarkLightSwitchDesktop } from '@/app/components/Switch' import { Footer } from '@/app/components/Footer' import { ConnectKeplrWallet } from '@/app/components/Wallet/ConnectKeplrWallet' import { usePathname, useRouter } from 'next/navigation' +import {SearchToolbar} from "@/app/components/Nav/Search"; const drawerWidth = 255 const bannerHeight = 80 @@ -292,6 +293,9 @@ export const Nav: FCWithChildren = ({ children }) => { display: 'flex', }} > + <Box> + <SearchToolbar/> + </Box> <Box sx={{ display: 'flex', diff --git a/explorer-nextjs/app/components/Nav/MobileNav.tsx b/explorer-nextjs/app/components/Nav/MobileNav.tsx index a2e23e620e9..91cb766ffcc 100644 --- a/explorer-nextjs/app/components/Nav/MobileNav.tsx +++ b/explorer-nextjs/app/components/Nav/MobileNav.tsx @@ -22,6 +22,7 @@ import { ExpandableButton } from './DesktopNav' import { ConnectKeplrWallet } from '../Wallet/ConnectKeplrWallet' import { NetworkTitle } from '../NetworkTitle' import { originalNavOptions } from '@/app/context/nav' +import {SearchToolbar} from "@/app/components/Nav/Search"; export const MobileNav: FCWithChildren = ({ children }) => { const theme = useTheme() @@ -70,7 +71,15 @@ export const MobileNav: FCWithChildren = ({ children }) => { </IconButton> {!isSmallMobile && <NetworkTitle />} </Box> - <ConnectKeplrWallet /> + <Box sx={{ + alignItems: 'center', + display: 'flex', + }}> + <Box mr={0.5}> + <SearchToolbar/> + </Box> + <ConnectKeplrWallet /> + </Box> </Toolbar> </AppBar> <Drawer diff --git a/explorer-nextjs/app/components/Nav/Search.tsx b/explorer-nextjs/app/components/Nav/Search.tsx new file mode 100644 index 00000000000..888125b84a0 --- /dev/null +++ b/explorer-nextjs/app/components/Nav/Search.tsx @@ -0,0 +1,76 @@ +import React from "react"; +import { styled, alpha } from '@mui/material/styles'; +import AppBar from '@mui/material/AppBar'; +import Box from '@mui/material/Box'; +import Toolbar from '@mui/material/Toolbar'; +import IconButton from '@mui/material/IconButton'; +import Typography from '@mui/material/Typography'; +import InputBase from '@mui/material/InputBase'; +import MenuIcon from '@mui/icons-material/Menu'; +import SearchIcon from '@mui/icons-material/Search'; +import {useRouter} from "next/navigation"; + +const Search = styled('div')(({ theme }) => ({ + position: 'relative', + borderRadius: theme.shape.borderRadius, + backgroundColor: alpha(theme.palette.common.white, 0.15), + '&:hover': { + backgroundColor: alpha(theme.palette.common.white, 0.25), + }, + marginLeft: 0, + width: '100%', + [theme.breakpoints.up('sm')]: { + marginLeft: theme.spacing(1), + width: 'auto', + }, +})); + +const SearchIconWrapper = styled('div')(({ theme }) => ({ + padding: theme.spacing(0, 2), + height: '100%', + position: 'absolute', + pointerEvents: 'none', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', +})); + +const StyledInputBase = styled(InputBase)(({ theme }) => ({ + color: 'inherit', + width: '100%', + '& .MuiInputBase-input': { + padding: theme.spacing(1, 1, 1, 0), + // vertical padding + font size from searchIcon + paddingLeft: `calc(1em + ${theme.spacing(4)})`, + [theme.breakpoints.up('sm')]: { + width: '30ch', + }, + }, +})); + +export const SearchToolbar = () => { + const [search, setSearch] = React.useState<string>(); + const router = useRouter(); + const handleSubmit = async (e: React.SyntheticEvent<HTMLFormElement>) => { + e.preventDefault(); + if(search?.trim().length) { + router.push(`/account/${search.trim()}`); + } + } + return ( + <Search> + <SearchIconWrapper> + <SearchIcon /> + </SearchIconWrapper> + <form onSubmit={handleSubmit}> + <StyledInputBase + placeholder="Search for account id…" + inputProps={{ 'aria-label': 'search' }} + onChange={(event: React.ChangeEvent<HTMLInputElement>) => { + setSearch(event.target.value); + }} + /> + </form> + </Search> + ); +} \ No newline at end of file diff --git a/explorer-nextjs/app/components/StyledLink.tsx b/explorer-nextjs/app/components/StyledLink.tsx index 4402b1d0d63..36dd7981807 100644 --- a/explorer-nextjs/app/components/StyledLink.tsx +++ b/explorer-nextjs/app/components/StyledLink.tsx @@ -4,7 +4,7 @@ import Link from 'next/link' type StyledLinkProps = { to: string - children: string + children: React.ReactNode target?: React.HTMLAttributeAnchorTarget dataTestId?: string color?: string diff --git a/explorer-nextjs/app/context/main.tsx b/explorer-nextjs/app/context/main.tsx index 4e2abfb9b74..0494b87749c 100644 --- a/explorer-nextjs/app/context/main.tsx +++ b/explorer-nextjs/app/context/main.tsx @@ -26,6 +26,7 @@ interface StateData { gateways?: ApiState<GatewayResponse> globalError?: string | undefined mixnodes?: ApiState<MixNodeResponse> + nodes?: ApiState<any> mode: PaletteMode validators?: ApiState<ValidatorsResponse> environment?: Environment @@ -37,6 +38,9 @@ interface StateApi { status?: MixnodeStatus ) => Promise<MixNodeResponse | undefined> filterMixnodes: (filters: any, status: any) => void + fetchNodes: () => Promise<any> + fetchNodeById: (id: number) => Promise<any> + fetchAccountById: (accountAddr: string) => Promise<any> toggleMode: () => void } @@ -47,6 +51,9 @@ export const MainContext = React.createContext<State>({ toggleMode: () => undefined, filterMixnodes: () => null, fetchMixnodes: () => Promise.resolve(undefined), + fetchNodes: async () => undefined, + fetchNodeById: async () => undefined, + fetchAccountById: async () => undefined, }) export const useMainContext = (): React.ContextType<typeof MainContext> => @@ -65,6 +72,7 @@ export const MainContextProvider: FCWithChildren = ({ children }) => { // various APIs for Overview page const [summaryOverview, setSummaryOverview] = React.useState<ApiState<SummaryOverviewResponse>>() + const [nodes, setNodes] = React.useState<ApiState<any>>() const [mixnodes, setMixnodes] = React.useState<ApiState<MixNodeResponse>>() const [gateways, setGateways] = React.useState<ApiState<GatewayResponse>>() const [validators, setValidators] = @@ -92,13 +100,11 @@ export const MainContextProvider: FCWithChildren = ({ children }) => { } } - const fetchMixnodes = async (status?: MixnodeStatus) => { + const fetchMixnodes = async () => { let data setMixnodes((d) => ({ ...d, isLoading: true })) try { - data = status - ? await Api.fetchMixnodesActiveSetByStatus(status) - : await Api.fetchMixnodes() + data = await Api.fetchMixnodes() setMixnodes({ data, isLoading: false }) } catch (error) { setMixnodes({ @@ -114,9 +120,7 @@ export const MainContextProvider: FCWithChildren = ({ children }) => { status?: MixnodeStatus ) => { setMixnodes((d) => ({ ...d, isLoading: true })) - const mxns = status - ? await Api.fetchMixnodesActiveSetByStatus(status) - : await Api.fetchMixnodes() + const mxns = await Api.fetchMixnodes() const filtered = mxns?.filter( (m) => @@ -205,6 +209,38 @@ export const MainContextProvider: FCWithChildren = ({ children }) => { } } + const fetchNodes = async () => { + setNodes({ data: undefined, isLoading: true }) + try { + const res = await Api.fetchNodes(); + res.forEach((node: any) => node.total_stake = + Math.round(Number.parseFloat(node.rewarding_details?.operator || "0") + + Number.parseFloat(node.rewarding_details?.delegates || "0")) + ); + setNodes({ + data: res.sort((a: any, b: any) => b.total_stake - a.total_stake), + isLoading: false, + }) + } catch (error) { + setNodes({ + error: + error instanceof Error + ? error + : new Error('Service provider api fail'), + isLoading: false, + }) + } }; + + const fetchNodeById = async (id: number) => { + const res = await Api.fetchNodeById(id); + return res; + }; + + const fetchAccountById = async (id: string) => { + const res = await Api.fetchAccountById(id); + return res; + }; + React.useEffect(() => { if (environment === 'mainnet') { fetchServiceProviders() @@ -231,12 +267,16 @@ export const MainContextProvider: FCWithChildren = ({ children }) => { globalError, mixnodes, mode, + nodes, summaryOverview, validators, serviceProviders, toggleMode, fetchMixnodes, filterMixnodes, + fetchNodes, + fetchNodeById, + fetchAccountById, }), [ environment, @@ -246,6 +286,7 @@ export const MainContextProvider: FCWithChildren = ({ children }) => { globalError, mixnodes, mode, + nodes, summaryOverview, validators, serviceProviders, diff --git a/explorer-nextjs/app/context/nav.tsx b/explorer-nextjs/app/context/nav.tsx index b2be00a5270..e41882edbba 100644 --- a/explorer-nextjs/app/context/nav.tsx +++ b/explorer-nextjs/app/context/nav.tsx @@ -2,7 +2,7 @@ import * as React from 'react' import { DelegateIcon } from '@/app/icons/DelevateSVG' -import { BIG_DIPPER } from '@/app/api/constants' +import { BLOCK_EXPLORER_BASE_URL } from '@/app/api/constants' import { OverviewSVG } from '@/app/icons/OverviewSVG' import { NodemapSVG } from '@/app/icons/NodemapSVG' import { NetworkComponentsSVG } from '@/app/icons/NetworksSVG' @@ -27,23 +27,23 @@ export const originalNavOptions: NavOptionType[] = [ title: 'Network Components', Icon: <NetworkComponentsSVG />, nested: [ + { + url: '/network-components/nodes', + title: 'Nodes', + }, { url: '/network-components/mixnodes', - title: 'Mixnodes', + title: 'Mixnodes (legacy)', }, { url: '/network-components/gateways', - title: 'Gateways', + title: 'Gateways (legacy)', }, { - url: `${BIG_DIPPER}/validators`, + url: `${BLOCK_EXPLORER_BASE_URL}/validators`, title: 'Validators', isExternal: true, }, - { - url: '/network-components/service-providers', - title: 'Service Providers', - }, ], }, { diff --git a/explorer-nextjs/app/context/node.tsx b/explorer-nextjs/app/context/node.tsx new file mode 100644 index 00000000000..2aa885439c2 --- /dev/null +++ b/explorer-nextjs/app/context/node.tsx @@ -0,0 +1,77 @@ +'use client' + +import * as React from 'react' +import { + ApiState, + NymNodeReportResponse, + UptimeStoryResponse, +} from '@/app/typeDefs/explorer-api' +import { Api } from '@/app/api' +import { useApiState } from './hooks' + +/** + * This context provides the state for a single gateway by identity key. + */ + +interface NymNodeState { + uptimeReport?: ApiState<NymNodeReportResponse> + uptimeHistory?: ApiState<UptimeStoryResponse> +} + +export const NymNodeContext = React.createContext<NymNodeState>({}) + +export const useNymNodeContext = (): React.ContextType<typeof NymNodeContext> => + React.useContext<NymNodeState>(NymNodeContext) + +/** + * Provides a state context for a gateway by identity + * @param gatewayIdentityKey The identity key of the gateway + */ +export const NymNodeContextProvider = ({ + nymNodeId, + children, +}: { + nymNodeId: string + children: JSX.Element +}) => { + const [uptimeReport, fetchUptimeReportById, clearUptimeReportById] = + useApiState<any>( + nymNodeId, + Api.fetchNymNodePerformanceById, + 'Failed to fetch gateway uptime report by id' + ) + + const [uptimeHistory, fetchUptimeHistory, clearUptimeHistory] = + useApiState<UptimeStoryResponse>( + nymNodeId, + async (arg) => { + const res = await Api.fetchNymNodeUptimeHistoryById(arg); + const uptimeHistory: UptimeStoryResponse = { + history: res.history.data, + identity: '', + owner: '', + } + return uptimeHistory; + }, + 'Failed to fetch gateway uptime history' + ) + + React.useEffect(() => { + // when the identity key changes, remove all previous data + clearUptimeReportById() + clearUptimeHistory() + Promise.all([fetchUptimeReportById(), fetchUptimeHistory()]) + }, [nymNodeId]) + + const state = React.useMemo<NymNodeState>( + () => ({ + uptimeReport, + uptimeHistory, + }), + [uptimeReport, uptimeHistory] + ) + + return ( + <NymNodeContext.Provider value={state}>{children}</NymNodeContext.Provider> + ) +} diff --git a/explorer-nextjs/app/network-components/gateways/[id]/page.tsx b/explorer-nextjs/app/network-components/gateways/[id]/page.tsx index 8f4b99e6f98..87f779872a9 100644 --- a/explorer-nextjs/app/network-components/gateways/[id]/page.tsx +++ b/explorer-nextjs/app/network-components/gateways/[id]/page.tsx @@ -3,7 +3,7 @@ import * as React from 'react' import { Alert, AlertTitle, Box, CircularProgress, Grid } from '@mui/material' import { useParams } from 'next/navigation' -import { GatewayBond } from '@/app/typeDefs/explorer-api' +import {GatewayBond, LocatedGateway} from '@/app/typeDefs/explorer-api' import { ColumnsType, DetailTable } from '@/app/components/DetailTable' import { gatewayEnrichedToGridRow, @@ -32,13 +32,6 @@ const columns: ColumnsType[] = [ title: 'Bond', headerAlign: 'left', }, - { - field: 'node_performance', - title: 'Routing Score', - headerAlign: 'left', - tooltipInfo: - "Gateway's most recent score (measured in the last 15 minutes). Routing score is relative to that of the network. Each time a gateway is tested, the test packets have to go through the full path of the network (gateway + 3 nodes). If a node in the path drop packets it will affect the score of the gateway and other nodes in the test", - }, { field: 'avgUptime', title: 'Avg. Score', @@ -74,7 +67,7 @@ const columns: ColumnsType[] = [ const PageGatewayDetailsWithState = ({ selectedGateway, }: { - selectedGateway: GatewayBond | undefined + selectedGateway: LocatedGateway | undefined }) => { const [enrichGateway, setEnrichGateway] = React.useState<GatewayEnrichedRowType>() @@ -97,7 +90,13 @@ const PageGatewayDetailsWithState = ({ return ( <Box component="main"> - <Title text="Gateway Detail" /> + <Title text="Legacy Gateway Detail" /> + + <Alert variant="filled" severity="warning" sx={{ my : 2, pt: 2 }}> + <AlertTitle> + Please update to the latest <code>nym-node</code> binary and migrate your bond and delegations from the wallet + </AlertTitle> + </Alert> <Grid container> <Grid item xs={12}> @@ -146,7 +145,7 @@ const PageGatewayDetailsWithState = ({ * Guard component to handle loadingW and not found states */ const PageGatewayDetailGuard = () => { - const [selectedGateway, setSelectedGateway] = React.useState<GatewayBond>() + const [selectedGateway, setSelectedGateway] = React.useState<LocatedGateway>() const { gateways } = useMainContext() const { id } = useParams() diff --git a/explorer-nextjs/app/network-components/gateways/page.tsx b/explorer-nextjs/app/network-components/gateways/page.tsx index d0306e73d09..c2660ce3870 100644 --- a/explorer-nextjs/app/network-components/gateways/page.tsx +++ b/explorer-nextjs/app/network-components/gateways/page.tsx @@ -18,7 +18,7 @@ import { CustomColumnHeading } from '@/app/components/CustomColumnHeading' import { Title } from '@/app/components/Title' import { unymToNym } from '@/app/utils/currency' import { Tooltip } from '@/app/components/Tooltip' -import { NYM_BIG_DIPPER } from '@/app/api/constants' +import { EXPLORER_FOR_ACCOUNTS } from '@/app/api/constants' import { splice } from '@/app/utils' import { VersionDisplaySelector, @@ -29,6 +29,23 @@ import { GatewayRowType, gatewayToGridRow, } from '@/app/components/Gateways/Gateways' +import {LocatedGateway} from "@/app/typeDefs/explorer-api"; + +const gatewaySanitize = (g?: LocatedGateway): boolean => { + if(!g) { + return false; + } + + if(!g.gateway.version || !g.gateway.version.trim().length) { + return false; + } + + if(g.gateway.version === "null") { + return false; + } + + return true; +} const PageGateways = () => { const { gateways } = useMainContext() @@ -39,7 +56,7 @@ const PageGateways = () => { const highestVersion = React.useMemo(() => { if (gateways?.data) { - const versions = gateways.data.reduce( + const versions = gateways.data.filter(gatewaySanitize).reduce( (a: string[], b) => [...a, b.gateway.version], [] ) @@ -51,7 +68,7 @@ const PageGateways = () => { }, [gateways]) const filterByLatestVersions = React.useMemo(() => { - const filtered = gateways?.data?.filter((gw) => { + const filtered = gateways?.data?.filter(gatewaySanitize).filter((gw) => { const versionDiff = diff(highestVersion, gw.gateway.version) return versionDiff === 'patch' || versionDiff === null }) @@ -60,7 +77,7 @@ const PageGateways = () => { }, [gateways]) const filterByOlderVersions = React.useMemo(() => { - const filtered = gateways?.data?.filter((gw) => { + const filtered = gateways?.data?.filter(gatewaySanitize).filter((gw) => { const versionDiff = diff(highestVersion, gw.gateway.version) return versionDiff === 'major' || versionDiff === 'minor' }) @@ -89,7 +106,7 @@ const PageGateways = () => { return [ { id: 'gateway-data', - header: 'Gatewsay Data', + header: 'Gateways Data', columns: [ { id: 'identity_key', @@ -116,41 +133,6 @@ const PageGateways = () => { ) }, }, - { - id: 'node_performance', - header: 'Node Performance', - accessorKey: 'node_performance', - size: 200, - Header: () => { - return ( - <Box display="flex"> - <InfoTooltip - id="gateways-list-routing-score" - title="Gateway's most recent score (measured in the last 15 minutes). Routing score is relative to that of the network. Each time a gateway is tested, the test packets have to go through the full path of the network (gateway + 3 nodes). If a node in the path drop packets it will affect the score of the gateway and other nodes in the test" - placement="top-start" - textColor={theme.palette.nym.networkExplorer.tooltip.color} - bgColor={ - theme.palette.nym.networkExplorer.tooltip.background - } - maxWidth={230} - arrow - /> - <CustomColumnHeading headingTitle="Routing Score" /> - </Box> - ) - }, - Cell: ({ row }) => { - return ( - <StyledLink - to={`/network-components/gateways/${row.original.identity_key}`} - data-testid="node-performance" - color="text.primary" - > - {`${row.original.node_performance}%`} - </StyledLink> - ) - }, - }, { id: 'version', header: 'Version', @@ -222,7 +204,7 @@ const PageGateways = () => { Cell: ({ row }) => { return ( <StyledLink - to={`${NYM_BIG_DIPPER}/account/${row.original.owner}`} + to={`${EXPLORER_FOR_ACCOUNTS}/account/${row.original.owner}`} target="_blank" data-testid="owner" color="text.primary" @@ -237,137 +219,6 @@ const PageGateways = () => { ] }, []) - const _columns: GridColDef[] = [ - { - field: 'node_performance', - align: 'center', - renderHeader: () => ( - <> - <InfoTooltip - id="gateways-list-routing-score" - title="Gateway's most recent score (measured in the last 15 minutes). Routing score is relative to that of the network. Each time a gateway is tested, the test packets have to go through the full path of the network (gateway + 3 nodes). If a node in the path drop packets it will affect the score of the gateway and other nodes in the test" - placement="top-start" - textColor={theme.palette.nym.networkExplorer.tooltip.color} - bgColor={theme.palette.nym.networkExplorer.tooltip.background} - maxWidth={230} - arrow - /> - <CustomColumnHeading headingTitle="Routing Score" /> - </> - ), - width: 120, - disableColumnMenu: true, - headerAlign: 'center', - headerClassName: 'MuiDataGrid-header-override', - renderCell: (params: GridRenderCellParams) => ( - <StyledLink - to={`/network-components/gateways/${params.row.identity_key}`} - data-testid="pledge-amount" - > - {`${params.value}%`} - </StyledLink> - ), - }, - { - field: 'version', - align: 'center', - renderHeader: () => <CustomColumnHeading headingTitle="Version" />, - width: 150, - disableColumnMenu: true, - headerAlign: 'center', - headerClassName: 'MuiDataGrid-header-override', - renderCell: (params: GridRenderCellParams) => ( - <StyledLink - to={`/network-components/gateways/${params.row.identity_key}`} - data-testid="version" - > - {params.value} - </StyledLink> - ), - sortComparator: (a, b) => { - if (gte(a, b)) return 1 - return -1 - }, - }, - { - field: 'location', - renderHeader: () => <CustomColumnHeading headingTitle="Location" />, - width: 180, - disableColumnMenu: true, - headerAlign: 'left', - headerClassName: 'MuiDataGrid-header-override', - renderCell: (params: GridRenderCellParams) => ( - <Box - sx={{ justifyContent: 'flex-start', cursor: 'pointer' }} - data-testid="location-button" - > - <Tooltip text={params.value} id="gateway-location-text"> - <Box - sx={{ - overflow: 'hidden', - whiteSpace: 'nowrap', - textOverflow: 'ellipsis', - }} - > - {params.value} - </Box> - </Tooltip> - </Box> - ), - }, - { - field: 'host', - renderHeader: () => <CustomColumnHeading headingTitle="IP:Port" />, - width: 180, - disableColumnMenu: true, - headerAlign: 'left', - headerClassName: 'MuiDataGrid-header-override', - renderCell: (params: GridRenderCellParams) => ( - <StyledLink - to={`/network-components/gateways/${params.row.identity_key}`} - data-testid="host" - > - {params.value} - </StyledLink> - ), - }, - { - field: 'owner', - headerName: 'Owner', - renderHeader: () => <CustomColumnHeading headingTitle="Owner" />, - width: 180, - disableColumnMenu: true, - headerAlign: 'left', - headerClassName: 'MuiDataGrid-header-override', - renderCell: (params: GridRenderCellParams) => ( - <StyledLink - to={`${NYM_BIG_DIPPER}/account/${params.value}`} - target="_blank" - data-testid="owner" - > - {splice(7, 29, params.value)} - </StyledLink> - ), - }, - { - field: 'bond', - width: 150, - disableColumnMenu: true, - type: 'number', - renderHeader: () => <CustomColumnHeading headingTitle="Bond" />, - headerClassName: 'MuiDataGrid-header-override', - headerAlign: 'left', - renderCell: (params: GridRenderCellParams) => ( - <StyledLink - to={`/network-components/gateways/${params.row.identity_key}`} - data-testid="pledge-amount" - > - {`${unymToNym(params.value, 6)}`} - </StyledLink> - ), - }, - ] - const table = useMaterialReactTable({ columns, data, @@ -376,7 +227,7 @@ const PageGateways = () => { return ( <> <Box mb={2}> - <Title text="Gateways" /> + <Title text="Legacy Gateways" /> </Box> <Grid container> <Grid item xs={12}> diff --git a/explorer-nextjs/app/network-components/mixnodes/[id]/page.tsx b/explorer-nextjs/app/network-components/mixnodes/[id]/page.tsx index 495312064de..5e03b5d1326 100644 --- a/explorer-nextjs/app/network-components/mixnodes/[id]/page.tsx +++ b/explorer-nextjs/app/network-components/mixnodes/[id]/page.tsx @@ -94,7 +94,12 @@ const PageMixnodeDetailWithState = () => { const isMobile = useIsMobile() return ( <Box component="main"> - <Title text="Mixnode Detail" /> + <Title text="Legacy Mixnode Detail" /> + <Alert variant="filled" severity="warning" sx={{ my : 2, pt: 2 }}> + <AlertTitle> + Please update to the latest <code>nym-node</code> binary and migrate your bond and delegations from the wallet + </AlertTitle> + </Alert> <Grid container spacing={2} mt={1} mb={6}> <Grid item xs={12}> {mixNodeRow && description?.data && ( diff --git a/explorer-nextjs/app/network-components/mixnodes/page.tsx b/explorer-nextjs/app/network-components/mixnodes/page.tsx index 4021e4173f9..2f2774a7f43 100644 --- a/explorer-nextjs/app/network-components/mixnodes/page.tsx +++ b/explorer-nextjs/app/network-components/mixnodes/page.tsx @@ -29,7 +29,7 @@ import { useMainContext } from '@/app/context/main' import { CopyToClipboard } from '@nymproject/react/clipboard/CopyToClipboard' import { splice } from '@/app/utils' import { currencyToString } from '@/app/utils/currency' -import { NYM_BIG_DIPPER } from '@/app/api/constants' +import { EXPLORER_FOR_ACCOUNTS } from '@/app/api/constants' import { MixnodeStatusWithAll, toMixnodeStatus, @@ -229,24 +229,6 @@ export default function MixnodesPage() { >{`${row.original.operating_cost} NYM`}</StyledLink> ), }, - { - id: 'node_performance', - accessorKey: 'node_performance', - size: 200, - header: 'Routing Score', - Header: () => ( - <CustomColumnHeading - headingTitle="Routing Score" - tooltipInfo="Mixnode's most recent score (measured in the last 15 minutes). Routing score is relative to that of the network. Each time a gateway is tested, the test packets have to go through the full path of the network (gateway + 3 nodes). If a node in the path drop packets it will affect the score of the gateway and other nodes in the test." - /> - ), - Cell: ({ row }) => ( - <StyledLink - to={`/network-components/mixnodes/${row.original.mix_id}`} - color={useGetMixNodeStatusColor(row.original.status)} - >{`${row.original.node_performance}%`}</StyledLink> - ), - }, { id: 'owner', accessorKey: 'owner', @@ -255,7 +237,7 @@ export default function MixnodesPage() { Header: () => <CustomColumnHeading headingTitle="Owner" />, Cell: ({ row }) => ( <StyledLink - to={`${NYM_BIG_DIPPER}/account/${row.original.owner}`} + to={`${EXPLORER_FOR_ACCOUNTS}/account/${row.original.owner}`} color={useGetMixNodeStatusColor(row.original.status)} target="_blank" data-testid="big-dipper-link" @@ -326,7 +308,7 @@ export default function MixnodesPage() { return ( <DelegationsProvider> <Box mb={2}> - <Title text="Mixnodes" /> + <Title text="Legacy Mixnodes" /> </Box> <Grid container> <Grid item xs={12}> diff --git a/explorer-nextjs/app/network-components/nodes/DeclaredRole.tsx b/explorer-nextjs/app/network-components/nodes/DeclaredRole.tsx new file mode 100644 index 00000000000..fac36689f2d --- /dev/null +++ b/explorer-nextjs/app/network-components/nodes/DeclaredRole.tsx @@ -0,0 +1,11 @@ +import React from "react"; +import { Chip } from "@mui/material"; + +export const DeclaredRole = ({ declared_role }: { declared_role?: any }) => ( + <> + {declared_role?.mixnode && <Chip size="small" label="Mixnode" sx={{ mr: 0.5 }} color="info" />} + {declared_role?.entry && <Chip size="small" label="Entry" sx={{ mr: 0.5 }} color="success" />} + {declared_role?.exit_nr && <Chip size="small" label="Exit NR" sx={{ mr: 0.5 }} color="warning" />} + {declared_role?.exit_ipr && <Chip size="small" label="Exit IPR" sx={{ mr: 0.5 }} color="warning" />} + </> +) \ No newline at end of file diff --git a/explorer-nextjs/app/network-components/nodes/[id]/NodeDelegationsTable.tsx b/explorer-nextjs/app/network-components/nodes/[id]/NodeDelegationsTable.tsx new file mode 100644 index 00000000000..a1b754ccce6 --- /dev/null +++ b/explorer-nextjs/app/network-components/nodes/[id]/NodeDelegationsTable.tsx @@ -0,0 +1,93 @@ +import React, {useMemo} from "react"; +import {MaterialReactTable, MRT_ColumnDef, useMaterialReactTable} from "material-react-table"; +import StyledLink from "../../../components/StyledLink"; +import {EXPLORER_FOR_ACCOUNTS} from "@/app/api/constants"; +import {splice} from "@/app/utils"; +import {humanReadableCurrencyToString} from "@/app/utils/currency"; +import {Typography} from "@mui/material"; +import {useTheme} from "@mui/material/styles"; +import WarningIcon from '@mui/icons-material/Warning'; +import { Tooltip } from '@/app/components/Tooltip' + +export const NodeDelegationsTable = ({ node }: { node: any}) => { + const columns = useMemo<MRT_ColumnDef<any>[]>(() => { + return [ + { + id: 'nym-node-delegation-data', + header: 'Nym Node Delegations', + columns: [ + { + id: 'owner', + header: 'Delegator', + accessorKey: 'owner', + size: 150, + Cell: ({ row }) => { + return ( + <StyledLink + to={`${EXPLORER_FOR_ACCOUNTS}/account/${row.original.owner || "-"}`} + target="_blank" + data-testid="bond_information.node.owner" + color="text.primary" + > + {splice(7, 29, row.original.owner)} + </StyledLink> + ) + }, + }, + { + id: 'amount', + header: 'Amount', + accessorKey: 'amount', + size: 150, + Cell: ({ row }) => ( + <>{humanReadableCurrencyToString(row.original.amount)}</> + ) + }, + { + id: 'height', + header: 'Delegated at height', + accessorKey: 'height', + size: 150, + }, + { + id: 'proxy', + header: 'From vesting account?', + accessorKey: 'proxy', + size: 250, + Cell: ({ row }) => { + if(row.original.proxy?.length) { + return ( + <VestingDelegationWarning>Please re-delegate from your main account</VestingDelegationWarning> + ) + } + } + }, + ] + } + ]; + }, []); + + const table = useMaterialReactTable({ + columns, + data: node ? node.delegations : [], + }); + + return ( + <MaterialReactTable table={table} /> + ); +} + +export const VestingDelegationWarning = ({children, plural}: { plural?: boolean, children: React.ReactNode}) => { + const theme = useTheme(); + return ( + <Tooltip + text={`${plural ? 'These delegations have' : 'This delegation has'} been made with a vesting account. All tokens are liquid, if you are the delegator, please move the tokens into your main account and make the delegation from there.`} + id="delegations" + > + <Typography fontSize="inherit" color={theme.palette.warning.main} display="flex" alignItems="center"> + <WarningIcon sx={{ mr: 0.5 }}/> + {children} + </Typography> + </Tooltip> + ); +} \ No newline at end of file diff --git a/explorer-nextjs/app/network-components/nodes/[id]/page.tsx b/explorer-nextjs/app/network-components/nodes/[id]/page.tsx new file mode 100644 index 00000000000..2f92542b3b3 --- /dev/null +++ b/explorer-nextjs/app/network-components/nodes/[id]/page.tsx @@ -0,0 +1,278 @@ +'use client' + +import * as React from 'react' +import { Alert, AlertTitle, Box, CircularProgress, Grid } from '@mui/material' +import { useParams } from 'next/navigation' +import { ColumnsType, DetailTable } from '@/app/components/DetailTable' +import { ComponentError } from '@/app/components/ComponentError' +import { ContentCard } from '@/app/components/ContentCard' +import { UptimeChart } from '@/app/components/UptimeChart' +import { + NymNodeContextProvider, + useNymNodeContext, +} from '@/app/context/node' +import { useMainContext } from '@/app/context/main' +import { Title } from '@/app/components/Title' +import Paper from "@mui/material/Paper"; +import Table from '@mui/material/Table'; +import TableBody from '@mui/material/TableBody'; +import TableCell from '@mui/material/TableCell'; +import TableContainer from '@mui/material/TableContainer'; +import TableRow from '@mui/material/TableRow'; +import {humanReadableCurrencyToString} from "@/app/utils/currency"; +import {DeclaredRole} from "@/app/network-components/nodes/DeclaredRole"; +import {NodeDelegationsTable, VestingDelegationWarning} from "@/app/network-components/nodes/[id]/NodeDelegationsTable"; + +const columns: ColumnsType[] = [ + { + field: 'identity_key', + title: 'Identity Key', + headerAlign: 'left', + width: 230, + }, + { + field: 'bond', + title: 'Bond', + headerAlign: 'left', + }, + { + field: 'host', + title: 'IP', + headerAlign: 'left', + width: 99, + }, + { + field: 'location', + title: 'Location', + headerAlign: 'left', + }, + { + field: 'owner', + title: 'Owner', + headerAlign: 'left', + }, + { + field: 'version', + title: 'Version', + headerAlign: 'left', + }, +] + +interface NodeEnrichedRowType { + node_id: number; + identity_key: string; + bond: string; + host: string; + location: string; + owner: string; + version: string; +} + +function nodeEnrichedToGridRow(node: any): NodeEnrichedRowType { + return { + node_id: node.node_id, + owner: node.bond_information?.owner || '', + identity_key: node.bond_information?.node?.identity_key || '', + location: node.location?.country_name || '', + bond: node.bond_information?.original_pledge.amount || 0, // TODO: format + host: node.bond_information?.node?.host || '', + version: node.description?.build_information?.build_version || '', + }; +} + + +/** + * Shows nym node details + */ +const PageNymNodeDetailsWithState = ({ + selectedNymNode, +}: { + selectedNymNode?: any +}) => { + const { uptimeHistory } = useNymNodeContext() + const enrichedData = React.useMemo(() => selectedNymNode ? [nodeEnrichedToGridRow(selectedNymNode)] : [], []); + + const hasVestingContractDelegations = React.useMemo(() => selectedNymNode?.delegations?.filter((d: any) => d.proxy)?.length, [selectedNymNode]); + + return ( + <Box component="main"> + <Title text="Nym Node Detail" /> + + <Grid container mt={4}> + <Grid item xs={12}> + <DetailTable + columnsData={columns} + tableName="Node detail table" + rows={enrichedData} + /> + </Grid> + </Grid> + + <Grid container mt={2} spacing={2}> + {selectedNymNode.rewarding_details && + <Grid item xs={12} md={4}> + <TableContainer component={Paper}> + <Table> + <TableBody> + <TableRow> + <TableCell colSpan={2}> + Delegations and Rewards + </TableCell> + </TableRow> + <TableRow> + <TableCell component="th" scope="row" sx={{ color: "inherit" }}> + <strong>Operator</strong> + </TableCell> + <TableCell align="right"> + {humanReadableCurrencyToString({ amount : selectedNymNode.rewarding_details.operator.split('.')[0], denom: "unym" })} + </TableCell> + </TableRow> + <TableRow> + <TableCell component="th" scope="row" sx={{ color: "inherit" }}> + <strong> + {hasVestingContractDelegations ? + <VestingDelegationWarning plural={true}> + Delegates ({selectedNymNode.rewarding_details.unique_delegations} delegates) + </VestingDelegationWarning> : + <>Delegates ({selectedNymNode.rewarding_details.unique_delegations} delegates)</> + } + </strong> + </TableCell> + <TableCell align="right"> + {humanReadableCurrencyToString({ amount : selectedNymNode.rewarding_details.delegates.split('.')[0], denom: "unym" })} + </TableCell> + </TableRow> + <TableRow> + <TableCell component="th" scope="row" sx={{ color: "inherit" }}> + <strong>Profit margin</strong> + </TableCell> + <TableCell align="right"> + {selectedNymNode.rewarding_details.cost_params.profit_margin_percent * 100}% + </TableCell> + </TableRow> + <TableRow> + <TableCell component="th" scope="row" sx={{ color: "inherit" }}> + <strong>Operator costs</strong> + </TableCell> + <TableCell align="right"> + {humanReadableCurrencyToString(selectedNymNode.rewarding_details.cost_params.interval_operating_cost)} + </TableCell> + </TableRow> + </TableBody> + </Table> + </TableContainer> + </Grid>} + + {selectedNymNode.description?.declared_role && <Grid item xs={12} md={4}> + <TableContainer component={Paper}> + <Table> + <TableBody> + <TableRow> + <TableCell colSpan={2}> + Node roles + </TableCell> + </TableRow> + <TableRow> + <TableCell> + Self declared roles + </TableCell> + <TableCell> + <DeclaredRole declared_role={selectedNymNode.description?.declared_role}/> + </TableCell> + </TableRow> + </TableBody> + </Table> + </TableContainer> + </Grid>} + </Grid> + + <Grid container spacing={2} mt={2}> + <Grid item xs={12} md={8}> + {uptimeHistory && ( + <ContentCard title="Routing Score"> + {uptimeHistory.error && ( + <ComponentError text="There was a problem retrieving routing score." /> + )} + <UptimeChart + loading={uptimeHistory.isLoading} + xLabel="Date" + yLabel="Daily average" + uptimeStory={uptimeHistory} + /> + </ContentCard> + )} + </Grid> + </Grid> + + <Box mt={2}> + <NodeDelegationsTable node={selectedNymNode}/> + </Box> + </Box> + ) +} + +/** + * Guard component to handle loading and not found states + */ +const PageNymNodeDetailGuard = () => { + const [selectedNode, setSelectedNode] = React.useState<any>() + const [isLoading, setLoading] = React.useState<boolean>(true); + const [error, setError] = React.useState<string>(); + const { fetchNodeById } = useMainContext() + const { id } = useParams() + + React.useEffect(() => { + setSelectedNode(undefined); + setLoading(true); + (async () => { + if(typeof(id) === "string") { + try { + const res = await fetchNodeById(Number.parseInt(id)); + setSelectedNode(res); + } catch(e: any) { + setError(e.message); + } + finally { + setLoading(false); + } + } + })(); + }, [id]) + + if (isLoading) { + return <CircularProgress /> + } + + // loaded, but not found + if (error) { + return ( + <Alert severity="warning"> + <AlertTitle>Nym node not found</AlertTitle> + Sorry, we could not find a node with id <code>{id || ''}</code> + </Alert> + ) + } + + return <PageNymNodeDetailsWithState selectedNymNode={selectedNode} /> +} + +/** + * Wrapper component that adds the node content based on the `id` in the address URL + */ +const PageNymNodeDetail = () => { + const { id } = useParams() + + if (!id || typeof id !== 'string') { + return ( + <Alert severity="error">Oh no! Could not find that node</Alert> + ) + } + + return ( + <NymNodeContextProvider nymNodeId={id}> + <PageNymNodeDetailGuard /> + </NymNodeContextProvider> + ) +} + +export default PageNymNodeDetail diff --git a/explorer-nextjs/app/network-components/nodes/page.tsx b/explorer-nextjs/app/network-components/nodes/page.tsx new file mode 100644 index 00000000000..75e5b97fdd0 --- /dev/null +++ b/explorer-nextjs/app/network-components/nodes/page.tsx @@ -0,0 +1,254 @@ +'use client' + +import React, { useMemo } from 'react' +import { Box, Card, Grid, Stack, Chip } from '@mui/material' +import { useTheme } from '@mui/material/styles' +import { + MRT_ColumnDef, + MaterialReactTable, + useMaterialReactTable, +} from 'material-react-table' +import { diff, gte, rcompare } from 'semver' +import { GridColDef, GridRenderCellParams } from '@mui/x-data-grid' +import { CopyToClipboard } from '@nymproject/react/clipboard/CopyToClipboard' +import { Tooltip as InfoTooltip } from '@nymproject/react/tooltip/Tooltip' +import { useMainContext } from '@/app/context/main' +import { CustomColumnHeading } from '@/app/components/CustomColumnHeading' +import { Title } from '@/app/components/Title' +import { humanReadableCurrencyToString } from '@/app/utils/currency' +import { Tooltip } from '@/app/components/Tooltip' +import { EXPLORER_FOR_ACCOUNTS } from '@/app/api/constants' +import { splice } from '@/app/utils' + +import StyledLink from '@/app/components/StyledLink' +import {DeclaredRole} from "@/app/network-components/nodes/DeclaredRole"; + +function getFlagEmoji(countryCode: string) { + const codePoints = countryCode + .toUpperCase() + .split('') + .map(char => 127397 + char.charCodeAt(0)); + return String.fromCodePoint(...codePoints); +} + +const PageNodes = () => { + const [isLoading, setLoading] = React.useState(true); + const { nodes, fetchNodes } = useMainContext() + + React.useEffect(() => { + (async () => { + try { + await fetchNodes(); + } finally { + setLoading(false); + } + })(); + }, []); + + const columns = useMemo<MRT_ColumnDef<any>[]>(() => { + return [ + { + id: 'nym-node-data', + header: 'Nym Node Data', + columns: [ + { + id: 'node_id', + header: 'Node Id', + accessorKey: 'node_id', + size: 75, + }, + { + id: 'identity_key', + header: 'Identity Key', + accessorKey: 'identity_key', + size: 250, + Cell: ({ row }) => { + return ( + <Stack direction="row" alignItems="center" gap={1}> + <CopyToClipboard + sx={{ mr: 0.5, color: 'grey.400' }} + smallIcons + value={row.original.bond_information.node.identity_key} + tooltip={`Copy identity key ${row.original.bond_information.node.identity_key} to clipboard`} + /> + <StyledLink + to={`/network-components/nodes/${row.original.node_id}`} + dataTestId="identity-link" + color="text.primary" + > + {splice(7, 29, row.original.bond_information.node.identity_key)} + </StyledLink> + </Stack> + ) + }, + }, + { + id: 'version', + header: 'Version', + accessorKey: 'description.build_information.build_version', + size: 75, + Cell: ({ row }) => { + return ( + <StyledLink + to={`/network-components/nodes/${row.original.node_id}`} + data-testid="version" + color="text.primary" + > + {row.original.description?.build_information?.build_version || "-"} + </StyledLink> + ) + }, + }, + { + id: 'contract_node_type', + header: 'Kind', + accessorKey: 'contract_node_type', + size: 150, + Cell: ({ row }) => { + return ( + <StyledLink + to={`/network-components/nodes/${row.original.node_id}`} + data-testid="contract_node_type" + color="text.primary" + > + <code>{row.original.contract_node_type || "-"}</code> + </StyledLink> + ) + }, + }, + { + id: 'declared_role', + header: 'Declare Role', + accessorKey: 'description.declared_role', + size: 250, + Cell: ({ row }) => { + return ( + <Box + sx={{ justifyContent: 'flex-start', cursor: 'pointer' }} + data-testid="declared_role-button" + > + <DeclaredRole declared_role={row.original.description?.declared_role}/> + </Box> + ) + }, + }, + { + id: 'total_stake', + header: 'Total Stake', + accessorKey: 'description.total_stake', + size: 250, + Cell: ({ row }) => { + return ( + <Box + sx={{ justifyContent: 'flex-start', cursor: 'pointer' }} + data-testid="total_stake-button" + > + {humanReadableCurrencyToString({ amount: row.original.total_stake || 0, denom: "unym" })} + </Box> + ) + }, + }, + { + id: 'location', + header: 'Location', + accessorKey: 'location.country_name', + size: 75, + Cell: ({ row }) => { + return ( + <Box + sx={{ justifyContent: 'flex-start', cursor: 'pointer' }} + data-testid="location-button" + > + <Tooltip + text={row.original.location?.country_name || "-"} + id="nym-node-location-text" + > + <Box + sx={{ + overflow: 'hidden', + whiteSpace: 'nowrap', + textOverflow: 'ellipsis', + }} + > + {row.original.location?.country_name ? <>{getFlagEmoji(row.original.location.two_letter_iso_country_code.toUpperCase())} {row.original.location.two_letter_iso_country_code}</> : <>-</> } + </Box> + </Tooltip> + </Box> + ) + }, + }, + { + id: 'host', + header: 'IP', + accessorKey: 'bond_information.node.host', + size: 150, + Cell: ({ row }) => { + return ( + <StyledLink + to={`/network-components/nodes/${row.original.node_id}`} + data-testid="host" + color="text.primary" + > + {row.original.bond_information?.node?.host || "-"} + </StyledLink> + ) + }, + }, + { + id: 'owner', + header: 'Owner', + accessorKey: 'bond_information.owner', + size: 150, + Cell: ({ row }) => { + return ( + <StyledLink + to={`${EXPLORER_FOR_ACCOUNTS}/account/${row.original.bond_information?.owner || "-"}`} + target="_blank" + data-testid="bond_information.node.owner" + color="text.primary" + > + {splice(7, 29, row.original.bond_information?.owner)} + </StyledLink> + ) + }, + }, + ], + }, + ] + }, []) + + const table = useMaterialReactTable({ + columns, + data: nodes?.data || [], + state: { + isLoading, + showLoadingOverlay: isLoading, + }, + initialState: { + isLoading: true, + showLoadingOverlay: true, + } + }) + + return ( + <> + <Box mb={2}> + <Title text="Nym Nodes" /> + </Box> + <Grid container> + <Grid item xs={12}> + <Card + sx={{ + padding: 2, + height: '100%', + }} + > + <MaterialReactTable table={table} /> + </Card> + </Grid> + </Grid> + </> + ) +} + +export default PageNodes diff --git a/explorer-nextjs/app/network-components/service-providers/page.tsx b/explorer-nextjs/app/network-components/service-providers/page.tsx deleted file mode 100644 index 7e8d9acc72c..00000000000 --- a/explorer-nextjs/app/network-components/service-providers/page.tsx +++ /dev/null @@ -1,140 +0,0 @@ -'use client' - -import React, { useMemo } from 'react' -import { - Box, - Button, - Card, - FormControl, - Grid, - ListItem, - Menu, - Typography, -} from '@mui/material' -import { TableToolbar } from '@/app/components/TableToolbar' -import { Title } from '@/app/components/Title' -import { useMainContext } from '@/app/context/main' -import { CustomColumnHeading } from '@/app/components/CustomColumnHeading' -import { - MRT_ColumnDef, - MaterialReactTable, - useMaterialReactTable, -} from 'material-react-table' -import { DirectoryServiceProvider } from '@/app/typeDefs/explorer-api' - -const SupportedApps = () => { - const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null) - const open = Boolean(anchorEl) - const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => { - setAnchorEl(event.currentTarget) - } - const handleClose = () => { - setAnchorEl(null) - } - const anchorRef = React.useRef<HTMLButtonElement>(null) - - return ( - <FormControl size="small"> - <Button - ref={anchorRef} - onClick={handleClick} - size="large" - variant="outlined" - color="inherit" - sx={{ mr: 2, textTransform: 'capitalize' }} - > - Supported Apps - </Button> - <Menu anchorEl={anchorEl} open={open} onClose={handleClose}> - <ListItem>Keybase</ListItem> - <ListItem>Telegram</ListItem> - <ListItem>Electrum</ListItem> - <ListItem>Blockstream Green</ListItem> - </Menu> - </FormControl> - ) -} - -const ServiceProviders = () => { - const { serviceProviders } = useMainContext() - - const columns = useMemo<MRT_ColumnDef<DirectoryServiceProvider>[]>(() => { - return [ - { - id: 'service-providers-data', - header: 'Service Providers Data', - columns: [ - { - id: 'address', - accessorKey: 'address', - header: 'Client ID', - size: 450, - }, - { - id: 'service_type-type', - accessorKey: 'service_type', - header: 'Type', - size: 100, - }, - { - id: 'routing_score-score', - accessorKey: 'routing_score', - header: 'Routing score', - Header() { - return ( - <CustomColumnHeading - headingTitle="Routing score" - tooltipInfo="Routing score is only displayed for the service providers that had a successful ping within the last two hours" - /> - ) - }, - Cell({ row }) { - return row.original.routing_score || '-' - }, - }, - ], - }, - ] - }, []) - - const table = useMaterialReactTable({ - columns, - data: serviceProviders?.data || [], - layoutMode: 'grid', - state: { - isLoading: serviceProviders?.isLoading, - }, - initialState: { - sorting: [ - { - id: 'routing_score', - desc: true, - }, - ], - }, - }) - - return ( - <> - <Box mb={2}> - <Title text="Service Providers" /> - </Box> - <Grid container> - <Grid item xs={12}> - <Card - sx={{ - padding: 2, - }} - > - <> - <TableToolbar childrenBefore={<SupportedApps />} /> - <MaterialReactTable table={table} /> - </> - </Card> - </Grid> - </Grid> - </> - ) -} - -export default ServiceProviders diff --git a/explorer-nextjs/app/page.tsx b/explorer-nextjs/app/page.tsx index 1ff7dd36139..2c6f9f781c1 100644 --- a/explorer-nextjs/app/page.tsx +++ b/explorer-nextjs/app/page.tsx @@ -13,7 +13,7 @@ import { GatewaysSVG } from '@/app/icons/GatewaysSVG' import { ValidatorsSVG } from '@/app/icons/ValidatorsSVG' import { ContentCard } from '@/app/components/ContentCard' import { WorldMap } from '@/app/components/WorldMap' -import { BIG_DIPPER } from '@/app/api/constants' +import { BLOCK_EXPLORER_BASE_URL } from '@/app/api/constants' import { formatNumber } from '@/app/utils' import { useMainContext } from './context/main' import { useRouter } from 'next/navigation' @@ -42,71 +42,59 @@ const PageOverview = () => { <> <Grid item xs={12} md={4}> <StatsCard - onClick={() => router.push('/network-components/mixnodes')} - title="Mixnodes" + onClick={() => router.push('/network-components/nodes')} + title="Nodes" icon={<MixnodesSVG />} - count={summaryOverview.data?.mixnodes.count || ''} - errorMsg={summaryOverview?.error} - /> - </Grid> - <Grid item xs={12} md={4}> - <StatsCard - onClick={() => - router.push('/network-components/mixnodes?status=active') - } - title="Active nodes" - icon={<Icons.Mixnodes.Status.Active />} - color={ - theme.palette.nym.networkExplorer.mixnodes.status.active - } - count={summaryOverview.data?.mixnodes.activeset.active} - errorMsg={summaryOverview?.error} - /> - </Grid> - <Grid item xs={12} md={4}> - <StatsCard - onClick={() => - router.push('/network-components/mixnodes?status=standby') - } - title="Standby nodes" - color={ - theme.palette.nym.networkExplorer.mixnodes.status.standby - } - icon={<Icons.Mixnodes.Status.Standby />} - count={summaryOverview.data?.mixnodes.activeset.standby} + count={summaryOverview.data?.nymnodes?.count || ''} errorMsg={summaryOverview?.error} /> </Grid> </> )} - {gateways && ( + {summaryOverview && ( <Grid item xs={12} md={4}> <StatsCard - onClick={() => router.push('/network-components/gateways')} - title="Gateways" - count={gateways?.data?.length || ''} - errorMsg={gateways?.error} + onClick={() => router.push('/network-components/nodes')} + title="Mixnodes" + count={summaryOverview.data?.nymnodes?.roles?.mixnode || ''} icon={<GatewaysSVG />} /> </Grid> )} - {serviceProviders && ( + {summaryOverview && ( <Grid item xs={12} md={4}> <StatsCard - onClick={() => - router.push('/network-components/service-providers') - } - title="Service providers" - icon={<PeopleAlt />} - count={serviceProviders.data?.length} - errorMsg={summaryOverview?.error} + onClick={() => router.push('/network-components/nodes')} + title="Entry Gateways" + count={summaryOverview.data?.nymnodes?.roles?.entry || ''} + icon={<GatewaysSVG />} + /> + </Grid> + )} + {summaryOverview && ( + <Grid item xs={12} md={4}> + <StatsCard + onClick={() => router.push('/network-components/nodes')} + title="Exit Gateways" + count={summaryOverview.data?.nymnodes?.roles?.exit_ipr || ''} + icon={<GatewaysSVG />} + /> + </Grid> + )} + {summaryOverview && ( + <Grid item xs={12} md={4}> + <StatsCard + onClick={() => router.push('/network-components/nodes')} + title="SOCKS5 Network Requesters" + count={summaryOverview.data?.nymnodes?.roles?.exit_nr || ''} + icon={<GatewaysSVG />} /> </Grid> )} {validators && ( <Grid item xs={12} md={4}> <StatsCard - onClick={() => window.open(`${BIG_DIPPER}/validators`)} + onClick={() => window.open(`${BLOCK_EXPLORER_BASE_URL}/validators`)} title="Validators" count={validators?.data?.count || ''} errorMsg={validators?.error} @@ -117,7 +105,7 @@ const PageOverview = () => { {block?.data && ( <Grid item xs={12}> <Link - href={`${BIG_DIPPER}/blocks`} + href={`${BLOCK_EXPLORER_BASE_URL}/blocks`} target="_blank" rel="noreferrer" underline="none" diff --git a/explorer-nextjs/app/typeDefs/explorer-api.ts b/explorer-nextjs/app/typeDefs/explorer-api.ts index 9319c33952e..b15490008d7 100644 --- a/explorer-nextjs/app/typeDefs/explorer-api.ts +++ b/explorer-nextjs/app/typeDefs/explorer-api.ts @@ -20,6 +20,15 @@ export interface SummaryOverviewResponse { validators: { count: number; }; + nymnodes: { + count: number; + roles: { + mixnode: number; + entry: number; + exit_nr: number; + exit_ipr: number; + }; + }; } export interface MixNode { @@ -156,7 +165,15 @@ export interface LocatedGateway { location?: Location; } -export type GatewayResponse = GatewayBond[]; +export type GatewayResponse = LocatedGateway[]; + +export interface NymNodeReportResponse { + identity: string; + owner: string; + most_recent: number; + last_hour: number; + last_day: number; +} export interface GatewayReportResponse { identity: string; diff --git a/explorer-nextjs/app/utils/currency.ts b/explorer-nextjs/app/utils/currency.ts index ad6ed1f1656..07bdc890de7 100644 --- a/explorer-nextjs/app/utils/currency.ts +++ b/explorer-nextjs/app/utils/currency.ts @@ -31,6 +31,16 @@ export const currencyToString = ({ amount, dp, denom = DENOM }: { amount: string return `${toDisplay(printableAmount, dp)} ${printableDenom}`; }; +function addThousandsSeparator(value: string) { + return value.toString().replace(/\B(?=(\d{3})+(?!\d))/g, " "); +} + +export const humanReadableCurrencyToString = ({ amount, denom }: { amount: string, denom: string }) => { + const str = currencyToString({ amount, denom, dp: 2 }); + const parts = str.split('.'); + return [addThousandsSeparator(parts[0]), parts[1]].join('.'); +} + export const stakingCurrencyToString = (amount: string, denom: string = DENOM_STAKING) => printableCoin({ amount, diff --git a/explorer-nextjs/package.json b/explorer-nextjs/package.json index 24e6656a35a..05ed83fcf23 100644 --- a/explorer-nextjs/package.json +++ b/explorer-nextjs/package.json @@ -17,7 +17,8 @@ "react-error-boundary": "^4.0.13", "material-react-table": "^2.12.1", "@mui/x-date-pickers": "7.1.1", - "@mui/x-data-grid": "7.1.1" + "@mui/x-data-grid": "7.1.1", + "@mui/x-charts": "^7.22.3" }, "devDependencies": { "@types/node": "^20", diff --git a/gateway/Cargo.toml b/gateway/Cargo.toml index f760a6ba23c..076f1304f86 100644 --- a/gateway/Cargo.toml +++ b/gateway/Cargo.toml @@ -58,7 +58,6 @@ nym-mixnet-client = { path = "../common/client-libs/mixnet-client" } nym-mixnode-common = { path = "../common/mixnode-common" } nym-network-defaults = { path = "../common/network-defaults" } nym-network-requester = { path = "../service-providers/network-requester" } -nym-node-http-api = { path = "../nym-node/nym-node-http-api" } nym-sdk = { path = "../sdk/rust/nym-sdk" } nym-sphinx = { path = "../common/nymsphinx" } nym-statistics-common = { path = "../common/statistics" } @@ -67,13 +66,13 @@ nym-topology = { path = "../common/topology" } nym-types = { path = "../common/types" } nym-validator-client = { path = "../common/client-libs/validator-client" } nym-ip-packet-router = { path = "../service-providers/ip-packet-router" } +nym-node-metrics = { path = "../nym-node/nym-node-metrics" } nym-wireguard = { path = "../common/wireguard" } nym-wireguard-types = { path = "../common/wireguard-types", default-features = false } defguard_wireguard_rs = { workspace = true } - [build-dependencies] tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } sqlx = { workspace = true, features = [ diff --git a/gateway/src/config.rs b/gateway/src/config.rs index 0c2d7568d0f..7d62118daa6 100644 --- a/gateway/src/config.rs +++ b/gateway/src/config.rs @@ -1,36 +1,19 @@ // Copyright 2024 - Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: GPL-3.0-only -use nym_network_defaults::{DEFAULT_NYM_NODE_HTTP_PORT, TICKETBOOK_VALIDITY_DAYS}; -use std::net::{IpAddr, Ipv4Addr, SocketAddr}; -use std::path::PathBuf; +use nym_network_defaults::TICKETBOOK_VALIDITY_DAYS; +use std::net::SocketAddr; use std::time::Duration; use url::Url; -use zeroize::{Zeroize, ZeroizeOnDrop}; -// 'DEBUG' -// where applicable, the below are defined in milliseconds -const DEFAULT_PRESENCE_SENDING_DELAY: Duration = Duration::from_millis(10_000); -const DEFAULT_PACKET_FORWARDING_INITIAL_BACKOFF: Duration = Duration::from_millis(10_000); -const DEFAULT_PACKET_FORWARDING_MAXIMUM_BACKOFF: Duration = Duration::from_millis(300_000); -const DEFAULT_INITIAL_CONNECTION_TIMEOUT: Duration = Duration::from_millis(1_500); -const DEFAULT_MAXIMUM_CONNECTION_BUFFER_SIZE: usize = 2000; - -const DEFAULT_STORED_MESSAGE_FILENAME_LENGTH: u16 = 16; -const DEFAULT_MESSAGE_RETRIEVAL_LIMIT: i64 = 100; - -const DEFAULT_CLIENT_BANDWIDTH_MAX_FLUSHING_RATE: Duration = Duration::from_millis(5); -const DEFAULT_CLIENT_BANDWIDTH_MAX_DELTA_FLUSHING_AMOUNT: i64 = 512 * 1024; // 512kB +// TODO: can we move those away? +pub const DEFAULT_CLIENT_BANDWIDTH_MAX_FLUSHING_RATE: Duration = Duration::from_millis(5); +pub const DEFAULT_CLIENT_BANDWIDTH_MAX_DELTA_FLUSHING_AMOUNT: i64 = 512 * 1024; // 512kB #[derive(Debug)] pub struct Config { - pub host: Host, - - pub http: Http, - pub gateway: Gateway, - // pub storage_paths: GatewayPaths, pub network_requester: NetworkRequester, pub ip_packet_router: IpPacketRouter, @@ -39,18 +22,13 @@ pub struct Config { } impl Config { - #[allow(clippy::too_many_arguments)] - pub fn externally_loaded( - host: impl Into<Host>, - http: impl Into<Http>, + pub fn new( gateway: impl Into<Gateway>, network_requester: impl Into<NetworkRequester>, ip_packet_router: impl Into<IpPacketRouter>, debug: impl Into<Debug>, ) -> Self { Config { - host: host.into(), - http: http.into(), gateway: gateway.into(), network_requester: network_requester.into(), ip_packet_router: ip_packet_router.into(), @@ -65,96 +43,23 @@ impl Config { pub fn get_nyxd_urls(&self) -> Vec<Url> { self.gateway.nyxd_urls.clone() } - - pub fn get_cosmos_mnemonic(&self) -> bip39::Mnemonic { - self.gateway.cosmos_mnemonic.clone() - } -} - -// TODO: this is very much a WIP. we need proper ssl certificate support here -#[derive(Debug, PartialEq)] -pub struct Host { - /// Ip address(es) of this host, such as 1.1.1.1 that external clients will use for connections. - pub public_ips: Vec<IpAddr>, - - /// Optional hostname of this node, for example nymtech.net. - // TODO: this is temporary. to be replaced by pulling the data directly from the certs. - pub hostname: Option<String>, -} - -impl Host { - pub fn validate(&self) -> bool { - if self.public_ips.is_empty() { - return false; - } - - true - } -} - -#[derive(Debug, PartialEq)] -pub struct Http { - /// Socket address this node will use for binding its http API. - /// default: `0.0.0.0:8000` - pub bind_address: SocketAddr, - - /// Path to assets directory of custom landing page of this node. - pub landing_page_assets_path: Option<PathBuf>, -} - -impl Default for Http { - fn default() -> Self { - Http { - bind_address: SocketAddr::new( - IpAddr::V4(Ipv4Addr::UNSPECIFIED), - DEFAULT_NYM_NODE_HTTP_PORT, - ), - landing_page_assets_path: None, - } - } } -// we only really care about the mnemonic being zeroized -#[derive(Debug, PartialEq, Eq, Zeroize, ZeroizeOnDrop)] +#[derive(Debug, PartialEq, Eq)] pub struct Gateway { - /// Version of the gateway for which this configuration was created. - pub version: String, - - /// ID specifies the human readable ID of this particular gateway. - pub id: String, - - /// Indicates whether this gateway is accepting only coconut credentials for accessing the - /// the mixnet, or if it also accepts non-paying clients - pub only_coconut_credentials: bool, - - /// Address to which this mixnode will bind to and will be listening for packets. - #[zeroize(skip)] - pub listening_address: IpAddr, - - /// Port used for listening for all mixnet traffic. - /// (default: 1789) - pub mix_port: u16, - - /// Port used for listening for all client-related traffic. - /// (default: 9000) - pub clients_port: u16, + /// Indicates whether this gateway is accepting only zk-nym credentials for accessing the mixnet + /// or if it also accepts non-paying clients + pub enforce_zk_nyms: bool, - /// If applicable, announced port for listening for secure websocket client traffic. - /// (default: None) - pub clients_wss_port: Option<u16>, + /// Socket address this node will use for binding its client websocket API. + /// default: `0.0.0.0:9000` + pub websocket_bind_address: SocketAddr, /// Addresses to APIs from which the node gets the view of the network. - #[zeroize(skip)] pub nym_api_urls: Vec<Url>, /// Addresses to validators which the node uses to check for double spending of ERC20 tokens. - #[zeroize(skip)] pub nyxd_urls: Vec<Url>, - - /// Mnemonic of a cosmos wallet used in checking for double spending. - // #[deprecated(note = "move to storage")] - // TODO: I don't think this should be stored directly in the config... - pub cosmos_mnemonic: bip39::Mnemonic, } #[derive(Debug, PartialEq)] @@ -185,60 +90,21 @@ impl Default for IpPacketRouter { #[derive(Debug)] pub struct Debug { - /// Initial value of an exponential backoff to reconnect to dropped TCP connection when - /// forwarding sphinx packets. - pub packet_forwarding_initial_backoff: Duration, - - /// Maximum value of an exponential backoff to reconnect to dropped TCP connection when - /// forwarding sphinx packets. - pub packet_forwarding_maximum_backoff: Duration, - - /// Timeout for establishing initial connection when trying to forward a sphinx packet. - pub initial_connection_timeout: Duration, - - /// Maximum number of packets that can be stored waiting to get sent to a particular connection. - pub maximum_connection_buffer_size: usize, - - /// Delay between each subsequent presence data being sent. - // DEAD FIELD - pub presence_sending_delay: Duration, - - /// Length of filenames for new client messages. - // DEAD FIELD - pub stored_messages_filename_length: u16, - - /// Number of messages from offline client that can be pulled at once from the storage. - pub message_retrieval_limit: i64, - /// Defines maximum delay between client bandwidth information being flushed to the persistent storage. pub client_bandwidth_max_flushing_rate: Duration, /// Defines a maximum change in client bandwidth before it gets flushed to the persistent storage. pub client_bandwidth_max_delta_flushing_amount: i64, - /// Specifies whether the mixnode should be using the legacy framing for the sphinx packets. - // it's set to true by default. The reason for that decision is to preserve compatibility with the - // existing nodes whilst everyone else is upgrading and getting the code for handling the new field. - // It shall be disabled in the subsequent releases. - pub use_legacy_framed_packet_version: bool, - pub zk_nym_tickets: ZkNymTicketHandlerDebug, } impl Default for Debug { fn default() -> Self { Debug { - packet_forwarding_initial_backoff: DEFAULT_PACKET_FORWARDING_INITIAL_BACKOFF, - packet_forwarding_maximum_backoff: DEFAULT_PACKET_FORWARDING_MAXIMUM_BACKOFF, - initial_connection_timeout: DEFAULT_INITIAL_CONNECTION_TIMEOUT, - presence_sending_delay: DEFAULT_PRESENCE_SENDING_DELAY, - maximum_connection_buffer_size: DEFAULT_MAXIMUM_CONNECTION_BUFFER_SIZE, - stored_messages_filename_length: DEFAULT_STORED_MESSAGE_FILENAME_LENGTH, - message_retrieval_limit: DEFAULT_MESSAGE_RETRIEVAL_LIMIT, client_bandwidth_max_flushing_rate: DEFAULT_CLIENT_BANDWIDTH_MAX_FLUSHING_RATE, client_bandwidth_max_delta_flushing_amount: DEFAULT_CLIENT_BANDWIDTH_MAX_DELTA_FLUSHING_AMOUNT, - use_legacy_framed_packet_version: false, zk_nym_tickets: Default::default(), } } diff --git a/gateway/src/error.rs b/gateway/src/error.rs index fbd85c4654b..474d2cbf64c 100644 --- a/gateway/src/error.rs +++ b/gateway/src/error.rs @@ -3,7 +3,7 @@ use nym_authenticator::error::AuthenticatorError; use nym_gateway_stats_storage::error::StatsStorageError; -use nym_gateway_storage::error::StorageError; +use nym_gateway_storage::error::GatewayStorageError; use nym_ip_packet_router::error::IpPacketRouterError; use nym_network_requester::error::{ClientCoreError, NetworkRequesterError}; use nym_validator_client::nyxd::error::NyxdError; @@ -38,7 +38,7 @@ pub enum GatewayError { #[error("storage failure: {source}")] StorageError { #[from] - source: StorageError, + source: GatewayStorageError, }, #[error("stats storage failure: {source}")] @@ -74,14 +74,8 @@ pub enum GatewayError { source: AuthenticatorError, }, - #[error("failed to startup local network requester")] - NetworkRequesterStartupFailure, - - #[error("failed to startup local ip packet router")] - IpPacketRouterStartupFailure, - - #[error("failed to startup local authenticator")] - AuthenticatorStartupFailure, + #[error("failed to startup local {typ}")] + ServiceProviderStartupFailure { typ: &'static str }, #[error("there are no nym API endpoints available")] NoNymApisAvailable, diff --git a/gateway/src/lib.rs b/gateway/src/lib.rs index 0f19d6f7c50..abac82f783d 100644 --- a/gateway/src/lib.rs +++ b/gateway/src/lib.rs @@ -9,4 +9,4 @@ pub mod error; pub mod node; pub use error::GatewayError; -pub use node::Gateway; +pub use node::GatewayTasksBuilder; diff --git a/gateway/src/node/client_handling/active_clients.rs b/gateway/src/node/client_handling/active_clients.rs index fbf83ce92cf..3dacfd3f63f 100644 --- a/gateway/src/node/client_handling/active_clients.rs +++ b/gateway/src/node/client_handling/active_clients.rs @@ -5,8 +5,6 @@ use super::websocket::message_receiver::{IsActiveRequestSender, MixMessageSender use crate::node::client_handling::embedded_clients::LocalEmbeddedClientHandle; use dashmap::DashMap; use nym_sphinx::DestinationAddressBytes; -use nym_statistics_common::gateways; -use nym_statistics_common::gateways::GatewayStatsReporter; use std::sync::Arc; use tracing::warn; @@ -34,10 +32,9 @@ impl ActiveClient { } } -#[derive(Clone)] -pub(crate) struct ActiveClientsStore { +#[derive(Clone, Default)] +pub struct ActiveClientsStore { inner: Arc<DashMap<DestinationAddressBytes, ActiveClient>>, - stats_event_reporter: GatewayStatsReporter, } #[derive(Clone)] @@ -51,11 +48,8 @@ pub(crate) struct ClientIncomingChannels { impl ActiveClientsStore { /// Creates new instance of `ActiveClientsStore` to store in-memory handles to all currently connected clients. - pub(crate) fn new(stats_event_reporter: GatewayStatsReporter) -> Self { - ActiveClientsStore { - inner: Arc::new(DashMap::new()), - stats_event_reporter, - } + pub fn new() -> Self { + Self::default() } /// Tries to obtain sending channel to specified client. Note that if stale entry existed, it is @@ -64,7 +58,7 @@ impl ActiveClientsStore { /// # Arguments /// /// * `client`: address of the client for which to obtain the handle. - pub(crate) fn get_sender(&self, client: DestinationAddressBytes) -> Option<MixMessageSender> { + pub fn get_sender(&self, client: DestinationAddressBytes) -> Option<MixMessageSender> { let entry = self.inner.get(&client)?; let handle = entry.value().get_sender(); @@ -130,8 +124,6 @@ impl ActiveClientsStore { /// * `client`: address of the client for which to remove the handle. pub(crate) fn disconnect(&self, client: DestinationAddressBytes) { self.inner.remove(&client); - self.stats_event_reporter - .report(gateways::GatewayStatsEvent::new_session_stop(client)); } /// Insert new client handle into the store. @@ -153,12 +145,10 @@ impl ActiveClientsStore { if self.inner.insert(client, entry).is_some() { panic!("inserted a duplicate remote client") } - self.stats_event_reporter - .report(gateways::GatewayStatsEvent::new_session_start(client)); } /// Inserts a handle to the embedded client - pub(crate) fn insert_embedded(&self, local_client_handle: LocalEmbeddedClientHandle) { + pub fn insert_embedded(&self, local_client_handle: LocalEmbeddedClientHandle) { let key = local_client_handle.client_destination(); let entry = ActiveClient::Embedded(local_client_handle); if self.inner.insert(key, entry).is_some() { diff --git a/gateway/src/node/client_handling/embedded_clients/mod.rs b/gateway/src/node/client_handling/embedded_clients/mod.rs index 7974bb9c419..95af759ed91 100644 --- a/gateway/src/node/client_handling/embedded_clients/mod.rs +++ b/gateway/src/node/client_handling/embedded_clients/mod.rs @@ -9,10 +9,11 @@ use nym_network_requester::{GatewayPacketRouter, PacketRouter}; use nym_sphinx::addressing::clients::Recipient; use nym_sphinx::DestinationAddressBytes; use nym_task::TaskClient; +use tokio::task::JoinHandle; use tracing::{debug, error, trace}; #[derive(Debug)] -pub(crate) struct LocalEmbeddedClientHandle { +pub struct LocalEmbeddedClientHandle { /// Nym address of the embedded client. pub(crate) address: Recipient, @@ -52,8 +53,8 @@ impl MessageRouter { } } - pub(crate) fn start_with_shutdown(self, shutdown: TaskClient) { - tokio::spawn(self.run_with_shutdown(shutdown)); + pub(crate) fn start_with_shutdown(self, shutdown: TaskClient) -> JoinHandle<()> { + tokio::spawn(self.run_with_shutdown(shutdown)) } fn handle_received_messages(&self, messages: Vec<Vec<u8>>) { diff --git a/gateway/src/node/client_handling/websocket/common_state.rs b/gateway/src/node/client_handling/websocket/common_state.rs index ec0a3eb1d15..4ff8b8991a2 100644 --- a/gateway/src/node/client_handling/websocket/common_state.rs +++ b/gateway/src/node/client_handling/websocket/common_state.rs @@ -1,18 +1,23 @@ // Copyright 2024 - Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: GPL-3.0-only +use crate::node::ActiveClientsStore; use nym_credential_verification::{ecash::EcashManager, BandwidthFlushingBehaviourConfig}; use nym_crypto::asymmetric::identity; -use nym_statistics_common::gateways::GatewayStatsReporter; +use nym_gateway_storage::GatewayStorage; +use nym_mixnet_client::forwarder::MixForwardingSender; +use nym_node_metrics::events::MetricEventsSender; use std::sync::Arc; // I can see this being possible expanded with say storage or client store #[derive(Clone)] -pub(crate) struct CommonHandlerState<S> { - pub(crate) ecash_verifier: Arc<EcashManager<S>>, - pub(crate) storage: S, +pub(crate) struct CommonHandlerState { + pub(crate) ecash_verifier: Arc<EcashManager>, + pub(crate) storage: GatewayStorage, pub(crate) local_identity: Arc<identity::KeyPair>, pub(crate) only_coconut_credentials: bool, pub(crate) bandwidth_cfg: BandwidthFlushingBehaviourConfig, - pub(crate) stats_event_reporter: GatewayStatsReporter, + pub(crate) metrics_sender: MetricEventsSender, + pub(crate) outbound_mix_sender: MixForwardingSender, + pub(crate) active_clients_store: ActiveClientsStore, } diff --git a/gateway/src/node/client_handling/websocket/connection_handler/authenticated.rs b/gateway/src/node/client_handling/websocket/connection_handler/authenticated.rs index fd4f2001779..d7bc9dd32e3 100644 --- a/gateway/src/node/client_handling/websocket/connection_handler/authenticated.rs +++ b/gateway/src/node/client_handling/websocket/connection_handler/authenticated.rs @@ -24,9 +24,10 @@ use nym_gateway_requests::{ ClientControlRequest, ClientRequest, GatewayRequestsError, SensitiveServerResponse, SimpleGatewayRequestsError, }; -use nym_gateway_storage::{error::StorageError, Storage}; +use nym_gateway_storage::error::GatewayStorageError; +use nym_node_metrics::events::MetricsEvent; use nym_sphinx::forwarding::packet::MixPacket; -use nym_statistics_common::gateways; +use nym_statistics_common::gateways::GatewaySessionEvent; use nym_task::TaskClient; use nym_validator_client::coconut::EcashApiError; use rand::{random, CryptoRng, Rng}; @@ -39,7 +40,7 @@ use tracing::*; #[derive(Debug, Error)] pub enum RequestHandlingError { #[error("Internal gateway storage error")] - StorageError(#[from] StorageError), + StorageError(#[from] GatewayStorageError), #[error( "the database entry for bandwidth of the registered client {client_address} is missing!" @@ -136,9 +137,9 @@ impl IntoWSMessage for ServerResponse { } } -pub(crate) struct AuthenticatedHandler<R, S, St> { - inner: FreshHandler<R, S, St>, - bandwidth_storage_manager: BandwidthStorageManager<St>, +pub(crate) struct AuthenticatedHandler<R, S> { + inner: FreshHandler<R, S>, + bandwidth_storage_manager: BandwidthStorageManager, client: ClientDetails, mix_receiver: MixMessageReceiver, // Occasionally the handler is requested to ping the connected client for confirm that it's @@ -149,20 +150,13 @@ pub(crate) struct AuthenticatedHandler<R, S, St> { } // explicitly remove handle from the global store upon being dropped -impl<R, S, St> Drop for AuthenticatedHandler<R, S, St> { +impl<R, S> Drop for AuthenticatedHandler<R, S> { fn drop(&mut self) { - self.inner - .active_clients_store - .disconnect(self.client.address) + self.disconnect_client() } } -impl<R, S, St> AuthenticatedHandler<R, S, St> -where - // TODO: those trait bounds here don't really make sense.... - R: Rng + CryptoRng, - St: Storage + Clone + 'static, -{ +impl<R, S> AuthenticatedHandler<R, S> { /// Upgrades `FreshHandler` into the Authenticated variant implying the client is now authenticated /// and thus allowed to perform more actions with the gateway, such as redeeming bandwidth or /// sending sphinx packets. @@ -173,7 +167,7 @@ where /// * `client`: details (i.e. address and shared keys) of the registered client /// * `mix_receiver`: channel used for receiving messages from the mixnet destined for this client. pub(crate) async fn upgrade( - fresh: FreshHandler<R, S, St>, + fresh: FreshHandler<R, S>, client: ClientDetails, mix_receiver: MixMessageReceiver, is_active_request_receiver: IsActiveRequestReceiver, @@ -191,7 +185,7 @@ where client_address: client.address.as_base58_string(), })?; - Ok(AuthenticatedHandler { + let handler = AuthenticatedHandler { bandwidth_storage_manager: BandwidthStorageManager::new( fresh.shared_state.storage.clone(), ClientBandwidth::new(bandwidth.into()), @@ -204,14 +198,24 @@ where mix_receiver, is_active_request_receiver, is_active_ping_pending_reply: None, - }) + }; + handler.send_metrics(GatewaySessionEvent::new_session_start( + handler.client.address, + )); + + Ok(handler) } - /// Explicitly removes handle from the global store. - fn disconnect(self) { + fn disconnect_client(&mut self) { self.inner + .shared_state .active_clients_store - .disconnect(self.client.address) + .disconnect(self.client.address); + self.send_metrics(GatewaySessionEvent::new_session_stop(self.client.address)); + } + + fn send_metrics(&self, event: impl Into<MetricsEvent>) { + self.inner.send_metrics(event) } /// Forwards the received mix packet from the client into the mix network. @@ -220,7 +224,12 @@ where /// /// * `mix_packet`: packet received from the client that should get forwarded into the network. fn forward_packet(&self, mix_packet: MixPacket) { - if let Err(err) = self.inner.outbound_mix_sender.unbounded_send(mix_packet) { + if let Err(err) = self + .inner + .shared_state + .outbound_mix_sender + .forward_packet(mix_packet) + { error!("We failed to forward requested mix packet - {err}. Presumably our mix forwarder has crashed. We cannot continue."); process::exit(1); } @@ -259,8 +268,8 @@ where trace!("available total bandwidth: {available_total}"); if let Ok(ticket_type) = maybe_ticket_type { - self.inner.shared_state.stats_event_reporter.report( - gateways::GatewayStatsEvent::new_ecash_ticket(self.client.address, ticket_type), + self.inner.shared_state.metrics_sender.report_unchecked( + GatewaySessionEvent::new_ecash_ticket(self.client.address, ticket_type), ); } else { error!("Somehow verified a ticket with an unknown ticket type"); @@ -372,7 +381,10 @@ where /// # Arguments /// /// * `raw_request`: raw message to handle. - async fn handle_text(&mut self, raw_request: String) -> Message { + async fn handle_text(&mut self, raw_request: String) -> Message + where + R: Rng + CryptoRng, + { trace!("text request"); let request = match ClientControlRequest::try_from(raw_request) { @@ -470,7 +482,10 @@ where client = %self.client.address.as_base58_string() ) )] - async fn handle_request(&mut self, raw_request: Message) -> Option<Message> { + async fn handle_request(&mut self, raw_request: Message) -> Option<Message> + where + R: Rng + CryptoRng, + { trace!("new request"); // apparently tungstenite auto-handles ping/pong/close messages so for now let's ignore @@ -542,8 +557,8 @@ where /// and for sphinx packets received from the mix network that should be sent back to the client. pub(crate) async fn listen_for_requests(mut self, mut shutdown: TaskClient) where + R: Rng + CryptoRng, S: AsyncRead + AsyncWrite + Unpin, - St: Storage, { trace!("Started listening for ALL incoming requests..."); @@ -612,7 +627,6 @@ where } } - self.disconnect(); trace!("The stream was closed!"); } } diff --git a/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs b/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs index a723a5ca2d0..188699c64ca 100644 --- a/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs +++ b/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs @@ -3,14 +3,9 @@ use crate::node::client_handling::websocket::common_state::CommonHandlerState; use crate::node::client_handling::websocket::connection_handler::INITIAL_MESSAGE_TIMEOUT; -use crate::node::client_handling::{ - active_clients::ActiveClientsStore, - websocket::{ - connection_handler::{ - AuthenticatedHandler, ClientDetails, InitialAuthResult, SocketStream, - }, - message_receiver::{IsActive, IsActiveRequestSender}, - }, +use crate::node::client_handling::websocket::{ + connection_handler::{AuthenticatedHandler, ClientDetails, InitialAuthResult, SocketStream}, + message_receiver::{IsActive, IsActiveRequestSender}, }; use futures::{ channel::{mpsc, oneshot}, @@ -27,11 +22,11 @@ use nym_gateway_requests::{ types::{ClientControlRequest, ServerResponse}, BinaryResponse, SharedGatewayKey, CURRENT_PROTOCOL_VERSION, INITIAL_PROTOCOL_VERSION, }; -use nym_gateway_storage::{error::StorageError, Storage}; -use nym_mixnet_client::forwarder::MixForwardingSender; +use nym_gateway_storage::error::GatewayStorageError; +use nym_node_metrics::events::MetricsEvent; use nym_sphinx::DestinationAddressBytes; use nym_task::TaskClient; -use rand::{CryptoRng, Rng}; +use rand::CryptoRng; use std::net::SocketAddr; use std::time::Duration; use thiserror::Error; @@ -43,7 +38,7 @@ use tracing::*; #[derive(Debug, Error)] pub(crate) enum InitialAuthenticationError { #[error("Internal gateway storage error")] - StorageError(#[from] StorageError), + StorageError(#[from] GatewayStorageError), #[error( "our datastore is corrupted. the stored key for client {client_id} is malformed: {source}" @@ -51,7 +46,7 @@ pub(crate) enum InitialAuthenticationError { MalformedStoredSharedKey { client_id: String, #[source] - source: StorageError, + source: GatewayStorageError, }, #[error("Failed to perform registration handshake: {0}")] @@ -107,11 +102,9 @@ pub(crate) enum InitialAuthenticationError { EmptyClientDetails, } -pub(crate) struct FreshHandler<R, S, St> { +pub(crate) struct FreshHandler<R, S> { rng: R, - pub(crate) shared_state: CommonHandlerState<St>, - pub(crate) active_clients_store: ActiveClientsStore, - pub(crate) outbound_mix_sender: MixForwardingSender, + pub(crate) shared_state: CommonHandlerState, pub(crate) socket_connection: SocketStream<S>, pub(crate) peer_address: SocketAddr, pub(crate) shutdown: TaskClient, @@ -120,11 +113,7 @@ pub(crate) struct FreshHandler<R, S, St> { pub(crate) negotiated_protocol: Option<u8>, } -impl<R, S, St> FreshHandler<R, S, St> -where - R: Rng + CryptoRng, - St: Storage + Clone + 'static, -{ +impl<R, S> FreshHandler<R, S> { // for time being we assume handle is always constructed from raw socket. // if we decide we want to change it, that's not too difficult // also at this point I'm not entirely sure how to deal with this warning without @@ -133,16 +122,12 @@ where pub(crate) fn new( rng: R, conn: S, - outbound_mix_sender: MixForwardingSender, - active_clients_store: ActiveClientsStore, - shared_state: CommonHandlerState<St>, + shared_state: CommonHandlerState, peer_address: SocketAddr, shutdown: TaskClient, ) -> Self { FreshHandler { rng, - active_clients_store, - outbound_mix_sender, socket_connection: SocketStream::RawTcp(conn), peer_address, negotiated_protocol: None, @@ -151,6 +136,10 @@ where } } + pub(crate) fn send_metrics(&self, event: impl Into<MetricsEvent>) { + self.shared_state.metrics_sender.report_unchecked(event) + } + /// Attempts to perform websocket handshake with the remote and upgrades the raw TCP socket /// to the framed WebSocket. pub(crate) async fn perform_websocket_handshake(&mut self) -> Result<(), WsError> @@ -494,7 +483,7 @@ where // The other handler reported that the client is not active, so we can // disconnect the other client and continue with this connection. debug!("Other handler reports it is not active"); - self.active_clients_store.disconnect(address); + self.shared_state.active_clients_store.disconnect(address); } IsActive::Active => { // The other handled reported a positive reply, so we have to assume it's @@ -513,14 +502,14 @@ where Ok(Err(_)) => { // Other channel failed to reply (the channel sender probably dropped) info!("Other connection failed to reply, disconnecting it in favour of this new connection"); - self.active_clients_store.disconnect(address); + self.shared_state.active_clients_store.disconnect(address); } Err(_) => { // Timeout waiting for reply warn!( "Other connection timed out, disconnecting it in favour of this new connection" ); - self.active_clients_store.disconnect(address); + self.shared_state.active_clients_store.disconnect(address); } } Ok(()) @@ -562,7 +551,11 @@ where .map_err(InitialAuthenticationError::MalformedIV)?; // Check for duplicate clients - if let Some(client_tx) = self.active_clients_store.get_remote_client(address) { + if let Some(client_tx) = self + .shared_state + .active_clients_store + .get_remote_client(address) + { warn!("Detected duplicate connection for client: {address}"); self.handle_duplicate_client(address, client_tx.is_active_request_sender) .await?; @@ -678,7 +671,11 @@ where debug!(remote_client = %remote_identity); - if self.active_clients_store.is_active(remote_address) { + if self + .shared_state + .active_clients_store + .is_active(remote_address) + { return Err(InitialAuthenticationError::DuplicateConnection); } @@ -786,7 +783,7 @@ where pub(crate) async fn handle_until_authenticated_or_failure( mut self, shutdown: &mut TaskClient, - ) -> Option<AuthenticatedHandler<R, S, St>> + ) -> Option<AuthenticatedHandler<R, S>> where S: AsyncRead + AsyncWrite + Unpin + Send, R: CryptoRng + RngCore + Send, @@ -822,7 +819,7 @@ where let (mix_sender, mix_receiver) = mpsc::unbounded(); // Channel for handlers to ask other handlers if they are still active. let (is_active_request_sender, is_active_request_receiver) = mpsc::unbounded(); - self.active_clients_store.insert_remote( + self.shared_state.active_clients_store.insert_remote( registration_details.address, mix_sender, is_active_request_sender, diff --git a/gateway/src/node/client_handling/websocket/connection_handler/mod.rs b/gateway/src/node/client_handling/websocket/connection_handler/mod.rs index caac41fba34..478b0ed6ebe 100644 --- a/gateway/src/node/client_handling/websocket/connection_handler/mod.rs +++ b/gateway/src/node/client_handling/websocket/connection_handler/mod.rs @@ -5,7 +5,6 @@ use crate::config::Config; use nym_credential_verification::BandwidthFlushingBehaviourConfig; use nym_gateway_requests::shared_key::SharedGatewayKey; use nym_gateway_requests::ServerResponse; -use nym_gateway_storage::Storage; use nym_sphinx::DestinationAddressBytes; use rand::{CryptoRng, Rng}; use std::time::Duration; @@ -90,11 +89,10 @@ impl InitialAuthResult { // imo there's no point in including the peer address in anything higher than debug #[instrument(level = "debug", skip_all, fields(peer = %handle.peer_address))] -pub(crate) async fn handle_connection<R, S, St>(mut handle: FreshHandler<R, S, St>) +pub(crate) async fn handle_connection<R, S>(mut handle: FreshHandler<R, S>) where R: Rng + CryptoRng + Send, S: AsyncRead + AsyncWrite + Unpin + Send, - St: Storage + Clone + 'static, { // don't accept any new requests if we have already received shutdown if handle.shutdown.is_shutdown() { diff --git a/gateway/src/node/client_handling/websocket/listener.rs b/gateway/src/node/client_handling/websocket/listener.rs index 425123d3e9a..ed484d17b65 100644 --- a/gateway/src/node/client_handling/websocket/listener.rs +++ b/gateway/src/node/client_handling/websocket/listener.rs @@ -1,41 +1,37 @@ // Copyright 2020 - Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: GPL-3.0-only -use crate::node::client_handling::active_clients::ActiveClientsStore; use crate::node::client_handling::websocket::common_state::CommonHandlerState; use crate::node::client_handling::websocket::connection_handler::FreshHandler; -use nym_gateway_storage::Storage; -use nym_mixnet_client::forwarder::MixForwardingSender; +use nym_task::TaskClient; use rand::rngs::OsRng; use std::net::SocketAddr; use std::process; use tokio::task::JoinHandle; use tracing::*; -pub(crate) struct Listener<S> { +pub struct Listener { address: SocketAddr, - shared_state: CommonHandlerState<S>, + shared_state: CommonHandlerState, + shutdown: TaskClient, } -impl<S> Listener<S> -where - S: Storage + Send + Sync + Clone + 'static, -{ - pub(crate) fn new(address: SocketAddr, shared_state: CommonHandlerState<S>) -> Self { +impl Listener { + pub(crate) fn new( + address: SocketAddr, + shared_state: CommonHandlerState, + shutdown: TaskClient, + ) -> Self { Listener { address, shared_state, + shutdown, } } // TODO: change the signature to pub(crate) async fn run(&self, handler: Handler) - pub(crate) async fn run( - &mut self, - outbound_mix_sender: MixForwardingSender, - active_clients_store: ActiveClientsStore, - mut shutdown: nym_task::TaskClient, - ) { + pub(crate) async fn run(&mut self) { info!("Starting websocket listener at {}", self.address); let tcp_listener = match tokio::net::TcpListener::bind(self.address).await { Ok(listener) => listener, @@ -45,24 +41,22 @@ where } }; - while !shutdown.is_shutdown() { + while !self.shutdown.is_shutdown() { tokio::select! { biased; - _ = shutdown.recv() => { + _ = self.shutdown.recv() => { trace!("client_handling::Listener: received shutdown"); } connection = tcp_listener.accept() => { match connection { Ok((socket, remote_addr)) => { - let shutdown = shutdown.clone().named(format!("ClientConnectionHandler_{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? let handle = FreshHandler::new( OsRng, socket, - outbound_mix_sender.clone(), - active_clients_store.clone(), self.shared_state.clone(), remote_addr, shutdown, @@ -77,15 +71,7 @@ where } } - pub(crate) fn start( - mut self, - outbound_mix_sender: MixForwardingSender, - active_clients_store: ActiveClientsStore, - shutdown: nym_task::TaskClient, - ) -> JoinHandle<()> { - tokio::spawn(async move { - self.run(outbound_mix_sender, active_clients_store, shutdown) - .await - }) + pub fn start(mut self) -> JoinHandle<()> { + tokio::spawn(async move { self.run().await }) } } diff --git a/gateway/src/node/helpers.rs b/gateway/src/node/helpers.rs deleted file mode 100644 index d0862f1688e..00000000000 --- a/gateway/src/node/helpers.rs +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright 2023 - Nym Technologies SA <contact@nymtech.net> -// SPDX-License-Identifier: GPL-3.0-only - -use async_trait::async_trait; -use nym_sdk::{NymApiTopologyProvider, NymApiTopologyProviderConfig, UserAgent}; -use nym_topology::{gateway, NymTopology, TopologyProvider}; -use std::sync::Arc; -use tokio::sync::Mutex; -use tracing::debug; -use url::Url; - -#[derive(Clone)] -pub struct GatewayTopologyProvider { - inner: Arc<Mutex<GatewayTopologyProviderInner>>, -} - -impl GatewayTopologyProvider { - pub fn new( - gateway_node: gateway::LegacyNode, - user_agent: UserAgent, - nym_api_url: Vec<Url>, - ) -> GatewayTopologyProvider { - GatewayTopologyProvider { - inner: Arc::new(Mutex::new(GatewayTopologyProviderInner { - inner: NymApiTopologyProvider::new( - NymApiTopologyProviderConfig { - min_mixnode_performance: 50, - min_gateway_performance: 0, - }, - nym_api_url, - env!("CARGO_PKG_VERSION").to_string(), - Some(user_agent), - ), - gateway_node, - })), - } - } -} - -struct GatewayTopologyProviderInner { - inner: NymApiTopologyProvider, - gateway_node: gateway::LegacyNode, -} - -#[async_trait] -impl TopologyProvider for GatewayTopologyProvider { - async fn get_new_topology(&mut self) -> Option<NymTopology> { - let mut guard = self.inner.lock().await; - match guard.inner.get_new_topology().await { - None => None, - Some(mut base) => { - if !base.gateway_exists(&guard.gateway_node.identity_key) { - debug!( - "{} didn't exist in topology. inserting it.", - guard.gateway_node.identity_key - ); - base.insert_gateway(guard.gateway_node.clone()); - } - Some(base) - } - } - } -} diff --git a/gateway/src/node/internal_service_providers.rs b/gateway/src/node/internal_service_providers.rs new file mode 100644 index 00000000000..b26d9f562f8 --- /dev/null +++ b/gateway/src/node/internal_service_providers.rs @@ -0,0 +1,226 @@ +// Copyright 2024 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: GPL-3.0-only + +use crate::node::client_handling::embedded_clients::{LocalEmbeddedClientHandle, MessageRouter}; +use crate::node::client_handling::websocket::message_receiver::{ + MixMessageReceiver, MixMessageSender, +}; +use crate::GatewayError; +use async_trait::async_trait; +use futures::channel::{mpsc, oneshot}; +use nym_authenticator::Authenticator; +use nym_crypto::asymmetric::ed25519; +use nym_ip_packet_router::error::IpPacketRouterError; +use nym_ip_packet_router::IpPacketRouter; +use nym_mixnet_client::forwarder::MixForwardingSender; +use nym_network_requester::error::NetworkRequesterError; +use nym_network_requester::NRServiceProviderBuilder; +use nym_sdk::mixnet::Recipient; +use nym_sdk::{GatewayTransceiver, LocalGateway, PacketRouter}; +use nym_task::TaskClient; +use std::fmt::Display; +use tokio::task::JoinHandle; +use tracing::error; + +pub trait LocalRecipient { + fn address(&self) -> Recipient; +} + +impl LocalRecipient for nym_network_requester::core::OnStartData { + fn address(&self) -> Recipient { + self.address + } +} + +impl LocalRecipient for nym_ip_packet_router::OnStartData { + fn address(&self) -> Recipient { + self.address + } +} + +impl LocalRecipient for nym_authenticator::OnStartData { + fn address(&self) -> Recipient { + self.address + } +} + +#[async_trait] +pub trait RunnableServiceProvider { + const NAME: &'static str; + + type OnStartData: LocalRecipient; + type Error; + async fn run_service_provider(self) -> Result<(), Self::Error>; +} + +#[async_trait] +impl RunnableServiceProvider for NRServiceProviderBuilder { + const NAME: &'static str = "network requester"; + type OnStartData = nym_network_requester::core::OnStartData; + type Error = NetworkRequesterError; + + async fn run_service_provider(self) -> Result<(), Self::Error> { + self.run_service_provider().await + } +} + +#[async_trait] +impl RunnableServiceProvider for IpPacketRouter { + const NAME: &'static str = "ip router"; + type OnStartData = nym_ip_packet_router::OnStartData; + type Error = IpPacketRouterError; + + async fn run_service_provider(self) -> Result<(), Self::Error> { + self.run_service_provider().await + } +} + +#[async_trait] +impl RunnableServiceProvider for Authenticator { + const NAME: &'static str = "authenticator"; + type OnStartData = nym_authenticator::OnStartData; + type Error = nym_authenticator::error::AuthenticatorError; + + async fn run_service_provider(self) -> Result<(), Self::Error> { + self.run_service_provider().await + } +} + +pub struct ServiceProviderBeingBuilt<T: RunnableServiceProvider> { + on_start_rx: oneshot::Receiver<T::OnStartData>, + sp_builder: T, + sp_message_router_builder: SpMessageRouterBuilder, +} + +pub struct StartedServiceProvider<T: RunnableServiceProvider> { + pub sp_join_handle: JoinHandle<()>, + pub message_router_join_handle: JoinHandle<()>, + pub on_start_data: T::OnStartData, + pub handle: LocalEmbeddedClientHandle, +} + +impl<T> ServiceProviderBeingBuilt<T> +where + T: RunnableServiceProvider + Send + Sync + 'static, + T::Error: Display + Send + Sync + 'static, +{ + pub(crate) fn new( + on_start_rx: oneshot::Receiver<T::OnStartData>, + sp_builder: T, + sp_message_router_builder: SpMessageRouterBuilder, + ) -> Self { + ServiceProviderBeingBuilt { + on_start_rx, + sp_builder, + sp_message_router_builder, + } + } + + pub async fn start_service_provider( + mut self, + ) -> Result<StartedServiceProvider<T>, GatewayError> { + let sp_join_handle = tokio::task::spawn(async move { + if let Err(err) = self.sp_builder.run_service_provider().await { + error!( + "the {} service provider encountered an error: {err}", + T::NAME + ) + } + }); + + let on_start_data = self + .on_start_rx + .await + .map_err(|_| GatewayError::ServiceProviderStartupFailure { typ: T::NAME })?; + + // this should be instantaneous since the data is sent on this channel before the on start is called; + // the failure should be impossible + let Ok(Some(packet_router)) = self.sp_message_router_builder.router_receiver.try_recv() + else { + return Err(GatewayError::ServiceProviderStartupFailure { typ: T::NAME }); + }; + + let mix_sender = self.sp_message_router_builder.mix_sender(); + let message_router_join_handle = self + .sp_message_router_builder + .start_message_router(packet_router); + + Ok(StartedServiceProvider { + sp_join_handle, + message_router_join_handle, + handle: LocalEmbeddedClientHandle::new(on_start_data.address(), mix_sender), + on_start_data, + }) + } +} + +pub struct ExitServiceProviders { + pub(crate) network_requester: ServiceProviderBeingBuilt<NRServiceProviderBuilder>, + pub(crate) ip_router: ServiceProviderBeingBuilt<IpPacketRouter>, +} + +impl ExitServiceProviders { + pub async fn start_service_providers( + self, + ) -> Result< + ( + StartedServiceProvider<NRServiceProviderBuilder>, + StartedServiceProvider<IpPacketRouter>, + ), + GatewayError, + > { + let started_nr = self.network_requester.start_service_provider().await?; + let started_ipr = self.ip_router.start_service_provider().await?; + + Ok((started_nr, started_ipr)) + } +} + +pub struct SpMessageRouterBuilder { + mix_sender: Option<MixMessageSender>, + mix_receiver: MixMessageReceiver, + router_receiver: oneshot::Receiver<PacketRouter>, + gateway_transceiver: Option<LocalGateway>, + shutdown: TaskClient, +} + +impl SpMessageRouterBuilder { + pub(crate) fn new( + node_identity: ed25519::PublicKey, + forwarding_channel: MixForwardingSender, + shutdown: TaskClient, + ) -> Self { + let (mix_sender, mix_receiver) = mpsc::unbounded(); + let (router_tx, router_rx) = oneshot::channel(); + + let transceiver = LocalGateway::new(node_identity, forwarding_channel, router_tx); + + SpMessageRouterBuilder { + mix_sender: Some(mix_sender), + mix_receiver, + router_receiver: router_rx, + gateway_transceiver: Some(transceiver), + shutdown, + } + } + + #[allow(clippy::expect_used)] + pub(crate) fn gateway_transceiver(&mut self) -> Box<dyn GatewayTransceiver + Send + Sync> { + Box::new( + self.gateway_transceiver + .take() + .expect("attempting to use the same gateway transceiver twice"), + ) + } + + #[allow(clippy::expect_used)] + fn mix_sender(&mut self) -> MixMessageSender { + self.mix_sender + .take() + .expect("attempting to use the same mix sender twice") + } + + fn start_message_router(self, packet_router: PacketRouter) -> JoinHandle<()> { + MessageRouter::new(self.mix_receiver, packet_router).start_with_shutdown(self.shutdown) + } +} diff --git a/gateway/src/node/mixnet_handling/receiver/connection_handler.rs b/gateway/src/node/mixnet_handling/receiver/connection_handler.rs index 4cb1a23c705..62e8274af8e 100644 --- a/gateway/src/node/mixnet_handling/receiver/connection_handler.rs +++ b/gateway/src/node/mixnet_handling/receiver/connection_handler.rs @@ -6,7 +6,7 @@ use crate::node::client_handling::websocket::message_receiver::MixMessageSender; use crate::node::mixnet_handling::receiver::packet_processing::PacketProcessor; use futures::channel::mpsc::SendError; use futures::StreamExt; -use nym_gateway_storage::{error::StorageError, Storage}; +use nym_gateway_storage::{error::GatewayStorageError, GatewayStorage}; use nym_mixnet_client::forwarder::MixForwardingSender; use nym_sphinx::forwarding::packet::MixPacket; use nym_sphinx::framing::codec::NymCodec; @@ -30,7 +30,7 @@ enum CriticalPacketProcessingError { AckForwardingFailure { source: SendError }, } -pub(crate) struct ConnectionHandler<St: Storage> { +pub(crate) struct ConnectionHandler { packet_processor: PacketProcessor, // TODO: investigate performance trade-offs for whether this cache even makes sense @@ -39,11 +39,11 @@ pub(crate) struct ConnectionHandler<St: Storage> { // and each `get` internally copies the channel, however, is it really that expensive? clients_store_cache: HashMap<DestinationAddressBytes, MixMessageSender>, active_clients_store: ActiveClientsStore, - storage: St, + storage: GatewayStorage, ack_sender: MixForwardingSender, } -impl<St: Storage + Clone> Clone for ConnectionHandler<St> { +impl Clone for ConnectionHandler { fn clone(&self) -> Self { // remove stale entries from the cache while cloning let mut clients_store_cache = HashMap::with_capacity(self.clients_store_cache.capacity()); @@ -63,10 +63,10 @@ impl<St: Storage + Clone> Clone for ConnectionHandler<St> { } } -impl<St: Storage> ConnectionHandler<St> { +impl ConnectionHandler { pub(crate) fn new( packet_processor: PacketProcessor, - storage: St, + storage: GatewayStorage, ack_sender: MixForwardingSender, active_clients_store: ActiveClientsStore, ) -> Self { @@ -123,7 +123,7 @@ impl<St: Storage> ConnectionHandler<St> { &self, client_address: DestinationAddressBytes, message: Vec<u8>, - ) -> Result<(), StorageError> { + ) -> Result<(), GatewayStorageError> { debug!("Storing received message for {client_address} on the disk...",); self.storage.store_message(client_address, message).await @@ -137,14 +137,9 @@ impl<St: Storage> ConnectionHandler<St> { if let Some(forward_ack) = forward_ack { let next_hop = forward_ack.next_hop(); trace!("Sending ack from packet for {client_address} to {next_hop}",); - self.ack_sender - .unbounded_send(forward_ack) - .map_err( - |source| CriticalPacketProcessingError::AckForwardingFailure { - source: source.into_send_error(), - }, - )?; + .forward_packet(forward_ack) + .map_err(|source| CriticalPacketProcessingError::AckForwardingFailure { source })?; } Ok(()) } diff --git a/gateway/src/node/mixnet_handling/receiver/listener.rs b/gateway/src/node/mixnet_handling/receiver/listener.rs index 86b99108efc..17dfe00cd80 100644 --- a/gateway/src/node/mixnet_handling/receiver/listener.rs +++ b/gateway/src/node/mixnet_handling/receiver/listener.rs @@ -2,7 +2,6 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::node::mixnet_handling::receiver::connection_handler::ConnectionHandler; -use nym_gateway_storage::Storage; use nym_task::TaskClient; use std::net::SocketAddr; use std::process; @@ -20,10 +19,7 @@ impl Listener { Listener { address, shutdown } } - pub(crate) async fn run<St>(&mut self, connection_handler: ConnectionHandler<St>) - where - St: Storage + Clone + 'static, - { + pub(crate) async fn run(&mut self, connection_handler: ConnectionHandler) { info!("Starting mixnet listener at {}", self.address); let tcp_listener = match tokio::net::TcpListener::bind(self.address).await { Ok(listener) => listener, @@ -52,10 +48,7 @@ impl Listener { } } - pub(crate) fn start<St>(mut self, connection_handler: ConnectionHandler<St>) -> JoinHandle<()> - where - St: Storage + Clone + 'static, - { + pub(crate) fn start(mut self, connection_handler: ConnectionHandler) -> JoinHandle<()> { info!("Running mix listener on {:?}", self.address.to_string()); tokio::spawn(async move { self.run(connection_handler).await }) diff --git a/gateway/src/node/mod.rs b/gateway/src/node/mod.rs index c399deefa9a..84b19903514 100644 --- a/gateway/src/node/mod.rs +++ b/gateway/src/node/mod.rs @@ -1,59 +1,43 @@ -// Copyright 2020-2023 - Nym Technologies SA <contact@nymtech.net> +// Copyright 2020-2024 - Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: GPL-3.0-only use crate::config::Config; use crate::error::GatewayError; -use crate::node::client_handling::active_clients::ActiveClientsStore; -use crate::node::client_handling::embedded_clients::{LocalEmbeddedClientHandle, MessageRouter}; use crate::node::client_handling::websocket; -use crate::node::helpers::GatewayTopologyProvider; -use crate::node::mixnet_handling::receiver::connection_handler::ConnectionHandler; -use futures::channel::{mpsc, oneshot}; +use crate::node::internal_service_providers::{ + ExitServiceProviders, ServiceProviderBeingBuilt, SpMessageRouterBuilder, +}; +use futures::channel::oneshot; +use nym_authenticator::Authenticator; use nym_credential_verification::ecash::{ credential_sender::CredentialHandlerConfig, EcashManager, }; -use nym_crypto::asymmetric::{encryption, identity}; -use nym_mixnet_client::forwarder::{MixForwardingSender, PacketForwarder}; +use nym_crypto::asymmetric::ed25519; +use nym_gateway_storage::models::WireguardPeer; +use nym_ip_packet_router::IpPacketRouter; +use nym_mixnet_client::forwarder::MixForwardingSender; use nym_network_defaults::NymNetworkDetails; -use nym_network_requester::{LocalGateway, NRServiceProviderBuilder}; -use nym_node_http_api::state::metrics::SharedSessionStats; -use nym_statistics_common::gateways::{self, GatewayStatsReporter}; -use nym_task::{TaskClient, TaskHandle, TaskManager}; -use nym_topology::NetworkAddress; -use nym_validator_client::client::NodeId; +use nym_network_requester::NRServiceProviderBuilder; +use nym_node_metrics::events::MetricEventsSender; +use nym_task::TaskClient; +use nym_topology::TopologyProvider; use nym_validator_client::nyxd::{Coin, CosmWasmClient}; -use nym_validator_client::{nyxd, DirectSigningHttpRpcNyxdClient, UserAgent}; +use nym_validator_client::{nyxd, DirectSigningHttpRpcNyxdClient}; use rand::seq::SliceRandom; use rand::thread_rng; -use statistics::GatewayStatisticsCollector; -use std::net::{IpAddr, Ipv4Addr, SocketAddr}; +use std::net::IpAddr; use std::path::PathBuf; -use std::process; use std::sync::Arc; use tracing::*; +use zeroize::Zeroizing; pub(crate) mod client_handling; -pub(crate) mod helpers; -pub(crate) mod mixnet_handling; -pub(crate) mod statistics; +mod internal_service_providers; +pub use client_handling::active_clients::ActiveClientsStore; pub use nym_gateway_stats_storage::PersistentStatsStorage; -pub use nym_gateway_storage::{PersistentStorage, Storage}; - -// TODO: should this struct live here? -struct StartedNetworkRequester { - /// Handle to interact with the local network requester - handle: LocalEmbeddedClientHandle, -} - -// TODO: should this struct live here? -#[allow(unused)] -struct StartedAuthenticator { - wg_api: Arc<nym_wireguard::WgApiWrapper>, - - /// Handle to interact with the local authenticator - handle: LocalEmbeddedClientHandle, -} +pub use nym_gateway_storage::{error::GatewayStorageError, GatewayStorage}; +pub use nym_sdk::{NymApiTopologyProvider, NymApiTopologyProviderConfig, UserAgent}; #[derive(Debug, Clone)] pub struct LocalNetworkRequesterOpts { @@ -76,621 +60,411 @@ pub struct LocalAuthenticatorOpts { pub custom_mixnet_path: Option<PathBuf>, } -pub struct Gateway<St = PersistentStorage> { +pub struct GatewayTasksBuilder { config: Config, network_requester_opts: Option<LocalNetworkRequesterOpts>, ip_packet_router_opts: Option<LocalIpPacketRouterOpts>, - // Use None when wireguard feature is not enabled too - #[allow(dead_code)] authenticator_opts: Option<LocalAuthenticatorOpts>, + // TODO: combine with authenticator, since you have to start both + wireguard_data: Option<nym_wireguard::WireguardData>, + /// ed25519 keypair used to assert one's identity. - identity_keypair: Arc<identity::KeyPair>, + identity_keypair: Arc<ed25519::KeyPair>, - /// x25519 keypair used for Diffie-Hellman. Currently only used for sphinx key derivation. - sphinx_keypair: Arc<encryption::KeyPair>, + storage: GatewayStorage, - client_storage: St, + mix_packet_sender: MixForwardingSender, - user_agent: UserAgent, + metrics_sender: MetricEventsSender, - stats_storage: PersistentStatsStorage, + mnemonic: Arc<Zeroizing<bip39::Mnemonic>>, - wireguard_data: Option<nym_wireguard::WireguardData>, + shutdown: TaskClient, - session_stats: Option<SharedSessionStats>, + // populated and cached as necessary + ecash_manager: Option<Arc<EcashManager>>, + + wireguard_peers: Option<Vec<WireguardPeer>>, + + wireguard_networks: Option<Vec<IpAddr>>, +} - task_client: Option<TaskClient>, +impl Drop for GatewayTasksBuilder { + fn drop(&mut self) { + // disarm the shutdown as it was already used to construct relevant tasks and we don't want the builder + // to cause shutdown + self.shutdown.disarm(); + } } -impl<St> Gateway<St> { - #[allow(clippy::too_many_arguments)] - pub fn new_loaded( +impl GatewayTasksBuilder { + pub fn new( config: Config, - network_requester_opts: Option<LocalNetworkRequesterOpts>, - ip_packet_router_opts: Option<LocalIpPacketRouterOpts>, - authenticator_opts: Option<LocalAuthenticatorOpts>, - identity_keypair: Arc<identity::KeyPair>, - sphinx_keypair: Arc<encryption::KeyPair>, - client_storage: St, - user_agent: UserAgent, - stats_storage: PersistentStatsStorage, - ) -> Self { - Gateway { + identity: Arc<ed25519::KeyPair>, + storage: GatewayStorage, + mix_packet_sender: MixForwardingSender, + metrics_sender: MetricEventsSender, + mnemonic: Arc<Zeroizing<bip39::Mnemonic>>, + shutdown: TaskClient, + ) -> GatewayTasksBuilder { + GatewayTasksBuilder { config, - network_requester_opts, - ip_packet_router_opts, - authenticator_opts, - identity_keypair, - sphinx_keypair, - client_storage, - user_agent, - stats_storage, + network_requester_opts: None, + ip_packet_router_opts: None, + authenticator_opts: None, wireguard_data: None, - session_stats: None, - task_client: None, + identity_keypair: identity, + storage, + mix_packet_sender, + metrics_sender, + mnemonic, + shutdown, + ecash_manager: None, + wireguard_peers: None, + wireguard_networks: None, } } - pub fn set_task_client(&mut self, task_client: TaskClient) { - self.task_client = Some(task_client) - } - - pub fn set_session_stats(&mut self, session_stats: SharedSessionStats) { - self.session_stats = Some(session_stats); + pub fn set_network_requester_opts( + &mut self, + network_requester_opts: Option<LocalNetworkRequesterOpts>, + ) { + self.network_requester_opts = network_requester_opts; } - pub fn set_wireguard_data(&mut self, wireguard_data: nym_wireguard::WireguardData) { - self.wireguard_data = Some(wireguard_data) + pub fn set_ip_packet_router_opts( + &mut self, + ip_packet_router_opts: Option<LocalIpPacketRouterOpts>, + ) { + self.ip_packet_router_opts = ip_packet_router_opts; } - fn gateway_topology_provider(&self) -> GatewayTopologyProvider { - GatewayTopologyProvider::new( - self.as_topology_node(), - self.user_agent.clone(), - self.config.gateway.nym_api_urls.clone(), - ) + pub fn set_authenticator_opts(&mut self, authenticator_opts: Option<LocalAuthenticatorOpts>) { + self.authenticator_opts = authenticator_opts; } - fn as_topology_node(&self) -> nym_topology::gateway::LegacyNode { - let ip = self - .config - .host - .public_ips - .first() - .copied() - .unwrap_or(IpAddr::V4(Ipv4Addr::LOCALHOST)); - let mix_host = SocketAddr::new(ip, self.config.gateway.mix_port); - - nym_topology::gateway::LegacyNode { - // those fields are irrelevant for the purposes of routing so it's fine if they're inaccurate. - // the only thing that matters is the identity key (and maybe version) - node_id: NodeId::MAX, - mix_host, - host: NetworkAddress::IpAddr(ip), - clients_ws_port: self.config.gateway.clients_port, - clients_wss_port: self.config.gateway.clients_wss_port, - sphinx_key: *self.sphinx_keypair.public_key(), - - identity_key: *self.identity_keypair.public_key(), - version: env!("CARGO_PKG_VERSION").into(), - } + pub fn set_wireguard_data(&mut self, wireguard_data: nym_wireguard::WireguardData) { + self.wireguard_data = Some(wireguard_data) } - fn start_mix_socket_listener( + // if this is to be used anywhere else, we might need some wrapper around it + async fn build_nyxd_signing_client( &self, - ack_sender: MixForwardingSender, - active_clients_store: ActiveClientsStore, - shutdown: TaskClient, - ) where - St: Storage + Clone + 'static, - { - info!("Starting mix socket listener..."); - - let packet_processor = - mixnet_handling::PacketProcessor::new(self.sphinx_keypair.private_key()); - - let connection_handler = ConnectionHandler::new( - packet_processor, - self.client_storage.clone(), - ack_sender, - active_clients_store, - ); - - let listening_address = SocketAddr::new( - self.config.gateway.listening_address, - self.config.gateway.mix_port, - ); - - mixnet_handling::Listener::new(listening_address, shutdown).start(connection_handler); - } + ) -> Result<DirectSigningHttpRpcNyxdClient, GatewayError> { + let endpoints = self.config.get_nyxd_urls(); + let validator_nyxd = endpoints + .choose(&mut thread_rng()) + .ok_or(GatewayError::NoNyxdAvailable)?; - #[cfg(target_os = "linux")] - async fn start_authenticator( - &mut self, - forwarding_channel: MixForwardingSender, - topology_provider: GatewayTopologyProvider, - shutdown: TaskClient, - ecash_verifier: Arc<EcashManager<St>>, - ) -> Result<StartedAuthenticator, Box<dyn std::error::Error + Send + Sync>> - where - St: Storage + Clone + 'static, - { - let opts = self - .authenticator_opts - .as_ref() - .ok_or(GatewayError::UnspecifiedAuthenticatorConfig)?; - let (router_tx, mut router_rx) = oneshot::channel(); - let (auth_mix_sender, auth_mix_receiver) = mpsc::unbounded(); - let router_shutdown = shutdown.fork("message_router"); - let transceiver = LocalGateway::new( - *self.identity_keypair.public_key(), - forwarding_channel, - router_tx, - ); - let all_peers = self.client_storage.get_all_wireguard_peers().await?; - let used_private_network_ips = all_peers - .iter() - .cloned() - .map(|wireguard_peer| { - defguard_wireguard_rs::host::Peer::try_from(wireguard_peer).map(|mut peer| { - peer.allowed_ips - .pop() - .ok_or(Box::new(GatewayError::InternalWireguardError(format!( - "no private IP set for peer {}", - peer.public_key - )))) - .map(|p| p.ip) - }) - }) - .collect::<Result<Result<Vec<_>, _>, _>>()??; - - if let Some(wireguard_data) = self.wireguard_data.take() { - let (on_start_tx, on_start_rx) = oneshot::channel(); - let mut authenticator_server = nym_authenticator::Authenticator::new( - opts.config.clone(), - wireguard_data.inner.clone(), - used_private_network_ips, - ) - .with_ecash_verifier(ecash_verifier) - .with_custom_gateway_transceiver(Box::new(transceiver)) - .with_shutdown(shutdown.fork("authenticator")) - .with_wait_for_gateway(true) - .with_minimum_gateway_performance(0) - .with_custom_topology_provider(Box::new(topology_provider)) - .with_on_start(on_start_tx); + let network_details = NymNetworkDetails::new_from_env(); + let client_config = nyxd::Config::try_from_nym_network_details(&network_details)?; - if let Some(custom_mixnet) = &opts.custom_mixnet_path { - authenticator_server = authenticator_server.with_stored_topology(custom_mixnet)? + let nyxd_client = DirectSigningHttpRpcNyxdClient::connect_with_mnemonic( + client_config, + validator_nyxd.as_ref(), + (**self.mnemonic).clone(), + )?; + + let mix_denom_base = nyxd_client.current_chain_details().mix_denom.base.clone(); + let account = nyxd_client.address(); + let balance = nyxd_client + .get_balance(&account, mix_denom_base.clone()) + .await? + .unwrap_or(Coin::new(0, mix_denom_base)); + + // see if we have at least 1nym (i.e. 1'000'000unym) + if balance.amount < 1_000_000 { + // don't allow constructing the client of we have to use zknym and don't have sufficient balance + if self.config.gateway.enforce_zk_nyms { + return Err(GatewayError::InsufficientNodeBalance { account, balance }); } - tokio::spawn(async move { - if let Err(e) = authenticator_server.run_service_provider().await { - error!("Run authenticator server - {e}"); - } - }); - - let start_data = on_start_rx - .await - .map_err(|_| GatewayError::AuthenticatorStartupFailure)?; + // TODO: this has to be enforced **ALL THE TIME in ENTRY mode**, + // because even if we don't demand zknym, somebody may send them and we need sufficient tokens for + // transaction fees for submitting redemption proposals + // but we're not going to introduce this check now as it would break a lot of existing gateways, + // so for now just log this error + error!("this gateway ({account}) has insufficient balance for possible zk-nym redemption transaction fees. it only has {balance} available.") + } - // this should be instantaneous since the data is sent on this channel before the on start is called; - // the failure should be impossible - let Ok(Some(packet_router)) = router_rx.try_recv() else { - return Err(Box::new(GatewayError::AuthenticatorStartupFailure)); - }; + Ok(nyxd_client) + } - MessageRouter::new(auth_mix_receiver, packet_router) - .start_with_shutdown(router_shutdown); + async fn build_ecash_manager(&self) -> Result<Arc<EcashManager>, GatewayError> { + let handler_config = CredentialHandlerConfig { + revocation_bandwidth_penalty: self + .config + .debug + .zk_nym_tickets + .revocation_bandwidth_penalty, + pending_poller: self.config.debug.zk_nym_tickets.pending_poller, + minimum_api_quorum: self.config.debug.zk_nym_tickets.minimum_api_quorum, + minimum_redemption_tickets: self.config.debug.zk_nym_tickets.minimum_redemption_tickets, + maximum_time_between_redemption: self + .config + .debug + .zk_nym_tickets + .maximum_time_between_redemption, + }; - let wg_api = nym_wireguard::start_wireguard( - self.client_storage.clone(), - all_peers, - shutdown, - wireguard_data, + let nyxd_client = self.build_nyxd_signing_client().await?; + let ecash_manager = Arc::new( + EcashManager::new( + handler_config, + nyxd_client, + self.identity_keypair.public_key().to_bytes(), + self.shutdown.fork("ecash-manager"), + self.storage.clone(), ) - .await?; - - Ok(StartedAuthenticator { - wg_api, - handle: LocalEmbeddedClientHandle::new(start_data.address, auth_mix_sender), - }) - } else { - Err(Box::new(GatewayError::InternalWireguardError( - "wireguard not set".to_string(), - ))) - } + .await?, + ); + Ok(ecash_manager) } - #[cfg(not(target_os = "linux"))] - async fn start_authenticator( - &self, - _forwarding_channel: MixForwardingSender, - _topology_provider: GatewayTopologyProvider, - _shutdown: TaskClient, - _ecash_verifier: Arc<EcashManager<St>>, - ) -> Result<StartedAuthenticator, Box<dyn std::error::Error + Send + Sync>> { - todo!("Authenticator is currently only supported on Linux"); + async fn ecash_manager(&mut self) -> Result<Arc<EcashManager>, GatewayError> { + match self.ecash_manager.clone() { + Some(cached) => Ok(cached), + None => { + let manager = self.build_ecash_manager().await?; + self.ecash_manager = Some(manager.clone()); + Ok(manager) + } + } } - fn start_client_websocket_listener( - &self, - forwarding_channel: MixForwardingSender, + pub async fn build_websocket_listener( + &mut self, active_clients_store: ActiveClientsStore, - shutdown: TaskClient, - ecash_verifier: Arc<EcashManager<St>>, - stats_event_reporter: GatewayStatsReporter, - ) where - St: Storage + Send + Sync + Clone + 'static, - { - info!("Starting client [web]socket listener..."); - - let listening_address = SocketAddr::new( - self.config.gateway.listening_address, - self.config.gateway.clients_port, - ); - + ) -> Result<websocket::Listener, GatewayError> { let shared_state = websocket::CommonHandlerState { - ecash_verifier, - storage: self.client_storage.clone(), + ecash_verifier: self.ecash_manager().await?, + storage: self.storage.clone(), local_identity: Arc::clone(&self.identity_keypair), - only_coconut_credentials: self.config.gateway.only_coconut_credentials, + only_coconut_credentials: self.config.gateway.enforce_zk_nyms, bandwidth_cfg: (&self.config).into(), - stats_event_reporter, + metrics_sender: self.metrics_sender.clone(), + outbound_mix_sender: self.mix_packet_sender.clone(), + active_clients_store: active_clients_store.clone(), }; - websocket::Listener::new(listening_address, shared_state).start( - forwarding_channel, - active_clients_store, - shutdown, - ); - } - - fn start_packet_forwarder(&self, shutdown: TaskClient) -> MixForwardingSender { - info!("Starting mix packet forwarder..."); - - let (mut packet_forwarder, packet_sender) = PacketForwarder::new( - self.config.debug.packet_forwarding_initial_backoff, - self.config.debug.packet_forwarding_maximum_backoff, - self.config.debug.initial_connection_timeout, - self.config.debug.maximum_connection_buffer_size, - self.config.debug.use_legacy_framed_packet_version, - shutdown, - ); - - tokio::spawn(async move { packet_forwarder.run().await }); - packet_sender - } - - fn start_stats_collector( - &self, - shared_session_stats: SharedSessionStats, - shutdown: TaskClient, - ) -> gateways::GatewayStatsReporter { - info!("Starting gateway stats collector..."); - - let (mut stats_collector, stats_event_sender) = - GatewayStatisticsCollector::new(shared_session_stats, self.stats_storage.clone()); - tokio::spawn(async move { stats_collector.run(shutdown).await }); - stats_event_sender + Ok(websocket::Listener::new( + self.config.gateway.websocket_bind_address, + shared_state, + self.shutdown.fork("websocket"), + )) } - // TODO: rethink the logic in this function... - async fn start_network_requester( - &self, - forwarding_channel: MixForwardingSender, - topology_provider: GatewayTopologyProvider, - shutdown: TaskClient, - ) -> Result<StartedNetworkRequester, GatewayError> { - info!("Starting network requester..."); - + fn build_network_requester( + &mut self, + topology_provider: Box<dyn TopologyProvider + Send + Sync>, + ) -> Result<ServiceProviderBeingBuilt<NRServiceProviderBuilder>, GatewayError> { // if network requester is enabled, configuration file must be provided! let Some(nr_opts) = &self.network_requester_opts else { return Err(GatewayError::UnspecifiedNetworkRequesterConfig); }; - // this gateway, whenever it has anything to send to its local NR will use fake_client_tx - let (nr_mix_sender, nr_mix_receiver) = mpsc::unbounded(); - let router_shutdown = shutdown.fork("message_router"); - - let (router_tx, mut router_rx) = oneshot::channel(); - - let transceiver = LocalGateway::new( + let mut message_router_builder = SpMessageRouterBuilder::new( *self.identity_keypair.public_key(), - forwarding_channel, - router_tx, + self.mix_packet_sender.clone(), + 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(shutdown) - .with_custom_gateway_transceiver(Box::new(transceiver)) + .with_shutdown(self.shutdown.fork("network-requester-sp")) + .with_custom_gateway_transceiver(transceiver) .with_wait_for_gateway(true) .with_minimum_gateway_performance(0) - .with_custom_topology_provider(Box::new(topology_provider)) + .with_custom_topology_provider(topology_provider) .with_on_start(on_start_tx); if let Some(custom_mixnet) = &nr_opts.custom_mixnet_path { nr_builder = nr_builder.with_stored_topology(custom_mixnet)? } - tokio::spawn(async move { - if let Err(err) = nr_builder.run_service_provider().await { - // no need to panic as we have passed a task client to the NR so we're most likely - // already in the process of shutting down - error!("network requester has failed: {err}") - } - }); - - let start_data = on_start_rx - .await - .map_err(|_| GatewayError::NetworkRequesterStartupFailure)?; - - // this should be instantaneous since the data is sent on this channel before the on start is called; - // the failure should be impossible - let Ok(Some(packet_router)) = router_rx.try_recv() else { - return Err(GatewayError::NetworkRequesterStartupFailure); - }; - - MessageRouter::new(nr_mix_receiver, packet_router).start_with_shutdown(router_shutdown); - let address = start_data.address; - - info!("the local network requester is running on {address}",); - Ok(StartedNetworkRequester { - handle: LocalEmbeddedClientHandle::new(address, nr_mix_sender), - }) + Ok(ServiceProviderBeingBuilt::new( + on_start_rx, + nr_builder, + message_router_builder, + )) } - async fn start_ip_packet_router( - &self, - forwarding_channel: MixForwardingSender, - topology_provider: GatewayTopologyProvider, - shutdown: TaskClient, - ) -> Result<LocalEmbeddedClientHandle, GatewayError> { - info!("Starting IP packet provider..."); - - // if network requester is enabled, configuration file must be provided! + fn build_ip_router( + &mut self, + topology_provider: Box<dyn TopologyProvider + Send + Sync>, + ) -> Result<ServiceProviderBeingBuilt<IpPacketRouter>, GatewayError> { let Some(ip_opts) = &self.ip_packet_router_opts else { return Err(GatewayError::UnspecifiedIpPacketRouterConfig); }; - // this gateway, whenever it has anything to send to its local NR will use fake_client_tx - let (ipr_mix_sender, ipr_mix_receiver) = mpsc::unbounded(); - let router_shutdown = shutdown.fork("message_router"); - - let (router_tx, mut router_rx) = oneshot::channel(); - - let transceiver = LocalGateway::new( + let mut message_router_builder = SpMessageRouterBuilder::new( *self.identity_keypair.public_key(), - forwarding_channel, - router_tx, + self.mix_packet_sender.clone(), + 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 = - nym_ip_packet_router::IpPacketRouter::new(ip_opts.config.clone()) - .with_shutdown(shutdown) - .with_custom_gateway_transceiver(Box::new(transceiver)) - .with_wait_for_gateway(true) - .with_minimum_gateway_performance(0) - .with_custom_topology_provider(Box::new(topology_provider)) - .with_on_start(on_start_tx); + let mut ip_packet_router = IpPacketRouter::new(ip_opts.config.clone()) + .with_shutdown(self.shutdown.fork("ipr-sp")) + .with_custom_gateway_transceiver(Box::new(transceiver)) + .with_wait_for_gateway(true) + .with_minimum_gateway_performance(0) + .with_custom_topology_provider(topology_provider) + .with_on_start(on_start_tx); if let Some(custom_mixnet) = &ip_opts.custom_mixnet_path { ip_packet_router = ip_packet_router.with_stored_topology(custom_mixnet)? } - tokio::spawn(async move { - if let Err(err) = ip_packet_router.run_service_provider().await { - // no need to panic as we have passed a task client to the ip packet router so - // we're most likely already in the process of shutting down - error!("ip packet router has failed: {err}") - } - }); - - let start_data = on_start_rx - .await - .map_err(|_| GatewayError::IpPacketRouterStartupFailure)?; - - // this should be instantaneous since the data is sent on this channel before the on start is called; - // the failure should be impossible - let Ok(Some(packet_router)) = router_rx.try_recv() else { - return Err(GatewayError::IpPacketRouterStartupFailure); - }; - - MessageRouter::new(ipr_mix_receiver, packet_router).start_with_shutdown(router_shutdown); - let address = start_data.address; - - info!("the local ip packet router is running on {address}"); - Ok(LocalEmbeddedClientHandle::new(address, ipr_mix_sender)) + Ok(ServiceProviderBeingBuilt::new( + on_start_rx, + ip_packet_router, + message_router_builder, + )) } - fn random_nyxd_client(&self) -> Result<DirectSigningHttpRpcNyxdClient, GatewayError> { - let endpoints = self.config.get_nyxd_urls(); - let validator_nyxd = endpoints - .choose(&mut thread_rng()) - .ok_or(GatewayError::NoNyxdAvailable)?; - - let network_details = NymNetworkDetails::new_from_env(); - let client_config = nyxd::Config::try_from_nym_network_details(&network_details)?; - - DirectSigningHttpRpcNyxdClient::connect_with_mnemonic( - client_config, - validator_nyxd.as_ref(), - self.config.get_cosmos_mnemonic(), - ) - .map_err(Into::into) + pub fn build_exit_service_providers( + &mut self, + // TODO: redesign the trait to allow cloning more easily + // (or use concrete types) + nr_topology_provider: Box<dyn TopologyProvider + Send + Sync>, + ipr_topology_provider: Box<dyn TopologyProvider + Send + Sync>, + ) -> Result<ExitServiceProviders, GatewayError> { + Ok(ExitServiceProviders { + network_requester: self.build_network_requester(nr_topology_provider)?, + ip_router: self.build_ip_router(ipr_topology_provider)?, + }) } - async fn check_if_bonded(&self) -> bool { - // TODO: if anything, this should be getting data directly from the contract - // as opposed to the validator API - for api_url in self.config.get_nym_api_endpoints() { - let client = nym_validator_client::NymApiClient::new(api_url.clone()); - match client.get_all_basic_nodes(None).await { - Ok(nodes) => { - return nodes.iter().any(|node| { - &node.ed25519_identity_pubkey == self.identity_keypair.public_key() - }) - } - Err(err) => { - error!("failed to grab initial network gateways from {api_url}: {err}",); - } - } + async fn build_wireguard_peers_and_networks( + &self, + ) -> Result<(Vec<WireguardPeer>, Vec<IpAddr>), GatewayError> { + let mut used_private_network_ips = vec![]; + let mut all_peers = vec![]; + for wireguard_peer in self.storage.get_all_wireguard_peers().await?.into_iter() { + let mut peer = defguard_wireguard_rs::host::Peer::try_from(wireguard_peer.clone())?; + let Some(peer) = peer.allowed_ips.pop() else { + let peer_identity = &peer.public_key; + warn!("Peer {peer_identity} has empty allowed ips. It will be removed",); + self.storage + .remove_wireguard_peer(&peer_identity.to_string()) + .await?; + continue; + }; + used_private_network_ips.push(peer.ip); + all_peers.push(wireguard_peer); } - error!( - "failed to grab initial network gateways from any of the available apis. Please try to startup again in few minutes", - ); - process::exit(1); + Ok((all_peers, used_private_network_ips)) } - pub async fn run(mut self) -> Result<(), GatewayError> - where - St: Storage + Clone + 'static, - { - info!("Starting nym gateway!"); - - if self.check_if_bonded().await { - warn!("You seem to have bonded your gateway before starting it - that's highly unrecommended as in the future it might result in slashing"); + // only used under linux + #[allow(dead_code)] + async fn get_wireguard_peers(&mut self) -> Result<Vec<WireguardPeer>, GatewayError> { + if let Some(cached) = self.wireguard_peers.take() { + return Ok(cached); } - let shutdown = self - .task_client - .take() - .map(Into::<TaskHandle>::into) - .unwrap_or_else(|| TaskHandle::Internal(TaskManager::new(10))) - .name_if_unnamed("gateway"); + let (peers, used_private_network_ips) = self.build_wireguard_peers_and_networks().await?; + // cache private networks for the other task - let nyxd_client = self.random_nyxd_client()?; - - if self.config.gateway.only_coconut_credentials { - debug!("the gateway is running in coconut-only mode - making sure it has enough tokens for credential redemption"); - let mix_denom_base = nyxd_client.current_chain_details().mix_denom.base.clone(); + self.wireguard_networks = Some(used_private_network_ips); + Ok(peers) + } - let account = nyxd_client.address(); - let balance = nyxd_client - .get_balance(&account, mix_denom_base.clone()) - .await? - .unwrap_or(Coin::new(0, mix_denom_base)); + async fn get_wireguard_networks(&mut self) -> Result<Vec<IpAddr>, GatewayError> { + if let Some(cached) = self.wireguard_networks.take() { + return Ok(cached); + } - error!("this gateway does not have enough tokens for covering transaction fees for credential redemption"); + let (peers, used_private_network_ips) = self.build_wireguard_peers_and_networks().await?; + // cache peers for the other task - // see if we have at least 1nym (i.e. 1'000'000unym) - if balance.amount < 1_000_000 { - return Err(GatewayError::InsufficientNodeBalance { account, balance }); - } - } - let shared_session_stats = self.session_stats.take().unwrap_or_default(); - let stats_event_sender = self.start_stats_collector( - shared_session_stats, - shutdown.fork("statistics::GatewayStatisticsCollector"), - ); + self.wireguard_peers = Some(peers); + Ok(used_private_network_ips) + } - let topology_provider = self.gateway_topology_provider(); + pub async fn build_wireguard_authenticator( + &mut self, + topology_provider: Box<dyn TopologyProvider + Send + Sync>, + ) -> Result<ServiceProviderBeingBuilt<Authenticator>, GatewayError> { + let ecash_manager = self.ecash_manager().await?; + let used_private_network_ips = self.get_wireguard_networks().await?; - let handler_config = CredentialHandlerConfig { - revocation_bandwidth_penalty: self - .config - .debug - .zk_nym_tickets - .revocation_bandwidth_penalty, - pending_poller: self.config.debug.zk_nym_tickets.pending_poller, - minimum_api_quorum: self.config.debug.zk_nym_tickets.minimum_api_quorum, - minimum_redemption_tickets: self.config.debug.zk_nym_tickets.minimum_redemption_tickets, - maximum_time_between_redemption: self - .config - .debug - .zk_nym_tickets - .maximum_time_between_redemption, + let Some(opts) = &self.authenticator_opts else { + return Err(GatewayError::UnspecifiedAuthenticatorConfig); + }; + let Some(wireguard_data) = &self.wireguard_data else { + return Err(GatewayError::InternalWireguardError( + "wireguard not set".to_string(), + )); }; - let ecash_verifier = Arc::new( - EcashManager::new( - handler_config, - nyxd_client, - self.identity_keypair.public_key().to_bytes(), - shutdown.fork("EcashVerifier"), - self.client_storage.clone(), - ) - .await?, + let mut message_router_builder = SpMessageRouterBuilder::new( + *self.identity_keypair.public_key(), + self.mix_packet_sender.clone(), + self.shutdown.fork("authenticator-message-router"), ); + let transceiver = message_router_builder.gateway_transceiver(); - let mix_forwarding_channel = self.start_packet_forwarder(shutdown.fork("PacketForwarder")); + let (on_start_tx, on_start_rx) = oneshot::channel(); - let active_clients_store = ActiveClientsStore::new(stats_event_sender.clone()); - self.start_mix_socket_listener( - mix_forwarding_channel.clone(), - active_clients_store.clone(), - shutdown.fork("mixnet_handling::Listener"), - ); + let mut authenticator_server = Authenticator::new( + opts.config.clone(), + wireguard_data.inner.clone(), + used_private_network_ips, + ) + .with_ecash_verifier(ecash_manager) + .with_custom_gateway_transceiver(transceiver) + .with_shutdown(self.shutdown.fork("authenticator-sp")) + .with_wait_for_gateway(true) + .with_minimum_gateway_performance(0) + .with_custom_topology_provider(topology_provider) + .with_on_start(on_start_tx); + + if let Some(custom_mixnet) = &opts.custom_mixnet_path { + authenticator_server = authenticator_server.with_stored_topology(custom_mixnet)? + } - self.start_client_websocket_listener( - mix_forwarding_channel.clone(), - active_clients_store.clone(), - shutdown.fork("websocket::Listener"), - ecash_verifier.clone(), - stats_event_sender.clone(), - ); + Ok(ServiceProviderBeingBuilt::new( + on_start_rx, + authenticator_server, + message_router_builder, + )) + } - if self.config.network_requester.enabled { - let embedded_nr = self - .start_network_requester( - mix_forwarding_channel.clone(), - topology_provider.clone(), - shutdown.fork("NetworkRequester"), - ) - .await?; - // insert information about embedded NR to the active clients store - active_clients_store.insert_embedded(embedded_nr.handle); - } else { - info!("embedded network requester is disabled"); - }; + #[cfg(not(target_os = "linux"))] + pub async fn try_start_wireguard( + &mut self, + ) -> Result<Arc<nym_wireguard::WgApiWrapper>, Box<dyn std::error::Error + Send + Sync>> { + unimplemented!("wireguard is not supported on this platform") + } - if self.config.ip_packet_router.enabled { - let embedded_ip_sp = self - .start_ip_packet_router( - mix_forwarding_channel.clone(), - topology_provider.clone(), - shutdown.fork("ip_service_provider"), - ) - .await?; - active_clients_store.insert_embedded(embedded_ip_sp); - } else { - info!("embedded ip packet router is disabled"); - }; + #[cfg(target_os = "linux")] + pub async fn try_start_wireguard( + &mut self, + ) -> Result<Arc<nym_wireguard::WgApiWrapper>, Box<dyn std::error::Error + Send + Sync>> { + let all_peers = self.get_wireguard_peers().await?; - let _wg_api = if self.wireguard_data.is_some() { - let embedded_auth = self - .start_authenticator( - mix_forwarding_channel, - topology_provider, - shutdown.fork("authenticator"), - ecash_verifier, - ) - .await - .map_err(|source| GatewayError::AuthenticatorStartError { source })?; - active_clients_store.insert_embedded(embedded_auth.handle); - Some(embedded_auth.wg_api) - } else { - None + let Some(wireguard_data) = self.wireguard_data.take() else { + return Err( + GatewayError::InternalWireguardError("wireguard not set".to_string()).into(), + ); }; - info!("Finished nym gateway startup procedure - it should now be able to receive mix and client traffic!"); - - info!( - "Public key: {:?}", - self.identity_keypair.public_key().to_string() - ); - - if let Err(source) = shutdown.wait_for_shutdown().await { - // that's a nasty workaround, but anyhow errors are generally nicer, especially on exit - return Err(GatewayError::ShutdownFailure { source }); - } - - Ok(()) + let wg_handle = nym_wireguard::start_wireguard( + self.storage.clone(), + all_peers, + self.shutdown.fork("wireguard"), + wireguard_data, + ) + .await?; + Ok(wg_handle) } } diff --git a/gateway/src/node/statistics/mod.rs b/gateway/src/node/statistics/mod.rs deleted file mode 100644 index 8b60c934a78..00000000000 --- a/gateway/src/node/statistics/mod.rs +++ /dev/null @@ -1,86 +0,0 @@ -// Copyright 2022 - Nym Technologies SA <contact@nymtech.net> -// SPDX-License-Identifier: GPL-3.0-only - -use nym_gateway_stats_storage::PersistentStatsStorage; -use nym_node_http_api::state::metrics::SharedSessionStats; -use nym_statistics_common::gateways::{ - GatewayStatsEvent, GatewayStatsReceiver, GatewayStatsReporter, -}; -use nym_task::TaskClient; -use sessions::SessionStatsHandler; -use std::time::Duration; -use time::OffsetDateTime; -use tracing::{error, trace, warn}; - -pub mod sessions; - -const STATISTICS_UPDATE_TIMER_INTERVAL: Duration = Duration::from_secs(3600); //update timer, no need to check everytime - -pub(crate) struct GatewayStatisticsCollector { - stats_event_rx: GatewayStatsReceiver, - session_stats: SessionStatsHandler, - //here goes additionnal stats handler -} - -impl GatewayStatisticsCollector { - pub fn new( - shared_session_stats: SharedSessionStats, - stats_storage: PersistentStatsStorage, - ) -> (GatewayStatisticsCollector, GatewayStatsReporter) { - let (stats_event_tx, stats_event_rx) = tokio::sync::mpsc::unbounded_channel(); - - let session_stats = SessionStatsHandler::new(shared_session_stats, stats_storage); - let collector = GatewayStatisticsCollector { - stats_event_rx, - session_stats, - }; - let reporter = GatewayStatsReporter::new(stats_event_tx); - (collector, reporter) - } - - async fn update_shared_state(&mut self, update_time: OffsetDateTime) { - if let Err(e) = self - .session_stats - .maybe_update_shared_state(update_time) - .await - { - error!("Failed to update session stats - {e}"); - } - //here goes additionnal stats handler update - } - - async fn on_start(&mut self) { - if let Err(e) = self.session_stats.on_start().await { - error!("Failed to cleanup session stats handler - {e}"); - } - //here goes additionnal stats handler start cleanup - } - - pub async fn run(&mut self, mut shutdown: TaskClient) { - self.on_start().await; - let mut update_interval = tokio::time::interval(STATISTICS_UPDATE_TIMER_INTERVAL); - while !shutdown.is_shutdown() { - tokio::select! { - biased; - _ = shutdown.recv() => { - trace!("StatisticsCollector: Received shutdown"); - }, - _ = update_interval.tick() => { - let now = OffsetDateTime::now_utc(); - self.update_shared_state(now).await; - }, - - Some(stat_event) = self.stats_event_rx.recv() => { - //dispatching event to proper handler - match stat_event { - GatewayStatsEvent::SessionStatsEvent(event) => { - if let Err(e) = self.session_stats.handle_event(event).await{ - warn!("Session event handling error - {e}"); - }}, - } - }, - - } - } - } -} diff --git a/mixnode/Cargo.toml b/mixnode/Cargo.toml deleted file mode 100644 index 9255e9e5890..00000000000 --- a/mixnode/Cargo.toml +++ /dev/null @@ -1,54 +0,0 @@ -# Copyright 2020 - Nym Technologies SA <contact@nymtech.net> -# SPDX-License-Identifier: GPL-3.0-only - -[package] -name = "nym-mixnode" -license = "GPL-3.0" -version = "1.1.37" -authors = [ - "Dave Hrycyszyn <futurechimp@users.noreply.github.com>", - "Jędrzej Stuczyński <andrew@nymtech.net>", - "Drazen Urch <durch@users.noreply.github.com>", -] -description = "Implementation of a Loopix-based Mixnode" -edition = "2021" -rust-version = "1.70" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -colored = { workspace = true } -futures = { workspace = true } -time.workspace = true -tokio = { workspace = true, features = ["rt-multi-thread", "net", "signal"] } -tokio-util = { workspace = true, features = ["codec"] } -url = { workspace = true, features = ["serde"] } -tracing = { workspace = true } -thiserror = { workspace = true } - -# internal -nym-crypto = { path = "../common/crypto" } -nym-contracts-common = { path = "../common/cosmwasm-smart-contracts/contracts-common" } -nym-http-api-common = { path = "../common/http-api-common" } -nym-mixnet-client = { path = "../common/client-libs/mixnet-client" } -nym-mixnode-common = { path = "../common/mixnode-common" } -nym-metrics = { path = "../common/nym-metrics" } -nym-nonexhaustive-delayqueue = { path = "../common/nonexhaustive-delayqueue" } -nym-node-http-api = { path = "../nym-node/nym-node-http-api" } -nym-sphinx = { path = "../common/nymsphinx" } -nym-sphinx-params = { path = "../common/nymsphinx/params" } -nym-task = { path = "../common/task" } -nym-types = { path = "../common/types" } -nym-topology = { path = "../common/topology" } -nym-validator-client = { path = "../common/client-libs/validator-client" } - -[dev-dependencies] -tokio = { workspace = true, features = [ - "rt-multi-thread", - "net", - "signal", - "test-util", -] } - -nym-sphinx-types = { path = "../common/nymsphinx/types" } -nym-sphinx-params = { path = "../common/nymsphinx/params" } diff --git a/mixnode/README.md b/mixnode/README.md deleted file mode 100644 index 8d9680669be..00000000000 --- a/mixnode/README.md +++ /dev/null @@ -1,75 +0,0 @@ -<!-- -Copyright 2020 - Nym Technologies SA <contact@nymtech.net> -SPDX-License-Identifier: GPL-3.0-only ---> - -# Nym Mixnode - -A Rust mixnode implementation. - -## License - -Copyright (C) 2020 Nym Technologies SA <contact@nymtech.net> - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see <https://www.gnu.org/licenses/>. - -## Usage - -* `nym-mixnode` prints a help message showing usage options -* `nym-mixnode run --help` prints a help message showing usage options for the run command -* `nym-mixnode run --layer 1 --host x.x.x.x` will start the mixnode in layer 1 and bind to the specified host IP address. Coordinate with other people in your network to find out which layer needs coverage. - -By default, the Nym Mixnode will start on port 1789. If desired, you can change the port using the `--port` option. - -## Install debian - -```bash -sudo curl -s --compressed "http://apt.nymtech.net.s3-website.eu-central-1.amazonaws.com/nymtech.gpg" | gpg --dearmor | sudo tee /etc/apt/trusted.gpg.d/nymtech.gpg > /dev/null -sudo echo "deb [signed-by=/etc/apt/trusted.gpg.d/nymtech.gpg] http://apt.nymtech.net.s3-website.eu-central-1.amazonaws.com/ squeeze main" > /etc/apt/sources.list.d/nymtech.list - -sudo apt-get update -sudo apt-get install nym-mixnode - -# See below for starting and managing the node -``` - -## Systemd support - -```bash -sudo systemctl enable nym-mixnode - -# Run -sudo systemctl start nym-mixnode - -# Check status -sudo systemctl status nym-mixnode - -# Logs -journalctl -f -u nym-mixnode - -``` - -## Build debian package - -```bash -# cargo install cargo-deb - -# Build package -cargo deb -p nym-mixnode - -# Install - -# This will init the mixnode to `/etc/nym` as `nym` user, and create a systemd service -sudo dpkg -i target/debian/<PACKAGE> -``` \ No newline at end of file diff --git a/mixnode/src/config.rs b/mixnode/src/config.rs deleted file mode 100644 index d20f891ec53..00000000000 --- a/mixnode/src/config.rs +++ /dev/null @@ -1,237 +0,0 @@ -// Copyright 2024 - Nym Technologies SA <contact@nymtech.net> -// SPDX-License-Identifier: GPL-3.0-only - -use std::net::{IpAddr, SocketAddr}; -use std::path::PathBuf; -use std::time::Duration; -use url::Url; - -// 'RTT MEASUREMENT' -const DEFAULT_PACKETS_PER_NODE: usize = 100; -const DEFAULT_CONNECTION_TIMEOUT: Duration = Duration::from_millis(5000); -const DEFAULT_PACKET_TIMEOUT: Duration = Duration::from_millis(1500); -const DEFAULT_DELAY_BETWEEN_PACKETS: Duration = Duration::from_millis(50); -const DEFAULT_BATCH_SIZE: usize = 50; -const DEFAULT_TESTING_INTERVAL: Duration = Duration::from_secs(60 * 60 * 12); -const DEFAULT_RETRY_TIMEOUT: Duration = Duration::from_secs(60 * 30); - -// 'DEBUG' -const DEFAULT_NODE_STATS_LOGGING_DELAY: Duration = Duration::from_millis(60_000); -const DEFAULT_NODE_STATS_UPDATING_DELAY: Duration = Duration::from_millis(30_000); -const DEFAULT_PACKET_FORWARDING_INITIAL_BACKOFF: Duration = Duration::from_millis(10_000); -const DEFAULT_PACKET_FORWARDING_MAXIMUM_BACKOFF: Duration = Duration::from_millis(300_000); -const DEFAULT_INITIAL_CONNECTION_TIMEOUT: Duration = Duration::from_millis(1_500); -const DEFAULT_MAXIMUM_CONNECTION_BUFFER_SIZE: usize = 2000; - -#[derive(Debug, PartialEq)] -pub struct Config { - pub host: Host, - - pub http: Http, - - pub mixnode: MixNode, - - pub verloc: Verloc, - - pub debug: Debug, -} - -impl Config { - pub fn externally_loaded( - host: impl Into<Host>, - http: impl Into<Http>, - mixnode: impl Into<MixNode>, - verloc: impl Into<Verloc>, - debug: impl Into<Debug>, - ) -> Self { - Config { - host: host.into(), - http: http.into(), - mixnode: mixnode.into(), - verloc: verloc.into(), - debug: debug.into(), - } - } - - // builder methods - pub fn with_custom_nym_apis(mut self, nym_api_urls: Vec<Url>) -> Self { - self.mixnode.nym_api_urls = nym_api_urls; - self - } - - pub fn with_listening_address(mut self, listening_address: IpAddr) -> Self { - self.mixnode.listening_address = listening_address; - - let http_port = self.http.bind_address.port(); - self.http.bind_address = SocketAddr::new(listening_address, http_port); - - self - } - - pub fn with_mix_port(mut self, port: u16) -> Self { - self.mixnode.mix_port = port; - self - } - - pub fn with_verloc_port(mut self, port: u16) -> Self { - self.mixnode.verloc_port = port; - self - } - - pub fn with_http_api_port(mut self, port: u16) -> Self { - let http_ip = self.http.bind_address.ip(); - self.http.bind_address = SocketAddr::new(http_ip, port); - self - } - - pub fn get_nym_api_endpoints(&self) -> Vec<Url> { - self.mixnode.nym_api_urls.clone() - } - - pub fn with_metrics_key(mut self, metrics_key: String) -> Self { - self.http.metrics_key = Some(metrics_key); - self - } - - pub fn metrics_key(&self) -> Option<&String> { - self.http.metrics_key.as_ref() - } -} - -// TODO: this is very much a WIP. we need proper ssl certificate support here -#[derive(Debug, PartialEq)] -pub struct Host { - /// Ip address(es) of this host, such as 1.1.1.1 that external clients will use for connections. - pub public_ips: Vec<IpAddr>, - - /// Optional hostname of this node, for example nymtech.net. - // TODO: this is temporary. to be replaced by pulling the data directly from the certs. - pub hostname: Option<String>, -} - -impl Host { - pub fn validate(&self) -> bool { - if self.public_ips.is_empty() { - return false; - } - - true - } -} - -#[derive(Debug, PartialEq)] -pub struct Http { - /// Socket address this node will use for binding its http API. - /// default: `0.0.0.0:8000` - pub bind_address: SocketAddr, - - /// Path to assets directory of custom landing page of this node. - pub landing_page_assets_path: Option<PathBuf>, - - pub metrics_key: Option<String>, -} - -#[derive(Debug, PartialEq)] -pub struct MixNode { - /// Version of the mixnode for which this configuration was created. - pub version: String, - - /// ID specifies the human readable ID of this particular mixnode. - pub id: String, - - /// Address to which this mixnode will bind to and will be listening for packets. - pub listening_address: IpAddr, - - /// Port used for listening for all mixnet traffic. - /// (default: 1789) - pub mix_port: u16, - - /// Port used for listening for verloc traffic. - /// (default: 1790) - pub verloc_port: u16, - - /// Addresses to nym APIs from which the node gets the view of the network. - pub nym_api_urls: Vec<Url>, -} - -#[derive(Debug, PartialEq)] -pub struct Verloc { - /// Specifies number of echo packets sent to each node during a measurement run. - pub packets_per_node: usize, - - /// Specifies maximum amount of time to wait for the connection to get established. - pub connection_timeout: Duration, - - /// Specifies maximum amount of time to wait for the reply packet to arrive before abandoning the test. - pub packet_timeout: Duration, - - /// Specifies delay between subsequent test packets being sent (after receiving a reply). - pub delay_between_packets: Duration, - - /// Specifies number of nodes being tested at once. - pub tested_nodes_batch_size: usize, - - /// Specifies delay between subsequent test runs. - pub testing_interval: Duration, - - /// Specifies delay between attempting to run the measurement again if the previous run failed - /// due to being unable to get the list of nodes. - pub retry_timeout: Duration, -} - -impl Default for Verloc { - fn default() -> Self { - Verloc { - packets_per_node: DEFAULT_PACKETS_PER_NODE, - connection_timeout: DEFAULT_CONNECTION_TIMEOUT, - packet_timeout: DEFAULT_PACKET_TIMEOUT, - delay_between_packets: DEFAULT_DELAY_BETWEEN_PACKETS, - tested_nodes_batch_size: DEFAULT_BATCH_SIZE, - testing_interval: DEFAULT_TESTING_INTERVAL, - retry_timeout: DEFAULT_RETRY_TIMEOUT, - } - } -} - -#[derive(Debug, PartialEq)] -pub struct Debug { - /// Delay between each subsequent node statistics being logged to the console - pub node_stats_logging_delay: Duration, - - /// Delay between each subsequent node statistics being updated - pub node_stats_updating_delay: Duration, - - /// Initial value of an exponential backoff to reconnect to dropped TCP connection when - /// forwarding sphinx packets. - pub packet_forwarding_initial_backoff: Duration, - - /// Maximum value of an exponential backoff to reconnect to dropped TCP connection when - /// forwarding sphinx packets. - pub packet_forwarding_maximum_backoff: Duration, - - /// Timeout for establishing initial connection when trying to forward a sphinx packet. - pub initial_connection_timeout: Duration, - - /// Maximum number of packets that can be stored waiting to get sent to a particular connection. - pub maximum_connection_buffer_size: usize, - - /// Specifies whether the mixnode should be using the legacy framing for the sphinx packets. - // it's set to true by default. The reason for that decision is to preserve compatibility with the - // existing nodes whilst everyone else is upgrading and getting the code for handling the new field. - // It shall be disabled in the subsequent releases. - pub use_legacy_framed_packet_version: bool, -} - -impl Default for Debug { - fn default() -> Self { - Debug { - node_stats_logging_delay: DEFAULT_NODE_STATS_LOGGING_DELAY, - node_stats_updating_delay: DEFAULT_NODE_STATS_UPDATING_DELAY, - packet_forwarding_initial_backoff: DEFAULT_PACKET_FORWARDING_INITIAL_BACKOFF, - packet_forwarding_maximum_backoff: DEFAULT_PACKET_FORWARDING_MAXIMUM_BACKOFF, - initial_connection_timeout: DEFAULT_INITIAL_CONNECTION_TIMEOUT, - maximum_connection_buffer_size: DEFAULT_MAXIMUM_CONNECTION_BUFFER_SIZE, - use_legacy_framed_packet_version: false, - } - } -} diff --git a/mixnode/src/lib.rs b/mixnode/src/lib.rs deleted file mode 100644 index 8cc9effac67..00000000000 --- a/mixnode/src/lib.rs +++ /dev/null @@ -1,7 +0,0 @@ -// Copyright 2024 - Nym Technologies SA <contact@nymtech.net> -// SPDX-License-Identifier: GPL-3.0-only - -pub mod config; -pub mod node; - -pub use node::MixNode; diff --git a/mixnode/src/node/listener/connection_handler/mod.rs b/mixnode/src/node/listener/connection_handler/mod.rs deleted file mode 100644 index 8d04330e72c..00000000000 --- a/mixnode/src/node/listener/connection_handler/mod.rs +++ /dev/null @@ -1,129 +0,0 @@ -// Copyright 2020 - Nym Technologies SA <contact@nymtech.net> -// SPDX-License-Identifier: GPL-3.0-only - -use crate::node::listener::connection_handler::packet_processing::PacketProcessor; -use crate::node::packet_delayforwarder::PacketDelayForwardSender; -use crate::node::TaskClient; -use futures::StreamExt; -use nym_metrics::nanos; -use nym_sphinx::forwarding::packet::MixPacket; -use nym_sphinx::framing::codec::NymCodec; -use nym_sphinx::framing::packet::FramedNymPacket; -use nym_sphinx::framing::processing::MixProcessingResult; -use nym_sphinx::Delay as SphinxDelay; -use packet_processing::process_received_packet; -use std::net::SocketAddr; -use tokio::net::TcpStream; -use tokio::time::Instant; -use tokio_util::codec::Framed; -use tracing::{debug, error, info, trace, warn}; - -pub(crate) mod packet_processing; - -#[derive(Clone)] -pub(crate) struct ConnectionHandler { - packet_processor: PacketProcessor, - delay_forwarding_channel: PacketDelayForwardSender, -} - -impl ConnectionHandler { - pub(crate) fn new( - packet_processor: PacketProcessor, - delay_forwarding_channel: PacketDelayForwardSender, - ) -> Self { - ConnectionHandler { - packet_processor, - delay_forwarding_channel, - } - } - - pub fn packet_processor(&self) -> &PacketProcessor { - &self.packet_processor - } - - fn delay_and_forward_packet(&self, mix_packet: MixPacket, delay: Option<SphinxDelay>) { - // determine instant at which packet should get forwarded. this way we minimise effect of - // being stuck in the queue [of the channel] to get inserted into the delay queue - let forward_instant = delay.map(|delay| Instant::now() + delay.to_duration()); - - // if unbounded_send() failed it means that the receiver channel was disconnected - // and hence something weird must have happened without a way of recovering - self.delay_forwarding_channel - .unbounded_send((mix_packet, forward_instant)) - .expect("the delay-forwarder has died!"); - } - - fn handle_received_packet(&self, framed_sphinx_packet: FramedNymPacket) { - // - // TODO: here be replay attack detection - it will require similar key cache to the one in - // packet processor for vpn packets, - // question: can it also be per connection vs global? - // - - // all processing such, key caching, etc. was done. - // however, if it was a forward hop, we still need to delay it - nanos!("handle_received_packet", { - self.packet_processor - .node_stats_update_sender() - .report_received(); - match process_received_packet(framed_sphinx_packet, self.packet_processor().inner()) { - Err(err) => debug!("We failed to process received sphinx packet - {err}"), - Ok(res) => match res { - MixProcessingResult::ForwardHop(forward_packet, delay) => { - self.delay_and_forward_packet(forward_packet, delay) - } - MixProcessingResult::FinalHop(..) => { - warn!("Somehow processed a loop cover message that we haven't implemented yet!") - } - }, - } - }) - } - - pub(crate) async fn handle_connection( - self, - conn: TcpStream, - remote: SocketAddr, - mut shutdown: TaskClient, - ) { - debug!("Starting connection handler for {:?}", remote); - shutdown.disarm(); - let mut framed_conn = Framed::new(conn, NymCodec); - while !shutdown.is_shutdown() { - tokio::select! { - biased; - _ = shutdown.recv() => { - trace!("ConnectionHandler: received shutdown"); - } - framed_sphinx_packet = framed_conn.next() => { - match framed_sphinx_packet { - Some(Ok(framed_sphinx_packet)) => { - // TODO: benchmark spawning tokio task with full processing vs just processing it - // synchronously (without delaying inside of course, - // delay is moved to a global DelayQueue) - // under higher load in single and multi-threaded situation. - - // in theory we could process multiple sphinx packet from the same connection in parallel, - // but we already handle multiple concurrent connections so if anything, making - // that change would only slow things down - self.handle_received_packet(framed_sphinx_packet); - } - Some(Err(err)) => { - error!( - "{remote:?} - The socket connection got corrupted with error: {err}. Closing the socket", - ); - return; - } - None => break, // stream got closed by remote - } - }, - } - } - - info!( - "Closing connection from {:?}", - framed_conn.into_inner().peer_addr() - ); - trace!("ConnectionHandler: Exiting"); - } -} diff --git a/mixnode/src/node/listener/connection_handler/packet_processing.rs b/mixnode/src/node/listener/connection_handler/packet_processing.rs deleted file mode 100644 index 9e6742a6b85..00000000000 --- a/mixnode/src/node/listener/connection_handler/packet_processing.rs +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright 2020 - Nym Technologies SA <contact@nymtech.net> -// SPDX-License-Identifier: GPL-3.0-only - -use crate::node::node_statistics; -use nym_crypto::asymmetric::encryption; -use nym_mixnode_common::packet_processor::error::MixProcessingError; -use nym_mixnode_common::packet_processor::processor::SphinxPacketProcessor; -use nym_sphinx::framing::packet::FramedNymPacket; -use nym_sphinx::framing::processing::{process_framed_packet, MixProcessingResult}; - -// PacketProcessor contains all data required to correctly unwrap and forward sphinx packets -#[derive(Clone)] -pub(crate) struct PacketProcessor { - /// Responsible for performing unwrapping - inner_processor: SphinxPacketProcessor, - - /// Responsible for updating metrics data - node_stats_update_sender: node_statistics::UpdateSender, -} - -impl PacketProcessor { - pub(crate) fn new( - encryption_key: &encryption::PrivateKey, - node_stats_update_sender: node_statistics::UpdateSender, - ) -> Self { - PacketProcessor { - inner_processor: SphinxPacketProcessor::new(encryption_key.into()), - node_stats_update_sender, - } - } - - pub fn inner(&self) -> &SphinxPacketProcessor { - &self.inner_processor - } - - pub fn node_stats_update_sender(&self) -> &node_statistics::UpdateSender { - &self.node_stats_update_sender - } -} - -pub fn process_received_packet( - packet: FramedNymPacket, - inner_processor: &SphinxPacketProcessor, -) -> Result<MixProcessingResult, MixProcessingError> { - Ok(process_framed_packet(packet, inner_processor.sphinx_key())?) -} diff --git a/mixnode/src/node/listener/mod.rs b/mixnode/src/node/listener/mod.rs deleted file mode 100644 index a532ffc5d05..00000000000 --- a/mixnode/src/node/listener/mod.rs +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright 2020 - Nym Technologies SA <contact@nymtech.net> -// SPDX-License-Identifier: GPL-3.0-only - -use crate::node::listener::connection_handler::ConnectionHandler; -use std::net::SocketAddr; -use std::process; -use tokio::net::TcpListener; -use tokio::task::JoinHandle; -use tracing::{error, info, trace, warn}; - -use super::TaskClient; - -pub(crate) mod connection_handler; - -pub(crate) struct Listener { - address: SocketAddr, - shutdown: TaskClient, -} - -impl Listener { - pub(crate) fn new(address: SocketAddr, shutdown: TaskClient) -> Self { - Listener { address, shutdown } - } - - async fn run(&mut self, connection_handler: ConnectionHandler) { - trace!("Starting Listener"); - let listener = match TcpListener::bind(self.address).await { - Ok(listener) => listener, - Err(err) => { - error!("Failed to bind to {} - {err}. Are you sure nothing else is running on the specified port and your user has sufficient permission to bind to the requested address?", self.address); - process::exit(1); - } - }; - - while !self.shutdown.is_shutdown() { - tokio::select! { - biased; - _ = self.shutdown.recv() => { - trace!("Listener: Received shutdown"); - } - connection = listener.accept() => { - match connection { - Ok((socket, remote_addr)) => { - let handler = connection_handler.clone(); - tokio::spawn(handler.handle_connection(socket, remote_addr, self.shutdown.clone())); - } - Err(err) => warn!("Failed to accept incoming connection - {err}"), - } - }, - }; - } - trace!("Listener: Exiting"); - } - - pub(crate) fn start(mut self, connection_handler: ConnectionHandler) -> JoinHandle<()> { - info!("Running mix listener on {:?}", self.address.to_string()); - - tokio::spawn(async move { self.run(connection_handler).await }) - } -} diff --git a/mixnode/src/node/mod.rs b/mixnode/src/node/mod.rs deleted file mode 100644 index 9e6358de57f..00000000000 --- a/mixnode/src/node/mod.rs +++ /dev/null @@ -1,217 +0,0 @@ -// Copyright 2020-2023 - Nym Technologies SA <contact@nymtech.net> -// SPDX-License-Identifier: GPL-3.0-only - -use crate::config::Config; -use crate::node::listener::connection_handler::packet_processing::PacketProcessor; -use crate::node::listener::connection_handler::ConnectionHandler; -use crate::node::listener::Listener; -use crate::node::packet_delayforwarder::{DelayForwarder, PacketDelayForwardSender}; -use nym_crypto::asymmetric::{encryption, identity}; -use nym_mixnode_common::verloc; -use nym_mixnode_common::verloc::VerlocMeasurer; -use nym_node_http_api::state::metrics::{SharedMixingStats, SharedVerlocStats}; -use nym_task::{TaskClient, TaskHandle}; -use std::net::SocketAddr; -use std::process; -use std::sync::Arc; -use tracing::{error, info, warn}; - -mod listener; -mod node_statistics; -mod packet_delayforwarder; - -// the MixNode will live for whole duration of this program -pub struct MixNode { - config: Config, - identity_keypair: Arc<identity::KeyPair>, - sphinx_keypair: Arc<encryption::KeyPair>, - - task_client: Option<TaskClient>, - mixing_stats: Option<SharedMixingStats>, - verloc_stats: Option<SharedVerlocStats>, -} - -impl MixNode { - pub fn new_loaded( - config: Config, - identity_keypair: Arc<identity::KeyPair>, - sphinx_keypair: Arc<encryption::KeyPair>, - ) -> Self { - MixNode { - task_client: None, - config, - identity_keypair, - sphinx_keypair, - mixing_stats: None, - verloc_stats: None, - } - } - - pub fn set_task_client(&mut self, task_client: TaskClient) { - self.task_client = Some(task_client) - } - - pub fn set_mixing_stats(&mut self, mixing_stats: SharedMixingStats) { - self.mixing_stats = Some(mixing_stats); - } - - pub fn set_verloc_stats(&mut self, verloc_stats: SharedVerlocStats) { - self.verloc_stats = Some(verloc_stats) - } - - fn start_node_stats_controller( - &mut self, - shutdown: TaskClient, - ) -> (SharedMixingStats, node_statistics::UpdateSender) { - info!("Starting node stats controller..."); - let mixing_stats = self.mixing_stats.take().unwrap_or_default(); - - let controller = node_statistics::Controller::new( - self.config.debug.node_stats_logging_delay, - self.config.debug.node_stats_updating_delay, - mixing_stats.clone(), - shutdown, - ); - let update_sender = controller.start(); - - (mixing_stats, update_sender) - } - - fn start_socket_listener( - &self, - node_stats_update_sender: node_statistics::UpdateSender, - delay_forwarding_channel: PacketDelayForwardSender, - shutdown: TaskClient, - ) { - info!("Starting socket listener..."); - - let packet_processor = - PacketProcessor::new(self.sphinx_keypair.private_key(), node_stats_update_sender); - - let connection_handler = ConnectionHandler::new(packet_processor, delay_forwarding_channel); - - let listening_address = SocketAddr::new( - self.config.mixnode.listening_address, - self.config.mixnode.mix_port, - ); - - Listener::new(listening_address, shutdown).start(connection_handler); - } - - fn start_packet_delay_forwarder( - &mut self, - node_stats_update_sender: node_statistics::UpdateSender, - shutdown: TaskClient, - ) -> PacketDelayForwardSender { - info!("Starting packet delay-forwarder..."); - - let client_config = nym_mixnet_client::Config::new( - self.config.debug.packet_forwarding_initial_backoff, - self.config.debug.packet_forwarding_maximum_backoff, - self.config.debug.initial_connection_timeout, - self.config.debug.maximum_connection_buffer_size, - self.config.debug.use_legacy_framed_packet_version, - ); - - let mut packet_forwarder = DelayForwarder::new( - nym_mixnet_client::Client::new(client_config), - node_stats_update_sender, - shutdown, - ); - - let packet_sender = packet_forwarder.sender(); - - tokio::spawn(async move { packet_forwarder.run().await }); - packet_sender - } - - fn start_verloc_measurements(&mut self, shutdown: TaskClient) -> SharedVerlocStats { - info!("Starting the round-trip-time measurer..."); - - // use the same binding address with the HARDCODED port for time being (I don't like that approach personally) - let listening_address = SocketAddr::new( - self.config.mixnode.listening_address, - self.config.mixnode.verloc_port, - ); - - let config = verloc::ConfigBuilder::new() - .listening_address(listening_address) - .packets_per_node(self.config.verloc.packets_per_node) - .connection_timeout(self.config.verloc.connection_timeout) - .packet_timeout(self.config.verloc.packet_timeout) - .delay_between_packets(self.config.verloc.delay_between_packets) - .tested_nodes_batch_size(self.config.verloc.tested_nodes_batch_size) - .testing_interval(self.config.verloc.testing_interval) - .retry_timeout(self.config.verloc.retry_timeout) - .nym_api_urls(self.config.get_nym_api_endpoints()) - .build(); - - let verloc_state = self.verloc_stats.take().unwrap_or_default(); - let mut verloc_measurer = - VerlocMeasurer::new(config, Arc::clone(&self.identity_keypair), shutdown); - verloc_measurer.set_shared_state(verloc_state.clone()); - - tokio::spawn(async move { verloc_measurer.run().await }); - verloc_state - } - - async fn check_if_bonded(&self) -> bool { - // TODO: if anything, this should be getting data directly from the contract - // as opposed to the validator API - for api_url in self.config.get_nym_api_endpoints() { - let client = nym_validator_client::NymApiClient::new(api_url.clone()); - match client.get_all_basic_nodes(None).await { - Ok(nodes) => { - return nodes.iter().any(|node| { - &node.ed25519_identity_pubkey == self.identity_keypair.public_key() - }) - } - Err(err) => { - error!("failed to grab initial network mixnodes from {api_url}: {err}",); - } - } - } - - error!( - "failed to grab initial network mixnodes from any of the available apis. Please try to startup again in few minutes", - ); - process::exit(1); - } - - async fn wait_for_interrupt(&self, shutdown: TaskHandle) { - let _res = shutdown.wait_for_shutdown().await; - info!("Stopping nym mixnode"); - } - - pub async fn run(&mut self) { - info!("Starting nym mixnode"); - - if self.check_if_bonded().await { - warn!("You seem to have bonded your mixnode before starting it - that's highly unrecommended as in the future it might result in slashing"); - } - - // Shutdown notifier for signalling tasks to stop - let shutdown = self - .task_client - .take() - .map(Into::<TaskHandle>::into) - .unwrap_or_default() - .name_if_unnamed("mixnode"); - - let (_, node_stats_update_sender) = - self.start_node_stats_controller(shutdown.fork("node_statistics::Controller")); - let delay_forwarding_channel = self.start_packet_delay_forwarder( - node_stats_update_sender.clone(), - shutdown.fork("DelayForwarder"), - ); - self.start_socket_listener( - node_stats_update_sender, - delay_forwarding_channel, - shutdown.fork("Listener"), - ); - self.start_verloc_measurements(shutdown.fork("VerlocMeasurer")); - - info!("Finished nym mixnode startup procedure - it should now be able to receive mix traffic!"); - self.wait_for_interrupt(shutdown).await; - } -} diff --git a/mixnode/src/node/node_statistics.rs b/mixnode/src/node/node_statistics.rs deleted file mode 100644 index 3e3f84cd875..00000000000 --- a/mixnode/src/node/node_statistics.rs +++ /dev/null @@ -1,435 +0,0 @@ -// Copyright 2023 - Nym Technologies SA <contact@nymtech.net> -// SPDX-License-Identifier: GPL-3.0-only - -use nym_metrics::inc_by; - -use super::TaskClient; -use futures::channel::mpsc; -use futures::lock::Mutex; -use futures::StreamExt; -use nym_node_http_api::state::metrics::SharedMixingStats; -use std::collections::HashMap; -use std::ops::DerefMut; -use std::sync::atomic::{AtomicU64, Ordering}; -use std::sync::Arc; -use std::time::Duration; -use time::OffsetDateTime; -use tracing::{debug, info, trace}; - -// convenience aliases -type PacketsMap = HashMap<String, u64>; -type PacketDataReceiver = mpsc::UnboundedReceiver<PacketEvent>; -type PacketDataSender = mpsc::UnboundedSender<PacketEvent>; - -trait MixingStatsUpdateExt { - async fn update(&self, new_received: u64, new_sent: PacketsMap, new_dropped: PacketsMap); -} - -impl MixingStatsUpdateExt for SharedMixingStats { - async fn update(&self, new_received: u64, new_sent: PacketsMap, new_dropped: PacketsMap) { - let mut guard = self.write().await; - let snapshot_time = OffsetDateTime::now_utc(); - - guard.previous_update_time = guard.update_time; - guard.update_time = snapshot_time; - - guard.packets_received_since_startup += new_received; - for count in new_sent.values() { - guard.packets_sent_since_startup_all += count; - } - - for count in new_dropped.values() { - guard.packets_dropped_since_startup_all += count; - } - - inc_by!("packets_received_since_startup", new_received); - inc_by!( - "packets_sent_since_startup_all", - new_sent.values().sum::<u64>() - ); - inc_by!( - "packets_dropped_since_startup_all", - new_dropped.values().sum::<u64>() - ); - - guard.packets_received_since_last_update = new_received; - guard.packets_sent_since_last_update = new_sent; - guard.packets_explicitly_dropped_since_last_update = new_dropped; - } -} - -pub(crate) enum PacketEvent { - Sent(String), - Received, - Dropped(String), -} - -#[derive(Debug, Clone)] -struct CurrentPacketData { - inner: Arc<PacketDataInner>, -} - -#[derive(Debug)] -struct PacketDataInner { - received: AtomicU64, - sent: Mutex<PacketsMap>, - dropped: Mutex<PacketsMap>, -} - -impl CurrentPacketData { - pub(crate) fn new() -> Self { - CurrentPacketData { - inner: Arc::new(PacketDataInner { - received: AtomicU64::new(0), - sent: Mutex::new(HashMap::new()), - dropped: Mutex::new(HashMap::new()), - }), - } - } - - fn increment_received(&self) { - self.inner.received.fetch_add(1, Ordering::SeqCst); - } - - async fn increment_sent(&self, destination: String) { - let mut unlocked = self.inner.sent.lock().await; - let receiver_count = unlocked.entry(destination).or_insert(0); - *receiver_count += 1; - } - - async fn increment_dropped(&self, destination: String) { - let mut unlocked = self.inner.dropped.lock().await; - let dropped_count = unlocked.entry(destination).or_insert(0); - *dropped_count += 1; - } - - async fn acquire_and_reset(&self) -> (u64, PacketsMap, PacketsMap) { - let mut unlocked_sent = self.inner.sent.lock().await; - let mut unlocked_dropped = self.inner.dropped.lock().await; - let received = self.inner.received.swap(0, Ordering::SeqCst); - - let sent = std::mem::take(unlocked_sent.deref_mut()); - let dropped = std::mem::take(unlocked_dropped.deref_mut()); - - (received, sent, dropped) - } -} - -// Worker that listens to a channel and updates the shared current packet data -struct UpdateHandler { - current_data: CurrentPacketData, - update_receiver: PacketDataReceiver, - shutdown: TaskClient, -} - -impl UpdateHandler { - fn new( - current_data: CurrentPacketData, - update_receiver: PacketDataReceiver, - shutdown: TaskClient, - ) -> Self { - UpdateHandler { - current_data, - update_receiver, - shutdown, - } - } - - async fn run(&mut self) { - trace!("Starting UpdateHandler"); - while !self.shutdown.is_shutdown() { - tokio::select! { - Some(packet_data) = self.update_receiver.next() => { - match packet_data { - PacketEvent::Received => self.current_data.increment_received(), - PacketEvent::Sent(destination) => { - self.current_data.increment_sent(destination).await - } - PacketEvent::Dropped(destination) => { - self.current_data.increment_dropped(destination).await - } - } - } - _ = self.shutdown.recv() => { - trace!("UpdateHandler: Received shutdown"); - break; - } - } - } - - trace!("UpdateHandler: Exiting"); - } -} - -// Channel to report statistics -#[derive(Clone)] -pub struct UpdateSender(PacketDataSender); - -impl UpdateSender { - pub(crate) fn new(update_sender: PacketDataSender) -> Self { - UpdateSender(update_sender) - } - - pub(crate) fn report_sent(&self, destination: String) { - // in unbounded_send() failed it means that the receiver channel was disconnected - // and hence something weird must have happened without a way of recovering - self.0 - .unbounded_send(PacketEvent::Sent(destination)) - .unwrap() - } - - // TODO: in the future this could be slightly optimised to get rid of the channel - // in favour of incrementing value directly - pub(crate) fn report_received(&self) { - // in unbounded_send() failed it means that the receiver channel was disconnected - // and hence something weird must have happened without a way of recovering - self.0.unbounded_send(PacketEvent::Received).unwrap() - } - - pub(crate) fn report_dropped(&self, destination: String) { - // in unbounded_send() failed it means that the receiver channel was disconnected - // and hence something weird must have happened without a way of recovering - self.0 - .unbounded_send(PacketEvent::Dropped(destination)) - .unwrap() - } -} - -// Worker that periodically updates the shared node stats from the current packet data buffer that -// the `UpdateHandler` updates. -struct StatsUpdater { - updating_delay: Duration, - current_packet_data: CurrentPacketData, - current_stats: SharedMixingStats, - shutdown: TaskClient, -} - -impl StatsUpdater { - fn new( - updating_delay: Duration, - current_packet_data: CurrentPacketData, - current_stats: SharedMixingStats, - shutdown: TaskClient, - ) -> Self { - StatsUpdater { - updating_delay, - current_packet_data, - current_stats, - shutdown, - } - } - - async fn update_stats(&self) { - // grab new data since last update - let (received, sent, dropped) = self.current_packet_data.acquire_and_reset().await; - self.current_stats.update(received, sent, dropped).await; - } - - async fn run(&mut self) { - while !self.shutdown.is_shutdown() { - tokio::select! { - _ = tokio::time::sleep(self.updating_delay) => self.update_stats().await, - _ = self.shutdown.recv() => { - trace!("StatsUpdater: Received shutdown"); - } - } - } - trace!("StatsUpdater: Exiting"); - } -} - -// TODO: question: should this data still be logged to the console or should we perhaps remove it -// since we have the http endpoint now? -struct PacketStatsConsoleLogger { - logging_delay: Duration, - stats: SharedMixingStats, - shutdown: TaskClient, -} - -impl PacketStatsConsoleLogger { - fn new(logging_delay: Duration, stats: SharedMixingStats, shutdown: TaskClient) -> Self { - PacketStatsConsoleLogger { - logging_delay, - stats, - shutdown, - } - } - - async fn log_running_stats(&mut self) { - let stats = self.stats.read().await; - - // it's super unlikely this will ever fail, but anything involving time is super weird - // so let's just guard against it - let time_difference = stats.update_time - stats.previous_update_time; - if time_difference.is_positive() { - // we honestly don't care if it was 30.000828427s or 30.002461449s, 30s is enough - let difference_secs = time_difference.whole_seconds(); - - info!( - "Since startup mixed {} packets! ({} in last {} seconds)", - stats.packets_sent_since_startup_all, - stats.packets_sent_since_last_update.values().sum::<u64>(), - difference_secs, - ); - if stats.packets_dropped_since_startup_all > 0 { - info!( - "Since startup dropped {} packets! ({} in last {} seconds)", - stats.packets_dropped_since_startup_all, - stats - .packets_explicitly_dropped_since_last_update - .values() - .sum::<u64>(), - difference_secs, - ); - } - - debug!( - "Since startup received {} packets ({} in last {} seconds)", - stats.packets_received_since_startup, - stats.packets_received_since_last_update, - difference_secs, - ); - trace!( - "Since startup sent packets to the following: \n{:#?} \n And in last {} seconds: {:#?})", - stats.packets_sent_since_startup_all, - difference_secs, - stats.packets_sent_since_last_update - ); - } else { - info!( - "Since startup mixed {} packets!", - stats.packets_sent_since_startup_all, - ); - if stats.packets_dropped_since_startup_all > 0 { - info!( - "Since startup dropped {} packets!", - stats.packets_dropped_since_startup_all, - ); - } - - debug!( - "Since startup received {} packets", - stats.packets_received_since_startup - ); - trace!( - "Since startup sent packets {}", - stats.packets_sent_since_startup_all - ); - } - } - - async fn run(&mut self) { - trace!("Starting PacketStatsConsoleLogger"); - while !self.shutdown.is_shutdown() { - tokio::select! { - _ = tokio::time::sleep(self.logging_delay) => self.log_running_stats().await, - _ = self.shutdown.recv() => { - trace!("PacketStatsConsoleLogger: Received shutdown"); - } - }; - } - trace!("PacketStatsConsoleLogger: Exiting"); - } -} - -// basically an easy single entry point to start all of the required tasks -pub struct Controller { - /// Responsible for handling data coming from UpdateSender - update_handler: UpdateHandler, - - /// Wrapper around channel sending information about new packet being received or sent - update_sender: UpdateSender, - - /// Responsible for logging stats to the console at given interval - console_logger: PacketStatsConsoleLogger, - - /// Responsible for updating stats at given interval - stats_updater: StatsUpdater, -} - -impl Controller { - pub(crate) fn new( - logging_delay: Duration, - stats_updating_delay: Duration, - mixing_stats: SharedMixingStats, - shutdown: TaskClient, - ) -> Self { - let (sender, receiver) = mpsc::unbounded(); - let shared_packet_data = CurrentPacketData::new(); - - Controller { - update_handler: UpdateHandler::new( - shared_packet_data.clone(), - receiver, - shutdown.clone(), - ), - update_sender: UpdateSender::new(sender), - console_logger: PacketStatsConsoleLogger::new( - logging_delay, - mixing_stats.clone(), - shutdown.clone(), - ), - stats_updater: StatsUpdater::new( - stats_updating_delay, - shared_packet_data, - mixing_stats.clone(), - shutdown, - ), - } - } - - // reporter is how node is going to be accessing the metrics data - pub(crate) fn start(self) -> UpdateSender { - // move out of self - let mut update_handler = self.update_handler; - let mut stats_updater = self.stats_updater; - let mut console_logger = self.console_logger; - - tokio::spawn(async move { update_handler.run().await }); - tokio::spawn(async move { stats_updater.run().await }); - tokio::spawn(async move { console_logger.run().await }); - - self.update_sender - } -} - -#[cfg(test)] -mod tests { - use super::*; - use nym_metrics::metrics; - use nym_task::TaskManager; - - #[tokio::test] - async fn node_stats_reported_are_received() { - let logging_delay = Duration::from_millis(20); - let stats_updating_delay = Duration::from_millis(10); - let shutdown = TaskManager::default(); - let stats = SharedMixingStats::new(); - let node_stats_controller = Controller::new( - logging_delay, - stats_updating_delay, - stats.clone(), - shutdown.subscribe(), - ); - - let update_sender = node_stats_controller.start(); - tokio::time::pause(); - - // Pass input - update_sender.report_sent("foo".to_string()); - update_sender.report_sent("foo".to_string()); - tokio::task::yield_now().await; - - tokio::time::advance(Duration::from_secs(1)).await; - tokio::task::yield_now().await; - - // Get output (stats) - let stats = stats.read().await; - assert_eq!(&stats.packets_sent_since_startup_all, &2); - assert_eq!(&stats.packets_sent_since_last_update.get("foo"), &Some(&2)); - assert_eq!(&stats.packets_sent_since_last_update.len(), &1); - assert_eq!(&stats.packets_received_since_startup, &0); - assert_eq!(&stats.packets_dropped_since_startup_all, &0); - assert_eq!(metrics!(), "# HELP nym_mixnode_packets_dropped_since_startup_all nym_mixnode_packets_dropped_since_startup_all\n# TYPE nym_mixnode_packets_dropped_since_startup_all counter\nnym_mixnode_packets_dropped_since_startup_all 0\n# HELP nym_mixnode_packets_received_since_startup nym_mixnode_packets_received_since_startup\n# TYPE nym_mixnode_packets_received_since_startup counter\nnym_mixnode_packets_received_since_startup 0\n# HELP nym_mixnode_packets_sent_since_startup_all nym_mixnode_packets_sent_since_startup_all\n# TYPE nym_mixnode_packets_sent_since_startup_all counter\nnym_mixnode_packets_sent_since_startup_all 2\n") - } -} diff --git a/mixnode/src/node/packet_delayforwarder.rs b/mixnode/src/node/packet_delayforwarder.rs deleted file mode 100644 index 99a61820ac8..00000000000 --- a/mixnode/src/node/packet_delayforwarder.rs +++ /dev/null @@ -1,319 +0,0 @@ -// Copyright 2020 - Nym Technologies SA <contact@nymtech.net> -// SPDX-License-Identifier: GPL-3.0-only - -use super::TaskClient; -use crate::node::node_statistics::UpdateSender; -use futures::channel::mpsc; -use futures::StreamExt; -use nym_nonexhaustive_delayqueue::{Expired, NonExhaustiveDelayQueue}; -use nym_sphinx::forwarding::packet::MixPacket; -use std::io; -use tokio::time::Instant; -use tracing::trace; - -// Delay + MixPacket vs Instant + MixPacket - -// rather than using Duration directly, we use an Instant, this way we minimise skew due to -// time packet spent waiting in the queue to get delayed -pub(crate) type PacketDelayForwardSender = mpsc::UnboundedSender<(MixPacket, Option<Instant>)>; -type PacketDelayForwardReceiver = mpsc::UnboundedReceiver<(MixPacket, Option<Instant>)>; - -/// Entity responsible for delaying received sphinx packet and forwarding it to next node. -pub(crate) struct DelayForwarder<C> -where - C: nym_mixnet_client::SendWithoutResponse, -{ - delay_queue: NonExhaustiveDelayQueue<MixPacket>, - mixnet_client: C, - packet_sender: PacketDelayForwardSender, - packet_receiver: PacketDelayForwardReceiver, - node_stats_update_sender: UpdateSender, - shutdown: TaskClient, -} - -impl<C> DelayForwarder<C> -where - C: nym_mixnet_client::SendWithoutResponse, -{ - pub(crate) fn new( - client: C, - node_stats_update_sender: UpdateSender, - shutdown: TaskClient, - ) -> DelayForwarder<C> { - let (packet_sender, packet_receiver) = mpsc::unbounded(); - - DelayForwarder::<C> { - delay_queue: NonExhaustiveDelayQueue::new(), - mixnet_client: client, - packet_sender, - packet_receiver, - node_stats_update_sender, - shutdown, - } - } - - pub(crate) fn sender(&self) -> PacketDelayForwardSender { - self.packet_sender.clone() - } - - fn forward_packet(&mut self, packet: MixPacket) { - let next_hop = packet.next_hop(); - let packet_type = packet.packet_type(); - let packet = packet.into_packet(); - - if let Err(err) = self - .mixnet_client - .send_without_response(next_hop, packet, packet_type) - { - if err.kind() == io::ErrorKind::WouldBlock { - // we only know for sure if we dropped a packet if our sending queue was full - // in any other case the connection might still be re-established (or created for the first time) - // and the packet might get sent, but we won't know about it - self.node_stats_update_sender - .report_dropped(next_hop.to_string()) - } else if err.kind() == io::ErrorKind::NotConnected { - // let's give the benefit of the doubt and assume we manage to establish connection - self.node_stats_update_sender - .report_sent(next_hop.to_string()); - } - } else { - self.node_stats_update_sender - .report_sent(next_hop.to_string()); - } - } - - /// Upon packet being finished getting delayed, forward it to the mixnet. - fn handle_done_delaying(&mut self, packet: Expired<MixPacket>) { - let delayed_packet = packet.into_inner(); - self.forward_packet(delayed_packet) - } - - fn handle_new_packet(&mut self, new_packet: (MixPacket, Option<Instant>)) { - // in case of a zero delay packet, don't bother putting it in the delay queue, - // just forward it immediately - if let Some(instant) = new_packet.1 { - // check if the delay has already expired, if so, don't bother putting it through - // the delay queue only to retrieve it immediately. Just forward it. - if instant.checked_duration_since(Instant::now()).is_none() { - self.forward_packet(new_packet.0) - } else { - self.delay_queue.insert_at(new_packet.0, instant); - } - } else { - self.forward_packet(new_packet.0) - } - } - - pub(crate) async fn run(&mut self) { - trace!("Starting DelayForwarder"); - loop { - tokio::select! { - delayed = self.delay_queue.next() => { - self.handle_done_delaying(delayed.unwrap()); - } - new_packet = self.packet_receiver.next() => { - // this one is impossible to ever panic - the object itself contains a sender - // and hence it can't happen that ALL senders are dropped - self.handle_new_packet(new_packet.unwrap()) - } - _ = self.shutdown.recv() => { - trace!("DelayForwarder: Received shutdown"); - break; - } - } - } - trace!("DelayForwarder: Exiting"); - } -} - -#[cfg(test)] -mod tests { - use super::*; - - use std::net::{IpAddr, Ipv4Addr, SocketAddr}; - use std::sync::{Arc, Mutex}; - use std::time::Duration; - - use nym_sphinx::NymPacket; - use nym_task::TaskManager; - - use nym_sphinx::addressing::nodes::NymNodeRoutingAddress; - use nym_sphinx_params::packet_sizes::PacketSize; - use nym_sphinx_params::PacketType; - use nym_sphinx_types::{ - crypto, Delay as SphinxDelay, Destination, DestinationAddressBytes, Node, NodeAddressBytes, - DESTINATION_ADDRESS_LENGTH, IDENTIFIER_LENGTH, NODE_ADDRESS_LENGTH, - }; - - #[derive(Default)] - struct TestClient { - pub packets_sent: Arc<Mutex<Vec<(NymNodeRoutingAddress, NymPacket, PacketType)>>>, - } - - impl nym_mixnet_client::SendWithoutResponse for TestClient { - fn send_without_response( - &mut self, - address: NymNodeRoutingAddress, - packet: NymPacket, - packet_type: PacketType, - ) -> io::Result<()> { - self.packets_sent - .lock() - .unwrap() - .push((address, packet, packet_type)); - Ok(()) - } - } - - fn make_valid_sphinx_packet(size: PacketSize) -> NymPacket { - let (_, node1_pk) = crypto::keygen(); - let node1 = Node::new( - NodeAddressBytes::from_bytes([5u8; NODE_ADDRESS_LENGTH]), - node1_pk, - ); - let (_, node2_pk) = crypto::keygen(); - let node2 = Node::new( - NodeAddressBytes::from_bytes([4u8; NODE_ADDRESS_LENGTH]), - node2_pk, - ); - let (_, node3_pk) = crypto::keygen(); - let node3 = Node::new( - NodeAddressBytes::from_bytes([2u8; NODE_ADDRESS_LENGTH]), - node3_pk, - ); - - let route = [node1, node2, node3]; - let destination = Destination::new( - DestinationAddressBytes::from_bytes([3u8; DESTINATION_ADDRESS_LENGTH]), - [4u8; IDENTIFIER_LENGTH], - ); - let delays = vec![ - SphinxDelay::new_from_nanos(42), - SphinxDelay::new_from_nanos(42), - SphinxDelay::new_from_nanos(42), - ]; - NymPacket::sphinx_build(size.payload_size(), b"foomp", &route, &destination, &delays) - .unwrap() - } - - fn make_valid_outfox_packet(size: PacketSize) -> NymPacket { - let (_, node1_pk) = crypto::keygen(); - let node1 = Node::new( - NodeAddressBytes::from_bytes([5u8; NODE_ADDRESS_LENGTH]), - node1_pk, - ); - let (_, node2_pk) = crypto::keygen(); - let node2 = Node::new( - NodeAddressBytes::from_bytes([4u8; NODE_ADDRESS_LENGTH]), - node2_pk, - ); - let (_, node3_pk) = crypto::keygen(); - let node3 = Node::new( - NodeAddressBytes::from_bytes([2u8; NODE_ADDRESS_LENGTH]), - node3_pk, - ); - - let (_, node4_pk) = crypto::keygen(); - let node4 = Node::new( - NodeAddressBytes::from_bytes([2u8; NODE_ADDRESS_LENGTH]), - node4_pk, - ); - - let destination = Destination::new( - DestinationAddressBytes::from_bytes([3u8; DESTINATION_ADDRESS_LENGTH]), - [4u8; IDENTIFIER_LENGTH], - ); - - let route = &[node1, node2, node3, node4]; - - let payload = vec![1; 48]; - - NymPacket::outfox_build(payload, route, &destination, Some(size.plaintext_size())).unwrap() - } - - #[tokio::test] - async fn packets_received_are_forwarded() { - // Wire up the DelayForwarder - let (stats_sender, _stats_receiver) = mpsc::unbounded(); - let node_stats_update_sender = UpdateSender::new(stats_sender); - let client = TestClient::default(); - let client_packets_sent = client.packets_sent.clone(); - let shutdown = TaskManager::default(); - let mut delay_forwarder = - DelayForwarder::new(client, node_stats_update_sender, shutdown.subscribe()); - let packet_sender = delay_forwarder.sender(); - - // Spawn the worker, listening on packet_sender channel - tokio::spawn(async move { delay_forwarder.run().await }); - - // Send a `MixPacket` down the channel without any delay attached. - let next_hop = - NymNodeRoutingAddress::from(SocketAddr::new(IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4)), 42)); - let mix_packet = MixPacket::new( - next_hop, - make_valid_sphinx_packet(PacketSize::default()), - PacketType::default(), - ); - let forward_instant = None; - packet_sender - .unbounded_send((mix_packet, forward_instant)) - .unwrap(); - - // Give the the worker a chance to act - tokio::time::sleep(Duration::from_millis(10)).await; - - // The client should have forwarded the packet straight away - assert_eq!( - client_packets_sent - .lock() - .unwrap() - .iter() - .map(|(a, _, _)| *a) - .collect::<Vec<_>>(), - vec![next_hop] - ); - } - - #[tokio::test] - async fn outfox_packets_received_are_forwarded() { - // Wire up the DelayForwarder - let (stats_sender, _stats_receiver) = mpsc::unbounded(); - let node_stats_update_sender = UpdateSender::new(stats_sender); - let client = TestClient::default(); - let client_packets_sent = client.packets_sent.clone(); - let shutdown = TaskManager::default(); - let mut delay_forwarder = - DelayForwarder::new(client, node_stats_update_sender, shutdown.subscribe()); - let packet_sender = delay_forwarder.sender(); - - // Spawn the worker, listening on packet_sender channel - tokio::spawn(async move { delay_forwarder.run().await }); - - // Send a `MixPacket` down the channel without any delay attached. - let next_hop = - NymNodeRoutingAddress::from(SocketAddr::new(IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4)), 42)); - let mix_packet = MixPacket::new( - next_hop, - make_valid_outfox_packet(PacketSize::default()), - PacketType::default(), - ); - let forward_instant = None; - packet_sender - .unbounded_send((mix_packet, forward_instant)) - .unwrap(); - - // Give the the worker a chance to act - tokio::time::sleep(Duration::from_millis(10)).await; - - // The client should have forwarded the packet straight away - assert_eq!( - client_packets_sent - .lock() - .unwrap() - .iter() - .map(|(a, _, _)| *a) - .collect::<Vec<_>>(), - vec![next_hop] - ); - } -} diff --git a/nym-api/Cargo.toml b/nym-api/Cargo.toml index 6f9602eb060..27814af1dba 100644 --- a/nym-api/Cargo.toml +++ b/nym-api/Cargo.toml @@ -4,7 +4,7 @@ [package] name = "nym-api" license = "GPL-3.0" -version = "1.1.46" +version = "1.1.47" authors.workspace = true edition = "2021" rust-version.workspace = true @@ -121,7 +121,7 @@ nym-types = { path = "../common/types" } nym-http-api-common = { path = "../common/http-api-common", features = ["utoipa"] } nym-serde-helpers = { path = "../common/serde-helpers", features = ["date"] } nym-ticketbooks-merkle = { path = "../common/ticketbooks-merkle" } -nym-statistics-common = {path ="../common/statistics" } +nym-statistics-common = { path = "../common/statistics" } [features] no-reward = [] diff --git a/nym-api/migrations/20241127110000_add_monitor_run_indexes.sql b/nym-api/migrations/20241127110000_add_monitor_run_indexes.sql new file mode 100644 index 00000000000..f639bbba7aa --- /dev/null +++ b/nym-api/migrations/20241127110000_add_monitor_run_indexes.sql @@ -0,0 +1,8 @@ +/* + * Copyright 2024 - Nym Technologies SA <contact@nymtech.net> + * SPDX-License-Identifier: Apache-2.0 + */ + +CREATE INDEX IF NOT EXISTS monitor_run_id on monitor_run(id); +CREATE INDEX IF NOT EXISTS monitor_run_timestamp on monitor_run(timestamp); +CREATE INDEX IF NOT EXISTS testing_route_monitor_run_id on testing_route(monitor_run_id); \ No newline at end of file diff --git a/nym-api/migrations/20241204120000_test_run_report.sql b/nym-api/migrations/20241204120000_test_run_report.sql new file mode 100644 index 00000000000..62b820b000d --- /dev/null +++ b/nym-api/migrations/20241204120000_test_run_report.sql @@ -0,0 +1,23 @@ +/* + * Copyright 2024 - Nym Technologies SA <contact@nymtech.net> + * SPDX-License-Identifier: GPL-3.0-only + */ + +CREATE TABLE monitor_run_report +( + monitor_run_id INTEGER PRIMARY KEY REFERENCES monitor_run (id), + network_reliability FLOAT NOT NULL, + packets_sent INTEGER NOT NULL, + packets_received INTEGER NOT NULL +); + +CREATE TABLE monitor_run_score +( +-- mixnode or gateway + typ TEXT NOT NULL, + monitor_run_id INTEGER NOT NULL REFERENCES monitor_run_report (monitor_run_id), + rounded_score INTEGER NOT NULL, + nodes_count INTEGER NOT NULL +); + +CREATE INDEX monitor_run_score_id ON monitor_run_score (monitor_run_id); \ No newline at end of file diff --git a/nym-api/nym-api-requests/src/helpers.rs b/nym-api/nym-api-requests/src/helpers.rs index 651030fc746..292ffb69f49 100644 --- a/nym-api/nym-api-requests/src/helpers.rs +++ b/nym-api/nym-api-requests/src/helpers.rs @@ -64,7 +64,7 @@ pub(crate) mod overengineered_offset_date_time_serde { ])), ]; - impl<'de> Visitor<'de> for OffsetDateTimeVisitor { + impl Visitor<'_> for OffsetDateTimeVisitor { type Value = OffsetDateTime; fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result { diff --git a/nym-api/nym-api-requests/src/models.rs b/nym-api/nym-api-requests/src/models.rs index ee87d37f74e..3b9c759620b 100644 --- a/nym-api/nym-api-requests/src/models.rs +++ b/nym-api/nym-api-requests/src/models.rs @@ -32,6 +32,7 @@ use schemars::gen::SchemaGenerator; use schemars::schema::{InstanceType, Schema, SchemaObject}; use schemars::JsonSchema; use serde::{Deserialize, Deserializer, Serialize}; +use std::collections::BTreeMap; use std::fmt::{Debug, Display, Formatter}; use std::net::IpAddr; use std::ops::{Deref, DerefMut}; @@ -1255,6 +1256,18 @@ pub struct PartialTestResult { pub type MixnodeTestResultResponse = PaginatedResponse<PartialTestResult>; pub type GatewayTestResultResponse = PaginatedResponse<PartialTestResult>; +#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] +pub struct NetworkMonitorRunDetailsResponse { + pub monitor_run_id: i64, + pub network_reliability: f64, + pub total_sent: usize, + pub total_received: usize, + + // integer score to number of nodes with that score + pub mixnode_results: BTreeMap<u8, usize>, + pub gateway_results: BTreeMap<u8, usize>, +} + #[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] pub struct NoiseDetails { #[schemars(with = "String")] @@ -1327,6 +1340,138 @@ impl NodeRefreshBody { } } +#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] +pub struct RewardedSetResponse { + pub entry_gateways: Vec<NodeId>, + + pub exit_gateways: Vec<NodeId>, + + pub layer1: Vec<NodeId>, + + pub layer2: Vec<NodeId>, + + pub layer3: Vec<NodeId>, + + pub standby: Vec<NodeId>, +} + +pub use config_score::*; +pub mod config_score { + use nym_contracts_common::NaiveFloat; + use serde::{Deserialize, Serialize}; + use std::cmp::Ordering; + use utoipa::ToSchema; + + #[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] + pub struct ConfigScoreDataResponse { + pub parameters: ConfigScoreParams, + pub version_history: Vec<HistoricalNymNodeVersionEntry>, + } + + #[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema, PartialEq)] + pub struct HistoricalNymNodeVersionEntry { + /// The unique, ordered, id of this particular entry + pub id: u32, + + /// Data associated with this particular version + pub version_information: HistoricalNymNodeVersion, + } + + impl PartialOrd for HistoricalNymNodeVersionEntry { + fn partial_cmp(&self, other: &Self) -> Option<Ordering> { + // we only care about id for the purposes of ordering as they should have unique data + self.id.partial_cmp(&other.id) + } + } + + impl From<nym_mixnet_contract_common::HistoricalNymNodeVersionEntry> + for HistoricalNymNodeVersionEntry + { + fn from(value: nym_mixnet_contract_common::HistoricalNymNodeVersionEntry) -> Self { + HistoricalNymNodeVersionEntry { + id: value.id, + version_information: value.version_information.into(), + } + } + } + + #[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema, PartialEq)] + pub struct HistoricalNymNodeVersion { + /// Version of the nym node that is going to be used for determining the version score of a node. + /// note: value stored here is pre-validated `semver::Version` + pub semver: String, + + /// Block height of when this version has been added to the contract + pub introduced_at_height: u64, + // for now ignore that field. it will give nothing useful to the users + // pub difference_since_genesis: TotalVersionDifference, + } + + impl From<nym_mixnet_contract_common::HistoricalNymNodeVersion> for HistoricalNymNodeVersion { + fn from(value: nym_mixnet_contract_common::HistoricalNymNodeVersion) -> Self { + HistoricalNymNodeVersion { + semver: value.semver, + introduced_at_height: value.introduced_at_height, + } + } + } + + #[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] + pub struct ConfigScoreParams { + /// Defines weights for calculating numbers of versions behind the current release. + pub version_weights: OutdatedVersionWeights, + + /// Defines the parameters of the formula for calculating the version score + pub version_score_formula_params: VersionScoreFormulaParams, + } + + /// Defines weights for calculating numbers of versions behind the current release. + #[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] + pub struct OutdatedVersionWeights { + pub major: u32, + pub minor: u32, + pub patch: u32, + pub prerelease: u32, + } + + /// Given the formula of version_score = penalty ^ (versions_behind_factor ^ penalty_scaling) + /// define the relevant parameters + #[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] + pub struct VersionScoreFormulaParams { + pub penalty: f64, + pub penalty_scaling: f64, + } + + impl From<nym_mixnet_contract_common::ConfigScoreParams> for ConfigScoreParams { + fn from(value: nym_mixnet_contract_common::ConfigScoreParams) -> Self { + ConfigScoreParams { + version_weights: value.version_weights.into(), + version_score_formula_params: value.version_score_formula_params.into(), + } + } + } + + impl From<nym_mixnet_contract_common::OutdatedVersionWeights> for OutdatedVersionWeights { + fn from(value: nym_mixnet_contract_common::OutdatedVersionWeights) -> Self { + OutdatedVersionWeights { + major: value.major, + minor: value.minor, + patch: value.patch, + prerelease: value.prerelease, + } + } + } + + impl From<nym_mixnet_contract_common::VersionScoreFormulaParams> for VersionScoreFormulaParams { + fn from(value: nym_mixnet_contract_common::VersionScoreFormulaParams) -> Self { + VersionScoreFormulaParams { + penalty: value.penalty.naive_to_f64(), + penalty_scaling: value.penalty_scaling.naive_to_f64(), + } + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/nym-api/src/ecash/api_routes/aggregation.rs b/nym-api/src/ecash/api_routes/aggregation.rs index 89478fbd09a..8b7baef6e66 100644 --- a/nym-api/src/ecash/api_routes/aggregation.rs +++ b/nym-api/src/ecash/api_routes/aggregation.rs @@ -6,7 +6,7 @@ use crate::ecash::error::EcashError; use crate::ecash::state::EcashState; use crate::node_status_api::models::AxumResult; use crate::support::http::state::AppState; -use axum::extract::Path; +use axum::extract::{Query, State}; use axum::{Json, Router}; use nym_api_requests::ecash::models::{ AggregatedCoinIndicesSignatureResponse, AggregatedExpirationDateSignatureResponse, @@ -21,28 +21,19 @@ use tracing::trace; use utoipa::IntoParams; /// routes with globally aggregated keys, signatures, etc. -pub(crate) fn aggregation_routes(ecash_state: Arc<EcashState>) -> Router<AppState> { +pub(crate) fn aggregation_routes() -> Router<AppState> { Router::new() .route( "/master-verification-key", - axum::routing::get({ - let ecash_state = Arc::clone(&ecash_state); - |epoch_id| master_verification_key(epoch_id, ecash_state) - }), + axum::routing::get(master_verification_key), ) .route( "/aggregated-expiration-date-signatures", - axum::routing::get({ - let ecash_state = Arc::clone(&ecash_state); - |expiration_date| expiration_date_signatures(expiration_date, ecash_state) - }), + axum::routing::get(expiration_date_signatures), ) .route( "/aggregated-coin-indices-signatures", - axum::routing::get({ - let ecash_state = Arc::clone(&ecash_state); - |epoch_id| coin_indices_signatures(epoch_id, ecash_state) - }), + axum::routing::get(coin_indices_signatures), ) } @@ -58,8 +49,8 @@ pub(crate) fn aggregation_routes(ecash_state: Arc<EcashState>) -> Router<AppStat ) )] async fn master_verification_key( - Path(EpochIdParam { epoch_id }): Path<EpochIdParam>, - state: Arc<EcashState>, + State(state): State<Arc<EcashState>>, + Query(EpochIdParam { epoch_id }): Query<EpochIdParam>, ) -> AxumResult<Json<VerificationKeyResponse>> { trace!("aggregated_verification_key request"); @@ -72,7 +63,6 @@ async fn master_verification_key( } #[derive(Deserialize, IntoParams)] -#[into_params(parameter_in = Path)] struct ExpirationDateParam { expiration_date: Option<String>, } @@ -89,8 +79,8 @@ struct ExpirationDateParam { ) )] async fn expiration_date_signatures( - Path(ExpirationDateParam { expiration_date }): Path<ExpirationDateParam>, - state: Arc<EcashState>, + State(state): State<Arc<EcashState>>, + Query(ExpirationDateParam { expiration_date }): Query<ExpirationDateParam>, ) -> AxumResult<Json<AggregatedExpirationDateSignatureResponse>> { trace!("aggregated_expiration_date_signatures request"); @@ -126,8 +116,8 @@ async fn expiration_date_signatures( ) )] async fn coin_indices_signatures( - Path(EpochIdParam { epoch_id }): Path<EpochIdParam>, - state: Arc<EcashState>, + Query(EpochIdParam { epoch_id }): Query<EpochIdParam>, + State(state): State<Arc<EcashState>>, ) -> AxumResult<Json<AggregatedCoinIndicesSignatureResponse>> { trace!("aggregated_coin_indices_signatures request"); diff --git a/nym-api/src/ecash/api_routes/handlers.rs b/nym-api/src/ecash/api_routes/handlers.rs index e9f97c48b0e..9114ebba9f2 100644 --- a/nym-api/src/ecash/api_routes/handlers.rs +++ b/nym-api/src/ecash/api_routes/handlers.rs @@ -5,15 +5,13 @@ use crate::ecash::api_routes::aggregation::aggregation_routes; use crate::ecash::api_routes::issued::issued_routes; use crate::ecash::api_routes::partial_signing::partial_signing_routes; use crate::ecash::api_routes::spending::spending_routes; -use crate::ecash::state::EcashState; use crate::support::http::state::AppState; use axum::Router; -use std::sync::Arc; -pub(crate) fn ecash_routes(ecash_state: Arc<EcashState>) -> Router<AppState> { +pub(crate) fn ecash_routes() -> Router<AppState> { Router::new() - .merge(aggregation_routes(Arc::clone(&ecash_state))) - .merge(issued_routes(Arc::clone(&ecash_state))) - .merge(partial_signing_routes(Arc::clone(&ecash_state))) - .merge(spending_routes(Arc::clone(&ecash_state))) + .merge(aggregation_routes()) + .merge(issued_routes()) + .merge(partial_signing_routes()) + .merge(spending_routes()) } diff --git a/nym-api/src/ecash/api_routes/helpers.rs b/nym-api/src/ecash/api_routes/helpers.rs index 0e7afa9b392..9d0270acc7a 100644 --- a/nym-api/src/ecash/api_routes/helpers.rs +++ b/nym-api/src/ecash/api_routes/helpers.rs @@ -2,7 +2,6 @@ // SPDX-License-Identifier: GPL-3.0-only #[derive(serde::Deserialize, utoipa::IntoParams)] -#[into_params(parameter_in = Path)] pub(super) struct EpochIdParam { pub(super) epoch_id: Option<u64>, } diff --git a/nym-api/src/ecash/api_routes/issued.rs b/nym-api/src/ecash/api_routes/issued.rs index 023b57f6a3e..376b5324753 100644 --- a/nym-api/src/ecash/api_routes/issued.rs +++ b/nym-api/src/ecash/api_routes/issued.rs @@ -4,7 +4,7 @@ use crate::ecash::state::EcashState; use crate::node_status_api::models::AxumResult; use crate::support::http::state::AppState; -use axum::extract::Path; +use axum::extract::{Path, State}; use axum::{Json, Router}; use nym_api_requests::ecash::models::{ IssuedTicketbooksChallengeRequest, IssuedTicketbooksChallengeResponse, @@ -17,21 +17,15 @@ use time::Date; use tracing::trace; use utoipa::{IntoParams, ToSchema}; -pub(crate) fn issued_routes(ecash_state: Arc<EcashState>) -> Router<AppState> { +pub(crate) fn issued_routes() -> Router<AppState> { Router::new() .route( "/issued-ticketbooks-for/:expiration_date", - axum::routing::get({ - let ecash_state = Arc::clone(&ecash_state); - |expiration_date| issued_ticketbooks_for(expiration_date, ecash_state) - }), + axum::routing::get(issued_ticketbooks_for), ) .route( "/issued-ticketbooks-challenge", - axum::routing::post({ - let ecash_state = Arc::clone(&ecash_state); - |body| issued_ticketbooks_challenge(body, ecash_state) - }), + axum::routing::post(issued_ticketbooks_challenge), ) } @@ -58,8 +52,8 @@ pub(crate) struct ExpirationDatePathParam { ) )] async fn issued_ticketbooks_for( + State(state): State<Arc<EcashState>>, Path(ExpirationDatePathParam { expiration_date }): Path<ExpirationDatePathParam>, - state: Arc<EcashState>, ) -> AxumResult<Json<IssuedTicketbooksForResponse>> { state.ensure_signer().await?; @@ -83,8 +77,8 @@ async fn issued_ticketbooks_for( ) )] async fn issued_ticketbooks_challenge( + State(state): State<Arc<EcashState>>, Json(challenge): Json<IssuedTicketbooksChallengeRequest>, - state: Arc<EcashState>, ) -> AxumResult<Json<IssuedTicketbooksChallengeResponse>> { trace!("replying to ticketbooks challenge: {:?}", challenge); state.ensure_signer().await?; diff --git a/nym-api/src/ecash/api_routes/partial_signing.rs b/nym-api/src/ecash/api_routes/partial_signing.rs index 7deb919d4e3..afe522cbee1 100644 --- a/nym-api/src/ecash/api_routes/partial_signing.rs +++ b/nym-api/src/ecash/api_routes/partial_signing.rs @@ -7,7 +7,7 @@ use crate::ecash::helpers::blind_sign; use crate::ecash::state::EcashState; use crate::node_status_api::models::AxumResult; use crate::support::http::state::AppState; -use axum::extract::Query; +use axum::extract::{Query, State}; use axum::{Json, Router}; use nym_api_requests::ecash::{ BlindSignRequestBody, BlindedSignatureResponse, PartialCoinIndicesSignatureResponse, @@ -22,28 +22,16 @@ use time::Date; use tracing::{debug, trace}; use utoipa::IntoParams; -pub(crate) fn partial_signing_routes(ecash_state: Arc<EcashState>) -> Router<AppState> { +pub(crate) fn partial_signing_routes() -> Router<AppState> { Router::new() - .route( - "/blind-sign", - axum::routing::post({ - let ecash_state = Arc::clone(&ecash_state); - |body| post_blind_sign(body, ecash_state) - }), - ) + .route("/blind-sign", axum::routing::post(post_blind_sign)) .route( "/partial-expiration-date-signatures", - axum::routing::get({ - let ecash_state = Arc::clone(&ecash_state); - |expiration_date| partial_expiration_date_signatures(expiration_date, ecash_state) - }), + axum::routing::get(partial_expiration_date_signatures), ) .route( "/partial-coin-indices-signatures", - axum::routing::get({ - let ecash_state = Arc::clone(&ecash_state); - |epoch_id| partial_coin_indices_signatures(epoch_id, ecash_state) - }), + axum::routing::get(partial_coin_indices_signatures), ) } @@ -59,8 +47,8 @@ pub(crate) fn partial_signing_routes(ecash_state: Arc<EcashState>) -> Router<App ) )] async fn post_blind_sign( + State(state): State<Arc<EcashState>>, Json(blind_sign_request_body): Json<BlindSignRequestBody>, - state: Arc<EcashState>, ) -> AxumResult<Json<BlindedSignatureResponse>> { state.ensure_signer().await?; @@ -134,8 +122,8 @@ struct ExpirationDateParam { ) )] async fn partial_expiration_date_signatures( + State(state): State<Arc<EcashState>>, Query(ExpirationDateParam { expiration_date }): Query<ExpirationDateParam>, - state: Arc<EcashState>, ) -> AxumResult<Json<PartialExpirationDateSignatureResponse>> { state.ensure_signer().await?; @@ -172,8 +160,8 @@ async fn partial_expiration_date_signatures( ) )] async fn partial_coin_indices_signatures( + State(state): State<Arc<EcashState>>, Query(EpochIdParam { epoch_id }): Query<EpochIdParam>, - state: Arc<EcashState>, ) -> AxumResult<Json<PartialCoinIndicesSignatureResponse>> { state.ensure_signer().await?; diff --git a/nym-api/src/ecash/api_routes/spending.rs b/nym-api/src/ecash/api_routes/spending.rs index b9788a4585c..f52aeda2eb0 100644 --- a/nym-api/src/ecash/api_routes/spending.rs +++ b/nym-api/src/ecash/api_routes/spending.rs @@ -5,6 +5,7 @@ use crate::ecash::error::EcashError; use crate::ecash::state::EcashState; use crate::node_status_api::models::{AxumErrorResponse, AxumResult}; use crate::support::http::state::AppState; +use axum::extract::State; use axum::{Json, Router}; use nym_api_requests::constants::MIN_BATCH_REDEMPTION_DELAY; use nym_api_requests::ecash::models::{ @@ -21,28 +22,16 @@ use time::{OffsetDateTime, Time}; use tracing::{error, warn}; #[allow(deprecated)] -pub(crate) fn spending_routes(ecash_state: Arc<EcashState>) -> Router<AppState> { +pub(crate) fn spending_routes() -> Router<AppState> { Router::new() - .route( - "/verify-ecash-ticket", - axum::routing::post({ - let ecash_state = Arc::clone(&ecash_state); - |body| verify_ticket(body, ecash_state) - }), - ) + .route("/verify-ecash-ticket", axum::routing::post(verify_ticket)) .route( "/batch-redeem-ecash-tickets", - axum::routing::post({ - let ecash_state = Arc::clone(&ecash_state); - |body| batch_redeem_tickets(body, ecash_state) - }), + axum::routing::post(batch_redeem_tickets), ) .route( "/double-spending-filter-v1", - axum::routing::get({ - let ecash_state = Arc::clone(&ecash_state); - || double_spending_filter_v1(ecash_state) - }), + axum::routing::get(double_spending_filter_v1), ) } @@ -67,9 +56,9 @@ fn reject_ticket( ) )] async fn verify_ticket( + State(state): State<Arc<EcashState>>, // TODO in the future: make it send binary data rather than json Json(verify_ticket_body): Json<VerifyEcashTicketBody>, - state: Arc<EcashState>, ) -> AxumResult<Json<EcashTicketVerificationResponse>> { state.ensure_signer().await?; @@ -170,9 +159,9 @@ async fn verify_ticket( ) )] async fn batch_redeem_tickets( + State(state): State<Arc<EcashState>>, // TODO in the future: make it send binary data rather than json Json(batch_redeem_credentials_body): Json<BatchRedeemTicketsBody>, - state: Arc<EcashState>, ) -> AxumResult<Json<EcashBatchTicketRedemptionResponse>> { state.ensure_signer().await?; @@ -214,7 +203,7 @@ async fn batch_redeem_tickets( // 5. check if **every** serial number included in the request has been verified by us // if we have more than requested, tough luck, they're going to lose them - let verified = state.get_redeemable_tickets(provider_info).await?; + let verified = state.get_redeemable_tickets(&provider_info).await?; let verified_tickets: HashSet<_> = verified.iter().map(|sn| sn.deref()).collect(); for sn in &received { @@ -226,8 +215,14 @@ async fn batch_redeem_tickets( } } + // 6. vote on the proposal // TODO: offload it to separate task with work queue and batching (of tx messages) to vote for multiple proposals in the same tx + // similarly to what we do inside the credential proxy state.accept_proposal(proposal_id).await?; + + // 7. update the time of the last verification for this provider + state.update_last_batch_verification(&provider_info).await?; + Ok(Json(EcashBatchTicketRedemptionResponse { proposal_accepted: true, })) @@ -244,8 +239,6 @@ async fn batch_redeem_tickets( ) )] #[deprecated] -async fn double_spending_filter_v1( - _state: Arc<EcashState>, -) -> AxumResult<Json<SpentCredentialsResponse>> { +async fn double_spending_filter_v1() -> AxumResult<Json<SpentCredentialsResponse>> { AxumResult::Err(AxumErrorResponse::internal_msg("permanently restricted")) } diff --git a/nym-api/src/ecash/client.rs b/nym-api/src/ecash/client.rs index 805252808ab..f6887f15e7f 100644 --- a/nym-api/src/ecash/client.rs +++ b/nym-api/src/ecash/client.rs @@ -27,7 +27,7 @@ use nym_validator_client::EcashApiClient; #[async_trait] pub trait Client { - async fn address(&self) -> AccountId; + async fn address(&self) -> Result<AccountId>; async fn dkg_contract_address(&self) -> Result<AccountId>; diff --git a/nym-api/src/ecash/dkg/client.rs b/nym-api/src/ecash/dkg/client.rs index 5c8f993690d..4cd8d413432 100644 --- a/nym-api/src/ecash/dkg/client.rs +++ b/nym-api/src/ecash/dkg/client.rs @@ -35,7 +35,7 @@ impl DkgClient { } } - pub(crate) async fn get_address(&self) -> AccountId { + pub(crate) async fn get_address(&self) -> Result<AccountId, EcashError> { self.inner.address().await } @@ -53,7 +53,7 @@ impl DkgClient { pub(crate) async fn group_member(&self) -> Result<MemberResponse, EcashError> { self.inner - .group_member(self.get_address().await.to_string()) + .group_member(self.get_address().await?.to_string()) .await } @@ -126,7 +126,7 @@ impl DkgClient { &self, epoch_id: EpochId, ) -> Result<Option<ContractVKShare>, EcashError> { - let address = self.inner.address().await; + let address = self.inner.address().await?; self.get_verification_key_share(epoch_id, address).await } @@ -138,7 +138,7 @@ impl DkgClient { } pub(crate) async fn get_vote(&self, proposal_id: u64) -> Result<VoteResponse, EcashError> { - let address = self.get_address().await.to_string(); + let address = self.get_address().await?.to_string(); self.inner.get_vote(proposal_id, address).await } diff --git a/nym-api/src/ecash/dkg/dealing.rs b/nym-api/src/ecash/dkg/dealing.rs index a0ae4ac8408..31da8a0ea54 100644 --- a/nym-api/src/ecash/dkg/dealing.rs +++ b/nym-api/src/ecash/dkg/dealing.rs @@ -155,7 +155,7 @@ impl<R: RngCore + CryptoRng> DkgController<R> { resharing: bool, ) -> Result<(), DealingGenerationError> { let dealing_state = self.state.dealing_exchange_state(epoch_id)?; - let address = self.dkg_client.get_address().await.to_string(); + let address = self.dkg_client.get_address().await?.to_string(); let status = self .dkg_client @@ -259,7 +259,7 @@ impl<R: RngCore + CryptoRng> DkgController<R> { .checked_sub(1) .expect("resharing epoch invariant has been broken"); - let address = self.dkg_client.get_address().await; + let address = self.dkg_client.get_address().await?; Ok(self .dkg_client .dealer_in_epoch(previous_epoch_id, address.to_string()) diff --git a/nym-api/src/ecash/dkg/key_derivation.rs b/nym-api/src/ecash/dkg/key_derivation.rs index f3e69e1fe6e..46818547d4f 100644 --- a/nym-api/src/ecash/dkg/key_derivation.rs +++ b/nym-api/src/ecash/dkg/key_derivation.rs @@ -494,7 +494,7 @@ impl<R: RngCore + CryptoRng> DkgController<R> { // submitted proposals and find the one with our address self.get_validation_proposals() .await? - .get(self.dkg_client.get_address().await.as_ref()) + .get(self.dkg_client.get_address().await?.as_ref()) .copied() .ok_or(KeyDerivationError::UnrecoverableProposalId) } diff --git a/nym-api/src/ecash/dkg/key_validation.rs b/nym-api/src/ecash/dkg/key_validation.rs index aff0ea90acf..cde57df982e 100644 --- a/nym-api/src/ecash/dkg/key_validation.rs +++ b/nym-api/src/ecash/dkg/key_validation.rs @@ -155,7 +155,7 @@ impl<R: RngCore + CryptoRng> DkgController<R> { }; // if this is our share, obviously vote for yes without spending time on verification - if owner.as_ref() == self.dkg_client.get_address().await.as_ref() { + if owner.as_ref() == self.dkg_client.get_address().await?.as_ref() { votes.insert(*proposal_id, true); continue; } @@ -313,7 +313,7 @@ mod tests { exchange_dealings(&mut controllers, false).await; derive_keypairs(&mut controllers, false).await; - let first_dealer = controllers[0].dkg_client.get_address().await; + let first_dealer = controllers[0].dkg_client.get_address().await?; { let mut guard = chain.lock().unwrap(); @@ -365,8 +365,8 @@ mod tests { exchange_dealings(&mut controllers, false).await; derive_keypairs(&mut controllers, false).await; - let first_dealer = controllers[0].dkg_client.get_address().await; - let second_dealer = controllers[1].dkg_client.get_address().await; + let first_dealer = controllers[0].dkg_client.get_address().await?; + let second_dealer = controllers[1].dkg_client.get_address().await?; { let mut guard = chain.lock().unwrap(); diff --git a/nym-api/src/ecash/error.rs b/nym-api/src/ecash/error.rs index 7696234ea2e..68fd79622fc 100644 --- a/nym-api/src/ecash/error.rs +++ b/nym-api/src/ecash/error.rs @@ -25,6 +25,9 @@ pub enum EcashError { #[error(transparent)] IOError(#[from] std::io::Error), + #[error("this instance is running without on-chain signing capabilities so no transactions can be sent")] + ChainSignerNotEnabled, + #[error("this operation couldn't be completed as this nym-api is not an active ecash signer")] NotASigner, diff --git a/nym-api/src/ecash/state/mod.rs b/nym-api/src/ecash/state/mod.rs index b27280b6a61..3e716d08f40 100644 --- a/nym-api/src/ecash/state/mod.rs +++ b/nym-api/src/ecash/state/mod.rs @@ -139,7 +139,9 @@ impl EcashState { .local .active_signer .get_or_init(epoch_id, || async { - let address = self.aux.client.address().await; + let Ok(address) = self.aux.client.address().await else { + return Ok(false); + }; let ecash_signers = self.aux.comm_channel.ecash_clients(epoch_id).await?; // check if any ecash signers for this epoch has the same cosmos address as this api @@ -246,7 +248,7 @@ impl EcashState { let threshold = self.aux.comm_channel.ecash_threshold(epoch_id).await?; // let mut shares = Mutex::new(Vec::with_capacity(all_apis.len())); - let cosmos_address = self.aux.client.address().await; + let cosmos_address = self.aux.client.address().await.ok(); let get_partial_signatures = |api: EcashApiClient| async { // move the api into the closure @@ -256,7 +258,7 @@ impl EcashState { // check if we're attempting to query ourselves, in that case just get local signature // rather than making the http query - let partial = if api.cosmos_address == cosmos_address { + let partial = if Some(api.cosmos_address) == cosmos_address { self.partial_coin_index_signatures(Some(epoch_id)) .await? .signatures @@ -380,7 +382,7 @@ impl EcashState { let all_apis = self.aux.comm_channel.ecash_clients(epoch_id).await?; let threshold = self.aux.comm_channel.ecash_threshold(epoch_id).await?; - let cosmos_address = self.aux.client.address().await; + let cosmos_address = self.aux.client.address().await.ok(); let get_partial_signatures = |api: EcashApiClient| async { // move the api into the closure @@ -390,7 +392,7 @@ impl EcashState { // check if we're attempting to query ourselves, in that case just get local signature // rather than making the http query - let partial = if api.cosmos_address == cosmos_address { + let partial = if Some(api.cosmos_address) == cosmos_address { self.partial_expiration_date_signatures(expiration_date) .await? .signatures @@ -888,7 +890,7 @@ impl EcashState { pub async fn get_redeemable_tickets( &self, - provider_info: TicketProvider, + provider_info: &TicketProvider, ) -> Result<Vec<SerialNumberWrapper>> { let since = provider_info .last_batch_verification @@ -901,6 +903,14 @@ impl EcashState { .map_err(Into::into) } + pub async fn update_last_batch_verification(&self, provider: &TicketProvider) -> Result<()> { + Ok(self + .aux + .storage + .update_last_batch_verification(provider.id, OffsetDateTime::now_utc()) + .await?) + } + pub async fn get_ticket_data_by_serial_number( &self, serial_number: &[u8], diff --git a/nym-api/src/ecash/storage/manager.rs b/nym-api/src/ecash/storage/manager.rs index f3b227b5b00..7e49215c704 100644 --- a/nym-api/src/ecash/storage/manager.rs +++ b/nym-api/src/ecash/storage/manager.rs @@ -82,6 +82,11 @@ pub trait EcashStorageManagerExt { provider_id: i64, since: OffsetDateTime, ) -> Result<Vec<SerialNumberWrapper>, sqlx::Error>; + async fn update_last_batch_verification( + &self, + provider_id: i64, + last_batch_verification: OffsetDateTime, + ) -> Result<(), sqlx::Error>; async fn get_spent_tickets_on( &self, @@ -215,15 +220,15 @@ impl EcashStorageManagerExt for StorageManager { "#, expiration_date ) - .fetch_all(&self.connection_pool) - .await? - .into_iter() - .filter_map(|r| r.merkle_leaf.try_into().inspect_err(|_| error!("possible database corruption: one of the stored merkle leaves is not a valid 32byte hash")).ok().map(|merkle_leaf| IssuedHash { - deposit_id: r.deposit_id, - merkle_leaf, - merkle_index: r.merkle_index as usize, - })) - .collect()) + .fetch_all(&self.connection_pool) + .await? + .into_iter() + .filter_map(|r| r.merkle_leaf.try_into().inspect_err(|_| error!("possible database corruption: one of the stored merkle leaves is not a valid 32byte hash")).ok().map(|merkle_leaf| IssuedHash { + deposit_id: r.deposit_id, + merkle_leaf, + merkle_index: r.merkle_index as usize, + })) + .collect()) } /// Store the provided issued credential information. @@ -344,8 +349,8 @@ impl EcashStorageManagerExt for StorageManager { verified_at, provider_id ) - .execute(&self.connection_pool) - .await?; + .execute(&self.connection_pool) + .await?; Ok(()) } @@ -382,6 +387,25 @@ impl EcashStorageManagerExt for StorageManager { .await } + async fn update_last_batch_verification( + &self, + provider_id: i64, + last_batch_verification: OffsetDateTime, + ) -> Result<(), sqlx::Error> { + sqlx::query!( + r#" + UPDATE ticket_providers + SET last_batch_verification = ? + WHERE id = ? + "#, + last_batch_verification, + provider_id + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } + async fn get_spent_tickets_on( &self, date: Date, @@ -510,8 +534,8 @@ impl EcashStorageManagerExt for StorageManager { epoch_id, data ) - .execute(&self.connection_pool) - .await?; + .execute(&self.connection_pool) + .await?; Ok(()) } @@ -544,8 +568,8 @@ impl EcashStorageManagerExt for StorageManager { epoch_id, data ) - .execute(&self.connection_pool) - .await?; + .execute(&self.connection_pool) + .await?; Ok(()) } diff --git a/nym-api/src/ecash/storage/mod.rs b/nym-api/src/ecash/storage/mod.rs index 019ab43a721..2d0d802f064 100644 --- a/nym-api/src/ecash/storage/mod.rs +++ b/nym-api/src/ecash/storage/mod.rs @@ -112,6 +112,11 @@ pub trait EcashStorageExt { provider_id: i64, since: OffsetDateTime, ) -> Result<Vec<SerialNumberWrapper>, NymApiStorageError>; + async fn update_last_batch_verification( + &self, + provider_id: i64, + last_batch_verification: OffsetDateTime, + ) -> Result<(), NymApiStorageError>; async fn get_all_spent_tickets_on( &self, @@ -395,6 +400,17 @@ impl EcashStorageExt for NymApiStorage { .map_err(Into::into) } + async fn update_last_batch_verification( + &self, + provider_id: i64, + last_batch_verification: OffsetDateTime, + ) -> Result<(), NymApiStorageError> { + Ok(self + .manager + .update_last_batch_verification(provider_id, last_batch_verification) + .await?) + } + async fn get_all_spent_tickets_on( &self, date: Date, diff --git a/nym-api/src/ecash/tests/fixtures.rs b/nym-api/src/ecash/tests/fixtures.rs index 80ee7917709..b49b9fc17ee 100644 --- a/nym-api/src/ecash/tests/fixtures.rs +++ b/nym-api/src/ecash/tests/fixtures.rs @@ -263,7 +263,7 @@ pub(crate) struct TestingDkgController { impl TestingDkgController { pub async fn address(&self) -> AccountId { - self.dkg_client.get_address().await + self.dkg_client.get_address().await.unwrap() } pub async fn cw_address(&self) -> Addr { diff --git a/nym-api/src/ecash/tests/helpers.rs b/nym-api/src/ecash/tests/helpers.rs index 5cd410056f0..458b57946e0 100644 --- a/nym-api/src/ecash/tests/helpers.rs +++ b/nym-api/src/ecash/tests/helpers.rs @@ -51,7 +51,7 @@ pub(crate) async fn initialise_dkg(controllers: &mut [TestingDkgController], res // add every dealer to group contract for controller in controllers.iter() { - let address = controller.dkg_client.get_address().await; + let address = controller.dkg_client.get_address().await.unwrap(); let mut chain = controllers[0].chain_state.lock().unwrap(); chain.add_member(address.as_ref(), 10); } @@ -76,7 +76,7 @@ pub(crate) async fn submit_public_keys(controllers: &mut [TestingDkgController], .unwrap(); } - let threshold = (2 * controllers.len() as u64 + 3 - 1) / 3; + let threshold = (2 * controllers.len() as u64).div_ceil(3); let mut guard = controllers[0].chain_state.lock().unwrap(); guard.dkg_contract.epoch.state = EpochState::DealingExchange { resharing }; diff --git a/nym-api/src/ecash/tests/mod.rs b/nym-api/src/ecash/tests/mod.rs index 2b0ef471f90..9af4c780527 100644 --- a/nym-api/src/ecash/tests/mod.rs +++ b/nym-api/src/ecash/tests/mod.rs @@ -11,6 +11,7 @@ use crate::node_describe_cache::DescribedNodes; use crate::node_status_api::handlers::unstable; use crate::node_status_api::NodeStatusCache; use crate::nym_contract_cache::cache::NymContractCache; +use crate::status::ApiStatusState; use crate::support::caching::cache::SharedCache; use crate::support::config; use crate::support::http::state::{AppState, ForcedRefresh}; @@ -524,8 +525,8 @@ impl DummyClient { #[async_trait] impl super::client::Client for DummyClient { - async fn address(&self) -> AccountId { - self.validator_address.clone() + async fn address(&self) -> Result<AccountId> { + Ok(self.validator_address.clone()) } async fn dkg_contract_address(&self) -> Result<AccountId> { @@ -1262,7 +1263,7 @@ struct TestFixture { } impl TestFixture { - fn build_app_state(storage: NymApiStorage) -> AppState { + fn build_app_state(storage: NymApiStorage, ecash_state: EcashState) -> AppState { AppState { forced_refresh: ForcedRefresh::new(true), nym_contract_cache: NymContractCache::new(), @@ -1275,6 +1276,8 @@ impl TestFixture { NymNetworkDetails::new_empty(), ), node_info_cache: unstable::NodeInfoCache::default(), + api_status: ApiStatusState::new(None), + ecash_state: Arc::new(ecash_state), } } @@ -1337,8 +1340,8 @@ impl TestFixture { TestFixture { axum: TestServer::new( Router::new() - .nest("/v1/ecash", ecash_routes(Arc::new(ecash_state))) - .with_state(Self::build_app_state(storage.clone())), + .nest("/v1/ecash", ecash_routes()) + .with_state(Self::build_app_state(storage.clone(), ecash_state)), ) .unwrap(), storage, diff --git a/nym-api/src/epoch_operations/error.rs b/nym-api/src/epoch_operations/error.rs index 65fc822dff0..a608817425c 100644 --- a/nym-api/src/epoch_operations/error.rs +++ b/nym-api/src/epoch_operations/error.rs @@ -11,6 +11,9 @@ use thiserror::Error; #[derive(Debug, Error)] pub enum RewardingError { + #[error("this instance is running without on-chain signing capabilities so no transactions can be sent")] + ChainSignerNotEnabled, + #[error("Our account ({our_address}) is not permitted to update rewarded set and perform rewarding. The allowed address is {allowed_address}")] Unauthorised { our_address: AccountId, diff --git a/nym-api/src/epoch_operations/mod.rs b/nym-api/src/epoch_operations/mod.rs index 6744679411f..0a1f8bb566b 100644 --- a/nym-api/src/epoch_operations/mod.rs +++ b/nym-api/src/epoch_operations/mod.rs @@ -120,23 +120,21 @@ impl EpochAdvancer { let epoch_end = interval.current_epoch_end(); - let legacy_mixnodes = self.nym_contract_cache.legacy_mixnodes_filtered().await; - let legacy_gateways = self.nym_contract_cache.legacy_gateways_filtered().await; - - // TODO: for the purposes of rewarding, this might have to grab some pre-filtered nodes instead, - // such as ones that use up to date version or have correct 'peanut' score let nym_nodes = self.nym_contract_cache.nym_nodes().await; - if legacy_mixnodes.is_empty() && legacy_gateways.is_empty() && nym_nodes.is_empty() { + if nym_nodes.is_empty() { // that's a bit weird, but ok warn!("there don't seem to be any nodes on the network!") } let epoch_status = self.nyxd_client.get_current_epoch_status().await?; if !epoch_status.is_in_progress() { - if epoch_status.being_advanced_by.as_str() - != self.nyxd_client.client_address().await.as_ref() - { + // SAFETY: before `EpochAdvancer` is started, `ensure_rewarding_permission` is called + // which is not allowed to progress if this instance is not using a signing client + #[allow(clippy::unwrap_used)] + let address = self.nyxd_client.client_address().await.unwrap(); + + if epoch_status.being_advanced_by.as_str() != address.as_ref() { // another nym-api is already handling error!("another nym-api ({}) is already advancing the epoch... but we shouldn't have other nym-apis yet!", epoch_status.being_advanced_by); return Ok(()); @@ -157,7 +155,7 @@ impl EpochAdvancer { // note: those operations don't really have to be atomic, so it's fine to send them // as separate transactions self.reconcile_epoch_events().await?; - self.update_rewarded_set_and_advance_epoch(&legacy_mixnodes, &legacy_gateways, &nym_nodes) + self.update_rewarded_set_and_advance_epoch(&nym_nodes) .await?; info!("Purging old node statuses from the storage..."); @@ -318,8 +316,10 @@ impl EpochAdvancer { pub(crate) async fn ensure_rewarding_permission( nyxd_client: &Client, ) -> Result<(), RewardingError> { + let Some(our_address) = nyxd_client.client_address().await else { + return Err(RewardingError::ChainSignerNotEnabled); + }; let allowed_address = nyxd_client.get_rewarding_validator_address().await?; - let our_address = nyxd_client.client_address().await; if allowed_address != our_address { Err(RewardingError::Unauthorised { our_address, diff --git a/nym-api/src/epoch_operations/rewarded_set_assignment.rs b/nym-api/src/epoch_operations/rewarded_set_assignment.rs index 58a62dc27c0..c2fda62dd61 100644 --- a/nym-api/src/epoch_operations/rewarded_set_assignment.rs +++ b/nym-api/src/epoch_operations/rewarded_set_assignment.rs @@ -5,8 +5,6 @@ use crate::epoch_operations::error::RewardingError; use crate::epoch_operations::helpers::stake_to_f64; use crate::EpochAdvancer; use cosmwasm_std::Decimal; -use nym_api_requests::legacy::{LegacyGatewayBondWithId, LegacyMixNodeDetailsWithLayer}; -use nym_mixnet_contract_common::helpers::IntoBaseDecimal; use nym_mixnet_contract_common::reward_params::{Performance, RewardedSetParams}; use nym_mixnet_contract_common::{EpochState, NodeId, NymNodeDetails, RewardedSet}; use rand::prelude::SliceRandom; @@ -204,8 +202,6 @@ impl EpochAdvancer { async fn attach_performance_to_eligible_nodes( &self, - legacy_mixnodes: &[LegacyMixNodeDetailsWithLayer], - legacy_gateways: &[LegacyGatewayBondWithId], nym_nodes: &[NymNodeDetails], ) -> Vec<NodeWithStakeAndPerformance> { let mut with_performance = Vec::new(); @@ -218,62 +214,6 @@ impl EpochAdvancer { return Vec::new(); }; - for mix in legacy_mixnodes { - let node_id = mix.mix_id(); - let total_stake = mix.total_stake(); - - let Some(annotation) = status_cache.get(&node_id) else { - debug!("couldn't find annotation for legacy mixnode {node_id}"); - continue; - }; - - if mix.bond_information.proxy.is_some() { - debug!("legacy mixnode {node_id} is using vested tokens"); - continue; - } - - let performance = annotation.detailed_performance.to_rewarding_performance(); - debug!( - "legacy mixnode {}: stake: {total_stake}, performance: {performance}", - mix.mix_id() - ); - - with_performance.push(NodeWithStakeAndPerformance { - node_id: mix.mix_id(), - available_roles: vec![AvailableRole::Mix], - total_stake, - performance, - }) - } - for gateway in legacy_gateways { - let node_id = gateway.node_id; - let total_stake = gateway - .bond - .pledge_amount - .amount - .into_base_decimal() - .unwrap_or_default(); - - let Some(annotation) = status_cache.get(&node_id) else { - debug!("couldn't find annotation for legacy gateway {node_id}"); - continue; - }; - - let performance = annotation.detailed_performance.to_rewarding_performance(); - - debug!( - "legacy gateway {}: stake: {total_stake}, performance: {performance}", - gateway.node_id - ); - - with_performance.push(NodeWithStakeAndPerformance { - node_id: gateway.node_id, - available_roles: vec![AvailableRole::EntryGateway], - total_stake, - performance, - }) - } - for nym_node in nym_nodes { let node_id = nym_node.node_id(); let total_stake = nym_node.total_stake(); @@ -283,7 +223,7 @@ impl EpochAdvancer { }; let Some(annotation) = status_cache.get(&node_id) else { - debug!("couldn't find annotation for nym-node gateway {node_id}"); + debug!("couldn't find annotation for nym-node {node_id}"); continue; }; @@ -319,8 +259,6 @@ impl EpochAdvancer { pub(super) async fn update_rewarded_set_and_advance_epoch( &self, - legacy_mixnodes: &[LegacyMixNodeDetailsWithLayer], - legacy_gateways: &[LegacyGatewayBondWithId], nym_nodes: &[NymNodeDetails], ) -> Result<(), RewardingError> { let epoch_status = self.nyxd_client.get_current_epoch_status().await?; @@ -333,13 +271,8 @@ impl EpochAdvancer { } info!("attempting to assign the rewarded set for the upcoming epoch..."); - let nodes_with_performance = self - .attach_performance_to_eligible_nodes( - legacy_mixnodes, - legacy_gateways, - nym_nodes, - ) - .await; + let nodes_with_performance = + self.attach_performance_to_eligible_nodes(nym_nodes).await; if let Err(err) = self ._update_rewarded_set_and_advance_epoch(nodes_with_performance) diff --git a/nym-api/src/network_monitor/gateways_reader.rs b/nym-api/src/network_monitor/gateways_reader.rs index 01bec7c52a6..bc3eeaa44cb 100644 --- a/nym-api/src/network_monitor/gateways_reader.rs +++ b/nym-api/src/network_monitor/gateways_reader.rs @@ -2,9 +2,8 @@ // SPDX-License-Identifier: GPL-3.0-only use futures::Stream; -use nym_crypto::asymmetric::identity; +use nym_crypto::asymmetric::{ed25519, identity}; use nym_gateway_client::{AcknowledgementReceiver, MixnetMessageReceiver}; -use nym_mixnet_contract_common::IdentityKey; use std::pin::Pin; use std::task::{Context, Poll}; use tokio_stream::StreamMap; @@ -15,8 +14,8 @@ pub(crate) enum GatewayMessages { } pub(crate) struct GatewaysReader { - ack_map: StreamMap<IdentityKey, AcknowledgementReceiver>, - stream_map: StreamMap<IdentityKey, MixnetMessageReceiver>, + ack_map: StreamMap<ed25519::PublicKey, AcknowledgementReceiver>, + stream_map: StreamMap<ed25519::PublicKey, MixnetMessageReceiver>, } impl GatewaysReader { @@ -33,19 +32,18 @@ impl GatewaysReader { message_receiver: MixnetMessageReceiver, ack_receiver: AcknowledgementReceiver, ) { - let channel_id = id.to_string(); - self.stream_map.insert(channel_id.clone(), message_receiver); - self.ack_map.insert(channel_id, ack_receiver); + self.stream_map.insert(id, message_receiver); + self.ack_map.insert(id, ack_receiver); } - pub fn remove_receivers(&mut self, id: &str) { - self.stream_map.remove(id); - self.ack_map.remove(id); + pub fn remove_receivers(&mut self, id: ed25519::PublicKey) { + self.stream_map.remove(&id); + self.ack_map.remove(&id); } } impl Stream for GatewaysReader { - type Item = (IdentityKey, GatewayMessages); + type Item = (ed25519::PublicKey, GatewayMessages); fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> { match Pin::new(&mut self.ack_map).poll_next(cx) { diff --git a/nym-api/src/network_monitor/mod.rs b/nym-api/src/network_monitor/mod.rs index 699498b2872..a7f9391fe28 100644 --- a/nym-api/src/network_monitor/mod.rs +++ b/nym-api/src/network_monitor/mod.rs @@ -12,10 +12,12 @@ use crate::network_monitor::monitor::sender::PacketSender; use crate::network_monitor::monitor::summary_producer::SummaryProducer; use crate::network_monitor::monitor::Monitor; use crate::node_describe_cache::DescribedNodes; +use crate::node_status_api::NodeStatusCache; use crate::nym_contract_cache::cache::NymContractCache; use crate::storage::NymApiStorage; use crate::support::caching::cache::SharedCache; -use crate::support::{config, nyxd}; +use crate::support::config::Config; +use crate::support::nyxd; use futures::channel::mpsc; use nym_bandwidth_controller::BandwidthController; use nym_credential_storage::persistent_storage::PersistentStorage; @@ -35,9 +37,10 @@ pub(crate) mod test_route; pub(crate) const ROUTE_TESTING_TEST_NONCE: u64 = 0; pub(crate) fn setup<'a>( - config: &'a config::NetworkMonitor, + config: &'a Config, nym_contract_cache: &NymContractCache, described_cache: SharedCache<DescribedNodes>, + node_status_cache: NodeStatusCache, storage: &NymApiStorage, nyxd_client: nyxd::Client, ) -> NetworkMonitorBuilder<'a> { @@ -47,24 +50,27 @@ pub(crate) fn setup<'a>( storage.to_owned(), nym_contract_cache.clone(), described_cache, + node_status_cache, ) } pub(crate) struct NetworkMonitorBuilder<'a> { - config: &'a config::NetworkMonitor, + config: &'a Config, nyxd_client: nyxd::Client, node_status_storage: NymApiStorage, contract_cache: NymContractCache, described_cache: SharedCache<DescribedNodes>, + node_status_cache: NodeStatusCache, } impl<'a> NetworkMonitorBuilder<'a> { pub(crate) fn new( - config: &'a config::NetworkMonitor, + config: &'a Config, nyxd_client: nyxd::Client, node_status_storage: NymApiStorage, contract_cache: NymContractCache, described_cache: SharedCache<DescribedNodes>, + node_status_cache: NodeStatusCache, ) -> Self { NetworkMonitorBuilder { config, @@ -72,10 +78,11 @@ impl<'a> NetworkMonitorBuilder<'a> { node_status_storage, contract_cache, described_cache, + node_status_cache, } } - pub(crate) async fn build<R: MessageReceiver + Send + 'static>( + pub(crate) async fn build<R: MessageReceiver + Send + Sync + 'static>( self, ) -> NetworkMonitorRunnables<R> { // TODO: those keys change constant throughout the whole execution of the monitor. @@ -94,7 +101,8 @@ impl<'a> NetworkMonitorBuilder<'a> { let packet_preparer = new_packet_preparer( self.contract_cache, self.described_cache, - self.config.debug.per_node_test_packets, + self.node_status_cache, + self.config.network_monitor.debug.per_node_test_packets, Arc::clone(&ack_key), *identity_keypair.public_key(), *encryption_keypair.public_key(), @@ -103,7 +111,11 @@ impl<'a> NetworkMonitorBuilder<'a> { let bandwidth_controller = { BandwidthController::new( nym_credential_storage::initialise_persistent_storage( - &self.config.storage_paths.credentials_database_path, + &self + .config + .network_monitor + .storage_paths + .credentials_database_path, ) .await, self.nyxd_client.clone(), @@ -114,9 +126,7 @@ impl<'a> NetworkMonitorBuilder<'a> { self.config, gateway_status_update_sender, Arc::clone(&identity_keypair), - self.config.debug.gateway_sending_rate, bandwidth_controller, - self.config.debug.disabled_credentials_mode, ); let received_processor = new_received_processor( @@ -124,14 +134,15 @@ impl<'a> NetworkMonitorBuilder<'a> { Arc::clone(&encryption_keypair), ack_key, ); - let summary_producer = new_summary_producer(self.config.debug.per_node_test_packets); + let summary_producer = + new_summary_producer(self.config.network_monitor.debug.per_node_test_packets); let packet_receiver = new_packet_receiver( gateway_status_update_receiver, received_processor_sender_channel, ); let monitor = Monitor::new( - self.config, + &self.config.network_monitor, packet_preparer, packet_sender, received_processor, @@ -147,12 +158,12 @@ impl<'a> NetworkMonitorBuilder<'a> { } } -pub(crate) struct NetworkMonitorRunnables<R: MessageReceiver + Send + 'static> { +pub(crate) struct NetworkMonitorRunnables<R: MessageReceiver + Send + Sync + 'static> { monitor: Monitor<R>, packet_receiver: PacketReceiver, } -impl<R: MessageReceiver + Send + 'static> NetworkMonitorRunnables<R> { +impl<R: MessageReceiver + Send + Sync + 'static> NetworkMonitorRunnables<R> { // TODO: note, that is not exactly doing what we want, because when // `ReceivedProcessor` is constructed, it already spawns a future // this needs to be refactored! @@ -169,6 +180,7 @@ impl<R: MessageReceiver + Send + 'static> NetworkMonitorRunnables<R> { fn new_packet_preparer( contract_cache: NymContractCache, described_cache: SharedCache<DescribedNodes>, + node_status_cache: NodeStatusCache, per_node_test_packets: usize, ack_key: Arc<AckKey>, self_public_identity: identity::PublicKey, @@ -177,6 +189,7 @@ fn new_packet_preparer( PacketPreparer::new( contract_cache, described_cache, + node_status_cache, per_node_test_packets, ack_key, self_public_identity, @@ -185,22 +198,16 @@ fn new_packet_preparer( } fn new_packet_sender( - config: &config::NetworkMonitor, + config: &Config, gateways_status_updater: GatewayClientUpdateSender, local_identity: Arc<identity::KeyPair>, - max_sending_rate: usize, bandwidth_controller: BandwidthController<nyxd::Client, PersistentStorage>, - disabled_credentials_mode: bool, ) -> PacketSender { PacketSender::new( + config, gateways_status_updater, local_identity, - config.debug.gateway_response_timeout, - config.debug.gateway_connection_timeout, - config.debug.max_concurrent_gateway_clients, - max_sending_rate, bandwidth_controller, - disabled_credentials_mode, ) } @@ -227,10 +234,11 @@ fn new_packet_receiver( // TODO: 1) does it still have to have separate builder or could we get rid of it now? // TODO: 2) how do we make it non-async as other 'start' methods? -pub(crate) async fn start<R: MessageReceiver + Send + 'static>( - config: &config::NetworkMonitor, +pub(crate) async fn start<R: MessageReceiver + Send + Sync + 'static>( + config: &Config, nym_contract_cache: &NymContractCache, described_cache: SharedCache<DescribedNodes>, + node_status_cache: NodeStatusCache, storage: &NymApiStorage, nyxd_client: nyxd::Client, shutdown: &TaskManager, @@ -239,6 +247,7 @@ pub(crate) async fn start<R: MessageReceiver + Send + 'static>( config, nym_contract_cache, described_cache, + node_status_cache, storage, nyxd_client, ); diff --git a/nym-api/src/network_monitor/monitor/gateway_client_handle.rs b/nym-api/src/network_monitor/monitor/gateway_client_handle.rs new file mode 100644 index 00000000000..6e3f157efb9 --- /dev/null +++ b/nym-api/src/network_monitor/monitor/gateway_client_handle.rs @@ -0,0 +1,54 @@ +// Copyright 2021 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: GPL-3.0-only + +use crate::network_monitor::monitor::receiver::{GatewayClientUpdate, GatewayClientUpdateSender}; +use crate::support::nyxd; +use nym_credential_storage::persistent_storage::PersistentStorage; +use nym_gateway_client::GatewayClient; +use std::ops::{Deref, DerefMut}; +use tracing::warn; + +pub(crate) struct GatewayClientHandle { + client: GatewayClient<nyxd::Client, PersistentStorage>, + gateways_status_updater: GatewayClientUpdateSender, +} + +impl GatewayClientHandle { + pub(crate) fn new( + client: GatewayClient<nyxd::Client, PersistentStorage>, + gateways_status_updater: GatewayClientUpdateSender, + ) -> Self { + GatewayClientHandle { + client, + gateways_status_updater, + } + } +} + +impl Drop for GatewayClientHandle { + fn drop(&mut self) { + if self + .gateways_status_updater + .unbounded_send(GatewayClientUpdate::Disconnect( + self.client.gateway_identity(), + )) + .is_err() + { + warn!("fail to cleanly shutdown gateway connection") + } + } +} + +impl Deref for GatewayClientHandle { + type Target = GatewayClient<nyxd::Client, PersistentStorage>; + + fn deref(&self) -> &Self::Target { + &self.client + } +} + +impl DerefMut for GatewayClientHandle { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.client + } +} diff --git a/nym-api/src/network_monitor/monitor/gateway_clients_cache.rs b/nym-api/src/network_monitor/monitor/gateway_clients_cache.rs deleted file mode 100644 index 4a321d57cd7..00000000000 --- a/nym-api/src/network_monitor/monitor/gateway_clients_cache.rs +++ /dev/null @@ -1,102 +0,0 @@ -// Copyright 2021 - Nym Technologies SA <contact@nymtech.net> -// SPDX-License-Identifier: GPL-3.0-only - -use crate::support::nyxd; -use nym_credential_storage::persistent_storage::PersistentStorage; -use nym_crypto::asymmetric::identity::PUBLIC_KEY_LENGTH; -use nym_gateway_client::GatewayClient; -use std::collections::HashMap; -use std::sync::Arc; -use tokio::sync::{Mutex, MutexGuard, TryLockError}; - -pub(crate) struct GatewayClientHandle(Arc<GatewayClientHandleInner>); - -struct GatewayClientHandleInner { - client: Mutex<Option<GatewayClient<nyxd::Client, PersistentStorage>>>, - raw_identity: [u8; PUBLIC_KEY_LENGTH], -} - -pub(crate) struct UnlockedGatewayClientHandle<'a>( - MutexGuard<'a, Option<GatewayClient<nyxd::Client, PersistentStorage>>>, -); - -impl GatewayClientHandle { - pub(crate) fn new(gateway_client: GatewayClient<nyxd::Client, PersistentStorage>) -> Self { - GatewayClientHandle(Arc::new(GatewayClientHandleInner { - raw_identity: gateway_client.gateway_identity().to_bytes(), - client: Mutex::new(Some(gateway_client)), - })) - } - - pub(crate) fn ptr_eq(&self, other: &Self) -> bool { - Arc::ptr_eq(&self.0, &other.0) - } - - // this could have also been achieved with a normal #[derive(Clone)] but I prefer to be explicit about it, - // because clippy would suggest some potentially confusing 'simplifications' regarding clone - pub(crate) fn clone_data_pointer(&self) -> Self { - GatewayClientHandle(Arc::clone(&self.0)) - } - - pub(crate) fn raw_identity(&self) -> [u8; PUBLIC_KEY_LENGTH] { - self.0.raw_identity - } - - pub(crate) async fn is_invalid(&self) -> bool { - self.0.client.lock().await.is_none() - } - - pub(crate) async fn lock_client(&self) -> UnlockedGatewayClientHandle<'_> { - UnlockedGatewayClientHandle(self.0.client.lock().await) - } - - pub(crate) fn lock_client_unchecked(&self) -> UnlockedGatewayClientHandle<'_> { - UnlockedGatewayClientHandle(self.0.client.try_lock().unwrap()) - } - - pub(crate) fn try_lock_client(&self) -> Result<UnlockedGatewayClientHandle<'_>, TryLockError> { - self.0.client.try_lock().map(UnlockedGatewayClientHandle) - } -} - -impl<'a> UnlockedGatewayClientHandle<'a> { - pub(crate) fn get_mut_unchecked( - &mut self, - ) -> &mut GatewayClient<nyxd::Client, PersistentStorage> { - self.0.as_mut().unwrap() - } - - pub(crate) fn inner_mut( - &mut self, - ) -> Option<&mut GatewayClient<nyxd::Client, PersistentStorage>> { - self.0.as_mut() - } - - pub(crate) fn invalidate(&mut self) { - *self.0 = None - } -} - -pub(crate) type GatewayClientsMap = HashMap<[u8; PUBLIC_KEY_LENGTH], GatewayClientHandle>; - -#[derive(Clone)] -pub(crate) struct ActiveGatewayClients { - // there is no point in using an RwLock here as there will only ever be two readers here and both - // potentially need write access. - // A BiLock would have been slightly better than a normal Mutex since it's optimised for two - // owners, but it's behind `unstable` feature flag in futures and it would be a headache if the API - // changed. - inner: Arc<Mutex<GatewayClientsMap>>, -} - -impl ActiveGatewayClients { - pub(crate) fn new() -> Self { - ActiveGatewayClients { - inner: Arc::new(Mutex::new(HashMap::new())), - } - } - - pub(crate) async fn lock(&self) -> MutexGuard<'_, GatewayClientsMap> { - self.inner.lock().await - } -} diff --git a/nym-api/src/network_monitor/monitor/gateways_pinger.rs b/nym-api/src/network_monitor/monitor/gateways_pinger.rs deleted file mode 100644 index ed09c2d0906..00000000000 --- a/nym-api/src/network_monitor/monitor/gateways_pinger.rs +++ /dev/null @@ -1,165 +0,0 @@ -// Copyright 2021 - Nym Technologies SA <contact@nymtech.net> -// SPDX-License-Identifier: GPL-3.0-only - -use crate::network_monitor::monitor::gateway_clients_cache::ActiveGatewayClients; -use crate::network_monitor::monitor::receiver::{GatewayClientUpdate, GatewayClientUpdateSender}; -use nym_crypto::asymmetric::identity; -use nym_crypto::asymmetric::identity::PUBLIC_KEY_LENGTH; -use nym_task::TaskClient; -use std::time::Duration; -use tokio::time::{sleep, Instant}; -use tracing::{debug, info, trace, warn}; - -// TODO: should it perhaps be moved to config along other timeout values? -const PING_TIMEOUT: Duration = Duration::from_secs(3); - -pub(crate) struct GatewayPinger { - gateway_clients: ActiveGatewayClients, - gateways_status_updater: GatewayClientUpdateSender, - pinging_interval: Duration, -} - -impl GatewayPinger { - pub(crate) fn new( - gateway_clients: ActiveGatewayClients, - gateways_status_updater: GatewayClientUpdateSender, - pinging_interval: Duration, - ) -> Self { - GatewayPinger { - gateway_clients, - gateways_status_updater, - pinging_interval, - } - } - - fn notify_connection_failure(&self, raw_gateway_id: [u8; PUBLIC_KEY_LENGTH]) { - // if this unwrap failed it means something extremely weird is going on - // and we got some solar flare bitflip type of corruption - let gateway_key = identity::PublicKey::from_bytes(&raw_gateway_id) - .expect("failed to recover gateways public key from valid bytes"); - - // remove the gateway listener channels - self.gateways_status_updater - .unbounded_send(GatewayClientUpdate::Failure(gateway_key)) - .expect("packet receiver seems to have died!"); - } - - async fn ping_and_cleanup_all_gateways(&self) { - info!("Pinging all active gateways"); - - let lock_acquire_start = Instant::now(); - let active_gateway_clients_guard = self.gateway_clients.lock().await; - trace!( - "Acquiring lock took {:?}", - Instant::now().duration_since(lock_acquire_start) - ); - - if active_gateway_clients_guard.is_empty() { - debug!("no gateways to ping"); - return; - } - - // don't keep the guard the entire time - clone all Arcs and drop it - // - // this clippy warning is a false positive as we cannot get rid of the collect by moving - // everything into a single iterator as it would require us to hold the lock the entire time - // and that is exactly what we want to avoid - #[allow(clippy::needless_collect)] - let active_gateway_clients = active_gateway_clients_guard - .iter() - .map(|(_, handle)| handle.clone_data_pointer()) - .collect::<Vec<_>>(); - drop(active_gateway_clients_guard); - - let ping_start = Instant::now(); - - let mut clients_to_purge = Vec::new(); - - // since we don't need to wait for response, we can just ping all gateways sequentially - // if it becomes problem later on, we can adjust it. - for client_handle in active_gateway_clients.into_iter() { - trace!( - "Pinging: {}", - identity::PublicKey::from_bytes(&client_handle.raw_identity()) - .unwrap() - .to_base58_string() - ); - // if we fail to obtain the lock it means the client is being currently used to send messages - // and hence we don't need to ping it to keep connection alive - if let Ok(mut unlocked_handle) = client_handle.try_lock_client() { - if let Some(active_client) = unlocked_handle.inner_mut() { - match tokio::time::timeout(PING_TIMEOUT, active_client.send_ping_message()) - .await - { - Err(_timeout) => { - warn!( - "we timed out trying to ping {} - assuming the connection is dead.", - active_client.gateway_identity().to_base58_string(), - ); - clients_to_purge.push(client_handle.raw_identity()); - } - Ok(Err(err)) => { - warn!( - "failed to send ping message to gateway {} - {} - assuming the connection is dead.", - active_client.gateway_identity().to_base58_string(), - err, - ); - clients_to_purge.push(client_handle.raw_identity()); - } - _ => {} - } - } else { - clients_to_purge.push(client_handle.raw_identity()); - } - } - } - - info!( - "Purging {} gateways, acquiring lock", - clients_to_purge.len() - ); - // purge all dead connections - // reacquire the guard - let lock_acquire_start = Instant::now(); - let mut active_gateway_clients_guard = self.gateway_clients.lock().await; - info!( - "Acquiring lock took {:?}", - Instant::now().duration_since(lock_acquire_start) - ); - - for gateway_id in clients_to_purge.into_iter() { - if let Some(removed_handle) = active_gateway_clients_guard.remove(&gateway_id) { - if !removed_handle.is_invalid().await { - info!("Handle is invalid, purging"); - // it was not invalidated by the packet sender meaning it probably was some unbonded node - // that was never cleared - self.notify_connection_failure(gateway_id); - } - info!("Handle is not invalid, not purged") - } - } - - let ping_end = Instant::now(); - let time_taken = ping_end.duration_since(ping_start); - info!("Pinging all active gateways took {:?}", time_taken); - } - - pub(crate) async fn run(&self, mut shutdown: TaskClient) { - while !shutdown.is_shutdown() { - tokio::select! { - _ = sleep(self.pinging_interval) => { - tokio::select! { - biased; - _ = shutdown.recv() => { - trace!("GatewaysPinger: Received shutdown"); - } - _ = self.ping_and_cleanup_all_gateways() => (), - } - } - _ = shutdown.recv() => { - trace!("GatewaysPinger: Received shutdown"); - } - } - } - } -} diff --git a/nym-api/src/network_monitor/monitor/mod.rs b/nym-api/src/network_monitor/monitor/mod.rs index 4d24cb0504d..1f659b86461 100644 --- a/nym-api/src/network_monitor/monitor/mod.rs +++ b/nym-api/src/network_monitor/monitor/mod.rs @@ -4,7 +4,7 @@ use crate::network_monitor::monitor::preparer::PacketPreparer; use crate::network_monitor::monitor::processor::ReceivedProcessor; use crate::network_monitor::monitor::sender::PacketSender; -use crate::network_monitor::monitor::summary_producer::{SummaryProducer, TestSummary}; +use crate::network_monitor::monitor::summary_producer::{SummaryProducer, TestReport, TestSummary}; use crate::network_monitor::test_packet::NodeTestMessage; use crate::network_monitor::test_route::TestRoute; use crate::storage::NymApiStorage; @@ -17,15 +17,14 @@ use std::collections::{HashMap, HashSet}; use tokio::time::{sleep, Duration, Instant}; use tracing::{debug, error, info, trace}; -pub(crate) mod gateway_clients_cache; -pub(crate) mod gateways_pinger; +pub(crate) mod gateway_client_handle; pub(crate) mod preparer; pub(crate) mod processor; pub(crate) mod receiver; pub(crate) mod sender; pub(crate) mod summary_producer; -pub(super) struct Monitor<R: MessageReceiver + Send + 'static> { +pub(super) struct Monitor<R: MessageReceiver + Send + Sync + 'static> { test_nonce: u64, packet_preparer: PacketPreparer, packet_sender: PacketSender, @@ -33,7 +32,6 @@ pub(super) struct Monitor<R: MessageReceiver + Send + 'static> { summary_producer: SummaryProducer, node_status_storage: NymApiStorage, run_interval: Duration, - gateway_ping_interval: Duration, packet_delivery_timeout: Duration, /// Number of test packets sent via each "random" route to verify whether they work correctly. @@ -49,7 +47,7 @@ pub(super) struct Monitor<R: MessageReceiver + Send + 'static> { packet_type: PacketType, } -impl<R: MessageReceiver + Send> Monitor<R> { +impl<R: MessageReceiver + Send + Sync> Monitor<R> { pub(super) fn new( config: &config::NetworkMonitor, packet_preparer: PacketPreparer, @@ -67,7 +65,6 @@ impl<R: MessageReceiver + Send> Monitor<R> { summary_producer, node_status_storage, run_interval: config.debug.run_interval, - gateway_ping_interval: config.debug.gateway_ping_interval, packet_delivery_timeout: config.debug.packet_delivery_timeout, route_test_packets: config.debug.route_test_packets, test_routes: config.debug.test_routes, @@ -78,10 +75,10 @@ impl<R: MessageReceiver + Send> Monitor<R> { // while it might have been cleaner to put this into a separate `Notifier` structure, // I don't see much point considering it's only a single, small, method - async fn submit_new_node_statuses(&mut self, test_summary: TestSummary) { + async fn submit_new_node_statuses(&mut self, test_summary: TestSummary, report: TestReport) { // indicate our run has completed successfully and should be used in any future // uptime calculations - if let Err(err) = self + let monitor_run_id = match self .node_status_storage .insert_monitor_run_results( test_summary.mixnode_results, @@ -94,8 +91,22 @@ impl<R: MessageReceiver + Send> Monitor<R> { ) .await { - error!("Failed to submit monitor run information to the database: {err}",); + Ok(id) => id, + Err(err) => { + error!("Failed to submit monitor run information to the database: {err}",); + return; + } + }; + + if let Err(err) = self + .node_status_storage + .insert_monitor_run_report(report, monitor_run_id) + .await + { + error!("failed to submit monitor run report to the database: {err}",); } + + info!("finished persisting monitor run with id {monitor_run_id}"); } fn analyse_received_test_route_packets( @@ -135,12 +146,15 @@ impl<R: MessageReceiver + Send> Monitor<R> { packets.push(gateway_packets); } - self.received_processor.set_route_test_nonce().await; - self.packet_sender.send_packets(packets).await; + self.received_processor.set_route_test_nonce(); + let gateway_clients = self.packet_sender.send_packets(packets).await; // give the packets some time to traverse the network sleep(self.packet_delivery_timeout).await; + // start all the disconnections in the background + drop(gateway_clients); + let received = self.received_processor.return_received().await; let mut results = self.analyse_received_test_route_packets(&received); @@ -197,7 +211,7 @@ impl<R: MessageReceiver + Send> Monitor<R> { // the actual target let candidates = match self .packet_preparer - .prepare_test_routes(remaining * 2, &mut blacklist) + .prepare_test_routes(remaining * 2) .await { Some(candidates) => candidates, @@ -247,12 +261,11 @@ impl<R: MessageReceiver + Send> Monitor<R> { .flat_map(|packets| packets.packets.iter()) .count(); - self.received_processor - .set_new_test_nonce(self.test_nonce) - .await; + self.received_processor.set_new_test_nonce(self.test_nonce); info!("Sending packets to all gateways..."); - self.packet_sender + let gateway_clients = self + .packet_sender .send_packets(prepared_packets.packets) .await; @@ -264,6 +277,9 @@ impl<R: MessageReceiver + Send> Monitor<R> { // give the packets some time to traverse the network sleep(self.packet_delivery_timeout).await; + // start all the disconnections in the background + drop(gateway_clients); + let received = self.received_processor.return_received().await; let total_received = received.len(); info!("Test routes: {:#?}", routes); @@ -279,9 +295,13 @@ impl<R: MessageReceiver + Send> Monitor<R> { ); let report = summary.create_report(total_sent, total_received); - info!("{report}"); - self.submit_new_node_statuses(summary).await; + let display_report = summary + .create_report(total_sent, total_received) + .to_display_report(&summary.route_results); + info!("{display_report}"); + + self.submit_new_node_statuses(summary, report).await; } async fn test_run(&mut self) { @@ -311,9 +331,6 @@ impl<R: MessageReceiver + Send> Monitor<R> { .wait_for_validator_cache_initial_values(self.minimum_test_routes) .await; - self.packet_sender - .spawn_gateways_pinger(self.gateway_ping_interval, shutdown.clone()); - let mut run_interval = tokio::time::interval(self.run_interval); while !shutdown.is_shutdown() { tokio::select! { diff --git a/nym-api/src/network_monitor/monitor/preparer.rs b/nym-api/src/network_monitor/monitor/preparer.rs index 04fdceebefb..50c3be80baa 100644 --- a/nym-api/src/network_monitor/monitor/preparer.rs +++ b/nym-api/src/network_monitor/monitor/preparer.rs @@ -4,10 +4,12 @@ use crate::network_monitor::monitor::sender::GatewayPackets; use crate::network_monitor::test_route::TestRoute; use crate::node_describe_cache::{DescribedNodes, NodeDescriptionTopologyExt}; +use crate::node_status_api::NodeStatusCache; use crate::nym_contract_cache::cache::{CachedRewardedSet, NymContractCache}; use crate::support::caching::cache::SharedCache; use nym_api_requests::legacy::{LegacyGatewayBondWithId, LegacyMixNodeBondWithLayer}; -use nym_api_requests::models::NymNodeDescription; +use nym_api_requests::models::{NodeAnnotation, NymNodeDescription}; +use nym_contracts_common::NaiveFloat; use nym_crypto::asymmetric::{encryption, identity}; use nym_mixnet_contract_common::{LegacyMixLayer, NodeId}; use nym_node_tester_utils::node::TestableNode; @@ -21,7 +23,7 @@ use nym_topology::mix::MixnodeConversionError; use nym_topology::{gateway, mix}; use rand::prelude::SliceRandom; use rand::{rngs::ThreadRng, thread_rng, Rng}; -use std::collections::{HashMap, HashSet}; +use std::collections::HashMap; use std::fmt::{self, Display, Formatter}; use std::sync::Arc; use std::time::Duration; @@ -77,6 +79,7 @@ pub(crate) struct PreparedPackets { pub(crate) struct PacketPreparer { contract_cache: NymContractCache, described_cache: SharedCache<DescribedNodes>, + node_status_cache: NodeStatusCache, /// Number of test packets sent to each node per_node_test_packets: usize, @@ -94,6 +97,7 @@ impl PacketPreparer { pub(crate) fn new( contract_cache: NymContractCache, described_cache: SharedCache<DescribedNodes>, + node_status_cache: NodeStatusCache, per_node_test_packets: usize, ack_key: Arc<AckKey>, self_public_identity: identity::PublicKey, @@ -102,6 +106,7 @@ impl PacketPreparer { PacketPreparer { contract_cache, described_cache, + node_status_cache, per_node_test_packets, ack_key, self_public_identity, @@ -205,20 +210,6 @@ impl PacketPreparer { (mixnodes, gateways) } - async fn filtered_legacy_mixnodes_and_gateways( - &self, - ) -> ( - Vec<LegacyMixNodeBondWithLayer>, - Vec<LegacyGatewayBondWithId>, - ) { - info!("Obtaining network topology..."); - - let mixnodes = self.contract_cache.legacy_mixnodes_filtered_basic().await; - let gateways = self.contract_cache.legacy_gateways_filtered().await; - - (mixnodes, gateways) - } - pub(crate) fn try_parse_mix_bond( &self, bond: &LegacyMixNodeBondWithLayer, @@ -276,58 +267,50 @@ impl PacketPreparer { parse_bond(gateway).map_err(|_| identity) } - fn layered_mixes<'a, R: Rng>( + fn to_legacy_layered_mixes<'a, R: Rng>( &self, rng: &mut R, - blacklist: &mut HashSet<NodeId>, rewarded_set: &CachedRewardedSet, - legacy_mixnodes: Vec<LegacyMixNodeBondWithLayer>, + node_statuses: &HashMap<NodeId, NodeAnnotation>, mixing_nym_nodes: impl Iterator<Item = &'a NymNodeDescription> + 'a, - ) -> HashMap<LegacyMixLayer, Vec<mix::LegacyNode>> { + ) -> HashMap<LegacyMixLayer, Vec<(mix::LegacyNode, f64)>> { let mut layered_mixes = HashMap::new(); - for mix in legacy_mixnodes { - let layer = mix.layer; - let layer_mixes = layered_mixes.entry(layer).or_insert_with(Vec::new); - let Ok(parsed_node) = self.try_parse_mix_bond(&mix) else { - blacklist.insert(mix.mix_id); - continue; - }; - layer_mixes.push(parsed_node) - } for mixing_nym_node in mixing_nym_nodes { let Some(parsed_node) = self.nym_node_to_legacy_mix(rng, rewarded_set, mixing_nym_node) else { continue; }; + // if the node is not present, default to 0.5 + let weight = node_statuses + .get(&mixing_nym_node.node_id) + .map(|node| node.last_24h_performance.naive_to_f64()) + .unwrap_or(0.5); let layer = parsed_node.layer; let layer_mixes = layered_mixes.entry(layer).or_insert_with(Vec::new); - layer_mixes.push(parsed_node) + layer_mixes.push((parsed_node, weight)) } layered_mixes } - fn all_gateways<'a>( + fn to_legacy_gateway_nodes<'a>( &self, - blacklist: &mut HashSet<NodeId>, - legacy_gateways: Vec<LegacyGatewayBondWithId>, + node_statuses: &HashMap<NodeId, NodeAnnotation>, gateway_capable_nym_nodes: impl Iterator<Item = &'a NymNodeDescription> + 'a, - ) -> Vec<gateway::LegacyNode> { + ) -> Vec<(gateway::LegacyNode, f64)> { let mut gateways = Vec::new(); - for gateway in legacy_gateways { - let Ok(parsed_node) = self.try_parse_gateway_bond(&gateway) else { - blacklist.insert(gateway.node_id); - continue; - }; - gateways.push(parsed_node) - } for gateway_capable_node in gateway_capable_nym_nodes { let Some(parsed_node) = self.nym_node_to_legacy_gateway(gateway_capable_node) else { continue; }; - gateways.push(parsed_node) + // if the node is not present, default to 0.5 + let weight = node_statuses + .get(&gateway_capable_node.node_id) + .map(|node| node.last_24h_performance.naive_to_f64()) + .unwrap_or(0.5); + gateways.push((parsed_node, weight)) } gateways @@ -337,15 +320,11 @@ impl PacketPreparer { // if failed to get parsed => onto the blacklist they go // if generated fewer than n, blacklist will be updated by external function with correctly generated // routes so that they wouldn't be reused - pub(crate) async fn prepare_test_routes( - &self, - n: usize, - blacklist: &mut HashSet<NodeId>, - ) -> Option<Vec<TestRoute>> { - let (legacy_mixnodes, legacy_gateways) = self.filtered_legacy_mixnodes_and_gateways().await; + pub(crate) async fn prepare_test_routes(&self, n: usize) -> Option<Vec<TestRoute>> { let rewarded_set = self.contract_cache.rewarded_set().await?; let descriptions = self.described_cache.get().await.ok()?; + let statuses = self.node_status_cache.node_annotations().await?; let mixing_nym_nodes = descriptions.mixing_nym_nodes(); // last I checked `gatewaying` wasn't a word : ) @@ -353,15 +332,10 @@ impl PacketPreparer { let mut rng = thread_rng(); - // separate mixes into layers for easier selection - let layered_mixes = self.layered_mixes( - &mut rng, - blacklist, - &rewarded_set, - legacy_mixnodes, - mixing_nym_nodes, - ); - let gateways = self.all_gateways(blacklist, legacy_gateways, gateway_capable_nym_nodes); + // separate mixes into layers for easier selection alongside the selection weights + let layered_mixes = + self.to_legacy_layered_mixes(&mut rng, &rewarded_set, &statuses, mixing_nym_nodes); + let gateways = self.to_legacy_gateway_nodes(&statuses, gateway_capable_nym_nodes); // get all nodes from each layer... let l1 = layered_mixes.get(&LegacyMixLayer::One)?; @@ -369,10 +343,26 @@ impl PacketPreparer { let l3 = layered_mixes.get(&LegacyMixLayer::Three)?; // try to choose n nodes from each of them (+ gateways)... - let rand_l1 = l1.choose_multiple(&mut rng, n).collect::<Vec<_>>(); - let rand_l2 = l2.choose_multiple(&mut rng, n).collect::<Vec<_>>(); - let rand_l3 = l3.choose_multiple(&mut rng, n).collect::<Vec<_>>(); - let rand_gateways = gateways.choose_multiple(&mut rng, n).collect::<Vec<_>>(); + let rand_l1 = l1 + .choose_multiple_weighted(&mut rng, n, |item| item.1) + .ok()? + .map(|node| node.0.clone()) + .collect::<Vec<_>>(); + let rand_l2 = l2 + .choose_multiple_weighted(&mut rng, n, |item| item.1) + .ok()? + .map(|node| node.0.clone()) + .collect::<Vec<_>>(); + let rand_l3 = l3 + .choose_multiple_weighted(&mut rng, n, |item| item.1) + .ok()? + .map(|node| node.0.clone()) + .collect::<Vec<_>>(); + let rand_gateways = gateways + .choose_multiple_weighted(&mut rng, n, |item| item.1) + .ok()? + .map(|node| node.0.clone()) + .collect::<Vec<_>>(); // the unwrap on `min()` is fine as we know the iterator is not empty let most_available = *[ diff --git a/nym-api/src/network_monitor/monitor/processor.rs b/nym-api/src/network_monitor/monitor/processor.rs index 0008bfa901e..7a65115d78d 100644 --- a/nym-api/src/network_monitor/monitor/processor.rs +++ b/nym-api/src/network_monitor/monitor/processor.rs @@ -5,17 +5,19 @@ use crate::network_monitor::gateways_reader::GatewayMessages; use crate::network_monitor::test_packet::{NodeTestMessage, NymApiTestMessageExt}; use crate::network_monitor::ROUTE_TESTING_TEST_NONCE; use futures::channel::mpsc; -use futures::lock::{Mutex, MutexGuard}; -use futures::{SinkExt, StreamExt}; +use futures::lock::Mutex; +use futures::StreamExt; use nym_crypto::asymmetric::encryption; use nym_node_tester_utils::error::NetworkTestingError; use nym_node_tester_utils::processor::TestPacketProcessor; use nym_sphinx::acknowledgements::AckKey; use nym_sphinx::receiver::{MessageReceiver, MessageRecoveryError}; use std::mem; +use std::ops::Deref; +use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::Arc; use thiserror::Error; -use tracing::{debug, error, trace, warn}; +use tracing::{error, trace, warn}; pub(crate) type ReceivedProcessorSender = mpsc::UnboundedSender<GatewayMessages>; pub(crate) type ReceivedProcessorReceiver = mpsc::UnboundedReceiver<GatewayMessages>; @@ -37,51 +39,71 @@ enum ProcessingError { ReceivedOutsideTestRun, } -// we can't use Notify due to possible edge case where both notification are consumed at once -enum LockPermit { - Release, - Free, +#[derive(Clone)] +struct SharedProcessorData { + inner: Arc<SharedProcessorDataInner>, } -struct ReceivedProcessorInner<R: MessageReceiver> { - /// Nonce of the current test run indicating which packets should get rejected. - test_nonce: Option<u64>, +impl SharedProcessorData { + async fn reset_run_information(&self) -> Vec<NodeTestMessage> { + self.test_nonce.store(u64::MAX, Ordering::SeqCst); + let mut guard = self.received_packets.lock().await; + mem::take(&mut *guard) + } +} - /// Channel for receiving packets/messages from the gateway clients - packets_receiver: ReceivedProcessorReceiver, +impl Deref for SharedProcessorData { + type Target = SharedProcessorDataInner; + fn deref(&self) -> &Self::Target { + &self.inner + } +} - test_processor: TestPacketProcessor<NymApiTestMessageExt, R>, +struct SharedProcessorDataInner { + /// Nonce of the current test run indicating which packets should get rejected. + test_nonce: AtomicU64, /// Vector containing all received (and decrypted) packets in the current test run. // TODO: perhaps a different structure would be better here - received_packets: Vec<NodeTestMessage>, + received_packets: Mutex<Vec<NodeTestMessage>>, } -impl<R: MessageReceiver> ReceivedProcessorInner<R> { - fn on_received_data(&mut self, raw_message: Vec<u8>) -> Result<(), ProcessingError> { +struct ReceiverTask<R: MessageReceiver> { + shared: SharedProcessorData, + packets_receiver: ReceivedProcessorReceiver, + test_processor: TestPacketProcessor<NymApiTestMessageExt, R>, +} + +impl<R> ReceiverTask<R> +where + R: MessageReceiver, +{ + async fn on_received_data(&mut self, raw_message: Vec<u8>) -> Result<(), ProcessingError> { // if the nonce is none it means the packet was received during the 'waiting' for the // next test run - if self.test_nonce.is_none() { + let test_nonce = self.shared.test_nonce.load(Ordering::SeqCst); + if test_nonce == u64::MAX { return Err(ProcessingError::ReceivedOutsideTestRun); } let test_msg = self.test_processor.process_mixnet_message(raw_message)?; - if test_msg.ext.test_nonce != self.test_nonce.unwrap() { + if test_msg.ext.test_nonce != test_nonce { return Err(ProcessingError::NonMatchingNonce { received: test_msg.ext.test_nonce, - expected: self.test_nonce.unwrap(), + expected: test_nonce, }); } - self.received_packets.push(test_msg); + self.shared.received_packets.lock().await.push(test_msg); Ok(()) } fn on_received_ack(&mut self, raw_ack: Vec<u8>) -> Result<(), ProcessingError> { // if the nonce is none it means the packet was received during the 'waiting' for the // next test run - if self.test_nonce.is_none() { + let test_nonce = self.shared.test_nonce.load(Ordering::SeqCst); + if test_nonce == u64::MAX { return Err(ProcessingError::ReceivedOutsideTestRun); } @@ -92,11 +114,11 @@ impl<R: MessageReceiver> ReceivedProcessorInner<R> { Ok(()) } - fn on_received(&mut self, messages: GatewayMessages) { + async fn on_received(&mut self, messages: GatewayMessages) { match messages { GatewayMessages::Data(data_msgs) => { for raw in data_msgs { - if let Err(err) = self.on_received_data(raw) { + if let Err(err) = self.on_received_data(raw).await { warn!(target: "Monitor", "failed to process received gateway message: {err}") } } @@ -110,137 +132,64 @@ impl<R: MessageReceiver> ReceivedProcessorInner<R> { } } } - - fn finish_run(&mut self) -> Vec<NodeTestMessage> { - self.test_nonce = None; - mem::take(&mut self.received_packets) - } } -pub(crate) struct ReceivedProcessor<R: MessageReceiver> { - permit_changer: Option<mpsc::Sender<LockPermit>>, - inner: Arc<Mutex<ReceivedProcessorInner<R>>>, +pub struct ReceivedProcessor<R: MessageReceiver> { + shared: SharedProcessorData, + receiver_task: Option<ReceiverTask<R>>, } -impl<R: MessageReceiver + Send + 'static> ReceivedProcessor<R> { +impl<R> ReceivedProcessor<R> +where + R: MessageReceiver, +{ pub(crate) fn new( packets_receiver: ReceivedProcessorReceiver, client_encryption_keypair: Arc<encryption::KeyPair>, ack_key: Arc<AckKey>, ) -> Self { - let inner: Arc<Mutex<ReceivedProcessorInner<R>>> = - Arc::new(Mutex::new(ReceivedProcessorInner { - test_nonce: None, - packets_receiver, - test_processor: TestPacketProcessor::new(client_encryption_keypair, ack_key), - received_packets: Vec::new(), - })); + let shared_data = SharedProcessorData { + inner: Arc::new(SharedProcessorDataInner { + test_nonce: AtomicU64::new(u64::MAX), + received_packets: Default::default(), + }), + }; ReceivedProcessor { - permit_changer: None, - inner, + shared: shared_data.clone(), + receiver_task: Some(ReceiverTask { + shared: shared_data, + packets_receiver, + test_processor: TestPacketProcessor::new(client_encryption_keypair, ack_key), + }), } } - pub(crate) fn start_receiving(&mut self) { - let inner = Arc::clone(&self.inner); - - // TODO: perhaps it should be using 0 size instead? - let (permit_sender, mut permit_receiver) = mpsc::channel(1); - self.permit_changer = Some(permit_sender); + pub(crate) fn start_receiving(&mut self) + where + R: Sync + Send + 'static, + { + let mut receiver_task = self + .receiver_task + .take() + .expect("network monitor has already started the receiver task!"); tokio::spawn(async move { - while let Some(permit) = wait_for_permit(&mut permit_receiver, &inner).await { - receive_or_release_permit(&mut permit_receiver, permit).await; - } - - async fn receive_or_release_permit<Q: MessageReceiver>( - permit_receiver: &mut mpsc::Receiver<LockPermit>, - mut inner: MutexGuard<'_, ReceivedProcessorInner<Q>>, - ) { - loop { - tokio::select! { - permit_receiver = permit_receiver.next() => match permit_receiver { - Some(LockPermit::Release) => return, - Some(LockPermit::Free) => error!("somehow we got notification that the lock is free to take while we already hold it!"), - None => return, - }, - messages = inner.packets_receiver.next() => match messages { - Some(messages) => inner.on_received(messages), - None => return, - }, - } - } - } - - // // this lint really looks like a false positive because when lifetimes are elided, - // // the compiler can't figure out appropriate lifetime bounds - // #[allow(clippy::needless_lifetimes)] - async fn wait_for_permit<'a: 'b, 'b, P: MessageReceiver>( - permit_receiver: &'b mut mpsc::Receiver<LockPermit>, - inner: &'a Mutex<ReceivedProcessorInner<P>>, - ) -> Option<MutexGuard<'a, ReceivedProcessorInner<P>>> { - loop { - match permit_receiver.next().await { - // we should only ever get this on the very first run - Some(LockPermit::Release) => debug!( - "somehow got request to drop our lock permit while we do not hold it!" - ), - Some(LockPermit::Free) => return Some(inner.lock().await), - None => return None, - } - } + while let Some(messages) = receiver_task.packets_receiver.next().await { + receiver_task.on_received(messages).await } }); } - pub(super) async fn set_route_test_nonce(&mut self) { - self.set_new_test_nonce(ROUTE_TESTING_TEST_NONCE).await + pub(super) fn set_route_test_nonce(&self) { + self.set_new_test_nonce(ROUTE_TESTING_TEST_NONCE) } - pub(super) async fn set_new_test_nonce(&mut self, test_nonce: u64) { - // ask for the lock back - self.permit_changer - .as_mut() - .expect("ReceivedProcessor hasn't started receiving!") - .send(LockPermit::Release) - .await - .expect("processing task has died!"); - let mut inner = self.inner.lock().await; - - inner.test_nonce = Some(test_nonce); - - // give the permit back - drop(inner); - self.permit_changer - .as_mut() - .expect("ReceivedProcessor hasn't started receiving!") - .send(LockPermit::Free) - .await - .expect("processing task has died!"); + pub(super) fn set_new_test_nonce(&self, test_nonce: u64) { + self.shared.test_nonce.store(test_nonce, Ordering::SeqCst); } - pub(super) async fn return_received(&mut self) -> Vec<NodeTestMessage> { - // ask for the lock back - self.permit_changer - .as_mut() - .expect("ReceivedProcessor hasn't started receiving!") - .send(LockPermit::Release) - .await - .expect("processing task has died!"); - let mut inner = self.inner.lock().await; - - let received = inner.finish_run(); - - // give the permit back - drop(inner); - self.permit_changer - .as_mut() - .expect("ReceivedProcessor hasn't started receiving!") - .send(LockPermit::Free) - .await - .expect("processing task has died!"); - - received + pub(super) async fn return_received(&self) -> Vec<NodeTestMessage> { + self.shared.reset_run_information().await } } diff --git a/nym-api/src/network_monitor/monitor/receiver.rs b/nym-api/src/network_monitor/monitor/receiver.rs index d6dcbf4b855..91a8c678110 100644 --- a/nym-api/src/network_monitor/monitor/receiver.rs +++ b/nym-api/src/network_monitor/monitor/receiver.rs @@ -14,7 +14,7 @@ pub(crate) type GatewayClientUpdateSender = mpsc::UnboundedSender<GatewayClientU pub(crate) type GatewayClientUpdateReceiver = mpsc::UnboundedReceiver<GatewayClientUpdate>; pub(crate) enum GatewayClientUpdate { - Failure(identity::PublicKey), + Disconnect(identity::PublicKey), New( identity::PublicKey, (MixnetMessageReceiver, AcknowledgementReceiver), @@ -45,8 +45,8 @@ impl PacketReceiver { self.gateways_reader .add_receivers(id, message_receiver, ack_receiver); } - GatewayClientUpdate::Failure(id) => { - self.gateways_reader.remove_receivers(&id.to_string()); + GatewayClientUpdate::Disconnect(id) => { + self.gateways_reader.remove_receivers(id); } } } diff --git a/nym-api/src/network_monitor/monitor/sender.rs b/nym-api/src/network_monitor/monitor/sender.rs index ac69a0bc45b..c1140a55ac4 100644 --- a/nym-api/src/network_monitor/monitor/sender.rs +++ b/nym-api/src/network_monitor/monitor/sender.rs @@ -1,35 +1,34 @@ // Copyright 2021-2023 - Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: GPL-3.0-only -use crate::network_monitor::monitor::gateway_clients_cache::{ - ActiveGatewayClients, GatewayClientHandle, -}; -use crate::network_monitor::monitor::gateways_pinger::GatewayPinger; +use crate::network_monitor::monitor::gateway_client_handle::GatewayClientHandle; use crate::network_monitor::monitor::receiver::{GatewayClientUpdate, GatewayClientUpdateSender}; +use crate::support::config::Config; use crate::support::nyxd; +use dashmap::DashMap; use futures::channel::mpsc; use futures::stream::{self, FuturesUnordered, StreamExt}; use futures::task::Context; use futures::{Future, Stream}; use nym_bandwidth_controller::BandwidthController; use nym_credential_storage::persistent_storage::PersistentStorage; -use nym_crypto::asymmetric::identity::{self, PUBLIC_KEY_LENGTH}; +use nym_crypto::asymmetric::ed25519; use nym_gateway_client::client::config::GatewayClientConfig; use nym_gateway_client::client::GatewayConfig; use nym_gateway_client::error::GatewayClientError; use nym_gateway_client::{ - AcknowledgementReceiver, GatewayClient, MixnetMessageReceiver, PacketRouter, + AcknowledgementReceiver, GatewayClient, MixnetMessageReceiver, PacketRouter, SharedGatewayKey, }; use nym_sphinx::forwarding::packet::MixPacket; -use nym_task::TaskClient; use pin_project::pin_project; +use sqlx::__rt::timeout; use std::mem; use std::num::NonZeroUsize; use std::pin::Pin; use std::sync::Arc; use std::task::Poll; use std::time::Duration; -use tracing::{debug, info, trace, warn}; +use tracing::{debug, error, info, trace, warn}; const TIME_CHUNK_SIZE: Duration = Duration::from_millis(50); @@ -39,7 +38,7 @@ pub(crate) struct GatewayPackets { pub(crate) clients_address: String, /// Public key of the target gateway. - pub(crate) pub_key: identity::PublicKey, + pub(crate) pub_key: ed25519::PublicKey, /// All the packets that are going to get sent to the gateway. pub(crate) packets: Vec<MixPacket>, @@ -48,7 +47,7 @@ pub(crate) struct GatewayPackets { impl GatewayPackets { pub(crate) fn new( clients_address: String, - pub_key: identity::PublicKey, + pub_key: ed25519::PublicKey, packets: Vec<MixPacket>, ) -> Self { GatewayPackets { @@ -66,7 +65,7 @@ impl GatewayPackets { } } - pub(crate) fn empty(clients_address: String, pub_key: identity::PublicKey) -> Self { + pub(crate) fn empty(clients_address: String, pub_key: ed25519::PublicKey) -> Self { GatewayPackets { clients_address, pub_key, @@ -89,96 +88,63 @@ impl GatewayPackets { // struct consisting of all external data required to construct a fresh gateway client struct FreshGatewayClientData { gateways_status_updater: GatewayClientUpdateSender, - local_identity: Arc<identity::KeyPair>, + local_identity: Arc<ed25519::KeyPair>, gateway_response_timeout: Duration, bandwidth_controller: BandwidthController<nyxd::Client, PersistentStorage>, disabled_credentials_mode: bool, + gateways_key_cache: DashMap<ed25519::PublicKey, Arc<SharedGatewayKey>>, } impl FreshGatewayClientData { - fn notify_connection_failure( - self: Arc<FreshGatewayClientData>, - raw_gateway_id: [u8; PUBLIC_KEY_LENGTH], - ) { - // if this unwrap failed it means something extremely weird is going on - // and we got some solar flare bitflip type of corruption - let gateway_key = identity::PublicKey::from_bytes(&raw_gateway_id) - .expect("failed to recover gateways public key from valid bytes"); - - // remove the gateway listener channels - self.gateways_status_updater - .unbounded_send(GatewayClientUpdate::Failure(gateway_key)) - .expect("packet receiver seems to have died!"); - } - fn notify_new_connection( self: Arc<FreshGatewayClientData>, - gateway_id: identity::PublicKey, - gateway_channels: Option<(MixnetMessageReceiver, AcknowledgementReceiver)>, + gateway_id: ed25519::PublicKey, + gateway_channels: (MixnetMessageReceiver, AcknowledgementReceiver), ) { - self.gateways_status_updater - .unbounded_send(GatewayClientUpdate::New( - gateway_id, - gateway_channels.expect("we created a new client, yet the channels are a None!"), - )) - .expect("packet receiver seems to have died!") + if self + .gateways_status_updater + .unbounded_send(GatewayClientUpdate::New(gateway_id, gateway_channels)) + .is_err() + { + error!("packet receiver seems to have died!") + } } } pub(crate) struct PacketSender { - // TODO: this has a potential long-term issue. If we keep those clients cached between runs, - // malicious gateways could figure out which traffic comes from the network monitor and always - // forward that traffic while dropping the rest. However, at the current stage such sophisticated - // behaviour is unlikely. - active_gateway_clients: ActiveGatewayClients, - fresh_gateway_client_data: Arc<FreshGatewayClientData>, gateway_connection_timeout: Duration, + gateway_bandwidth_claim_timeout: Duration, max_concurrent_clients: usize, max_sending_rate: usize, } impl PacketSender { - // at this point I'm not entirely sure how to deal with this warning without - // some considerable refactoring - #[allow(clippy::too_many_arguments)] pub(crate) fn new( + config: &Config, gateways_status_updater: GatewayClientUpdateSender, - local_identity: Arc<identity::KeyPair>, - gateway_response_timeout: Duration, - gateway_connection_timeout: Duration, - max_concurrent_clients: usize, - max_sending_rate: usize, + local_identity: Arc<ed25519::KeyPair>, bandwidth_controller: BandwidthController<nyxd::Client, PersistentStorage>, - disabled_credentials_mode: bool, ) -> Self { PacketSender { - active_gateway_clients: ActiveGatewayClients::new(), fresh_gateway_client_data: Arc::new(FreshGatewayClientData { gateways_status_updater, local_identity, - gateway_response_timeout, + gateway_response_timeout: config.network_monitor.debug.gateway_response_timeout, bandwidth_controller, - disabled_credentials_mode, + disabled_credentials_mode: config.network_monitor.debug.disabled_credentials_mode, + gateways_key_cache: Default::default(), }), - gateway_connection_timeout, - max_concurrent_clients, - max_sending_rate, + gateway_connection_timeout: config.network_monitor.debug.gateway_connection_timeout, + gateway_bandwidth_claim_timeout: config + .network_monitor + .debug + .gateway_bandwidth_claim_timeout, + max_concurrent_clients: config.network_monitor.debug.max_concurrent_gateway_clients, + max_sending_rate: config.network_monitor.debug.gateway_sending_rate, } } - pub(crate) fn spawn_gateways_pinger(&self, pinging_interval: Duration, shutdown: TaskClient) { - let gateway_pinger = GatewayPinger::new( - self.active_gateway_clients.clone(), - self.fresh_gateway_client_data - .gateways_status_updater - .clone(), - pinging_interval, - ); - - tokio::spawn(async move { gateway_pinger.run(shutdown).await }); - } - fn new_gateway_client_handle( config: GatewayConfig, fresh_gateway_client_data: &FreshGatewayClientData, @@ -190,8 +156,6 @@ impl PacketSender { let task_client = nym_task::TaskClient::dummy().named(format!("gateway-{}", config.gateway_identity)); - // TODO: future optimization: if we're remaking client for a gateway to which we used to be connected in the past, - // use old shared keys let (message_sender, message_receiver) = mpsc::unbounded(); // currently we do not care about acks at all, but we must keep the channel alive @@ -204,13 +168,18 @@ impl PacketSender { task_client.fork("packet-router"), ); + let shared_keys = fresh_gateway_client_data + .gateways_key_cache + .get(&config.gateway_identity) + .map(|k| k.value().clone()); + let gateway_client = GatewayClient::new( GatewayClientConfig::new_default() .with_disabled_credentials_mode(fresh_gateway_client_data.disabled_credentials_mode) .with_response_timeout(fresh_gateway_client_data.gateway_response_timeout), config, Arc::clone(&fresh_gateway_client_data.local_identity), - None, + shared_keys, gateway_packet_router, Some(fresh_gateway_client_data.bandwidth_controller.clone()), nym_statistics_common::clients::ClientStatsSender::new(None), @@ -218,7 +187,10 @@ impl PacketSender { ); ( - GatewayClientHandle::new(gateway_client), + GatewayClientHandle::new( + gateway_client, + fresh_gateway_client_data.gateways_status_updater.clone(), + ), (message_receiver, ack_receiver), ) } @@ -228,11 +200,11 @@ impl PacketSender { mut mix_packets: Vec<MixPacket>, max_sending_rate: usize, ) -> Result<(), GatewayClientError> { - let gateway_id = client.gateway_identity().to_base58_string(); + let gateway_id = client.gateway_identity(); + info!( - "Got {} packets to send to gateway {}", + "Got {} packets to send to gateway {gateway_id}", mix_packets.len(), - gateway_id ); if mix_packets.len() <= max_sending_rate { @@ -282,47 +254,79 @@ impl PacketSender { Ok(()) } + async fn client_startup( + connection_timeout: Duration, + bandwidth_claim_timeout: Duration, + client: &mut GatewayClientHandle, + ) -> Option<Arc<SharedGatewayKey>> { + let gateway_identity = client.gateway_identity(); + + // 1. attempt to authenticate + let shared_key = + match timeout(connection_timeout, client.perform_initial_authentication()).await { + Err(_timeout) => { + warn!("timed out while trying to authenticate with gateway {gateway_identity}"); + return None; + } + Ok(Err(err)) => { + warn!("failed to authenticate with gateway ({gateway_identity}): {err}"); + return None; + } + Ok(Ok(res)) => res.initial_shared_key, + }; + + // 2. maybe claim bandwidth + match timeout(bandwidth_claim_timeout, client.claim_initial_bandwidth()).await { + Err(_timeout) => { + warn!("timed out while trying to claim initial bandwidth with gateway {gateway_identity}"); + return None; + } + Ok(Err(err)) => { + warn!("failed to claim bandwidth with gateway ({gateway_identity}): {err}"); + return None; + } + Ok(Ok(_)) => (), + } + + // 3. start internal listener + if let Err(err) = client.start_listening_for_mixnet_messages() { + warn!("failed to start message listener for {gateway_identity}: {err}"); + return None; + } + + Some(shared_key) + } + async fn create_new_gateway_client_handle_and_authenticate( config: GatewayConfig, fresh_gateway_client_data: &FreshGatewayClientData, gateway_connection_timeout: Duration, + gateway_bandwidth_claim_timeout: Duration, ) -> Option<( GatewayClientHandle, (MixnetMessageReceiver, AcknowledgementReceiver), )> { let gateway_identity = config.gateway_identity; - let (new_client, (message_receiver, ack_receiver)) = + let (mut new_client, (message_receiver, ack_receiver)) = Self::new_gateway_client_handle(config, fresh_gateway_client_data); - // Put this in timeout in case the gateway has incorrectly set their ulimit and our connection - // gets stuck in their TCP queue and just hangs on our end but does not terminate - // (an actual bug we experienced) - // - // Note: locking the client in unchecked manner is fine here as we just created the lock - // and it wasn't shared with anyone, therefore we're the only one holding reference to it - // and hence it's impossible to fail to obtain the permit. - let mut unlocked_client = new_client.lock_client_unchecked(); - - // SAFETY: it's fine to use the deprecated method here as we're creating brand new clients each time, - // and there's no need to deal with any key upgrades - #[allow(deprecated)] - match tokio::time::timeout( + match Self::client_startup( gateway_connection_timeout, - unlocked_client.get_mut_unchecked().authenticate_and_start(), + gateway_bandwidth_claim_timeout, + &mut new_client, ) .await { - Ok(Ok(_)) => { - drop(unlocked_client); + Some(shared_key) => { + fresh_gateway_client_data + .gateways_key_cache + .insert(gateway_identity, shared_key); Some((new_client, (message_receiver, ack_receiver))) } - Ok(Err(err)) => { - warn!("failed to authenticate with new gateway ({gateway_identity}): {err}",); - // we failed to create a client, can't do much here - None - } - Err(_) => { - warn!("timed out while trying to authenticate with new gateway {gateway_identity}",); + None => { + fresh_gateway_client_data + .gateways_key_cache + .remove(&gateway_identity); None } } @@ -345,123 +349,63 @@ impl PacketSender { // than just concurrently? async fn send_gateway_packets( gateway_connection_timeout: Duration, + gateway_bandwidth_claim_timeout: Duration, packets: GatewayPackets, fresh_gateway_client_data: Arc<FreshGatewayClientData>, - client: Option<GatewayClientHandle>, max_sending_rate: usize, ) -> Option<GatewayClientHandle> { - let existing_client = client.is_some(); - - // Note that in the worst case scenario we will only wait for a second or two to obtain the lock - // as other possibly entity holding the lock (the gateway pinger) is attempting to send - // the ping messages with a maximum timeout. - let (client, gateway_channels) = if let Some(client) = client { - if client.is_invalid().await { - warn!("Our existing client was invalid - two test runs happened back to back without cleanup"); - return None; - } - (client, None) - } else { - let (client, gateway_channels) = - Self::create_new_gateway_client_handle_and_authenticate( - packets.gateway_config(), - &fresh_gateway_client_data, - gateway_connection_timeout, - ) - .await?; - (client, Some(gateway_channels)) - }; + let (mut client, gateway_channels) = + Self::create_new_gateway_client_handle_and_authenticate( + packets.gateway_config(), + &fresh_gateway_client_data, + gateway_connection_timeout, + gateway_bandwidth_claim_timeout, + ) + .await?; + + let identity = client.gateway_identity(); let estimated_time = Duration::from_secs_f64(packets.packets.len() as f64 / max_sending_rate as f64); // give some leeway let timeout = estimated_time * 3; - let mut guard = client.lock_client().await; - let unwrapped_client = guard.get_mut_unchecked(); - - if let Err(err) = Self::check_remaining_bandwidth(unwrapped_client).await { - warn!( - "Failed to claim additional bandwidth for {} - {err}", - unwrapped_client.gateway_identity().to_base58_string(), - ); - if existing_client { - guard.invalidate(); - fresh_gateway_client_data.notify_connection_failure(packets.pub_key.to_bytes()); - } + if let Err(err) = Self::check_remaining_bandwidth(&mut client).await { + warn!("Failed to claim additional bandwidth for {identity}: {err}",); return None; } match tokio::time::timeout( timeout, - Self::attempt_to_send_packets(unwrapped_client, packets.packets, max_sending_rate), + Self::attempt_to_send_packets(&mut client, packets.packets, max_sending_rate), ) .await { Err(_timeout) => { - warn!( - "failed to send packets to {} - we timed out", - packets.pub_key.to_base58_string(), - ); - // if this was a fresh client, there's no need to do anything as it was never - // registered to get read - if existing_client { - guard.invalidate(); - fresh_gateway_client_data.notify_connection_failure(packets.pub_key.to_bytes()); - } + warn!("failed to send packets to {identity} - we timed out",); return None; } Ok(Err(err)) => { - warn!( - "failed to send packets to {} - {:?}", - packets.pub_key.to_base58_string(), - err - ); - // if this was a fresh client, there's no need to do anything as it was never - // registered to get read - if existing_client { - guard.invalidate(); - fresh_gateway_client_data.notify_connection_failure(packets.pub_key.to_bytes()); - } + warn!("failed to send packets to {identity}: {err}",); return None; } Ok(Ok(_)) => { - if !existing_client { - fresh_gateway_client_data - .notify_new_connection(packets.pub_key, gateway_channels); - } + fresh_gateway_client_data.notify_new_connection(identity, gateway_channels) } } - drop(guard); Some(client) } - // point of this is to basically insert handles of fresh clients that didn't exist here before - async fn merge_client_handles(&self, handles: Vec<GatewayClientHandle>) { - let mut guard = self.active_gateway_clients.lock().await; - for handle in handles { - let raw_identity = handle.raw_identity(); - if let Some(existing) = guard.get(&raw_identity) { - if !handle.ptr_eq(existing) { - panic!("Duplicate client detected!") - } - - if handle.is_invalid().await { - guard.remove(&raw_identity); - } - } else { - // client never existed -> just insert it - guard.insert(raw_identity, handle); - } - } - } - - pub(super) async fn send_packets(&mut self, packets: Vec<GatewayPackets>) { + pub(super) async fn send_packets( + &mut self, + packets: Vec<GatewayPackets>, + ) -> Vec<GatewayClientHandle> { // we know that each of the elements in the packets array will only ever access a single, // unique element from the existing clients let gateway_connection_timeout = self.gateway_connection_timeout; + let gateway_bandwidth_claim_timeout = self.gateway_bandwidth_claim_timeout; let max_concurrent_clients = if self.max_concurrent_clients > 0 { Some(self.max_concurrent_clients) } else { @@ -469,41 +413,24 @@ impl PacketSender { }; let max_sending_rate = self.max_sending_rate; - let guard = self.active_gateway_clients.lock().await; - // this clippy warning is a false positive as we cannot get rid of the collect by moving - // everything into a single iterator as it would require us to hold the lock the entire time - // and that is exactly what we want to avoid - #[allow(clippy::needless_collect)] let stream_data = packets .into_iter() - .map(|packets| { - let existing_client = guard - .get(&packets.pub_key.to_bytes()) - .map(|client| client.clone_data_pointer()); - ( - packets, - Arc::clone(&self.fresh_gateway_client_data), - existing_client, - ) - }) + .map(|packets| (packets, Arc::clone(&self.fresh_gateway_client_data))) .collect::<Vec<_>>(); - // drop the guard immediately so that the other task (gateway pinger) would not need to wait until - // we're done sending packets (note: without this drop, we wouldn't be able to ping gateways that - // we're not interacting with right now) - drop(guard); - // can't chain it all nicely together as there's no adapter method defined on Stream directly // for ForEachConcurrentClientUse - let used_clients = ForEachConcurrentClientUse::new( + // + // we need to keep clients alive until the test finishes so that we could keep receiving + ForEachConcurrentClientUse::new( stream::iter(stream_data.into_iter()), max_concurrent_clients, - |(packets, fresh_data, client)| async move { + |(packets, fresh_data)| async move { Self::send_gateway_packets( gateway_connection_timeout, + gateway_bandwidth_claim_timeout, packets, fresh_data, - client, max_sending_rate, ) .await @@ -512,9 +439,7 @@ impl PacketSender { .await .into_iter() .flatten() - .collect(); - - self.merge_client_handles(used_clients).await; + .collect() } } diff --git a/nym-api/src/network_monitor/monitor/summary_producer.rs b/nym-api/src/network_monitor/monitor/summary_producer.rs index 5f8449f75fe..a55212c89c4 100644 --- a/nym-api/src/network_monitor/monitor/summary_producer.rs +++ b/nym-api/src/network_monitor/monitor/summary_producer.rs @@ -32,38 +32,53 @@ impl RouteResult { } } -#[derive(Default, Debug)] +#[derive(Debug)] pub(crate) struct TestReport { - pub(crate) network_reliability: f32, + pub(crate) network_reliability: f64, pub(crate) total_sent: usize, pub(crate) total_received: usize, - pub(crate) route_results: Vec<RouteResult>, - - pub(crate) exceptional_mixnodes: usize, - pub(crate) exceptional_gateways: usize, - - pub(crate) fine_mixnodes: usize, - pub(crate) fine_gateways: usize, - - pub(crate) poor_mixnodes: usize, - pub(crate) poor_gateways: usize, - - pub(crate) unreliable_mixnodes: usize, - pub(crate) unreliable_gateways: usize, - - pub(crate) unroutable_mixnodes: usize, - pub(crate) unroutable_gateways: usize, + // integer score to number of nodes with that score + pub(crate) mixnode_results: HashMap<u8, usize>, + pub(crate) gateway_results: HashMap<u8, usize>, } impl TestReport { - fn new( + pub(crate) fn new( total_sent: usize, total_received: usize, - mixnode_results: &[NodeResult], - gateway_results: &[NodeResult], - route_results: &[RouteResult], + raw_mixnode_results: &[NodeResult], + raw_gateway_results: &[NodeResult], ) -> Self { + let network_reliability = total_received as f64 / total_sent as f64 * 100.0; + + let mut mixnode_results = HashMap::new(); + let mut gateway_results = HashMap::new(); + + for res in raw_mixnode_results { + mixnode_results + .entry(res.reliability) + .and_modify(|c| *c += 1) + .or_insert(1); + } + + for res in raw_gateway_results { + gateway_results + .entry(res.reliability) + .and_modify(|c| *c += 1) + .or_insert(1); + } + + TestReport { + network_reliability, + total_sent, + total_received, + mixnode_results, + gateway_results, + } + } + + pub(crate) fn to_display_report(&self, route_results: &[RouteResult]) -> DisplayTestReport { let mut exceptional_mixnodes = 0; let mut exceptional_gateways = 0; @@ -79,40 +94,38 @@ impl TestReport { let mut unroutable_mixnodes = 0; let mut unroutable_gateways = 0; - for mixnode_result in mixnode_results { - if mixnode_result.reliability >= EXCEPTIONAL_THRESHOLD { - exceptional_mixnodes += 1; - } else if mixnode_result.reliability >= FINE_THRESHOLD { - fine_mixnodes += 1; - } else if mixnode_result.reliability >= POOR_THRESHOLD { - poor_mixnodes += 1; - } else if mixnode_result.reliability >= UNRELIABLE_THRESHOLD { - unreliable_mixnodes += 1; + for (&score, &count) in &self.mixnode_results { + if score >= EXCEPTIONAL_THRESHOLD { + exceptional_mixnodes += count; + } else if score >= FINE_THRESHOLD { + fine_mixnodes += count; + } else if score >= POOR_THRESHOLD { + poor_mixnodes += count; + } else if score >= UNRELIABLE_THRESHOLD { + unreliable_mixnodes += count; } else { - unroutable_mixnodes += 1; + unroutable_mixnodes += count; } } - for gateway_result in gateway_results { - if gateway_result.reliability >= EXCEPTIONAL_THRESHOLD { - exceptional_gateways += 1; - } else if gateway_result.reliability >= FINE_THRESHOLD { - fine_gateways += 1; - } else if gateway_result.reliability >= POOR_THRESHOLD { - poor_gateways += 1; - } else if gateway_result.reliability >= UNRELIABLE_THRESHOLD { - unreliable_gateways += 1; + for (&score, &count) in &self.gateway_results { + if score >= EXCEPTIONAL_THRESHOLD { + exceptional_gateways += count; + } else if score >= FINE_THRESHOLD { + fine_gateways += count; + } else if score >= POOR_THRESHOLD { + poor_gateways += count; + } else if score >= UNRELIABLE_THRESHOLD { + unreliable_gateways += count; } else { - unroutable_gateways += 1; + unroutable_gateways += count; } } - let network_reliability = total_received as f32 / total_sent as f32 * 100.0; - - TestReport { - network_reliability, - total_sent, - total_received, + DisplayTestReport { + network_reliability: self.network_reliability, + total_sent: self.total_sent, + total_received: self.total_received, route_results: route_results.to_vec(), exceptional_mixnodes, exceptional_gateways, @@ -128,7 +141,31 @@ impl TestReport { } } -impl Display for TestReport { +#[derive(Default, Debug)] +pub(crate) struct DisplayTestReport { + pub(crate) network_reliability: f64, + pub(crate) total_sent: usize, + pub(crate) total_received: usize, + + pub(crate) route_results: Vec<RouteResult>, + + pub(crate) exceptional_mixnodes: usize, + pub(crate) exceptional_gateways: usize, + + pub(crate) fine_mixnodes: usize, + pub(crate) fine_gateways: usize, + + pub(crate) poor_mixnodes: usize, + pub(crate) poor_gateways: usize, + + pub(crate) unreliable_mixnodes: usize, + pub(crate) unreliable_gateways: usize, + + pub(crate) unroutable_mixnodes: usize, + pub(crate) unroutable_gateways: usize, +} + +impl Display for DisplayTestReport { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { writeln!(f, "Mix Network Test Report")?; writeln!( @@ -218,7 +255,6 @@ impl TestSummary { total_received, &self.mixnode_results, &self.gateway_results, - &self.route_results, ) } } diff --git a/nym-api/src/node_status_api/cache/node_sets.rs b/nym-api/src/node_status_api/cache/node_sets.rs index 0ea8820ab52..e03dd2807da 100644 --- a/nym-api/src/node_status_api/cache/node_sets.rs +++ b/nym-api/src/node_status_api/cache/node_sets.rs @@ -5,6 +5,7 @@ use crate::node_describe_cache::DescribedNodes; use crate::node_status_api::helpers::RewardedSetStatus; use crate::node_status_api::models::Uptime; use crate::node_status_api::reward_estimate::{compute_apy_from_reward, compute_reward_estimate}; +use crate::nym_contract_cache::cache::data::ConfigScoreData; use crate::nym_contract_cache::cache::CachedRewardedSet; use crate::support::storage::NymApiStorage; use nym_api_requests::legacy::{LegacyGatewayBondWithId, LegacyMixNodeDetailsWithLayer}; @@ -14,7 +15,7 @@ use nym_api_requests::models::{ MixNodeBondAnnotated, NodeAnnotation, NodePerformance, NymNodeDescription, RoutingScore, }; use nym_contracts_common::NaiveFloat; -use nym_mixnet_contract_common::{ConfigScoreParams, Interval, NodeId}; +use nym_mixnet_contract_common::{Interval, NodeId, VersionScoreFormulaParams}; use nym_mixnet_contract_common::{NymNodeDetails, RewardingParams}; use nym_topology::NetworkAddress; use std::collections::{HashMap, HashSet}; @@ -90,24 +91,37 @@ async fn get_routing_score( RoutingScore::new(score as f64) } +fn versions_behind_factor_to_config_score( + versions_behind: u32, + params: VersionScoreFormulaParams, +) -> f64 { + let penalty = params.penalty.naive_to_f64(); + let scaling = params.penalty_scaling.naive_to_f64(); + + // version_score = penalty ^ (num_versions_behind ^ penalty_scaling) + penalty.powf((versions_behind as f64).powf(scaling)) +} + fn calculate_config_score( - config_score_params: &ConfigScoreParams, + config_score_data: &ConfigScoreData, described_data: Option<&NymNodeDescription>, ) -> ConfigScore { let Some(described) = described_data else { return ConfigScore::unavailable(); }; - let Ok(reported_semver) = described - .description - .build_information - .build_version - .parse::<semver::Version>() - else { + let node_version = &described.description.build_information.build_version; + let Ok(reported_semver) = node_version.parse::<semver::Version>() else { return ConfigScore::bad_semver(); }; + let versions_behind = config_score_data + .config_score_params + .version_weights + .versions_behind_factor( + &reported_semver, + &config_score_data.nym_node_version_history, + ); - let versions_behind = config_score_params.versions_behind(&reported_semver); let runs_nym_node = described.description.build_information.binary_name == "nym-node"; let accepted_terms_and_conditions = described .description @@ -117,17 +131,12 @@ fn calculate_config_score( let version_score = if !runs_nym_node || !accepted_terms_and_conditions { 0. } else { - let penalty = config_score_params - .version_score_formula_params - .penalty - .naive_to_f64(); - let scaling = config_score_params - .version_score_formula_params - .penalty_scaling - .naive_to_f64(); - - // version_score = penalty ^ (num_versions_behind ^ penalty_scaling) - penalty.powf((versions_behind as f64).powf(scaling)) + versions_behind_factor_to_config_score( + versions_behind, + config_score_data + .config_score_params + .version_score_formula_params, + ) }; ConfigScore::new( @@ -285,7 +294,7 @@ pub(crate) async fn annotate_legacy_gateways_with_details( #[allow(clippy::too_many_arguments)] pub(crate) async fn produce_node_annotations( storage: &NymApiStorage, - config_score_params: &ConfigScoreParams, + config_score_data: &ConfigScoreData, legacy_mixnodes: &[LegacyMixNodeDetailsWithLayer], legacy_gateways: &[LegacyGatewayBondWithId], nym_nodes: &[NymNodeDetails], @@ -301,7 +310,7 @@ pub(crate) async fn produce_node_annotations( let routing_score = get_routing_score(storage, node_id, LegacyMixnode, current_interval).await; let config_score = - calculate_config_score(config_score_params, described_nodes.get_node(&node_id)); + calculate_config_score(config_score_data, described_nodes.get_node(&node_id)); let performance = routing_score.score * config_score.score; // map it from 0-1 range into 0-100 @@ -327,7 +336,7 @@ pub(crate) async fn produce_node_annotations( let routing_score = get_routing_score(storage, node_id, LegacyGateway, current_interval).await; let config_score = - calculate_config_score(config_score_params, described_nodes.get_node(&node_id)); + calculate_config_score(config_score_data, described_nodes.get_node(&node_id)); let performance = routing_score.score * config_score.score; // map it from 0-1 range into 0-100 @@ -352,7 +361,7 @@ pub(crate) async fn produce_node_annotations( let node_id = nym_node.node_id(); let routing_score = get_routing_score(storage, node_id, NymNode, current_interval).await; let config_score = - calculate_config_score(config_score_params, described_nodes.get_node(&node_id)); + calculate_config_score(config_score_data, described_nodes.get_node(&node_id)); let performance = routing_score.score * config_score.score; // map it from 0-1 range into 0-100 diff --git a/nym-api/src/node_status_api/cache/refresher.rs b/nym-api/src/node_status_api/cache/refresher.rs index 1d1771d7bbc..b9f4aed5075 100644 --- a/nym-api/src/node_status_api/cache/refresher.rs +++ b/nym-api/src/node_status_api/cache/refresher.rs @@ -144,9 +144,9 @@ impl NodeStatusCacheRefresher { let rewarded_set = self.contract_cache.rewarded_set_owned().await; let gateway_bonds = self.contract_cache.legacy_gateways_all().await; let nym_nodes = self.contract_cache.nym_nodes().await; - let config_score_params = self + let config_score_data = self .contract_cache - .config_score_params() + .config_score_data_owned() .await .into_inner() .ok_or(NodeStatusCacheError::SourceDataMissing)?; @@ -181,7 +181,7 @@ impl NodeStatusCacheRefresher { // Create annotated data let node_annotations = produce_node_annotations( &self.storage, - &config_score_params, + &config_score_data, &mixnode_details, &gateway_bonds, &nym_nodes, diff --git a/nym-api/src/node_status_api/handlers/mod.rs b/nym-api/src/node_status_api/handlers/mod.rs index f42cd59d841..b720294af94 100644 --- a/nym-api/src/node_status_api/handlers/mod.rs +++ b/nym-api/src/node_status_api/handlers/mod.rs @@ -1,8 +1,13 @@ // Copyright 2021-2024 - Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: GPL-3.0-only +use crate::node_status_api::models::AxumResult; +use crate::support::caching::cache::UninitialisedCache; use crate::support::http::state::AppState; -use axum::Router; +use axum::extract::State; +use axum::routing::get; +use axum::{Json, Router}; +use nym_api_requests::models::ConfigScoreDataResponse; use nym_mixnet_contract_common::NodeId; use serde::Deserialize; use utoipa::IntoParams; @@ -11,7 +16,7 @@ pub(crate) mod network_monitor; pub(crate) mod unstable; pub(crate) mod without_monitor; -pub(crate) fn node_status_routes(network_monitor: bool) -> Router<AppState> { +pub(crate) fn status_routes(network_monitor: bool) -> Router<AppState> { // in the minimal variant we would not have access to endpoints relying on existence // of the network monitor and the associated storage let without_network_monitor = without_monitor::mandatory_routes(); @@ -23,6 +28,7 @@ pub(crate) fn node_status_routes(network_monitor: bool) -> Router<AppState> { } else { without_network_monitor } + .route("/config-score-details", get(config_score_details)) } #[derive(Deserialize, IntoParams)] @@ -30,3 +36,24 @@ pub(crate) fn node_status_routes(network_monitor: bool) -> Router<AppState> { struct MixIdParam { mix_id: NodeId, } + +#[utoipa::path( + tag = "Status", + get, + path = "/config-score-details", + context_path = "/v1/status", + responses( + (status = 200, body = ConfigScoreDataResponse) + ), +)] +async fn config_score_details( + State(state): State<AppState>, +) -> AxumResult<Json<ConfigScoreDataResponse>> { + let data = state + .nym_contract_cache() + .maybe_config_score_data_owned() + .await + .ok_or(UninitialisedCache)?; + + Ok(Json(data.into_inner().into())) +} diff --git a/nym-api/src/node_status_api/handlers/network_monitor.rs b/nym-api/src/node_status_api/handlers/network_monitor.rs index 5f71eac079c..dbd37200894 100644 --- a/nym-api/src/node_status_api/handlers/network_monitor.rs +++ b/nym-api/src/node_status_api/handlers/network_monitor.rs @@ -1,6 +1,8 @@ // Copyright 2021-2024 - Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: GPL-3.0-only +use super::unstable; +use crate::node_status_api::handlers::unstable::{latest_monitor_run_report, monitor_run_report}; use crate::node_status_api::handlers::MixIdParam; use crate::node_status_api::helpers::{ _compute_mixnode_reward_estimation, _gateway_core_status_count, _gateway_report, @@ -23,8 +25,6 @@ use nym_api_requests::models::{ use serde::Deserialize; use utoipa::IntoParams; -use super::unstable; - // we want to mark the routes as deprecated in swagger, but still expose them #[allow(deprecated)] pub(super) fn network_monitor_routes() -> Router<AppState> { @@ -84,6 +84,18 @@ pub(super) fn network_monitor_routes() -> Router<AppState> { axum::routing::get(unstable::gateway_test_results), ), ) + .nest( + "/network-monitor/unstable", + Router::new() + .route( + "/run/:monitor_run_id/details", + axum::routing::get(monitor_run_report), + ) + .route( + "/run/latest/details", + axum::routing::get(latest_monitor_run_report), + ), + ) } #[utoipa::path( diff --git a/nym-api/src/node_status_api/handlers/unstable.rs b/nym-api/src/node_status_api/handlers/unstable.rs index 2b72f729084..1378b6a055c 100644 --- a/nym-api/src/node_status_api/handlers/unstable.rs +++ b/nym-api/src/node_status_api/handlers/unstable.rs @@ -6,15 +6,17 @@ use crate::node_status_api::models::{AxumErrorResponse, AxumResult}; use crate::support::http::helpers::PaginationRequest; use crate::support::http::state::AppState; use crate::support::storage::NymApiStorage; +use anyhow::bail; use axum::extract::{Path, Query, State}; use axum::Json; use nym_api_requests::models::{ - GatewayTestResultResponse, MixnodeTestResultResponse, PartialTestResult, TestNode, TestRoute, + GatewayTestResultResponse, MixnodeTestResultResponse, NetworkMonitorRunDetailsResponse, + PartialTestResult, TestNode, TestRoute, }; use nym_api_requests::pagination::Pagination; use nym_mixnet_contract_common::NodeId; use std::cmp::min; -use std::collections::HashMap; +use std::collections::{BTreeMap, HashMap}; use std::sync::Arc; use tokio::sync::RwLock; use tracing::{error, trace}; @@ -301,3 +303,88 @@ pub async fn gateway_test_results( ))), } } + +async fn _monitor_run_report( + monitor_run_id: i64, + storage: &NymApiStorage, +) -> anyhow::Result<NetworkMonitorRunDetailsResponse> { + let Some((raw_report, raw_scores)) = storage.get_monitor_run_report(monitor_run_id).await? + else { + bail!("no results found for monitor run {monitor_run_id}"); + }; + + let mut mixnode_results = BTreeMap::new(); + let mut gateway_results = BTreeMap::new(); + + for score in raw_scores { + if score.typ == "mixnode" { + mixnode_results.insert(score.rounded_score, score.nodes_count as usize); + } else if score.typ == "gateway" { + gateway_results.insert(score.rounded_score, score.nodes_count as usize); + } + } + + Ok(NetworkMonitorRunDetailsResponse { + monitor_run_id, + network_reliability: raw_report.network_reliability, + total_sent: raw_report.packets_sent as usize, + total_received: raw_report.packets_received as usize, + mixnode_results, + gateway_results, + }) +} + +async fn _latest_monitor_run_report( + storage: &NymApiStorage, +) -> anyhow::Result<NetworkMonitorRunDetailsResponse> { + let Some(latest_id) = storage.get_latest_monitor_run_id().await? else { + bail!("no network monitor run found"); + }; + + _monitor_run_report(latest_id, storage).await +} + +#[utoipa::path( + tag = "UNSTABLE - DO **NOT** USE", + get, + params( + PaginationRequest + ), + path = "/v1/status/network-monitor/unstable/run/{monitor_run_id}/details", + responses( + (status = 200, body = NetworkMonitorRunDetailsResponse) + ) +)] +pub async fn monitor_run_report( + Path(monitor_run_id): Path<i64>, + State(state): State<AppState>, +) -> AxumResult<Json<NetworkMonitorRunDetailsResponse>> { + match _monitor_run_report(monitor_run_id, state.storage()).await { + Ok(res) => Ok(Json(res)), + Err(err) => Err(AxumErrorResponse::internal_msg(format!( + "failed to retrieve monitor run report for run {monitor_run_id}: {err}" + ))), + } +} + +#[utoipa::path( + tag = "UNSTABLE - DO **NOT** USE", + get, + params( + PaginationRequest + ), + path = "/v1/status/network-monitor/unstable/run/latest/details", + responses( + (status = 200, body = NetworkMonitorRunDetailsResponse) + ) +)] +pub async fn latest_monitor_run_report( + State(state): State<AppState>, +) -> AxumResult<Json<NetworkMonitorRunDetailsResponse>> { + match _latest_monitor_run_report(state.storage()).await { + Ok(res) => Ok(Json(res)), + Err(err) => Err(AxumErrorResponse::internal_msg(format!( + "failed to retrieve the latest monitor run report: {err}" + ))), + } +} diff --git a/nym-api/src/nym_contract_cache/cache/data.rs b/nym-api/src/nym_contract_cache/cache/data.rs index 3bfe058f729..d259109f988 100644 --- a/nym-api/src/nym_contract_cache/cache/data.rs +++ b/nym-api/src/nym_contract_cache/cache/data.rs @@ -3,10 +3,12 @@ use crate::support::caching::Cache; use nym_api_requests::legacy::{LegacyGatewayBondWithId, LegacyMixNodeDetailsWithLayer}; +use nym_api_requests::models::{ConfigScoreDataResponse, RewardedSetResponse}; use nym_contracts_common::ContractBuildInformation; use nym_mixnet_contract_common::nym_node::Role; use nym_mixnet_contract_common::{ - ConfigScoreParams, Interval, NodeId, NymNodeDetails, RewardedSet, RewardingParams, + ConfigScoreParams, HistoricalNymNodeVersionEntry, Interval, NodeId, NymNodeDetails, + RewardedSet, RewardingParams, }; use nym_validator_client::nyxd::AccountId; use std::collections::{HashMap, HashSet}; @@ -52,6 +54,19 @@ impl From<CachedRewardedSet> for RewardedSet { } } +impl From<&CachedRewardedSet> for RewardedSetResponse { + fn from(value: &CachedRewardedSet) -> Self { + RewardedSetResponse { + entry_gateways: value.entry_gateways.iter().copied().collect(), + exit_gateways: value.exit_gateways.iter().copied().collect(), + layer1: value.layer1.iter().copied().collect(), + layer2: value.layer2.iter().copied().collect(), + layer3: value.layer3.iter().copied().collect(), + standby: value.standby.iter().copied().collect(), + } + } +} + impl CachedRewardedSet { pub(crate) fn role(&self, node_id: NodeId) -> Option<Role> { if self.entry_gateways.contains(&node_id) { @@ -112,6 +127,25 @@ impl CachedRewardedSet { } } +#[derive(Clone)] +pub(crate) struct ConfigScoreData { + pub(crate) config_score_params: ConfigScoreParams, + pub(crate) nym_node_version_history: Vec<HistoricalNymNodeVersionEntry>, +} + +impl From<ConfigScoreData> for ConfigScoreDataResponse { + fn from(value: ConfigScoreData) -> Self { + ConfigScoreDataResponse { + parameters: value.config_score_params.into(), + version_history: value + .nym_node_version_history + .into_iter() + .map(Into::into) + .collect(), + } + } +} + pub(crate) struct ContractCacheData { pub(crate) legacy_mixnodes: Cache<Vec<LegacyMixNodeDetailsWithLayer>>, pub(crate) legacy_gateways: Cache<Vec<LegacyGatewayBondWithId>>, @@ -123,7 +157,7 @@ pub(crate) struct ContractCacheData { pub(crate) legacy_mixnodes_blacklist: Cache<HashSet<NodeId>>, pub(crate) legacy_gateways_blacklist: Cache<HashSet<NodeId>>, - pub(crate) config_score_params: Cache<Option<ConfigScoreParams>>, + pub(crate) config_score_data: Cache<Option<ConfigScoreData>>, pub(crate) current_reward_params: Cache<Option<RewardingParams>>, pub(crate) current_interval: Cache<Option<Interval>>, @@ -143,7 +177,7 @@ impl ContractCacheData { current_interval: Cache::default(), current_reward_params: Cache::default(), contracts_info: Cache::default(), - config_score_params: Default::default(), + config_score_data: Default::default(), } } } diff --git a/nym-api/src/nym_contract_cache/cache/mod.rs b/nym-api/src/nym_contract_cache/cache/mod.rs index eeb9b2a7c28..1738901550e 100644 --- a/nym-api/src/nym_contract_cache/cache/mod.rs +++ b/nym-api/src/nym_contract_cache/cache/mod.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::node_describe_cache::RefreshData; -use crate::nym_contract_cache::cache::data::CachedContractsInfo; +use crate::nym_contract_cache::cache::data::{CachedContractsInfo, ConfigScoreData}; use crate::support::caching::Cache; use data::ContractCacheData; use nym_api_requests::legacy::{ @@ -11,7 +11,8 @@ use nym_api_requests::legacy::{ use nym_api_requests::models::MixnodeStatus; use nym_crypto::asymmetric::ed25519; use nym_mixnet_contract_common::{ - ConfigScoreParams, Interval, NodeId, NymNodeDetails, RewardedSet, RewardingParams, + ConfigScoreParams, HistoricalNymNodeVersionEntry, Interval, NodeId, NymNodeDetails, + RewardedSet, RewardingParams, }; use std::{ collections::HashSet, @@ -25,7 +26,7 @@ use tokio::sync::{RwLock, RwLockReadGuard}; use tokio::time; use tracing::{debug, error}; -mod data; +pub(crate) mod data; pub(crate) mod refresher; pub(crate) use self::data::CachedRewardedSet; @@ -81,19 +82,23 @@ impl NymContractCache { nym_nodes: Vec<NymNodeDetails>, rewarded_set: RewardedSet, config_score_params: ConfigScoreParams, + nym_node_version_history: Vec<HistoricalNymNodeVersionEntry>, rewarding_params: RewardingParams, current_interval: Interval, nym_contracts_info: CachedContractsInfo, ) { match time::timeout(Duration::from_millis(100), self.inner.write()).await { Ok(mut cache) => { + let config_score_data = ConfigScoreData { + config_score_params, + nym_node_version_history, + }; + cache.legacy_mixnodes.unchecked_update(mixnodes); cache.legacy_gateways.unchecked_update(gateways); cache.nym_nodes.unchecked_update(nym_nodes); cache.rewarded_set.unchecked_update(rewarded_set); - cache - .config_score_params - .unchecked_update(config_score_params); + cache.config_score_data.unchecked_update(config_score_data); cache .current_reward_params .unchecked_update(Some(rewarding_params)); @@ -219,14 +224,6 @@ impl NymContractCache { .into_inner() } - pub async fn legacy_mixnodes_filtered_basic(&self) -> Vec<LegacyMixNodeBondWithLayer> { - self.legacy_mixnodes_filtered() - .await - .into_iter() - .map(|bond| bond.bond_information) - .collect() - } - pub async fn legacy_mixnodes_all_basic(&self) -> Vec<LegacyMixNodeBondWithLayer> { self.legacy_mixnodes_all() .await @@ -277,8 +274,12 @@ impl NymContractCache { .unwrap_or_default() } - pub async fn config_score_params(&self) -> Cache<Option<ConfigScoreParams>> { - self.get_owned(|cache| cache.config_score_params.clone_cache()) + pub async fn maybe_config_score_data_owned(&self) -> Option<Cache<ConfigScoreData>> { + self.config_score_data_owned().await.transpose() + } + + pub async fn config_score_data_owned(&self) -> Cache<Option<ConfigScoreData>> { + self.get_owned(|cache| cache.config_score_data.clone_cache()) .await .unwrap_or_default() } diff --git a/nym-api/src/nym_contract_cache/cache/refresher.rs b/nym-api/src/nym_contract_cache/cache/refresher.rs index 64d6672517f..6681ac669e7 100644 --- a/nym-api/src/nym_contract_cache/cache/refresher.rs +++ b/nym-api/src/nym_contract_cache/cache/refresher.rs @@ -178,6 +178,7 @@ impl NymContractCacheRefresher { } let config_score_params = self.nyxd_client.get_config_score_params().await?; + let nym_node_version_history = self.nyxd_client.get_nym_node_version_history().await?; let contract_info = self.get_nym_contracts_info().await?; info!( @@ -194,6 +195,7 @@ impl NymContractCacheRefresher { nym_nodes, rewarded_set, config_score_params, + nym_node_version_history, rewarding_params, current_interval, contract_info, @@ -214,25 +216,6 @@ impl NymContractCacheRefresher { .unwrap_or_default() } - // fn collect_rewarded_and_active_set_details( - // all_mixnodes: &[MixNodeDetails], - // rewarded_set_nodes: RewardedSet, - // ) -> (Vec<MixNodeDetails>, Vec<MixNodeDetails>) { - // let mut active_set = Vec::new(); - // let mut rewarded_set = Vec::new(); - // - // for mix in all_mixnodes { - // if let Some(status) = rewarded_set_nodes.get(&mix.mix_id()) { - // rewarded_set.push(mix.clone()); - // if status.is_active() { - // active_set.push(mix.clone()) - // } - // } - // } - // - // (rewarded_set, active_set) - // } - pub(crate) async fn run(&self, mut shutdown: TaskClient) { let mut interval = time::interval(self.caching_interval); while !shutdown.is_shutdown() { diff --git a/nym-api/src/nym_nodes/handlers/mod.rs b/nym-api/src/nym_nodes/handlers/mod.rs index 42cfee3c86c..a3646e8b50b 100644 --- a/nym-api/src/nym_nodes/handlers/mod.rs +++ b/nym-api/src/nym_nodes/handlers/mod.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::node_status_api::models::{AxumErrorResponse, AxumResult}; +use crate::support::caching::cache::UninitialisedCache; use crate::support::http::helpers::{NodeIdParam, PaginationRequest}; use crate::support::http::state::AppState; use axum::extract::{Path, Query, State}; @@ -9,7 +10,8 @@ use axum::routing::{get, post}; use axum::{Json, Router}; use nym_api_requests::models::{ AnnotationResponse, NodeDatePerformanceResponse, NodePerformanceResponse, NodeRefreshBody, - NoiseDetails, NymNodeDescription, PerformanceHistoryResponse, UptimeHistoryResponse, + NoiseDetails, NymNodeDescription, PerformanceHistoryResponse, RewardedSetResponse, + UptimeHistoryResponse, }; use nym_api_requests::pagination::{PaginatedResponse, Pagination}; use nym_contracts_common::NaiveFloat; @@ -17,6 +19,7 @@ use nym_mixnet_contract_common::reward_params::Performance; use nym_mixnet_contract_common::NymNodeDetails; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +use std::ops::Deref; use std::time::Duration; use time::{Date, OffsetDateTime}; use utoipa::{IntoParams, ToSchema}; @@ -42,6 +45,27 @@ 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)) +} + +#[utoipa::path( + tag = "Nym Nodes", + get, + request_body = NodeRefreshBody, + path = "/rewarded-set", + context_path = "/v1/nym-nodes", + responses( + (status = 200, body = RewardedSetResponse) + ), +)] +async fn rewarded_set(State(state): State<AppState>) -> AxumResult<Json<RewardedSetResponse>> { + let cached_rewarded_set = state + .nym_contract_cache() + .rewarded_set() + .await + .ok_or(UninitialisedCache)?; + + Ok(Json(cached_rewarded_set.deref().deref().into())) } #[utoipa::path( diff --git a/nym-api/src/nym_nodes/handlers/unstable/helpers.rs b/nym-api/src/nym_nodes/handlers/unstable/helpers.rs index 4f7a20155e4..38c981a97a2 100644 --- a/nym-api/src/nym_nodes/handlers/unstable/helpers.rs +++ b/nym-api/src/nym_nodes/handlers/unstable/helpers.rs @@ -5,13 +5,10 @@ use nym_api_requests::models::{ GatewayBondAnnotated, MalformedNodeBond, MixNodeBondAnnotated, OffsetDateTimeJsonSchemaWrapper, }; use nym_api_requests::nym_nodes::{NodeRole, SkimmedNode}; -use nym_bin_common::version_checker; use nym_mixnet_contract_common::reward_params::Performance; use time::OffsetDateTime; pub(crate) trait LegacyAnnotation { - fn version(&self) -> &str; - fn performance(&self) -> Performance; fn identity(&self) -> &str; @@ -20,10 +17,6 @@ pub(crate) trait LegacyAnnotation { } impl LegacyAnnotation for MixNodeBondAnnotated { - fn version(&self) -> &str { - self.version() - } - fn performance(&self) -> Performance { self.node_performance.last_24h } @@ -38,10 +31,6 @@ impl LegacyAnnotation for MixNodeBondAnnotated { } impl LegacyAnnotation for GatewayBondAnnotated { - fn version(&self) -> &str { - self.version() - } - fn performance(&self) -> Performance { self.node_performance.last_24h } @@ -60,12 +49,3 @@ pub(crate) fn refreshed_at( ) -> OffsetDateTimeJsonSchemaWrapper { iter.into_iter().min().unwrap().into() } - -pub(crate) fn semver(requirement: &Option<String>, declared: &str) -> bool { - if let Some(semver_compat) = requirement.as_ref() { - if !version_checker::is_minor_version_compatible(declared, semver_compat) { - return false; - } - } - true -} diff --git a/nym-api/src/nym_nodes/handlers/unstable/mod.rs b/nym-api/src/nym_nodes/handlers/unstable/mod.rs index a9110a599de..88d0338e6f4 100644 --- a/nym-api/src/nym_nodes/handlers/unstable/mod.rs +++ b/nym-api/src/nym_nodes/handlers/unstable/mod.rs @@ -80,6 +80,7 @@ struct NodesParamsWithRole { #[param(inline)] role: Option<NodeRoleQueryParam>, + #[allow(dead_code)] semver_compatibility: Option<String>, no_legacy: Option<bool>, page: Option<u32>, @@ -88,6 +89,7 @@ struct NodesParamsWithRole { #[derive(Debug, Deserialize, utoipa::IntoParams)] struct NodesParams { + #[allow(dead_code)] semver_compatibility: Option<String>, no_legacy: Option<bool>, page: Option<u32>, diff --git a/nym-api/src/nym_nodes/handlers/unstable/skimmed.rs b/nym-api/src/nym_nodes/handlers/unstable/skimmed.rs index a4aba882cfe..fda8d8bad26 100644 --- a/nym-api/src/nym_nodes/handlers/unstable/skimmed.rs +++ b/nym-api/src/nym_nodes/handlers/unstable/skimmed.rs @@ -4,7 +4,7 @@ use crate::node_describe_cache::DescribedNodes; use crate::node_status_api::models::{AxumErrorResponse, AxumResult}; use crate::nym_contract_cache::cache::CachedRewardedSet; -use crate::nym_nodes::handlers::unstable::helpers::{refreshed_at, semver, LegacyAnnotation}; +use crate::nym_nodes::handlers::unstable::helpers::{refreshed_at, LegacyAnnotation}; use crate::nym_nodes::handlers::unstable::{NodesParams, NodesParamsWithRole}; use crate::support::caching::Cache; use crate::support::http::state::AppState; @@ -25,7 +25,6 @@ pub type PaginatedSkimmedNodes = AxumResult<Json<PaginatedCachedNodesResponse<Sk /// Given all relevant caches, build part of response for JUST Nym Nodes fn build_nym_nodes_response<'a, NI>( rewarded_set: &CachedRewardedSet, - required_semver: &Option<String>, nym_nodes_subset: NI, annotations: &HashMap<NodeId, NodeAnnotation>, active_only: bool, @@ -37,11 +36,6 @@ where for nym_node in nym_nodes_subset { let node_id = nym_node.node_id; - // if we have wrong version, ignore - if !semver(required_semver, nym_node.version()) { - continue; - } - let role: NodeRole = rewarded_set.role(node_id).into(); // if the role is inactive, see if our filter allows it @@ -61,7 +55,6 @@ where /// Given all relevant caches, add appropriate legacy nodes to the part of the response fn add_legacy<LN>( nodes: &mut Vec<SkimmedNode>, - required_semver: &Option<String>, rewarded_set: &CachedRewardedSet, describe_cache: &DescribedNodes, annotated_legacy_nodes: &HashMap<NodeId, LN>, @@ -70,11 +63,6 @@ fn add_legacy<LN>( LN: LegacyAnnotation, { for (node_id, legacy) in annotated_legacy_nodes.iter() { - // if we have wrong version, ignore - if !semver(required_semver, legacy.version()) { - continue; - } - let role: NodeRole = rewarded_set.role(*node_id).into(); // if the role is inactive, see if our filter allows it @@ -121,7 +109,6 @@ where // TODO: implement it let _ = query_params.per_page; let _ = query_params.page; - let semver_compatibility = query_params.semver_compatibility; // 1. get the rewarded set let rewarded_set = state.rewarded_set().await?; @@ -134,13 +121,8 @@ where let describe_cache = state.describe_nodes_cache_data().await?; // 4. start building the response - let mut nodes = build_nym_nodes_response( - &rewarded_set, - &semver_compatibility, - nym_nodes_subset, - &annotations, - active_only, - ); + let mut nodes = + build_nym_nodes_response(&rewarded_set, nym_nodes_subset, &annotations, active_only); // 5. if we allow legacy nodes, repeat the procedure for them, otherwise return just nym-nodes if let Some(true) = query_params.no_legacy { @@ -162,7 +144,6 @@ where let annotated_legacy_nodes = annotated_legacy_nodes_getter(state).await?; add_legacy( &mut nodes, - &semver_compatibility, &rewarded_set, &describe_cache, &annotated_legacy_nodes, @@ -239,14 +220,13 @@ pub(super) async fn deprecated_mixnodes_basic( async fn nodes_basic( state: State<AppState>, - Query(query_params): Query<NodesParams>, + Query(_query_params): Query<NodesParams>, active_only: bool, ) -> PaginatedSkimmedNodes { // unfortunately we have to build the response semi-manually here as we need to add two sources of legacy nodes // 1. grab all relevant described nym-nodes let rewarded_set = state.rewarded_set().await?; - let semver_compatibility = &query_params.semver_compatibility; let describe_cache = state.describe_nodes_cache_data().await?; let all_nym_nodes = describe_cache.all_nym_nodes(); @@ -254,18 +234,12 @@ async fn nodes_basic( let legacy_mixnodes = state.legacy_mixnode_annotations().await?; let legacy_gateways = state.legacy_gateways_annotations().await?; - let mut nodes = build_nym_nodes_response( - &rewarded_set, - semver_compatibility, - all_nym_nodes, - &annotations, - active_only, - ); + let mut nodes = + build_nym_nodes_response(&rewarded_set, all_nym_nodes, &annotations, active_only); // add legacy gateways to the response add_legacy( &mut nodes, - semver_compatibility, &rewarded_set, &describe_cache, &legacy_gateways, @@ -275,7 +249,6 @@ async fn nodes_basic( // add legacy mixnodes to the response add_legacy( &mut nodes, - semver_compatibility, &rewarded_set, &describe_cache, &legacy_mixnodes, diff --git a/nym-api/src/status/handlers.rs b/nym-api/src/status/handlers.rs index 7dc316aac25..8b177cadd66 100644 --- a/nym-api/src/status/handlers.rs +++ b/nym-api/src/status/handlers.rs @@ -4,37 +4,20 @@ use crate::node_status_api::models::{AxumErrorResponse, AxumResult}; use crate::status::ApiStatusState; use crate::support::http::state::AppState; +use axum::extract::State; use axum::Json; use axum::Router; use nym_api_requests::models::{ApiHealthResponse, SignerInformationResponse}; use nym_bin_common::build_information::BinaryBuildInformationOwned; use nym_compact_ecash::Base58; -use std::sync::Arc; pub(crate) fn api_status_routes() -> Router<AppState> { - let api_status_state = Arc::new(ApiStatusState::new()); - Router::new() - .route( - "/health", - axum::routing::get({ - let state = Arc::clone(&api_status_state); - || health(state) - }), - ) - .route( - "/build-information", - axum::routing::get({ - let state = Arc::clone(&api_status_state); - || build_information(state) - }), - ) + .route("/health", axum::routing::get(health)) + .route("/build-information", axum::routing::get(build_information)) .route( "/signer-information", - axum::routing::get({ - let state = Arc::clone(&api_status_state); - || signer_information(state) - }), + axum::routing::get(signer_information), ) } @@ -46,7 +29,7 @@ pub(crate) fn api_status_routes() -> Router<AppState> { (status = 200, body = ApiHealthResponse) ) )] -async fn health(state: Arc<ApiStatusState>) -> Json<ApiHealthResponse> { +async fn health(State(state): State<ApiStatusState>) -> Json<ApiHealthResponse> { let uptime = state.startup_time.elapsed(); let health = ApiHealthResponse::new_healthy(uptime); Json(health) @@ -60,7 +43,9 @@ async fn health(state: Arc<ApiStatusState>) -> Json<ApiHealthResponse> { (status = 200, body = BinaryBuildInformationOwned) ) )] -async fn build_information(state: Arc<ApiStatusState>) -> Json<BinaryBuildInformationOwned> { +async fn build_information( + State(state): State<ApiStatusState>, +) -> Json<BinaryBuildInformationOwned> { Json(state.build_information.to_owned()) } @@ -73,7 +58,7 @@ async fn build_information(state: Arc<ApiStatusState>) -> Json<BinaryBuildInform ) )] async fn signer_information( - state: Arc<ApiStatusState>, + State(state): State<ApiStatusState>, ) -> AxumResult<Json<SignerInformationResponse>> { let signer_state = state.signer_information.as_ref().ok_or_else(|| { AxumErrorResponse::internal_msg("this api does not expose zk-nym signing functionalities") diff --git a/nym-api/src/status/mod.rs b/nym-api/src/status/mod.rs index 0ed94a18196..f7ff611d6d3 100644 --- a/nym-api/src/status/mod.rs +++ b/nym-api/src/status/mod.rs @@ -4,12 +4,25 @@ use crate::ecash; use nym_bin_common::bin_info; use nym_bin_common::build_information::BinaryBuildInformation; - +use std::ops::Deref; +use std::sync::Arc; use tokio::time::Instant; pub(crate) mod handlers; +#[derive(Clone)] pub(crate) struct ApiStatusState { + inner: Arc<ApiStatusStateInner>, +} + +impl Deref for ApiStatusState { + type Target = ApiStatusStateInner; + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +pub(crate) struct ApiStatusStateInner { startup_time: Instant, build_information: BinaryBuildInformation, signer_information: Option<SignerState>, @@ -27,15 +40,13 @@ pub(crate) struct SignerState { } impl ApiStatusState { - pub fn new() -> Self { + pub fn new(signer_information: Option<SignerState>) -> Self { ApiStatusState { - startup_time: Instant::now(), - build_information: bin_info!(), - signer_information: None, + inner: Arc::new(ApiStatusStateInner { + startup_time: Instant::now(), + build_information: bin_info!(), + signer_information, + }), } } - - pub fn add_zk_nym_signer(&mut self, signer_information: SignerState) { - self.signer_information = Some(signer_information) - } } diff --git a/nym-api/src/support/caching/cache.rs b/nym-api/src/support/caching/cache.rs index 405f6022a3c..da746399f54 100644 --- a/nym-api/src/support/caching/cache.rs +++ b/nym-api/src/support/caching/cache.rs @@ -103,6 +103,15 @@ pub struct Cache<T> { as_at: OffsetDateTime, } +impl<T> Cache<Option<T>> { + pub(crate) fn transpose(self) -> Option<Cache<T>> { + self.value.map(|value| Cache { + value, + as_at: self.as_at, + }) + } +} + impl<T> Cache<T> { // ugh. I hate to expose it, but it'd have broken pre-existing code pub(crate) fn new(value: T) -> Self { diff --git a/nym-api/src/support/cli/init.rs b/nym-api/src/support/cli/init.rs index 71efedbea40..d93e90545c0 100644 --- a/nym-api/src/support/cli/init.rs +++ b/nym-api/src/support/cli/init.rs @@ -68,6 +68,9 @@ pub(crate) struct Args { /// default: `127.0.0.1:8080` in `debug` builds and `0.0.0.0:8080` in `release` #[clap(long)] pub(crate) bind_address: Option<SocketAddr>, + + #[clap(hide = true, long, default_value_t = false)] + pub(crate) allow_illegal_ips: bool, // #[clap(short, long, default_value_t = OutputFormat::default())] // output: OutputFormat, } diff --git a/nym-api/src/support/cli/run.rs b/nym-api/src/support/cli/run.rs index e7bf4c07762..fa229d6425b 100644 --- a/nym-api/src/support/cli/run.rs +++ b/nym-api/src/support/cli/run.rs @@ -2,7 +2,6 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::circulating_supply_api::cache::CirculatingSupplyCache; -use crate::ecash::api_routes::handlers::ecash_routes; use crate::ecash::client::Client; use crate::ecash::comm::QueryCommunicationChannel; use crate::ecash::dkg::controller::keys::{ @@ -101,6 +100,9 @@ pub(crate) struct Args { /// default: `127.0.0.1:8080` in `debug` builds and `0.0.0.0:8080` in `release` #[clap(long)] pub(crate) bind_address: Option<SocketAddr>, + + #[clap(hide = true, long, default_value_t = false)] + pub(crate) allow_illegal_ips: bool, } async fn start_nym_api_tasks_axum(config: &Config) -> anyhow::Result<ShutdownHandles> { @@ -138,8 +140,6 @@ async fn start_nym_api_tasks_axum(config: &Config) -> anyhow::Result<ShutdownHan let described_nodes_cache = SharedCache::<DescribedNodes>::new(); let node_info_cache = unstable::NodeInfoCache::default(); - let mut status_state = ApiStatusState::new(); - let ecash_contract = nyxd_client .get_ecash_contract_address() .await @@ -161,8 +161,8 @@ async fn start_nym_api_tasks_axum(config: &Config) -> anyhow::Result<ShutdownHan // if ecash signer is enabled, there are additional constraints on the nym-api, // such as having sufficient token balance - let router = if config.ecash_signer.enabled { - let cosmos_address = nyxd_client.address().await; + let signer_information = if config.ecash_signer.enabled { + let cosmos_address = nyxd_client.address().await?; // make sure we have some tokens to cover multisig fees let balance = nyxd_client.balance(&mix_denom).await?; @@ -177,16 +177,14 @@ async fn start_nym_api_tasks_axum(config: &Config) -> anyhow::Result<ShutdownHan .clone() .map(|u| u.to_string()) .unwrap_or_default(); - status_state.add_zk_nym_signer(SignerState { + Some(SignerState { cosmos_address: cosmos_address.to_string(), identity: encoded_identity, announce_address, ecash_keypair: ecash_keypair_wrapper.clone(), - }); - - router.nest("/v1/ecash", ecash_routes(Arc::new(ecash_state))) + }) } else { - router + None }; let router = router.with_state(AppState { @@ -200,6 +198,8 @@ async fn start_nym_api_tasks_axum(config: &Config) -> anyhow::Result<ShutdownHan described_nodes_cache: described_nodes_cache.clone(), network_details, node_info_cache, + api_status: ApiStatusState::new(signer_information), + ecash_state: Arc::new(ecash_state), }); let task_manager = TaskManager::new(TASK_MANAGER_TIMEOUT_S); @@ -259,9 +259,10 @@ async fn start_nym_api_tasks_axum(config: &Config) -> anyhow::Result<ShutdownHan // if the monitoring is enabled if config.network_monitor.enabled { network_monitor::start::<SphinxMessageReceiver>( - &config.network_monitor, + config, &nym_contract_cache_state, described_nodes_cache.clone(), + node_status_cache_state.clone(), &storage, nyxd_client.clone(), &task_manager, diff --git a/nym-api/src/support/config/mod.rs b/nym-api/src/support/config/mod.rs index 5a07c223bee..19fb92a7f45 100644 --- a/nym-api/src/support/config/mod.rs +++ b/nym-api/src/support/config/mod.rs @@ -38,13 +38,12 @@ const DEFAULT_GATEWAY_SENDING_RATE: usize = 200; const DEFAULT_MAX_CONCURRENT_GATEWAY_CLIENTS: usize = 50; const DEFAULT_PACKET_DELIVERY_TIMEOUT: Duration = Duration::from_secs(20); const DEFAULT_MONITOR_RUN_INTERVAL: Duration = Duration::from_secs(15 * 60); -const DEFAULT_GATEWAY_PING_INTERVAL: Duration = Duration::from_secs(60); // Set this to a high value for now, so that we don't risk sporadic timeouts that might cause // bought bandwidth tokens to not have time to be spent; Once we remove the gateway from the // bandwidth bridging protocol, we can come back to a smaller timeout value const DEFAULT_GATEWAY_RESPONSE_TIMEOUT: Duration = Duration::from_secs(5 * 60); -// This timeout value should be big enough to accommodate an initial bandwidth acquirement -const DEFAULT_GATEWAY_CONNECTION_TIMEOUT: Duration = Duration::from_secs(2 * 60); +const DEFAULT_GATEWAY_CONNECTION_TIMEOUT: Duration = Duration::from_secs(15); +const DEFAULT_GATEWAY_BANDWIDTH_CLAIM_TIMEOUT: Duration = Duration::from_secs(2 * 60); const DEFAULT_TEST_ROUTES: usize = 3; const DEFAULT_MINIMUM_TEST_ROUTES: usize = 1; @@ -175,6 +174,9 @@ impl Config { if let Some(http_bind_address) = args.bind_address { self.base.bind_address = http_bind_address } + if args.allow_illegal_ips { + self.topology_cacher.debug.node_describe_allow_illegal_ips = true + } self } @@ -323,11 +325,6 @@ pub struct NetworkMonitorDebug { #[serde(with = "humantime_serde")] pub run_interval: Duration, - /// Specifies interval at which we should be sending ping packets to all active gateways - /// in order to keep the websocket connections alive. - #[serde(with = "humantime_serde")] - pub gateway_ping_interval: Duration, - /// Specifies maximum rate (in packets per second) of test packets being sent to gateway pub gateway_sending_rate: usize, @@ -343,6 +340,10 @@ pub struct NetworkMonitorDebug { #[serde(with = "humantime_serde")] pub gateway_connection_timeout: Duration, + /// Maximum allowed time for the gateway bandwidth claim to get resolved + #[serde(with = "humantime_serde")] + pub gateway_bandwidth_claim_timeout: Duration, + /// Specifies the duration the monitor is going to wait after sending all measurement /// packets before declaring nodes unreachable. #[serde(with = "humantime_serde")] @@ -370,11 +371,11 @@ impl Default for NetworkMonitorDebug { min_gateway_reliability: DEFAULT_MIN_GATEWAY_RELIABILITY, disabled_credentials_mode: true, run_interval: DEFAULT_MONITOR_RUN_INTERVAL, - gateway_ping_interval: DEFAULT_GATEWAY_PING_INTERVAL, gateway_sending_rate: DEFAULT_GATEWAY_SENDING_RATE, max_concurrent_gateway_clients: DEFAULT_MAX_CONCURRENT_GATEWAY_CLIENTS, gateway_response_timeout: DEFAULT_GATEWAY_RESPONSE_TIMEOUT, gateway_connection_timeout: DEFAULT_GATEWAY_CONNECTION_TIMEOUT, + gateway_bandwidth_claim_timeout: DEFAULT_GATEWAY_BANDWIDTH_CLAIM_TIMEOUT, packet_delivery_timeout: DEFAULT_PACKET_DELIVERY_TIMEOUT, test_routes: DEFAULT_TEST_ROUTES, minimum_test_routes: DEFAULT_MINIMUM_TEST_ROUTES, diff --git a/nym-api/src/support/config/override.rs b/nym-api/src/support/config/override.rs index 059d0f81364..a2f6d9c76c5 100644 --- a/nym-api/src/support/config/override.rs +++ b/nym-api/src/support/config/override.rs @@ -31,6 +31,8 @@ pub(crate) struct OverrideConfig { /// Socket address this api will use for binding its http API. /// default: `127.0.0.1:8080` in `debug` builds and `0.0.0.0:8080` in `release` pub(crate) bind_address: Option<SocketAddr>, + + pub(crate) allow_illegal_ips: bool, } impl From<init::Args> for OverrideConfig { @@ -44,6 +46,7 @@ impl From<init::Args> for OverrideConfig { announce_address: args.announce_address, monitor_credentials_mode: Some(args.monitor_credentials_mode), bind_address: args.bind_address, + allow_illegal_ips: args.allow_illegal_ips, } } } @@ -59,6 +62,7 @@ impl From<run::Args> for OverrideConfig { announce_address: args.announce_address, monitor_credentials_mode: args.monitor_credentials_mode, bind_address: args.bind_address, + allow_illegal_ips: args.allow_illegal_ips, } } } diff --git a/nym-api/src/support/http/openapi.rs b/nym-api/src/support/http/openapi.rs index 876bf35cb78..99713e16507 100644 --- a/nym-api/src/support/http/openapi.rs +++ b/nym-api/src/support/http/openapi.rs @@ -17,6 +17,11 @@ use utoipauto::utoipauto; #[derive(OpenApi)] #[openapi( info(title = "Nym API"), + servers( + (url = "/api", description = "Main Nym Api Server"), + (url = "/", description = "Auxiliary Nym Api Instances"), + (url = "/", description = "Local Development Server") + ), tags(), components(schemas( models::CirculatingSupplyResponse, diff --git a/nym-api/src/support/http/router.rs b/nym-api/src/support/http/router.rs index 38eec8d76e9..4c291fbc587 100644 --- a/nym-api/src/support/http/router.rs +++ b/nym-api/src/support/http/router.rs @@ -2,8 +2,9 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::circulating_supply_api::handlers::circulating_supply_routes; +use crate::ecash::api_routes::handlers::ecash_routes; use crate::network::handlers::nym_network_routes; -use crate::node_status_api::handlers::node_status_routes; +use crate::node_status_api::handlers::status_routes; use crate::nym_contract_cache::handlers::nym_contract_cache_routes; use crate::nym_nodes::handlers::legacy::legacy_nym_node_routes; use crate::nym_nodes::handlers::nym_node_routes; @@ -16,7 +17,7 @@ use axum::response::Redirect; use axum::routing::get; use axum::Router; use core::net::SocketAddr; -use nym_http_api_common::logging::logger; +use nym_http_api_common::middleware::logging::logger; use tokio::net::TcpListener; use tokio_util::sync::WaitForCancellationFutureOwned; use tower_http::cors::CorsLayer; @@ -58,10 +59,11 @@ impl RouterBuilder { .merge(nym_contract_cache_routes()) .merge(legacy_nym_node_routes()) .nest("/circulating-supply", circulating_supply_routes()) - .nest("/status", node_status_routes(network_monitor)) + .nest("/status", status_routes(network_monitor)) .nest("/network", nym_network_routes()) .nest("/api-status", status::handlers::api_status_routes()) .nest("/nym-nodes", nym_node_routes()) + .nest("/ecash", ecash_routes()) .nest("/unstable", unstable_routes()), // CORS layer needs to be "outside" of routes ); @@ -70,6 +72,7 @@ impl RouterBuilder { } } + #[allow(dead_code)] pub(crate) fn nest(self, path: &str, router: Router<AppState>) -> Self { Self { unfinished_router: self.unfinished_router.nest(path, router), diff --git a/nym-api/src/support/http/state.rs b/nym-api/src/support/http/state.rs index f45d25bbd29..6c277441eaa 100644 --- a/nym-api/src/support/http/state.rs +++ b/nym-api/src/support/http/state.rs @@ -2,15 +2,18 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::circulating_supply_api::cache::CirculatingSupplyCache; +use crate::ecash::state::EcashState; use crate::network::models::NetworkDetails; use crate::node_describe_cache::DescribedNodes; use crate::node_status_api::handlers::unstable; use crate::node_status_api::models::AxumErrorResponse; use crate::node_status_api::NodeStatusCache; use crate::nym_contract_cache::cache::{CachedRewardedSet, NymContractCache}; +use crate::status::ApiStatusState; use crate::support::caching::cache::SharedCache; use crate::support::caching::Cache; use crate::support::storage; +use axum::extract::FromRef; use nym_api_requests::models::{GatewayBondAnnotated, MixNodeBondAnnotated, NodeAnnotation}; use nym_mixnet_contract_common::NodeId; use nym_task::TaskManager; @@ -80,6 +83,21 @@ pub(crate) struct AppState { pub(crate) described_nodes_cache: SharedCache<DescribedNodes>, pub(crate) network_details: NetworkDetails, pub(crate) node_info_cache: unstable::NodeInfoCache, + pub(crate) api_status: ApiStatusState, + // todo: refactor it into inner: Arc<EcashStateInner> + pub(crate) ecash_state: Arc<EcashState>, +} + +impl FromRef<AppState> for ApiStatusState { + fn from_ref(app_state: &AppState) -> Self { + app_state.api_status.clone() + } +} + +impl FromRef<AppState> for Arc<EcashState> { + fn from_ref(app_state: &AppState) -> Self { + app_state.ecash_state.clone() + } } #[derive(Clone)] diff --git a/nym-api/src/support/nyxd/mod.rs b/nym-api/src/support/nyxd/mod.rs index 012f9d369da..cb97f758340 100644 --- a/nym-api/src/support/nyxd/mod.rs +++ b/nym-api/src/support/nyxd/mod.rs @@ -29,8 +29,8 @@ use nym_mixnet_contract_common::mixnode::MixNodeDetails; use nym_mixnet_contract_common::nym_node::Role; use nym_mixnet_contract_common::reward_params::RewardingParams; use nym_mixnet_contract_common::{ - ConfigScoreParams, CurrentIntervalResponse, EpochStatus, ExecuteMsg, GatewayBond, IdentityKey, - NymNodeDetails, RewardedSet, RoleAssignment, + ConfigScoreParams, CurrentIntervalResponse, EpochStatus, ExecuteMsg, GatewayBond, + HistoricalNymNodeVersionEntry, IdentityKey, NymNodeDetails, RewardedSet, RoleAssignment, }; use nym_validator_client::coconut::EcashApiError; use nym_validator_client::nyxd::contract_traits::mixnet_query_client::MixnetQueryClientExt; @@ -77,16 +77,6 @@ macro_rules! nyxd_query { }}; } -macro_rules! nyxd_signing_shared { - ($self:expr, $($op:tt)*) => {{ - let guard = $self.inner.read().await; - match &*guard { - $crate::support::nyxd::ClientInner::Signing(client) => client.$($op)*, - $crate::support::nyxd::ClientInner::Query(_) => panic!("attempted to use a signing method on a query client"), - } - }}; -} - macro_rules! nyxd_signing { ($self:expr, $($op:tt)*) => {{ let guard = $self.inner.write().await; @@ -140,13 +130,19 @@ impl Client { self.inner.read().await } - pub(crate) async fn client_address(&self) -> AccountId { - nyxd_signing_shared!(self, address()) + pub(crate) async fn client_address(&self) -> Option<AccountId> { + let guard = self.inner.read().await; + match &*guard { + ClientInner::Signing(client) => Some(client.address()), + ClientInner::Query(_) => None, + } } pub(crate) async fn balance<S: Into<String>>(&self, denom: S) -> Result<Coin, NyxdError> { - let address = self.client_address().await; let denom = denom.into(); + let Some(address) = self.client_address().await else { + return Ok(Coin::new(0, denom)); + }; let balance = nyxd_query!(self, get_balance(&address, denom.clone()).await?); match balance { @@ -236,6 +232,12 @@ impl Client { .map(|state| state.config_score_params) } + pub(crate) async fn get_nym_node_version_history( + &self, + ) -> Result<Vec<HistoricalNymNodeVersionEntry>, NyxdError> { + nyxd_query!(self, get_full_nym_node_version_history().await) + } + pub(crate) async fn get_current_interval(&self) -> Result<CurrentIntervalResponse, NyxdError> { nyxd_query!(self, get_current_interval_details().await) } @@ -394,8 +396,10 @@ impl Client { #[async_trait] impl crate::ecash::client::Client for Client { - async fn address(&self) -> AccountId { - self.client_address().await + async fn address(&self) -> Result<AccountId, EcashError> { + self.client_address() + .await + .ok_or(EcashError::ChainSignerNotEnabled) } async fn dkg_contract_address(&self) -> Result<AccountId, EcashError> { @@ -481,7 +485,7 @@ impl crate::ecash::client::Client for Client { async fn get_self_registered_dealer_details( &self, ) -> crate::ecash::error::Result<DealerDetailsResponse> { - let self_address = &self.address().await; + let self_address = &self.address().await?; Ok(nyxd_query!(self, get_dealer_details(self_address).await?)) } diff --git a/nym-api/src/support/storage/manager.rs b/nym-api/src/support/storage/manager.rs index f34ddb4045c..e30b316c0d9 100644 --- a/nym-api/src/support/storage/manager.rs +++ b/nym-api/src/support/storage/manager.rs @@ -4,8 +4,9 @@ use crate::node_status_api::models::{HistoricalUptime as ApiHistoricalUptime, Uptime}; use crate::node_status_api::utils::{ActiveGatewayStatuses, ActiveMixnodeStatuses}; use crate::support::storage::models::{ - ActiveGateway, ActiveMixnode, GatewayDetails, HistoricalUptime, MixnodeDetails, NodeStatus, - RewardingReport, TestedGatewayStatus, TestedMixnodeStatus, TestingRoute, + ActiveGateway, ActiveMixnode, GatewayDetails, HistoricalUptime, MixnodeDetails, + MonitorRunReport, MonitorRunScore, NodeStatus, RewardingReport, TestedGatewayStatus, + TestedMixnodeStatus, TestingRoute, }; use crate::support::storage::DbIdCache; use nym_mixnet_contract_common::{EpochId, IdentityKey, NodeId}; @@ -883,6 +884,78 @@ impl StorageManager { Ok(res.last_insert_rowid()) } + pub(super) async fn insert_monitor_run_report( + &self, + monitor_run_id: i64, + network_reliability: f64, + total_packets_sent: u32, + total_packets_received: u32, + ) -> Result<(), sqlx::Error> { + sqlx::query!( + r#" + INSERT INTO monitor_run_report( + monitor_run_id, + network_reliability, + packets_sent, + packets_received + ) VALUES (?, ?, ?, ?) + "#, + monitor_run_id, + network_reliability, + total_packets_sent, + total_packets_received + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + pub(super) async fn get_monitor_run_report( + &self, + monitor_run_id: i64, + ) -> Result<Option<MonitorRunReport>, sqlx::Error> { + sqlx::query_as("SELECT * FROM monitor_run_report WHERE monitor_run_id = ?") + .bind(monitor_run_id) + .fetch_optional(&self.connection_pool) + .await + } + + pub(super) async fn get_latest_monitor_run_id(&self) -> Result<Option<i64>, sqlx::Error> { + sqlx::query!("SELECT id from monitor_run ORDER BY id DESC limit 1") + .fetch_optional(&self.connection_pool) + .await + .map(|r| r.map(|r| r.id)) + } + + pub(super) async fn insert_monitor_run_scores( + &self, + scores: Vec<MonitorRunScore>, + ) -> Result<(), sqlx::Error> { + let mut query_builder = sqlx::QueryBuilder::new( + "INSERT INTO monitor_run_score (typ, monitor_run_id, rounded_score, nodes_count) ", + ); + + query_builder.push_values(scores, |mut b, score| { + b.push_bind(score.typ) + .push_bind(score.monitor_run_id) + .push_bind(score.rounded_score) + .push_bind(score.nodes_count); + }); + + query_builder.build().execute(&self.connection_pool).await?; + Ok(()) + } + + pub(super) async fn get_monitor_run_scores( + &self, + monitor_run_id: i64, + ) -> Result<Vec<MonitorRunScore>, sqlx::Error> { + sqlx::query_as("SELECT * FROM monitor_run_score WHERE monitor_run_id = ?") + .bind(monitor_run_id) + .fetch_all(&self.connection_pool) + .await + } + /// Obtains number of network monitor test runs that have occurred within the specified interval. /// /// # Arguments diff --git a/nym-api/src/support/storage/mod.rs b/nym-api/src/support/storage/mod.rs index b89062d09a2..72ff9aea1df 100644 --- a/nym-api/src/support/storage/mod.rs +++ b/nym-api/src/support/storage/mod.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: GPL-3.0-only use self::manager::{AvgGatewayReliability, AvgMixnodeReliability}; +use crate::network_monitor::monitor::summary_producer::TestReport; use crate::network_monitor::test_route::TestRoute; use crate::node_status_api::models::{ GatewayStatusReport, GatewayUptimeHistory, HistoricalUptime as ApiHistoricalUptime, @@ -11,7 +12,8 @@ use crate::node_status_api::{ONE_DAY, ONE_HOUR}; use crate::storage::manager::StorageManager; use crate::storage::models::{NodeStatus, TestingRoute}; use crate::support::storage::models::{ - GatewayDetails, HistoricalUptime, MixnodeDetails, TestedGatewayStatus, TestedMixnodeStatus, + GatewayDetails, HistoricalUptime, MixnodeDetails, MonitorRunReport, MonitorRunScore, + TestedGatewayStatus, TestedMixnodeStatus, }; use dashmap::DashMap; use nym_mixnet_contract_common::NodeId; @@ -730,7 +732,7 @@ impl NymApiStorage { mixnode_results: Vec<NodeResult>, gateway_results: Vec<NodeResult>, test_routes: Vec<TestRoute>, - ) -> Result<(), NymApiStorageError> { + ) -> Result<i64, NymApiStorageError> { info!("Submitting new node results to the database. There are {} mixnode results and {} gateway results", mixnode_results.len(), gateway_results.len()); let now = OffsetDateTime::now_utc().unix_timestamp(); @@ -749,9 +751,63 @@ impl NymApiStorage { self.insert_test_route(monitor_run_id, test_route).await?; } + Ok(monitor_run_id) + } + + pub(crate) async fn insert_monitor_run_report( + &self, + report: TestReport, + monitor_run_id: i64, + ) -> Result<(), NymApiStorageError> { + self.manager + .insert_monitor_run_report( + monitor_run_id, + report.network_reliability, + report.total_sent as u32, + report.total_received as u32, + ) + .await?; + + let mut scores = Vec::new(); + for (score, count) in report.mixnode_results { + scores.push(MonitorRunScore { + typ: "mixnode".to_string(), + monitor_run_id, + rounded_score: score, + nodes_count: count as u32, + }) + } + for (score, count) in report.gateway_results { + scores.push(MonitorRunScore { + typ: "gateway".to_string(), + monitor_run_id, + rounded_score: score, + nodes_count: count as u32, + }) + } + + self.manager.insert_monitor_run_scores(scores).await?; + Ok(()) } + pub(crate) async fn get_monitor_run_report( + &self, + monitor_run_id: i64, + ) -> Result<Option<(MonitorRunReport, Vec<MonitorRunScore>)>, NymApiStorageError> { + let Some(report) = self.manager.get_monitor_run_report(monitor_run_id).await? else { + return Ok(None); + }; + let scores = self.manager.get_monitor_run_scores(monitor_run_id).await?; + Ok(Some((report, scores))) + } + + pub(crate) async fn get_latest_monitor_run_id( + &self, + ) -> Result<Option<i64>, NymApiStorageError> { + Ok(self.manager.get_latest_monitor_run_id().await?) + } + pub(crate) async fn submit_mixnode_statuses_v2( &self, mixnode_results: &[NodeResult], diff --git a/nym-api/src/support/storage/models.rs b/nym-api/src/support/storage/models.rs index 1418cab9f87..e082961b01a 100644 --- a/nym-api/src/support/storage/models.rs +++ b/nym-api/src/support/storage/models.rs @@ -6,6 +6,23 @@ use nym_mixnet_contract_common::NodeId; use sqlx::FromRow; use time::Date; +#[derive(sqlx::FromRow, Debug, Clone, Copy)] +pub(crate) struct MonitorRunReport { + #[allow(dead_code)] + pub(crate) monitor_run_id: i64, + pub(crate) network_reliability: f64, + pub(crate) packets_sent: i64, + pub(crate) packets_received: i64, +} + +#[derive(sqlx::FromRow, Debug, Clone)] +pub(crate) struct MonitorRunScore { + pub(crate) typ: String, + pub(crate) monitor_run_id: i64, + pub(crate) rounded_score: u8, + pub(crate) nodes_count: u32, +} + // Internally used struct to catch results from the database to calculate uptimes for given mixnode/gateway pub(crate) struct NodeStatus { pub timestamp: Option<i64>, diff --git a/nym-credential-proxy/nym-credential-proxy-requests/Cargo.toml b/nym-credential-proxy/nym-credential-proxy-requests/Cargo.toml index 86e281eb8a6..ed887405d24 100644 --- a/nym-credential-proxy/nym-credential-proxy-requests/Cargo.toml +++ b/nym-credential-proxy/nym-credential-proxy-requests/Cargo.toml @@ -34,7 +34,6 @@ nym-serde-helpers = { path = "../../common/serde-helpers", features = ["bs58"] } workspace = true features = ["tokio"] - [features] default = ["query-types"] query-types = ["nym-http-api-common"] diff --git a/nym-credential-proxy/nym-credential-proxy-requests/src/api/v1/ticketbook/models.rs b/nym-credential-proxy/nym-credential-proxy-requests/src/api/v1/ticketbook/models.rs index 1d88b28d004..81082ea154d 100644 --- a/nym-credential-proxy/nym-credential-proxy-requests/src/api/v1/ticketbook/models.rs +++ b/nym-credential-proxy/nym-credential-proxy-requests/src/api/v1/ticketbook/models.rs @@ -268,6 +268,9 @@ pub struct WebhookTicketbookWalletSharesRequest { pub struct TicketbookObtainQueryParams { pub output: Option<Output>, + #[serde(default)] + pub skip_webhook: bool, + pub include_master_verification_key: bool, pub include_coin_index_signatures: bool, diff --git a/nym-credential-proxy/nym-credential-proxy/Cargo.toml b/nym-credential-proxy/nym-credential-proxy/Cargo.toml index d7a789b83e4..8069d51493e 100644 --- a/nym-credential-proxy/nym-credential-proxy/Cargo.toml +++ b/nym-credential-proxy/nym-credential-proxy/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nym-credential-proxy" -version = "0.1.3" +version = "0.1.6" authors.workspace = true repository.workspace = true homepage.workspace = true @@ -48,6 +48,7 @@ nym-config = { path = "../../common/config" } nym-crypto = { path = "../../common/crypto", features = ["asymmetric", "rand", "serde"] } nym-credentials = { path = "../../common/credentials" } nym-credentials-interface = { path = "../../common/credentials-interface" } +nym-ecash-contract-common = { path = "../../common/cosmwasm-smart-contracts/ecash-contract" } nym-http-api-common = { path = "../../common/http-api-common", features = ["utoipa"] } nym-validator-client = { path = "../../common/client-libs/validator-client" } nym-network-defaults = { path = "../../common/network-defaults" } diff --git a/nym-credential-proxy/nym-credential-proxy/Dockerfile b/nym-credential-proxy/nym-credential-proxy/Dockerfile index e4548e89289..b6da10f4f53 100644 --- a/nym-credential-proxy/nym-credential-proxy/Dockerfile +++ b/nym-credential-proxy/nym-credential-proxy/Dockerfile @@ -30,6 +30,6 @@ RUN apt update && apt install -yy curl ca-certificates WORKDIR /nym -COPY --from=builder /usr/src/nym/nym-credential-proxy/target/release/nym-credential-proxy ./ +COPY --from=builder /usr/src/nym/target/release/nym-credential-proxy ./ ENTRYPOINT [ "/nym/nym-credential-proxy" ] diff --git a/nym-credential-proxy/nym-credential-proxy/src/cli.rs b/nym-credential-proxy/nym-credential-proxy/src/cli.rs index 3ade5649b05..cf5b25cf230 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/cli.rs +++ b/nym-credential-proxy/nym-credential-proxy/src/cli.rs @@ -55,6 +55,15 @@ pub struct Cli { )] pub(crate) http_auth_token: String, + /// Specify the maximum number of deposits the credential proxy can make in a single transaction + /// (default: 32) + #[clap( + long, + env = "NYM_CREDENTIAL_PROXY_MAX_CONCURRENT_DEPOSITS", + default_value_t = 32 + )] + pub(crate) max_concurrent_deposits: usize, + #[clap(long, env = "NYM_CREDENTIAL_PROXY_PERSISTENT_STORAGE_STORAGE")] pub(crate) persistent_storage_path: Option<PathBuf>, } diff --git a/nym-credential-proxy/nym-credential-proxy/src/credentials/ticketbook/mod.rs b/nym-credential-proxy/nym-credential-proxy/src/credentials/ticketbook/mod.rs index 3a91724a4fc..5e2c858b26b 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/credentials/ticketbook/mod.rs +++ b/nym-credential-proxy/nym-credential-proxy/src/credentials/ticketbook/mod.rs @@ -1,6 +1,7 @@ // Copyright 2024 Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: GPL-3.0-only +use crate::deposit_maker::{DepositRequest, DepositResponse}; use crate::error::VpnApiError; use crate::http::state::ApiState; use crate::storage::models::BlindedShares; @@ -14,21 +15,48 @@ use nym_credentials::IssuanceTicketBook; use nym_credentials_interface::Base58; use nym_crypto::asymmetric::ed25519; use nym_validator_client::ecash::BlindSignRequestBody; -use nym_validator_client::nyxd::contract_traits::EcashSigningClient; -use nym_validator_client::nyxd::cosmwasm_client::ToSingletonContractData; +use nym_validator_client::nyxd::Coin; use rand::rngs::OsRng; use std::collections::HashMap; use std::sync::Arc; use std::time::Duration; use time::OffsetDateTime; -use tokio::sync::Mutex; -use tokio::time::timeout; -use tracing::{debug, error, info, instrument}; +use tokio::sync::{oneshot, Mutex}; +use tokio::time::{timeout, Instant}; +use tracing::{debug, error, info, instrument, warn}; use uuid::Uuid; // use the same type alias as our contract without importing the whole thing just for this single line pub type NodeId = u64; +#[instrument(skip(state), ret, err(Display))] +async fn make_deposit( + state: &ApiState, + pub_key: ed25519::PublicKey, + deposit_amount: &Coin, +) -> Result<DepositResponse, VpnApiError> { + let start = Instant::now(); + let (on_done_tx, on_done_rx) = oneshot::channel(); + let request = DepositRequest::new(pub_key, deposit_amount, on_done_tx); + state.request_deposit(request).await; + + let time_taken = start.elapsed(); + let formatted = humantime::format_duration(time_taken); + + let Ok(deposit_response) = on_done_rx.await else { + error!("failed to receive deposit response: the corresponding sender channel got dropped by the DepositMaker!"); + return Err(VpnApiError::DepositFailure); + }; + + if time_taken > Duration::from_secs(20) { + warn!("attempting to resolve deposit request took {formatted}. perhaps the buffer is too small or the process/chain is overloaded?") + } else { + debug!("attempting to resolve deposit request took {formatted}") + } + + deposit_response.ok_or(VpnApiError::DepositFailure) +} + #[instrument( skip(state, request_data, request, requested_on), fields( @@ -59,25 +87,12 @@ pub(crate) async fn try_obtain_wallet_shares( .await?; let ecash_api_clients = state.ecash_clients(epoch).await?.clone(); - let chain_write_permit = state.start_chain_tx().await; - - info!("starting the deposit!"); - // TODO: batch those up - // TODO: batch those up - let deposit_res = chain_write_permit - .make_ticketbook_deposit( - ed25519_keypair.public_key().to_base58_string(), - deposit_amount.clone(), - None, - ) - .await?; - - // explicitly drop it here so other tasks could start using it - drop(chain_write_permit); + let DepositResponse { + deposit_id, + tx_hash, + } = make_deposit(state, *ed25519_keypair.public_key(), &deposit_amount).await?; - let deposit_id = deposit_res.parse_singleton_u32_contract_data()?; - let tx_hash = deposit_res.transaction_hash; - info!(deposit_id = %deposit_id, tx_hash = %tx_hash, "deposit finished"); + info!(deposit_id = %deposit_id, "deposit finished"); // store the deposit information so if we fail, we could perhaps still reuse it for another issuance state @@ -342,6 +357,7 @@ pub(crate) async fn try_obtain_blinded_ticketbook_async( params: TicketbookObtainQueryParams, pending: BlindedShares, ) { + let skip_webhook = params.skip_webhook; if let Err(err) = try_obtain_blinded_ticketbook_async_inner( &state, request, @@ -352,6 +368,11 @@ pub(crate) async fn try_obtain_blinded_ticketbook_async( ) .await { + if skip_webhook { + info!(uuid = %request,"the webhook is not going to be called for this request"); + return; + } + // post to the webhook to notify of errors on this side if let Err(webhook_err) = try_trigger_webhook_request_for_error( &state, diff --git a/nym-credential-proxy/nym-credential-proxy/src/deposit_maker.rs b/nym-credential-proxy/nym-credential-proxy/src/deposit_maker.rs new file mode 100644 index 00000000000..2ec685d656a --- /dev/null +++ b/nym-credential-proxy/nym-credential-proxy/src/deposit_maker.rs @@ -0,0 +1,205 @@ +// Copyright 2024 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: GPL-3.0-only + +use crate::error::VpnApiError; +use crate::http::state::ChainClient; +use nym_crypto::asymmetric::ed25519; +use nym_ecash_contract_common::deposit::DepositId; +use nym_validator_client::nyxd::cosmwasm_client::ContractResponseData; +use nym_validator_client::nyxd::{Coin, Hash}; +use tokio::sync::{mpsc, oneshot}; +use tokio_util::sync::CancellationToken; +use tracing::{debug, error, info, warn}; + +#[derive(Debug)] +pub(crate) struct DepositResponse { + pub tx_hash: Hash, + pub deposit_id: DepositId, +} + +pub(crate) struct DepositRequest { + pubkey: ed25519::PublicKey, + deposit_amount: Coin, + on_done: oneshot::Sender<Option<DepositResponse>>, +} + +impl DepositRequest { + pub(crate) fn new( + pubkey: ed25519::PublicKey, + deposit_amount: &Coin, + on_done: oneshot::Sender<Option<DepositResponse>>, + ) -> Self { + DepositRequest { + pubkey, + deposit_amount: deposit_amount.clone(), + on_done, + } + } +} + +pub(crate) type DepositRequestReceiver = mpsc::Receiver<DepositRequest>; + +pub(crate) fn new_control_channels( + max_concurrent_deposits: usize, +) -> (DepositRequestSender, DepositRequestReceiver) { + let (tx, rx) = mpsc::channel(max_concurrent_deposits); + (tx.into(), rx) +} + +#[derive(Debug, Clone)] +pub struct DepositRequestSender(mpsc::Sender<DepositRequest>); + +impl From<mpsc::Sender<DepositRequest>> for DepositRequestSender { + fn from(inner: mpsc::Sender<DepositRequest>) -> Self { + DepositRequestSender(inner) + } +} + +impl DepositRequestSender { + pub(crate) async fn request_deposit(&self, request: DepositRequest) { + if self.0.send(request).await.is_err() { + error!("failed to request deposit: the DepositMaker must have died!") + } + } +} + +pub(crate) struct DepositMaker { + client: ChainClient, + max_concurrent_deposits: usize, + deposit_request_sender: DepositRequestSender, + deposit_request_receiver: DepositRequestReceiver, + short_sha: &'static str, + cancellation_token: CancellationToken, +} + +impl DepositMaker { + pub(crate) fn new( + short_sha: &'static str, + client: ChainClient, + max_concurrent_deposits: usize, + cancellation_token: CancellationToken, + ) -> Self { + let (deposit_request_sender, deposit_request_receiver) = + new_control_channels(max_concurrent_deposits); + + DepositMaker { + client, + max_concurrent_deposits, + deposit_request_sender, + deposit_request_receiver, + short_sha, + cancellation_token, + } + } + + pub(crate) fn deposit_request_sender(&self) -> DepositRequestSender { + self.deposit_request_sender.clone() + } + + pub(crate) async fn process_deposit_requests( + &mut self, + requests: Vec<DepositRequest>, + ) -> Result<(), VpnApiError> { + let chain_write_permit = self.client.start_chain_tx().await; + + info!("starting deposits"); + let mut contents = Vec::new(); + let mut replies = Vec::new(); + for request in requests { + // check if the channel is still open in case the receiver client has cancelled the request + if request.on_done.is_closed() { + warn!( + "the request for deposit from {} got cancelled", + request.pubkey + ); + continue; + } + + contents.push((request.pubkey.to_base58_string(), request.deposit_amount)); + replies.push(request.on_done); + } + + let deposits_res = chain_write_permit + .make_deposits(self.short_sha, contents) + .await; + let execute_res = match deposits_res { + Ok(res) => res, + Err(err) => { + // we have to let requesters know the deposit(s) failed + for reply in replies { + if reply.send(None).is_err() { + warn!("one of the deposit requesters has been terminated") + } + } + return Err(err); + } + }; + + let tx_hash = execute_res.transaction_hash; + info!("{} deposits made in transaction: {tx_hash}", replies.len()); + + let contract_data = match execute_res.to_contract_data() { + Ok(contract_data) => contract_data, + Err(err) => { + // that one is tricky. deposits technically got made, but we somehow failed to parse response, + // in this case terminate the proxy with 0 exit code so it wouldn't get automatically restarted + // because it requires some serious MANUAL intervention + error!("CRITICAL FAILURE: failed to parse out deposit information from the contract transaction. either the chain got upgraded and the schema changed or the ecash contract got changed! terminating the process. it has to be inspected manually. error was: {err}"); + self.cancellation_token.cancel(); + return Err(VpnApiError::DepositFailure); + } + }; + + if contract_data.len() != replies.len() { + // another critical failure, that one should be quite impossible and thus has to be manually inspected + error!("CRITICAL FAILURE: failed to parse out all deposit information from the contract transaction. got {} responses while we sent {} deposits! either the chain got upgraded and the schema changed or the ecash contract got changed! terminating the process. it has to be inspected manually", contract_data.len(), replies.len()); + self.cancellation_token.cancel(); + return Err(VpnApiError::DepositFailure); + } + + for (reply_channel, response) in replies.into_iter().zip(contract_data) { + let response_index = response.message_index; + let deposit_id = match response.parse_singleton_u32_contract_data() { + Ok(deposit_id) => deposit_id, + Err(err) => { + // another impossibility + error!("CRITICAL FAILURE: failed to parse out deposit id out of the response at index {response_index}: {err}. either the chain got upgraded and the schema changed or the ecash contract got changed! terminating the process. it has to be inspected manually"); + self.cancellation_token.cancel(); + return Err(VpnApiError::DepositFailure); + } + }; + + if reply_channel + .send(Some(DepositResponse { + deposit_id, + tx_hash, + })) + .is_err() + { + warn!("one of the deposit requesters has been terminated. deposit {deposit_id} will remain unclaimed!"); + // this shouldn't happen as the requester task shouldn't be killed, but it's not a critical failure + // we just lost some tokens, but it's not an undefined on-chain behaviour + } + } + + Ok(()) + } + + pub async fn run_forever(mut self) { + info!("starting the deposit maker task"); + loop { + let mut receive_buffer = Vec::with_capacity(self.max_concurrent_deposits); + tokio::select! { + _ = self.cancellation_token.cancelled() => { + break + } + received = self.deposit_request_receiver.recv_many(&mut receive_buffer, self.max_concurrent_deposits) => { + debug!("received {received} deposit requests"); + if let Err(err) = self.process_deposit_requests(receive_buffer).await { + error!("failed to process received deposit requests: {err}") + } + } + } + } + } +} diff --git a/nym-credential-proxy/nym-credential-proxy/src/error.rs b/nym-credential-proxy/nym-credential-proxy/src/error.rs index 4ffba86be79..56e18a0673d 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/error.rs +++ b/nym-credential-proxy/nym-credential-proxy/src/error.rs @@ -115,6 +115,9 @@ pub enum VpnApiError { #[error("timed out while attempting to obtain partial wallet from {client_repr}")] EcashApiRequestTimeout { client_repr: String }, + + #[error("failed to create deposit")] + DepositFailure, } impl VpnApiError { diff --git a/nym-credential-proxy/nym-credential-proxy/src/http/middleware/logging.rs b/nym-credential-proxy/nym-credential-proxy/src/http/middleware/logging.rs deleted file mode 100644 index 825104ec5b7..00000000000 --- a/nym-credential-proxy/nym-credential-proxy/src/http/middleware/logging.rs +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright 2024 - Nym Technologies SA <contact@nymtech.net> -// SPDX-License-Identifier: GPL-3.0-only - -use axum::{ - extract::{ConnectInfo, Request}, - http::{ - header::{HOST, USER_AGENT}, - HeaderValue, - }, - middleware::Next, - response::IntoResponse, -}; -use colored::*; -use std::net::SocketAddr; -use tokio::time::Instant; -use tracing::info; - -/// Simple logger for requests -pub async fn logger( - ConnectInfo(addr): ConnectInfo<SocketAddr>, - req: Request, - next: Next, -) -> impl IntoResponse { - let method = req.method().to_string().green(); - let uri = req.uri().to_string().blue(); - let agent = header_map( - req.headers().get(USER_AGENT), - "Unknown User Agent".to_string(), - ); - - let host = header_map(req.headers().get(HOST), "Unknown Host".to_string()); - - let start = Instant::now(); - let res = next.run(req).await; - let time_taken = start.elapsed(); - let status = res.status(); - let print_status = if status.is_client_error() || status.is_server_error() { - status.to_string().red() - } else if status.is_success() { - status.to_string().green() - } else { - status.to_string().yellow() - }; - - let taken = "time taken".bold(); - - let time_taken = match time_taken.as_millis() { - ms if ms > 500 => format!("{taken}: {}", format!("{ms}ms").red()), - ms if ms > 200 => format!("{taken}: {}", format!("{ms}ms").yellow()), - ms if ms > 50 => format!("{taken}: {}", format!("{ms}ms").bright_yellow()), - ms => format!("{taken}: {ms}ms"), - }; - - let agent_str = "agent".bold(); - info!("[{addr} -> {host}] {method} '{uri}': {print_status} {time_taken} {agent_str}: {agent}"); - - res -} - -fn header_map(header: Option<&HeaderValue>, msg: String) -> String { - header - .map(|x| x.to_str().unwrap_or(&msg).to_string()) - .unwrap_or(msg) -} diff --git a/nym-credential-proxy/nym-credential-proxy/src/http/middleware/mod.rs b/nym-credential-proxy/nym-credential-proxy/src/http/middleware/mod.rs deleted file mode 100644 index b51d9a59b5c..00000000000 --- a/nym-credential-proxy/nym-credential-proxy/src/http/middleware/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -// Copyright 2024 Nym Technologies SA <contact@nymtech.net> -// SPDX-License-Identifier: GPL-3.0-only - -pub mod auth; -pub mod logging; diff --git a/nym-credential-proxy/nym-credential-proxy/src/http/mod.rs b/nym-credential-proxy/nym-credential-proxy/src/http/mod.rs index 1267b3abfb8..e9359ac16ae 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/http/mod.rs +++ b/nym-credential-proxy/nym-credential-proxy/src/http/mod.rs @@ -10,7 +10,6 @@ use tokio_util::sync::CancellationToken; use tracing::info; pub mod helpers; -pub mod middleware; pub mod router; pub mod state; pub mod types; @@ -22,10 +21,15 @@ pub struct HttpServer { } impl HttpServer { - pub fn new(bind_address: SocketAddr, state: ApiState, auth_token: String) -> Self { + pub fn new( + bind_address: SocketAddr, + state: ApiState, + auth_token: String, + cancellation: CancellationToken, + ) -> Self { HttpServer { bind_address, - cancellation: state.cancellation_token(), + cancellation, router: build_router(state, auth_token), } } diff --git a/nym-credential-proxy/nym-credential-proxy/src/http/router/api/mod.rs b/nym-credential-proxy/nym-credential-proxy/src/http/router/api/mod.rs index 597887fb30f..aea8d4c5c62 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/http/router/api/mod.rs +++ b/nym-credential-proxy/nym-credential-proxy/src/http/router/api/mod.rs @@ -4,8 +4,8 @@ use crate::http::state::ApiState; use axum::Router; use nym_credential_proxy_requests::routes; +use nym_http_api_common::middleware::bearer_auth::AuthLayer; -use crate::http::middleware::auth::AuthLayer; pub(crate) use nym_http_api_common::{Output, OutputParams}; pub mod v1; diff --git a/nym-credential-proxy/nym-credential-proxy/src/http/router/api/v1/mod.rs b/nym-credential-proxy/nym-credential-proxy/src/http/router/api/v1/mod.rs index 59ee7b4df95..b77fe0d09ed 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/http/router/api/v1/mod.rs +++ b/nym-credential-proxy/nym-credential-proxy/src/http/router/api/v1/mod.rs @@ -1,13 +1,11 @@ // Copyright 2024 Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: GPL-3.0-only -use crate::http::middleware::auth::AuthLayer; use crate::http::state::ApiState; use axum::Router; use nym_credential_proxy_requests::routes::api::v1; +use nym_http_api_common::middleware::bearer_auth::AuthLayer; -// pub mod bandwidth_voucher; -// pub mod freepass; pub mod openapi; pub mod ticketbook; diff --git a/nym-credential-proxy/nym-credential-proxy/src/http/router/api/v1/ticketbook/mod.rs b/nym-credential-proxy/nym-credential-proxy/src/http/router/api/v1/ticketbook/mod.rs index 2dccc8d3494..335107f3dba 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/http/router/api/v1/ticketbook/mod.rs +++ b/nym-credential-proxy/nym-credential-proxy/src/http/router/api/v1/ticketbook/mod.rs @@ -21,7 +21,7 @@ use nym_credential_proxy_requests::api::v1::ticketbook::models::{ use nym_credential_proxy_requests::routes::api::v1::ticketbook; use nym_http_api_common::{FormattedResponse, OutputParams}; use time::OffsetDateTime; -use tracing::{error, info, span, warn, Level}; +use tracing::{error, info, span, warn, Instrument, Level}; pub(crate) mod shares; @@ -71,55 +71,58 @@ pub(crate) async fn obtain_ticketbook_shares( let requested_on = OffsetDateTime::now_utc(); let span = span!(Level::INFO, "obtain ticketboook", uuid = %uuid); - let _entered = span.enter(); - info!(""); + async move { + info!(""); - let output = params.output.unwrap_or_default(); + let output = params.output.unwrap_or_default(); - state.ensure_not_in_epoch_transition(Some(uuid)).await?; - let epoch_id = state - .current_epoch_id() - .await - .map_err(|err| RequestError::new_server_error(err, uuid))?; - - if let Err(err) = ensure_sane_expiration_date(payload.expiration_date) { - warn!("failure due to invalid expiration date"); - return Err(RequestError::new_with_uuid( - err.to_string(), - uuid, - StatusCode::BAD_REQUEST, - )); - } + state.ensure_not_in_epoch_transition(Some(uuid)).await?; + let epoch_id = state + .current_epoch_id() + .await + .map_err(|err| RequestError::new_server_error(err, uuid))?; - // if additional data was requested, grab them first in case there are any cache/network issues - let ( - master_verification_key, - aggregated_expiration_date_signatures, - aggregated_coin_index_signatures, - ) = state - .response_global_data( - params.include_master_verification_key, - params.include_expiration_date_signatures, - params.include_coin_index_signatures, - epoch_id, - payload.expiration_date, - uuid, - ) - .await?; + if let Err(err) = ensure_sane_expiration_date(payload.expiration_date) { + warn!("failure due to invalid expiration date"); + return Err(RequestError::new_with_uuid( + err.to_string(), + uuid, + StatusCode::BAD_REQUEST, + )); + } - let shares = try_obtain_wallet_shares(&state, uuid, requested_on, payload) - .await - .inspect_err(|err| warn!("request failure: {err}")) - .map_err(|err| RequestError::new(err.to_string(), StatusCode::INTERNAL_SERVER_ERROR))?; + // if additional data was requested, grab them first in case there are any cache/network issues + let ( + master_verification_key, + aggregated_expiration_date_signatures, + aggregated_coin_index_signatures, + ) = state + .response_global_data( + params.include_master_verification_key, + params.include_expiration_date_signatures, + params.include_coin_index_signatures, + epoch_id, + payload.expiration_date, + uuid, + ) + .await?; - info!("request was successful!"); - Ok(output.to_response(TicketbookWalletSharesResponse { - epoch_id, - shares, - master_verification_key, - aggregated_coin_index_signatures, - aggregated_expiration_date_signatures, - })) + let shares = try_obtain_wallet_shares(&state, uuid, requested_on, payload) + .await + .inspect_err(|err| warn!("request failure: {err}")) + .map_err(|err| RequestError::new(err.to_string(), StatusCode::INTERNAL_SERVER_ERROR))?; + + info!("request was successful!"); + Ok(output.to_response(TicketbookWalletSharesResponse { + epoch_id, + shares, + master_verification_key, + aggregated_coin_index_signatures, + aggregated_expiration_date_signatures, + })) + } + .instrument(span) + .await } /// Attempt to obtain blinded shares of an ecash ticketbook wallet asynchronously @@ -159,63 +162,69 @@ pub(crate) async fn obtain_ticketbook_shares_async( let requested_on = OffsetDateTime::now_utc(); let span = span!(Level::INFO, "[async] obtain ticketboook", uuid = %uuid); - let _entered = span.enter(); - info!(""); - - let output = params.output.unwrap_or_default(); + async move { + info!(""); + let output = params.output.unwrap_or_default(); - // 1. perform basic validation - state.ensure_not_in_epoch_transition(Some(uuid)).await?; + // 1. perform basic validation + state.ensure_not_in_epoch_transition(Some(uuid)).await?; - if let Err(err) = ensure_sane_expiration_date(payload.inner.expiration_date) { - warn!("failure due to invalid expiration date"); - return Err(RequestError::new_with_uuid( - err.to_string(), - uuid, - StatusCode::BAD_REQUEST, - )); - } - - // 2. store the request to retrieve the id - let pending = match state - .storage() - .insert_new_pending_async_shares_request(uuid, &payload.device_id, &payload.credential_id) - .await - { - Err(err) => { - error!("failed to insert new pending async shares: {err}"); + if let Err(err) = ensure_sane_expiration_date(payload.inner.expiration_date) { + warn!("failure due to invalid expiration date"); return Err(RequestError::new_with_uuid( err.to_string(), uuid, - StatusCode::CONFLICT, + StatusCode::BAD_REQUEST, )); } - Ok(pending) => pending, - }; - let id = pending.id; - - // 3. try to spawn a new task attempting to resolve the request - if state - .try_spawn(try_obtain_blinded_ticketbook_async( - state.clone(), - uuid, - requested_on, - payload, - params, - pending, - )) - .is_none() - { - // we're going through the shutdown - return Err(RequestError::new_with_uuid( - "server shutdown in progress", - uuid, - StatusCode::INTERNAL_SERVER_ERROR, - )); - } - // 4. in the meantime, return the id to the user - Ok(output.to_response(TicketbookWalletSharesAsyncResponse { id, uuid })) + // 2. store the request to retrieve the id + let pending = match state + .storage() + .insert_new_pending_async_shares_request( + uuid, + &payload.device_id, + &payload.credential_id, + ) + .await + { + Err(err) => { + error!("failed to insert new pending async shares: {err}"); + return Err(RequestError::new_with_uuid( + err.to_string(), + uuid, + StatusCode::CONFLICT, + )); + } + Ok(pending) => pending, + }; + let id = pending.id; + + // 3. try to spawn a new task attempting to resolve the request + if state + .try_spawn(try_obtain_blinded_ticketbook_async( + state.clone(), + uuid, + requested_on, + payload, + params, + pending, + )) + .is_none() + { + // we're going through the shutdown + return Err(RequestError::new_with_uuid( + "server shutdown in progress", + uuid, + StatusCode::INTERNAL_SERVER_ERROR, + )); + } + + // 4. in the meantime, return the id to the user + Ok(output.to_response(TicketbookWalletSharesAsyncResponse { id, uuid })) + } + .instrument(span) + .await } /// Obtain the current value of the bandwidth voucher deposit diff --git a/nym-credential-proxy/nym-credential-proxy/src/http/router/api/v1/ticketbook/shares.rs b/nym-credential-proxy/nym-credential-proxy/src/http/router/api/v1/ticketbook/shares.rs index 66be4e99616..cfe4893a69b 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/http/router/api/v1/ticketbook/shares.rs +++ b/nym-credential-proxy/nym-credential-proxy/src/http/router/api/v1/ticketbook/shares.rs @@ -17,7 +17,7 @@ use nym_credential_proxy_requests::api::v1::ticketbook::models::{ use nym_credential_proxy_requests::routes::api::v1::ticketbook::shares; use nym_http_api_common::OutputParams; use nym_validator_client::nym_api::EpochId; -use tracing::{debug, span, Level}; +use tracing::{debug, span, Instrument, Level}; use uuid::Uuid; async fn shares_to_response( @@ -100,50 +100,51 @@ pub(crate) async fn query_for_shares_by_id( let uuid = random_uuid(); let span = span!(Level::INFO, "query shares by id", uuid = %uuid, share_id = %share_id); - let _entered = span.enter(); - debug!(""); - - // TODO: edge case: this will **NOT** work if shares got created in epoch X, - // but this query happened in epoch X+1 - let shares = match state - .storage() - .load_wallet_shares_by_shares_id(share_id) - .await - { - Ok(shares) => { - if shares.is_empty() { - debug!("shares not found"); - - // check for explicit error - match state - .storage() - .load_shares_error_by_shares_id(share_id) - .await - { - Ok(maybe_error_message) => { - if let Some(error_message) = maybe_error_message { - return Err(RequestError::new_with_uuid( - format!("failed to obtain wallet shares: {error_message} - share_id = {share_id}"), - uuid, - StatusCode::INTERNAL_SERVER_ERROR, - )); + async move { + debug!(""); + + // TODO: edge case: this will **NOT** work if shares got created in epoch X, + // but this query happened in epoch X+1 + let shares = match state + .storage() + .load_wallet_shares_by_shares_id(share_id) + .await + { + Ok(shares) => { + if shares.is_empty() { + debug!("shares not found"); + + // check for explicit error + match state + .storage() + .load_shares_error_by_shares_id(share_id) + .await + { + Ok(maybe_error_message) => { + if let Some(error_message) = maybe_error_message { + return Err(RequestError::new_with_uuid( + format!("failed to obtain wallet shares: {error_message} - share_id = {share_id}"), + uuid, + StatusCode::INTERNAL_SERVER_ERROR, + )); + } } + Err(err) => return db_failure(err, uuid), } - Err(err) => return db_failure(err, uuid), - } - return Err(RequestError::new_with_uuid( - format!("not found - share_id = {share_id}"), - uuid, - StatusCode::NOT_FOUND, - )); + return Err(RequestError::new_with_uuid( + format!("not found - share_id = {share_id}"), + uuid, + StatusCode::NOT_FOUND, + )); + } + shares } - shares - } - Err(err) => return db_failure(err, uuid), - }; + Err(err) => return db_failure(err, uuid), + }; - shares_to_response(state, uuid, shares, params).await + shares_to_response(state, uuid, shares, params).await + }.instrument(span).await } /// Query by id for blinded wallet shares of a ticketbook @@ -174,50 +175,51 @@ pub(crate) async fn query_for_shares_by_device_id_and_credential_id( let uuid = random_uuid(); let span = span!(Level::INFO, "query shares by device and credential ids", uuid = %uuid, device_id = %device_id, credential_id = %credential_id); - let _entered = span.enter(); - debug!(""); - - // TODO: edge case: this will **NOT** work if shares got created in epoch X, - // but this query happened in epoch X+1 - let shares = match state - .storage() - .load_wallet_shares_by_device_and_credential_id(&device_id, &credential_id) - .await - { - Ok(shares) => { - if shares.is_empty() { - debug!("shares not found"); - - // check for explicit error - match state - .storage() - .load_shares_error_by_device_and_credential_id(&device_id, &credential_id) - .await - { - Ok(maybe_error_message) => { - if let Some(error_message) = maybe_error_message { - return Err(RequestError::new_with_uuid( - format!("failed to obtain wallet shares: {error_message} - device_id = {device_id}, credential_id = {credential_id}"), - uuid, - StatusCode::INTERNAL_SERVER_ERROR, - )); + async move { + debug!(""); + + // TODO: edge case: this will **NOT** work if shares got created in epoch X, + // but this query happened in epoch X+1 + let shares = match state + .storage() + .load_wallet_shares_by_device_and_credential_id(&device_id, &credential_id) + .await + { + Ok(shares) => { + if shares.is_empty() { + debug!("shares not found"); + + // check for explicit error + match state + .storage() + .load_shares_error_by_device_and_credential_id(&device_id, &credential_id) + .await + { + Ok(maybe_error_message) => { + if let Some(error_message) = maybe_error_message { + return Err(RequestError::new_with_uuid( + format!("failed to obtain wallet shares: {error_message} - device_id = {device_id}, credential_id = {credential_id}"), + uuid, + StatusCode::INTERNAL_SERVER_ERROR, + )); + } } + Err(err) => return db_failure(err, uuid), } - Err(err) => return db_failure(err, uuid), - } - return Err(RequestError::new_with_uuid( - format!("not found - device_id = {device_id}, credential_id = {credential_id}"), - uuid, - StatusCode::NOT_FOUND, - )); + return Err(RequestError::new_with_uuid( + format!("not found - device_id = {device_id}, credential_id = {credential_id}"), + uuid, + StatusCode::NOT_FOUND, + )); + } + shares } - shares - } - Err(err) => return db_failure(err, uuid), - }; + Err(err) => return db_failure(err, uuid), + }; - shares_to_response(state, uuid, shares, params).await + shares_to_response(state, uuid, shares, params).await + }.instrument(span).await } pub(crate) fn routes() -> Router<ApiState> { diff --git a/nym-credential-proxy/nym-credential-proxy/src/http/router/mod.rs b/nym-credential-proxy/nym-credential-proxy/src/http/router/mod.rs index fd0ecf267af..c7030b470de 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/http/router/mod.rs +++ b/nym-credential-proxy/nym-credential-proxy/src/http/router/mod.rs @@ -1,13 +1,13 @@ // Copyright 2024 Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: GPL-3.0-only -use crate::http::middleware::auth::AuthLayer; -use crate::http::middleware::logging; use crate::http::state::ApiState; use axum::response::Redirect; use axum::routing::{get, MethodRouter}; use axum::Router; use nym_credential_proxy_requests::routes; +use nym_http_api_common::middleware::bearer_auth::AuthLayer; +use nym_http_api_common::middleware::logging; use std::sync::Arc; use zeroize::Zeroizing; diff --git a/nym-credential-proxy/nym-credential-proxy/src/http/state/mod.rs b/nym-credential-proxy/nym-credential-proxy/src/http/state/mod.rs index c4c4a8ebe3b..dc6309e8a7f 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/http/state/mod.rs +++ b/nym-credential-proxy/nym-credential-proxy/src/http/state/mod.rs @@ -1,6 +1,7 @@ // Copyright 2024 Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: GPL-3.0-only +use crate::deposit_maker::{DepositRequest, DepositRequestSender}; use crate::error::VpnApiError; use crate::helpers::LockTimer; use crate::http::types::RequestError; @@ -28,20 +29,24 @@ use nym_credentials::{ AggregatedCoinIndicesSignatures, AggregatedExpirationDateSignatures, EpochVerificationKey, }; use nym_credentials_interface::VerificationKeyAuth; +use nym_ecash_contract_common::msg::ExecuteMsg; use nym_validator_client::coconut::EcashApiError; use nym_validator_client::nym_api::EpochId; use nym_validator_client::nyxd::contract_traits::dkg_query_client::Epoch; use nym_validator_client::nyxd::contract_traits::{ DkgQueryClient, EcashQueryClient, NymContractsProvider, PagedDkgQueryClient, }; -use nym_validator_client::nyxd::{Coin, NyxdClient}; +use nym_validator_client::nyxd::cosmwasm_client::types::ExecuteResult; +use nym_validator_client::nyxd::{Coin, CosmWasmClient, NyxdClient}; use nym_validator_client::{nyxd, DirectSigningHttpRpcNyxdClient, EcashApiClient}; use std::future::Future; use std::ops::Deref; use std::sync::Arc; +use std::time::Duration; use time::{Date, OffsetDateTime}; use tokio::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard}; use tokio::task::JoinHandle; +use tokio::time::Instant; use tokio_util::sync::CancellationToken; use tokio_util::task::TaskTracker; use tracing::{debug, info, warn}; @@ -59,36 +64,19 @@ impl ApiState { pub async fn new( storage: VpnApiStorage, zk_nym_web_hook_config: ZkNymWebHookConfig, - mnemonic: Mnemonic, + client: ChainClient, + deposit_requester: DepositRequestSender, + cancellation_token: CancellationToken, ) -> Result<Self, VpnApiError> { - let network_details = nym_network_defaults::NymNetworkDetails::new_from_env(); - let client_config = nyxd::Config::try_from_nym_network_details(&network_details)?; - - let nyxd_url = network_details - .endpoints - .first() - .ok_or_else(|| VpnApiError::NoNyxEndpointsAvailable)? - .nyxd_url - .as_str(); - - let client = NyxdClient::connect_with_mnemonic(client_config, nyxd_url, mnemonic)?; - - if client.ecash_contract_address().is_none() { - return Err(VpnApiError::UnavailableEcashContract); - } - - if client.dkg_contract_address().is_none() { - return Err(VpnApiError::UnavailableDKGContract); - } - let state = ApiState { inner: Arc::new(ApiStateInner { storage, - client: RwLock::new(client), + client, ecash_state: EcashState::default(), zk_nym_web_hook_config, task_tracker: TaskTracker::new(), - cancellation_token: CancellationToken::new(), + deposit_requester, + cancellation_token, }), }; @@ -136,10 +124,6 @@ impl ApiState { self.inner.task_tracker.wait().await } - pub(crate) fn cancellation_token(&self) -> CancellationToken { - self.inner.cancellation_token.clone() - } - pub(crate) fn zk_nym_web_hook(&self) -> &ZkNymWebHookConfig { &self.inner.zk_nym_web_hook_config } @@ -220,16 +204,19 @@ impl ApiState { } pub(crate) async fn query_chain(&self) -> RwLockReadGuard<DirectSigningHttpRpcNyxdClient> { - let _acquire_timer = LockTimer::new("acquire chain query permit"); - self.inner.client.read().await + self.inner.client.query_chain().await } - pub(crate) async fn start_chain_tx(&self) -> ChainWritePermit { - let _acquire_timer = LockTimer::new("acquire exclusive chain write permit"); + pub(crate) async fn request_deposit(&self, request: DepositRequest) { + let start = Instant::now(); + self.inner.deposit_requester.request_deposit(request).await; - ChainWritePermit { - lock_timer: LockTimer::new("exclusive chain access permit"), - inner: self.inner.client.write().await, + let time_taken = start.elapsed(); + let formatted = humantime::format_duration(time_taken); + if time_taken > Duration::from_secs(10) { + warn!("attempting to push new deposit request onto the queue took {formatted}. perhaps the buffer is too small or the process/chain is overloaded?") + } else { + debug!("attempting to push new deposit request onto the queue took {formatted}") } } @@ -604,10 +591,57 @@ impl ApiState { } } +#[derive(Clone)] +pub struct ChainClient(Arc<RwLock<DirectSigningHttpRpcNyxdClient>>); + +impl ChainClient { + pub fn new(mnemonic: Mnemonic) -> Result<Self, VpnApiError> { + let network_details = nym_network_defaults::NymNetworkDetails::new_from_env(); + let client_config = nyxd::Config::try_from_nym_network_details(&network_details)?; + + let nyxd_url = network_details + .endpoints + .first() + .ok_or_else(|| VpnApiError::NoNyxEndpointsAvailable)? + .nyxd_url + .as_str(); + + let client = NyxdClient::connect_with_mnemonic(client_config, nyxd_url, mnemonic)?; + + if client.ecash_contract_address().is_none() { + return Err(VpnApiError::UnavailableEcashContract); + } + + if client.dkg_contract_address().is_none() { + return Err(VpnApiError::UnavailableDKGContract); + } + + Ok(ChainClient(Arc::new(RwLock::new(client)))) + } + + pub(crate) async fn query_chain(&self) -> ChainReadPermit { + let _acquire_timer = LockTimer::new("acquire chain query permit"); + self.0.read().await + } + + pub(crate) async fn start_chain_tx(&self) -> ChainWritePermit { + let _acquire_timer = LockTimer::new("acquire exclusive chain write permit"); + + ChainWritePermit { + lock_timer: LockTimer::new("exclusive chain access permit"), + inner: self.0.write().await, + } + } +} + +// + struct ApiStateInner { storage: VpnApiStorage, - client: RwLock<DirectSigningHttpRpcNyxdClient>, + client: ChainClient, + + deposit_requester: DepositRequestSender, zk_nym_web_hook_config: ZkNymWebHookConfig, @@ -666,6 +700,8 @@ pub(crate) struct EcashState { CachedImmutableItems<Date, AggregatedExpirationDateSignatures>, } +pub(crate) type ChainReadPermit<'a> = RwLockReadGuard<'a, DirectSigningHttpRpcNyxdClient>; + // explicitly wrap the WriteGuard for extra information regarding time taken pub(crate) struct ChainWritePermit<'a> { // it's not really dead, we only care about it being dropped @@ -674,7 +710,56 @@ pub(crate) struct ChainWritePermit<'a> { inner: RwLockWriteGuard<'a, DirectSigningHttpRpcNyxdClient>, } -impl<'a> Deref for ChainWritePermit<'a> { +impl<'a> ChainWritePermit<'a> { + pub(crate) async fn make_deposits( + self, + short_sha: &'static str, + info: Vec<(String, Coin)>, + ) -> Result<ExecuteResult, VpnApiError> { + let address = self.inner.address(); + let starting_sequence = self.inner.get_sequence(&address).await?.sequence; + + let deposits = info.len(); + + let ecash_contract = self + .inner + .ecash_contract_address() + .ok_or(VpnApiError::UnavailableEcashContract)?; + let deposit_messages = info + .into_iter() + .map(|(identity_key, amount)| { + ( + ExecuteMsg::DepositTicketBookFunds { identity_key }, + vec![amount], + ) + }) + .collect::<Vec<_>>(); + + let res = self + .inner + .execute_multiple( + ecash_contract, + deposit_messages, + None, + format!("cp-{short_sha}: performing {deposits} deposits"), + ) + .await?; + + loop { + let updated_sequence = self.inner.get_sequence(&address).await?.sequence; + + if updated_sequence > starting_sequence { + break; + } + warn!("wrong sequence number... waiting before releasing chain lock"); + tokio::time::sleep(Duration::from_millis(50)).await; + } + + Ok(res) + } +} + +impl Deref for ChainWritePermit<'_> { type Target = DirectSigningHttpRpcNyxdClient; fn deref(&self) -> &Self::Target { diff --git a/nym-credential-proxy/nym-credential-proxy/src/main.rs b/nym-credential-proxy/nym-credential-proxy/src/main.rs index b534f442c8d..71dd082b106 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/main.rs +++ b/nym-credential-proxy/nym-credential-proxy/src/main.rs @@ -7,19 +7,23 @@ #![warn(clippy::dbg_macro)] use crate::cli::Cli; +use crate::deposit_maker::DepositMaker; use crate::error::VpnApiError; -use crate::http::state::ApiState; +use crate::http::state::{ApiState, ChainClient}; use crate::http::HttpServer; use crate::storage::VpnApiStorage; use crate::tasks::StoragePruner; use clap::Parser; use nym_bin_common::logging::setup_tracing_logger; +use nym_bin_common::{bin_info, bin_info_owned}; use nym_network_defaults::setup_env; -use tracing::{info, trace}; +use tokio_util::sync::CancellationToken; +use tracing::{error, info, trace}; pub mod cli; pub mod config; pub mod credentials; +mod deposit_maker; pub mod error; pub mod helpers; pub mod http; @@ -50,6 +54,20 @@ pub async fn wait_for_signal() { } } +fn build_sha_short() -> &'static str { + let bin_info = bin_info!(); + if bin_info.commit_sha.len() < 7 { + panic!("unavailable build commit sha") + } + + if bin_info.commit_sha == "VERGEN_IDEMPOTENT_OUTPUT" { + error!("the binary hasn't been built correctly. it doesn't have a commit sha information"); + return "unknown"; + } + + &bin_info.commit_sha[..7] +} + async fn run_api(cli: Cli) -> Result<(), VpnApiError> { // create the tasks let bind_address = cli.bind_address(); @@ -58,14 +76,37 @@ async fn run_api(cli: Cli) -> Result<(), VpnApiError> { let mnemonic = cli.mnemonic; let auth_token = cli.http_auth_token; let webhook_cfg = cli.webhook; - let api_state = ApiState::new(storage.clone(), webhook_cfg, mnemonic).await?; - let http_server = HttpServer::new(bind_address, api_state.clone(), auth_token); + let chain_client = ChainClient::new(mnemonic)?; + let cancellation_token = CancellationToken::new(); + + let deposit_maker = DepositMaker::new( + build_sha_short(), + chain_client.clone(), + cli.max_concurrent_deposits, + cancellation_token.clone(), + ); - let storage_pruner = StoragePruner::new(api_state.cancellation_token(), storage); + let deposit_request_sender = deposit_maker.deposit_request_sender(); + let api_state = ApiState::new( + storage.clone(), + webhook_cfg, + chain_client, + deposit_request_sender, + cancellation_token.clone(), + ) + .await?; + let http_server = HttpServer::new( + bind_address, + api_state.clone(), + auth_token, + cancellation_token.clone(), + ); + let storage_pruner = StoragePruner::new(cancellation_token, storage); // spawn all the tasks api_state.try_spawn(http_server.run_forever()); api_state.try_spawn(storage_pruner.run_forever()); + api_state.try_spawn(deposit_maker.run_forever()); // wait for cancel signal (SIGINT, SIGTERM or SIGQUIT) wait_for_signal().await; @@ -78,10 +119,10 @@ async fn run_api(cli: Cli) -> Result<(), VpnApiError> { #[tokio::main] async fn main() -> anyhow::Result<()> { - std::env::set_var( - "RUST_LOG", - "trace,handlebars=warn,tendermint_rpc=warn,h2=warn,hyper=warn,rustls=warn,reqwest=warn,tungstenite=warn,async_tungstenite=warn,tokio_util=warn,tokio_tungstenite=warn,tokio-util=warn,nym_validator_client=info", - ); + // std::env::set_var( + // "RUST_LOG", + // "trace,handlebars=warn,tendermint_rpc=warn,h2=warn,hyper=warn,rustls=warn,reqwest=warn,tungstenite=warn,async_tungstenite=warn,tokio_util=warn,tokio_tungstenite=warn,tokio-util=warn,axum=warn,sqlx-core=warn,nym_validator_client=info", + // ); let cli = Cli::parse(); cli.webhook.ensure_valid_client_url()?; @@ -90,6 +131,9 @@ async fn main() -> anyhow::Result<()> { setup_env(cli.config_env_file.as_ref()); setup_tracing_logger(); + let bin_info = bin_info_owned!(); + info!("using the following version: {bin_info}"); + run_api(cli).await?; Ok(()) } diff --git a/nym-credential-proxy/nym-credential-proxy/src/storage/mod.rs b/nym-credential-proxy/nym-credential-proxy/src/storage/mod.rs index 9d45149d0cc..6133d1acbc1 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/storage/mod.rs +++ b/nym-credential-proxy/nym-credential-proxy/src/storage/mod.rs @@ -19,7 +19,9 @@ use nym_validator_client::nyxd::Coin; use sqlx::ConnectOptions; use std::fmt::Debug; use std::path::Path; +use std::time::Duration; use time::{Date, OffsetDateTime}; +use tracing::log::LevelFilter; use tracing::{debug, error, info, instrument}; use uuid::Uuid; use zeroize::Zeroizing; @@ -40,9 +42,15 @@ impl VpnApiStorage { let opts = sqlx::sqlite::SqliteConnectOptions::new() .filename(database_path) .create_if_missing(true) - .disable_statement_logging(); + .log_statements(LevelFilter::Trace) + .log_slow_statements(LevelFilter::Warn, Duration::from_millis(250)); - let connection_pool = match sqlx::SqlitePool::connect_with(opts).await { + let pool_opts = sqlx::sqlite::SqlitePoolOptions::new() + .min_connections(5) + .max_connections(25) + .acquire_timeout(Duration::from_secs(60)); + + let connection_pool = match pool_opts.connect_with(opts).await { Ok(db) => db, Err(err) => { error!("Failed to connect to SQLx database: {err}"); diff --git a/nym-credential-proxy/nym-credential-proxy/src/tasks.rs b/nym-credential-proxy/nym-credential-proxy/src/tasks.rs index 6389f584529..f886b7fd440 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/tasks.rs +++ b/nym-credential-proxy/nym-credential-proxy/src/tasks.rs @@ -19,10 +19,11 @@ impl StoragePruner { } pub async fn run_forever(self) { - while !self.cancellation_token.is_cancelled() { + info!("starting the storage pruner task"); + loop { tokio::select! { _ = self.cancellation_token.cancelled() => { - // The token was cancelled, task can shut down + break } _ = tokio::time::sleep(std::time::Duration::from_secs(60 * 60)) => { match self.storage.prune_old_blinded_shares().await { diff --git a/nym-credential-proxy/nym-credential-proxy/src/webhook.rs b/nym-credential-proxy/nym-credential-proxy/src/webhook.rs index 9ebdca250f4..d32f27c9291 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/webhook.rs +++ b/nym-credential-proxy/nym-credential-proxy/src/webhook.rs @@ -5,7 +5,7 @@ use crate::error::VpnApiError; use clap::Args; use reqwest::header::AUTHORIZATION; use serde::Serialize; -use tracing::{debug, error, instrument, span, Level}; +use tracing::{debug, error, instrument, span, Instrument, Level}; use url::Url; use uuid::Uuid; @@ -46,30 +46,33 @@ impl ZkNymWebHookConfig { pub async fn try_trigger<T: Serialize + ?Sized>(&self, original_uuid: Uuid, payload: &T) { let url = self.unchecked_client_url(); let span = span!(Level::DEBUG, "webhook", uuid = %original_uuid, url = %url); - let _entered = span.enter(); - debug!("🕸️ about to trigger the webhook"); + async move { + debug!("🕸️ about to trigger the webhook"); - match reqwest::Client::new() - .post(url.clone()) - .header(AUTHORIZATION, self.bearer_token()) - .json(payload) - .send() - .await - { - Ok(res) => { - if !res.status().is_success() { - error!("❌🕸️ failed to call webhook: {res:?}"); - } else { - debug!("✅🕸️ webhook triggered successfully: {res:?}"); - if let Ok(body) = res.text().await { - debug!("body = {body}"); + match reqwest::Client::new() + .post(url.clone()) + .header(AUTHORIZATION, self.bearer_token()) + .json(payload) + .send() + .await + { + Ok(res) => { + if !res.status().is_success() { + error!("❌🕸️ failed to call webhook: {res:?}"); + } else { + debug!("✅🕸️ webhook triggered successfully: {res:?}"); + if let Ok(body) = res.text().await { + debug!("body = {body}"); + } } } - } - Err(err) => { - error!("failed to call webhook: {err}") + Err(err) => { + error!("failed to call webhook: {err}") + } } } + .instrument(span) + .await } } diff --git a/nym-network-monitor/Cargo.toml b/nym-network-monitor/Cargo.toml index 5984b1ec485..3d80fed9a00 100644 --- a/nym-network-monitor/Cargo.toml +++ b/nym-network-monitor/Cargo.toml @@ -30,6 +30,7 @@ utoipa-swagger-ui = { workspace = true, features = ["axum"] } # internal nym-bin-common = { path = "../common/bin-common" } +nym-client-core = { path = "../common/client-core" } nym-crypto = { path = "../common/crypto" } nym-network-defaults = { path = "../common/network-defaults" } nym-sdk = { path = "../sdk/rust/nym-sdk" } diff --git a/nym-network-monitor/src/main.rs b/nym-network-monitor/src/main.rs index e209a64533f..141944eb8a4 100644 --- a/nym-network-monitor/src/main.rs +++ b/nym-network-monitor/src/main.rs @@ -88,6 +88,7 @@ async fn make_client(topology: NymTopology) -> Result<MixnetClient> { let mixnet_client = mixnet::MixnetClientBuilder::new_ephemeral() .network_details(net) .custom_topology_provider(topology_provider) + .debug_config(mixnet_debug_config(0)) // .enable_credentials_mode() .build()?; @@ -216,3 +217,14 @@ async fn main() -> Result<()> { Ok(()) } + +fn mixnet_debug_config(min_gateway_performance: u8) -> nym_client_core::config::DebugConfig { + let mut debug_config = nym_client_core::config::DebugConfig::default(); + debug_config + .traffic + .disable_main_poisson_packet_distribution = true; + debug_config.cover_traffic.disable_loop_cover_traffic_stream = true; + debug_config.topology.minimum_gateway_performance = min_gateway_performance; + debug_config.traffic.deterministic_route_selection = true; + debug_config +} diff --git a/nym-node-status-api/nym-node-status-api/Cargo.toml b/nym-node-status-api/nym-node-status-api/Cargo.toml index 0dfb0caffb6..98684f75f53 100644 --- a/nym-node-status-api/nym-node-status-api/Cargo.toml +++ b/nym-node-status-api/nym-node-status-api/Cargo.toml @@ -3,7 +3,7 @@ [package] name = "nym-node-status-api" -version = "1.0.0-rc.3" +version = "1.0.0-rc.7" authors.workspace = true repository.workspace = true homepage.workspace = true @@ -21,11 +21,13 @@ cosmwasm-std = { workspace = true } envy = { workspace = true } futures-util = { workspace = true } moka = { workspace = true, features = ["future"] } -nym-bin-common = { path = "../../common/bin-common", features = ["models"]} +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"] } nym-explorer-client = { path = "../../explorer-api/explorer-client" } nym-network-defaults = { path = "../../common/network-defaults" } +nym-serde-helpers = { path = "../../common/serde-helpers"} +nym-statistics-common = { path = "../../common/statistics" } nym-validator-client = { path = "../../common/client-libs/validator-client" } nym-task = { path = "../../common/task" } nym-node-requests = { path = "../../nym-node/nym-node-requests", features = ["openapi"] } @@ -36,8 +38,9 @@ serde_json = { workspace = true } serde_json_path = { workspace = true } strum = { workspace = true } strum_macros = { workspace = true } -sqlx = { workspace = true, features = ["runtime-tokio-rustls", "sqlite"] } +sqlx = { workspace = true, features = ["runtime-tokio-rustls", "sqlite", "time"] } thiserror = { workspace = true } +time = { workspace = true } tokio = { workspace = true, features = ["rt-multi-thread"] } tokio-util = { workspace = true } tracing = { workspace = true } @@ -52,12 +55,15 @@ utoipa-swagger-ui = { workspace = true, features = ["axum"] } # utoipauto = { git = "https://github.com/ProbablyClem/utoipauto", rev = "eb04cba" } utoipauto = { workspace = true } +nym-node-metrics = { path = "../../nym-node/nym-node-metrics" } + + [build-dependencies] anyhow = { workspace = true } tokio = { workspace = true, features = ["macros"] } sqlx = { workspace = true, features = [ - "runtime-tokio-rustls", - "sqlite", - "macros", - "migrate", + "runtime-tokio-rustls", + "sqlite", + "macros", + "migrate", ] } diff --git a/nym-node-status-api/nym-node-status-api/migrations/002_session_stats.sql b/nym-node-status-api/nym-node-status-api/migrations/002_session_stats.sql new file mode 100644 index 00000000000..d29fe986a33 --- /dev/null +++ b/nym-node-status-api/nym-node-status-api/migrations/002_session_stats.sql @@ -0,0 +1,17 @@ + +CREATE TABLE gateway_session_stats ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + gateway_identity_key VARCHAR NOT NULL, + node_id INTEGER NOT NULL, + day DATE NOT NULL, + unique_active_clients INTEGER NOT NULL, + session_started INTEGER NOT NULL, + users_hashes VARCHAR, + vpn_sessions VARCHAR, + mixnet_sessions VARCHAR, + unknown_sessions VARCHAR, + UNIQUE (node_id, day) -- This constraint automatically creates an index + ); +CREATE INDEX idx_gateway_session_stats_identity_key ON gateway_session_stats (gateway_identity_key); +CREATE INDEX idx_gateway_session_stats_day ON gateway_session_stats (day); + diff --git a/nym-node-status-api/nym-node-status-api/src/cli/mod.rs b/nym-node-status-api/nym-node-status-api/src/cli/mod.rs index f329e4a9a03..e9d76a1f755 100644 --- a/nym-node-status-api/nym-node-status-api/src/cli/mod.rs +++ b/nym-node-status-api/nym-node-status-api/src/cli/mod.rs @@ -45,11 +45,6 @@ pub(crate) struct Cli { #[arg(value_parser = parse_duration)] pub(crate) nym_api_client_timeout: Duration, - /// Explorer api client timeout. - #[clap(long, default_value = "15", env = "EXPLORER_CLIENT_TIMEOUT")] - #[arg(value_parser = parse_duration)] - pub(crate) explorer_client_timeout: Duration, - /// Connection url for the database. #[clap(long, env = "DATABASE_URL")] pub(crate) database_url: String, @@ -70,10 +65,18 @@ pub(crate) struct Cli { #[arg(value_parser = parse_duration)] pub(crate) testruns_refresh_interval: Duration, + #[clap(long, default_value = "86400", env = "NODE_STATUS_API_GEODATA_TTL")] + #[arg(value_parser = parse_duration)] + pub(crate) geodata_ttl: Duration, + #[clap(env = "NODE_STATUS_API_AGENT_KEY_LIST")] #[arg(value_delimiter = ',')] pub(crate) agent_key_list: Vec<String>, + /// https://github.com/ipinfo/rust + #[clap(long, env = "IPINFO_API_TOKEN")] + pub(crate) ipinfo_api_token: String, + #[clap( long, default_value_t = 40, diff --git a/nym-node-status-api/nym-node-status-api/src/db/models.rs b/nym-node-status-api/nym-node-status-api/src/db/models.rs index 596f634f2e7..9de09271bfc 100644 --- a/nym-node-status-api/nym-node-status-api/src/db/models.rs +++ b/nym-node-status-api/nym-node-status-api/src/db/models.rs @@ -4,7 +4,9 @@ use crate::{ }; use nym_node_requests::api::v1::node::models::NodeDescription; use serde::{Deserialize, Serialize}; +use sqlx::FromRow; use strum_macros::{EnumString, FromRepr}; +use time::Date; use utoipa::ToSchema; pub(crate) struct GatewayRecord { @@ -12,6 +14,7 @@ pub(crate) struct GatewayRecord { 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>, pub(crate) last_updated_utc: i64, pub(crate) performance: u8, @@ -215,7 +218,6 @@ pub(crate) const MIXNODES_BONDED_RESERVE: &str = "mixnodes.bonded.reserve"; pub(crate) const MIXNODES_BLACKLISTED_COUNT: &str = "mixnodes.blacklisted.count"; pub(crate) const GATEWAYS_BONDED_COUNT: &str = "gateways.bonded.count"; -pub(crate) const GATEWAYS_EXPLORER_COUNT: &str = "gateways.explorer.count"; pub(crate) const GATEWAYS_BLACKLISTED_COUNT: &str = "gateways.blacklisted.count"; pub(crate) const MIXNODES_HISTORICAL_COUNT: &str = "mixnodes.historical.count"; @@ -272,7 +274,6 @@ pub(crate) mod gateway { pub(crate) bonded: GatewaySummaryBonded, pub(crate) blacklisted: GatewaySummaryBlacklisted, pub(crate) historical: GatewaySummaryHistorical, - pub(crate) explorer: GatewaySummaryExplorer, } #[derive(Debug, Clone, Deserialize, Serialize, ToSchema)] @@ -334,3 +335,44 @@ pub struct GatewayInfoDto { pub self_described: Option<String>, pub explorer_pretty_bond: Option<String>, } + +#[derive(Debug, Clone, FromRow)] +pub struct GatewaySessionsRecord { + pub gateway_identity_key: String, + pub node_id: i64, + pub day: Date, + pub unique_active_clients: i64, + pub session_started: i64, + pub users_hashes: Option<String>, + pub vpn_sessions: Option<String>, + pub mixnet_sessions: Option<String>, + pub unknown_sessions: Option<String>, +} + +impl TryFrom<GatewaySessionsRecord> for http::models::SessionStats { + type Error = anyhow::Error; + + fn try_from(value: GatewaySessionsRecord) -> Result<Self, Self::Error> { + let users_hashes = value.users_hashes.clone().unwrap_or("null".to_string()); + let vpn_sessions = value.vpn_sessions.clone().unwrap_or("null".to_string()); + let mixnet_sessions = value.mixnet_sessions.clone().unwrap_or("null".to_string()); + let unknown_sessions = value.unknown_sessions.clone().unwrap_or("null".to_string()); + + let users_hashes = serde_json::from_str(&users_hashes).unwrap_or(None); + let vpn_sessions = serde_json::from_str(&vpn_sessions).unwrap_or(None); + let mixnet_sessions = serde_json::from_str(&mixnet_sessions).unwrap_or(None); + let unknown_sessions = serde_json::from_str(&unknown_sessions).unwrap_or(None); + + Ok(http::models::SessionStats { + gateway_identity_key: value.gateway_identity_key.clone(), + node_id: value.node_id as u32, + day: value.day, + unique_active_clients: value.unique_active_clients, + session_started: value.session_started, + users_hashes, + vpn_sessions, + mixnet_sessions, + unknown_sessions, + }) + } +} diff --git a/nym-node-status-api/nym-node-status-api/src/db/queries/gateways_stats.rs b/nym-node-status-api/nym-node-status-api/src/db/queries/gateways_stats.rs new file mode 100644 index 00000000000..8ba813ee1f2 --- /dev/null +++ b/nym-node-status-api/nym-node-status-api/src/db/queries/gateways_stats.rs @@ -0,0 +1,75 @@ +use crate::{ + db::{models::GatewaySessionsRecord, DbPool}, + http::models::SessionStats, +}; +use futures_util::TryStreamExt; +use time::Date; +use tracing::error; + +pub(crate) async fn insert_session_records( + pool: &DbPool, + records: Vec<GatewaySessionsRecord>, +) -> anyhow::Result<()> { + let mut tx = pool.begin().await?; + for record in records { + sqlx::query!( + "INSERT OR IGNORE INTO gateway_session_stats + (gateway_identity_key, node_id, day, + unique_active_clients, session_started, users_hashes, + vpn_sessions, mixnet_sessions, unknown_sessions) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", + record.gateway_identity_key, + record.node_id, + record.day, + record.unique_active_clients, + record.session_started, + record.users_hashes, + record.vpn_sessions, + record.mixnet_sessions, + record.unknown_sessions, + ) + .execute(&mut *tx) + .await?; + } + tx.commit().await?; + + Ok(()) +} + +pub(crate) async fn get_sessions_stats(pool: &DbPool) -> anyhow::Result<Vec<SessionStats>> { + let mut conn = pool.acquire().await?; + let items = sqlx::query_as( + "SELECT gateway_identity_key, + node_id, + day, + unique_active_clients, + session_started, + users_hashes, + vpn_sessions, + mixnet_sessions, + unknown_sessions + FROM gateway_session_stats", + ) + .fetch(&mut *conn) + .try_collect::<Vec<GatewaySessionsRecord>>() + .await?; + + let items: Vec<SessionStats> = items + .into_iter() + .map(|item| item.try_into()) + .collect::<anyhow::Result<Vec<_>>>() + .map_err(|e| { + error!("Conversion from database failed: {e}. Invalidly stored data?"); + e + })?; + + Ok(items) +} + +pub(crate) async fn delete_old_records(pool: &DbPool, cut_off: Date) -> anyhow::Result<()> { + let mut conn = pool.acquire().await?; + sqlx::query!("DELETE FROM gateway_session_stats WHERE day <= ?", cut_off) + .execute(&mut *conn) + .await?; + Ok(()) +} diff --git a/nym-node-status-api/nym-node-status-api/src/db/queries/mod.rs b/nym-node-status-api/nym-node-status-api/src/db/queries/mod.rs index fe22ec27aa4..8c87ab2fc03 100644 --- a/nym-node-status-api/nym-node-status-api/src/db/queries/mod.rs +++ b/nym-node-status-api/nym-node-status-api/src/db/queries/mod.rs @@ -1,4 +1,5 @@ mod gateways; +mod gateways_stats; mod misc; mod mixnodes; mod summary; @@ -13,3 +14,5 @@ pub(crate) use mixnodes::{ ensure_mixnodes_still_bonded, get_all_mixnodes, get_daily_stats, insert_mixnodes, }; pub(crate) use summary::{get_summary, get_summary_history}; + +pub(crate) use gateways_stats::{delete_old_records, get_sessions_stats, insert_session_records}; diff --git a/nym-node-status-api/nym-node-status-api/src/db/queries/summary.rs b/nym-node-status-api/nym-node-status-api/src/db/queries/summary.rs index 103712a9a4e..4b2ecd22a59 100644 --- a/nym-node-status-api/nym-node-status-api/src/db/queries/summary.rs +++ b/nym-node-status-api/nym-node-status-api/src/db/queries/summary.rs @@ -8,7 +8,7 @@ use crate::{ models::{ gateway::{ GatewaySummary, GatewaySummaryBlacklisted, GatewaySummaryBonded, - GatewaySummaryExplorer, GatewaySummaryHistorical, + GatewaySummaryHistorical, }, mixnode::{ MixnodeSummary, MixnodeSummaryBlacklisted, MixnodeSummaryBonded, @@ -82,7 +82,6 @@ async fn from_summary_dto(items: Vec<SummaryDto>) -> HttpResult<NetworkSummary> 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_EXPLORER_COUNT: &str = "gateways.explorer.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"; @@ -96,7 +95,6 @@ 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 = [ GATEWAYS_BONDED_COUNT, - GATEWAYS_EXPLORER_COUNT, GATEWAYS_HISTORICAL_COUNT, GATEWAYS_BLACKLISTED_COUNT, MIXNODES_BLACKLISTED_COUNT, @@ -139,10 +137,6 @@ async fn from_summary_dto(items: Vec<SummaryDto>) -> HttpResult<NetworkSummary> .unwrap_or_default(); let gateways_bonded_count: SummaryDto = map.get(GATEWAYS_BONDED_COUNT).cloned().unwrap_or_default(); - let gateways_explorer_count: SummaryDto = map - .get(GATEWAYS_EXPLORER_COUNT) - .cloned() - .unwrap_or_default(); let mixnodes_historical_count: SummaryDto = map .get(MIXNODES_HISTORICAL_COUNT) .cloned() @@ -187,10 +181,6 @@ async fn from_summary_dto(items: Vec<SummaryDto>) -> HttpResult<NetworkSummary> count: to_count_i32(&gateways_historical_count), last_updated_utc: to_timestamp(&gateways_historical_count), }, - explorer: GatewaySummaryExplorer { - count: to_count_i32(&gateways_explorer_count), - last_updated_utc: to_timestamp(&gateways_explorer_count), - }, }, }) } diff --git a/nym-node-status-api/nym-node-status-api/src/http/api/metrics/mod.rs b/nym-node-status-api/nym-node-status-api/src/http/api/metrics/mod.rs new file mode 100644 index 00000000000..8703f928304 --- /dev/null +++ b/nym-node-status-api/nym-node-status-api/src/http/api/metrics/mod.rs @@ -0,0 +1,10 @@ +use axum::Router; + +use crate::http::state::AppState; + +pub(crate) mod sessions; + +pub(crate) fn routes() -> Router<AppState> { + Router::new().nest("/sessions", sessions::routes()) + //eventually add other metrics type +} diff --git a/nym-node-status-api/nym-node-status-api/src/http/api/metrics/sessions.rs b/nym-node-status-api/nym-node-status-api/src/http/api/metrics/sessions.rs new file mode 100644 index 00000000000..e2e4cd9cedc --- /dev/null +++ b/nym-node-status-api/nym-node-status-api/src/http/api/metrics/sessions.rs @@ -0,0 +1,83 @@ +use axum::{ + extract::{Query, State}, + Json, Router, +}; +use time::Date; +use tracing::instrument; + +use crate::http::{ + error::{HttpError, HttpResult}, + models::SessionStats, + state::AppState, + PagedResult, Pagination, +}; + +pub(crate) fn routes() -> Router<AppState> { + Router::new().route("/", axum::routing::get(get_all_sessions)) + // .route("/:node_id", axum::routing::get(get_node_sessions)) + // .route("/:day", axum::routing::get(get_daily_sessions)) +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, utoipa::IntoParams)] +#[into_params(parameter_in = Query)] +pub(crate) struct SessionQueryParams { + size: Option<usize>, + page: Option<usize>, + node_id: Option<String>, + day: Option<String>, +} + +#[utoipa::path( + tag = "Sessions", + get, + params( + SessionQueryParams + ), + path = "/v2/metrics/sessions", + responses( + (status = 200, body = PagedSessionStats) + ) +)] +#[instrument(level = tracing::Level::DEBUG, skip(state))] +async fn get_all_sessions( + Query(SessionQueryParams { + size, + page, + node_id, + day, + }): Query<SessionQueryParams>, + State(state): State<AppState>, +) -> HttpResult<Json<PagedResult<SessionStats>>> { + let db = state.db_pool(); + let res = state.cache().get_sessions_stats(db).await; + + let day_filtered = if let Some(day) = day { + if let Ok(parsed_day) = + Date::parse(&day, &time::format_description::well_known::Iso8601::DATE) + { + res.into_iter().filter(|s| s.day == parsed_day).collect() + } else { + return Err(HttpError::invalid_input(day)); + } + } else { + res + }; + + let day_and_node_filtered = if let Some(node_id) = node_id { + if let Ok(parsed_node_id) = node_id.parse::<u32>() { + day_filtered + .into_iter() + .filter(|s| s.node_id == parsed_node_id) + .collect() + } else { + return Err(HttpError::invalid_input(node_id)); + } + } else { + day_filtered + }; + + Ok(Json(PagedResult::paginate( + Pagination { size, page }, + day_and_node_filtered, + ))) +} diff --git a/nym-node-status-api/nym-node-status-api/src/http/api/mod.rs b/nym-node-status-api/nym-node-status-api/src/http/api/mod.rs index ed24fa80f5a..6483a44519a 100644 --- a/nym-node-status-api/nym-node-status-api/src/http/api/mod.rs +++ b/nym-node-status-api/nym-node-status-api/src/http/api/mod.rs @@ -8,6 +8,7 @@ use utoipa_swagger_ui::SwaggerUi; use crate::http::{server::HttpServer, state::AppState}; pub(crate) mod gateways; +pub(crate) mod metrics; pub(crate) mod mixnodes; pub(crate) mod services; pub(crate) mod summary; @@ -34,7 +35,8 @@ impl RouterBuilder { .nest("/gateways", gateways::routes()) .nest("/mixnodes", mixnodes::routes()) .nest("/services", services::routes()) - .nest("/summary", summary::routes()), + .nest("/summary", summary::routes()) + .nest("/metrics", metrics::routes()), ) .nest( "/internal", diff --git a/nym-node-status-api/nym-node-status-api/src/http/api_docs.rs b/nym-node-status-api/nym-node-status-api/src/http/api_docs.rs index aa86a56ab0a..fec4d25cd5d 100644 --- a/nym-node-status-api/nym-node-status-api/src/http/api_docs.rs +++ b/nym-node-status-api/nym-node-status-api/src/http/api_docs.rs @@ -1,4 +1,4 @@ -use crate::http::{Gateway, GatewaySkinny, Mixnode, Service}; +use crate::http::{Gateway, GatewaySkinny, Mixnode, Service, SessionStats}; use utoipa::OpenApi; use utoipauto::utoipauto; diff --git a/nym-node-status-api/nym-node-status-api/src/http/mod.rs b/nym-node-status-api/nym-node-status-api/src/http/mod.rs index 1cc317337f9..b1a7bf742d6 100644 --- a/nym-node-status-api/nym-node-status-api/src/http/mod.rs +++ b/nym-node-status-api/nym-node-status-api/src/http/mod.rs @@ -1,4 +1,4 @@ -use models::{Gateway, GatewaySkinny, Mixnode, Service}; +use models::{Gateway, GatewaySkinny, Mixnode, Service, SessionStats}; pub(crate) mod api; pub(crate) mod api_docs; @@ -20,6 +20,7 @@ pub(crate) mod state; PagedGatewaySkinny = PagedResult<GatewaySkinny>, PagedMixnode = PagedResult<Mixnode>, PagedService = PagedResult<Service>, + PagedSessionStats = PagedResult<SessionStats> )] pub struct PagedResult<T> { pub page: usize, diff --git a/nym-node-status-api/nym-node-status-api/src/http/models.rs b/nym-node-status-api/nym-node-status-api/src/http/models.rs index aee2124b6c0..276424d2b01 100644 --- a/nym-node-status-api/nym-node-status-api/src/http/models.rs +++ b/nym-node-status-api/nym-node-status-api/src/http/models.rs @@ -74,3 +74,17 @@ pub(crate) struct SummaryHistory { pub value_json: serde_json::Value, pub timestamp_utc: String, } + +#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)] +pub struct SessionStats { + pub gateway_identity_key: String, + pub node_id: u32, + #[serde(with = "nym_serde_helpers::date")] + pub day: time::Date, + pub unique_active_clients: i64, + pub session_started: i64, + pub users_hashes: Option<serde_json::Value>, + pub vpn_sessions: Option<serde_json::Value>, + pub mixnet_sessions: Option<serde_json::Value>, + pub unknown_sessions: Option<serde_json::Value>, +} diff --git a/nym-node-status-api/nym-node-status-api/src/http/state.rs b/nym-node-status-api/nym-node-status-api/src/http/state.rs index 393da5c0b87..0c79c0e16f2 100644 --- a/nym-node-status-api/nym-node-status-api/src/http/state.rs +++ b/nym-node-status-api/nym-node-status-api/src/http/state.rs @@ -9,6 +9,8 @@ use crate::{ http::models::{DailyStats, Gateway, Mixnode, SummaryHistory}, }; +use super::models::SessionStats; + #[derive(Debug, Clone)] pub(crate) struct AppState { db_pool: DbPool, @@ -53,6 +55,7 @@ static GATEWAYS_LIST_KEY: &str = "gateways"; static MIXNODES_LIST_KEY: &str = "mixnodes"; static MIXSTATS_LIST_KEY: &str = "mixstats"; static SUMMARY_HISTORY_LIST_KEY: &str = "summary-history"; +static SESSION_STATS_LIST_KEY: &str = "session-stats"; #[derive(Debug, Clone)] pub(crate) struct HttpCache { @@ -60,6 +63,7 @@ pub(crate) struct HttpCache { mixnodes: Cache<String, Arc<RwLock<Vec<Mixnode>>>>, mixstats: Cache<String, Arc<RwLock<Vec<DailyStats>>>>, history: Cache<String, Arc<RwLock<Vec<SummaryHistory>>>>, + session_stats: Cache<String, Arc<RwLock<Vec<SessionStats>>>>, } impl HttpCache { @@ -81,6 +85,10 @@ impl HttpCache { .max_capacity(2) .time_to_live(Duration::from_secs(ttl_seconds)) .build(), + session_stats: Cache::builder() + .max_capacity(2) + .time_to_live(Duration::from_secs(ttl_seconds)) + .build(), } } @@ -238,4 +246,39 @@ impl HttpCache { }) .await } + + pub async fn get_sessions_stats(&self, db: &DbPool) -> Vec<SessionStats> { + match self.session_stats.get(SESSION_STATS_LIST_KEY).await { + Some(guard) => { + let read_lock = guard.read().await; + read_lock.to_vec() + } + None => { + let session_stats = crate::db::queries::get_sessions_stats(db) + .await + .unwrap_or_default(); + self.upsert_sessions_stats(session_stats.clone()).await; + session_stats + } + } + } + + pub async fn upsert_sessions_stats( + &self, + session_stats: Vec<SessionStats>, + ) -> Entry<String, Arc<RwLock<Vec<SessionStats>>>> { + self.session_stats + .entry_by_ref(SESSION_STATS_LIST_KEY) + .and_upsert_with(|maybe_entry| async { + if let Some(entry) = maybe_entry { + let v = entry.into_value(); + let mut guard = v.write().await; + *guard = session_stats; + v.clone() + } else { + Arc::new(RwLock::new(session_stats)) + } + }) + .await + } } diff --git a/nym-node-status-api/nym-node-status-api/src/main.rs b/nym-node-status-api/nym-node-status-api/src/main.rs index 6fcdf344732..4fd01d79609 100644 --- a/nym-node-status-api/nym-node-status-api/src/main.rs +++ b/nym-node-status-api/nym-node-status-api/src/main.rs @@ -7,6 +7,7 @@ mod db; mod http; mod logging; mod monitor; +mod node_scraper; mod testruns; #[tokio::main] @@ -28,13 +29,15 @@ async fn main() -> anyhow::Result<()> { let storage = db::Storage::init(connection_url).await?; let db_pool = storage.pool_owned(); let args_clone = args.clone(); + tokio::spawn(async move { monitor::spawn_in_background( db_pool, - args_clone.explorer_client_timeout, args_clone.nym_api_client_timeout, - &args_clone.nyxd_addr, + args_clone.nyxd_addr, args_clone.monitor_refresh_interval, + args_clone.ipinfo_api_token, + args_clone.geodata_ttl, ) .await; tracing::info!("Started monitor task"); @@ -42,6 +45,12 @@ async fn main() -> anyhow::Result<()> { testruns::spawn(storage.pool_owned(), args.testruns_refresh_interval).await; + let db_pool_scraper = storage.pool_owned(); + tokio::spawn(async move { + node_scraper::spawn_in_background(db_pool_scraper, args_clone.nym_api_client_timeout).await; + tracing::info!("Started metrics scraper task"); + }); + let shutdown_handles = http::server::start_http_api( storage.pool_owned(), args.http_port, diff --git a/nym-node-status-api/nym-node-status-api/src/monitor/geodata.rs b/nym-node-status-api/nym-node-status-api/src/monitor/geodata.rs new file mode 100644 index 00000000000..369acee1a75 --- /dev/null +++ b/nym-node-status-api/nym-node-status-api/src/monitor/geodata.rs @@ -0,0 +1,184 @@ +use cosmwasm_std::{Addr, Coin}; +use serde::{Deserialize, Deserializer, Serialize}; + +pub(crate) struct IpInfoClient { + client: reqwest::Client, + token: String, +} + +impl IpInfoClient { + pub(crate) fn new(token: impl Into<String>) -> Self { + let client = reqwest::Client::new(); + let token = token.into(); + + Self { client, token } + } + + pub(crate) async fn locate_ip(&self, ip: impl AsRef<str>) -> anyhow::Result<Location> { + let url = format!("https://ipinfo.io/{}?token={}", ip.as_ref(), &self.token); + let response = self + .client + .get(url) + .send() + .await + // map non 2xx responses to error + .and_then(|res| res.error_for_status()) + .map_err(|err| { + if matches!(err.status(), Some(reqwest::StatusCode::TOO_MANY_REQUESTS)) { + tracing::error!("ipinfo rate limit exceeded"); + } + anyhow::Error::from(err) + })?; + let raw_response = response.text().await?; + let response: LocationResponse = + serde_json::from_str(&raw_response).inspect_err(|e| tracing::error!("{e}"))?; + let location = response.into(); + + Ok(location) + } + + /// check DOESN'T consume bandwidth allowance + pub(crate) async fn check_remaining_bandwidth( + &self, + ) -> anyhow::Result<ipinfo::MeResponseRequests> { + let url = format!("https://ipinfo.io/me?token={}", &self.token); + let response = self + .client + .get(url) + .send() + .await + // map non 2xx responses to error + .and_then(|res| res.error_for_status()) + .map_err(|err| { + if matches!(err.status(), Some(reqwest::StatusCode::TOO_MANY_REQUESTS)) { + tracing::error!("ipinfo rate limit exceeded"); + } + anyhow::Error::from(err) + })?; + let response: ipinfo::MeResponse = response.json().await?; + + Ok(response.requests) + } +} + +#[derive(Debug, Clone, Serialize)] +pub(crate) struct NodeGeoData { + pub(crate) identity_key: String, + pub(crate) owner: Addr, + pub(crate) pledge_amount: Coin, + pub(crate) location: Location, +} + +#[derive(Debug, Clone, Default, Serialize)] +pub(crate) struct Location { + pub(crate) two_letter_iso_country_code: String, + #[serde(flatten)] + pub(crate) location: Coordinates, +} + +impl From<LocationResponse> for Location { + fn from(value: LocationResponse) -> Self { + Self { + two_letter_iso_country_code: value.two_letter_iso_country_code, + location: value.loc, + } + } +} + +#[derive(Debug, Clone, Deserialize)] +pub(crate) struct LocationResponse { + #[serde(rename = "country")] + pub(crate) two_letter_iso_country_code: String, + #[serde(deserialize_with = "deserialize_loc")] + pub(crate) loc: Coordinates, +} + +fn deserialize_loc<'de, D>(deserializer: D) -> Result<Coordinates, D::Error> +where + D: Deserializer<'de>, +{ + let loc_raw = String::deserialize(deserializer)?; + match loc_raw.split_once(',') { + Some((lat, long)) => Ok(Coordinates { + latitude: lat.parse().map_err(serde::de::Error::custom)?, + longitude: long.parse().map_err(serde::de::Error::custom)?, + }), + None => Err(serde::de::Error::custom("coordinates")), + } +} + +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +pub(crate) struct Coordinates { + pub(crate) latitude: f64, + pub(crate) longitude: f64, +} + +impl Location { + pub(crate) fn empty() -> Self { + Self { + two_letter_iso_country_code: String::new(), + location: Coordinates::default(), + } + } +} + +pub(crate) mod ipinfo { + use super::*; + + // clippy doesn't understand it's used for typed deserialization + #[allow(dead_code)] + #[derive(Debug, Clone, Deserialize)] + /// `/me` is undocumented in their developers page + /// https://ipinfo.io/developers/responses + /// but explained here + /// https://community.ipinfo.io/t/easy-way-to-check-allowance-usage/5755/2 + pub(crate) struct MeResponse { + token: String, + pub(crate) requests: MeResponseRequests, + } + + // clippy doesn't understand it's used for typed deserialization + #[allow(dead_code)] + #[derive(Debug, Clone, Deserialize)] + pub(crate) struct MeResponseRequests { + pub(crate) day: u64, + pub(crate) month: u64, + pub(crate) limit: u64, + pub(crate) remaining: u64, + } +} + +#[cfg(test)] +mod api_regression { + + use super::*; + use std::{env::var, sync::LazyLock}; + + static IPINFO_TOKEN: LazyLock<String> = LazyLock::new(|| var("IPINFO_API_TOKEN").unwrap()); + + #[tokio::test] + async fn should_parse_response() { + let client = IpInfoClient::new(&(*IPINFO_TOKEN)); + let my_ip = reqwest::get("https://api.ipify.org") + .await + .expect("Couldn't get own IP") + .text() + .await + .unwrap(); + + let location_result = client.locate_ip(my_ip).await; + assert!(location_result.is_ok(), "Did ipinfo response change?"); + + assert!( + client.check_remaining_bandwidth().await.is_ok(), + "Failed to check remaining bandwidth?" + ); + + // when serialized, these fields should be present because they're exposed over API + let location_result = location_result.unwrap(); + let json = serde_json::to_value(&location_result).unwrap(); + assert!(json.get("two_letter_iso_country_code").is_some()); + assert!(json.get("latitude").is_some()); + assert!(json.get("longitude").is_some()); + } +} diff --git a/nym-node-status-api/nym-node-status-api/src/monitor/mod.rs b/nym-node-status-api/nym-node-status-api/src/monitor/mod.rs index 7b6c85b6d56..5c8bc86deb6 100644 --- a/nym-node-status-api/nym-node-status-api/src/monitor/mod.rs +++ b/nym-node-status-api/nym-node-status-api/src/monitor/mod.rs @@ -2,16 +2,17 @@ use crate::db::models::{ gateway, mixnode, GatewayRecord, MixnodeRecord, NetworkSummary, GATEWAYS_BLACKLISTED_COUNT, - GATEWAYS_BONDED_COUNT, GATEWAYS_EXPLORER_COUNT, GATEWAYS_HISTORICAL_COUNT, - MIXNODES_BLACKLISTED_COUNT, MIXNODES_BONDED_ACTIVE, MIXNODES_BONDED_COUNT, - MIXNODES_BONDED_INACTIVE, MIXNODES_BONDED_RESERVE, MIXNODES_HISTORICAL_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, }; use crate::db::{queries, DbPool}; +use crate::monitor::geodata::{Location, NodeGeoData}; use anyhow::anyhow; use cosmwasm_std::Decimal; -use nym_explorer_client::{ExplorerClient, PrettyDetailedGatewayBond}; +use moka::future::Cache; use nym_network_defaults::NymNetworkDetails; -use nym_validator_client::client::NymApiClientExt; +use nym_validator_client::client::{NodeId, NymApiClientExt}; use nym_validator_client::models::{ LegacyDescribedMixNode, MixNodeBondAnnotated, NymNodeDescription, }; @@ -20,40 +21,55 @@ use nym_validator_client::nyxd::contract_traits::PagedMixnetQueryClient; use nym_validator_client::nyxd::{AccountId, NyxdClient}; use nym_validator_client::NymApiClient; use reqwest::Url; -use std::collections::HashSet; +use std::collections::{HashMap, HashSet}; use std::str::FromStr; use tokio::time::Duration; use tracing::instrument; +pub(crate) use geodata::IpInfoClient; + +mod geodata; + // TODO dz should be configurable const FAILURE_RETRY_DELAY: Duration = Duration::from_secs(60); static DELEGATION_PROGRAM_WALLET: &str = "n1rnxpdpx3kldygsklfft0gech7fhfcux4zst5lw"; +struct Monitor { + db_pool: DbPool, + network_details: NymNetworkDetails, + nym_api_client_timeout: Duration, + nyxd_addr: Url, + ipinfo: IpInfoClient, + geocache: Cache<NodeId, Location>, +} + // TODO dz: query many NYM APIs: // multiple instances running directory cache, ask sachin #[instrument(level = "debug", name = "data_monitor", skip_all)] pub(crate) async fn spawn_in_background( db_pool: DbPool, - explorer_client_timeout: Duration, nym_api_client_timeout: Duration, - nyxd_addr: &Url, + nyxd_addr: Url, refresh_interval: Duration, + ipinfo_api_token: String, + geodata_ttl: Duration, ) { - let network_defaults = nym_network_defaults::NymNetworkDetails::new_from_env(); + let geocache = Cache::builder().time_to_live(geodata_ttl).build(); + let ipinfo = IpInfoClient::new(ipinfo_api_token.clone()); + let mut monitor = Monitor { + db_pool, + network_details: nym_network_defaults::NymNetworkDetails::new_from_env(), + nym_api_client_timeout, + nyxd_addr, + ipinfo, + geocache, + }; loop { tracing::info!("Refreshing node info..."); - if let Err(e) = run( - &db_pool, - &network_defaults, - explorer_client_timeout, - nym_api_client_timeout, - nyxd_addr, - ) - .await - { + if let Err(e) = monitor.run().await { tracing::error!( "Monitor run failed: {e}, retrying in {}s...", FAILURE_RETRY_DELAY.as_secs() @@ -70,345 +86,371 @@ pub(crate) async fn spawn_in_background( } } -async fn run( - pool: &DbPool, - network_details: &NymNetworkDetails, - explorer_client_timeout: Duration, - nym_api_client_timeout: Duration, - nyxd_addr: &Url, -) -> anyhow::Result<()> { - let default_api_url = network_details - .endpoints - .first() - .expect("rust sdk mainnet default incorrectly configured") - .api_url() - .clone() - .expect("rust sdk mainnet default missing api_url"); - let default_explorer_url = network_details.explorer_api.clone().map(|url| { - url.parse() - .expect("rust sdk mainnet default explorer url not parseable") - }); - - // TODO dz replace explorer api with ipinfo.io - let default_explorer_url = - default_explorer_url.expect("explorer url missing in network config"); - let explorer_client = - ExplorerClient::new_with_timeout(default_explorer_url, explorer_client_timeout)?; - let explorer_gateways = explorer_client - .unstable_get_gateways() - .await - .log_error("unstable_get_gateways")?; - - let api_client = NymApiClient::new_with_timeout(default_api_url, nym_api_client_timeout); - - let all_nodes = api_client - .get_all_described_nodes() - .await - .log_error("get_all_described_nodes")?; - tracing::debug!("Fetched {} total nodes", all_nodes.len()); - - let gateways = all_nodes - .iter() - .filter(|node| node.description.declared_role.entry) - .collect::<Vec<_>>(); - tracing::debug!("Of those, {} gateways", gateways.len()); - for gw in gateways.iter() { - tracing::debug!("{}", gw.ed25519_identity_key().to_base58_string()); - } +impl Monitor { + async fn run(&mut self) -> anyhow::Result<()> { + self.check_ipinfo_bandwidth().await; - let mixnodes = all_nodes - .iter() - .filter(|node| node.description.declared_role.mixnode) - .collect::<Vec<_>>(); - tracing::debug!("Of those, {} mixnodes", mixnodes.len()); - - log_gw_in_explorer_not_api(explorer_gateways.as_slice(), gateways.as_slice()); + let default_api_url = self + .network_details + .endpoints + .first() + .expect("rust sdk mainnet default incorrectly configured") + .api_url() + .clone() + .expect("rust sdk mainnet default missing api_url"); - let all_skimmed_nodes = api_client - .get_all_basic_nodes(None) - .await - .log_error("get_all_basic_nodes")?; + let api_client = + NymApiClient::new_with_timeout(default_api_url, self.nym_api_client_timeout); - let mixnodes = api_client - .get_cached_mixnodes() - .await - .log_error("get_cached_mixnodes")?; - tracing::debug!("Fetched {} mixnodes", mixnodes.len()); + let all_nodes = api_client + .get_all_described_nodes() + .await + .log_error("get_all_described_nodes")?; + tracing::debug!("Fetched {} total nodes", all_nodes.len()); - // let gateways_blacklisted = gateways.iter().filter(|gw|gw.) - 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 + let gateways = all_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)) + // for faster reads + .collect::<HashMap<_, _>>(); + + let mut gateway_geodata = Vec::new(); + for gateway in gateways.iter() { + if let Some(node_info) = bonded_node_info.get(&gateway.node_id) { + 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(), + location: self.location_cached(gateway).await, + }; + gateway_geodata.push(gw_geodata); } - }) - .collect::<HashSet<_>>(); - - // Cached mixnodes don't include blacklisted nodes - // We need that to calculate the total locked tokens later - let mixnodes = api_client - .nym_api - .get_mixnodes_detailed_unfiltered() - .await - .log_error("get_mixnodes_detailed_unfiltered")?; - let mixnodes_described = api_client - .nym_api - .get_mixnodes_described() - .await - .log_error("get_mixnodes_described")?; - let mixnodes_active = api_client - .nym_api - .get_active_mixnodes() - .await - .log_error("get_active_mixnodes")?; - let delegation_program_members = - get_delegation_program_details(network_details, nyxd_addr).await?; - - // keep stats for later - let count_bonded_mixnodes = mixnodes.len(); - let count_bonded_gateways = gateways.len(); - let count_explorer_gateways = explorer_gateways.len(); - let count_bonded_mixnodes_active = mixnodes_active.len(); - - let gateway_records = prepare_gateway_data( - &gateways, - &gateways_blacklisted, - explorer_gateways, - all_skimmed_nodes, - )?; - queries::insert_gateways(pool, gateway_records) - .await - .map(|_| { - tracing::debug!("Gateway info written to DB!"); - })?; - - // instead of counting blacklisted GWs returned from API cache, count from the active set - 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()) + // 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 + .nym_api + .get_mixnodes_detailed_unfiltered() + .await + .log_error("get_mixnodes_detailed_unfiltered")?; + let mixnodes_described = api_client + .nym_api + .get_mixnodes_described() + .await + .log_error("get_mixnodes_described")?; + let mixnodes_active = api_client + .nym_api + .get_basic_active_mixing_assigned_nodes(false, None, None) + .await + .log_error("get_active_mixnodes")? + .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 count_bonded_gateways = gateways.len(); + let count_bonded_mixnodes_active = mixnodes_active.len(); + + let gateway_records = self.prepare_gateway_data( + &gateways, + &gateways_blacklisted, + gateway_geodata, + all_skimmed_nodes, + )?; + + let pool = self.db_pool.clone(); + queries::insert_gateways(&pool, gateway_records) .await .map(|_| { - tracing::debug!( - "Gateway blacklist info written to DB! {} blacklisted by Nym API", - count_gateways_blacklisted - ) + tracing::debug!("Gateway info written to DB!"); })?; - } - let mixnode_records = - prepare_mixnode_data(&mixnodes, mixnodes_described, delegation_program_members)?; - queries::insert_mixnodes(pool, mixnode_records) - .await - .map(|_| { - tracing::debug!("Mixnode info written to DB!"); - })?; - - let count_mixnodes_blacklisted = 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, &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?; - - // - // write summary keys and values to table - // - - 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), - (GATEWAYS_EXPLORER_COUNT, &count_explorer_gateways), - (MIXNODES_HISTORICAL_COUNT, &all_historical_mixnodes), - (GATEWAYS_HISTORICAL_COUNT, &all_historical_gateways), - (GATEWAYS_BLACKLISTED_COUNT, &count_gateways_blacklisted), - ]; - - let last_updated = chrono::offset::Utc::now(); - let last_updated_utc = last_updated.timestamp().to_string(); - let network_summary = NetworkSummary { - 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()?, - last_updated_utc: last_updated_utc.to_owned(), - }, - historical: mixnode::MixnodeSummaryHistorical { - count: all_historical_mixnodes.cast_checked()?, - last_updated_utc: last_updated_utc.to_owned(), - }, - }, - 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()?, - last_updated_utc: last_updated_utc.to_owned(), - }, - historical: gateway::GatewaySummaryHistorical { - count: all_historical_gateways.cast_checked()?, - last_updated_utc: last_updated_utc.to_owned(), + 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_described, + delegation_program_members, + )?; + queries::insert_mixnodes(&pool, mixnode_records) + .await + .map(|_| { + 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?; + + // + // write summary keys and values to table + // + + 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), + ]; + + let last_updated = chrono::offset::Utc::now(); + let last_updated_utc = last_updated.timestamp().to_string(); + let network_summary = NetworkSummary { + 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()?, + last_updated_utc: last_updated_utc.to_owned(), + }, + historical: mixnode::MixnodeSummaryHistorical { + count: all_historical_mixnodes.cast_checked()?, + last_updated_utc: last_updated_utc.to_owned(), + }, }, - explorer: gateway::GatewaySummaryExplorer { - count: count_explorer_gateways.cast_checked()?, - last_updated_utc: last_updated_utc.to_owned(), + 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()?, + last_updated_utc: last_updated_utc.to_owned(), + }, + historical: gateway::GatewaySummaryHistorical { + count: all_historical_gateways.cast_checked()?, + last_updated_utc: last_updated_utc.to_owned(), + }, }, - }, - }; + }; - queries::insert_summaries(pool, &nodes_summary, &network_summary, last_updated).await?; + queries::insert_summaries(&pool, &nodes_summary, &network_summary, last_updated).await?; - let mut log_lines: Vec<String> = vec![]; - for (key, value) in nodes_summary.iter() { - log_lines.push(format!("{} = {}", key, value)); + let mut log_lines: Vec<String> = vec![]; + 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")); + + Ok(()) } - 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")); - - Ok(()) -} - -fn prepare_gateway_data( - gateways: &[&NymNodeDescription], - gateways_blacklisted: &HashSet<String>, - explorer_gateways: Vec<PrettyDetailedGatewayBond>, - skimmed_gateways: Vec<SkimmedNode>, -) -> anyhow::Result<Vec<GatewayRecord>> { - let mut gateway_records = Vec::new(); - for gateway in gateways { - 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)?; + #[instrument(level = "debug", skip_all)] + async fn location_cached(&mut self, node: &NymNodeDescription) -> Location { + let node_id = node.node_id; + + match self.geocache.get(&node_id).await { + Some(location) => return location, + None => { + for ip in node.description.host_information.ip_address.iter() { + if let Ok(location) = self.ipinfo.locate_ip(ip.to_string()).await { + self.geocache.insert(node_id, location.clone()).await; + return location; + } + } + // if no data could be retrieved + tracing::debug!("No geodata could be retrieved for {}", node_id); + Location::empty() + } + } + } - let explorer_pretty_bond = explorer_gateways - .iter() - .find(|g| g.gateway.identity_key.eq(&identity_key)); - let explorer_pretty_bond = explorer_pretty_bond.and_then(|g| serde_json::to_string(g).ok()); + fn prepare_gateway_data( + &self, + gateways: &[&NymNodeDescription], + gateways_blacklisted: &HashSet<String>, + gateway_geodata: Vec<NodeGeoData>, + skimmed_gateways: Vec<SkimmedNode>, + ) -> anyhow::Result<Vec<GatewayRecord>> { + let mut gateway_records = Vec::new(); + + for gateway in gateways { + 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)?; + + let explorer_pretty_bond = gateway_geodata + .iter() + .find(|g| g.identity_key.eq(&identity_key)); + let explorer_pretty_bond = + explorer_pretty_bond.and_then(|g| serde_json::to_string(g).ok()); + + let performance = skimmed_gateways + .iter() + .find(|g| { + g.ed25519_identity_pubkey + .to_base58_string() + .eq(&identity_key) + }) + .map(|g| g.performance) + .unwrap_or_default() + .round_to_integer(); + + gateway_records.push(GatewayRecord { + identity_key: identity_key.to_owned(), + bonded, + blacklisted, + self_described, + explorer_pretty_bond, + last_updated_utc, + performance, + }); + } - let performance = skimmed_gateways - .iter() - .find(|g| { - g.ed25519_identity_pubkey - .to_base58_string() - .eq(&identity_key) - }) - .map(|g| g.performance) - .unwrap_or_default() - .round_to_integer(); - - gateway_records.push(GatewayRecord { - identity_key: identity_key.to_owned(), - bonded, - blacklisted, - self_described, - explorer_pretty_bond, - last_updated_utc, - performance, - }); + Ok(gateway_records) } - Ok(gateway_records) -} + fn prepare_mixnode_data( + &self, + mixnodes: &[MixNodeBondAnnotated], + mixnodes_described: Vec<LegacyDescribedMixNode>, + delegation_program_members: Vec<u32>, + ) -> anyhow::Result<Vec<MixnodeRecord>> { + let mut mixnode_records = Vec::new(); + + for mixnode in mixnodes { + let mix_id = mixnode.mix_id(); + 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; + // Contains all the information including what's above + let full_details = serde_json::to_string(&mixnode)?; + + let mixnode_described = mixnodes_described.iter().find(|m| m.bond.mix_id == mix_id); + let self_described = mixnode_described.and_then(|v| serde_json::to_string(v).ok()); + let is_dp_delegatee = delegation_program_members.contains(&mix_id); + + let last_updated_utc = chrono::offset::Utc::now().timestamp(); + + mixnode_records.push(MixnodeRecord { + mix_id, + identity_key: identity_key.to_owned(), + bonded, + total_stake, + host, + http_port, + blacklisted, + full_details, + self_described, + last_updated_utc, + is_dp_delegatee, + }); + } -fn prepare_mixnode_data( - mixnodes: &[MixNodeBondAnnotated], - mixnodes_described: Vec<LegacyDescribedMixNode>, - delegation_program_members: Vec<u32>, -) -> anyhow::Result<Vec<MixnodeRecord>> { - let mut mixnode_records = Vec::new(); - - for mixnode in mixnodes { - let mix_id = mixnode.mix_id(); - 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; - // Contains all the information including what's above - let full_details = serde_json::to_string(&mixnode)?; - - let mixnode_described = mixnodes_described.iter().find(|m| m.bond.mix_id == mix_id); - let self_described = mixnode_described.and_then(|v| serde_json::to_string(v).ok()); - let is_dp_delegatee = delegation_program_members.contains(&mix_id); - - let last_updated_utc = chrono::offset::Utc::now().timestamp(); - - mixnode_records.push(MixnodeRecord { - mix_id, - identity_key: identity_key.to_owned(), - bonded, - total_stake, - host, - http_port, - blacklisted, - full_details, - self_described, - last_updated_utc, - is_dp_delegatee, - }); + Ok(mixnode_records) } - Ok(mixnode_records) -} - -fn log_gw_in_explorer_not_api( - explorer: &[PrettyDetailedGatewayBond], - api_gateways: &[&NymNodeDescription], -) { - let api_gateways = api_gateways - .iter() - .map(|gw| gw.ed25519_identity_key().to_base58_string()) - .collect::<HashSet<_>>(); - let explorer_only = explorer - .iter() - .filter(|gw| !api_gateways.contains(&gw.gateway.identity_key.to_string())) - .collect::<Vec<_>>(); - - tracing::debug!( - "Gateways listed by explorer but not by Nym API: {}", - explorer_only.len() - ); - for gw in explorer_only.iter() { - tracing::debug!("{}", gw.gateway.identity_key.to_string()); + async fn check_ipinfo_bandwidth(&self) { + match self.ipinfo.check_remaining_bandwidth().await { + Ok(bandwidth) => { + tracing::info!( + "ipinfo monthly bandwidth: {}/{} spent", + bandwidth.month, + bandwidth.limit + ); + } + Err(err) => { + tracing::debug!("Couldn't check ipinfo bandwidth: {}", err); + } + } } } diff --git a/nym-node-status-api/nym-node-status-api/src/node_scraper/error.rs b/nym-node-status-api/nym-node-status-api/src/node_scraper/error.rs new file mode 100644 index 00000000000..42f96647e3e --- /dev/null +++ b/nym-node-status-api/nym-node-status-api/src/node_scraper/error.rs @@ -0,0 +1,18 @@ +use nym_network_defaults::DEFAULT_NYM_NODE_HTTP_PORT; +use nym_node_requests::api::client::NymNodeApiClientError; +use nym_validator_client::client::NodeId; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum NodeScraperError { + #[error("node {node_id} has provided malformed host information ({host}: {source}")] + MalformedHost { + host: String, + node_id: NodeId, + #[source] + source: NymNodeApiClientError, + }, + + #[error("node {node_id} with host '{host}' doesn't seem to expose its declared http port nor any of the standard API ports, i.e.: 80, 443 or {}", DEFAULT_NYM_NODE_HTTP_PORT)] + NoHttpPortsAvailable { host: String, node_id: NodeId }, +} diff --git a/nym-node-status-api/nym-node-status-api/src/node_scraper/mod.rs b/nym-node-status-api/nym-node-status-api/src/node_scraper/mod.rs new file mode 100644 index 00000000000..8d0664316fa --- /dev/null +++ b/nym-node-status-api/nym-node-status-api/src/node_scraper/mod.rs @@ -0,0 +1,278 @@ +use crate::db::{models::GatewaySessionsRecord, queries, DbPool}; +use error::NodeScraperError; +use nym_network_defaults::{NymNetworkDetails, DEFAULT_NYM_NODE_HTTP_PORT}; +use nym_node_requests::api::{client::NymNodeApiClientExt, v1::metrics::models::SessionStats}; +use nym_validator_client::{ + client::{NodeId, NymNodeDetails}, + models::{DescribedNodeType, NymNodeDescription}, + NymApiClient, +}; +use time::OffsetDateTime; + +use nym_node_metrics::entry::SessionType; +use std::collections::HashMap; +use tokio::time::Duration; +use tracing::instrument; + +mod error; + +const FAILURE_RETRY_DELAY: Duration = Duration::from_secs(60); +const REFRESH_INTERVAL: Duration = Duration::from_secs(60 * 60 * 6); //6h, data only update once a day +const STALE_DURATION: Duration = Duration::from_secs(86400 * 365); //one year + +#[instrument(level = "debug", name = "node_scraper", skip_all)] +pub(crate) async fn spawn_in_background(db_pool: DbPool, nym_api_client_timeout: Duration) { + let network_defaults = nym_network_defaults::NymNetworkDetails::new_from_env(); + + loop { + //No graceful shutdown? + tracing::info!("Refreshing node self-described metrics..."); + + if let Err(e) = run(&db_pool, &network_defaults, nym_api_client_timeout).await { + tracing::error!( + "Metrics collection failed: {e}, retrying in {}s...", + FAILURE_RETRY_DELAY.as_secs() + ); + + tokio::time::sleep(FAILURE_RETRY_DELAY).await; + } else { + tracing::info!( + "Metrics successfully collected, sleeping for {}s...", + REFRESH_INTERVAL.as_secs() + ); + tokio::time::sleep(REFRESH_INTERVAL).await; + } + } +} + +async fn run( + pool: &DbPool, + network_details: &NymNetworkDetails, + nym_api_client_timeout: Duration, +) -> anyhow::Result<()> { + let default_api_url = network_details + .endpoints + .first() + .expect("rust sdk mainnet default incorrectly configured") + .api_url() + .clone() + .expect("rust sdk mainnet default missing api_url"); + + let api_client = NymApiClient::new_with_timeout(default_api_url, nym_api_client_timeout); + + //SW TBC what nodes exactly need to be scraped, the skimmed node endpoint seems to return more nodes + let bonded_nodes = api_client.get_all_bonded_nym_nodes().await?; + let all_nodes = api_client.get_all_described_nodes().await?; //legacy node that did not upgrade the contract bond yet + tracing::debug!("Fetched {} total nodes", all_nodes.len()); + + let mut nodes_to_scrape: HashMap<NodeId, MetricsScrapingData> = bonded_nodes + .into_iter() + .map(|n| (n.node_id(), n.into())) + .collect(); + + all_nodes + .into_iter() + .filter(|n| n.contract_node_type != DescribedNodeType::LegacyMixnode) + .for_each(|n| { + nodes_to_scrape.entry(n.node_id).or_insert_with(|| n.into()); + }); + tracing::debug!("Will try to scrape {} nodes", nodes_to_scrape.len()); + + let mut session_records = Vec::new(); + for n in nodes_to_scrape.into_values() { + if let Some(stat) = n.try_scrape_metrics().await { + session_records.push(prepare_session_data(stat, &n)); + } + } + + queries::insert_session_records(pool, session_records) + .await + .map(|_| { + tracing::debug!("Session info written to DB!"); + })?; + let cut_off_date = (OffsetDateTime::now_utc() - STALE_DURATION).date(); + queries::delete_old_records(pool, cut_off_date) + .await + .map(|_| { + tracing::debug!("Cleared old data before {}", cut_off_date); + })?; + + Ok(()) +} + +#[derive(Debug)] +struct MetricsScrapingData { + host: String, + node_id: NodeId, + id_key: String, + port: Option<u16>, +} + +impl MetricsScrapingData { + pub fn new( + host: impl Into<String>, + node_id: NodeId, + id_key: String, + port: Option<u16>, + ) -> Self { + MetricsScrapingData { + host: host.into(), + node_id, + id_key, + port, + } + } + + async fn try_scrape_metrics(&self) -> Option<SessionStats> { + match self.try_get_client().await { + Ok(client) => { + match client.get_sessions_metrics().await { + Ok(session_stats) => { + if session_stats.update_time != OffsetDateTime::UNIX_EPOCH { + Some(session_stats) + } else { + //means no data + None + } + } + Err(e) => { + tracing::error!("[metrics scraper]: {e}"); + None + } + } + } + Err(e) => { + tracing::error!("[metrics scraper]: {e}"); + None + } + } + } + + async fn try_get_client(&self) -> Result<nym_node_requests::api::Client, NodeScraperError> { + // first try the standard port in case the operator didn't put the node behind the proxy, + // then default https (443) + // finally default http (80) + let mut addresses_to_try = vec![ + format!("http://{0}:{DEFAULT_NYM_NODE_HTTP_PORT}", self.host), // 'standard' nym-node + format!("https://{0}", self.host), // node behind https proxy (443) + format!("http://{0}", self.host), // node behind http proxy (80) + ]; + + // note: I removed 'standard' legacy mixnode port because it should now be automatically pulled via + // the 'custom_port' since it should have been present in the contract. + + if let Some(port) = self.port { + addresses_to_try.insert(0, format!("http://{0}:{port}", self.host)); + } + + for address in addresses_to_try { + // if provided host was malformed, no point in continuing + let client = match nym_node_requests::api::Client::builder(address).and_then(|b| { + b.with_timeout(Duration::from_secs(5)) + .with_user_agent("node-status-api-metrics-scraper") + .build() + }) { + Ok(client) => client, + Err(err) => { + return Err(NodeScraperError::MalformedHost { + host: self.host.to_string(), + node_id: self.node_id, + source: err, + }); + } + }; + + if let Ok(health) = client.get_health().await { + if health.status.is_up() { + return Ok(client); + } + } + } + + Err(NodeScraperError::NoHttpPortsAvailable { + host: self.host.to_string(), + node_id: self.node_id, + }) + } +} + +impl From<NymNodeDetails> for MetricsScrapingData { + fn from(value: NymNodeDetails) -> Self { + MetricsScrapingData::new( + value.bond_information.node.host.clone(), + value.node_id(), + value.bond_information.node.identity_key, + value.bond_information.node.custom_http_port, + ) + } +} + +impl From<NymNodeDescription> for MetricsScrapingData { + fn from(value: NymNodeDescription) -> Self { + MetricsScrapingData::new( + value.description.host_information.ip_address[0].to_string(), + value.node_id, + value.ed25519_identity_key().to_base58_string(), + None, + ) + } +} + +fn prepare_session_data( + stat: SessionStats, + node_data: &MetricsScrapingData, +) -> GatewaySessionsRecord { + let users_hashes = if !stat.unique_active_users_hashes.is_empty() { + Some(serde_json::to_string(&stat.unique_active_users_hashes).unwrap()) + } else { + None + }; + let vpn_durations = stat + .sessions + .iter() + .filter(|s| SessionType::from_string(&s.typ) == SessionType::Vpn) + .map(|s| s.duration_ms) + .collect::<Vec<_>>(); + + let mixnet_durations = stat + .sessions + .iter() + .filter(|s| SessionType::from_string(&s.typ) == SessionType::Mixnet) + .map(|s| s.duration_ms) + .collect::<Vec<_>>(); + + let unkown_durations = stat + .sessions + .iter() + .filter(|s| SessionType::from_string(&s.typ) == SessionType::Unknown) + .map(|s| s.duration_ms) + .collect::<Vec<_>>(); + + let vpn_sessions = if !vpn_durations.is_empty() { + Some(serde_json::to_string(&vpn_durations).unwrap()) + } else { + None + }; + let mixnet_sessions = if !mixnet_durations.is_empty() { + Some(serde_json::to_string(&mixnet_durations).unwrap()) + } else { + None + }; + let unknown_sessions = if !unkown_durations.is_empty() { + Some(serde_json::to_string(&unkown_durations).unwrap()) + } else { + None + }; + + GatewaySessionsRecord { + gateway_identity_key: node_data.id_key.clone(), + node_id: node_data.node_id as i64, + day: stat.update_time.date(), + unique_active_clients: stat.unique_active_users as i64, + session_started: stat.sessions_started as i64, + users_hashes, + vpn_sessions, + mixnet_sessions, + unknown_sessions, + } +} diff --git a/nym-node/Cargo.toml b/nym-node/Cargo.toml index 16f66a6fee8..75cfde1b155 100644 --- a/nym-node/Cargo.toml +++ b/nym-node/Cargo.toml @@ -3,7 +3,7 @@ [package] name = "nym-node" -version = "1.1.11" +version = "1.2.0" authors.workspace = true repository.workspace = true homepage.workspace = true @@ -14,21 +14,27 @@ license = "GPL-3.0" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +async-trait = { workspace = true } anyhow.workspace = true bip39 = { workspace = true, features = ["zeroize"] } bs58.workspace = true celes = { workspace = true } # country codes colored = { workspace = true } clap = { workspace = true, features = ["cargo", "env"] } +dashmap = { workspace = true } +futures = { workspace = true } humantime-serde = { workspace = true } +human-repr = { workspace = true } ipnetwork = { workspace = true } rand = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json.workspace = true +si-scale = { workspace = true } thiserror.workspace = true tracing.workspace = true tracing-subscriber.workspace = true -tokio = { workspace = true, features = ["macros", "sync"] } +tokio = { workspace = true, features = ["macros", "sync", "rt-multi-thread"] } +tokio-util = { workspace = true, features = ["codec"] } toml = { workspace = true } url = { workspace = true, features = ["serde"] } zeroize = { workspace = true, features = ["zeroize_derive"] } @@ -47,23 +53,47 @@ nym-bin-common = { path = "../common/bin-common", features = [ nym-client-core-config-types = { path = "../common/client-core/config-types" } nym-config = { path = "../common/config" } nym-crypto = { path = "../common/crypto", features = ["asymmetric", "rand"] } -nym-node-http-api = { path = "nym-node-http-api" } +nym-nonexhaustive-delayqueue = { path = "../common/nonexhaustive-delayqueue" } +nym-mixnet-client = { path = "../common/client-libs/mixnet-client" } nym-pemstore = { path = "../common/pemstore" } nym-sphinx-acknowledgements = { path = "../common/nymsphinx/acknowledgements" } nym-sphinx-addressing = { path = "../common/nymsphinx/addressing" } +nym-sphinx-framing = { path = "../common/nymsphinx/framing" } +nym-sphinx-types = { path = "../common/nymsphinx/types" } +nym-sphinx-forwarding = { path = "../common/nymsphinx/forwarding" } nym-task = { path = "../common/task" } nym-types = { path = "../common/types" } nym-validator-client = { path = "../common/client-libs/validator-client" } nym-wireguard = { path = "../common/wireguard" } nym-wireguard-types = { path = "../common/wireguard-types", default-features = false } +nym-verloc = { path = "../common/verloc" } +nym-metrics = { path = "../common/nym-metrics" } +nym-gateway-stats-storage = { path = "../common/gateway-stats-storage" } +nym-topology = { path = "../common/topology" } + + +# http server +# useful for `#[axum_macros::debug_handler]` +#axum-macros = "0.3.8" +axum.workspace = true +axum-extra = { workspace = true, features = ["typed-header"] } +headers.workspace = true +time = { workspace = true, features = ["serde"] } +tower-http = { workspace = true, features = ["fs"] } +utoipa = { workspace = true, features = ["axum_extras", "time"] } +utoipa-swagger-ui = { workspace = true, features = ["axum"] } + +nym-http-api-common = { path = "../common/http-api-common", features = ["utoipa"] } +nym-node-requests = { path = "nym-node-requests", default-features = false, features = ["openapi"] } +nym-node-metrics = { path = "nym-node-metrics" } # nodes: -nym-mixnode = { path = "../mixnode" } nym-gateway = { path = "../gateway" } nym-authenticator = { path = "../service-providers/authenticator" } nym-network-requester = { path = "../service-providers/network-requester" } nym-ip-packet-router = { path = "../service-providers/ip-packet-router" } + [build-dependencies] # temporary bonding information v1 (to grab and parse nym-mixnode and nym-gateway package versions) cargo_metadata = { workspace = true } diff --git a/nym-node/nym-node-http-api/Cargo.toml b/nym-node/nym-node-http-api/Cargo.toml deleted file mode 100644 index 27b3fb53d5f..00000000000 --- a/nym-node/nym-node-http-api/Cargo.toml +++ /dev/null @@ -1,47 +0,0 @@ -[package] -name = "nym-node-http-api" -version = "0.1.0" -edition = "2021" -license.workspace = true - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -axum.workspace = true -axum-extra = { workspace = true, features = ["typed-header"] } -headers.workspace = true - -# useful for `#[axum_macros::debug_handler]` -#axum-macros = "0.3.8" -thiserror.workspace = true -time = { workspace = true, features = ["serde"] } -tokio = { workspace = true, features = ["macros"] } -tower-http = { workspace = true, features = ["fs"] } -tracing.workspace = true -utoipa = { workspace = true, features = ["axum_extras", "time"] } -utoipa-swagger-ui = { workspace = true, features = ["axum"] } - -colored = { workspace = true } -ipnetwork = { workspace = true } -rand = { workspace = true } - -# Wireguard: -fastrand = { workspace = true } - -nym-crypto = { path = "../../common/crypto", features = ["asymmetric", "rand"] } -nym-http-api-common = { path = "../../common/http-api-common", features = ["utoipa"] } -nym-node-requests = { path = "../nym-node-requests", default-features = false, features = ["openapi"] } -nym-task = { path = "../../common/task" } - -nym-metrics = { path = "../../common/nym-metrics" } -nym-wireguard = { path = "../../common/wireguard" } - -[dev-dependencies] -base64 = { workspace = true } -hyper.workspace = true -dashmap.workspace = true -serde_json.workspace = true - -hmac = { workspace = true } -tower = { workspace = true } -x25519-dalek = { workspace = true } diff --git a/nym-node/nym-node-http-api/src/lib.rs b/nym-node/nym-node-http-api/src/lib.rs deleted file mode 100644 index 2621a30edab..00000000000 --- a/nym-node/nym-node-http-api/src/lib.rs +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright 2023 - Nym Technologies SA <contact@nymtech.net> -// SPDX-License-Identifier: GPL-3.0-only - -use axum::extract::connect_info::IntoMakeServiceWithConnectInfo; -use axum::extract::ConnectInfo; -use axum::middleware::AddExtension; -use axum::serve::Serve; -use axum::Router; -use nym_task::TaskClient; -use std::net::SocketAddr; -use tracing::{debug, error}; - -pub mod error; -pub mod middleware; -pub mod router; -pub mod state; - -pub use error::NymNodeHttpError; -pub use router::{api, landing_page, Config, NymNodeRouter}; - -// I guess this wasn't really meant to be extracted into separate type haha -type InnerService = IntoMakeServiceWithConnectInfo<Router, SocketAddr>; -type ConnectInfoExt = AddExtension<Router, ConnectInfo<SocketAddr>>; -pub type ServeService = Serve<InnerService, ConnectInfoExt>; - -pub struct NymNodeHTTPServer { - task_client: Option<TaskClient>, - inner: ServeService, -} - -impl NymNodeHTTPServer { - pub(crate) fn new(inner: ServeService) -> Self { - NymNodeHTTPServer { - task_client: None, - inner, - } - } - - #[must_use] - pub fn with_task_client(mut self, task_client: TaskClient) -> Self { - self.task_client = Some(task_client); - self - } - - async fn run_server_forever(server: ServeService) { - if let Err(err) = server.await { - error!("the HTTP server has terminated with the error: {err}"); - } else { - error!("the HTTP server has terminated with producing any errors"); - } - } - - pub async fn run(self) { - if let Some(mut task_client) = self.task_client { - tokio::select! { - _ = task_client.recv_with_delay() => { - debug!("NymNodeHTTPServer: Received shutdown"); - } - _ = Self::run_server_forever(self.inner) => { } - } - } else { - Self::run_server_forever(self.inner).await - } - - debug!("NymNodeHTTPServer: Exiting"); - } -} diff --git a/nym-node/nym-node-http-api/src/middleware/logging.rs b/nym-node/nym-node-http-api/src/middleware/logging.rs deleted file mode 100644 index ae3c5f1d4e9..00000000000 --- a/nym-node/nym-node-http-api/src/middleware/logging.rs +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright 2023-2024 - Nym Technologies SA <contact@nymtech.net> -// SPDX-License-Identifier: GPL-3.0-only - -use axum::{ - extract::{ConnectInfo, Request}, - http::{ - header::{HOST, USER_AGENT}, - HeaderValue, - }, - middleware::Next, - response::IntoResponse, -}; -use colored::*; -use std::net::SocketAddr; -use tracing::info; - -/// Simple logger for requests -pub async fn logger( - ConnectInfo(addr): ConnectInfo<SocketAddr>, - req: Request, - next: Next, -) -> impl IntoResponse { - let method = req.method().to_string().green(); - let uri = req.uri().to_string().blue(); - let agent = header_map( - req.headers().get(USER_AGENT), - "Unknown User Agent".to_string(), - ); - - let host = header_map(req.headers().get(HOST), "Unknown Host".to_string()); - - let res = next.run(req).await; - let status = res.status(); - let print_status = if status.is_client_error() || status.is_server_error() { - status.to_string().red() - } else if status.is_success() { - status.to_string().green() - } else { - status.to_string().yellow() - }; - - info!(target: "incoming request", "[{addr} -> {host}] {method} '{uri}': {print_status} / agent: {agent}"); - - res -} - -fn header_map(header: Option<&HeaderValue>, msg: String) -> String { - header - .map(|x| x.to_str().unwrap_or(&msg).to_string()) - .unwrap_or(msg) -} diff --git a/nym-node/nym-node-http-api/src/middleware/mod.rs b/nym-node/nym-node-http-api/src/middleware/mod.rs deleted file mode 100644 index 54a67e01478..00000000000 --- a/nym-node/nym-node-http-api/src/middleware/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -// Copyright 2023 - Nym Technologies SA <contact@nymtech.net> -// SPDX-License-Identifier: GPL-3.0-only - -pub mod logging; diff --git a/nym-node/nym-node-http-api/src/mod.rs b/nym-node/nym-node-http-api/src/mod.rs deleted file mode 100644 index 77cb66ba14f..00000000000 --- a/nym-node/nym-node-http-api/src/mod.rs +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright 2023 - Nym Technologies SA <contact@nymtech.net> -// SPDX-License-Identifier: GPL-3.0-only - -use axum::extract::connect_info::IntoMakeServiceWithConnectInfo; -use axum::Router; -use hyper::server::conn::AddrIncoming; -use hyper::Server; -use nym_task::TaskClient; -use std::net::SocketAddr; -use tracing::{debug, error, info}; - -pub mod middleware; -pub mod router; -pub mod state; - -pub use router::{api, landing_page, Config, NymNodeRouter}; - -pub struct NymNodeHTTPServer { - task_client: Option<TaskClient>, - inner: Server<AddrIncoming, IntoMakeServiceWithConnectInfo<Router, SocketAddr>>, -} - -impl NymNodeHTTPServer { - pub(crate) fn new( - inner: Server<AddrIncoming, IntoMakeServiceWithConnectInfo<Router, SocketAddr>>, - ) -> Self { - NymNodeHTTPServer { - task_client: None, - inner, - } - } - - #[must_use] - pub fn with_task_client(mut self, task_client: TaskClient) -> Self { - self.task_client = Some(task_client); - self - } - - async fn run_server_forever( - server: Server<AddrIncoming, IntoMakeServiceWithConnectInfo<Router, SocketAddr>>, - ) { - if let Err(err) = server.await { - error!("the HTTP server has terminated with the error: {err}"); - } else { - error!("the HTTP server has terminated with producing any errors"); - } - } - - pub async fn run(self) { - info!("Started NymNodeHTTPServer on {}", self.inner.local_addr()); - if let Some(mut task_client) = self.task_client { - tokio::select! { - _ = task_client.recv_with_delay() => { - debug!("NymNodeHTTPServer: Received shutdown"); - } - _ = Self::run_server_forever(self.inner) => { } - } - } else { - Self::run_server_forever(self.inner).await - } - - debug!("NymNodeHTTPServer: Exiting"); - } -} diff --git a/nym-node/nym-node-http-api/src/router/api/v1/metrics/mixing.rs b/nym-node/nym-node-http-api/src/router/api/v1/metrics/mixing.rs deleted file mode 100644 index 8ae07c61e69..00000000000 --- a/nym-node/nym-node-http-api/src/router/api/v1/metrics/mixing.rs +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright 2024 - Nym Technologies SA <contact@nymtech.net> -// SPDX-License-Identifier: GPL-3.0-only - -use crate::state::metrics::MetricsAppState; -use axum::extract::{Query, State}; -use nym_http_api_common::{FormattedResponse, OutputParams}; -use nym_node_requests::api::v1::metrics::models::MixingStats; - -/// If applicable, returns mixing statistics information of this node. -/// This information is **PURELY** self-reported and in no way validated. -#[utoipa::path( - get, - path = "/mixing", - context_path = "/api/v1/metrics", - tag = "Metrics", - responses( - (status = 200, content( - ("application/json" = MixingStats), - ("application/yaml" = MixingStats) - )) - ), - params(OutputParams), -)] -pub(crate) async fn mixing_stats( - Query(output): Query<OutputParams>, - State(metrics_state): State<MetricsAppState>, -) -> MixingStatsResponse { - let output = output.output.unwrap_or_default(); - let response = metrics_state.mixing_stats.read().await.as_response(); - output.to_response(response) -} - -pub type MixingStatsResponse = FormattedResponse<MixingStats>; diff --git a/nym-node/nym-node-http-api/src/router/api/v1/metrics/sessions.rs b/nym-node/nym-node-http-api/src/router/api/v1/metrics/sessions.rs deleted file mode 100644 index 59eceb8f89c..00000000000 --- a/nym-node/nym-node-http-api/src/router/api/v1/metrics/sessions.rs +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright 2024 - Nym Technologies SA <contact@nymtech.net> -// SPDX-License-Identifier: GPL-3.0-only - -use crate::state::metrics::MetricsAppState; -use axum::extract::{Query, State}; -use nym_http_api_common::{FormattedResponse, OutputParams}; -use nym_node_requests::api::v1::metrics::models::SessionStats; - -/// If applicable, returns sessions statistics information of this node. -/// This information is **PURELY** self-reported and in no way validated. -#[utoipa::path( - get, - path = "/sessions", - context_path = "/api/v1/metrics", - tag = "Metrics", - responses( - (status = 200, content( - ("application/json" = SessionStats), - ("application/yaml" = SessionStats) - )) - ), - params(OutputParams), -)] -pub(crate) async fn sessions_stats( - Query(output): Query<OutputParams>, - State(metrics_state): State<MetricsAppState>, -) -> SessionStatsResponse { - let output = output.output.unwrap_or_default(); - let response = metrics_state.session_stats.read().await.as_response(); - output.to_response(response) -} - -pub type SessionStatsResponse = FormattedResponse<SessionStats>; diff --git a/nym-node/nym-node-http-api/src/router/api/v1/metrics/verloc.rs b/nym-node/nym-node-http-api/src/router/api/v1/metrics/verloc.rs deleted file mode 100644 index c622ccf1239..00000000000 --- a/nym-node/nym-node-http-api/src/router/api/v1/metrics/verloc.rs +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright 2024 - Nym Technologies SA <contact@nymtech.net> -// SPDX-License-Identifier: GPL-3.0-only - -use crate::state::metrics::MetricsAppState; -use axum::extract::{Query, State}; -use nym_http_api_common::{FormattedResponse, OutputParams}; -use nym_node_requests::api::v1::metrics::models::VerlocStats; - -/// If applicable, returns verloc statistics information of this node. -#[utoipa::path( - get, - path = "/verloc", - context_path = "/api/v1/metrics", - tag = "Metrics", - responses( - (status = 200, content( - ("application/json" = VerlocStats), - ("application/yaml" = VerlocStats) - )) - ), - params(OutputParams), -)] -pub(crate) async fn verloc_stats( - Query(output): Query<OutputParams>, - State(metrics_state): State<MetricsAppState>, -) -> VerlocStatsResponse { - let output = output.output.unwrap_or_default(); - let response = metrics_state.verloc.read().await.as_response(); - output.to_response(response) -} - -pub type VerlocStatsResponse = FormattedResponse<VerlocStats>; diff --git a/nym-node/nym-node-http-api/src/state/metrics.rs b/nym-node/nym-node-http-api/src/state/metrics.rs deleted file mode 100644 index 4caaada7903..00000000000 --- a/nym-node/nym-node-http-api/src/state/metrics.rs +++ /dev/null @@ -1,214 +0,0 @@ -// Copyright 2024 - Nym Technologies SA <contact@nymtech.net> -// SPDX-License-Identifier: GPL-3.0-only - -use crate::state::AppState; -use axum::extract::FromRef; -use nym_node_requests::api::v1::metrics::models::{ - MixingStats, Session, SessionStats, VerlocResult, VerlocResultData, VerlocStats, -}; -use std::collections::HashMap; -use std::sync::Arc; -use time::macros::time; -use time::{Date, OffsetDateTime}; -use tokio::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard}; - -pub use nym_node_requests::api::v1::metrics::models::{VerlocMeasurement, VerlocNodeResult}; - -type PacketsMap = HashMap<String, u64>; - -#[derive(Clone, Debug, Default)] -pub struct SharedMixingStats { - inner: Arc<RwLock<MixingStatsState>>, -} - -impl SharedMixingStats { - pub fn new() -> SharedMixingStats { - let now = OffsetDateTime::now_utc(); - - SharedMixingStats { - inner: Arc::new(RwLock::new(MixingStatsState { - update_time: now, - previous_update_time: now, - ..Default::default() - })), - } - } - - pub async fn read(&self) -> RwLockReadGuard<'_, MixingStatsState> { - self.inner.read().await - } - - pub async fn write(&self) -> RwLockWriteGuard<'_, MixingStatsState> { - self.inner.write().await - } -} - -#[derive(Debug)] -pub struct MixingStatsState { - pub update_time: OffsetDateTime, - - pub previous_update_time: OffsetDateTime, - - pub packets_received_since_startup: u64, - pub packets_sent_since_startup_all: u64, - pub packets_dropped_since_startup_all: u64, - pub packets_received_since_last_update: u64, - - // note: sent does not imply forwarded. We don't know if it was delivered successfully - pub packets_sent_since_last_update: PacketsMap, - - // we know for sure we dropped packets to those destinations - pub packets_explicitly_dropped_since_last_update: PacketsMap, -} - -impl MixingStatsState { - pub fn as_response(&self) -> MixingStats { - MixingStats { - update_time: self.update_time, - previous_update_time: self.previous_update_time, - received_since_startup: self.packets_received_since_startup, - sent_since_startup: self.packets_sent_since_startup_all, - dropped_since_startup: self.packets_dropped_since_startup_all, - received_since_last_update: self.packets_received_since_last_update, - sent_since_last_update: self.packets_sent_since_last_update.values().sum(), - dropped_since_last_update: self - .packets_explicitly_dropped_since_last_update - .values() - .sum(), - } - } -} - -impl Default for MixingStatsState { - fn default() -> Self { - MixingStatsState { - update_time: OffsetDateTime::UNIX_EPOCH, - previous_update_time: OffsetDateTime::UNIX_EPOCH, - packets_received_since_startup: 0, - packets_sent_since_startup_all: 0, - packets_dropped_since_startup_all: 0, - packets_received_since_last_update: 0, - packets_sent_since_last_update: Default::default(), - packets_explicitly_dropped_since_last_update: Default::default(), - } - } -} - -#[derive(Clone, Debug, Default)] -pub struct SharedVerlocStats { - inner: Arc<RwLock<VerlocStatsState>>, -} - -#[derive(Clone, Debug, Default)] -pub struct VerlocStatsState { - pub current_run_data: VerlocResultData, - pub previous_run_data: VerlocResultData, -} - -impl SharedVerlocStats { - pub async fn read(&self) -> RwLockReadGuard<'_, VerlocStatsState> { - self.inner.read().await - } - - pub async fn write(&self) -> RwLockWriteGuard<'_, VerlocStatsState> { - self.inner.write().await - } -} - -impl VerlocStatsState { - pub fn as_response(&self) -> VerlocStats { - let previous = if !self.previous_run_data.run_finished() { - VerlocResult::Unavailable - } else { - VerlocResult::Data(self.previous_run_data.clone()) - }; - - let current = if !self.current_run_data.run_finished() { - VerlocResult::MeasurementInProgress - } else { - VerlocResult::Data(self.current_run_data.clone()) - }; - - VerlocStats { previous, current } - } -} - -#[derive(Clone, Debug, Default)] -pub struct SharedSessionStats { - inner: Arc<RwLock<SessionStatsState>>, -} - -impl SharedSessionStats { - pub fn new() -> SharedSessionStats { - SharedSessionStats { - inner: Arc::new(RwLock::new(Default::default())), - } - } - - pub async fn read(&self) -> RwLockReadGuard<'_, SessionStatsState> { - self.inner.read().await - } - - pub async fn write(&self) -> RwLockWriteGuard<'_, SessionStatsState> { - self.inner.write().await - } -} - -type FinishedSessions = Vec<(u64, String)>; - -#[derive(Debug, Clone)] -pub struct SessionStatsState { - pub update_time: Date, - pub unique_active_users_count: u32, - pub unique_active_users_hashes: Vec<String>, - pub session_started: u32, - pub sessions: FinishedSessions, -} - -impl SessionStatsState { - pub fn as_response(&self) -> SessionStats { - let sessions = self - .sessions - .clone() - .into_iter() - .map(|(duration_ms, typ)| Session { duration_ms, typ }) - .collect(); - SessionStats { - update_time: self.update_time.with_time(time!(0:00)).assume_utc(), - unique_active_users: self.unique_active_users_count, - unique_active_users_hashes: self.unique_active_users_hashes.clone(), - sessions, - sessions_started: self.session_started, - sessions_finished: self.sessions.len() as u32, - } - } -} - -impl Default for SessionStatsState { - fn default() -> Self { - SessionStatsState { - update_time: OffsetDateTime::UNIX_EPOCH.date(), - unique_active_users_count: 0, - unique_active_users_hashes: Default::default(), - session_started: 0, - sessions: Default::default(), - } - } -} - -#[derive(Debug, Clone, Default)] -pub struct MetricsAppState { - pub(crate) prometheus_access_token: Option<String>, - - pub(crate) mixing_stats: SharedMixingStats, - - pub(crate) session_stats: SharedSessionStats, - - pub(crate) verloc: SharedVerlocStats, -} - -impl FromRef<AppState> for MetricsAppState { - fn from_ref(app_state: &AppState) -> Self { - app_state.metrics.clone() - } -} diff --git a/nym-node/nym-node-http-api/src/state/mod.rs b/nym-node/nym-node-http-api/src/state/mod.rs deleted file mode 100644 index 67f16d1bd3c..00000000000 --- a/nym-node/nym-node-http-api/src/state/mod.rs +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright 2023-2024 - Nym Technologies SA <contact@nymtech.net> -// SPDX-License-Identifier: GPL-3.0-only - -use crate::state::metrics::{ - MetricsAppState, SharedMixingStats, SharedSessionStats, SharedVerlocStats, -}; -use tokio::time::Instant; - -pub mod metrics; - -#[derive(Debug, Clone)] -pub struct AppState { - pub(crate) startup_time: Instant, - - pub(crate) metrics: MetricsAppState, -} - -impl AppState { - #[allow(clippy::new_without_default)] - pub fn new() -> Self { - AppState { - // is it 100% accurate? - // no. - // does it have to be? - // also no. - startup_time: Instant::now(), - metrics: Default::default(), - } - } - - #[must_use] - pub fn with_mixing_stats(mut self, mixing_stats: SharedMixingStats) -> Self { - self.metrics.mixing_stats = mixing_stats; - self - } - - #[must_use] - pub fn with_sessions_stats(mut self, session_stats: SharedSessionStats) -> Self { - self.metrics.session_stats = session_stats; - self - } - - #[must_use] - pub fn with_verloc_stats(mut self, verloc_stats: SharedVerlocStats) -> Self { - self.metrics.verloc = verloc_stats; - self - } - - #[must_use] - pub fn with_metrics_key(mut self, bearer_token: impl Into<Option<String>>) -> Self { - self.metrics.prometheus_access_token = bearer_token.into(); - self - } -} diff --git a/nym-node/nym-node-metrics/Cargo.toml b/nym-node/nym-node-metrics/Cargo.toml new file mode 100644 index 00000000000..77970401149 --- /dev/null +++ b/nym-node/nym-node-metrics/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "nym-node-metrics" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +dashmap = { workspace = true } +futures = { workspace = true } +tokio = { workspace = true, features = ["sync"] } +time = { workspace = true } +strum = { workspace = true } +tracing = { workspace = true } + +nym-metrics = { path = "../../common/nym-metrics" } +nym-statistics-common = { path = "../../common/statistics" } \ No newline at end of file diff --git a/nym-node/nym-node-metrics/src/entry.rs b/nym-node/nym-node-metrics/src/entry.rs new file mode 100644 index 00000000000..ab8da08fd63 --- /dev/null +++ b/nym-node/nym-node-metrics/src/entry.rs @@ -0,0 +1,112 @@ +// Copyright 2024 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: Apache-2.0 + +use nym_statistics_common::hash_identifier; +use std::time::Duration; +use time::{Date, OffsetDateTime}; +use tokio::sync::{RwLock, RwLockReadGuard}; + +#[derive(Default)] +pub struct EntryStats { + sessions: RwLock<ClientSessions>, +} + +impl EntryStats { + pub async fn update_client_sessions(&self, new: ClientSessions) { + *self.sessions.write().await = new + } + + pub async fn client_sessions(&self) -> RwLockReadGuard<ClientSessions> { + self.sessions.read().await + } +} + +pub struct ClientSessions { + pub update_time: Date, + pub unique_users: Vec<String>, + pub sessions_started: u32, + pub finished_sessions: Vec<FinishedSession>, +} + +impl Default for ClientSessions { + fn default() -> Self { + ClientSessions { + update_time: OffsetDateTime::UNIX_EPOCH.date(), + unique_users: vec![], + sessions_started: 0, + finished_sessions: vec![], + } + } +} + +impl ClientSessions { + pub fn new( + update_time: Date, + unique_users: Vec<String>, + sessions_started: u32, + sessions: Vec<FinishedSession>, + ) -> Self { + ClientSessions { + update_time, + unique_users: unique_users.into_iter().map(hash_identifier).collect(), + sessions_started, + finished_sessions: sessions, + } + } +} + +pub struct FinishedSession { + pub duration: Duration, + pub typ: SessionType, +} + +impl FinishedSession { + pub fn new(duration: Duration, typ: SessionType) -> Self { + FinishedSession { duration, typ } + } +} + +#[derive(PartialEq, Copy, Clone, strum::Display, strum::EnumString)] +pub enum SessionType { + Vpn, + Mixnet, + Unknown, +} + +impl SessionType { + pub fn from_string<S: AsRef<str>>(s: S) -> Self { + s.as_ref().parse().unwrap_or(Self::Unknown) + } +} + +pub struct ActiveSession { + pub start: OffsetDateTime, + pub typ: SessionType, +} + +impl ActiveSession { + pub fn new(start_time: OffsetDateTime) -> Self { + ActiveSession { + start: start_time, + typ: SessionType::Unknown, + } + } + + pub fn set_type(&mut self, typ: SessionType) { + self.typ = typ; + } + + pub fn end_at(self, stop_time: OffsetDateTime) -> Option<FinishedSession> { + let session_duration = stop_time - self.start; + //ensure duration is positive to fit in a u64 + //u64::max milliseconds is 500k millenia so no overflow issue + if session_duration > Duration::ZERO { + Some(FinishedSession { + duration: session_duration.unsigned_abs(), + typ: self.typ, + }) + } else { + None + } + } +} diff --git a/nym-node/nym-node-metrics/src/events.rs b/nym-node/nym-node-metrics/src/events.rs new file mode 100644 index 00000000000..92fa3dbb94d --- /dev/null +++ b/nym-node/nym-node-metrics/src/events.rs @@ -0,0 +1,53 @@ +// Copyright 2024 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: Apache-2.0 + +use futures::channel::mpsc; +use futures::channel::mpsc::SendError; +pub use nym_statistics_common::gateways::GatewaySessionEvent; +use tracing::error; + +pub fn events_channels() -> (MetricEventsSender, MetricEventsReceiver) { + let (tx, rx) = mpsc::unbounded(); + (tx.into(), rx) +} + +#[derive(Clone)] +pub struct MetricEventsSender(mpsc::UnboundedSender<MetricsEvent>); + +impl From<mpsc::UnboundedSender<MetricsEvent>> for MetricEventsSender { + fn from(tx: mpsc::UnboundedSender<MetricsEvent>) -> Self { + MetricEventsSender(tx) + } +} + +impl MetricEventsSender { + pub fn report(&self, metric: impl Into<MetricsEvent>) -> Result<(), SendError> { + self.0 + .unbounded_send(metric.into()) + .map_err(|err| err.into_send_error()) + } + + pub fn report_unchecked(&self, metric: impl Into<MetricsEvent>) { + if let Err(err) = self.report(metric) { + error!("failed to send metric information: {err}") + } + } + + #[allow(clippy::len_without_is_empty)] + pub fn len(&self) -> usize { + self.0.len() + } +} + +pub type MetricEventsReceiver = mpsc::UnboundedReceiver<MetricsEvent>; + +// please create a new variant per "category" of metrics +pub enum MetricsEvent { + GatewayClientSession(GatewaySessionEvent), +} + +impl From<GatewaySessionEvent> for MetricsEvent { + fn from(gateway_stats: GatewaySessionEvent) -> Self { + MetricsEvent::GatewayClientSession(gateway_stats) + } +} diff --git a/nym-node/nym-node-metrics/src/lib.rs b/nym-node/nym-node-metrics/src/lib.rs new file mode 100644 index 00000000000..58ab0f77f7e --- /dev/null +++ b/nym-node/nym-node-metrics/src/lib.rs @@ -0,0 +1,39 @@ +// Copyright 2024 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: Apache-2.0 + +use crate::entry::EntryStats; +use crate::mixnet::MixingStats; +use crate::network::NetworkStats; +use std::ops::Deref; +use std::sync::Arc; + +pub mod entry; +pub mod events; +pub mod mixnet; +pub mod network; + +#[derive(Clone, Default)] +pub struct NymNodeMetrics { + inner: Arc<NymNodeMetricsInner>, +} + +impl NymNodeMetrics { + pub fn new() -> Self { + NymNodeMetrics::default() + } +} + +impl Deref for NymNodeMetrics { + type Target = NymNodeMetricsInner; + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +#[derive(Default)] +pub struct NymNodeMetricsInner { + pub mixnet: MixingStats, + pub entry: EntryStats, + + pub network: NetworkStats, +} diff --git a/nym-node/nym-node-metrics/src/mixnet.rs b/nym-node/nym-node-metrics/src/mixnet.rs new file mode 100644 index 00000000000..043d2d1e7cb --- /dev/null +++ b/nym-node/nym-node-metrics/src/mixnet.rs @@ -0,0 +1,292 @@ +// Copyright 2024 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: Apache-2.0 + +use dashmap::DashMap; +use std::net::{IpAddr, SocketAddr}; +use std::sync::atomic::{AtomicI64, AtomicUsize, Ordering}; +use time::OffsetDateTime; + +#[derive(Default)] +pub struct MixingStats { + // updated on each packet + pub ingress: IngressMixingStats, + + // updated on each packet + pub egress: EgressMixingStats, + + // updated on a timer + pub legacy: LegacyMixingStats, +} + +impl MixingStats { + pub fn update_legacy_stats( + &self, + received_since_last_update: usize, + sent_since_last_update: usize, + dropped_since_last_update: usize, + update_timestamp: i64, + ) { + self.legacy + .received_since_last_update + .store(received_since_last_update, Ordering::Relaxed); + self.legacy + .sent_since_last_update + .store(sent_since_last_update, Ordering::Relaxed); + self.legacy + .dropped_since_last_update + .store(dropped_since_last_update, Ordering::Relaxed); + + let old_last = self.legacy.last_update_ts.load(Ordering::Acquire); + self.legacy + .previous_update_ts + .store(old_last, Ordering::Release); + self.legacy + .last_update_ts + .store(update_timestamp, Ordering::Release); + } + + pub fn ingress_malformed_packet(&self, source: IpAddr) { + self.ingress + .malformed_packets_received + .fetch_add(1, Ordering::Relaxed); + self.ingress.senders.entry(source).or_default().malformed += 1; + } + + pub fn ingress_received_forward_packet(&self, source: IpAddr) { + self.ingress + .forward_hop_packets_received + .fetch_add(1, Ordering::Relaxed); + self.ingress + .senders + .entry(source) + .or_default() + .forward_packets + .received += 1; + } + + pub fn ingress_received_final_hop_packet(&self, source: IpAddr) { + self.ingress + .final_hop_packets_received + .fetch_add(1, Ordering::Relaxed); + self.ingress + .senders + .entry(source) + .or_default() + .final_hop_packets + .received += 1; + } + + pub fn ingress_excessive_delay_packet(&self) { + self.ingress + .excessive_delay_packets + .fetch_add(1, Ordering::Relaxed); + } + + pub fn ingress_dropped_forward_packet(&self, source: IpAddr) { + self.ingress + .forward_hop_packets_dropped + .fetch_add(1, Ordering::Relaxed); + self.ingress + .senders + .entry(source) + .or_default() + .forward_packets + .dropped += 1; + } + + pub fn ingress_dropped_final_hop_packet(&self, source: IpAddr) { + self.ingress + .final_hop_packets_dropped + .fetch_add(1, Ordering::Relaxed); + self.ingress + .senders + .entry(source) + .or_default() + .final_hop_packets + .dropped += 1; + } + + pub fn egress_sent_forward_packet(&self, target: SocketAddr) { + self.egress + .forward_hop_packets_sent + .fetch_add(1, Ordering::Relaxed); + self.egress + .forward_recipients + .entry(target) + .or_default() + .sent += 1; + } + + pub fn egress_sent_ack(&self) { + self.egress.ack_packets_sent.fetch_add(1, Ordering::Relaxed); + } + + pub fn egress_dropped_forward_packet(&self, target: SocketAddr) { + self.egress + .forward_hop_packets_dropped + .fetch_add(1, Ordering::Relaxed); + self.egress + .forward_recipients + .entry(target) + .or_default() + .dropped += 1; + } + + pub fn egress_dropped_final_hop_packet(&self) { + todo!() + // self.egress + // .final_hop_packets_dropped + // .fetch_add(1, Ordering::Relaxed); + } +} + +#[derive(Clone, Copy, Default, PartialEq)] +pub struct EgressRecipientStats { + pub dropped: usize, + pub sent: usize, +} + +#[derive(Default)] +pub struct EgressMixingStats { + // this includes ACKS! + forward_hop_packets_sent: AtomicUsize, + + ack_packets_sent: AtomicUsize, + + forward_hop_packets_dropped: AtomicUsize, + + forward_recipients: DashMap<SocketAddr, EgressRecipientStats>, +} + +impl EgressMixingStats { + pub fn forward_hop_packets_sent(&self) -> usize { + self.forward_hop_packets_sent.load(Ordering::Relaxed) + } + + pub fn ack_packets_sent(&self) -> usize { + self.ack_packets_sent.load(Ordering::Relaxed) + } + + pub fn forward_hop_packets_dropped(&self) -> usize { + self.forward_hop_packets_dropped.load(Ordering::Relaxed) + } + + pub fn forward_recipients(&self) -> &DashMap<SocketAddr, EgressRecipientStats> { + &self.forward_recipients + } + + pub fn remove_stale_forward_recipient(&self, recipient: SocketAddr) { + self.forward_recipients.remove(&recipient); + } +} + +#[derive(Clone, Copy, Default, PartialEq)] +pub struct IngressPacketsStats { + pub dropped: usize, + pub received: usize, +} + +#[derive(Clone, Copy, Default, PartialEq)] +pub struct IngressRecipientStats { + pub forward_packets: IngressPacketsStats, + pub final_hop_packets: IngressPacketsStats, + pub malformed: usize, +} + +#[derive(Default)] +pub struct IngressMixingStats { + // forward hop packets (i.e. to mixnode) + forward_hop_packets_received: AtomicUsize, + + // final hop packets (i.e. to gateway) + final_hop_packets_received: AtomicUsize, + + // packets that failed to get unwrapped + malformed_packets_received: AtomicUsize, + + // (forward) packets that had invalid, i.e. too large, delays + excessive_delay_packets: AtomicUsize, + + // forward hop packets (i.e. to mixnode) + forward_hop_packets_dropped: AtomicUsize, + + // final hop packets (i.e. to gateway) + final_hop_packets_dropped: AtomicUsize, + + senders: DashMap<IpAddr, IngressRecipientStats>, +} + +impl IngressMixingStats { + pub fn forward_hop_packets_received(&self) -> usize { + self.forward_hop_packets_received.load(Ordering::Relaxed) + } + + pub fn final_hop_packets_received(&self) -> usize { + self.final_hop_packets_received.load(Ordering::Relaxed) + } + + pub fn malformed_packets_received(&self) -> usize { + self.malformed_packets_received.load(Ordering::Relaxed) + } + + pub fn excessive_delay_packets(&self) -> usize { + self.excessive_delay_packets.load(Ordering::Relaxed) + } + + pub fn forward_hop_packets_dropped(&self) -> usize { + self.forward_hop_packets_dropped.load(Ordering::Relaxed) + } + + pub fn final_hop_packets_dropped(&self) -> usize { + self.final_hop_packets_dropped.load(Ordering::Relaxed) + } + + pub fn senders(&self) -> &DashMap<IpAddr, IngressRecipientStats> { + &self.senders + } + + pub fn remove_stale_sender(&self, sender: IpAddr) { + self.senders.remove(&sender); + } +} + +#[derive(Debug, Default)] +pub struct LegacyMixingStats { + last_update_ts: AtomicI64, + previous_update_ts: AtomicI64, + + received_since_last_update: AtomicUsize, + + // note: sent does not imply forwarded. We don't know if it was delivered successfully + sent_since_last_update: AtomicUsize, + + // we know for sure we dropped those packets + dropped_since_last_update: AtomicUsize, +} + +impl LegacyMixingStats { + pub fn last_update(&self) -> OffsetDateTime { + // SAFETY: all values put here are guaranteed to be valid timestamps + #[allow(clippy::unwrap_used)] + OffsetDateTime::from_unix_timestamp(self.last_update_ts.load(Ordering::Relaxed)).unwrap() + } + + pub fn previous_update(&self) -> OffsetDateTime { + // SAFETY: all values put here are guaranteed to be valid timestamps + #[allow(clippy::unwrap_used)] + OffsetDateTime::from_unix_timestamp(self.previous_update_ts.load(Ordering::Relaxed)) + .unwrap() + } + + pub fn received_since_last_update(&self) -> usize { + self.received_since_last_update.load(Ordering::Relaxed) + } + + pub fn sent_since_last_update(&self) -> usize { + self.sent_since_last_update.load(Ordering::Relaxed) + } + + pub fn dropped_since_last_update(&self) -> usize { + self.dropped_since_last_update.load(Ordering::Relaxed) + } +} diff --git a/nym-node/nym-node-metrics/src/network.rs b/nym-node/nym-node-metrics/src/network.rs new file mode 100644 index 00000000000..de00d785603 --- /dev/null +++ b/nym-node/nym-node-metrics/src/network.rs @@ -0,0 +1,27 @@ +// Copyright 2024 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: Apache-2.0 + +use std::sync::atomic::{AtomicUsize, Ordering}; + +#[derive(Default)] +pub struct NetworkStats { + // for now just experiment with basic data, we could always extend it + active_ingress_mixnet_connections: AtomicUsize, +} + +impl NetworkStats { + pub fn new_active_ingress_mixnet_client(&self) { + self.active_ingress_mixnet_connections + .fetch_add(1, Ordering::Relaxed); + } + + pub fn disconnected_ingress_mixnet_client(&self) { + self.active_ingress_mixnet_connections + .fetch_sub(1, Ordering::Relaxed); + } + + pub fn active_ingress_mixnet_connections_count(&self) -> usize { + self.active_ingress_mixnet_connections + .load(Ordering::Relaxed) + } +} diff --git a/nym-node/nym-node-requests/src/api/client.rs b/nym-node/nym-node-requests/src/api/client.rs index 11ec66a4b93..068b128ef27 100644 --- a/nym-node/nym-node-requests/src/api/client.rs +++ b/nym-node/nym-node-requests/src/api/client.rs @@ -19,6 +19,7 @@ use crate::api::v1::network_requester::models::NetworkRequester; pub use nym_http_api_client::Client; use super::v1::gateway::models::Wireguard; +use super::v1::metrics::models::SessionStats; pub type NymNodeApiClientError = HttpClientError<ErrorResponse>; @@ -87,6 +88,11 @@ pub trait NymNodeApiClientExt: ApiClient { self.get_json_from(routes::api::v1::gateway::client_interfaces::wireguard_absolute()) .await } + + async fn get_sessions_metrics(&self) -> Result<SessionStats, NymNodeApiClientError> { + self.get_json_from(routes::api::v1::metrics::sessions_absolute()) + .await + } } #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] diff --git a/nym-node/nym-node-requests/src/api/v1/metrics/models.rs b/nym-node/nym-node-requests/src/api/v1/metrics/models.rs index a32506db2d5..8e430534c50 100644 --- a/nym-node/nym-node-requests/src/api/v1/metrics/models.rs +++ b/nym-node/nym-node-requests/src/api/v1/metrics/models.rs @@ -1,303 +1,177 @@ // Copyright 2024 - Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: Apache-2.0 -use nym_crypto::asymmetric::identity::{self, serde_helpers::bs58_ed25519_pubkey}; -use serde::{Deserialize, Serialize}; -use std::cmp::Ordering; -use std::fmt; -use std::fmt::{Display, Formatter}; -use std::time::Duration; -use time::OffsetDateTime; +pub use mixing::*; +pub use session::*; +pub use verloc::*; + +pub mod packets { + use serde::{Deserialize, Serialize}; + + #[derive(Serialize, Deserialize, Debug, Clone, Copy)] + #[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] + pub struct PacketsStats { + pub ingress_mixing: IngressMixingStats, + pub egress_mixing: EgressMixingStats, + } -#[derive(Serialize, Deserialize, Debug, Clone)] -#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] -pub struct MixingStats { - #[serde(with = "time::serde::rfc3339")] - pub update_time: OffsetDateTime, + #[derive(Serialize, Deserialize, Debug, Clone, Copy)] + #[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] + pub struct IngressMixingStats { + // forward hop packets (i.e. to mixnode) + pub forward_hop_packets_received: usize, - #[serde(with = "time::serde::rfc3339")] - pub previous_update_time: OffsetDateTime, + // final hop packets (i.e. to gateway) + pub final_hop_packets_received: usize, - pub received_since_startup: u64, + // packets that failed to get unwrapped + pub malformed_packets_received: usize, - // note: sent does not imply forwarded. We don't know if it was delivered successfully - pub sent_since_startup: u64, + // (forward) packets that had invalid, i.e. too large, delays + pub excessive_delay_packets: usize, - // we know for sure we dropped those packets - pub dropped_since_startup: u64, + // forward hop packets (i.e. to mixnode) + pub forward_hop_packets_dropped: usize, - pub received_since_last_update: u64, + // final hop packets (i.e. to gateway) + pub final_hop_packets_dropped: usize, + } - // note: sent does not imply forwarded. We don't know if it was delivered successfully - pub sent_since_last_update: u64, + #[derive(Serialize, Deserialize, Debug, Clone, Copy)] + #[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] + pub struct EgressMixingStats { + pub forward_hop_packets_sent: usize, - // we know for sure we dropped those packets - pub dropped_since_last_update: u64, -} + pub forward_hop_packets_dropped: usize, -#[derive(Serialize, Deserialize, Debug, Clone)] -#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] -pub struct Session { - pub duration_ms: u64, - pub typ: String, + pub ack_packets_sent: usize, + } } -#[derive(Serialize, Deserialize, Debug, Clone)] -#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] -pub struct SessionStats { - #[serde(with = "time::serde::rfc3339")] - pub update_time: OffsetDateTime, +pub mod mixing { + use serde::{Deserialize, Serialize}; + use time::OffsetDateTime; - pub unique_active_users: u32, + #[derive(Serialize, Deserialize, Debug, Clone)] + #[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] + pub struct LegacyMixingStats { + #[serde(with = "time::serde::rfc3339")] + pub update_time: OffsetDateTime, - pub unique_active_users_hashes: Vec<String>, + #[serde(with = "time::serde::rfc3339")] + pub previous_update_time: OffsetDateTime, - pub sessions: Vec<Session>, + pub received_since_startup: u64, - pub sessions_started: u32, + // note: sent does not imply forwarded. We don't know if it was delivered successfully + pub sent_since_startup: u64, - pub sessions_finished: u32, -} - -#[derive(Serialize, Deserialize, Default, Debug, Clone)] -#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] -pub struct VerlocStats { - pub previous: VerlocResult, - pub current: VerlocResult, -} - -#[derive(Serialize, Deserialize, Default, Debug, Clone)] -#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] -#[serde(rename_all = "camelCase")] -pub enum VerlocResult { - Data(VerlocResultData), - MeasurementInProgress, - #[default] - Unavailable, -} + // we know for sure we dropped those packets + pub dropped_since_startup: u64, -#[derive(Serialize, Deserialize, Debug, Clone)] -#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] -pub struct VerlocResultData { - pub nodes_tested: usize, + pub received_since_last_update: u64, - #[serde(with = "time::serde::rfc3339")] - pub run_started: OffsetDateTime, + // note: sent does not imply forwarded. We don't know if it was delivered successfully + pub sent_since_last_update: u64, - #[serde(with = "time::serde::rfc3339::option")] - pub run_finished: Option<OffsetDateTime>, - - pub results: Vec<VerlocNodeResult>, + // we know for sure we dropped those packets + pub dropped_since_last_update: u64, + } } -impl Default for VerlocResultData { - fn default() -> Self { - VerlocResultData { - nodes_tested: 0, - run_started: OffsetDateTime::now_utc(), - run_finished: None, - results: vec![], - } +pub mod verloc { + use nym_crypto::asymmetric::ed25519::{self, serde_helpers::bs58_ed25519_pubkey}; + use serde::{Deserialize, Serialize}; + use std::time::Duration; + use time::OffsetDateTime; + + #[derive(Serialize, Deserialize, Default, Debug, Clone)] + #[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] + pub struct VerlocStats { + pub previous: VerlocResult, + pub current: VerlocResult, } -} -impl VerlocResultData { - pub fn run_finished(&self) -> bool { - self.run_finished.is_some() + #[derive(Serialize, Deserialize, Default, Debug, Clone)] + #[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] + #[serde(rename_all = "camelCase")] + pub enum VerlocResult { + Data(VerlocResultData), + MeasurementInProgress, + #[default] + Unavailable, } -} -#[derive(Serialize, Deserialize, Debug, Clone, Copy, Eq, PartialEq)] -#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] -pub struct VerlocNodeResult { - #[serde(with = "bs58_ed25519_pubkey")] - pub node_identity: identity::PublicKey, + #[derive(Serialize, Deserialize, Debug, Clone)] + #[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] + pub struct VerlocResultData { + pub nodes_tested: usize, - pub latest_measurement: Option<VerlocMeasurement>, -} + #[serde(with = "time::serde::rfc3339")] + pub run_started: OffsetDateTime, -impl VerlocNodeResult { - pub fn new( - node_identity: identity::PublicKey, - latest_measurement: Option<VerlocMeasurement>, - ) -> Self { - VerlocNodeResult { - node_identity, - latest_measurement, - } - } -} + #[serde(with = "time::serde::rfc3339::option")] + pub run_finished: Option<OffsetDateTime>, -impl PartialOrd for VerlocNodeResult { - fn partial_cmp(&self, other: &Self) -> Option<Ordering> { - Some(self.cmp(other)) + pub results: Vec<VerlocNodeResult>, } -} -impl Ord for VerlocNodeResult { - fn cmp(&self, other: &Self) -> Ordering { - // if both have measurement, compare measurements - // then if only one have measurement, prefer that one - // completely ignore identity as it makes no sense to order by it - if let Some(self_measurement) = &self.latest_measurement { - if let Some(other_measurement) = &other.latest_measurement { - self_measurement.cmp(other_measurement) - } else { - Ordering::Less - } - } else if other.latest_measurement.is_some() { - Ordering::Greater - } else { - Ordering::Equal - } + #[derive(Serialize, Deserialize, Debug, Clone, Copy, Eq, PartialEq)] + #[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] + pub struct VerlocNodeResult { + #[serde(with = "bs58_ed25519_pubkey")] + pub node_identity: ed25519::PublicKey, + + pub latest_measurement: Option<VerlocMeasurement>, } -} -#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] -pub struct VerlocMeasurement { - /// Minimum RTT duration it took to receive an echo packet. - #[serde(serialize_with = "humantime_serde::serialize")] - pub minimum: Duration, + #[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] + #[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] + pub struct VerlocMeasurement { + /// Minimum RTT duration it took to receive an echo packet. + #[serde(serialize_with = "humantime_serde::serialize")] + pub minimum: Duration, - /// Average RTT duration it took to receive the echo packets. - #[serde(serialize_with = "humantime_serde::serialize")] - pub mean: Duration, + /// Average RTT duration it took to receive the echo packets. + #[serde(serialize_with = "humantime_serde::serialize")] + pub mean: Duration, - /// Maximum RTT duration it took to receive an echo packet. - #[serde(serialize_with = "humantime_serde::serialize")] - pub maximum: Duration, + /// Maximum RTT duration it took to receive an echo packet. + #[serde(serialize_with = "humantime_serde::serialize")] + pub maximum: Duration, - /// The standard deviation of the RTT duration it took to receive the echo packets. - #[serde(serialize_with = "humantime_serde::serialize")] - pub standard_deviation: Duration, + /// The standard deviation of the RTT duration it took to receive the echo packets. + #[serde(serialize_with = "humantime_serde::serialize")] + pub standard_deviation: Duration, + } } -impl VerlocMeasurement { - pub fn new(raw_results: &[Duration]) -> Self { - let minimum = raw_results.iter().min().copied().unwrap_or_default(); - let maximum = raw_results.iter().max().copied().unwrap_or_default(); +pub mod session { + use serde::{Deserialize, Serialize}; + use time::OffsetDateTime; - let mean = Self::duration_mean(raw_results); - let standard_deviation = Self::duration_standard_deviation(raw_results, mean); - - VerlocMeasurement { - minimum, - mean, - maximum, - standard_deviation, - } + #[derive(Serialize, Deserialize, Debug, Clone)] + #[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] + pub struct Session { + pub duration_ms: u64, + pub typ: String, } - fn duration_mean(data: &[Duration]) -> Duration { - if data.is_empty() { - return Default::default(); - } - - let sum = data.iter().sum::<Duration>(); - let count = data.len() as u32; - - sum / count - } + #[derive(Serialize, Deserialize, Debug, Clone)] + #[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] + pub struct SessionStats { + #[serde(with = "time::serde::rfc3339")] + pub update_time: OffsetDateTime, - fn duration_standard_deviation(data: &[Duration], mean: Duration) -> Duration { - if data.is_empty() { - return Default::default(); - } - - let variance_micros = data - .iter() - .map(|&value| { - // make sure we don't underflow - let diff = if mean > value { - mean - value - } else { - value - mean - }; - // we don't need nanos precision - let diff_micros = diff.as_micros(); - diff_micros * diff_micros - }) - .sum::<u128>() - / data.len() as u128; - - // we shouldn't really overflow as our differences shouldn't be larger than couple seconds at the worst possible case scenario - let std_deviation_micros = (variance_micros as f64).sqrt() as u64; - Duration::from_micros(std_deviation_micros) - } -} + pub unique_active_users: u32, -impl Display for VerlocMeasurement { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - write!( - f, - "rtt min/avg/max/mdev = {} / {} / {} / {}", - humantime::format_duration(self.minimum), - humantime::format_duration(self.mean), - humantime::format_duration(self.maximum), - humantime::format_duration(self.standard_deviation) - ) - } -} + #[serde(default = "Vec::new")] // field was added later + pub unique_active_users_hashes: Vec<String>, -impl PartialOrd for VerlocMeasurement { - fn partial_cmp(&self, other: &Self) -> Option<Ordering> { - Some(self.cmp(other)) - } -} + pub sessions: Vec<Session>, -impl Ord for VerlocMeasurement { - fn cmp(&self, other: &Self) -> Ordering { - // minimum value is most important, then look at standard deviation, then mean and finally maximum - let min_cmp = self.minimum.cmp(&other.minimum); - if min_cmp != Ordering::Equal { - return min_cmp; - } - let std_dev_cmp = self.standard_deviation.cmp(&other.standard_deviation); - if std_dev_cmp != Ordering::Equal { - return std_dev_cmp; - } - let std_dev_cmp = self.mean.cmp(&other.mean); - if std_dev_cmp != Ordering::Equal { - return std_dev_cmp; - } - self.maximum.cmp(&other.maximum) - } -} + pub sessions_started: u32, -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn sorting_vec_of_verlocs() { - let some_identity = - identity::PublicKey::from_base58_string("Be9wH7xuXBRJAuV1pC7MALZv6a61RvWQ3SypsNarqTt") - .unwrap(); - let no_measurement = VerlocNodeResult::new(some_identity, None); - let low_min = VerlocNodeResult::new( - some_identity, - Some(VerlocMeasurement { - minimum: Duration::from_millis(42), - mean: Duration::from_millis(43), - maximum: Duration::from_millis(44), - standard_deviation: Duration::from_millis(45), - }), - ); - let higher_min = VerlocNodeResult::new( - some_identity, - Some(VerlocMeasurement { - minimum: Duration::from_millis(420), - mean: Duration::from_millis(430), - maximum: Duration::from_millis(440), - standard_deviation: Duration::from_millis(450), - }), - ); - - let mut vec_verloc = vec![no_measurement, low_min, no_measurement, higher_min]; - vec_verloc.sort(); - - let expected_sorted = vec![low_min, higher_min, no_measurement, no_measurement]; - assert_eq!(expected_sorted, vec_verloc); + pub sessions_finished: u32, } } diff --git a/nym-node/nym-node-requests/src/lib.rs b/nym-node/nym-node-requests/src/lib.rs index 983b1a6aecd..661936797a7 100644 --- a/nym-node/nym-node-requests/src/lib.rs +++ b/nym-node/nym-node-requests/src/lib.rs @@ -64,12 +64,14 @@ pub mod routes { pub mod metrics { use super::*; - pub const MIXING: &str = "/mixing"; + pub const LEGACY_MIXING: &str = "/mixing"; + pub const PACKETS_STATS: &str = "/packets-stats"; pub const SESSIONS: &str = "/sessions"; pub const VERLOC: &str = "/verloc"; pub const PROMETHEUS: &str = "/prometheus"; - absolute_route!(mixing_absolute, metrics_absolute(), MIXING); + absolute_route!(legacy_mixing_absolute, metrics_absolute(), LEGACY_MIXING); + absolute_route!(packets_stats_absolute, metrics_absolute(), PACKETS_STATS); absolute_route!(sessions_absolute, metrics_absolute(), SESSIONS); absolute_route!(verloc_absolute, metrics_absolute(), VERLOC); absolute_route!(prometheus_absolute, metrics_absolute(), PROMETHEUS); diff --git a/nym-node/src/cli/commands/bonding_information.rs b/nym-node/src/cli/commands/bonding_information.rs index cefa4da954a..a864e14cbb0 100644 --- a/nym-node/src/cli/commands/bonding_information.rs +++ b/nym-node/src/cli/commands/bonding_information.rs @@ -2,25 +2,16 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::cli::helpers::ConfigArgs; -use crate::env::vars::NYMNODE_MODE_ARG; -use crate::node::bonding_information::BondingInformationV1; +use crate::config::upgrade_helpers::try_load_current_config; +use crate::error::NymNodeError; +use crate::node::bonding_information::BondingInformation; use nym_bin_common::output_format::OutputFormat; -use nym_node::config::upgrade_helpers::try_load_current_config; -use nym_node::config::NodeMode; -use nym_node::error::NymNodeError; #[derive(Debug, clap::Args)] pub struct Args { #[clap(flatten)] pub(crate) config: ConfigArgs, - #[clap( - long, - value_enum, - env = NYMNODE_MODE_ARG - )] - pub(crate) mode: Option<NodeMode>, - /// Specify the output format of the bonding information (`text` or `json`) #[clap( short, @@ -32,10 +23,7 @@ pub struct Args { pub async fn execute(args: Args) -> Result<(), NymNodeError> { let config = try_load_current_config(args.config.config_path()).await?; - let mut info = BondingInformationV1::try_load(&config)?; - if let Some(mode) = args.mode { - info = info.with_mode(mode) - } + let info = BondingInformation::try_load(&config)?; args.output.to_stdout(&info); Ok(()) } diff --git a/nym-node/src/cli/commands/build_info.rs b/nym-node/src/cli/commands/build_info.rs index e8f28ba3c4f..612f681e4e6 100644 --- a/nym-node/src/cli/commands/build_info.rs +++ b/nym-node/src/cli/commands/build_info.rs @@ -1,9 +1,9 @@ // Copyright 2024 - Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: GPL-3.0-only +use crate::error::NymNodeError; use nym_bin_common::bin_info_owned; use nym_bin_common::output_format::OutputFormat; -use nym_node::error::NymNodeError; #[derive(clap::Args, Debug)] pub(crate) struct Args { diff --git a/nym-node/src/cli/commands/migrate.rs b/nym-node/src/cli/commands/migrate.rs index efb721245d0..b47ee6853c9 100644 --- a/nym-node/src/cli/commands/migrate.rs +++ b/nym-node/src/cli/commands/migrate.rs @@ -1,9 +1,9 @@ // Copyright 2024 - Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: GPL-3.0-only +use crate::error::NymNodeError; use colored::Color::TrueColor; use colored::Colorize; -use nym_node::error::NymNodeError; #[derive(clap::Args, Debug)] pub(crate) struct Args { diff --git a/nym-node/src/cli/commands/node_details.rs b/nym-node/src/cli/commands/node_details.rs index 64e6a4f3ac5..1b8ec693600 100644 --- a/nym-node/src/cli/commands/node_details.rs +++ b/nym-node/src/cli/commands/node_details.rs @@ -2,10 +2,10 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::cli::helpers::ConfigArgs; +use crate::config::upgrade_helpers::try_load_current_config; +use crate::error::NymNodeError; use crate::node::NymNode; use nym_bin_common::output_format::OutputFormat; -use nym_node::config::upgrade_helpers::try_load_current_config; -use nym_node::error::NymNodeError; #[derive(Debug, clap::Args)] pub(crate) struct Args { @@ -23,7 +23,7 @@ pub(crate) struct Args { pub async fn execute(args: Args) -> Result<(), NymNodeError> { let config = try_load_current_config(args.config.config_path()).await?; - let details = NymNode::new(config).await?.display_details(); + let details = NymNode::new(config).await?.display_details()?; args.output.to_stdout(&details); Ok(()) } diff --git a/nym-node/src/cli/commands/run/args.rs b/nym-node/src/cli/commands/run/args.rs index 68e59fd44ce..d366d0169be 100644 --- a/nym-node/src/cli/commands/run/args.rs +++ b/nym-node/src/cli/commands/run/args.rs @@ -2,14 +2,14 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::cli::helpers::{ - ConfigArgs, EntryGatewayArgs, ExitGatewayArgs, HostArgs, HttpArgs, MixnetArgs, MixnodeArgs, - WireguardArgs, + ConfigArgs, EntryGatewayArgs, ExitGatewayArgs, HostArgs, HttpArgs, MetricsArgs, MixnetArgs, + VerlocArgs, WireguardArgs, }; +use crate::config::persistence::NymNodePaths; +use crate::config::{Config, ConfigBuilder, NodeMode, NodeModes}; use crate::env::vars::*; +use crate::error::NymNodeError; use nym_bin_common::output_format::OutputFormat; -use nym_node::config::persistence::NymNodePaths; -use nym_node::config::{Config, ConfigBuilder, NodeMode}; -use nym_node::error::NymNodeError; use std::path::PathBuf; use zeroize::Zeroizing; @@ -55,13 +55,25 @@ pub(crate) struct Args { )] pub(crate) local: bool, - /// Specifies the current mode of this nym-node. + /// Specifies the current mode(s) of this nym-node. #[clap( long, value_enum, - env = NYMNODE_MODE_ARG + env = NYMNODE_MODE_ARG, + num_args(0..=3), + group = "node_mode" )] - pub(crate) mode: Option<NodeMode>, + pub(crate) mode: Option<Vec<NodeMode>>, + + /// Specifies the current mode(s) of this nym-node as a single flag. + #[clap( + long, + value_enum, + env = NYMNODE_MODES_ARG, + value_delimiter = ',', + group = "node_mode" + )] + pub(crate) modes: Vec<NodeMode>, /// If this node has been initialised before, specify whether to write any new changes to the config file. #[clap( @@ -99,11 +111,14 @@ pub(crate) struct Args { #[clap(flatten)] mixnet: MixnetArgs, + #[clap(flatten)] + metrics: MetricsArgs, + #[clap(flatten)] wireguard: WireguardArgs, #[clap(flatten)] - mixnode: MixnodeArgs, + verloc: VerlocArgs, #[clap(flatten)] entry_gateway: EntryGatewayArgs, @@ -119,6 +134,18 @@ impl Args { } impl Args { + pub(crate) fn custom_modes(&self) -> Option<NodeModes> { + if let Some(explicit_modes) = &self.mode { + return Some(explicit_modes.as_slice().into()); + } + + if !self.modes.is_empty() { + return Some(self.modes.as_slice().into()); + } + + None + } + pub(crate) fn build_config(self) -> Result<Config, NymNodeError> { let config_path = self.config.config_path(); let data_dir = Config::default_data_directory(&config_path)?; @@ -133,35 +160,41 @@ impl Args { })?; let config = ConfigBuilder::new(id, config_path.clone(), data_dir.clone()) - .with_mode(self.mode.unwrap_or_default()) + // the old default behaviour of running in mixnode mode if nothing is explicitly set + .with_modes( + self.custom_modes() + .unwrap_or(*NodeModes::default().with_mixnode()), + ) .with_host(self.host.build_config_section()) .with_http(self.http.build_config_section()) .with_mixnet(self.mixnet.build_config_section()) .with_wireguard(self.wireguard.build_config_section(&data_dir)) .with_storage_paths(NymNodePaths::new(&data_dir)) - .with_mixnode(self.mixnode.build_config_section()) - .with_entry_gateway(self.entry_gateway.build_config_section(&data_dir)) - .with_exit_gateway(self.exit_gateway.build_config_section(&data_dir)) + .with_verloc(self.verloc.build_config_section()) + .with_metrics(self.metrics.build_config_section()) + .with_gateway_tasks(self.entry_gateway.build_config_section(&data_dir)) + .with_service_providers(self.exit_gateway.build_config_section(&data_dir)) .build(); Ok(config) } pub(crate) fn override_config(self, mut config: Config) -> Config { - if let Some(mode) = self.mode { - config.mode = mode; + if let Some(modes) = self.custom_modes() { + config.modes = modes; } + config.host = self.host.override_config_section(config.host); config.http = self.http.override_config_section(config.http); config.mixnet = self.mixnet.override_config_section(config.mixnet); config.wireguard = self.wireguard.override_config_section(config.wireguard); - config.mixnode = self.mixnode.override_config_section(config.mixnode); - config.entry_gateway = self + config.metrics = self.metrics.override_config_section(config.metrics); + config.gateway_tasks = self .entry_gateway - .override_config_section(config.entry_gateway); - config.exit_gateway = self + .override_config_section(config.gateway_tasks); + config.service_providers = self .exit_gateway - .override_config_section(config.exit_gateway); + .override_config_section(config.service_providers); config } } diff --git a/nym-node/src/cli/commands/run/mod.rs b/nym-node/src/cli/commands/run/mod.rs index 8135e66f8c5..bcaa1e5106e 100644 --- a/nym-node/src/cli/commands/run/mod.rs +++ b/nym-node/src/cli/commands/run/mod.rs @@ -1,15 +1,14 @@ // Copyright 2024 - Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: GPL-3.0-only -use crate::node::bonding_information::BondingInformationV1; +use crate::config::upgrade_helpers::try_load_current_config; +use crate::error::NymNodeError; +use crate::node::bonding_information::BondingInformation; use crate::node::NymNode; use nym_config::helpers::SPECIAL_ADDRESSES; -use nym_node::config::upgrade_helpers::try_load_current_config; -use nym_node::error::NymNodeError; use std::fs; use std::net::IpAddr; -use tracing::log::warn; -use tracing::{debug, info, trace}; +use tracing::{debug, info, trace, warn}; mod args; @@ -80,6 +79,10 @@ pub(crate) async fn execute(mut args: Args) -> Result<(), NymNodeError> { config }; + if !config.modes.any_enabled() { + warn!("this node is going to run without mixnode or gateway support! consider providing `mode` value"); + } + if config.host.public_ips.is_empty() { return Err(NymNodeError::NoPublicIps); } @@ -95,11 +98,8 @@ pub(crate) async fn execute(mut args: Args) -> Result<(), NymNodeError> { "writing bonding information to '{}'", bonding_info_path.display() ); - let info = BondingInformationV1::from_data( - nym_node.mode(), - nym_node.ed25519_identity_key().to_base58_string(), - nym_node.x25519_sphinx_key().to_base58_string(), - ); + let info = + BondingInformation::from_data(nym_node.config(), *nym_node.ed25519_identity_key()); let data = output.format(&info); fs::write(&bonding_info_path, data).map_err(|source| { NymNodeError::BondingInfoWriteFailure { diff --git a/nym-node/src/cli/commands/sign.rs b/nym-node/src/cli/commands/sign.rs index d22b3c9e283..ab220773969 100644 --- a/nym-node/src/cli/commands/sign.rs +++ b/nym-node/src/cli/commands/sign.rs @@ -2,11 +2,11 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::cli::helpers::ConfigArgs; +use crate::config::upgrade_helpers::try_load_current_config; +use crate::error::NymNodeError; use crate::node::helpers::load_ed25519_identity_keypair; use nym_bin_common::output_format::OutputFormat; use nym_crypto::asymmetric::identity; -use nym_node::config::upgrade_helpers::try_load_current_config; -use nym_node::error::NymNodeError; use nym_types::helpers::ConsoleSigningOutput; // I don't think it makes sense to expose 'text' and 'contract-msg' as env variables diff --git a/nym-node/src/cli/helpers.rs b/nym-node/src/cli/helpers.rs index 4ca33dbed34..b17a86be1b9 100644 --- a/nym-node/src/cli/helpers.rs +++ b/nym-node/src/cli/helpers.rs @@ -2,12 +2,12 @@ // SPDX-License-Identifier: GPL-3.0-only use super::DEFAULT_NYMNODE_ID; +use crate::config; +use crate::config::default_config_filepath; use crate::env::vars::*; use celes::Country; use clap::builder::ArgPredicate; use clap::Args; -use nym_node::config; -use nym_node::config::default_config_filepath; use std::net::{IpAddr, SocketAddr}; use std::path::{Path, PathBuf}; use url::Url; @@ -313,7 +313,7 @@ impl WireguardArgs { } #[derive(clap::Args, Debug)] -pub(crate) struct MixnodeArgs { +pub(crate) struct VerlocArgs { /// Socket address this node will use for binding its verloc API. /// default: `0.0.0.0:1790` #[clap( @@ -332,21 +332,43 @@ pub(crate) struct MixnodeArgs { pub(crate) verloc_announce_port: Option<u16>, } -impl MixnodeArgs { - // TODO: could we perhaps make a clap error here and call `safe_exit` instead? - pub(crate) fn build_config_section(self) -> config::MixnodeConfig { - self.override_config_section(config::MixnodeConfig::new_default()) +impl VerlocArgs { + pub(crate) fn build_config_section(self) -> config::Verloc { + self.override_config_section(config::Verloc::default()) } - pub(crate) fn override_config_section( - self, - mut section: config::MixnodeConfig, - ) -> config::MixnodeConfig { + pub(crate) fn override_config_section(self, mut section: config::Verloc) -> config::Verloc { if let Some(bind_address) = self.verloc_bind_address { - section.verloc.bind_address = bind_address + section.bind_address = bind_address } if let Some(announce_port) = self.verloc_announce_port { - section.verloc.announce_port = Some(announce_port) + section.announce_port = Some(announce_port) + } + section + } +} + +#[derive(clap::Args, Debug)] +pub(crate) struct MetricsArgs { + /// Specify whether running statistics of this node should be logged to the console. + #[clap( + long, + env = NYMNODE_ENABLE_CONSOLE_LOGGING + )] + enable_console_logging: Option<bool>, +} + +impl MetricsArgs { + pub(crate) fn build_config_section(self) -> config::MetricsConfig { + self.override_config_section(config::MetricsConfig::default()) + } + + pub(crate) fn override_config_section( + self, + mut section: config::MetricsConfig, + ) -> config::MetricsConfig { + if let Some(enable_console_logging) = self.enable_console_logging { + section.debug.log_stats_to_console = enable_console_logging; } section } @@ -400,14 +422,14 @@ impl EntryGatewayArgs { pub(crate) fn build_config_section<P: AsRef<Path>>( self, data_dir: P, - ) -> config::EntryGatewayConfig { - self.override_config_section(config::EntryGatewayConfig::new_default(data_dir)) + ) -> config::GatewayTasksConfig { + self.override_config_section(config::GatewayTasksConfig::new_default(data_dir)) } pub(crate) fn override_config_section( self, - mut section: config::EntryGatewayConfig, - ) -> config::EntryGatewayConfig { + mut section: config::GatewayTasksConfig, + ) -> config::GatewayTasksConfig { if let Some(bind_address) = self.entry_bind_address { section.bind_address = bind_address } @@ -448,14 +470,14 @@ impl ExitGatewayArgs { pub(crate) fn build_config_section<P: AsRef<Path>>( self, data_dir: P, - ) -> config::ExitGatewayConfig { - self.override_config_section(config::ExitGatewayConfig::new_default(data_dir)) + ) -> config::ServiceProvidersConfig { + self.override_config_section(config::ServiceProvidersConfig::new_default(data_dir)) } pub(crate) fn override_config_section( self, - mut section: config::ExitGatewayConfig, - ) -> config::ExitGatewayConfig { + mut section: config::ServiceProvidersConfig, + ) -> config::ServiceProvidersConfig { if let Some(upstream_exit_policy) = self.upstream_exit_policy_url { section.upstream_exit_policy_url = upstream_exit_policy } diff --git a/nym-node/src/cli/mod.rs b/nym-node/src/cli/mod.rs index 762e2d9bce8..d3374c7fdf5 100644 --- a/nym-node/src/cli/mod.rs +++ b/nym-node/src/cli/mod.rs @@ -3,9 +3,9 @@ use crate::cli::commands::{bonding_information, build_info, migrate, node_details, run, sign}; use crate::env::vars::{NYMNODE_CONFIG_ENV_FILE_ARG, NYMNODE_NO_BANNER_ARG}; +use crate::error::NymNodeError; use clap::{Parser, Subcommand}; use nym_bin_common::bin_info; -use nym_node::error::NymNodeError; use std::sync::OnceLock; mod commands; diff --git a/nym-node/src/config/exit_gateway.rs b/nym-node/src/config/exit_gateway.rs deleted file mode 100644 index 20aa884c083..00000000000 --- a/nym-node/src/config/exit_gateway.rs +++ /dev/null @@ -1,297 +0,0 @@ -// Copyright 2024 - Nym Technologies SA <contact@nymtech.net> -// SPDX-License-Identifier: GPL-3.0-only - -use crate::config::helpers::ephemeral_gateway_config; -use crate::config::persistence::ExitGatewayPaths; -use crate::config::Config; -use crate::error::ExitGatewayError; -use nym_client_core_config_types::DebugConfig as ClientDebugConfig; -use nym_config::defaults::mainnet; -use nym_gateway::node::{ - LocalAuthenticatorOpts, LocalIpPacketRouterOpts, LocalNetworkRequesterOpts, -}; -use serde::{Deserialize, Serialize}; -use std::path::Path; -use url::Url; - -use super::{ - helpers::{base_client_config, EphemeralConfig}, - LocalWireguardOpts, -}; - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct ExitGatewayConfig { - pub storage_paths: ExitGatewayPaths, - - /// specifies whether this exit node should run in 'open-proxy' mode - /// and thus would attempt to resolve **ANY** request it receives. - pub open_proxy: bool, - - /// Specifies the url for an upstream source of the exit policy used by this node. - pub upstream_exit_policy_url: Url, - - pub network_requester: NetworkRequester, - - pub ip_packet_router: IpPacketRouter, - - #[serde(default)] - pub debug: Debug, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct Debug { - /// Number of messages from offline client that can be pulled at once (i.e. with a single SQL query) from the storage. - pub message_retrieval_limit: i64, -} - -impl Debug { - const DEFAULT_MESSAGE_RETRIEVAL_LIMIT: i64 = 100; -} - -impl Default for Debug { - fn default() -> Self { - Debug { - message_retrieval_limit: Self::DEFAULT_MESSAGE_RETRIEVAL_LIMIT, - } - } -} - -impl ExitGatewayConfig { - pub fn new_default<P: AsRef<Path>>(data_dir: P) -> Self { - #[allow(clippy::expect_used)] - // SAFETY: - // we expect our default values to be well-formed - ExitGatewayConfig { - storage_paths: ExitGatewayPaths::new(data_dir), - open_proxy: false, - upstream_exit_policy_url: mainnet::EXIT_POLICY_URL - .parse() - .expect("invalid default exit policy URL"), - network_requester: Default::default(), - ip_packet_router: Default::default(), - debug: Default::default(), - } - } -} - -#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Serialize)] -pub struct NetworkRequester { - #[serde(default)] - pub debug: NetworkRequesterDebug, -} - -#[allow(clippy::derivable_impls)] -impl Default for NetworkRequester { - fn default() -> Self { - NetworkRequester { - debug: Default::default(), - } - } -} - -#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Serialize)] -pub struct NetworkRequesterDebug { - /// Specifies whether network requester service is enabled in this process. - /// This is only here for debugging purposes as exit gateway should always run **both** - /// network requester and an ip packet router. - pub enabled: bool, - - /// Disable Poisson sending rate. - /// This is equivalent to setting client_debug.traffic.disable_main_poisson_packet_distribution = true - /// (or is it (?)) - pub disable_poisson_rate: bool, - - /// Shared detailed client configuration options - #[serde(flatten)] - pub client_debug: ClientDebugConfig, -} - -impl Default for NetworkRequesterDebug { - fn default() -> Self { - NetworkRequesterDebug { - enabled: true, - disable_poisson_rate: true, - client_debug: Default::default(), - } - } -} - -#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)] -pub struct IpPacketRouter { - #[serde(default)] - pub debug: IpPacketRouterDebug, -} - -#[allow(clippy::derivable_impls)] -impl Default for IpPacketRouter { - fn default() -> Self { - IpPacketRouter { - debug: Default::default(), - } - } -} - -#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Serialize)] -#[serde(default)] -pub struct IpPacketRouterDebug { - /// Specifies whether ip packet routing service is enabled in this process. - /// This is only here for debugging purposes as exit gateway should always run **both** - /// network requester and an ip packet router. - pub enabled: bool, - - /// Disable Poisson sending rate. - /// This is equivalent to setting client_debug.traffic.disable_main_poisson_packet_distribution = true - /// (or is it (?)) - pub disable_poisson_rate: bool, - - /// Shared detailed client configuration options - #[serde(flatten)] - pub client_debug: ClientDebugConfig, -} - -impl Default for IpPacketRouterDebug { - fn default() -> Self { - IpPacketRouterDebug { - enabled: true, - disable_poisson_rate: true, - client_debug: Default::default(), - } - } -} - -// that function is rather disgusting, but I hope it's not going to live for too long -pub fn ephemeral_exit_gateway_config( - config: Config, - mnemonic: &bip39::Mnemonic, -) -> Result<EphemeralConfig, ExitGatewayError> { - let mut nr_opts = LocalNetworkRequesterOpts { - config: nym_network_requester::Config { - base: nym_client_core_config_types::Config { - client: base_client_config(&config), - debug: config.exit_gateway.network_requester.debug.client_debug, - }, - network_requester: nym_network_requester::config::NetworkRequester { - open_proxy: config.exit_gateway.open_proxy, - disable_poisson_rate: config - .exit_gateway - .network_requester - .debug - .disable_poisson_rate, - upstream_exit_policy_url: Some( - config.exit_gateway.upstream_exit_policy_url.clone(), - ), - }, - storage_paths: nym_network_requester::config::NetworkRequesterPaths { - common_paths: config - .exit_gateway - .storage_paths - .network_requester - .to_common_client_paths(), - }, - network_requester_debug: Default::default(), - logging: config.logging, - }, - custom_mixnet_path: None, - }; - - // SAFETY: this function can only fail if fastmode or nocover is set alongside medium_toggle which is not the case here - #[allow(clippy::unwrap_used)] - nr_opts - .config - .base - .try_apply_traffic_modes( - nr_opts.config.network_requester.disable_poisson_rate, - false, - false, - false, - ) - .unwrap(); - - let mut ipr_opts = LocalIpPacketRouterOpts { - config: nym_ip_packet_router::Config { - base: nym_client_core_config_types::Config { - client: base_client_config(&config), - debug: config.exit_gateway.ip_packet_router.debug.client_debug, - }, - ip_packet_router: nym_ip_packet_router::config::IpPacketRouter { - disable_poisson_rate: config - .exit_gateway - .ip_packet_router - .debug - .disable_poisson_rate, - upstream_exit_policy_url: Some( - config.exit_gateway.upstream_exit_policy_url.clone(), - ), - }, - storage_paths: nym_ip_packet_router::config::IpPacketRouterPaths { - common_paths: config - .exit_gateway - .storage_paths - .ip_packet_router - .to_common_client_paths(), - ip_packet_router_description: Default::default(), - }, - - logging: config.logging, - }, - custom_mixnet_path: None, - }; - - if ipr_opts.config.ip_packet_router.disable_poisson_rate { - ipr_opts.config.base.set_no_poisson_process() - } - - let mut auth_opts = LocalAuthenticatorOpts { - config: nym_authenticator::Config { - base: nym_client_core_config_types::Config { - client: base_client_config(&config), - debug: config.authenticator.debug.client_debug, - }, - authenticator: config.wireguard.clone().into(), - storage_paths: nym_authenticator::config::AuthenticatorPaths { - common_paths: config - .exit_gateway - .storage_paths - .authenticator - .to_common_client_paths(), - }, - logging: config.logging, - }, - custom_mixnet_path: None, - }; - - if config.authenticator.debug.disable_poisson_rate { - auth_opts.config.base.set_no_poisson_process(); - } - - let ipr_enabled = config.exit_gateway.ip_packet_router.debug.enabled; - let nr_enabled = config.exit_gateway.network_requester.debug.enabled; - - let wg_opts = LocalWireguardOpts { - config: super::Wireguard { - enabled: config.wireguard.enabled, - bind_address: config.wireguard.bind_address, - private_ipv4: config.wireguard.private_ipv4, - private_ipv6: config.wireguard.private_ipv6, - announced_port: config.wireguard.announced_port, - private_network_prefix_v4: config.wireguard.private_network_prefix_v4, - private_network_prefix_v6: config.wireguard.private_network_prefix_v6, - storage_paths: config.wireguard.storage_paths.clone(), - }, - custom_mixnet_path: None, - }; - - let mut gateway = ephemeral_gateway_config(config, mnemonic)?; - gateway.ip_packet_router.enabled = ipr_enabled; - gateway.network_requester.enabled = nr_enabled; - - Ok(EphemeralConfig { - nr_opts: Some(nr_opts), - ipr_opts: Some(ipr_opts), - auth_opts, - wg_opts, - gateway, - }) -} diff --git a/nym-node/src/config/entry_gateway.rs b/nym-node/src/config/gateway_tasks.rs similarity index 66% rename from nym-node/src/config/entry_gateway.rs rename to nym-node/src/config/gateway_tasks.rs index 8ab330e0902..677d381155a 100644 --- a/nym-node/src/config/entry_gateway.rs +++ b/nym-node/src/config/gateway_tasks.rs @@ -1,30 +1,23 @@ // Copyright 2024 - Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: GPL-3.0-only -use crate::config::helpers::ephemeral_gateway_config; -use crate::config::persistence::EntryGatewayPaths; -use crate::config::Config; -use crate::error::EntryGatewayError; +use crate::config::persistence::GatewayTasksPaths; use nym_config::defaults::{DEFAULT_CLIENT_LISTENING_PORT, TICKETBOOK_VALIDITY_DAYS}; use nym_config::helpers::inaddr_any; use nym_config::serde_helpers::de_maybe_port; -use nym_gateway::node::LocalAuthenticatorOpts; use serde::{Deserialize, Serialize}; use std::net::SocketAddr; use std::path::Path; use std::time::Duration; -use super::helpers::{base_client_config, EphemeralConfig}; -use super::LocalWireguardOpts; - pub const DEFAULT_WS_PORT: u16 = DEFAULT_CLIENT_LISTENING_PORT; #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(deny_unknown_fields)] -pub struct EntryGatewayConfig { - pub storage_paths: EntryGatewayPaths, +pub struct GatewayTasksConfig { + pub storage_paths: GatewayTasksPaths, - /// Indicates whether this gateway is accepting only coconut credentials for accessing the mixnet + /// Indicates whether this gateway is accepting only zk-nym credentials for accessing the mixnet /// or if it also accepts non-paying clients pub enforce_zk_nyms: bool, @@ -136,10 +129,10 @@ impl Default for ZkNymTicketHandlerDebug { } } -impl EntryGatewayConfig { +impl GatewayTasksConfig { pub fn new_default<P: AsRef<Path>>(data_dir: P) -> Self { - EntryGatewayConfig { - storage_paths: EntryGatewayPaths::new(data_dir), + GatewayTasksConfig { + storage_paths: GatewayTasksPaths::new(data_dir), enforce_zk_nyms: false, bind_address: SocketAddr::new(inaddr_any(), DEFAULT_WS_PORT), announce_ws_port: None, @@ -148,55 +141,3 @@ impl EntryGatewayConfig { } } } - -// a temporary solution until all nodes are even more tightly integrated -pub fn ephemeral_entry_gateway_config( - config: Config, - mnemonic: &bip39::Mnemonic, -) -> Result<EphemeralConfig, EntryGatewayError> { - let mut auth_opts = LocalAuthenticatorOpts { - config: nym_authenticator::Config { - base: nym_client_core_config_types::Config { - client: base_client_config(&config), - debug: config.authenticator.debug.client_debug, - }, - authenticator: config.wireguard.clone().into(), - storage_paths: nym_authenticator::config::AuthenticatorPaths { - common_paths: config - .exit_gateway - .storage_paths - .authenticator - .to_common_client_paths(), - }, - logging: config.logging, - }, - custom_mixnet_path: None, - }; - - if config.authenticator.debug.disable_poisson_rate { - auth_opts.config.base.set_no_poisson_process(); - } - - let wg_opts = LocalWireguardOpts { - config: super::Wireguard { - enabled: config.wireguard.enabled, - bind_address: config.wireguard.bind_address, - private_ipv4: config.wireguard.private_ipv4, - private_ipv6: config.wireguard.private_ipv6, - announced_port: config.wireguard.announced_port, - private_network_prefix_v4: config.wireguard.private_network_prefix_v4, - private_network_prefix_v6: config.wireguard.private_network_prefix_v6, - storage_paths: config.wireguard.storage_paths.clone(), - }, - custom_mixnet_path: None, - }; - - let gateway = ephemeral_gateway_config(config, mnemonic)?; - Ok(EphemeralConfig { - nr_opts: None, - ipr_opts: None, - auth_opts, - wg_opts, - gateway, - }) -} diff --git a/nym-node/src/config/helpers.rs b/nym-node/src/config/helpers.rs index 9b7c9d3af97..3ba91b5432b 100644 --- a/nym-node/src/config/helpers.rs +++ b/nym-node/src/config/helpers.rs @@ -1,106 +1,54 @@ // Copyright 2024 - Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: GPL-3.0-only +use super::LocalWireguardOpts; use crate::config::Config; use clap::crate_version; use nym_gateway::node::{ LocalAuthenticatorOpts, LocalIpPacketRouterOpts, LocalNetworkRequesterOpts, }; -use std::net::IpAddr; -use thiserror::Error; - -use super::LocalWireguardOpts; - -#[derive(Debug, Error)] -#[error("currently it's not supported to have different ip addresses for clients and mixnet ({clients_bind_ip} and {mix_bind_ip} were used)")] -pub struct UnsupportedGatewayAddresses { - clients_bind_ip: IpAddr, - mix_bind_ip: IpAddr, -} - -// a temporary solution until all nodes are even more tightly integrated -pub fn ephemeral_gateway_config( - config: Config, - mnemonic: &bip39::Mnemonic, -) -> Result<nym_gateway::config::Config, UnsupportedGatewayAddresses> { - let host = nym_gateway::config::Host { - public_ips: config.host.public_ips, - hostname: config.host.hostname, - }; - - let http = nym_gateway::config::Http { - bind_address: config.http.bind_address, - landing_page_assets_path: config.http.landing_page_assets_path, - }; - - let clients_bind_ip = config.entry_gateway.bind_address.ip(); - let mix_bind_ip = config.mixnet.bind_address.ip(); - if clients_bind_ip != mix_bind_ip { - return Err(UnsupportedGatewayAddresses { - clients_bind_ip, - mix_bind_ip, - }); - } - - // SAFETY: we're using hardcoded valid url here (that won't be used anyway) - #[allow(clippy::unwrap_used)] - let gateway = nym_gateway::config::Gateway { - // that field is very much irrelevant, but I guess let's keep them for now - version: format!("{}-nym-node", crate_version!()), - id: config.id, - only_coconut_credentials: config.entry_gateway.enforce_zk_nyms, - listening_address: clients_bind_ip, - mix_port: config.mixnet.bind_address.port(), - clients_port: config.entry_gateway.bind_address.port(), - clients_wss_port: config.entry_gateway.announce_wss_port, - nym_api_urls: config.mixnet.nym_api_urls, - nyxd_urls: config.mixnet.nyxd_urls, - - // that's nasty but can't do anything about it for this temporary solution : ( - cosmos_mnemonic: mnemonic.clone(), - }; - Ok(nym_gateway::config::Config::externally_loaded( - host, - http, - gateway, - nym_gateway::config::NetworkRequester { enabled: false }, - nym_gateway::config::IpPacketRouter { enabled: false }, +// a temporary solution until further refactoring is made +fn ephemeral_gateway_config(config: &Config) -> nym_gateway::config::Config { + nym_gateway::config::Config::new( + nym_gateway::config::Gateway { + enforce_zk_nyms: config.gateway_tasks.enforce_zk_nyms, + websocket_bind_address: config.gateway_tasks.bind_address, + nym_api_urls: config.mixnet.nym_api_urls.clone(), + nyxd_urls: config.mixnet.nyxd_urls.clone(), + }, + nym_gateway::config::NetworkRequester { + enabled: config.service_providers.ip_packet_router.debug.enabled, + }, + nym_gateway::config::IpPacketRouter { + enabled: config.service_providers.network_requester.debug.enabled, + }, nym_gateway::config::Debug { - packet_forwarding_initial_backoff: config - .mixnet - .debug - .packet_forwarding_initial_backoff, - packet_forwarding_maximum_backoff: config - .mixnet - .debug - .packet_forwarding_maximum_backoff, - initial_connection_timeout: config.mixnet.debug.initial_connection_timeout, - maximum_connection_buffer_size: config.mixnet.debug.maximum_connection_buffer_size, - message_retrieval_limit: config.entry_gateway.debug.message_retrieval_limit, - use_legacy_framed_packet_version: false, + client_bandwidth_max_flushing_rate: + nym_gateway::config::DEFAULT_CLIENT_BANDWIDTH_MAX_FLUSHING_RATE, + client_bandwidth_max_delta_flushing_amount: + nym_gateway::config::DEFAULT_CLIENT_BANDWIDTH_MAX_DELTA_FLUSHING_AMOUNT, zk_nym_tickets: nym_gateway::config::ZkNymTicketHandlerDebug { revocation_bandwidth_penalty: config - .entry_gateway + .gateway_tasks .debug .zk_nym_tickets .revocation_bandwidth_penalty, - pending_poller: config.entry_gateway.debug.zk_nym_tickets.pending_poller, - minimum_api_quorum: config.entry_gateway.debug.zk_nym_tickets.minimum_api_quorum, + pending_poller: config.gateway_tasks.debug.zk_nym_tickets.pending_poller, + minimum_api_quorum: config.gateway_tasks.debug.zk_nym_tickets.minimum_api_quorum, minimum_redemption_tickets: config - .entry_gateway + .gateway_tasks .debug .zk_nym_tickets .minimum_redemption_tickets, maximum_time_between_redemption: config - .entry_gateway + .gateway_tasks .debug .zk_nym_tickets .maximum_time_between_redemption, }, - ..Default::default() }, - )) + ) } pub fn base_client_config(config: &Config) -> nym_client_core_config_types::Client { @@ -114,10 +62,145 @@ pub fn base_client_config(config: &Config) -> nym_client_core_config_types::Clie } } -pub struct EphemeralConfig { +pub struct GatewayTasksConfig { pub gateway: nym_gateway::config::Config, pub nr_opts: Option<LocalNetworkRequesterOpts>, pub ipr_opts: Option<LocalIpPacketRouterOpts>, - pub auth_opts: LocalAuthenticatorOpts, + pub auth_opts: Option<LocalAuthenticatorOpts>, + #[allow(dead_code)] pub wg_opts: LocalWireguardOpts, } + +// that function is rather disgusting, but I hope it's not going to live for too long +pub fn gateway_tasks_config(config: &Config) -> GatewayTasksConfig { + let mut nr_opts = LocalNetworkRequesterOpts { + config: nym_network_requester::Config { + base: nym_client_core_config_types::Config { + client: base_client_config(config), + debug: config + .service_providers + .network_requester + .debug + .client_debug, + }, + network_requester: nym_network_requester::config::NetworkRequester { + open_proxy: config.service_providers.open_proxy, + disable_poisson_rate: config + .service_providers + .network_requester + .debug + .disable_poisson_rate, + upstream_exit_policy_url: Some( + config.service_providers.upstream_exit_policy_url.clone(), + ), + }, + storage_paths: nym_network_requester::config::NetworkRequesterPaths { + common_paths: config + .service_providers + .storage_paths + .network_requester + .to_common_client_paths(), + }, + network_requester_debug: Default::default(), + logging: config.logging, + }, + custom_mixnet_path: None, + }; + + // SAFETY: this function can only fail if fastmode or nocover is set alongside medium_toggle which is not the case here + #[allow(clippy::unwrap_used)] + nr_opts + .config + .base + .try_apply_traffic_modes( + nr_opts.config.network_requester.disable_poisson_rate, + false, + false, + false, + ) + .unwrap(); + + let mut ipr_opts = LocalIpPacketRouterOpts { + config: nym_ip_packet_router::Config { + base: nym_client_core_config_types::Config { + client: base_client_config(config), + debug: config.service_providers.ip_packet_router.debug.client_debug, + }, + ip_packet_router: nym_ip_packet_router::config::IpPacketRouter { + disable_poisson_rate: config + .service_providers + .ip_packet_router + .debug + .disable_poisson_rate, + upstream_exit_policy_url: Some( + config.service_providers.upstream_exit_policy_url.clone(), + ), + }, + storage_paths: nym_ip_packet_router::config::IpPacketRouterPaths { + common_paths: config + .service_providers + .storage_paths + .ip_packet_router + .to_common_client_paths(), + ip_packet_router_description: Default::default(), + }, + + logging: config.logging, + }, + custom_mixnet_path: None, + }; + + if ipr_opts.config.ip_packet_router.disable_poisson_rate { + ipr_opts.config.base.set_no_poisson_process() + } + + let mut auth_opts = LocalAuthenticatorOpts { + config: nym_authenticator::Config { + base: nym_client_core_config_types::Config { + client: base_client_config(config), + debug: config.service_providers.authenticator.debug.client_debug, + }, + authenticator: config.wireguard.clone().into(), + storage_paths: nym_authenticator::config::AuthenticatorPaths { + common_paths: config + .service_providers + .storage_paths + .authenticator + .to_common_client_paths(), + }, + logging: config.logging, + }, + custom_mixnet_path: None, + }; + + if config + .service_providers + .authenticator + .debug + .disable_poisson_rate + { + auth_opts.config.base.set_no_poisson_process(); + } + + let wg_opts = LocalWireguardOpts { + config: super::Wireguard { + enabled: config.wireguard.enabled, + bind_address: config.wireguard.bind_address, + private_ipv4: config.wireguard.private_ipv4, + private_ipv6: config.wireguard.private_ipv6, + announced_port: config.wireguard.announced_port, + private_network_prefix_v4: config.wireguard.private_network_prefix_v4, + private_network_prefix_v6: config.wireguard.private_network_prefix_v6, + storage_paths: config.wireguard.storage_paths.clone(), + }, + custom_mixnet_path: None, + }; + + GatewayTasksConfig { + gateway: ephemeral_gateway_config(config), + nr_opts: Some(nr_opts), + ipr_opts: Some(ipr_opts), + auth_opts: Some(auth_opts), + wg_opts, + } +} diff --git a/nym-node/src/config/metrics.rs b/nym-node/src/config/metrics.rs new file mode 100644 index 00000000000..80f9dcea6db --- /dev/null +++ b/nym-node/src/config/metrics.rs @@ -0,0 +1,60 @@ +// Copyright 2024 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: GPL-3.0-only + +use serde::{Deserialize, Serialize}; +use std::time::Duration; + +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct MetricsConfig { + #[serde(default)] + pub debug: Debug, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct Debug { + /// Specify whether running statistics of this node should be logged to the console. + pub log_stats_to_console: bool, + + /// Specify the rate of which the metrics aggregator should call the `on_update` methods of all its registered handlers. + #[serde(with = "humantime_serde")] + pub aggregator_update_rate: Duration, + + /// Specify the target rate of clearing old stale mixnet metrics. + #[serde(with = "humantime_serde")] + pub stale_mixnet_metrics_cleaner_rate: Duration, + + /// Specify the rate of updating clients sessions + #[serde(with = "humantime_serde")] + pub clients_sessions_update_rate: Duration, + + /// If console logging is enabled, specify the interval at which that happens + #[serde(with = "humantime_serde")] + pub console_logging_update_interval: Duration, + + /// Specify the update rate of running stats for the legacy `/metrics/mixing` endpoint + #[serde(with = "humantime_serde")] + pub legacy_mixing_metrics_update_rate: Duration, +} + +impl Debug { + const DEFAULT_CONSOLE_LOGGING_INTERVAL: Duration = Duration::from_millis(60_000); + const DEFAULT_LEGACY_MIXING_UPDATE_RATE: Duration = Duration::from_millis(30_000); + const DEFAULT_AGGREGATOR_UPDATE_RATE: Duration = Duration::from_secs(5); + const DEFAULT_STALE_MIXNET_ETRICS_UPDATE_RATE: Duration = Duration::from_secs(3600); + const DEFAULT_CLIENT_SESSIONS_UPDATE_RATE: Duration = Duration::from_secs(3600); +} + +impl Default for Debug { + fn default() -> Self { + Debug { + log_stats_to_console: true, + console_logging_update_interval: Self::DEFAULT_CONSOLE_LOGGING_INTERVAL, + legacy_mixing_metrics_update_rate: Self::DEFAULT_LEGACY_MIXING_UPDATE_RATE, + aggregator_update_rate: Self::DEFAULT_AGGREGATOR_UPDATE_RATE, + stale_mixnet_metrics_cleaner_rate: Self::DEFAULT_STALE_MIXNET_ETRICS_UPDATE_RATE, + clients_sessions_update_rate: Self::DEFAULT_CLIENT_SESSIONS_UPDATE_RATE, + } + } +} diff --git a/nym-node/src/config/mixnode.rs b/nym-node/src/config/mixnode.rs deleted file mode 100644 index 92f9279c2f6..00000000000 --- a/nym-node/src/config/mixnode.rs +++ /dev/null @@ -1,221 +0,0 @@ -// Copyright 2024 - Nym Technologies SA <contact@nymtech.net> -// SPDX-License-Identifier: GPL-3.0-only - -use crate::config::persistence::MixnodePaths; -use crate::config::Config; -use crate::error::MixnodeError; -use clap::crate_version; -use nym_config::defaults::DEFAULT_VERLOC_LISTENING_PORT; -use nym_config::helpers::inaddr_any; -use nym_config::serde_helpers::de_maybe_port; -use serde::{Deserialize, Serialize}; -use std::net::SocketAddr; -use std::time::Duration; - -pub const DEFAULT_VERLOC_PORT: u16 = DEFAULT_VERLOC_LISTENING_PORT; - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct MixnodeConfig { - pub storage_paths: MixnodePaths, - - pub verloc: Verloc, - - #[serde(default)] - pub debug: Debug, -} - -impl MixnodeConfig { - pub fn new_default() -> Self { - MixnodeConfig { - storage_paths: MixnodePaths {}, - verloc: Default::default(), - debug: Default::default(), - } - } -} - -#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)] -#[serde(deny_unknown_fields)] -pub struct Verloc { - /// Socket address this node will use for binding its verloc API. - /// default: `0.0.0.0:1790` - pub bind_address: SocketAddr, - - /// If applicable, custom port announced in the self-described API that other clients and nodes - /// will use. - /// Useful when the node is behind a proxy. - #[serde(deserialize_with = "de_maybe_port")] - #[serde(default)] - pub announce_port: Option<u16>, - - #[serde(default)] - pub debug: VerlocDebug, -} - -impl Default for Verloc { - fn default() -> Self { - Verloc { - bind_address: SocketAddr::new(inaddr_any(), DEFAULT_VERLOC_PORT), - announce_port: None, - debug: Default::default(), - } - } -} - -#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)] -#[serde(deny_unknown_fields)] -pub struct VerlocDebug { - /// Specifies number of echo packets sent to each node during a measurement run. - pub packets_per_node: usize, - - /// Specifies maximum amount of time to wait for the connection to get established. - #[serde(with = "humantime_serde")] - pub connection_timeout: Duration, - - /// Specifies maximum amount of time to wait for the reply packet to arrive before abandoning the test. - #[serde(with = "humantime_serde")] - pub packet_timeout: Duration, - - /// Specifies delay between subsequent test packets being sent (after receiving a reply). - #[serde(with = "humantime_serde")] - pub delay_between_packets: Duration, - - /// Specifies number of nodes being tested at once. - pub tested_nodes_batch_size: usize, - - /// Specifies delay between subsequent test runs. - #[serde(with = "humantime_serde")] - pub testing_interval: Duration, - - /// Specifies delay between attempting to run the measurement again if the previous run failed - /// due to being unable to get the list of nodes. - #[serde(with = "humantime_serde")] - pub retry_timeout: Duration, -} - -impl VerlocDebug { - const DEFAULT_PACKETS_PER_NODE: usize = 100; - const DEFAULT_CONNECTION_TIMEOUT: Duration = Duration::from_millis(5000); - const DEFAULT_PACKET_TIMEOUT: Duration = Duration::from_millis(1500); - const DEFAULT_DELAY_BETWEEN_PACKETS: Duration = Duration::from_millis(50); - const DEFAULT_BATCH_SIZE: usize = 50; - const DEFAULT_TESTING_INTERVAL: Duration = Duration::from_secs(60 * 60 * 12); - const DEFAULT_RETRY_TIMEOUT: Duration = Duration::from_secs(60 * 30); -} - -impl Default for VerlocDebug { - fn default() -> Self { - VerlocDebug { - packets_per_node: VerlocDebug::DEFAULT_PACKETS_PER_NODE, - connection_timeout: VerlocDebug::DEFAULT_CONNECTION_TIMEOUT, - packet_timeout: VerlocDebug::DEFAULT_PACKET_TIMEOUT, - delay_between_packets: VerlocDebug::DEFAULT_DELAY_BETWEEN_PACKETS, - tested_nodes_batch_size: VerlocDebug::DEFAULT_BATCH_SIZE, - testing_interval: VerlocDebug::DEFAULT_TESTING_INTERVAL, - retry_timeout: VerlocDebug::DEFAULT_RETRY_TIMEOUT, - } - } -} - -impl From<VerlocDebug> for nym_mixnode::config::Verloc { - fn from(value: VerlocDebug) -> Self { - nym_mixnode::config::Verloc { - packets_per_node: value.packets_per_node, - connection_timeout: value.connection_timeout, - packet_timeout: value.packet_timeout, - delay_between_packets: value.delay_between_packets, - tested_nodes_batch_size: value.tested_nodes_batch_size, - testing_interval: value.testing_interval, - retry_timeout: value.retry_timeout, - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct Debug { - /// Delay between each subsequent node statistics being logged to the console - #[serde(with = "humantime_serde")] - pub node_stats_logging_delay: Duration, - - /// Delay between each subsequent node statistics being updated - #[serde(with = "humantime_serde")] - pub node_stats_updating_delay: Duration, -} - -impl Debug { - const DEFAULT_NODE_STATS_LOGGING_DELAY: Duration = Duration::from_millis(60_000); - const DEFAULT_NODE_STATS_UPDATING_DELAY: Duration = Duration::from_millis(30_000); -} - -impl Default for Debug { - fn default() -> Self { - Debug { - node_stats_logging_delay: Debug::DEFAULT_NODE_STATS_LOGGING_DELAY, - node_stats_updating_delay: Debug::DEFAULT_NODE_STATS_UPDATING_DELAY, - } - } -} - -// a temporary solution until all nodes are even more tightly integrated -pub fn ephemeral_mixnode_config( - config: Config, -) -> Result<nym_mixnode::config::Config, MixnodeError> { - let host = nym_mixnode::config::Host { - public_ips: config.host.public_ips, - hostname: config.host.hostname, - }; - - let http = nym_mixnode::config::Http { - bind_address: config.http.bind_address, - landing_page_assets_path: config.http.landing_page_assets_path, - metrics_key: config.http.access_token, - }; - - let verloc_bind_ip = config.mixnode.verloc.bind_address.ip(); - let mix_bind_ip = config.mixnet.bind_address.ip(); - if verloc_bind_ip != mix_bind_ip { - return Err(MixnodeError::UnsupportedAddresses { - verloc_bind_ip, - mix_bind_ip, - }); - } - - let listening_address = mix_bind_ip; - let mix_port = config.mixnet.bind_address.port(); - let verloc_port = config.mixnode.verloc.bind_address.port(); - let nym_api_urls = config.mixnet.nym_api_urls; - - let mixnode = nym_mixnode::config::MixNode { - // that field is very much irrelevant, but I guess let's keep them for now - version: format!("{}-nym-node", crate_version!()), - id: config.id, - listening_address, - mix_port, - verloc_port, - nym_api_urls, - }; - - Ok(nym_mixnode::config::Config::externally_loaded( - host, - http, - mixnode, - config.mixnode.verloc.debug, - nym_mixnode::config::Debug { - node_stats_logging_delay: config.mixnode.debug.node_stats_logging_delay, - node_stats_updating_delay: config.mixnode.debug.node_stats_updating_delay, - packet_forwarding_initial_backoff: config - .mixnet - .debug - .packet_forwarding_initial_backoff, - packet_forwarding_maximum_backoff: config - .mixnet - .debug - .packet_forwarding_maximum_backoff, - initial_connection_timeout: config.mixnet.debug.initial_connection_timeout, - maximum_connection_buffer_size: config.mixnet.debug.maximum_connection_buffer_size, - use_legacy_framed_packet_version: false, - }, - )) -} diff --git a/nym-node/src/config/mod.rs b/nym-node/src/config/mod.rs index 626c9cf098e..6b996f24be0 100644 --- a/nym-node/src/config/mod.rs +++ b/nym-node/src/config/mod.rs @@ -4,13 +4,13 @@ use crate::config::persistence::NymNodePaths; use crate::config::template::CONFIG_TEMPLATE; use crate::error::NymNodeError; -use authenticator::Authenticator; use celes::Country; use clap::ValueEnum; use nym_bin_common::logging::LoggingSettings; use nym_config::defaults::{ - mainnet, var_names, DEFAULT_MIX_LISTENING_PORT, DEFAULT_NYM_NODE_HTTP_PORT, WG_PORT, - WG_TUN_DEVICE_IP_ADDRESS_V4, WG_TUN_DEVICE_IP_ADDRESS_V6, + mainnet, var_names, DEFAULT_MIX_LISTENING_PORT, DEFAULT_NYM_NODE_HTTP_PORT, + DEFAULT_VERLOC_LISTENING_PORT, WG_PORT, WG_TUN_DEVICE_IP_ADDRESS_V4, + WG_TUN_DEVICE_IP_ADDRESS_V6, }; use nym_config::defaults::{WG_TUN_DEVICE_NETMASK_V4, WG_TUN_DEVICE_NETMASK_V6}; use nym_config::helpers::inaddr_any; @@ -30,18 +30,18 @@ use tracing::{debug, error}; use url::Url; pub mod authenticator; -pub mod entry_gateway; -pub mod exit_gateway; +pub mod gateway_tasks; pub mod helpers; -pub mod mixnode; +pub mod metrics; mod old_configs; pub mod persistence; +pub mod service_providers; mod template; pub mod upgrade_helpers; -pub use crate::config::entry_gateway::EntryGatewayConfig; -pub use crate::config::exit_gateway::ExitGatewayConfig; -pub use crate::config::mixnode::MixnodeConfig; +pub use crate::config::gateway_tasks::GatewayTasksConfig; +pub use crate::config::metrics::MetricsConfig; +pub use crate::config::service_providers::ServiceProvidersConfig; const DEFAULT_NYMNODES_DIR: &str = "nym-nodes"; @@ -89,6 +89,62 @@ impl Display for NodeMode { } } +#[derive(Debug, Default, Serialize, Deserialize, Clone, Copy)] +pub struct NodeModes { + /// Specifies whether this node can operate in a mixnode mode. + pub mixnode: bool, + + /// Specifies whether this node can operate in an entry mode. + pub entry: bool, + + /// Specifies whether this node can operate in an exit mode. + pub exit: bool, + // TODO: would it make sense to also put WG here for completion? +} + +impl From<&[NodeMode]> for NodeModes { + fn from(modes: &[NodeMode]) -> Self { + let mut out = NodeModes::default(); + for &mode in modes { + out.with_mode(mode); + } + out + } +} + +impl NodeModes { + pub fn any_enabled(&self) -> bool { + self.mixnode || self.entry || self.exit + } + + pub fn with_mode(&mut self, mode: NodeMode) -> &mut Self { + match mode { + NodeMode::Mixnode => self.with_mixnode(), + NodeMode::EntryGateway => self.with_entry(), + NodeMode::ExitGateway => self.with_exit(), + } + } + + pub fn expects_final_hop_traffic(&self) -> bool { + self.entry || self.exit + } + + pub fn with_mixnode(&mut self) -> &mut Self { + self.mixnode = true; + self + } + + pub fn with_entry(&mut self) -> &mut Self { + self.entry = true; + self + } + + pub fn with_exit(&mut self) -> &mut Self { + self.exit = true; + self + } +} + pub struct ConfigBuilder { pub id: String, @@ -96,7 +152,7 @@ pub struct ConfigBuilder { pub data_dir: PathBuf, - pub mode: NodeMode, + pub modes: NodeModes, pub mixnet: Option<Mixnet>, @@ -104,17 +160,17 @@ pub struct ConfigBuilder { pub http: Option<Http>, + pub verloc: Option<Verloc>, + pub wireguard: Option<Wireguard>, pub storage_paths: Option<NymNodePaths>, - pub mixnode: Option<MixnodeConfig>, - - pub entry_gateway: Option<EntryGatewayConfig>, + pub gateway_tasks: Option<GatewayTasksConfig>, - pub exit_gateway: Option<ExitGatewayConfig>, + pub service_providers: Option<ServiceProvidersConfig>, - pub authenticator: Option<Authenticator>, + pub metrics: Option<MetricsConfig>, pub logging: Option<LoggingSettings>, } @@ -128,19 +184,19 @@ impl ConfigBuilder { host: None, http: None, mixnet: None, + verloc: None, wireguard: None, - mode: NodeMode::default(), + modes: NodeModes::default(), storage_paths: None, - mixnode: None, - entry_gateway: None, - exit_gateway: None, - authenticator: None, + gateway_tasks: None, + service_providers: None, + metrics: None, logging: None, } } - pub fn with_mode(mut self, mode: impl Into<NodeMode>) -> Self { - self.mode = mode.into(); + pub fn with_modes(mut self, mode: impl Into<NodeModes>) -> Self { + self.modes = mode.into(); self } @@ -154,6 +210,11 @@ impl ConfigBuilder { self } + pub fn with_verloc(mut self, section: impl Into<Option<Verloc>>) -> Self { + self.verloc = section.into(); + self + } + pub fn with_mixnet(mut self, section: impl Into<Option<Mixnet>>) -> Self { self.mixnet = section.into(); self @@ -169,49 +230,48 @@ impl ConfigBuilder { self } - pub fn with_mixnode(mut self, section: impl Into<Option<MixnodeConfig>>) -> Self { - self.mixnode = section.into(); + pub fn with_metrics(mut self, section: impl Into<Option<MetricsConfig>>) -> Self { + self.metrics = section.into(); self } - pub fn with_entry_gateway(mut self, section: impl Into<Option<EntryGatewayConfig>>) -> Self { - self.entry_gateway = section.into(); + pub fn with_gateway_tasks(mut self, section: impl Into<Option<GatewayTasksConfig>>) -> Self { + self.gateway_tasks = section.into(); self } - pub fn with_exit_gateway(mut self, section: impl Into<Option<ExitGatewayConfig>>) -> Self { - self.exit_gateway = section.into(); - self - } - - pub fn with_logging(mut self, section: impl Into<Option<LoggingSettings>>) -> Self { - self.logging = section.into(); + pub fn with_service_providers( + mut self, + section: impl Into<Option<ServiceProvidersConfig>>, + ) -> Self { + self.service_providers = section.into(); self } pub fn build(self) -> Config { Config { id: self.id, - mode: self.mode, + modes: self.modes, host: self.host.unwrap_or_default(), http: self.http.unwrap_or_default(), mixnet: self.mixnet.unwrap_or_default(), + verloc: self.verloc.unwrap_or_default(), wireguard: self .wireguard .unwrap_or_else(|| Wireguard::new_default(&self.data_dir)), storage_paths: self .storage_paths .unwrap_or_else(|| NymNodePaths::new(&self.data_dir)), - mixnode: self.mixnode.unwrap_or_else(MixnodeConfig::new_default), - entry_gateway: self - .entry_gateway - .unwrap_or_else(|| EntryGatewayConfig::new_default(&self.data_dir)), - exit_gateway: self - .exit_gateway - .unwrap_or_else(|| ExitGatewayConfig::new_default(&self.data_dir)), + metrics: self.metrics.unwrap_or_default(), + gateway_tasks: self + .gateway_tasks + .unwrap_or_else(|| GatewayTasksConfig::new_default(&self.data_dir)), + service_providers: self + .service_providers + .unwrap_or_else(|| ServiceProvidersConfig::new_default(&self.data_dir)), logging: self.logging.unwrap_or_default(), save_path: Some(self.config_path), - authenticator: self.authenticator.unwrap_or_default(), + debug: Default::default(), } } } @@ -226,9 +286,8 @@ pub struct Config { /// Human-readable ID of this particular node. pub id: String, - /// Current mode of this nym-node. - /// Expect this field to be changed in the future to allow running the node in multiple modes (i.e. mixnode + gateway) - pub mode: NodeMode, + /// Current modes of this nym-node. + pub modes: NodeModes, pub host: Host, @@ -240,18 +299,25 @@ pub struct Config { #[serde(default)] pub http: Http, - pub wireguard: Wireguard, + #[serde(default)] + pub verloc: Verloc, - pub mixnode: MixnodeConfig, + pub wireguard: Wireguard, - pub entry_gateway: EntryGatewayConfig, + #[serde(alias = "entry_gateway")] + pub gateway_tasks: GatewayTasksConfig, - pub exit_gateway: ExitGatewayConfig, + #[serde(alias = "exit_gateway")] + pub service_providers: ServiceProvidersConfig, - pub authenticator: Authenticator, + #[serde(default)] + pub metrics: MetricsConfig, #[serde(default)] pub logging: LoggingSettings, + + #[serde(default)] + pub debug: Debug, } impl NymConfigTemplate for Config { @@ -437,6 +503,10 @@ pub struct Mixnet { #[serde(default)] #[serde(deny_unknown_fields)] pub struct MixnetDebug { + /// Specifies the duration of time this node is willing to delay a forward packet for. + #[serde(with = "humantime_serde")] + pub maximum_forward_packet_delay: Duration, + /// Initial value of an exponential backoff to reconnect to dropped TCP connection when /// forwarding sphinx packets. #[serde(with = "humantime_serde")] @@ -459,6 +529,10 @@ pub struct MixnetDebug { } impl MixnetDebug { + // given that genuine clients are using mean delay of 50ms, + // the probability of them delaying for over 10s is 10^-87 + // which for all intents and purposes will never happen + const DEFAULT_MAXIMUM_FORWARD_PACKET_DELAY: Duration = Duration::from_secs(10); const DEFAULT_PACKET_FORWARDING_INITIAL_BACKOFF: Duration = Duration::from_millis(10_000); const DEFAULT_PACKET_FORWARDING_MAXIMUM_BACKOFF: Duration = Duration::from_millis(300_000); const DEFAULT_INITIAL_CONNECTION_TIMEOUT: Duration = Duration::from_millis(1_500); @@ -468,6 +542,7 @@ impl MixnetDebug { impl Default for MixnetDebug { fn default() -> Self { MixnetDebug { + maximum_forward_packet_delay: Self::DEFAULT_MAXIMUM_FORWARD_PACKET_DELAY, packet_forwarding_initial_backoff: Self::DEFAULT_PACKET_FORWARDING_INITIAL_BACKOFF, packet_forwarding_maximum_backoff: Self::DEFAULT_PACKET_FORWARDING_MAXIMUM_BACKOFF, initial_connection_timeout: Self::DEFAULT_INITIAL_CONNECTION_TIMEOUT, @@ -507,6 +582,93 @@ impl Default for Mixnet { } } +#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct Verloc { + /// Socket address this node will use for binding its verloc API. + /// default: `0.0.0.0:1790` + pub bind_address: SocketAddr, + + /// If applicable, custom port announced in the self-described API that other clients and nodes + /// will use. + /// Useful when the node is behind a proxy. + #[serde(deserialize_with = "de_maybe_port")] + #[serde(default)] + pub announce_port: Option<u16>, + + #[serde(default)] + pub debug: VerlocDebug, +} + +impl Verloc { + pub const DEFAULT_VERLOC_PORT: u16 = DEFAULT_VERLOC_LISTENING_PORT; +} + +impl Default for Verloc { + fn default() -> Self { + Verloc { + bind_address: SocketAddr::new(inaddr_any(), Self::DEFAULT_VERLOC_PORT), + announce_port: None, + debug: Default::default(), + } + } +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct VerlocDebug { + /// Specifies number of echo packets sent to each node during a measurement run. + pub packets_per_node: usize, + + /// Specifies maximum amount of time to wait for the connection to get established. + #[serde(with = "humantime_serde")] + pub connection_timeout: Duration, + + /// Specifies maximum amount of time to wait for the reply packet to arrive before abandoning the test. + #[serde(with = "humantime_serde")] + pub packet_timeout: Duration, + + /// Specifies delay between subsequent test packets being sent (after receiving a reply). + #[serde(with = "humantime_serde")] + pub delay_between_packets: Duration, + + /// Specifies number of nodes being tested at once. + pub tested_nodes_batch_size: usize, + + /// Specifies delay between subsequent test runs. + #[serde(with = "humantime_serde")] + pub testing_interval: Duration, + + /// Specifies delay between attempting to run the measurement again if the previous run failed + /// due to being unable to get the list of nodes. + #[serde(with = "humantime_serde")] + pub retry_timeout: Duration, +} + +impl VerlocDebug { + const DEFAULT_PACKETS_PER_NODE: usize = 100; + const DEFAULT_CONNECTION_TIMEOUT: Duration = Duration::from_millis(5000); + const DEFAULT_PACKET_TIMEOUT: Duration = Duration::from_millis(1500); + const DEFAULT_DELAY_BETWEEN_PACKETS: Duration = Duration::from_millis(50); + const DEFAULT_BATCH_SIZE: usize = 50; + const DEFAULT_TESTING_INTERVAL: Duration = Duration::from_secs(60 * 60 * 12); + const DEFAULT_RETRY_TIMEOUT: Duration = Duration::from_secs(60 * 30); +} + +impl Default for VerlocDebug { + fn default() -> Self { + VerlocDebug { + packets_per_node: Self::DEFAULT_PACKETS_PER_NODE, + connection_timeout: Self::DEFAULT_CONNECTION_TIMEOUT, + packet_timeout: Self::DEFAULT_PACKET_TIMEOUT, + delay_between_packets: Self::DEFAULT_DELAY_BETWEEN_PACKETS, + tested_nodes_batch_size: Self::DEFAULT_BATCH_SIZE, + testing_interval: Self::DEFAULT_TESTING_INTERVAL, + retry_timeout: Self::DEFAULT_RETRY_TIMEOUT, + } + } +} + #[derive(Debug, Clone, Deserialize, PartialEq, Serialize)] #[serde(deny_unknown_fields)] pub struct Wireguard { @@ -584,7 +746,29 @@ impl From<Wireguard> for nym_authenticator::config::Authenticator { #[derive(Debug, Clone)] pub struct LocalWireguardOpts { + #[allow(dead_code)] pub config: Wireguard, + #[allow(dead_code)] pub custom_mixnet_path: Option<PathBuf>, } + +#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct Debug { + /// Specifies the time to live of the internal topology provider cache. + #[serde(with = "humantime_serde")] + pub topology_cache_ttl: Duration, +} + +impl Debug { + pub const DEFAULT_TOPOLOGY_CACHE_TTL: Duration = Duration::from_secs(5 * 60); +} + +impl Default for Debug { + fn default() -> Self { + Debug { + topology_cache_ttl: Self::DEFAULT_TOPOLOGY_CACHE_TTL, + } + } +} diff --git a/nym-node/src/config/old_configs/mod.rs b/nym-node/src/config/old_configs/mod.rs index 58040fde6b6..f7b12e860da 100644 --- a/nym-node/src/config/old_configs/mod.rs +++ b/nym-node/src/config/old_configs/mod.rs @@ -6,9 +6,11 @@ mod old_config_v2; mod old_config_v3; mod old_config_v4; mod old_config_v5; +mod old_config_v6; pub use old_config_v1::try_upgrade_config_v1; pub use old_config_v2::try_upgrade_config_v2; pub use old_config_v3::try_upgrade_config_v3; pub use old_config_v4::try_upgrade_config_v4; pub use old_config_v5::try_upgrade_config_v5; +pub use old_config_v6::try_upgrade_config_v6; diff --git a/nym-node/src/config/old_configs/old_config_v1.rs b/nym-node/src/config/old_configs/old_config_v1.rs index a09eee36e7c..b268ea86ab0 100644 --- a/nym-node/src/config/old_configs/old_config_v1.rs +++ b/nym-node/src/config/old_configs/old_config_v1.rs @@ -11,6 +11,7 @@ use nym_pemstore::store_keypair; use old_configs::old_config_v2::*; use rand::rngs::OsRng; use serde::{Deserialize, Serialize}; +use tracing::instrument; #[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)] #[serde(deny_unknown_fields)] @@ -21,25 +22,6 @@ pub struct WireguardPathsV1 { pub public_diffie_hellman_key_file: PathBuf, } -impl WireguardPathsV1 { - pub fn new<P: AsRef<Path>>(data_dir: P) -> Self { - let data_dir = data_dir.as_ref(); - WireguardPathsV1 { - private_diffie_hellman_key_file: data_dir - .join(persistence::DEFAULT_X25519_WG_DH_KEY_FILENAME), - public_diffie_hellman_key_file: data_dir - .join(persistence::DEFAULT_X25519_WG_PUBLIC_DH_KEY_FILENAME), - } - } - - pub fn x25519_wireguard_storage_paths(&self) -> nym_pemstore::KeyPairPath { - nym_pemstore::KeyPairPath::new( - &self.private_diffie_hellman_key_file, - &self.public_diffie_hellman_key_file, - ) - } -} - #[derive(Debug, Clone, Deserialize, PartialEq, Serialize)] #[serde(deny_unknown_fields)] pub struct WireguardV1 { @@ -655,77 +637,7 @@ pub struct ConfigV1 { pub logging: LoggingSettingsV1, } -impl NymConfigTemplate for ConfigV1 { - fn template(&self) -> &'static str { - CONFIG_TEMPLATE - } -} - impl ConfigV1 { - pub fn save(&self) -> Result<(), NymNodeError> { - let save_location = self.save_location(); - debug!( - "attempting to save config file to '{}'", - save_location.display() - ); - save_formatted_config_to_file(self, &save_location).map_err(|source| { - NymNodeError::ConfigSaveFailure { - id: self.id.clone(), - path: save_location, - source, - } - }) - } - - pub fn save_location(&self) -> PathBuf { - self.save_path - .clone() - .unwrap_or(self.default_save_location()) - } - - pub fn default_save_location(&self) -> PathBuf { - default_config_filepath(&self.id) - } - - pub fn default_data_directory<P: AsRef<Path>>(config_path: P) -> Result<PathBuf, NymNodeError> { - let config_path = config_path.as_ref(); - - // we got a proper path to the .toml file - let Some(config_dir) = config_path.parent() else { - error!( - "'{}' does not have a parent directory. Have you pointed to the fs root?", - config_path.display() - ); - return Err(NymNodeError::DataDirDerivationFailure); - }; - - let Some(config_dir_name) = config_dir.file_name() else { - error!( - "could not obtain parent directory name of '{}'. Have you used relative paths?", - config_path.display() - ); - return Err(NymNodeError::DataDirDerivationFailure); - }; - - if config_dir_name != DEFAULT_CONFIG_DIR { - error!( - "the parent directory of '{}' ({}) is not {DEFAULT_CONFIG_DIR}. currently this is not supported", - config_path.display(), config_dir_name.to_str().unwrap_or("UNKNOWN") - ); - return Err(NymNodeError::DataDirDerivationFailure); - } - - let Some(node_dir) = config_dir.parent() else { - error!( - "'{}' does not have a parent directory. Have you pointed to the fs root?", - config_dir.display() - ); - return Err(NymNodeError::DataDirDerivationFailure); - }; - - Ok(node_dir.join(DEFAULT_DATA_DIR)) - } - // simple wrapper that reads config file and assigns path location fn read_from_path<P: AsRef<Path>>(path: P) -> Result<Self, NymNodeError> { let path = path.as_ref(); @@ -738,10 +650,6 @@ impl ConfigV1 { debug!("loaded config file from {}", path.display()); Ok(loaded) } - - pub fn read_from_toml_file<P: AsRef<Path>>(path: P) -> Result<Self, NymNodeError> { - Self::read_from_path(path) - } } fn initialise(config: &WireguardV2) -> std::io::Result<()> { @@ -756,15 +664,16 @@ fn initialise(config: &WireguardV2) -> std::io::Result<()> { Ok(()) } +#[instrument(skip_all)] pub async fn try_upgrade_config_v1<P: AsRef<Path>>( path: P, prev_config: Option<ConfigV1>, ) -> Result<ConfigV2, NymNodeError> { - tracing::debug!("Updating from 1.1.2"); + debug!("attempting to load v1 config..."); let old_cfg = if let Some(prev_config) = prev_config { prev_config } else { - ConfigV1::read_from_path(&path)? + ConfigV1::read_from_path(&path).inspect_err(|err| debug!("failed: {err}"))? }; let wireguard = WireguardV2 { enabled: old_cfg.wireguard.enabled, diff --git a/nym-node/src/config/old_configs/old_config_v2.rs b/nym-node/src/config/old_configs/old_config_v2.rs index 28e35e66bcb..712555b3b50 100644 --- a/nym-node/src/config/old_configs/old_config_v2.rs +++ b/nym-node/src/config/old_configs/old_config_v2.rs @@ -16,6 +16,7 @@ use nym_sphinx_acknowledgements::AckKey; use old_configs::old_config_v3::*; use rand::rngs::OsRng; use serde::{Deserialize, Serialize}; +use tracing::instrument; #[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)] #[serde(deny_unknown_fields)] @@ -34,7 +35,6 @@ impl WireguardPathsV2 { .join(persistence::DEFAULT_X25519_WG_PUBLIC_DH_KEY_FILENAME), } } - pub fn x25519_wireguard_storage_paths(&self) -> nym_pemstore::KeyPairPath { nym_pemstore::KeyPairPath::new( &self.private_diffie_hellman_key_file, @@ -658,38 +658,7 @@ pub struct ConfigV2 { pub logging: LoggingSettingsV2, } -impl NymConfigTemplate for ConfigV2 { - fn template(&self) -> &'static str { - CONFIG_TEMPLATE - } -} - impl ConfigV2 { - pub fn save(&self) -> Result<(), NymNodeError> { - let save_location = self.save_location(); - debug!( - "attempting to save config file to '{}'", - save_location.display() - ); - save_formatted_config_to_file(self, &save_location).map_err(|source| { - NymNodeError::ConfigSaveFailure { - id: self.id.clone(), - path: save_location, - source, - } - }) - } - - pub fn save_location(&self) -> PathBuf { - self.save_path - .clone() - .unwrap_or(self.default_save_location()) - } - - pub fn default_save_location(&self) -> PathBuf { - default_config_filepath(&self.id) - } - pub fn default_data_directory<P: AsRef<Path>>(config_path: P) -> Result<PathBuf, NymNodeError> { let config_path = config_path.as_ref(); @@ -741,10 +710,6 @@ impl ConfigV2 { debug!("loaded config file from {}", path.display()); Ok(loaded) } - - pub fn read_from_toml_file<P: AsRef<Path>>(path: P) -> Result<Self, NymNodeError> { - Self::read_from_path(path) - } } pub async fn initialise( @@ -786,15 +751,16 @@ pub async fn initialise( Ok(()) } +#[instrument(skip_all)] pub async fn try_upgrade_config_v2<P: AsRef<Path>>( path: P, prev_config: Option<ConfigV2>, ) -> Result<ConfigV3, NymNodeError> { - tracing::debug!("Updating from 1.1.3"); + debug!("attempting to load v2 config..."); let old_cfg = if let Some(prev_config) = prev_config { prev_config } else { - ConfigV2::read_from_path(&path)? + ConfigV2::read_from_path(&path).inspect_err(|err| debug!("failed: {err}"))? }; let authenticator_paths = AuthenticatorPathsV3::new( diff --git a/nym-node/src/config/old_configs/old_config_v3.rs b/nym-node/src/config/old_configs/old_config_v3.rs index 01ce8a9e494..6796f1aa8cf 100644 --- a/nym-node/src/config/old_configs/old_config_v3.rs +++ b/nym-node/src/config/old_configs/old_config_v3.rs @@ -3,20 +3,13 @@ #![allow(dead_code)] -use crate::{config::*, error::KeyIOFailure}; +use crate::config::*; use nym_client_core_config_types::DebugConfig as ClientDebugConfig; use nym_config::serde_helpers::de_maybe_port; -use nym_crypto::asymmetric::{ed25519, x25519}; -use nym_network_requester::{ - set_active_gateway, setup_fs_gateways_storage, store_gateway_details, CustomGatewayDetails, - GatewayDetails, -}; -use nym_pemstore::{store_key, store_keypair}; -use nym_sphinx_acknowledgements::AckKey; use old_configs::old_config_v4::*; use persistence::*; -use rand::rngs::OsRng; use serde::{Deserialize, Serialize}; +use tracing::instrument; #[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)] #[serde(deny_unknown_fields)] @@ -25,25 +18,6 @@ pub struct WireguardPathsV3 { pub public_diffie_hellman_key_file: PathBuf, } -impl WireguardPathsV3 { - pub fn new<P: AsRef<Path>>(data_dir: P) -> Self { - let data_dir = data_dir.as_ref(); - WireguardPathsV3 { - private_diffie_hellman_key_file: data_dir - .join(persistence::DEFAULT_X25519_WG_DH_KEY_FILENAME), - public_diffie_hellman_key_file: data_dir - .join(persistence::DEFAULT_X25519_WG_PUBLIC_DH_KEY_FILENAME), - } - } - - pub fn x25519_wireguard_storage_paths(&self) -> nym_pemstore::KeyPairPath { - nym_pemstore::KeyPairPath::new( - &self.private_diffie_hellman_key_file, - &self.public_diffie_hellman_key_file, - ) - } -} - #[derive(Debug, Clone, Deserialize, PartialEq, Serialize)] #[serde(deny_unknown_fields)] pub struct WireguardV3 { @@ -810,77 +784,7 @@ pub struct ConfigV3 { pub logging: LoggingSettingsV3, } -impl NymConfigTemplate for ConfigV3 { - fn template(&self) -> &'static str { - CONFIG_TEMPLATE - } -} - impl ConfigV3 { - pub fn save(&self) -> Result<(), NymNodeError> { - let save_location = self.save_location(); - debug!( - "attempting to save config file to '{}'", - save_location.display() - ); - save_formatted_config_to_file(self, &save_location).map_err(|source| { - NymNodeError::ConfigSaveFailure { - id: self.id.clone(), - path: save_location, - source, - } - }) - } - - pub fn save_location(&self) -> PathBuf { - self.save_path - .clone() - .unwrap_or(self.default_save_location()) - } - - pub fn default_save_location(&self) -> PathBuf { - default_config_filepath(&self.id) - } - - pub fn default_data_directory<P: AsRef<Path>>(config_path: P) -> Result<PathBuf, NymNodeError> { - let config_path = config_path.as_ref(); - - // we got a proper path to the .toml file - let Some(config_dir) = config_path.parent() else { - error!( - "'{}' does not have a parent directory. Have you pointed to the fs root?", - config_path.display() - ); - return Err(NymNodeError::DataDirDerivationFailure); - }; - - let Some(config_dir_name) = config_dir.file_name() else { - error!( - "could not obtain parent directory name of '{}'. Have you used relative paths?", - config_path.display() - ); - return Err(NymNodeError::DataDirDerivationFailure); - }; - - if config_dir_name != DEFAULT_CONFIG_DIR { - error!( - "the parent directory of '{}' ({}) is not {DEFAULT_CONFIG_DIR}. currently this is not supported", - config_path.display(), config_dir_name.to_str().unwrap_or("UNKNOWN") - ); - return Err(NymNodeError::DataDirDerivationFailure); - } - - let Some(node_dir) = config_dir.parent() else { - error!( - "'{}' does not have a parent directory. Have you pointed to the fs root?", - config_dir.display() - ); - return Err(NymNodeError::DataDirDerivationFailure); - }; - - Ok(node_dir.join(DEFAULT_DATA_DIR)) - } - // simple wrapper that reads config file and assigns path location fn read_from_path<P: AsRef<Path>>(path: P) -> Result<Self, NymNodeError> { let path = path.as_ref(); @@ -893,63 +797,21 @@ impl ConfigV3 { debug!("loaded config file from {}", path.display()); Ok(loaded) } - - pub fn read_from_toml_file<P: AsRef<Path>>(path: P) -> Result<Self, NymNodeError> { - Self::read_from_path(path) - } -} - -pub async fn initialise( - paths: &AuthenticatorPaths, - public_key: nym_crypto::asymmetric::identity::PublicKey, -) -> Result<(), NymNodeError> { - let mut rng = OsRng; - let ed25519_keys = ed25519::KeyPair::new(&mut rng); - let x25519_keys = x25519::KeyPair::new(&mut rng); - let aes128ctr_key = AckKey::new(&mut rng); - let gateway_details = GatewayDetails::Custom(CustomGatewayDetails::new(public_key)).into(); - - store_keypair(&ed25519_keys, &paths.ed25519_identity_storage_paths()).map_err(|e| { - KeyIOFailure::KeyPairStoreFailure { - keys: "ed25519-identity".to_string(), - paths: paths.ed25519_identity_storage_paths(), - err: e, - } - })?; - store_keypair(&x25519_keys, &paths.x25519_diffie_hellman_storage_paths()).map_err(|e| { - KeyIOFailure::KeyPairStoreFailure { - keys: "x25519-dh".to_string(), - paths: paths.x25519_diffie_hellman_storage_paths(), - err: e, - } - })?; - store_key(&aes128ctr_key, &paths.ack_key_file).map_err(|e| KeyIOFailure::KeyStoreFailure { - key: "ack".to_string(), - path: paths.ack_key_file.clone(), - err: e, - })?; - - // insert all required information into the gateways store - // (I hate that we have to do it, but that's currently the simplest thing to do) - let storage = setup_fs_gateways_storage(&paths.gateway_registrations).await?; - store_gateway_details(&storage, &gateway_details).await?; - set_active_gateway(&storage, &gateway_details.gateway_id().to_base58_string()).await?; - - Ok(()) } +#[instrument(skip_all)] pub async fn try_upgrade_config_v3<P: AsRef<Path>>( path: P, prev_config: Option<ConfigV3>, ) -> Result<ConfigV4, NymNodeError> { - tracing::debug!("Updating from 1.1.4"); + debug!("attempting to load v3 config..."); let old_cfg = if let Some(prev_config) = prev_config { prev_config } else { - ConfigV3::read_from_path(&path)? + ConfigV3::read_from_path(&path).inspect_err(|err| debug!("failed: {err}"))? }; - let exit_gateway_paths = ExitGatewayPaths::new( + let exit_gateway_paths = ServiceProvidersPaths::new( old_cfg .exit_gateway .storage_paths diff --git a/nym-node/src/config/old_configs/old_config_v4.rs b/nym-node/src/config/old_configs/old_config_v4.rs index 8e8e3758d04..fb6d3aaa302 100644 --- a/nym-node/src/config/old_configs/old_config_v4.rs +++ b/nym-node/src/config/old_configs/old_config_v4.rs @@ -3,23 +3,16 @@ #![allow(dead_code)] -use crate::{config::*, error::KeyIOFailure}; +use crate::config::*; use nym_client_core_config_types::{ disk_persistence::{ClientKeysPaths, CommonClientPaths}, DebugConfig as ClientDebugConfig, }; use nym_config::{defaults::TICKETBOOK_VALIDITY_DAYS, serde_helpers::de_maybe_port}; -use nym_crypto::asymmetric::{ed25519, x25519}; -use nym_network_requester::{ - set_active_gateway, setup_fs_gateways_storage, store_gateway_details, CustomGatewayDetails, - GatewayDetails, -}; -use nym_pemstore::{store_key, store_keypair}; -use nym_sphinx_acknowledgements::AckKey; use old_configs::old_config_v5::*; use persistence::*; -use rand::rngs::OsRng; use serde::{Deserialize, Serialize}; +use tracing::instrument; #[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)] #[serde(deny_unknown_fields)] @@ -28,25 +21,6 @@ pub struct WireguardPathsV4 { pub public_diffie_hellman_key_file: PathBuf, } -impl WireguardPathsV4 { - pub fn new<P: AsRef<Path>>(data_dir: P) -> Self { - let data_dir = data_dir.as_ref(); - WireguardPathsV4 { - private_diffie_hellman_key_file: data_dir - .join(persistence::DEFAULT_X25519_WG_DH_KEY_FILENAME), - public_diffie_hellman_key_file: data_dir - .join(persistence::DEFAULT_X25519_WG_PUBLIC_DH_KEY_FILENAME), - } - } - - pub fn x25519_wireguard_storage_paths(&self) -> nym_pemstore::KeyPairPath { - nym_pemstore::KeyPairPath::new( - &self.private_diffie_hellman_key_file, - &self.public_diffie_hellman_key_file, - ) - } -} - #[derive(Debug, Clone, Deserialize, PartialEq, Serialize)] #[serde(deny_unknown_fields)] pub struct WireguardV4 { @@ -203,7 +177,7 @@ pub struct MixnetV4 { /// If applicable, custom port announced in the self-described API that other clients and nodes /// will use. /// Useful when the node is behind a proxy. - #[serde(deserialize_with = "de_maybe_port")] + #[serde(deserialize_with = "de_maybe_port", default)] pub announce_port: Option<u16>, /// Addresses to nym APIs from which the node gets the view of the network. @@ -385,7 +359,7 @@ pub struct VerlocV4 { /// default: `0.0.0.0:1790` pub bind_address: SocketAddr, - #[serde(deserialize_with = "de_maybe_port")] + #[serde(deserialize_with = "de_maybe_port", default)] pub announce_port: Option<u16>, #[serde(default)] @@ -932,77 +906,7 @@ pub struct ConfigV4 { pub logging: LoggingSettingsV4, } -impl NymConfigTemplate for ConfigV4 { - fn template(&self) -> &'static str { - CONFIG_TEMPLATE - } -} - impl ConfigV4 { - pub fn save(&self) -> Result<(), NymNodeError> { - let save_location = self.save_location(); - debug!( - "attempting to save config file to '{}'", - save_location.display() - ); - save_formatted_config_to_file(self, &save_location).map_err(|source| { - NymNodeError::ConfigSaveFailure { - id: self.id.clone(), - path: save_location, - source, - } - }) - } - - pub fn save_location(&self) -> PathBuf { - self.save_path - .clone() - .unwrap_or(self.default_save_location()) - } - - pub fn default_save_location(&self) -> PathBuf { - default_config_filepath(&self.id) - } - - pub fn default_data_directory<P: AsRef<Path>>(config_path: P) -> Result<PathBuf, NymNodeError> { - let config_path = config_path.as_ref(); - - // we got a proper path to the .toml file - let Some(config_dir) = config_path.parent() else { - error!( - "'{}' does not have a parent directory. Have you pointed to the fs root?", - config_path.display() - ); - return Err(NymNodeError::DataDirDerivationFailure); - }; - - let Some(config_dir_name) = config_dir.file_name() else { - error!( - "could not obtain parent directory name of '{}'. Have you used relative paths?", - config_path.display() - ); - return Err(NymNodeError::DataDirDerivationFailure); - }; - - if config_dir_name != DEFAULT_CONFIG_DIR { - error!( - "the parent directory of '{}' ({}) is not {DEFAULT_CONFIG_DIR}. currently this is not supported", - config_path.display(), config_dir_name.to_str().unwrap_or("UNKNOWN") - ); - return Err(NymNodeError::DataDirDerivationFailure); - } - - let Some(node_dir) = config_dir.parent() else { - error!( - "'{}' does not have a parent directory. Have you pointed to the fs root?", - config_dir.display() - ); - return Err(NymNodeError::DataDirDerivationFailure); - }; - - Ok(node_dir.join(DEFAULT_DATA_DIR)) - } - // simple wrapper that reads config file and assigns path location fn read_from_path<P: AsRef<Path>>(path: P) -> Result<Self, NymNodeError> { let path = path.as_ref(); @@ -1015,63 +919,21 @@ impl ConfigV4 { debug!("loaded config file from {}", path.display()); Ok(loaded) } - - pub fn read_from_toml_file<P: AsRef<Path>>(path: P) -> Result<Self, NymNodeError> { - Self::read_from_path(path) - } -} - -pub async fn initialise( - paths: &AuthenticatorPaths, - public_key: nym_crypto::asymmetric::identity::PublicKey, -) -> Result<(), NymNodeError> { - let mut rng = OsRng; - let ed25519_keys = ed25519::KeyPair::new(&mut rng); - let x25519_keys = x25519::KeyPair::new(&mut rng); - let aes128ctr_key = AckKey::new(&mut rng); - let gateway_details = GatewayDetails::Custom(CustomGatewayDetails::new(public_key)).into(); - - store_keypair(&ed25519_keys, &paths.ed25519_identity_storage_paths()).map_err(|e| { - KeyIOFailure::KeyPairStoreFailure { - keys: "ed25519-identity".to_string(), - paths: paths.ed25519_identity_storage_paths(), - err: e, - } - })?; - store_keypair(&x25519_keys, &paths.x25519_diffie_hellman_storage_paths()).map_err(|e| { - KeyIOFailure::KeyPairStoreFailure { - keys: "x25519-dh".to_string(), - paths: paths.x25519_diffie_hellman_storage_paths(), - err: e, - } - })?; - store_key(&aes128ctr_key, &paths.ack_key_file).map_err(|e| KeyIOFailure::KeyStoreFailure { - key: "ack".to_string(), - path: paths.ack_key_file.clone(), - err: e, - })?; - - // insert all required information into the gateways store - // (I hate that we have to do it, but that's currently the simplest thing to do) - let storage = setup_fs_gateways_storage(&paths.gateway_registrations).await?; - store_gateway_details(&storage, &gateway_details).await?; - set_active_gateway(&storage, &gateway_details.gateway_id().to_base58_string()).await?; - - Ok(()) } +#[instrument(skip_all)] pub async fn try_upgrade_config_v4<P: AsRef<Path>>( path: P, prev_config: Option<ConfigV4>, ) -> Result<ConfigV5, NymNodeError> { - tracing::debug!("Updating from 1.1.5"); + debug!("attempting to load v4 config..."); let old_cfg = if let Some(prev_config) = prev_config { prev_config } else { - ConfigV4::read_from_path(&path)? + ConfigV4::read_from_path(&path).inspect_err(|err| debug!("failed: {err}"))? }; - let exit_gateway_paths = ExitGatewayPaths::new( + let exit_gateway_paths = ServiceProvidersPaths::new( old_cfg .exit_gateway .storage_paths @@ -1080,7 +942,7 @@ pub async fn try_upgrade_config_v4<P: AsRef<Path>>( .ok_or(NymNodeError::DataDirDerivationFailure)?, ); - let entry_gateway_paths = EntryGatewayPaths::new( + let entry_gateway_paths = GatewayTasksPaths::new( old_cfg .entry_gateway .storage_paths diff --git a/nym-node/src/config/old_configs/old_config_v5.rs b/nym-node/src/config/old_configs/old_config_v5.rs index 936882f1135..09848023583 100644 --- a/nym-node/src/config/old_configs/old_config_v5.rs +++ b/nym-node/src/config/old_configs/old_config_v5.rs @@ -3,28 +3,16 @@ #![allow(dead_code)] -use crate::{config::*, error::KeyIOFailure}; -use entry_gateway::{Debug as EntryGatewayConfigDebug, ZkNymTicketHandlerDebug}; -use exit_gateway::{ - Debug as ExitGatewayConfigDebug, IpPacketRouter, IpPacketRouterDebug, NetworkRequester, - NetworkRequesterDebug, -}; -use mixnode::{Verloc, VerlocDebug}; +use crate::config::*; use nym_client_core_config_types::{ disk_persistence::{ClientKeysPaths, CommonClientPaths}, DebugConfig as ClientDebugConfig, }; use nym_config::{defaults::TICKETBOOK_VALIDITY_DAYS, serde_helpers::de_maybe_port}; -use nym_crypto::asymmetric::{ed25519, x25519}; -use nym_network_requester::{ - set_active_gateway, setup_fs_gateways_storage, store_gateway_details, CustomGatewayDetails, - GatewayDetails, -}; -use nym_pemstore::{store_key, store_keypair}; -use nym_sphinx_acknowledgements::AckKey; +use old_configs::old_config_v6::*; use persistence::*; -use rand::rngs::OsRng; use serde::{Deserialize, Serialize}; +use tracing::instrument; #[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)] #[serde(deny_unknown_fields)] @@ -33,25 +21,6 @@ pub struct WireguardPathsV5 { pub public_diffie_hellman_key_file: PathBuf, } -impl WireguardPathsV5 { - pub fn new<P: AsRef<Path>>(data_dir: P) -> Self { - let data_dir = data_dir.as_ref(); - WireguardPathsV5 { - private_diffie_hellman_key_file: data_dir - .join(persistence::DEFAULT_X25519_WG_DH_KEY_FILENAME), - public_diffie_hellman_key_file: data_dir - .join(persistence::DEFAULT_X25519_WG_PUBLIC_DH_KEY_FILENAME), - } - } - - pub fn x25519_wireguard_storage_paths(&self) -> nym_pemstore::KeyPairPath { - nym_pemstore::KeyPairPath::new( - &self.private_diffie_hellman_key_file, - &self.public_diffie_hellman_key_file, - ) - } -} - #[derive(Debug, Clone, Deserialize, PartialEq, Serialize)] #[serde(deny_unknown_fields)] pub struct WireguardV5 { @@ -93,12 +62,12 @@ pub enum NodeModeV5 { ExitGateway, } -impl From<NodeModeV5> for NodeMode { +impl From<NodeModeV5> for NodeModeV6 { fn from(config: NodeModeV5) -> Self { match config { - NodeModeV5::Mixnode => NodeMode::Mixnode, - NodeModeV5::EntryGateway => NodeMode::EntryGateway, - NodeModeV5::ExitGateway => NodeMode::ExitGateway, + NodeModeV5::Mixnode => NodeModeV6::Mixnode, + NodeModeV5::EntryGateway => NodeModeV6::EntryGateway, + NodeModeV5::ExitGateway => NodeModeV6::ExitGateway, } } } @@ -208,7 +177,7 @@ pub struct MixnetV5 { /// If applicable, custom port announced in the self-described API that other clients and nodes /// will use. /// Useful when the node is behind a proxy. - #[serde(deserialize_with = "de_maybe_port")] + #[serde(deserialize_with = "de_maybe_port", default)] pub announce_port: Option<u16>, /// Addresses to nym APIs from which the node gets the view of the network. @@ -390,7 +359,7 @@ pub struct VerlocV5 { /// default: `0.0.0.0:1790` pub bind_address: SocketAddr, - #[serde(deserialize_with = "de_maybe_port")] + #[serde(deserialize_with = "de_maybe_port", default)] pub announce_port: Option<u16>, #[serde(default)] @@ -941,77 +910,7 @@ pub struct ConfigV5 { pub logging: LoggingSettingsV5, } -impl NymConfigTemplate for ConfigV5 { - fn template(&self) -> &'static str { - CONFIG_TEMPLATE - } -} - impl ConfigV5 { - pub fn save(&self) -> Result<(), NymNodeError> { - let save_location = self.save_location(); - debug!( - "attempting to save config file to '{}'", - save_location.display() - ); - save_formatted_config_to_file(self, &save_location).map_err(|source| { - NymNodeError::ConfigSaveFailure { - id: self.id.clone(), - path: save_location, - source, - } - }) - } - - pub fn save_location(&self) -> PathBuf { - self.save_path - .clone() - .unwrap_or(self.default_save_location()) - } - - pub fn default_save_location(&self) -> PathBuf { - default_config_filepath(&self.id) - } - - pub fn default_data_directory<P: AsRef<Path>>(config_path: P) -> Result<PathBuf, NymNodeError> { - let config_path = config_path.as_ref(); - - // we got a proper path to the .toml file - let Some(config_dir) = config_path.parent() else { - error!( - "'{}' does not have a parent directory. Have you pointed to the fs root?", - config_path.display() - ); - return Err(NymNodeError::DataDirDerivationFailure); - }; - - let Some(config_dir_name) = config_dir.file_name() else { - error!( - "could not obtain parent directory name of '{}'. Have you used relative paths?", - config_path.display() - ); - return Err(NymNodeError::DataDirDerivationFailure); - }; - - if config_dir_name != DEFAULT_CONFIG_DIR { - error!( - "the parent directory of '{}' ({}) is not {DEFAULT_CONFIG_DIR}. currently this is not supported", - config_path.display(), config_dir_name.to_str().unwrap_or("UNKNOWN") - ); - return Err(NymNodeError::DataDirDerivationFailure); - } - - let Some(node_dir) = config_dir.parent() else { - error!( - "'{}' does not have a parent directory. Have you pointed to the fs root?", - config_dir.display() - ); - return Err(NymNodeError::DataDirDerivationFailure); - }; - - Ok(node_dir.join(DEFAULT_DATA_DIR)) - } - // simple wrapper that reads config file and assigns path location fn read_from_path<P: AsRef<Path>>(path: P) -> Result<Self, NymNodeError> { let path = path.as_ref(); @@ -1024,60 +923,18 @@ impl ConfigV5 { debug!("loaded config file from {}", path.display()); Ok(loaded) } - - pub fn read_from_toml_file<P: AsRef<Path>>(path: P) -> Result<Self, NymNodeError> { - Self::read_from_path(path) - } -} - -pub async fn initialise( - paths: &AuthenticatorPaths, - public_key: nym_crypto::asymmetric::identity::PublicKey, -) -> Result<(), NymNodeError> { - let mut rng = OsRng; - let ed25519_keys = ed25519::KeyPair::new(&mut rng); - let x25519_keys = x25519::KeyPair::new(&mut rng); - let aes128ctr_key = AckKey::new(&mut rng); - let gateway_details = GatewayDetails::Custom(CustomGatewayDetails::new(public_key)).into(); - - store_keypair(&ed25519_keys, &paths.ed25519_identity_storage_paths()).map_err(|e| { - KeyIOFailure::KeyPairStoreFailure { - keys: "ed25519-identity".to_string(), - paths: paths.ed25519_identity_storage_paths(), - err: e, - } - })?; - store_keypair(&x25519_keys, &paths.x25519_diffie_hellman_storage_paths()).map_err(|e| { - KeyIOFailure::KeyPairStoreFailure { - keys: "x25519-dh".to_string(), - paths: paths.x25519_diffie_hellman_storage_paths(), - err: e, - } - })?; - store_key(&aes128ctr_key, &paths.ack_key_file).map_err(|e| KeyIOFailure::KeyStoreFailure { - key: "ack".to_string(), - path: paths.ack_key_file.clone(), - err: e, - })?; - - // insert all required information into the gateways store - // (I hate that we have to do it, but that's currently the simplest thing to do) - let storage = setup_fs_gateways_storage(&paths.gateway_registrations).await?; - store_gateway_details(&storage, &gateway_details).await?; - set_active_gateway(&storage, &gateway_details.gateway_id().to_base58_string()).await?; - - Ok(()) } +#[instrument(skip_all)] pub async fn try_upgrade_config_v5<P: AsRef<Path>>( path: P, prev_config: Option<ConfigV5>, -) -> Result<Config, NymNodeError> { - tracing::debug!("Updating from 1.1.6"); +) -> Result<ConfigV6, NymNodeError> { + debug!("attempting to load v5 config..."); let old_cfg = if let Some(prev_config) = prev_config { prev_config } else { - ConfigV5::read_from_path(&path)? + ConfigV5::read_from_path(&path).inspect_err(|err| debug!("failed: {err}"))? }; let (private_ipv4, private_ipv6) = match old_cfg.wireguard.private_ip { @@ -1085,21 +942,21 @@ pub async fn try_upgrade_config_v5<P: AsRef<Path>>( IpAddr::V6(ipv6_addr) => (WG_TUN_DEVICE_IP_ADDRESS_V4, ipv6_addr), }; - let cfg = Config { + let cfg = ConfigV6 { save_path: old_cfg.save_path, id: old_cfg.id, mode: old_cfg.mode.into(), - host: Host { + host: HostV6 { public_ips: old_cfg.host.public_ips, hostname: old_cfg.host.hostname, location: old_cfg.host.location, }, - mixnet: Mixnet { + mixnet: MixnetV6 { bind_address: old_cfg.mixnet.bind_address, announce_port: old_cfg.mixnet.announce_port, nym_api_urls: old_cfg.mixnet.nym_api_urls, nyxd_urls: old_cfg.mixnet.nyxd_urls, - debug: MixnetDebug { + debug: MixnetDebugV6 { packet_forwarding_initial_backoff: old_cfg .mixnet .debug @@ -1113,8 +970,8 @@ pub async fn try_upgrade_config_v5<P: AsRef<Path>>( unsafe_disable_noise: old_cfg.mixnet.debug.unsafe_disable_noise, }, }, - storage_paths: NymNodePaths { - keys: KeysPaths { + storage_paths: NymNodePathsV6 { + keys: KeysPathsV6 { private_ed25519_identity_key_file: old_cfg .storage_paths .keys @@ -1142,7 +999,7 @@ pub async fn try_upgrade_config_v5<P: AsRef<Path>>( }, description: old_cfg.storage_paths.description, }, - http: Http { + http: HttpV6 { bind_address: old_cfg.http.bind_address, landing_page_assets_path: old_cfg.http.landing_page_assets_path, access_token: old_cfg.http.access_token, @@ -1150,7 +1007,7 @@ pub async fn try_upgrade_config_v5<P: AsRef<Path>>( expose_system_hardware: old_cfg.http.expose_system_hardware, expose_crypto_hardware: old_cfg.http.expose_crypto_hardware, }, - wireguard: Wireguard { + wireguard: WireguardV6 { enabled: old_cfg.wireguard.enabled, bind_address: old_cfg.wireguard.bind_address, private_ipv4, @@ -1158,7 +1015,7 @@ pub async fn try_upgrade_config_v5<P: AsRef<Path>>( announced_port: old_cfg.wireguard.announced_port, private_network_prefix_v4: old_cfg.wireguard.private_network_prefix, private_network_prefix_v6: WG_TUN_DEVICE_NETMASK_V6, - storage_paths: WireguardPaths { + storage_paths: WireguardPathsV6 { private_diffie_hellman_key_file: old_cfg .wireguard .storage_paths @@ -1169,12 +1026,12 @@ pub async fn try_upgrade_config_v5<P: AsRef<Path>>( .public_diffie_hellman_key_file, }, }, - mixnode: MixnodeConfig { - storage_paths: MixnodePaths {}, - verloc: Verloc { + mixnode: MixnodeConfigV6 { + storage_paths: MixnodePathsV6 {}, + verloc: VerlocV6 { bind_address: old_cfg.mixnode.verloc.bind_address, announce_port: old_cfg.mixnode.verloc.announce_port, - debug: VerlocDebug { + debug: VerlocDebugV6 { packets_per_node: old_cfg.mixnode.verloc.debug.packets_per_node, connection_timeout: old_cfg.mixnode.verloc.debug.connection_timeout, packet_timeout: old_cfg.mixnode.verloc.debug.packet_timeout, @@ -1184,17 +1041,17 @@ pub async fn try_upgrade_config_v5<P: AsRef<Path>>( retry_timeout: old_cfg.mixnode.verloc.debug.retry_timeout, }, }, - debug: mixnode::Debug { + debug: DebugV6 { node_stats_logging_delay: old_cfg.mixnode.debug.node_stats_logging_delay, node_stats_updating_delay: old_cfg.mixnode.debug.node_stats_updating_delay, }, }, - entry_gateway: EntryGatewayConfig { - storage_paths: EntryGatewayPaths { + entry_gateway: EntryGatewayConfigV6 { + storage_paths: EntryGatewayPathsV6 { clients_storage: old_cfg.entry_gateway.storage_paths.clients_storage, stats_storage: old_cfg.entry_gateway.storage_paths.stats_storage, cosmos_mnemonic: old_cfg.entry_gateway.storage_paths.cosmos_mnemonic, - authenticator: AuthenticatorPaths { + authenticator: AuthenticatorPathsV6 { private_ed25519_identity_key_file: old_cfg .entry_gateway .storage_paths @@ -1236,9 +1093,9 @@ pub async fn try_upgrade_config_v5<P: AsRef<Path>>( bind_address: old_cfg.entry_gateway.bind_address, announce_ws_port: old_cfg.entry_gateway.announce_ws_port, announce_wss_port: old_cfg.entry_gateway.announce_wss_port, - debug: EntryGatewayConfigDebug { + debug: EntryGatewayConfigDebugV6 { message_retrieval_limit: old_cfg.entry_gateway.debug.message_retrieval_limit, - zk_nym_tickets: ZkNymTicketHandlerDebug { + zk_nym_tickets: ZkNymTicketHandlerDebugV6 { revocation_bandwidth_penalty: old_cfg .entry_gateway .debug @@ -1263,11 +1120,11 @@ pub async fn try_upgrade_config_v5<P: AsRef<Path>>( }, }, }, - exit_gateway: ExitGatewayConfig { - storage_paths: ExitGatewayPaths { + exit_gateway: ExitGatewayConfigV6 { + storage_paths: ExitGatewayPathsV6 { clients_storage: old_cfg.exit_gateway.storage_paths.clients_storage, stats_storage: old_cfg.exit_gateway.storage_paths.stats_storage, - network_requester: NetworkRequesterPaths { + network_requester: NetworkRequesterPathsV6 { private_ed25519_identity_key_file: old_cfg .exit_gateway .storage_paths @@ -1304,7 +1161,7 @@ pub async fn try_upgrade_config_v5<P: AsRef<Path>>( .network_requester .gateway_registrations, }, - ip_packet_router: IpPacketRouterPaths { + ip_packet_router: IpPacketRouterPathsV6 { private_ed25519_identity_key_file: old_cfg .exit_gateway .storage_paths @@ -1341,7 +1198,7 @@ pub async fn try_upgrade_config_v5<P: AsRef<Path>>( .ip_packet_router .gateway_registrations, }, - authenticator: AuthenticatorPaths { + authenticator: AuthenticatorPathsV6 { private_ed25519_identity_key_file: old_cfg .exit_gateway .storage_paths @@ -1381,8 +1238,8 @@ pub async fn try_upgrade_config_v5<P: AsRef<Path>>( }, open_proxy: old_cfg.exit_gateway.open_proxy, upstream_exit_policy_url: old_cfg.exit_gateway.upstream_exit_policy_url, - network_requester: NetworkRequester { - debug: NetworkRequesterDebug { + network_requester: NetworkRequesterV6 { + debug: NetworkRequesterDebugV6 { enabled: old_cfg.exit_gateway.network_requester.debug.enabled, disable_poisson_rate: old_cfg .exit_gateway @@ -1392,8 +1249,8 @@ pub async fn try_upgrade_config_v5<P: AsRef<Path>>( client_debug: old_cfg.exit_gateway.network_requester.debug.client_debug, }, }, - ip_packet_router: IpPacketRouter { - debug: IpPacketRouterDebug { + ip_packet_router: IpPacketRouterV6 { + debug: IpPacketRouterDebugV6 { enabled: old_cfg.exit_gateway.ip_packet_router.debug.enabled, disable_poisson_rate: old_cfg .exit_gateway @@ -1403,12 +1260,12 @@ pub async fn try_upgrade_config_v5<P: AsRef<Path>>( client_debug: old_cfg.exit_gateway.ip_packet_router.debug.client_debug, }, }, - debug: ExitGatewayConfigDebug { + debug: ExitGatewayDebugV6 { message_retrieval_limit: old_cfg.exit_gateway.debug.message_retrieval_limit, }, }, authenticator: Default::default(), - logging: LoggingSettings {}, + logging: LoggingSettingsV6 {}, }; Ok(cfg) diff --git a/nym-node/src/config/old_configs/old_config_v6.rs b/nym-node/src/config/old_configs/old_config_v6.rs new file mode 100644 index 00000000000..b920bf035fd --- /dev/null +++ b/nym-node/src/config/old_configs/old_config_v6.rs @@ -0,0 +1,1256 @@ +// Copyright 2024 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: GPL-3.0-only + +#![allow(dead_code)] + +use crate::config::authenticator::{Authenticator, AuthenticatorDebug}; +use crate::config::gateway_tasks::ZkNymTicketHandlerDebug; +use crate::config::service_providers::{ + IpPacketRouter, IpPacketRouterDebug, NetworkRequester, NetworkRequesterDebug, +}; +use crate::config::*; +use crate::error::NymNodeError; +use celes::Country; +use clap::ValueEnum; +use nym_client_core_config_types::{ + disk_persistence::{ClientKeysPaths, CommonClientPaths}, + DebugConfig as ClientDebugConfig, +}; +use nym_config::defaults::{mainnet, var_names}; +use nym_config::helpers::inaddr_any; +use nym_config::{ + defaults::TICKETBOOK_VALIDITY_DAYS, + serde_helpers::{de_maybe_port, de_maybe_stringified}, +}; +use nym_config::{parse_urls, read_config_from_toml_file}; +use persistence::*; +use serde::{Deserialize, Serialize}; +use std::env; +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; +use std::path::{Path, PathBuf}; +use std::time::Duration; +use tracing::{debug, instrument}; +use url::Url; + +#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct WireguardPathsV6 { + pub private_diffie_hellman_key_file: PathBuf, + pub public_diffie_hellman_key_file: PathBuf, +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct WireguardV6 { + /// Specifies whether the wireguard service is enabled on this node. + pub enabled: bool, + + /// Socket address this node will use for binding its wireguard interface. + /// default: `0.0.0.0:51822` + pub bind_address: SocketAddr, + + /// Private IPv4 address of the wireguard gateway. + /// default: `10.1.0.1` + pub private_ipv4: Ipv4Addr, + + /// Private IPv6 address of the wireguard gateway. + /// default: `fc01::1` + pub private_ipv6: Ipv6Addr, + + /// Port announced to external clients wishing to connect to the wireguard interface. + /// Useful in the instances where the node is behind a proxy. + pub announced_port: u16, + + /// The prefix denoting the maximum number of the clients that can be connected via Wireguard using IPv4. + /// The maximum value for IPv4 is 32 + pub private_network_prefix_v4: u8, + + /// The prefix denoting the maximum number of the clients that can be connected via Wireguard using IPv6. + /// The maximum value for IPv6 is 128 + pub private_network_prefix_v6: u8, + + /// Paths for wireguard keys, client registries, etc. + pub storage_paths: WireguardPathsV6, +} + +// a temporary solution until all "types" are run at the same time +#[derive(Debug, Default, Serialize, Deserialize, ValueEnum, Clone, Copy)] +#[serde(rename_all = "snake_case")] +pub enum NodeModeV6 { + #[default] + Mixnode, + + EntryGateway, + + ExitGateway, +} + +impl From<NodeModeV6> for NodeModes { + fn from(config: NodeModeV6) -> Self { + match config { + NodeModeV6::Mixnode => *NodeModes::default().with_mixnode(), + NodeModeV6::EntryGateway => *NodeModes::default().with_entry(), + // in old version exit implied entry + NodeModeV6::ExitGateway => *NodeModes::default().with_entry().with_exit(), + } + } +} + +// TODO: this is very much a WIP. we need proper ssl certificate support here +#[derive(Debug, Clone, Default, Deserialize, PartialEq, Serialize)] +#[serde(default)] +#[serde(deny_unknown_fields)] +pub struct HostV6 { + /// Ip address(es) of this host, such as 1.1.1.1 that external clients will use for connections. + /// If no values are provided, when this node gets included in the network, + /// its ip addresses will be populated by whatever value is resolved by associated nym-api. + pub public_ips: Vec<IpAddr>, + + /// Optional hostname of this node, for example nymtech.net. + // TODO: this is temporary. to be replaced by pulling the data directly from the certs. + #[serde(deserialize_with = "de_maybe_stringified")] + pub hostname: Option<String>, + + /// Optional ISO 3166 alpha-2 two-letter country code of the node's **physical** location + #[serde(deserialize_with = "de_maybe_stringified")] + pub location: Option<Country>, +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)] +#[serde(default)] +#[serde(deny_unknown_fields)] +pub struct MixnetDebugV6 { + /// Initial value of an exponential backoff to reconnect to dropped TCP connection when + /// forwarding sphinx packets. + #[serde(with = "humantime_serde")] + pub packet_forwarding_initial_backoff: Duration, + + /// Maximum value of an exponential backoff to reconnect to dropped TCP connection when + /// forwarding sphinx packets. + #[serde(with = "humantime_serde")] + pub packet_forwarding_maximum_backoff: Duration, + + /// Timeout for establishing initial connection when trying to forward a sphinx packet. + #[serde(with = "humantime_serde")] + pub initial_connection_timeout: Duration, + + /// Maximum number of packets that can be stored waiting to get sent to a particular connection. + pub maximum_connection_buffer_size: usize, + + /// Specifies whether this node should **NOT** use noise protocol in the connections (currently not implemented) + pub unsafe_disable_noise: bool, +} + +impl MixnetDebugV6 { + const DEFAULT_PACKET_FORWARDING_INITIAL_BACKOFF: Duration = Duration::from_millis(10_000); + const DEFAULT_PACKET_FORWARDING_MAXIMUM_BACKOFF: Duration = Duration::from_millis(300_000); + const DEFAULT_INITIAL_CONNECTION_TIMEOUT: Duration = Duration::from_millis(1_500); + const DEFAULT_MAXIMUM_CONNECTION_BUFFER_SIZE: usize = 2000; +} + +impl Default for MixnetDebugV6 { + fn default() -> Self { + MixnetDebugV6 { + packet_forwarding_initial_backoff: Self::DEFAULT_PACKET_FORWARDING_INITIAL_BACKOFF, + packet_forwarding_maximum_backoff: Self::DEFAULT_PACKET_FORWARDING_MAXIMUM_BACKOFF, + initial_connection_timeout: Self::DEFAULT_INITIAL_CONNECTION_TIMEOUT, + maximum_connection_buffer_size: Self::DEFAULT_MAXIMUM_CONNECTION_BUFFER_SIZE, + // to be changed by @SW once the implementation is there + unsafe_disable_noise: true, + } + } +} + +impl Default for MixnetV6 { + fn default() -> Self { + // SAFETY: + // our hardcoded values should always be valid + #[allow(clippy::expect_used)] + // is if there's anything set in the environment, otherwise fallback to mainnet + let nym_api_urls = if let Ok(env_value) = env::var(var_names::NYM_API) { + parse_urls(&env_value) + } else { + vec![mainnet::NYM_API.parse().expect("Invalid default API URL")] + }; + + #[allow(clippy::expect_used)] + let nyxd_urls = if let Ok(env_value) = env::var(var_names::NYXD) { + parse_urls(&env_value) + } else { + vec![mainnet::NYXD_URL.parse().expect("Invalid default nyxd URL")] + }; + + MixnetV6 { + bind_address: SocketAddr::new(inaddr_any(), DEFAULT_MIXNET_PORT), + announce_port: None, + nym_api_urls, + nyxd_urls, + debug: Default::default(), + } + } +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)] +#[serde(default)] +#[serde(deny_unknown_fields)] +pub struct MixnetV6 { + /// Address this node will bind to for listening for mixnet packets + /// default: `0.0.0.0:1789` + pub bind_address: SocketAddr, + + /// If applicable, custom port announced in the self-described API that other clients and nodes + /// will use. + /// Useful when the node is behind a proxy. + #[serde(deserialize_with = "de_maybe_port")] + pub announce_port: Option<u16>, + + /// Addresses to nym APIs from which the node gets the view of the network. + pub nym_api_urls: Vec<Url>, + + /// Addresses to nyxd which the node uses to interact with the nyx chain. + pub nyxd_urls: Vec<Url>, + + #[serde(default)] + pub debug: MixnetDebugV6, +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct KeysPathsV6 { + /// Path to file containing ed25519 identity private key. + pub private_ed25519_identity_key_file: PathBuf, + + /// Path to file containing ed25519 identity public key. + pub public_ed25519_identity_key_file: PathBuf, + + /// Path to file containing x25519 sphinx private key. + pub private_x25519_sphinx_key_file: PathBuf, + + /// Path to file containing x25519 sphinx public key. + pub public_x25519_sphinx_key_file: PathBuf, + + /// Path to file containing x25519 noise private key. + pub private_x25519_noise_key_file: PathBuf, + + /// Path to file containing x25519 noise public key. + pub public_x25519_noise_key_file: PathBuf, +} + +impl KeysPathsV6 { + pub fn new<P: AsRef<Path>>(data_dir: P) -> Self { + let data_dir = data_dir.as_ref(); + + KeysPathsV6 { + private_ed25519_identity_key_file: data_dir + .join(DEFAULT_ED25519_PRIVATE_IDENTITY_KEY_FILENAME), + public_ed25519_identity_key_file: data_dir + .join(DEFAULT_ED25519_PUBLIC_IDENTITY_KEY_FILENAME), + private_x25519_sphinx_key_file: data_dir + .join(DEFAULT_X25519_PRIVATE_SPHINX_KEY_FILENAME), + public_x25519_sphinx_key_file: data_dir.join(DEFAULT_X25519_PUBLIC_SPHINX_KEY_FILENAME), + private_x25519_noise_key_file: data_dir.join(DEFAULT_X25519_PRIVATE_NOISE_KEY_FILENAME), + public_x25519_noise_key_file: data_dir.join(DEFAULT_X25519_PUBLIC_NOISE_KEY_FILENAME), + } + } + + pub fn ed25519_identity_storage_paths(&self) -> nym_pemstore::KeyPairPath { + nym_pemstore::KeyPairPath::new( + &self.private_ed25519_identity_key_file, + &self.public_ed25519_identity_key_file, + ) + } + + pub fn x25519_sphinx_storage_paths(&self) -> nym_pemstore::KeyPairPath { + nym_pemstore::KeyPairPath::new( + &self.private_x25519_sphinx_key_file, + &self.public_x25519_sphinx_key_file, + ) + } + + pub fn x25519_noise_storage_paths(&self) -> nym_pemstore::KeyPairPath { + nym_pemstore::KeyPairPath::new( + &self.private_x25519_noise_key_file, + &self.public_x25519_noise_key_file, + ) + } +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct NymNodePathsV6 { + pub keys: KeysPathsV6, + + /// Path to a file containing basic node description: human-readable name, website, details, etc. + pub description: PathBuf, +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)] +#[serde(default)] +#[serde(deny_unknown_fields)] +pub struct HttpV6 { + /// Socket address this node will use for binding its http API. + /// default: `0.0.0.0:8080` + pub bind_address: SocketAddr, + + /// Path to assets directory of custom landing page of this node. + #[serde(deserialize_with = "de_maybe_stringified")] + pub landing_page_assets_path: Option<PathBuf>, + + /// An optional bearer token for accessing certain http endpoints. + /// Currently only used for obtaining mixnode's stats. + #[serde(default)] + pub access_token: Option<String>, + + /// Specify whether basic system information should be exposed. + /// default: true + pub expose_system_info: bool, + + /// Specify whether basic system hardware information should be exposed. + /// This option is superseded by `expose_system_info` + /// default: true + pub expose_system_hardware: bool, + + /// Specify whether detailed system crypto hardware information should be exposed. + /// This option is superseded by `expose_system_hardware` + /// default: true + pub expose_crypto_hardware: bool, +} + +impl Default for HttpV6 { + fn default() -> Self { + HttpV6 { + bind_address: SocketAddr::new(inaddr_any(), DEFAULT_HTTP_PORT), + landing_page_assets_path: None, + access_token: None, + expose_system_info: true, + expose_system_hardware: true, + expose_crypto_hardware: true, + } + } +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct MixnodePathsV6 {} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct DebugV6 { + /// Delay between each subsequent node statistics being logged to the console + #[serde(with = "humantime_serde")] + pub node_stats_logging_delay: Duration, + + /// Delay between each subsequent node statistics being updated + #[serde(with = "humantime_serde")] + pub node_stats_updating_delay: Duration, +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct VerlocDebugV6 { + /// Specifies number of echo packets sent to each node during a measurement run. + pub packets_per_node: usize, + + /// Specifies maximum amount of time to wait for the connection to get established. + #[serde(with = "humantime_serde")] + pub connection_timeout: Duration, + + /// Specifies maximum amount of time to wait for the reply packet to arrive before abandoning the test. + #[serde(with = "humantime_serde")] + pub packet_timeout: Duration, + + /// Specifies delay between subsequent test packets being sent (after receiving a reply). + #[serde(with = "humantime_serde")] + pub delay_between_packets: Duration, + + /// Specifies number of nodes being tested at once. + pub tested_nodes_batch_size: usize, + + /// Specifies delay between subsequent test runs. + #[serde(with = "humantime_serde")] + pub testing_interval: Duration, + + /// Specifies delay between attempting to run the measurement again if the previous run failed + /// due to being unable to get the list of nodes. + #[serde(with = "humantime_serde")] + pub retry_timeout: Duration, +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct VerlocV6 { + /// Socket address this node will use for binding its verloc API. + /// default: `0.0.0.0:1790` + pub bind_address: SocketAddr, + + #[serde(deserialize_with = "de_maybe_port")] + pub announce_port: Option<u16>, + + #[serde(default)] + pub debug: VerlocDebugV6, +} + +impl VerlocDebugV6 { + const DEFAULT_PACKETS_PER_NODE: usize = 100; + const DEFAULT_CONNECTION_TIMEOUT: Duration = Duration::from_millis(5000); + const DEFAULT_PACKET_TIMEOUT: Duration = Duration::from_millis(1500); + const DEFAULT_DELAY_BETWEEN_PACKETS: Duration = Duration::from_millis(50); + const DEFAULT_BATCH_SIZE: usize = 50; + const DEFAULT_TESTING_INTERVAL: Duration = Duration::from_secs(60 * 60 * 12); + const DEFAULT_RETRY_TIMEOUT: Duration = Duration::from_secs(60 * 30); +} + +impl Default for VerlocDebugV6 { + fn default() -> Self { + VerlocDebugV6 { + packets_per_node: Self::DEFAULT_PACKETS_PER_NODE, + connection_timeout: Self::DEFAULT_CONNECTION_TIMEOUT, + packet_timeout: Self::DEFAULT_PACKET_TIMEOUT, + delay_between_packets: Self::DEFAULT_DELAY_BETWEEN_PACKETS, + tested_nodes_batch_size: Self::DEFAULT_BATCH_SIZE, + testing_interval: Self::DEFAULT_TESTING_INTERVAL, + retry_timeout: Self::DEFAULT_RETRY_TIMEOUT, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct MixnodeConfigV6 { + pub storage_paths: MixnodePathsV6, + + pub verloc: VerlocV6, + + #[serde(default)] + pub debug: DebugV6, +} + +impl DebugV6 { + const DEFAULT_NODE_STATS_LOGGING_DELAY: Duration = Duration::from_millis(60_000); + const DEFAULT_NODE_STATS_UPDATING_DELAY: Duration = Duration::from_millis(30_000); +} + +impl Default for DebugV6 { + fn default() -> Self { + DebugV6 { + node_stats_logging_delay: Self::DEFAULT_NODE_STATS_LOGGING_DELAY, + node_stats_updating_delay: Self::DEFAULT_NODE_STATS_UPDATING_DELAY, + } + } +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct EntryGatewayPathsV6 { + /// Path to sqlite database containing all persistent data: messages for offline clients, + /// derived shared keys and available client bandwidths. + pub clients_storage: PathBuf, + + pub stats_storage: PathBuf, + + /// Path to file containing cosmos account mnemonic used for zk-nym redemption. + pub cosmos_mnemonic: PathBuf, + + pub authenticator: AuthenticatorPathsV6, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(default)] +pub struct ZkNymTicketHandlerDebugV6 { + /// Specifies the multiplier for revoking a malformed/double-spent ticket + /// (if it has to go all the way to the nym-api for verification) + /// e.g. if one ticket grants 100Mb and `revocation_bandwidth_penalty` is set to 1.5, + /// the client will lose 150Mb + pub revocation_bandwidth_penalty: f32, + + /// Specifies the interval for attempting to resolve any failed, pending operations, + /// such as ticket verification or redemption. + #[serde(with = "humantime_serde")] + pub pending_poller: Duration, + + pub minimum_api_quorum: f32, + + /// Specifies the minimum number of tickets this gateway will attempt to redeem. + pub minimum_redemption_tickets: usize, + + /// Specifies the maximum time between two subsequent tickets redemptions. + /// That's required as nym-apis will purge all ticket information for tickets older than maximum validity. + #[serde(with = "humantime_serde")] + pub maximum_time_between_redemption: Duration, +} + +impl ZkNymTicketHandlerDebugV6 { + pub const DEFAULT_REVOCATION_BANDWIDTH_PENALTY: f32 = 10.0; + pub const DEFAULT_PENDING_POLLER: Duration = Duration::from_secs(300); + pub const DEFAULT_MINIMUM_API_QUORUM: f32 = 0.8; + pub const DEFAULT_MINIMUM_REDEMPTION_TICKETS: usize = 100; + + // use min(4/5 of max validity, validity - 1), but making sure it's no greater than 1 day + // ASSUMPTION: our validity period is AT LEAST 2 days + // + // this could have been a constant, but it's more readable as a function + pub const fn default_maximum_time_between_redemption() -> Duration { + let desired_secs = TICKETBOOK_VALIDITY_DAYS * (86400 * 4) / 5; + let desired_secs_alt = (TICKETBOOK_VALIDITY_DAYS - 1) * 86400; + + // can't use `min` in const context + let target_secs = if desired_secs < desired_secs_alt { + desired_secs + } else { + desired_secs_alt + }; + + assert!( + target_secs > 86400, + "the maximum time between redemption can't be lower than 1 day!" + ); + Duration::from_secs(target_secs as u64) + } +} + +impl Default for ZkNymTicketHandlerDebugV6 { + fn default() -> Self { + ZkNymTicketHandlerDebugV6 { + revocation_bandwidth_penalty: Self::DEFAULT_REVOCATION_BANDWIDTH_PENALTY, + pending_poller: Self::DEFAULT_PENDING_POLLER, + minimum_api_quorum: Self::DEFAULT_MINIMUM_API_QUORUM, + minimum_redemption_tickets: Self::DEFAULT_MINIMUM_REDEMPTION_TICKETS, + maximum_time_between_redemption: Self::default_maximum_time_between_redemption(), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct EntryGatewayConfigDebugV6 { + /// Number of messages from offline client that can be pulled at once (i.e. with a single SQL query) from the storage. + pub message_retrieval_limit: i64, + pub zk_nym_tickets: ZkNymTicketHandlerDebugV6, +} + +impl EntryGatewayConfigDebugV6 { + const DEFAULT_MESSAGE_RETRIEVAL_LIMIT: i64 = 100; +} + +impl Default for EntryGatewayConfigDebugV6 { + fn default() -> Self { + EntryGatewayConfigDebugV6 { + message_retrieval_limit: Self::DEFAULT_MESSAGE_RETRIEVAL_LIMIT, + zk_nym_tickets: Default::default(), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct EntryGatewayConfigV6 { + pub storage_paths: EntryGatewayPathsV6, + + /// Indicates whether this gateway is accepting only coconut credentials for accessing the mixnet + /// or if it also accepts non-paying clients + pub enforce_zk_nyms: bool, + + /// Socket address this node will use for binding its client websocket API. + /// default: `0.0.0.0:9000` + pub bind_address: SocketAddr, + + /// Custom announced port for listening for websocket client traffic. + /// If unspecified, the value from the `bind_address` will be used instead + /// default: None + #[serde(deserialize_with = "de_maybe_port")] + pub announce_ws_port: Option<u16>, + + /// If applicable, announced port for listening for secure websocket client traffic. + /// (default: None) + #[serde(deserialize_with = "de_maybe_port")] + pub announce_wss_port: Option<u16>, + + #[serde(default)] + pub debug: EntryGatewayConfigDebugV6, +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct NetworkRequesterPathsV6 { + /// Path to file containing network requester ed25519 identity private key. + pub private_ed25519_identity_key_file: PathBuf, + + /// Path to file containing network requester ed25519 identity public key. + pub public_ed25519_identity_key_file: PathBuf, + + /// Path to file containing network requester x25519 diffie hellman private key. + pub private_x25519_diffie_hellman_key_file: PathBuf, + + /// Path to file containing network requester x25519 diffie hellman public key. + pub public_x25519_diffie_hellman_key_file: PathBuf, + + /// Path to file containing key used for encrypting and decrypting the content of an + /// acknowledgement so that nobody besides the client knows which packet it refers to. + pub ack_key_file: PathBuf, + + /// Path to the persistent store for received reply surbs, unused encryption keys and used sender tags. + pub reply_surb_database: PathBuf, + + /// Normally this is a path to the file containing information about gateways used by this client, + /// i.e. details such as their public keys, owner addresses or the network information. + /// but in this case it just has the basic information of "we're using custom gateway". + /// Due to how clients are started up, this file has to exist. + pub gateway_registrations: PathBuf, + // it's possible we might have to add credential storage here for return tickets +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct IpPacketRouterPathsV6 { + /// Path to file containing ip packet router ed25519 identity private key. + pub private_ed25519_identity_key_file: PathBuf, + + /// Path to file containing ip packet router ed25519 identity public key. + pub public_ed25519_identity_key_file: PathBuf, + + /// Path to file containing ip packet router x25519 diffie hellman private key. + pub private_x25519_diffie_hellman_key_file: PathBuf, + + /// Path to file containing ip packet router x25519 diffie hellman public key. + pub public_x25519_diffie_hellman_key_file: PathBuf, + + /// Path to file containing key used for encrypting and decrypting the content of an + /// acknowledgement so that nobody besides the client knows which packet it refers to. + pub ack_key_file: PathBuf, + + /// Path to the persistent store for received reply surbs, unused encryption keys and used sender tags. + pub reply_surb_database: PathBuf, + + /// Normally this is a path to the file containing information about gateways used by this client, + /// i.e. details such as their public keys, owner addresses or the network information. + /// but in this case it just has the basic information of "we're using custom gateway". + /// Due to how clients are started up, this file has to exist. + pub gateway_registrations: PathBuf, + // it's possible we might have to add credential storage here for return tickets +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct AuthenticatorPathsV6 { + /// Path to file containing authenticator ed25519 identity private key. + pub private_ed25519_identity_key_file: PathBuf, + + /// Path to file containing authenticator ed25519 identity public key. + pub public_ed25519_identity_key_file: PathBuf, + + /// Path to file containing authenticator x25519 diffie hellman private key. + pub private_x25519_diffie_hellman_key_file: PathBuf, + + /// Path to file containing authenticator x25519 diffie hellman public key. + pub public_x25519_diffie_hellman_key_file: PathBuf, + + /// Path to file containing key used for encrypting and decrypting the content of an + /// acknowledgement so that nobody besides the client knows which packet it refers to. + pub ack_key_file: PathBuf, + + /// Path to the persistent store for received reply surbs, unused encryption keys and used sender tags. + pub reply_surb_database: PathBuf, + + /// Normally this is a path to the file containing information about gateways used by this client, + /// i.e. details such as their public keys, owner addresses or the network information. + /// but in this case it just has the basic information of "we're using custom gateway". + /// Due to how clients are started up, this file has to exist. + pub gateway_registrations: PathBuf, + // it's possible we might have to add credential storage here for return tickets +} + +impl AuthenticatorPathsV6 { + pub fn new<P: AsRef<Path>>(data_dir: P) -> Self { + let data_dir = data_dir.as_ref(); + AuthenticatorPathsV6 { + private_ed25519_identity_key_file: data_dir + .join(DEFAULT_ED25519_AUTH_PRIVATE_IDENTITY_KEY_FILENAME), + public_ed25519_identity_key_file: data_dir + .join(DEFAULT_ED25519_AUTH_PUBLIC_IDENTITY_KEY_FILENAME), + private_x25519_diffie_hellman_key_file: data_dir + .join(DEFAULT_X25519_AUTH_PRIVATE_DH_KEY_FILENAME), + public_x25519_diffie_hellman_key_file: data_dir + .join(DEFAULT_X25519_AUTH_PUBLIC_DH_KEY_FILENAME), + ack_key_file: data_dir.join(DEFAULT_AUTH_ACK_KEY_FILENAME), + reply_surb_database: data_dir.join(DEFAULT_AUTH_REPLY_SURB_DB_FILENAME), + gateway_registrations: data_dir.join(DEFAULT_AUTH_GATEWAYS_DB_FILENAME), + } + } + + pub fn to_common_client_paths(&self) -> CommonClientPaths { + CommonClientPaths { + keys: ClientKeysPaths { + private_identity_key_file: self.private_ed25519_identity_key_file.clone(), + public_identity_key_file: self.public_ed25519_identity_key_file.clone(), + private_encryption_key_file: self.private_x25519_diffie_hellman_key_file.clone(), + public_encryption_key_file: self.public_x25519_diffie_hellman_key_file.clone(), + ack_key_file: self.ack_key_file.clone(), + }, + gateway_registrations: self.gateway_registrations.clone(), + + // not needed for embedded providers + credentials_database: Default::default(), + reply_surb_database: self.reply_surb_database.clone(), + } + } + + pub fn ed25519_identity_storage_paths(&self) -> nym_pemstore::KeyPairPath { + nym_pemstore::KeyPairPath::new( + &self.private_ed25519_identity_key_file, + &self.public_ed25519_identity_key_file, + ) + } + + pub fn x25519_diffie_hellman_storage_paths(&self) -> nym_pemstore::KeyPairPath { + nym_pemstore::KeyPairPath::new( + &self.private_x25519_diffie_hellman_key_file, + &self.public_x25519_diffie_hellman_key_file, + ) + } +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct ExitGatewayPathsV6 { + pub clients_storage: PathBuf, + + pub stats_storage: PathBuf, + + pub network_requester: NetworkRequesterPathsV6, + + pub ip_packet_router: IpPacketRouterPathsV6, + + pub authenticator: AuthenticatorPathsV6, +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)] +pub struct AuthenticatorV6 { + #[serde(default)] + pub debug: AuthenticatorDebugV6, +} + +#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Serialize)] +#[serde(default)] +pub struct AuthenticatorDebugV6 { + /// Specifies whether authenticator service is enabled in this process. + /// This is only here for debugging purposes as exit gateway should always run + /// the authenticator. + pub enabled: bool, + + /// Disable Poisson sending rate. + /// This is equivalent to setting client_debug.traffic.disable_main_poisson_packet_distribution = true + /// (or is it (?)) + pub disable_poisson_rate: bool, + + /// Shared detailed client configuration options + #[serde(flatten)] + pub client_debug: ClientDebugConfig, +} + +impl Default for AuthenticatorDebugV6 { + fn default() -> Self { + AuthenticatorDebugV6 { + enabled: true, + disable_poisson_rate: true, + client_debug: Default::default(), + } + } +} + +#[allow(clippy::derivable_impls)] +impl Default for AuthenticatorV6 { + fn default() -> Self { + AuthenticatorV6 { + debug: Default::default(), + } + } +} + +#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Serialize)] +#[serde(default)] +pub struct IpPacketRouterDebugV6 { + /// Specifies whether ip packet routing service is enabled in this process. + /// This is only here for debugging purposes as exit gateway should always run **both** + /// network requester and an ip packet router. + pub enabled: bool, + + /// Disable Poisson sending rate. + /// This is equivalent to setting client_debug.traffic.disable_main_poisson_packet_distribution = true + /// (or is it (?)) + pub disable_poisson_rate: bool, + + /// Shared detailed client configuration options + #[serde(flatten)] + pub client_debug: ClientDebugConfig, +} + +impl Default for IpPacketRouterDebugV6 { + fn default() -> Self { + IpPacketRouterDebugV6 { + enabled: true, + disable_poisson_rate: true, + client_debug: Default::default(), + } + } +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)] +pub struct IpPacketRouterV6 { + #[serde(default)] + pub debug: IpPacketRouterDebugV6, +} + +#[allow(clippy::derivable_impls)] +impl Default for IpPacketRouterV6 { + fn default() -> Self { + IpPacketRouterV6 { + debug: Default::default(), + } + } +} + +#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Serialize)] +pub struct NetworkRequesterDebugV6 { + /// Specifies whether network requester service is enabled in this process. + /// This is only here for debugging purposes as exit gateway should always run **both** + /// network requester and an ip packet router. + pub enabled: bool, + + /// Disable Poisson sending rate. + /// This is equivalent to setting client_debug.traffic.disable_main_poisson_packet_distribution = true + /// (or is it (?)) + pub disable_poisson_rate: bool, + + /// Shared detailed client configuration options + #[serde(flatten)] + pub client_debug: ClientDebugConfig, +} + +impl Default for NetworkRequesterDebugV6 { + fn default() -> Self { + NetworkRequesterDebugV6 { + enabled: true, + disable_poisson_rate: true, + client_debug: Default::default(), + } + } +} + +#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Serialize)] +pub struct NetworkRequesterV6 { + #[serde(default)] + pub debug: NetworkRequesterDebugV6, +} + +#[allow(clippy::derivable_impls)] +impl Default for NetworkRequesterV6 { + fn default() -> Self { + NetworkRequesterV6 { + debug: Default::default(), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct ExitGatewayDebugV6 { + /// Number of messages from offline client that can be pulled at once (i.e. with a single SQL query) from the storage. + pub message_retrieval_limit: i64, +} + +impl ExitGatewayDebugV6 { + const DEFAULT_MESSAGE_RETRIEVAL_LIMIT: i64 = 100; +} + +impl Default for ExitGatewayDebugV6 { + fn default() -> Self { + ExitGatewayDebugV6 { + message_retrieval_limit: Self::DEFAULT_MESSAGE_RETRIEVAL_LIMIT, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct ExitGatewayConfigV6 { + pub storage_paths: ExitGatewayPathsV6, + + /// specifies whether this exit node should run in 'open-proxy' mode + /// and thus would attempt to resolve **ANY** request it receives. + pub open_proxy: bool, + + /// Specifies the url for an upstream source of the exit policy used by this node. + pub upstream_exit_policy_url: Url, + + pub network_requester: NetworkRequesterV6, + + pub ip_packet_router: IpPacketRouterV6, + + #[serde(default)] + pub debug: ExitGatewayDebugV6, +} + +#[derive(Debug, Default, Copy, Clone, Deserialize, PartialEq, Eq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct LoggingSettingsV6 { + // well, we need to implement something here at some point... +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct ConfigV6 { + // additional metadata holding on-disk location of this config file + #[serde(skip)] + pub(crate) save_path: Option<PathBuf>, + + /// Human-readable ID of this particular node. + pub id: String, + + /// Current mode of this nym-node. + /// Expect this field to be changed in the future to allow running the node in multiple modes (i.e. mixnode + gateway) + pub mode: NodeModeV6, + + pub host: HostV6, + + pub mixnet: MixnetV6, + + /// Storage paths to persistent nym-node data, such as its long term keys. + pub storage_paths: NymNodePathsV6, + + #[serde(default)] + pub http: HttpV6, + + pub wireguard: WireguardV6, + + pub mixnode: MixnodeConfigV6, + + pub entry_gateway: EntryGatewayConfigV6, + + pub exit_gateway: ExitGatewayConfigV6, + + pub authenticator: AuthenticatorV6, + + #[serde(default)] + pub logging: LoggingSettingsV6, +} + +impl ConfigV6 { + // simple wrapper that reads config file and assigns path location + fn read_from_path<P: AsRef<Path>>(path: P) -> Result<Self, NymNodeError> { + let path = path.as_ref(); + let mut loaded: ConfigV6 = + read_config_from_toml_file(path).map_err(|source| NymNodeError::ConfigLoadFailure { + path: path.to_path_buf(), + source, + })?; + loaded.save_path = Some(path.to_path_buf()); + debug!("loaded config file from {}", path.display()); + Ok(loaded) + } +} + +#[instrument(skip_all)] +pub async fn try_upgrade_config_v6<P: AsRef<Path>>( + path: P, + prev_config: Option<ConfigV6>, +) -> Result<Config, NymNodeError> { + debug!("attempting to load v6 config..."); + + let old_cfg = if let Some(prev_config) = prev_config { + prev_config + } else { + ConfigV6::read_from_path(&path)? + }; + + let cfg = Config { + save_path: old_cfg.save_path, + id: old_cfg.id, + modes: old_cfg.mode.into(), + host: Host { + public_ips: old_cfg.host.public_ips, + hostname: old_cfg.host.hostname, + location: old_cfg.host.location, + }, + mixnet: Mixnet { + bind_address: old_cfg.mixnet.bind_address, + announce_port: old_cfg.mixnet.announce_port, + nym_api_urls: old_cfg.mixnet.nym_api_urls, + nyxd_urls: old_cfg.mixnet.nyxd_urls, + debug: MixnetDebug { + maximum_forward_packet_delay: MixnetDebug::DEFAULT_MAXIMUM_FORWARD_PACKET_DELAY, + packet_forwarding_initial_backoff: old_cfg + .mixnet + .debug + .packet_forwarding_initial_backoff, + packet_forwarding_maximum_backoff: old_cfg + .mixnet + .debug + .packet_forwarding_maximum_backoff, + initial_connection_timeout: old_cfg.mixnet.debug.initial_connection_timeout, + maximum_connection_buffer_size: old_cfg.mixnet.debug.maximum_connection_buffer_size, + unsafe_disable_noise: old_cfg.mixnet.debug.unsafe_disable_noise, + }, + }, + storage_paths: NymNodePaths { + keys: KeysPaths { + private_ed25519_identity_key_file: old_cfg + .storage_paths + .keys + .private_ed25519_identity_key_file, + public_ed25519_identity_key_file: old_cfg + .storage_paths + .keys + .public_ed25519_identity_key_file, + private_x25519_sphinx_key_file: old_cfg + .storage_paths + .keys + .private_x25519_sphinx_key_file, + public_x25519_sphinx_key_file: old_cfg + .storage_paths + .keys + .public_x25519_sphinx_key_file, + private_x25519_noise_key_file: old_cfg + .storage_paths + .keys + .private_x25519_noise_key_file, + public_x25519_noise_key_file: old_cfg + .storage_paths + .keys + .public_x25519_noise_key_file, + }, + description: old_cfg.storage_paths.description, + }, + http: Http { + bind_address: old_cfg.http.bind_address, + landing_page_assets_path: old_cfg.http.landing_page_assets_path, + access_token: old_cfg.http.access_token, + expose_system_info: old_cfg.http.expose_system_info, + expose_system_hardware: old_cfg.http.expose_system_hardware, + expose_crypto_hardware: old_cfg.http.expose_crypto_hardware, + }, + verloc: Verloc { + bind_address: old_cfg.mixnode.verloc.bind_address, + announce_port: old_cfg.mixnode.verloc.announce_port, + debug: VerlocDebug { + packets_per_node: old_cfg.mixnode.verloc.debug.packets_per_node, + connection_timeout: old_cfg.mixnode.verloc.debug.connection_timeout, + packet_timeout: old_cfg.mixnode.verloc.debug.packet_timeout, + delay_between_packets: old_cfg.mixnode.verloc.debug.delay_between_packets, + tested_nodes_batch_size: old_cfg.mixnode.verloc.debug.tested_nodes_batch_size, + testing_interval: old_cfg.mixnode.verloc.debug.testing_interval, + retry_timeout: old_cfg.mixnode.verloc.debug.retry_timeout, + }, + }, + wireguard: Wireguard { + enabled: old_cfg.wireguard.enabled, + bind_address: old_cfg.wireguard.bind_address, + private_ipv4: old_cfg.wireguard.private_ipv4, + private_ipv6: old_cfg.wireguard.private_ipv6, + announced_port: old_cfg.wireguard.announced_port, + private_network_prefix_v4: old_cfg.wireguard.private_network_prefix_v4, + private_network_prefix_v6: old_cfg.wireguard.private_network_prefix_v6, + storage_paths: WireguardPaths { + private_diffie_hellman_key_file: old_cfg + .wireguard + .storage_paths + .private_diffie_hellman_key_file, + public_diffie_hellman_key_file: old_cfg + .wireguard + .storage_paths + .public_diffie_hellman_key_file, + }, + }, + gateway_tasks: GatewayTasksConfig { + storage_paths: GatewayTasksPaths { + clients_storage: old_cfg.entry_gateway.storage_paths.clients_storage, + stats_storage: old_cfg.entry_gateway.storage_paths.stats_storage, + cosmos_mnemonic: old_cfg.entry_gateway.storage_paths.cosmos_mnemonic, + }, + enforce_zk_nyms: old_cfg.entry_gateway.enforce_zk_nyms, + bind_address: old_cfg.entry_gateway.bind_address, + announce_ws_port: old_cfg.entry_gateway.announce_ws_port, + announce_wss_port: old_cfg.entry_gateway.announce_wss_port, + debug: gateway_tasks::Debug { + message_retrieval_limit: old_cfg.entry_gateway.debug.message_retrieval_limit, + zk_nym_tickets: ZkNymTicketHandlerDebug { + revocation_bandwidth_penalty: old_cfg + .entry_gateway + .debug + .zk_nym_tickets + .revocation_bandwidth_penalty, + pending_poller: old_cfg.entry_gateway.debug.zk_nym_tickets.pending_poller, + minimum_api_quorum: old_cfg + .entry_gateway + .debug + .zk_nym_tickets + .minimum_api_quorum, + minimum_redemption_tickets: old_cfg + .entry_gateway + .debug + .zk_nym_tickets + .minimum_redemption_tickets, + maximum_time_between_redemption: old_cfg + .entry_gateway + .debug + .zk_nym_tickets + .maximum_time_between_redemption, + }, + }, + }, + service_providers: ServiceProvidersConfig { + storage_paths: ServiceProvidersPaths { + clients_storage: old_cfg.exit_gateway.storage_paths.clients_storage, + stats_storage: old_cfg.exit_gateway.storage_paths.stats_storage, + network_requester: NetworkRequesterPaths { + private_ed25519_identity_key_file: old_cfg + .exit_gateway + .storage_paths + .network_requester + .private_ed25519_identity_key_file, + public_ed25519_identity_key_file: old_cfg + .exit_gateway + .storage_paths + .network_requester + .public_ed25519_identity_key_file, + private_x25519_diffie_hellman_key_file: old_cfg + .exit_gateway + .storage_paths + .network_requester + .private_x25519_diffie_hellman_key_file, + public_x25519_diffie_hellman_key_file: old_cfg + .exit_gateway + .storage_paths + .network_requester + .public_x25519_diffie_hellman_key_file, + ack_key_file: old_cfg + .exit_gateway + .storage_paths + .network_requester + .ack_key_file, + reply_surb_database: old_cfg + .exit_gateway + .storage_paths + .network_requester + .reply_surb_database, + gateway_registrations: old_cfg + .exit_gateway + .storage_paths + .network_requester + .gateway_registrations, + }, + ip_packet_router: IpPacketRouterPaths { + private_ed25519_identity_key_file: old_cfg + .exit_gateway + .storage_paths + .ip_packet_router + .private_ed25519_identity_key_file, + public_ed25519_identity_key_file: old_cfg + .exit_gateway + .storage_paths + .ip_packet_router + .public_ed25519_identity_key_file, + private_x25519_diffie_hellman_key_file: old_cfg + .exit_gateway + .storage_paths + .ip_packet_router + .private_x25519_diffie_hellman_key_file, + public_x25519_diffie_hellman_key_file: old_cfg + .exit_gateway + .storage_paths + .ip_packet_router + .public_x25519_diffie_hellman_key_file, + ack_key_file: old_cfg + .exit_gateway + .storage_paths + .ip_packet_router + .ack_key_file, + reply_surb_database: old_cfg + .exit_gateway + .storage_paths + .ip_packet_router + .reply_surb_database, + gateway_registrations: old_cfg + .exit_gateway + .storage_paths + .ip_packet_router + .gateway_registrations, + }, + authenticator: AuthenticatorPaths { + private_ed25519_identity_key_file: old_cfg + .exit_gateway + .storage_paths + .authenticator + .private_ed25519_identity_key_file, + public_ed25519_identity_key_file: old_cfg + .exit_gateway + .storage_paths + .authenticator + .public_ed25519_identity_key_file, + private_x25519_diffie_hellman_key_file: old_cfg + .exit_gateway + .storage_paths + .authenticator + .private_x25519_diffie_hellman_key_file, + public_x25519_diffie_hellman_key_file: old_cfg + .exit_gateway + .storage_paths + .authenticator + .public_x25519_diffie_hellman_key_file, + ack_key_file: old_cfg + .exit_gateway + .storage_paths + .authenticator + .ack_key_file, + reply_surb_database: old_cfg + .exit_gateway + .storage_paths + .authenticator + .reply_surb_database, + gateway_registrations: old_cfg + .exit_gateway + .storage_paths + .authenticator + .gateway_registrations, + }, + }, + open_proxy: old_cfg.exit_gateway.open_proxy, + upstream_exit_policy_url: old_cfg.exit_gateway.upstream_exit_policy_url, + network_requester: NetworkRequester { + debug: NetworkRequesterDebug { + enabled: old_cfg.exit_gateway.network_requester.debug.enabled, + disable_poisson_rate: old_cfg + .exit_gateway + .network_requester + .debug + .disable_poisson_rate, + client_debug: old_cfg.exit_gateway.network_requester.debug.client_debug, + }, + }, + ip_packet_router: IpPacketRouter { + debug: IpPacketRouterDebug { + enabled: old_cfg.exit_gateway.ip_packet_router.debug.enabled, + disable_poisson_rate: old_cfg + .exit_gateway + .ip_packet_router + .debug + .disable_poisson_rate, + client_debug: old_cfg.exit_gateway.ip_packet_router.debug.client_debug, + }, + }, + authenticator: Authenticator { + debug: AuthenticatorDebug { + enabled: old_cfg.authenticator.debug.enabled, + disable_poisson_rate: old_cfg.authenticator.debug.disable_poisson_rate, + client_debug: old_cfg.authenticator.debug.client_debug, + }, + }, + debug: service_providers::Debug { + message_retrieval_limit: old_cfg.exit_gateway.debug.message_retrieval_limit, + }, + }, + metrics: Default::default(), + logging: LoggingSettings {}, + debug: Default::default(), + }; + Ok(cfg) +} diff --git a/nym-node/src/config/persistence.rs b/nym-node/src/config/persistence.rs index e62ced0c55b..b3db6c827bd 100644 --- a/nym-node/src/config/persistence.rs +++ b/nym-node/src/config/persistence.rs @@ -19,8 +19,6 @@ pub const DEFAULT_X25519_PRIVATE_NOISE_KEY_FILENAME: &str = "x25519_noise"; pub const DEFAULT_X25519_PUBLIC_NOISE_KEY_FILENAME: &str = "x25519_noise.pub"; pub const DEFAULT_NYMNODE_DESCRIPTION_FILENAME: &str = "description.toml"; -pub const DEFAULT_DESCRIPTION_FILENAME: &str = "description.toml"; - // Mixnode: // Entry Gateway: @@ -72,7 +70,7 @@ impl NymNodePaths { NymNodePaths { keys: KeysPaths::new(data_dir), - description: data_dir.join(DEFAULT_DESCRIPTION_FILENAME), + description: data_dir.join(DEFAULT_NYMNODE_DESCRIPTION_FILENAME), } } } @@ -140,11 +138,7 @@ impl KeysPaths { #[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)] #[serde(deny_unknown_fields)] -pub struct MixnodePaths {} - -#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)] -#[serde(deny_unknown_fields)] -pub struct EntryGatewayPaths { +pub struct GatewayTasksPaths { /// Path to sqlite database containing all persistent data: messages for offline clients, /// derived shared keys, available client bandwidths and wireguard peers. pub clients_storage: PathBuf, @@ -154,17 +148,14 @@ pub struct EntryGatewayPaths { /// Path to file containing cosmos account mnemonic used for zk-nym redemption. pub cosmos_mnemonic: PathBuf, - - pub authenticator: AuthenticatorPaths, } -impl EntryGatewayPaths { +impl GatewayTasksPaths { pub fn new<P: AsRef<Path>>(data_dir: P) -> Self { - EntryGatewayPaths { + GatewayTasksPaths { clients_storage: data_dir.as_ref().join(DEFAULT_CLIENTS_STORAGE_FILENAME), stats_storage: data_dir.as_ref().join(DEFAULT_STATS_STORAGE_FILENAME), cosmos_mnemonic: data_dir.as_ref().join(DEFAULT_MNEMONIC_FILENAME), - authenticator: AuthenticatorPaths::new(data_dir), } } @@ -208,7 +199,7 @@ impl EntryGatewayPaths { #[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)] #[serde(deny_unknown_fields)] -pub struct ExitGatewayPaths { +pub struct ServiceProvidersPaths { /// Path to sqlite database containing all persistent data: messages for offline clients, /// derived shared keys, available client bandwidths and wireguard peers. pub clients_storage: PathBuf, @@ -463,10 +454,10 @@ impl AuthenticatorPaths { } } -impl ExitGatewayPaths { +impl ServiceProvidersPaths { pub fn new<P: AsRef<Path>>(data_dir: P) -> Self { let data_dir = data_dir.as_ref(); - ExitGatewayPaths { + ServiceProvidersPaths { clients_storage: data_dir.join(DEFAULT_CLIENTS_STORAGE_FILENAME), stats_storage: data_dir.join(DEFAULT_STATS_STORAGE_FILENAME), network_requester: NetworkRequesterPaths::new(data_dir), diff --git a/nym-node/src/config/service_providers.rs b/nym-node/src/config/service_providers.rs new file mode 100644 index 00000000000..0c66990926e --- /dev/null +++ b/nym-node/src/config/service_providers.rs @@ -0,0 +1,155 @@ +// Copyright 2024 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: GPL-3.0-only + +use crate::config::authenticator::Authenticator; +use crate::config::persistence::ServiceProvidersPaths; +use nym_client_core_config_types::DebugConfig as ClientDebugConfig; +use nym_config::defaults::mainnet; +use serde::{Deserialize, Serialize}; +use std::path::Path; +use url::Url; + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct ServiceProvidersConfig { + pub storage_paths: ServiceProvidersPaths, + + /// specifies whether this exit node should run in 'open-proxy' mode + /// and thus would attempt to resolve **ANY** request it receives. + pub open_proxy: bool, + + /// Specifies the url for an upstream source of the exit policy used by this node. + pub upstream_exit_policy_url: Url, + + pub network_requester: NetworkRequester, + + pub ip_packet_router: IpPacketRouter, + + pub authenticator: Authenticator, + + #[serde(default)] + pub debug: Debug, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct Debug { + /// Number of messages from offline client that can be pulled at once (i.e. with a single SQL query) from the storage. + pub message_retrieval_limit: i64, +} + +impl Debug { + const DEFAULT_MESSAGE_RETRIEVAL_LIMIT: i64 = 100; +} + +impl Default for Debug { + fn default() -> Self { + Debug { + message_retrieval_limit: Self::DEFAULT_MESSAGE_RETRIEVAL_LIMIT, + } + } +} + +impl ServiceProvidersConfig { + pub fn new_default<P: AsRef<Path>>(data_dir: P) -> Self { + #[allow(clippy::expect_used)] + // SAFETY: + // we expect our default values to be well-formed + ServiceProvidersConfig { + storage_paths: ServiceProvidersPaths::new(data_dir), + open_proxy: false, + upstream_exit_policy_url: mainnet::EXIT_POLICY_URL + .parse() + .expect("invalid default exit policy URL"), + network_requester: Default::default(), + ip_packet_router: Default::default(), + authenticator: Default::default(), + debug: Default::default(), + } + } +} + +#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Serialize)] +pub struct NetworkRequester { + #[serde(default)] + pub debug: NetworkRequesterDebug, +} + +#[allow(clippy::derivable_impls)] +impl Default for NetworkRequester { + fn default() -> Self { + NetworkRequester { + debug: Default::default(), + } + } +} + +#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Serialize)] +pub struct NetworkRequesterDebug { + /// Specifies whether network requester service is enabled in this process. + /// This is only here for debugging purposes as exit gateway should always run **both** + /// network requester and an ip packet router. + pub enabled: bool, + + /// Disable Poisson sending rate. + /// This is equivalent to setting client_debug.traffic.disable_main_poisson_packet_distribution = true + /// (or is it (?)) + pub disable_poisson_rate: bool, + + /// Shared detailed client configuration options + #[serde(flatten)] + pub client_debug: ClientDebugConfig, +} + +impl Default for NetworkRequesterDebug { + fn default() -> Self { + NetworkRequesterDebug { + enabled: true, + disable_poisson_rate: true, + client_debug: Default::default(), + } + } +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)] +pub struct IpPacketRouter { + #[serde(default)] + pub debug: IpPacketRouterDebug, +} + +#[allow(clippy::derivable_impls)] +impl Default for IpPacketRouter { + fn default() -> Self { + IpPacketRouter { + debug: Default::default(), + } + } +} + +#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Serialize)] +#[serde(default)] +pub struct IpPacketRouterDebug { + /// Specifies whether ip packet routing service is enabled in this process. + /// This is only here for debugging purposes as exit gateway should always run **both** + /// network requester and an ip packet router. + pub enabled: bool, + + /// Disable Poisson sending rate. + /// This is equivalent to setting client_debug.traffic.disable_main_poisson_packet_distribution = true + /// (or is it (?)) + pub disable_poisson_rate: bool, + + /// Shared detailed client configuration options + #[serde(flatten)] + pub client_debug: ClientDebugConfig, +} + +impl Default for IpPacketRouterDebug { + fn default() -> Self { + IpPacketRouterDebug { + enabled: true, + disable_poisson_rate: true, + client_debug: Default::default(), + } + } +} diff --git a/nym-node/src/config/template.rs b/nym-node/src/config/template.rs index 58edfae032f..459b79bca1a 100644 --- a/nym-node/src/config/template.rs +++ b/nym-node/src/config/template.rs @@ -18,9 +18,17 @@ pub(crate) const CONFIG_TEMPLATE: &str = r#" # Human-readable ID of this particular node. id = '{{ id }}' -# Current mode of this nym-node. -# Expect this field to be changed in the future to allow running the node in multiple modes (i.e. mixnode + gateway) -mode = '{{ mode }}' +# Current modes of this nym-node. + +[modes] +# Specifies whether this node can operate in a mixnode mode. +mixnode = {{ modes.mixnode }} + +# Specifies whether this node can operate in an entry mode. +entry = {{ modes.entry }} + +# Specifies whether this node can operate in an exit mode. +exit = {{ modes.exit }} [host] # Ip address(es) of this host, such as 1.1.1.1 that external clients will use for connections. @@ -149,195 +157,158 @@ private_diffie_hellman_key_file = '{{ wireguard.storage_paths.private_diffie_hel public_diffie_hellman_key_file = '{{ wireguard.storage_paths.public_diffie_hellman_key_file }}' -##### mixnode mode nym-node config options ##### - -[mixnode] +##### verloc config options ##### -[mixnode.verloc] +[verloc] # Socket address this node will use for binding its verloc API. # default: `0.0.0.0:1790` -bind_address = '{{ mixnode.verloc.bind_address }}' +bind_address = '{{ verloc.bind_address }}' # If applicable, custom port announced in the self-described API that other clients and nodes # will use. # Useful when the node is behind a proxy. # (default: 0 - disabled) -announce_port ={{#if mixnode.verloc.announce_port }} {{ mixnode.verloc.announce_port }} {{else}} 0 {{/if}} +announce_port ={{#if verloc.announce_port }} {{ verloc.announce_port }} {{else}} 0 {{/if}} -[mixnode.storage_paths] -# currently empty -##### entry-gateway mode nym-node config options ##### +##### gateway tasks config options ##### -[entry_gateway] +[gateway_tasks] # Indicates whether this gateway is accepting only coconut credentials for accessing the mixnet # or if it also accepts non-paying clients -enforce_zk_nyms = {{ entry_gateway.enforce_zk_nyms }} +enforce_zk_nyms = {{ gateway_tasks.enforce_zk_nyms }} # Socket address this node will use for binding its client websocket API. # default: `0.0.0.0:9000` -bind_address = '{{ entry_gateway.bind_address }}' +bind_address = '{{ gateway_tasks.bind_address }}' # Custom announced port for listening for websocket client traffic. # If unspecified, the value from the `bind_address` will be used instead # (default: 0 - unspecified) -announce_ws_port = {{#if entry_gateway.announce_ws_port }} {{ entry_gateway.announce_ws_port }} {{else}} 0 {{/if}} +announce_ws_port = {{#if gateway_tasks.announce_ws_port }} {{ gateway_tasks.announce_ws_port }} {{else}} 0 {{/if}} # If applicable, announced port for listening for secure websocket client traffic. # (default: 0 - disabled) -announce_wss_port = {{#if entry_gateway.announce_wss_port }} {{ entry_gateway.announce_wss_port }} {{else}} 0 {{/if}} +announce_wss_port = {{#if gateway_tasks.announce_wss_port }} {{ gateway_tasks.announce_wss_port }} {{else}} 0 {{/if}} -[entry_gateway.storage_paths] +[gateway_tasks.storage_paths] # Path to sqlite database containing all persistent data: messages for offline clients, # derived shared keys, available client bandwidths and wireguard peers. -clients_storage = '{{ entry_gateway.storage_paths.clients_storage }}' +clients_storage = '{{ gateway_tasks.storage_paths.clients_storage }}' # Path to sqlite database containing all persistent stats data. -stats_storage = '{{ entry_gateway.storage_paths.stats_storage }}' +stats_storage = '{{ gateway_tasks.storage_paths.stats_storage }}' # Path to file containing cosmos account mnemonic used for zk-nym redemption. -cosmos_mnemonic = '{{ entry_gateway.storage_paths.cosmos_mnemonic }}' - -[entry_gateway.storage_paths.authenticator] -# Path to file containing authenticator ed25519 identity private key. -private_ed25519_identity_key_file = '{{ entry_gateway.storage_paths.authenticator.private_ed25519_identity_key_file }}' - -# Path to file containing authenticator ed25519 identity public key. -public_ed25519_identity_key_file = '{{ entry_gateway.storage_paths.authenticator.public_ed25519_identity_key_file }}' - -# Path to file containing authenticator x25519 diffie hellman private key. -private_x25519_diffie_hellman_key_file = '{{ entry_gateway.storage_paths.authenticator.private_x25519_diffie_hellman_key_file }}' - -# Path to file containing authenticator x25519 diffie hellman public key. -public_x25519_diffie_hellman_key_file = '{{ entry_gateway.storage_paths.authenticator.public_x25519_diffie_hellman_key_file }}' - -# Path to file containing key used for encrypting and decrypting the content of an -# acknowledgement so that nobody besides the client knows which packet it refers to. -ack_key_file = '{{ entry_gateway.storage_paths.authenticator.ack_key_file }}' +cosmos_mnemonic = '{{ gateway_tasks.storage_paths.cosmos_mnemonic }}' -# Path to the persistent store for received reply surbs, unused encryption keys and used sender tags. -reply_surb_database = '{{ entry_gateway.storage_paths.authenticator.reply_surb_database }}' - -# Normally this is a path to the file containing information about gateways used by this client, -# i.e. details such as their public keys, owner addresses or the network information. -# but in this case it just has the basic information of "we're using custom gateway". -# Due to how clients are started up, this file has to exist. -gateway_registrations = '{{ entry_gateway.storage_paths.authenticator.gateway_registrations }}' +##### service providers nym-node config options ##### -##### exit-gateway mode nym-node config options ##### - -[exit_gateway] +[service_providers] # specifies whether this exit node should run in 'open-proxy' mode # and thus would attempt to resolve **ANY** request it receives. -open_proxy = {{ exit_gateway.open_proxy }} +open_proxy = {{ service_providers.open_proxy }} # Specifies the custom url for an upstream source of the exit policy used by this node. -upstream_exit_policy_url = '{{ exit_gateway.upstream_exit_policy_url }}' +upstream_exit_policy_url = '{{ service_providers.upstream_exit_policy_url }}' -[exit_gateway.network_requester] +[service_providers.network_requester] # currently empty (there are some debug options one might want to configure) -[exit_gateway.ip_packet_router] +[service_providers.ip_packet_router] # currently empty (there are some debug options one might want to configure) -[exit_gateway.storage_paths] +[service_providers.authenticator] +# currently empty (there are some debug options one might want to configure) + +[service_providers.storage_paths] # Path to sqlite database containing all persistent data: messages for offline clients, # derived shared keys, available client bandwidths and wireguard peers. -clients_storage = '{{ exit_gateway.storage_paths.clients_storage }}' +clients_storage = '{{ service_providers.storage_paths.clients_storage }}' # Path to sqlite database containing all persistent stats data. -stats_storage = '{{ exit_gateway.storage_paths.stats_storage }}' - +stats_storage = '{{ service_providers.storage_paths.stats_storage }}' -[exit_gateway.storage_paths.network_requester] +[service_providers.storage_paths.network_requester] # Path to file containing network requester ed25519 identity private key. -private_ed25519_identity_key_file = '{{ exit_gateway.storage_paths.network_requester.private_ed25519_identity_key_file }}' +private_ed25519_identity_key_file = '{{ service_providers.storage_paths.network_requester.private_ed25519_identity_key_file }}' # Path to file containing network requester ed25519 identity public key. -public_ed25519_identity_key_file = '{{ exit_gateway.storage_paths.network_requester.public_ed25519_identity_key_file }}' +public_ed25519_identity_key_file = '{{ service_providers.storage_paths.network_requester.public_ed25519_identity_key_file }}' # Path to file containing network requester x25519 diffie hellman private key. -private_x25519_diffie_hellman_key_file = '{{ exit_gateway.storage_paths.network_requester.private_x25519_diffie_hellman_key_file }}' +private_x25519_diffie_hellman_key_file = '{{ service_providers.storage_paths.network_requester.private_x25519_diffie_hellman_key_file }}' # Path to file containing network requester x25519 diffie hellman public key. -public_x25519_diffie_hellman_key_file = '{{ exit_gateway.storage_paths.network_requester.public_x25519_diffie_hellman_key_file }}' +public_x25519_diffie_hellman_key_file = '{{ service_providers.storage_paths.network_requester.public_x25519_diffie_hellman_key_file }}' # Path to file containing key used for encrypting and decrypting the content of an # acknowledgement so that nobody besides the client knows which packet it refers to. -ack_key_file = '{{ exit_gateway.storage_paths.network_requester.ack_key_file }}' +ack_key_file = '{{ service_providers.storage_paths.network_requester.ack_key_file }}' # Path to the persistent store for received reply surbs, unused encryption keys and used sender tags. -reply_surb_database = '{{ exit_gateway.storage_paths.network_requester.reply_surb_database }}' +reply_surb_database = '{{ service_providers.storage_paths.network_requester.reply_surb_database }}' # Normally this is a path to the file containing information about gateways used by this client, # i.e. details such as their public keys, owner addresses or the network information. # but in this case it just has the basic information of "we're using custom gateway". # Due to how clients are started up, this file has to exist. -gateway_registrations = '{{ exit_gateway.storage_paths.network_requester.gateway_registrations }}' +gateway_registrations = '{{ service_providers.storage_paths.network_requester.gateway_registrations }}' -[exit_gateway.storage_paths.ip_packet_router] +[service_providers.storage_paths.ip_packet_router] # Path to file containing ip packet router ed25519 identity private key. -private_ed25519_identity_key_file = '{{ exit_gateway.storage_paths.ip_packet_router.private_ed25519_identity_key_file }}' +private_ed25519_identity_key_file = '{{ service_providers.storage_paths.ip_packet_router.private_ed25519_identity_key_file }}' # Path to file containing ip packet router ed25519 identity public key. -public_ed25519_identity_key_file = '{{ exit_gateway.storage_paths.ip_packet_router.public_ed25519_identity_key_file }}' +public_ed25519_identity_key_file = '{{ service_providers.storage_paths.ip_packet_router.public_ed25519_identity_key_file }}' # Path to file containing ip packet router x25519 diffie hellman private key. -private_x25519_diffie_hellman_key_file = '{{ exit_gateway.storage_paths.ip_packet_router.private_x25519_diffie_hellman_key_file }}' +private_x25519_diffie_hellman_key_file = '{{ service_providers.storage_paths.ip_packet_router.private_x25519_diffie_hellman_key_file }}' # Path to file containing ip packet router x25519 diffie hellman public key. -public_x25519_diffie_hellman_key_file = '{{ exit_gateway.storage_paths.ip_packet_router.public_x25519_diffie_hellman_key_file }}' +public_x25519_diffie_hellman_key_file = '{{ service_providers.storage_paths.ip_packet_router.public_x25519_diffie_hellman_key_file }}' # Path to file containing key used for encrypting and decrypting the content of an # acknowledgement so that nobody besides the client knows which packet it refers to. -ack_key_file = '{{ exit_gateway.storage_paths.ip_packet_router.ack_key_file }}' +ack_key_file = '{{ service_providers.storage_paths.ip_packet_router.ack_key_file }}' # Path to the persistent store for received reply surbs, unused encryption keys and used sender tags. -reply_surb_database = '{{ exit_gateway.storage_paths.ip_packet_router.reply_surb_database }}' +reply_surb_database = '{{ service_providers.storage_paths.ip_packet_router.reply_surb_database }}' # Normally this is a path to the file containing information about gateways used by this client, # i.e. details such as their public keys, owner addresses or the network information. # but in this case it just has the basic information of "we're using custom gateway". # Due to how clients are started up, this file has to exist. -gateway_registrations = '{{ exit_gateway.storage_paths.ip_packet_router.gateway_registrations }}' +gateway_registrations = '{{ service_providers.storage_paths.ip_packet_router.gateway_registrations }}' -[exit_gateway.storage_paths.authenticator] +[service_providers.storage_paths.authenticator] # Path to file containing authenticator ed25519 identity private key. -private_ed25519_identity_key_file = '{{ exit_gateway.storage_paths.authenticator.private_ed25519_identity_key_file }}' +private_ed25519_identity_key_file = '{{ service_providers.storage_paths.authenticator.private_ed25519_identity_key_file }}' # Path to file containing authenticator ed25519 identity public key. -public_ed25519_identity_key_file = '{{ exit_gateway.storage_paths.authenticator.public_ed25519_identity_key_file }}' +public_ed25519_identity_key_file = '{{ service_providers.storage_paths.authenticator.public_ed25519_identity_key_file }}' # Path to file containing authenticator x25519 diffie hellman private key. -private_x25519_diffie_hellman_key_file = '{{ exit_gateway.storage_paths.authenticator.private_x25519_diffie_hellman_key_file }}' +private_x25519_diffie_hellman_key_file = '{{ service_providers.storage_paths.authenticator.private_x25519_diffie_hellman_key_file }}' # Path to file containing authenticator x25519 diffie hellman public key. -public_x25519_diffie_hellman_key_file = '{{ exit_gateway.storage_paths.authenticator.public_x25519_diffie_hellman_key_file }}' +public_x25519_diffie_hellman_key_file = '{{ service_providers.storage_paths.authenticator.public_x25519_diffie_hellman_key_file }}' # Path to file containing key used for encrypting and decrypting the content of an # acknowledgement so that nobody besides the client knows which packet it refers to. -ack_key_file = '{{ exit_gateway.storage_paths.authenticator.ack_key_file }}' +ack_key_file = '{{ service_providers.storage_paths.authenticator.ack_key_file }}' # Path to the persistent store for received reply surbs, unused encryption keys and used sender tags. -reply_surb_database = '{{ exit_gateway.storage_paths.authenticator.reply_surb_database }}' +reply_surb_database = '{{ service_providers.storage_paths.authenticator.reply_surb_database }}' # Normally this is a path to the file containing information about gateways used by this client, # i.e. details such as their public keys, owner addresses or the network information. # but in this case it just has the basic information of "we're using custom gateway". # Due to how clients are started up, this file has to exist. -gateway_registrations = '{{ exit_gateway.storage_paths.authenticator.gateway_registrations }}' - - -[authenticator] -# currently empty (there are some debug options one might want to configure) - -##### logging configuration options ##### - -[logging] +gateway_registrations = '{{ service_providers.storage_paths.authenticator.gateway_registrations }}' -# TODO "#; diff --git a/nym-node/src/config/upgrade_helpers.rs b/nym-node/src/config/upgrade_helpers.rs index bae75192ecf..26cc748f37c 100644 --- a/nym-node/src/config/upgrade_helpers.rs +++ b/nym-node/src/config/upgrade_helpers.rs @@ -5,6 +5,7 @@ use crate::config::old_configs::*; use crate::config::Config; use crate::error::NymNodeError; use std::path::Path; +use tracing::debug; // currently there are no upgrades async fn try_upgrade_config(path: &Path) -> Result<(), NymNodeError> { @@ -12,7 +13,8 @@ async fn try_upgrade_config(path: &Path) -> Result<(), NymNodeError> { let cfg = try_upgrade_config_v2(path, cfg).await.ok(); let cfg = try_upgrade_config_v3(path, cfg).await.ok(); let cfg = try_upgrade_config_v4(path, cfg).await.ok(); - match try_upgrade_config_v5(path, cfg).await { + let cfg = try_upgrade_config_v5(path, cfg).await.ok(); + match try_upgrade_config_v6(path, cfg).await { Ok(cfg) => cfg.save(), Err(e) => { tracing::error!("Failed to finish upgrade - {e}"); @@ -24,7 +26,10 @@ async fn try_upgrade_config(path: &Path) -> Result<(), NymNodeError> { pub async fn try_load_current_config<P: AsRef<Path>>( config_path: P, ) -> Result<Config, NymNodeError> { - if let Ok(cfg) = Config::read_from_toml_file(config_path.as_ref()) { + if let Ok(cfg) = Config::read_from_toml_file(config_path.as_ref()) + .inspect_err(|err| debug!("didn't manage to load the current config: {err}")) + { + debug!("managed to load the current version of the config"); return Ok(cfg); } diff --git a/nym-node/src/env.rs b/nym-node/src/env.rs index b4662fa69e5..e471cf91bd9 100644 --- a/nym-node/src/env.rs +++ b/nym-node/src/env.rs @@ -17,6 +17,7 @@ pub mod vars { pub const NYMNODE_BONDING_INFORMATION_OUTPUT_ARG: &str = "NYMNODE_BONDING_INFORMATION_OUTPUT"; pub const NYMNODE_MODE_ARG: &str = "NYMNODE_MODE"; + pub const NYMNODE_MODES_ARG: &str = "NYMNODE_MODES"; pub const NYMNODE_ACCEPT_OPERATOR_TERMS: &str = "NYMNODE_ACCEPT_OPERATOR_TERMS"; @@ -46,10 +47,13 @@ pub mod vars { pub const NYMNODE_WG_ANNOUNCED_PORT_ARG: &str = "NYMNODE_WG_ANNOUNCED_PORT"; pub const NYMNODE_WG_PRIVATE_NETWORK_PREFIX_ARG: &str = "NYMNODE_WG_PRIVATE_NETWORK_PREFIX"; - // mixnode: + // verloc: pub const NYMNODE_VERLOC_BIND_ADDRESS_ARG: &str = "NYMNODE_VERLOC_BIND_ADDRESS"; pub const NYMNODE_VERLOC_ANNOUNCE_PORT_ARG: &str = "NYMNODE_VERLOC_ANNOUNCE_PORT"; + // metrics + pub const NYMNODE_ENABLE_CONSOLE_LOGGING: &str = "NYMNODE_ENABLE_CONSOLE_LOGGING"; + // entry gateway: pub const NYMNODE_ENTRY_BIND_ADDRESS_ARG: &str = "NYMNODE_ENTRY_BIND_ADDRESS"; pub const NYMNODE_ENTRY_ANNOUNCE_WS_PORT_ARG: &str = "NYMNODE_ENTRY_ANNOUNCE_WS_PORT"; diff --git a/nym-node/src/error.rs b/nym-node/src/error.rs index afbed84a0e6..4794b129616 100644 --- a/nym-node/src/error.rs +++ b/nym-node/src/error.rs @@ -1,16 +1,16 @@ // Copyright 2023 - Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: GPL-3.0-only -use crate::config::helpers::UnsupportedGatewayAddresses; +use crate::node::http::error::NymNodeHttpError; use crate::wireguard::error::WireguardError; use nym_ip_packet_router::error::ClientCoreError; -use nym_node_http_api::NymNodeHttpError; use std::io; use std::net::IpAddr; use std::path::PathBuf; use thiserror::Error; #[derive(Debug, Error)] +#[allow(clippy::enum_variant_names)] pub enum KeyIOFailure { #[error("failed to load {keys} keys from {:?} (private key) and {:?} (public key): {err}", .paths.private_key_path, .paths.public_key_path)] KeyPairLoadFailure { @@ -56,9 +56,6 @@ pub enum NymNodeError { #[error("could not derive path to data directory of this nym node")] DataDirDerivationFailure, - #[error("could not derive path to config directory of this nym node")] - ConfigDirDerivationFailure, - #[error(transparent)] HttpFailure(#[from] NymNodeHttpError), @@ -116,6 +113,9 @@ pub enum NymNodeError { source: WireguardError, }, + #[error("wireguard data is no longer available - has it been reused?")] + WireguardDataUnavailable, + #[deprecated] #[error(transparent)] KeyRecoveryError { @@ -129,9 +129,6 @@ pub enum NymNodeError { #[error("could not initialise nym-node as '--{name}' has not been specified which is required for a first time setup. (config section: {section})")] MissingInitArg { section: String, name: String }, - #[error("failed to migrate {node_type}: {message}")] - MigrationFailure { node_type: String, message: String }, - #[error("there was an issue with wireguard IP network: {source}")] IpNetworkError { #[from] @@ -139,13 +136,16 @@ pub enum NymNodeError { }, #[error(transparent)] - MixnodeFailure(#[from] MixnodeError), + GatewayFailure(#[from] nym_gateway::GatewayError), + + #[error(transparent)] + GatewayTasksStartupFailure(Box<dyn std::error::Error + Send + Sync>), #[error(transparent)] EntryGatewayFailure(#[from] EntryGatewayError), #[error(transparent)] - ExitGatewayFailure(#[from] ExitGatewayError), + ServiceProvidersFailure(#[from] ServiceProvidersError), // TODO: more granular errors #[error(transparent)] @@ -155,15 +155,6 @@ pub enum NymNodeError { FailedUpgrade, } -#[derive(Debug, Error)] -pub enum MixnodeError { - #[error("currently it's not supported to have different ip addresses for verloc and mixnet ({verloc_bind_ip} and {mix_bind_ip} were used)")] - UnsupportedAddresses { - verloc_bind_ip: IpAddr, - mix_bind_ip: IpAddr, - }, -} - #[derive(Debug, Error)] pub enum EntryGatewayError { #[error(transparent)] @@ -193,25 +184,16 @@ pub enum EntryGatewayError { source: bip39::Error, }, - #[error(transparent)] - UnsupportedAddresses(#[from] UnsupportedGatewayAddresses), - #[error("entry gateway failure: {0}")] External(#[from] nym_gateway::GatewayError), } #[derive(Debug, Error)] -pub enum ExitGatewayError { +pub enum ServiceProvidersError { #[error(transparent)] KeyFailure(#[from] KeyIOFailure), - #[error(transparent)] - UnsupportedAddresses(#[from] UnsupportedGatewayAddresses), - // TODO: more granular errors #[error(transparent)] ExternalClientCore(#[from] ClientCoreError), - - #[error("exit gateway failure: {0}")] - External(#[from] nym_gateway::GatewayError), } diff --git a/nym-node/src/lib.rs b/nym-node/src/lib.rs deleted file mode 100644 index 320d22559dd..00000000000 --- a/nym-node/src/lib.rs +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright 2023-2024 - Nym Technologies SA <contact@nymtech.net> -// SPDX-License-Identifier: GPL-3.0-only - -#![warn(clippy::expect_used)] -#![warn(clippy::unwrap_used)] - -pub mod config; -pub mod error; -pub mod wireguard; - -pub use nym_node_http_api as http; diff --git a/nym-node/src/main.rs b/nym-node/src/main.rs index 8d65de2aa6b..9d66d778d2b 100644 --- a/nym-node/src/main.rs +++ b/nym-node/src/main.rs @@ -10,9 +10,12 @@ use nym_bin_common::logging::maybe_print_banner; use nym_config::defaults::setup_env; mod cli; +pub(crate) mod config; mod env; +pub(crate) mod error; mod logging; pub(crate) mod node; +pub(crate) mod wireguard; #[tokio::main] async fn main() -> anyhow::Result<()> { diff --git a/nym-node/src/node/bonding_information.rs b/nym-node/src/node/bonding_information.rs index 5132d377628..94d151dd030 100644 --- a/nym-node/src/node/bonding_information.rs +++ b/nym-node/src/node/bonding_information.rs @@ -1,170 +1,59 @@ // Copyright 2024 - Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: GPL-3.0-only -use crate::node::helpers::{ - bonding_version, load_ed25519_identity_public_key, load_x25519_sphinx_public_key, -}; -use nym_node::config::{Config, NodeMode}; -use nym_node::error::NymNodeError; +use crate::config::Config; +use crate::error::NymNodeError; +use crate::node::helpers::load_ed25519_identity_public_key; +use nym_crypto::asymmetric::ed25519; use serde::{Deserialize, Serialize}; use std::fmt::{Display, Formatter}; -#[derive(Serialize, Deserialize, Debug)] -#[serde(tag = "node_type")] -pub enum BondingInformationV1 { - Mixnode(MixnodeBondingInformation), - Gateway(GatewayBondingInformation), +#[derive(Debug, Deserialize, Serialize)] +pub struct BondingInformation { + host: String, + identity_key: ed25519::PublicKey, } -impl Display for BondingInformationV1 { +impl Display for BondingInformation { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - BondingInformationV1::Mixnode(m) => m.fmt(f), - BondingInformationV1::Gateway(g) => g.fmt(f), - } - } -} - -// TODO: work in progress, I'm not 100% sure yet what will be needed -// #[derive(Serialize, Deserialize, Debug)] -// pub struct BondingInformationV2 { -// pub(crate) ed25519_identity_key: ed25519::PublicKey, -// pub(crate) x25519_sphinx_key: x25519::PublicKey, -// } -// -// impl Display for BondingInformationV2 { -// fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { -// writeln!( -// f, -// "ed25519 identity key: {}", -// self.ed25519_identity_key.to_base58_string() -// )?; -// write!( -// f, -// "x25519 sphinx key: {}", -// self.x25519_sphinx_key.to_base58_string() -// ) -// } -// } - -#[derive(Serialize, Deserialize, Debug)] -pub struct MixnodeBondingInformation { - pub(crate) version: String, - pub(crate) host: String, - pub(crate) identity_key: String, - pub(crate) sphinx_key: String, -} - -impl MixnodeBondingInformation { - pub fn from_data( - ed25519_identity_key: String, - x25519_sphinx_key: String, - ) -> MixnodeBondingInformation { - MixnodeBondingInformation { - version: bonding_version(), - host: "YOU NEED TO FILL THIS FIELD MANUALLY".to_string(), - identity_key: ed25519_identity_key, - sphinx_key: x25519_sphinx_key, - } - } -} - -impl Display for MixnodeBondingInformation { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - writeln!(f, "Node type: Mixnode")?; writeln!(f, "Identity Key: {}", self.identity_key)?; - writeln!(f, "Sphinx Key: {}", self.sphinx_key)?; writeln!(f, "Host: {}", self.host)?; - writeln!(f, "Version: {}", self.version)?; - Ok(()) - } -} - -#[derive(Serialize, Deserialize, Debug)] -pub struct GatewayBondingInformation { - pub(crate) version: String, - pub(crate) host: String, - pub(crate) location: String, - pub(crate) identity_key: String, - pub(crate) sphinx_key: String, -} + writeln!(f, "Custom HTTP Port: you might want to set it if your node won't be accessible on any of the ports: 80/443/8080")?; -impl GatewayBondingInformation { - pub fn from_data( - ed25519_identity_key: String, - x25519_sphinx_key: String, - ) -> GatewayBondingInformation { - GatewayBondingInformation { - version: bonding_version(), - host: "YOU NEED TO FILL THIS FIELD MANUALLY".to_string(), - location: "YOU NEED TO FILL THIS FIELD MANUALLY".to_string(), - identity_key: ed25519_identity_key, - sphinx_key: x25519_sphinx_key, - } - } -} - -impl Display for GatewayBondingInformation { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - writeln!(f, "Node type: Gateway")?; - writeln!(f, "Identity Key: {}", self.identity_key)?; - writeln!(f, "Sphinx Key: {}", self.sphinx_key)?; - writeln!(f, "Location: {}", self.location)?; - writeln!(f, "Host: {}", self.host)?; - writeln!(f, "Version: {}", self.version)?; Ok(()) } } -impl BondingInformationV1 { - pub fn from_data( - mode: NodeMode, - ed25519_identity_key: String, - x25519_sphinx_key: String, - ) -> BondingInformationV1 { - match mode { - NodeMode::Mixnode => BondingInformationV1::Mixnode( - MixnodeBondingInformation::from_data(ed25519_identity_key, x25519_sphinx_key), - ), - NodeMode::EntryGateway | NodeMode::ExitGateway => BondingInformationV1::Gateway( - GatewayBondingInformation::from_data(ed25519_identity_key, x25519_sphinx_key), - ), - } - } - - fn ed25519_identity_key(&self) -> String { - match self { - BondingInformationV1::Mixnode(m) => m.identity_key.clone(), - BondingInformationV1::Gateway(g) => g.identity_key.clone(), - } - } - - fn x25519_sphinx_key(&self) -> String { - match self { - BondingInformationV1::Mixnode(m) => m.sphinx_key.clone(), - BondingInformationV1::Gateway(g) => g.sphinx_key.clone(), +impl BondingInformation { + pub fn from_data(config: &Config, ed25519_identity_key: ed25519::PublicKey) -> Self { + let host = match config.host.hostname { + Some(ref host) => host.clone(), + None => match config.host.public_ips.first() { + Some(first_ip) => { + if !first_ip.is_loopback() + && !first_ip.is_multicast() + && !first_ip.is_unspecified() + { + first_ip.to_string() + } else { + "NO KNOWN VALID HOSTNAMES - YOU NEED TO FILL IT MANUALLY".to_string() + } + } + None => "NO KNOWN VALID HOSTNAMES - YOU NEED TO FILL IT MANUALLY".to_string(), + }, + }; + + BondingInformation { + host, + identity_key: ed25519_identity_key, } } - pub fn try_load(config: &Config) -> Result<BondingInformationV1, NymNodeError> { + pub fn try_load(config: &Config) -> Result<BondingInformation, NymNodeError> { let ed25519_identity_key = load_ed25519_identity_public_key( &config.storage_paths.keys.public_ed25519_identity_key_file, )?; - let x25519_sphinx_key = load_x25519_sphinx_public_key( - &config.storage_paths.keys.public_x25519_sphinx_key_file, - )?; - let mode = config.mode; - - Ok(Self::from_data( - mode, - ed25519_identity_key.to_base58_string(), - x25519_sphinx_key.to_base58_string(), - )) - } - pub fn with_mode(self, mode: NodeMode) -> Self { - let identity_key = self.ed25519_identity_key(); - let sphinx_key = self.x25519_sphinx_key(); - Self::from_data(mode, identity_key, sphinx_key) + Ok(Self::from_data(config, ed25519_identity_key)) } } diff --git a/nym-node/src/node/description.rs b/nym-node/src/node/description.rs index 0f5b7137277..e28efab75dd 100644 --- a/nym-node/src/node/description.rs +++ b/nym-node/src/node/description.rs @@ -1,8 +1,8 @@ // Copyright 2024 - Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: GPL-3.0-only -use nym_node::error::NymNodeError; -use nym_node_http_api::api::api_requests::v1::node::models::NodeDescription; +use crate::error::NymNodeError; +use nym_node_requests::api::v1::node::models::NodeDescription; use std::fs; use std::fs::create_dir_all; use std::path::Path; diff --git a/nym-node/src/node/helpers.rs b/nym-node/src/node/helpers.rs index 30555787576..aa9e3525e45 100644 --- a/nym-node/src/node/helpers.rs +++ b/nym-node/src/node/helpers.rs @@ -1,33 +1,19 @@ // Copyright 2024 - Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: GPL-3.0-only +use crate::config::NodeModes; +use crate::error::{KeyIOFailure, NymNodeError}; use nym_crypto::asymmetric::{ed25519, x25519}; -use nym_node::config::NodeMode; -use nym_node::error::{KeyIOFailure, NymNodeError}; -use nym_node_http_api::api::api_requests::v1::node::models::NodeDescription; +use nym_node_requests::api::v1::node::models::NodeDescription; use nym_pemstore::traits::{PemStorableKey, PemStorableKeyPair}; use nym_pemstore::KeyPairPath; -use semver::{BuildMetadata, Version}; use serde::Serialize; use std::fmt::{Display, Formatter}; use std::path::Path; -#[allow(clippy::unwrap_used)] -pub fn bonding_version() -> String { - // SAFETY: - // the value has been put there by cargo - let raw = env!("CARGO_PKG_VERSION"); - let mut semver: Version = raw.parse().unwrap(); - - // if it's not empty, then we messed up our own versioning - assert!(semver.build.is_empty()); - semver.build = BuildMetadata::new("nymnode").unwrap(); - semver.to_string() -} - #[derive(Debug, Serialize)] pub(crate) struct DisplayDetails { - pub(crate) current_mode: NodeMode, + pub(crate) current_modes: NodeModes, pub(crate) description: NodeDescription, @@ -43,7 +29,7 @@ pub(crate) struct DisplayDetails { impl Display for DisplayDetails { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - writeln!(f, "current mode: {}", self.current_mode)?; + writeln!(f, "current mode: {:#?}", self.current_modes)?; writeln!(f, "moniker: '{}'", self.description.moniker)?; writeln!(f, "website: '{}'", self.description.website)?; writeln!( @@ -152,12 +138,6 @@ pub(crate) fn load_x25519_wireguard_keypair( Ok(load_keypair(paths, "x25519-wireguard")?) } -pub(crate) fn load_x25519_sphinx_public_key<P: AsRef<Path>>( - path: P, -) -> Result<x25519::PublicKey, NymNodeError> { - Ok(load_key(path, "x25519-sphinx-public-key")?) -} - pub(crate) fn store_ed25519_identity_keypair( keys: &ed25519::KeyPair, paths: KeyPairPath, diff --git a/nym-node/nym-node-http-api/src/error.rs b/nym-node/src/node/http/error.rs similarity index 100% rename from nym-node/nym-node-http-api/src/error.rs rename to nym-node/src/node/http/error.rs diff --git a/nym-node/src/node/http/helpers/mod.rs b/nym-node/src/node/http/helpers/mod.rs new file mode 100644 index 00000000000..a819bdd062d --- /dev/null +++ b/nym-node/src/node/http/helpers/mod.rs @@ -0,0 +1,38 @@ +// Copyright 2024 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: GPL-3.0-only + +use crate::config::Config; +use crate::error::NymNodeError; +use crate::node::http::api::api_requests; +use crate::node::http::error::NymNodeHttpError; +use nym_crypto::asymmetric::{ed25519, x25519}; +use nym_node_requests::api::SignedHostInformation; + +pub mod system_info; + +pub(crate) fn sign_host_details( + config: &Config, + x22519_sphinx: &x25519::PublicKey, + x25519_noise: &x25519::PublicKey, + ed22519_identity: &ed25519::KeyPair, +) -> Result<SignedHostInformation, NymNodeError> { + let x25519_noise = if config.mixnet.debug.unsafe_disable_noise { + None + } else { + Some(*x25519_noise) + }; + + let host_info = api_requests::v1::node::models::HostInformation { + ip_address: config.host.public_ips.clone(), + hostname: config.host.hostname.clone(), + keys: api_requests::v1::node::models::HostKeys { + ed25519_identity: *ed22519_identity.public_key(), + x25519_sphinx: *x22519_sphinx, + x25519_noise, + }, + }; + + let signed_info = SignedHostInformation::new(host_info, ed22519_identity.private_key()) + .map_err(NymNodeHttpError::from)?; + Ok(signed_info) +} diff --git a/nym-node/src/node/http/system_info.rs b/nym-node/src/node/http/helpers/system_info.rs similarity index 94% rename from nym-node/src/node/http/system_info.rs rename to nym-node/src/node/http/helpers/system_info.rs index 128ce372294..980c305ba90 100644 --- a/nym-node/src/node/http/system_info.rs +++ b/nym-node/src/node/http/helpers/system_info.rs @@ -2,9 +2,7 @@ // SPDX-License-Identifier: GPL-3.0-only use cupid::TopologyType; -use nym_node_http_api::api::api_requests::v1::node::models::{ - Cpu, CryptoHardware, Hardware, HostSystem, -}; +use nym_node_requests::api::v1::node::models::{Cpu, CryptoHardware, Hardware, HostSystem}; use sysinfo::System; fn crypto_hardware() -> Option<CryptoHardware> { diff --git a/nym-node/src/node/http/mod.rs b/nym-node/src/node/http/mod.rs index defdd9628e0..f05ec0355a9 100644 --- a/nym-node/src/node/http/mod.rs +++ b/nym-node/src/node/http/mod.rs @@ -1,40 +1,65 @@ -// Copyright 2024 - Nym Technologies SA <contact@nymtech.net> +// Copyright 2023 - Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: GPL-3.0-only -use nym_crypto::asymmetric::{ed25519, x25519}; -use nym_node::config::Config; -use nym_node::error::NymNodeError; -use nym_node_http_api::api::api_requests; -use nym_node_http_api::api::api_requests::SignedHostInformation; -use nym_node_http_api::NymNodeHttpError; - -pub(crate) mod system_info; - -pub(crate) fn sign_host_details( - config: &Config, - x22519_sphinx: &x25519::PublicKey, - x25519_noise: &x25519::PublicKey, - ed22519_identity: &ed25519::KeyPair, -) -> Result<api_requests::v1::node::models::SignedHostInformation, NymNodeError> { - let x25519_noise = if config.mixnet.debug.unsafe_disable_noise { - None - } else { - Some(*x25519_noise) - }; - - let host_info = api_requests::v1::node::models::HostInformation { - ip_address: config.host.public_ips.clone(), - hostname: config.host.hostname.clone(), - keys: api_requests::v1::node::models::HostKeys { - ed25519_identity: *ed22519_identity.public_key(), - x25519_sphinx: *x22519_sphinx, - x25519_noise, - }, - }; - - let signed_info = SignedHostInformation::new(host_info, ed22519_identity.private_key()) - .map_err(NymNodeHttpError::from)?; - Ok(signed_info) +use axum::extract::connect_info::IntoMakeServiceWithConnectInfo; +use axum::extract::ConnectInfo; +use axum::middleware::AddExtension; +use axum::serve::Serve; +use axum::Router; +use nym_task::TaskClient; +use std::net::SocketAddr; +use tracing::{debug, error}; + +pub use router::{api, HttpServerConfig, NymNodeRouter}; + +pub mod error; +pub mod helpers; +pub mod router; +pub mod state; + +type InnerService = IntoMakeServiceWithConnectInfo<Router, SocketAddr>; +type ConnectInfoExt = AddExtension<Router, ConnectInfo<SocketAddr>>; +pub type ServeService = Serve<InnerService, ConnectInfoExt>; + +pub struct NymNodeHttpServer { + task_client: Option<TaskClient>, + inner: ServeService, } -// pub(crate) fn run_http_api(config: &Config, task_client: TaskClient) +impl NymNodeHttpServer { + pub(crate) fn new(inner: ServeService) -> Self { + NymNodeHttpServer { + task_client: None, + inner, + } + } + + #[must_use] + pub fn with_task_client(mut self, task_client: TaskClient) -> Self { + self.task_client = Some(task_client); + self + } + + async fn run_server_forever(server: ServeService) { + if let Err(err) = server.await { + error!("the HTTP server has terminated with the error: {err}"); + } else { + error!("the HTTP server has terminated with producing any errors"); + } + } + + pub async fn run(self) { + if let Some(mut task_client) = self.task_client { + tokio::select! { + _ = task_client.recv_with_delay() => { + debug!("NymNodeHTTPServer: Received shutdown"); + } + _ = Self::run_server_forever(self.inner) => { } + } + } else { + Self::run_server_forever(self.inner).await + } + + debug!("NymNodeHTTPServer: Exiting"); + } +} diff --git a/nym-node/nym-node-http-api/src/router/api/mod.rs b/nym-node/src/node/http/router/api/mod.rs similarity index 79% rename from nym-node/nym-node-http-api/src/router/api/mod.rs rename to nym-node/src/node/http/router/api/mod.rs index 25498d9a550..ea3c5e4c059 100644 --- a/nym-node/nym-node-http-api/src/router/api/mod.rs +++ b/nym-node/src/node/http/router/api/mod.rs @@ -1,14 +1,12 @@ // Copyright 2023 - Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: GPL-3.0-only -use crate::state::AppState; +use crate::node::http::state::AppState; use axum::Router; use nym_node_requests::routes; pub mod v1; -pub(crate) use nym_http_api_common::{FormattedResponse, Output, OutputParams}; - pub use nym_node_requests::api as api_requests; #[derive(Debug, Clone)] diff --git a/nym-node/nym-node-http-api/src/router/api/v1/authenticator/mod.rs b/nym-node/src/node/http/router/api/v1/authenticator/mod.rs similarity index 100% rename from nym-node/nym-node-http-api/src/router/api/v1/authenticator/mod.rs rename to nym-node/src/node/http/router/api/v1/authenticator/mod.rs diff --git a/nym-node/nym-node-http-api/src/router/api/v1/authenticator/root.rs b/nym-node/src/node/http/router/api/v1/authenticator/root.rs similarity index 94% rename from nym-node/nym-node-http-api/src/router/api/v1/authenticator/root.rs rename to nym-node/src/node/http/router/api/v1/authenticator/root.rs index 9fb21042062..054d224779d 100644 --- a/nym-node/nym-node-http-api/src/router/api/v1/authenticator/root.rs +++ b/nym-node/src/node/http/router/api/v1/authenticator/root.rs @@ -1,9 +1,9 @@ // Copyright 2024 - Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: GPL-3.0-only -use crate::router::api::{FormattedResponse, OutputParams}; use axum::extract::Query; use axum::http::StatusCode; +use nym_http_api_common::{FormattedResponse, OutputParams}; use nym_node_requests::api::v1::authenticator::models::Authenticator; /// Returns root authenticator information diff --git a/nym-node/nym-node-http-api/src/router/api/v1/gateway/client_interfaces/mod.rs b/nym-node/src/node/http/router/api/v1/gateway/client_interfaces/mod.rs similarity index 98% rename from nym-node/nym-node-http-api/src/router/api/v1/gateway/client_interfaces/mod.rs rename to nym-node/src/node/http/router/api/v1/gateway/client_interfaces/mod.rs index 809cc1fc9eb..96215f71920 100644 --- a/nym-node/nym-node-http-api/src/router/api/v1/gateway/client_interfaces/mod.rs +++ b/nym-node/src/node/http/router/api/v1/gateway/client_interfaces/mod.rs @@ -1,11 +1,11 @@ // Copyright 2023 - Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: GPL-3.0-only -use crate::api::{FormattedResponse, OutputParams}; use axum::extract::Query; use axum::http::StatusCode; use axum::routing::get; use axum::Router; +use nym_http_api_common::{FormattedResponse, OutputParams}; use nym_node_requests::api::v1::gateway::models::{ClientInterfaces, WebSockets, Wireguard}; use nym_node_requests::routes::api::v1::gateway::client_interfaces; diff --git a/nym-node/nym-node-http-api/src/router/api/v1/gateway/mod.rs b/nym-node/src/node/http/router/api/v1/gateway/mod.rs similarity index 100% rename from nym-node/nym-node-http-api/src/router/api/v1/gateway/mod.rs rename to nym-node/src/node/http/router/api/v1/gateway/mod.rs diff --git a/nym-node/nym-node-http-api/src/router/api/v1/gateway/root.rs b/nym-node/src/node/http/router/api/v1/gateway/root.rs similarity index 94% rename from nym-node/nym-node-http-api/src/router/api/v1/gateway/root.rs rename to nym-node/src/node/http/router/api/v1/gateway/root.rs index cb6fc6cbfdb..4738acb30ec 100644 --- a/nym-node/nym-node-http-api/src/router/api/v1/gateway/root.rs +++ b/nym-node/src/node/http/router/api/v1/gateway/root.rs @@ -1,9 +1,9 @@ // Copyright 2023 - Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: GPL-3.0-only -use crate::router::api::{FormattedResponse, OutputParams}; use axum::extract::Query; use axum::http::StatusCode; +use nym_http_api_common::{FormattedResponse, OutputParams}; use nym_node_requests::api::v1::gateway::models::Gateway; /// Returns root gateway information diff --git a/nym-node/nym-node-http-api/src/router/api/v1/health.rs b/nym-node/src/node/http/router/api/v1/health.rs similarity index 90% rename from nym-node/nym-node-http-api/src/router/api/v1/health.rs rename to nym-node/src/node/http/router/api/v1/health.rs index 4beb5f6af5f..0972ea6339e 100644 --- a/nym-node/nym-node-http-api/src/router/api/v1/health.rs +++ b/nym-node/src/node/http/router/api/v1/health.rs @@ -1,9 +1,9 @@ // Copyright 2023 - Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: GPL-3.0-only -use crate::api::{FormattedResponse, OutputParams}; -use crate::state::AppState; +use crate::node::http::state::AppState; use axum::extract::{Query, State}; +use nym_http_api_common::{FormattedResponse, OutputParams}; use nym_node_requests::api::v1::health::models::NodeHealth; /// Returns health status of this node. diff --git a/nym-node/nym-node-http-api/src/router/api/v1/ip_packet_router/mod.rs b/nym-node/src/node/http/router/api/v1/ip_packet_router/mod.rs similarity index 100% rename from nym-node/nym-node-http-api/src/router/api/v1/ip_packet_router/mod.rs rename to nym-node/src/node/http/router/api/v1/ip_packet_router/mod.rs diff --git a/nym-node/nym-node-http-api/src/router/api/v1/ip_packet_router/root.rs b/nym-node/src/node/http/router/api/v1/ip_packet_router/root.rs similarity index 94% rename from nym-node/nym-node-http-api/src/router/api/v1/ip_packet_router/root.rs rename to nym-node/src/node/http/router/api/v1/ip_packet_router/root.rs index 41fc8e536c6..18c503bd5ce 100644 --- a/nym-node/nym-node-http-api/src/router/api/v1/ip_packet_router/root.rs +++ b/nym-node/src/node/http/router/api/v1/ip_packet_router/root.rs @@ -1,9 +1,9 @@ // Copyright 2023 - Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: GPL-3.0-only -use crate::router::api::{FormattedResponse, OutputParams}; use axum::extract::Query; use axum::http::StatusCode; +use nym_http_api_common::{FormattedResponse, OutputParams}; use nym_node_requests::api::v1::ip_packet_router::models::IpPacketRouter; /// Returns root network requester information diff --git a/nym-node/src/node/http/router/api/v1/metrics/legacy_mixing.rs b/nym-node/src/node/http/router/api/v1/metrics/legacy_mixing.rs new file mode 100644 index 00000000000..23cfd0607d7 --- /dev/null +++ b/nym-node/src/node/http/router/api/v1/metrics/legacy_mixing.rs @@ -0,0 +1,47 @@ +// Copyright 2024 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: GPL-3.0-only + +use crate::node::http::state::metrics::MetricsAppState; +use axum::extract::{Query, State}; +use nym_http_api_common::{FormattedResponse, OutputParams}; +use nym_node_metrics::NymNodeMetrics; +use nym_node_requests::api::v1::metrics::models::LegacyMixingStats; + +/// If applicable, returns mixing statistics information of this node. +/// This information is **PURELY** self-reported and in no way validated. +#[utoipa::path( + get, + path = "/mixing", + context_path = "/api/v1/metrics", + tag = "Metrics", + responses( + (status = 200, content( + ("application/json" = LegacyMixingStats), + ("application/yaml" = LegacyMixingStats) + )) + ), + params(OutputParams), +)] +#[deprecated] +pub(crate) async fn legacy_mixing_stats( + Query(output): Query<OutputParams>, + State(metrics_state): State<MetricsAppState>, +) -> LegacyMixingStatsResponse { + let output = output.output.unwrap_or_default(); + output.to_response(build_legacy_response(&metrics_state.metrics)) +} + +fn build_legacy_response(metrics: &NymNodeMetrics) -> LegacyMixingStats { + LegacyMixingStats { + update_time: metrics.mixnet.legacy.last_update(), + previous_update_time: metrics.mixnet.legacy.previous_update(), + received_since_startup: metrics.mixnet.ingress.forward_hop_packets_received() as u64, + sent_since_startup: metrics.mixnet.egress.forward_hop_packets_sent() as u64, + dropped_since_startup: metrics.mixnet.egress.forward_hop_packets_dropped() as u64, + received_since_last_update: metrics.mixnet.legacy.received_since_last_update() as u64, + sent_since_last_update: metrics.mixnet.legacy.sent_since_last_update() as u64, + dropped_since_last_update: metrics.mixnet.legacy.dropped_since_last_update() as u64, + } +} + +pub type LegacyMixingStatsResponse = FormattedResponse<LegacyMixingStats>; diff --git a/nym-node/nym-node-http-api/src/router/api/v1/metrics/mod.rs b/nym-node/src/node/http/router/api/v1/metrics/mod.rs similarity index 53% rename from nym-node/nym-node-http-api/src/router/api/v1/metrics/mod.rs rename to nym-node/src/node/http/router/api/v1/metrics/mod.rs index cabf4aedf52..201dbc7a26d 100644 --- a/nym-node/nym-node-http-api/src/router/api/v1/metrics/mod.rs +++ b/nym-node/src/node/http/router/api/v1/metrics/mod.rs @@ -1,17 +1,18 @@ // Copyright 2024 - Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: GPL-3.0-only -use crate::api::v1::metrics::mixing::mixing_stats; -use crate::api::v1::metrics::prometheus::prometheus_metrics; -use crate::api::v1::metrics::sessions::sessions_stats; -use crate::api::v1::metrics::verloc::verloc_stats; -use crate::state::metrics::MetricsAppState; +use crate::node::http::api::v1::metrics::packets_stats::packets_stats; +use crate::node::http::api::v1::metrics::prometheus::prometheus_metrics; +use crate::node::http::api::v1::metrics::sessions::sessions_stats; +use crate::node::http::api::v1::metrics::verloc::verloc_stats; +use crate::node::http::state::metrics::MetricsAppState; use axum::extract::FromRef; use axum::routing::get; use axum::Router; use nym_node_requests::routes::api::v1::metrics; -pub mod mixing; +pub mod legacy_mixing; +pub mod packets_stats; pub mod prometheus; pub mod sessions; pub mod verloc; @@ -21,13 +22,18 @@ pub struct Config { // } +#[allow(deprecated)] pub(super) fn routes<S>(_config: Config) -> Router<S> where S: Send + Sync + 'static + Clone, MetricsAppState: FromRef<S>, { Router::new() - .route(metrics::MIXING, get(mixing_stats)) + .route( + metrics::LEGACY_MIXING, + get(legacy_mixing::legacy_mixing_stats), + ) + .route(metrics::PACKETS_STATS, get(packets_stats)) .route(metrics::SESSIONS, get(sessions_stats)) .route(metrics::VERLOC, get(verloc_stats)) .route(metrics::PROMETHEUS, get(prometheus_metrics)) diff --git a/nym-node/src/node/http/router/api/v1/metrics/packets_stats.rs b/nym-node/src/node/http/router/api/v1/metrics/packets_stats.rs new file mode 100644 index 00000000000..d1a8e27f555 --- /dev/null +++ b/nym-node/src/node/http/router/api/v1/metrics/packets_stats.rs @@ -0,0 +1,53 @@ +// Copyright 2024 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: Apache-2.0 + +use crate::node::http::state::metrics::MetricsAppState; +use axum::extract::{Query, State}; +use nym_http_api_common::{FormattedResponse, OutputParams}; +use nym_node_metrics::NymNodeMetrics; +use nym_node_requests::api::v1::metrics::models::packets::{ + EgressMixingStats, IngressMixingStats, PacketsStats, +}; + +/// If applicable, returns packets statistics information of this node. +/// This information is **PURELY** self-reported and in no way validated. +#[utoipa::path( + get, + path = "/packets-stats", + context_path = "/api/v1/metrics", + tag = "Metrics", + responses( + (status = 200, content( + ("application/json" = PacketsStats), + ("application/yaml" = PacketsStats) + )) + ), + params(OutputParams), +)] +pub(crate) async fn packets_stats( + Query(output): Query<OutputParams>, + State(metrics_state): State<MetricsAppState>, +) -> PacketsStatsResponse { + let output = output.output.unwrap_or_default(); + output.to_response(build_response(&metrics_state.metrics)) +} + +fn build_response(metrics: &NymNodeMetrics) -> PacketsStats { + PacketsStats { + ingress_mixing: IngressMixingStats { + forward_hop_packets_received: metrics.mixnet.ingress.forward_hop_packets_received(), + final_hop_packets_received: metrics.mixnet.ingress.final_hop_packets_received(), + malformed_packets_received: metrics.mixnet.ingress.malformed_packets_received(), + excessive_delay_packets: metrics.mixnet.ingress.excessive_delay_packets(), + forward_hop_packets_dropped: metrics.mixnet.ingress.forward_hop_packets_dropped(), + final_hop_packets_dropped: metrics.mixnet.ingress.final_hop_packets_dropped(), + }, + egress_mixing: EgressMixingStats { + forward_hop_packets_sent: metrics.mixnet.egress.forward_hop_packets_sent(), + forward_hop_packets_dropped: metrics.mixnet.egress.forward_hop_packets_dropped(), + ack_packets_sent: metrics.mixnet.egress.ack_packets_sent(), + }, + } +} + +pub type PacketsStatsResponse = FormattedResponse<PacketsStats>; diff --git a/nym-node/nym-node-http-api/src/router/api/v1/metrics/prometheus.rs b/nym-node/src/node/http/router/api/v1/metrics/prometheus.rs similarity index 96% rename from nym-node/nym-node-http-api/src/router/api/v1/metrics/prometheus.rs rename to nym-node/src/node/http/router/api/v1/metrics/prometheus.rs index a404cb98715..197cc5a1c83 100644 --- a/nym-node/nym-node-http-api/src/router/api/v1/metrics/prometheus.rs +++ b/nym-node/src/node/http/router/api/v1/metrics/prometheus.rs @@ -1,7 +1,7 @@ // Copyright 2024 - Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: GPL-3.0-only -use crate::state::metrics::MetricsAppState; +use crate::node::http::state::metrics::MetricsAppState; use axum::extract::State; use axum::http::StatusCode; use axum_extra::TypedHeader; diff --git a/nym-node/src/node/http/router/api/v1/metrics/sessions.rs b/nym-node/src/node/http/router/api/v1/metrics/sessions.rs new file mode 100644 index 00000000000..0b76e832051 --- /dev/null +++ b/nym-node/src/node/http/router/api/v1/metrics/sessions.rs @@ -0,0 +1,54 @@ +// Copyright 2024 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: GPL-3.0-only + +use crate::node::http::state::metrics::MetricsAppState; +use axum::extract::{Query, State}; +use nym_http_api_common::{FormattedResponse, OutputParams}; +use nym_node_metrics::NymNodeMetrics; +use nym_node_requests::api::v1::metrics::models::{Session, SessionStats}; +use time::macros::time; + +/// If applicable, returns sessions statistics information of this node. +/// This information is **PURELY** self-reported and in no way validated. +#[utoipa::path( + get, + path = "/sessions", + context_path = "/api/v1/metrics", + tag = "Metrics", + responses( + (status = 200, content( + ("application/json" = SessionStats), + ("application/yaml" = SessionStats) + )) + ), + params(OutputParams), +)] +pub(crate) async fn sessions_stats( + Query(output): Query<OutputParams>, + State(metrics_state): State<MetricsAppState>, +) -> SessionStatsResponse { + let output = output.output.unwrap_or_default(); + output.to_response(build_response(&metrics_state.metrics).await) +} + +async fn build_response(metrics: &NymNodeMetrics) -> SessionStats { + let guard = metrics.entry.client_sessions().await; + let sessions = guard + .finished_sessions + .iter() + .map(|finished| Session { + duration_ms: finished.duration.as_millis() as u64, + typ: finished.typ.to_string(), + }) + .collect(); + SessionStats { + update_time: guard.update_time.with_time(time!(0:00)).assume_utc(), + unique_active_users: guard.unique_users.len() as u32, + unique_active_users_hashes: guard.unique_users.clone(), + sessions, + sessions_started: guard.sessions_started, + sessions_finished: guard.finished_sessions.len() as u32, + } +} + +pub type SessionStatsResponse = FormattedResponse<SessionStats>; diff --git a/nym-node/src/node/http/router/api/v1/metrics/verloc.rs b/nym-node/src/node/http/router/api/v1/metrics/verloc.rs new file mode 100644 index 00000000000..1a15986373c --- /dev/null +++ b/nym-node/src/node/http/router/api/v1/metrics/verloc.rs @@ -0,0 +1,73 @@ +// Copyright 2024 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: GPL-3.0-only + +use crate::node::http::state::metrics::MetricsAppState; +use axum::extract::{Query, State}; +use nym_http_api_common::{FormattedResponse, OutputParams}; +use nym_node_requests::api::v1::metrics::models::{ + VerlocMeasurement, VerlocNodeResult, VerlocResult, VerlocResultData, VerlocStats, +}; +use nym_verloc::measurements::SharedVerlocStats; + +/// If applicable, returns verloc statistics information of this node. +#[utoipa::path( + get, + path = "/verloc", + context_path = "/api/v1/metrics", + tag = "Metrics", + responses( + (status = 200, content( + ("application/json" = VerlocStats), + ("application/yaml" = VerlocStats) + )) + ), + params(OutputParams), +)] +pub(crate) async fn verloc_stats( + Query(output): Query<OutputParams>, + State(metrics_state): State<MetricsAppState>, +) -> VerlocStatsResponse { + let output = output.output.unwrap_or_default(); + output.to_response(build_response(&metrics_state.verloc).await) +} + +async fn build_response(verloc_stats: &SharedVerlocStats) -> VerlocStats { + fn verloc_result_to_response(data: &nym_verloc::models::VerlocResultData) -> VerlocResultData { + VerlocResultData { + nodes_tested: data.nodes_tested, + run_started: data.run_started, + run_finished: data.run_finished, + results: data + .results + .iter() + .map(|r| VerlocNodeResult { + node_identity: r.node_identity, + latest_measurement: r.latest_measurement.map(|l| VerlocMeasurement { + minimum: l.minimum, + mean: l.mean, + maximum: l.maximum, + standard_deviation: l.standard_deviation, + }), + }) + .collect(), + } + } + + let guard = verloc_stats.read().await; + + let previous = if !guard.previous_run_data.run_finished() { + VerlocResult::Unavailable + } else { + VerlocResult::Data(verloc_result_to_response(&guard.previous_run_data)) + }; + + let current = if !guard.current_run_data.run_finished() { + VerlocResult::MeasurementInProgress + } else { + VerlocResult::Data(verloc_result_to_response(&guard.previous_run_data)) + }; + + VerlocStats { previous, current } +} + +pub type VerlocStatsResponse = FormattedResponse<VerlocStats>; diff --git a/nym-node/nym-node-http-api/src/router/api/v1/mixnode/mod.rs b/nym-node/src/node/http/router/api/v1/mixnode/mod.rs similarity index 100% rename from nym-node/nym-node-http-api/src/router/api/v1/mixnode/mod.rs rename to nym-node/src/node/http/router/api/v1/mixnode/mod.rs diff --git a/nym-node/nym-node-http-api/src/router/api/v1/mixnode/root.rs b/nym-node/src/node/http/router/api/v1/mixnode/root.rs similarity index 94% rename from nym-node/nym-node-http-api/src/router/api/v1/mixnode/root.rs rename to nym-node/src/node/http/router/api/v1/mixnode/root.rs index d7f56ba3e01..4f1133ebc3c 100644 --- a/nym-node/nym-node-http-api/src/router/api/v1/mixnode/root.rs +++ b/nym-node/src/node/http/router/api/v1/mixnode/root.rs @@ -1,9 +1,9 @@ // Copyright 2023 - Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: GPL-3.0-only -use crate::router::api::{FormattedResponse, OutputParams}; use axum::extract::Query; use axum::http::StatusCode; +use nym_http_api_common::{FormattedResponse, OutputParams}; use nym_node_requests::api::v1::mixnode::models::Mixnode; /// Returns root mixnode information diff --git a/nym-node/nym-node-http-api/src/router/api/v1/mod.rs b/nym-node/src/node/http/router/api/v1/mod.rs similarity index 97% rename from nym-node/nym-node-http-api/src/router/api/v1/mod.rs rename to nym-node/src/node/http/router/api/v1/mod.rs index f25b7cc8013..41ccef50cd3 100644 --- a/nym-node/nym-node-http-api/src/router/api/v1/mod.rs +++ b/nym-node/src/node/http/router/api/v1/mod.rs @@ -1,7 +1,7 @@ // Copyright 2023 - Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: GPL-3.0-only -use crate::state::AppState; +use crate::node::http::state::AppState; use axum::routing::get; use axum::Router; use nym_node_requests::routes::api::v1; diff --git a/nym-node/nym-node-http-api/src/router/api/v1/network_requester/exit_policy.rs b/nym-node/src/node/http/router/api/v1/network_requester/exit_policy.rs similarity index 93% rename from nym-node/nym-node-http-api/src/router/api/v1/network_requester/exit_policy.rs rename to nym-node/src/node/http/router/api/v1/network_requester/exit_policy.rs index 70bc69ec62d..a58cd8862c2 100644 --- a/nym-node/nym-node-http-api/src/router/api/v1/network_requester/exit_policy.rs +++ b/nym-node/src/node/http/router/api/v1/network_requester/exit_policy.rs @@ -1,8 +1,8 @@ // Copyright 2023 - Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: GPL-3.0-only -use crate::api::{FormattedResponse, OutputParams}; use axum::extract::Query; +use nym_http_api_common::{FormattedResponse, OutputParams}; use nym_node_requests::api::v1::network_requester::exit_policy::models::UsedExitPolicy; /// Returns information about the exit policy used by this node. diff --git a/nym-node/nym-node-http-api/src/router/api/v1/network_requester/mod.rs b/nym-node/src/node/http/router/api/v1/network_requester/mod.rs similarity index 93% rename from nym-node/nym-node-http-api/src/router/api/v1/network_requester/mod.rs rename to nym-node/src/node/http/router/api/v1/network_requester/mod.rs index 917b5a668b2..5524b515084 100644 --- a/nym-node/nym-node-http-api/src/router/api/v1/network_requester/mod.rs +++ b/nym-node/src/node/http/router/api/v1/network_requester/mod.rs @@ -1,7 +1,7 @@ // Copyright 2023 - Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: GPL-3.0-only -use crate::api::v1::network_requester::exit_policy::node_exit_policy; +use crate::node::http::api::v1::network_requester::exit_policy::node_exit_policy; use axum::routing::get; use axum::Router; use nym_node_requests::api::v1::network_requester::exit_policy::models::UsedExitPolicy; diff --git a/nym-node/nym-node-http-api/src/router/api/v1/network_requester/root.rs b/nym-node/src/node/http/router/api/v1/network_requester/root.rs similarity index 94% rename from nym-node/nym-node-http-api/src/router/api/v1/network_requester/root.rs rename to nym-node/src/node/http/router/api/v1/network_requester/root.rs index ac92133e533..426614b07f7 100644 --- a/nym-node/nym-node-http-api/src/router/api/v1/network_requester/root.rs +++ b/nym-node/src/node/http/router/api/v1/network_requester/root.rs @@ -1,9 +1,9 @@ // Copyright 2023 - Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: GPL-3.0-only -use crate::router::api::{FormattedResponse, OutputParams}; use axum::extract::Query; use axum::http::StatusCode; +use nym_http_api_common::{FormattedResponse, OutputParams}; use nym_node_requests::api::v1::network_requester::models::NetworkRequester; /// Returns root network requester information diff --git a/nym-node/nym-node-http-api/src/router/api/v1/node/auxiliary.rs b/nym-node/src/node/http/router/api/v1/node/auxiliary.rs similarity index 88% rename from nym-node/nym-node-http-api/src/router/api/v1/node/auxiliary.rs rename to nym-node/src/node/http/router/api/v1/node/auxiliary.rs index bc4d4dae941..9db2b5f6dd8 100644 --- a/nym-node/nym-node-http-api/src/router/api/v1/node/auxiliary.rs +++ b/nym-node/src/node/http/router/api/v1/node/auxiliary.rs @@ -1,9 +1,9 @@ // Copyright 2024 - Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: GPL-3.0-only -use crate::api::{FormattedResponse, OutputParams}; -use crate::router::types::RequestError; +use crate::node::http::router::types::RequestError; use axum::extract::Query; +use nym_http_api_common::{FormattedResponse, OutputParams}; use nym_node_requests::api::v1::node::models::AuxiliaryDetails; /// Returns auxiliary details of this node. diff --git a/nym-node/nym-node-http-api/src/router/api/v1/node/build_information.rs b/nym-node/src/node/http/router/api/v1/node/build_information.rs similarity index 93% rename from nym-node/nym-node-http-api/src/router/api/v1/node/build_information.rs rename to nym-node/src/node/http/router/api/v1/node/build_information.rs index b877f43d175..6073482c3e6 100644 --- a/nym-node/nym-node-http-api/src/router/api/v1/node/build_information.rs +++ b/nym-node/src/node/http/router/api/v1/node/build_information.rs @@ -1,8 +1,8 @@ // Copyright 2023 - Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: GPL-3.0-only -use crate::router::api::{FormattedResponse, OutputParams}; use axum::extract::Query; +use nym_http_api_common::{FormattedResponse, OutputParams}; use nym_node_requests::api::v1::node::models::BinaryBuildInformationOwned; /// Returns build metadata of the binary running the API diff --git a/nym-node/nym-node-http-api/src/router/api/v1/node/description.rs b/nym-node/src/node/http/router/api/v1/node/description.rs similarity index 88% rename from nym-node/nym-node-http-api/src/router/api/v1/node/description.rs rename to nym-node/src/node/http/router/api/v1/node/description.rs index f49d311505d..ff569c78fbe 100644 --- a/nym-node/nym-node-http-api/src/router/api/v1/node/description.rs +++ b/nym-node/src/node/http/router/api/v1/node/description.rs @@ -1,9 +1,9 @@ // Copyright 2024 - Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: GPL-3.0-only -use crate::api::{FormattedResponse, OutputParams}; -use crate::router::types::RequestError; +use crate::node::http::router::types::RequestError; use axum::extract::Query; +use nym_http_api_common::{FormattedResponse, OutputParams}; use nym_node_requests::api::v1::node::models::NodeDescription; /// Returns human-readable description of this node. diff --git a/nym-node/nym-node-http-api/src/router/api/v1/node/hardware.rs b/nym-node/src/node/http/router/api/v1/node/hardware.rs similarity index 91% rename from nym-node/nym-node-http-api/src/router/api/v1/node/hardware.rs rename to nym-node/src/node/http/router/api/v1/node/hardware.rs index 2d17b9e3a20..969e93458f0 100644 --- a/nym-node/nym-node-http-api/src/router/api/v1/node/hardware.rs +++ b/nym-node/src/node/http/router/api/v1/node/hardware.rs @@ -1,10 +1,10 @@ // Copyright 2024 - Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: GPL-3.0-only -use crate::api::{FormattedResponse, OutputParams}; -use crate::router::types::RequestError; +use crate::node::http::router::types::RequestError; use axum::extract::Query; use axum::http::StatusCode; +use nym_http_api_common::{FormattedResponse, OutputParams}; use nym_node_requests::api::v1::node::models::HostSystem; /// Returns build system information of the host running the binary. diff --git a/nym-node/nym-node-http-api/src/router/api/v1/node/host_information.rs b/nym-node/src/node/http/router/api/v1/node/host_information.rs similarity index 93% rename from nym-node/nym-node-http-api/src/router/api/v1/node/host_information.rs rename to nym-node/src/node/http/router/api/v1/node/host_information.rs index b4d7f6d363e..d6cea0acde6 100644 --- a/nym-node/nym-node-http-api/src/router/api/v1/node/host_information.rs +++ b/nym-node/src/node/http/router/api/v1/node/host_information.rs @@ -1,8 +1,8 @@ // Copyright 2023 - Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: GPL-3.0-only -use crate::api::{FormattedResponse, OutputParams}; use axum::extract::Query; +use nym_http_api_common::{FormattedResponse, OutputParams}; use nym_node_requests::api::v1::node::models::SignedHostInformation; /// Returns host information of this node. diff --git a/nym-node/nym-node-http-api/src/router/api/v1/node/mod.rs b/nym-node/src/node/http/router/api/v1/node/mod.rs similarity index 84% rename from nym-node/nym-node-http-api/src/router/api/v1/node/mod.rs rename to nym-node/src/node/http/router/api/v1/node/mod.rs index 6589b57aeb6..f62b17ab3b7 100644 --- a/nym-node/nym-node-http-api/src/router/api/v1/node/mod.rs +++ b/nym-node/src/node/http/router/api/v1/node/mod.rs @@ -1,12 +1,12 @@ // Copyright 2023 - Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: GPL-3.0-only -use crate::api::v1::node::auxiliary::auxiliary; -use crate::api::v1::node::build_information::build_information; -use crate::api::v1::node::description::description; -use crate::api::v1::node::hardware::host_system; -use crate::api::v1::node::host_information::host_information; -use crate::api::v1::node::roles::roles; +use crate::node::http::api::v1::node::auxiliary::auxiliary; +use crate::node::http::api::v1::node::build_information::build_information; +use crate::node::http::api::v1::node::description::description; +use crate::node::http::api::v1::node::hardware::host_system; +use crate::node::http::api::v1::node::host_information::host_information; +use crate::node::http::api::v1::node::roles::roles; use axum::routing::get; use axum::Router; use nym_node_requests::api::v1::node::models; diff --git a/nym-node/nym-node-http-api/src/router/api/v1/node/roles.rs b/nym-node/src/node/http/router/api/v1/node/roles.rs similarity index 92% rename from nym-node/nym-node-http-api/src/router/api/v1/node/roles.rs rename to nym-node/src/node/http/router/api/v1/node/roles.rs index 145b13798dd..40b98d868af 100644 --- a/nym-node/nym-node-http-api/src/router/api/v1/node/roles.rs +++ b/nym-node/src/node/http/router/api/v1/node/roles.rs @@ -1,8 +1,8 @@ // Copyright 2023 - Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: GPL-3.0-only -use crate::router::api::{FormattedResponse, OutputParams}; use axum::extract::Query; +use nym_http_api_common::{FormattedResponse, OutputParams}; use nym_node_requests::api::v1::node::models::NodeRoles; /// Returns roles supported by this node diff --git a/nym-node/nym-node-http-api/src/router/api/v1/openapi.rs b/nym-node/src/node/http/router/api/v1/openapi.rs similarity index 91% rename from nym-node/nym-node-http-api/src/router/api/v1/openapi.rs rename to nym-node/src/node/http/router/api/v1/openapi.rs index a0ef945f38e..a46812464f3 100644 --- a/nym-node/nym-node-http-api/src/router/api/v1/openapi.rs +++ b/nym-node/src/node/http/router/api/v1/openapi.rs @@ -1,8 +1,8 @@ // Copyright 2023 - Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: GPL-3.0-only -use crate::router::api; -use crate::router::types::{ErrorResponse, RequestError}; +use crate::node::http::router::api; +use crate::node::http::router::types::{ErrorResponse, RequestError}; use axum::Router; use nym_node_requests::api as api_requests; use nym_node_requests::routes::api::{v1, v1_absolute}; @@ -20,7 +20,8 @@ use utoipa_swagger_ui::SwaggerUi; api::v1::node::hardware::host_system, api::v1::node::description::description, api::v1::node::auxiliary::auxiliary, - api::v1::metrics::mixing::mixing_stats, + api::v1::metrics::legacy_mixing::legacy_mixing_stats, + api::v1::metrics::packets_stats::packets_stats, api::v1::metrics::verloc::verloc_stats, api::v1::metrics::prometheus::prometheus_metrics, api::v1::health::root_health, @@ -35,8 +36,8 @@ use utoipa_swagger_ui::SwaggerUi; components( schemas( ErrorResponse, - api::Output, - api::OutputParams, + nym_http_api_common::Output, + nym_http_api_common::OutputParams, api_requests::v1::health::models::NodeHealth, api_requests::v1::health::models::NodeStatus, api_requests::v1::node::models::BinaryBuildInformationOwned, @@ -50,7 +51,7 @@ use utoipa_swagger_ui::SwaggerUi; api_requests::v1::node::models::CryptoHardware, api_requests::v1::node::models::NodeDescription, api_requests::v1::node::models::AuxiliaryDetails, - api_requests::v1::metrics::models::MixingStats, + api_requests::v1::metrics::models::LegacyMixingStats, api_requests::v1::metrics::models::VerlocStats, api_requests::v1::metrics::models::VerlocResult, api_requests::v1::metrics::models::VerlocResultData, diff --git a/nym-node/nym-node-http-api/src/router/landing_page.rs b/nym-node/src/node/http/router/landing_page.rs similarity index 100% rename from nym-node/nym-node-http-api/src/router/landing_page.rs rename to nym-node/src/node/http/router/landing_page.rs diff --git a/nym-node/nym-node-http-api/src/router/mod.rs b/nym-node/src/node/http/router/mod.rs similarity index 67% rename from nym-node/nym-node-http-api/src/router/mod.rs rename to nym-node/src/node/http/router/mod.rs index 5721274174f..449638c8d6c 100644 --- a/nym-node/nym-node-http-api/src/router/mod.rs +++ b/nym-node/src/node/http/router/mod.rs @@ -1,48 +1,44 @@ // Copyright 2023 - Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: GPL-3.0-only -use crate::error::NymNodeHttpError; -use crate::middleware::logging; -use crate::state::AppState; -use crate::NymNodeHTTPServer; +use crate::node::http::error::NymNodeHttpError; +use crate::node::http::state::AppState; +use crate::node::http::NymNodeHttpServer; use axum::response::Redirect; use axum::routing::get; use axum::Router; +use nym_bin_common::bin_info_owned; +use nym_http_api_common::middleware::logging; use nym_node_requests::api::v1::authenticator::models::Authenticator; -use nym_node_requests::api::v1::gateway::models::{Gateway, Wireguard}; +use nym_node_requests::api::v1::gateway::models::Gateway; use nym_node_requests::api::v1::ip_packet_router::models::IpPacketRouter; use nym_node_requests::api::v1::mixnode::models::Mixnode; use nym_node_requests::api::v1::network_requester::exit_policy::models::UsedExitPolicy; use nym_node_requests::api::v1::network_requester::models::NetworkRequester; -use nym_node_requests::api::v1::node::models; use nym_node_requests::api::v1::node::models::{AuxiliaryDetails, HostSystem, NodeDescription}; use nym_node_requests::api::SignedHostInformation; use nym_node_requests::routes; use std::net::SocketAddr; use std::path::Path; -use tracing::warn; pub mod api; pub mod landing_page; pub mod types; #[derive(Debug, Clone)] -pub struct Config { +pub struct HttpServerConfig { pub landing: landing_page::Config, pub api: api::Config, } -impl Config { - pub fn new( - build_information: models::BinaryBuildInformationOwned, - host_information: SignedHostInformation, - ) -> Self { - Config { +impl HttpServerConfig { + pub fn new(host_information: SignedHostInformation) -> Self { + HttpServerConfig { landing: Default::default(), api: api::Config { v1_config: api::v1::Config { node: api::v1::node::Config { - build_information, + build_information: bin_info_owned!(), host_information, system_info: None, roles: Default::default(), @@ -60,18 +56,6 @@ impl Config { } } - pub fn with_wireguard_interface(mut self, wireguard: Wireguard) -> Self { - match &mut self.api.v1_config.gateway.details { - Some(gw) => gw.client_interfaces.wireguard = Some(wireguard), - None => { - warn!( - "can't add wireguard interface information as the gateway role is not enabled." - ); - } - } - self - } - #[must_use] pub fn with_landing_page_assets<P: AsRef<Path>>(mut self, assets_path: Option<P>) -> Self { self.landing.assets_path = assets_path.map(|p| p.as_ref().to_path_buf()); @@ -96,36 +80,18 @@ impl Config { self } - #[must_use] - pub fn with_gateway(mut self, gateway: Gateway) -> Self { - self.api.v1_config.node.roles.gateway_enabled = true; - self.with_gateway_details(gateway) - } - #[must_use] pub fn with_gateway_details(mut self, gateway: Gateway) -> Self { self.api.v1_config.gateway.details = Some(gateway); self } - #[must_use] - pub fn with_mixnode(mut self, mixnode: Mixnode) -> Self { - self.api.v1_config.node.roles.mixnode_enabled = true; - self.with_mixnode_details(mixnode) - } - #[must_use] pub fn with_mixnode_details(mut self, mixnode: Mixnode) -> Self { self.api.v1_config.mixnode.details = Some(mixnode); self } - #[must_use] - pub fn with_network_requester(mut self, network_requester: NetworkRequester) -> Self { - self.api.v1_config.node.roles.network_requester_enabled = true; - self.with_network_requester_details(network_requester) - } - #[must_use] pub fn with_network_requester_details(mut self, network_requester: NetworkRequester) -> Self { self.api.v1_config.network_requester.details = Some(network_requester); @@ -138,12 +104,6 @@ impl Config { self } - #[must_use] - pub fn with_ip_packet_router(mut self, ip_packet_router: IpPacketRouter) -> Self { - self.api.v1_config.node.roles.ip_packet_router_enabled = true; - self.with_ip_packet_router_details(ip_packet_router) - } - #[must_use] pub fn with_ip_packet_router_details(mut self, ip_packet_router: IpPacketRouter) -> Self { self.api.v1_config.ip_packet_router.details = Some(ip_packet_router); @@ -162,10 +122,7 @@ pub struct NymNodeRouter { } impl NymNodeRouter { - // TODO: move the wg state to a builder - pub fn new(config: Config, app_state: Option<AppState>) -> NymNodeRouter { - let state = app_state.unwrap_or(AppState::new()); - + pub fn new(config: HttpServerConfig, state: AppState) -> NymNodeRouter { NymNodeRouter { inner: Router::new() // redirection for old legacy mixnode routes @@ -179,7 +136,9 @@ impl NymNodeRouter { ) .route( "/stats", - get(|| async { Redirect::to(&routes::api::v1::metrics::mixing_absolute()) }), + get(|| async { + Redirect::to(&routes::api::v1::metrics::legacy_mixing_absolute()) + }), ) .route( "/verloc", @@ -198,23 +157,10 @@ impl NymNodeRouter { } } - // this is only a temporary method until everything is properly moved into the nym-node itself - #[must_use] - pub fn with_route(mut self, path: &str, router: Router) -> Self { - self.inner = self.inner.nest(path, router); - self - } - - #[must_use] - pub fn with_merged(mut self, router: Router) -> Self { - self.inner = self.inner.merge(router); - self - } - pub async fn build_server( self, bind_address: &SocketAddr, - ) -> Result<NymNodeHTTPServer, NymNodeHttpError> { + ) -> Result<NymNodeHttpServer, NymNodeHttpError> { let listener = tokio::net::TcpListener::bind(bind_address) .await .map_err(|source| NymNodeHttpError::HttpBindFailure { @@ -228,6 +174,6 @@ impl NymNodeRouter { .into_make_service_with_connect_info::<SocketAddr>(), ); - Ok(NymNodeHTTPServer::new(axum_server)) + Ok(NymNodeHttpServer::new(axum_server)) } } diff --git a/nym-node/nym-node-http-api/src/router/types.rs b/nym-node/src/node/http/router/types.rs similarity index 100% rename from nym-node/nym-node-http-api/src/router/types.rs rename to nym-node/src/node/http/router/types.rs diff --git a/nym-node/src/node/http/state/metrics.rs b/nym-node/src/node/http/state/metrics.rs new file mode 100644 index 00000000000..c9156c021e4 --- /dev/null +++ b/nym-node/src/node/http/state/metrics.rs @@ -0,0 +1,23 @@ +// Copyright 2024 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: GPL-3.0-only + +use crate::node::http::state::AppState; +use axum::extract::FromRef; +use nym_node_metrics::NymNodeMetrics; + +pub use nym_verloc::measurements::metrics::SharedVerlocStats; + +#[derive(Clone)] +pub struct MetricsAppState { + pub(crate) prometheus_access_token: Option<String>, + + pub(crate) metrics: NymNodeMetrics, + + pub(crate) verloc: SharedVerlocStats, +} + +impl FromRef<AppState> for MetricsAppState { + fn from_ref(app_state: &AppState) -> Self { + app_state.metrics.clone() + } +} diff --git a/nym-node/src/node/http/state/mod.rs b/nym-node/src/node/http/state/mod.rs new file mode 100644 index 00000000000..b9b58c0cce0 --- /dev/null +++ b/nym-node/src/node/http/state/mod.rs @@ -0,0 +1,40 @@ +// Copyright 2023-2024 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: GPL-3.0-only + +use crate::node::http::state::metrics::MetricsAppState; +use nym_node_metrics::NymNodeMetrics; +use nym_verloc::measurements::SharedVerlocStats; +use tokio::time::Instant; + +pub mod metrics; + +#[derive(Clone)] +pub struct AppState { + pub(crate) startup_time: Instant, + + pub(crate) metrics: MetricsAppState, +} + +impl AppState { + #[allow(clippy::new_without_default)] + pub fn new(metrics: NymNodeMetrics, verloc: SharedVerlocStats) -> Self { + AppState { + // is it 100% accurate? + // no. + // does it have to be? + // also no. + startup_time: Instant::now(), + metrics: MetricsAppState { + prometheus_access_token: None, + metrics, + verloc, + }, + } + } + + #[must_use] + pub fn with_metrics_key(mut self, bearer_token: impl Into<Option<String>>) -> Self { + self.metrics.prometheus_access_token = bearer_token.into(); + self + } +} diff --git a/nym-node/src/node/metrics/aggregator.rs b/nym-node/src/node/metrics/aggregator.rs new file mode 100644 index 00000000000..c166eb0d3cb --- /dev/null +++ b/nym-node/src/node/metrics/aggregator.rs @@ -0,0 +1,147 @@ +// Copyright 2024 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: GPL-3.0-only + +use crate::node::metrics::handler::{HandlerWrapper, MetricsHandler, RegistrableHandler}; +use futures::StreamExt; +use nym_node_metrics::events::{ + events_channels, MetricEventsReceiver, MetricEventsSender, MetricsEvent, +}; +use std::any; +use std::any::TypeId; +use std::collections::HashMap; +use std::ops::DerefMut; +use std::time::Duration; +use tokio::task::JoinHandle; +use tokio::time::{interval_at, Instant}; +use tracing::{debug, error, trace, warn}; + +pub(crate) struct MetricsAggregator { + // possible issue: this has to be low enough so that frequent handlers would be called sufficiently + // (if handler doesn't need an update, its internal methods won't be called so it's not going to be wasteful) + handlers_update_interval: Duration, + + registered_handlers: HashMap<TypeId, Box<dyn RegistrableHandler>>, + // registered_handlers: HashMap<TypeId, Box<dyn Any + Send + Sync + 'static>>, + event_sender: MetricEventsSender, + event_receiver: MetricEventsReceiver, + shutdown: nym_task::TaskClient, +} + +impl MetricsAggregator { + pub fn new(handlers_update_interval: Duration, shutdown: nym_task::TaskClient) -> Self { + let (event_sender, event_receiver) = events_channels(); + + MetricsAggregator { + handlers_update_interval, + registered_handlers: Default::default(), + event_sender, + event_receiver, + shutdown, + } + } + + pub fn sender(&self) -> MetricEventsSender { + self.event_sender.clone() + } + + pub fn register_handler<H>(&mut self, handler: H, update_interval: Duration) + where + H: MetricsHandler, + { + let events_name = any::type_name::<H::Events>(); + let handler_name = any::type_name::<H>(); + + debug!("registering handler '{handler_name}' for events of type '{events_name}'"); + + let type_id = TypeId::of::<H::Events>(); + if self.registered_handlers.contains_key(&type_id) { + panic!("duplicate handler for '{events_name}' (id: {type_id:?})",) + }; + + self.registered_handlers.insert( + type_id, + Box::new(HandlerWrapper::new(update_interval, handler)), + ); + } + + async fn periodic_handlers_update(&mut self) { + for handler in self.registered_handlers.values_mut() { + handler.on_update().await; + } + } + + async fn handle_metrics_event<T: 'static>(&mut self, event: T) { + let Some(handler) = self.registered_handlers.get_mut(&TypeId::of::<T>()) else { + let name = any::type_name::<T>(); + + warn!("no registered handler for events of type {name}"); + return; + }; + + #[allow(clippy::expect_used)] + let handler: &mut HandlerWrapper<T> = handler + .deref_mut() + .as_any_mut() + .downcast_mut() + .expect("handler downcasting failure"); + + handler.handle_event(event).await; + } + + async fn handle_event(&mut self, event: MetricsEvent) { + match event { + MetricsEvent::GatewayClientSession(client_session) => { + self.handle_metrics_event(client_session).await + } + } + } + + async fn on_start(&mut self) { + for handler in self.registered_handlers.values_mut() { + handler.on_start().await; + } + } + + pub async fn run(&mut self) { + self.on_start().await; + + let start = Instant::now() + self.handlers_update_interval; + let mut update_interval = interval_at(start, self.handlers_update_interval); + + let mut processed = 0; + trace!("starting MetricsAggregator"); + loop { + tokio::select! { + biased; + _ = self.shutdown.recv() => { + debug!("MetricsAggregator: Received shutdown"); + break; + } + _ = update_interval.tick() => { + self.periodic_handlers_update().await; + } + new_event = self.event_receiver.next() => { + // this one is impossible to ever panic - the struct itself contains a sender + // and hence it can't happen that ALL senders are dropped + #[allow(clippy::unwrap_used)] + self.handle_event(new_event.unwrap()).await; + if processed % 1000 == 0 { + let queue_len = self.event_sender.len(); + match queue_len { + n if n > 200 => error!("there are currently {n} pending events waiting to get processed!"), + n if n > 50 => warn!("there are currently {n} pending events waiting to get processed"), + n => trace!("there are currently {n} pending events waiting to get processed"), + } + } + processed += 1; + } + + } + } + trace!("MetricsAggregator: Exiting"); + } + + pub fn start(mut self) -> JoinHandle<()> { + tokio::spawn(async move { self.run().await }) + } +} diff --git a/nym-node/src/node/metrics/console_logger.rs b/nym-node/src/node/metrics/console_logger.rs new file mode 100644 index 00000000000..ea3d11e94c6 --- /dev/null +++ b/nym-node/src/node/metrics/console_logger.rs @@ -0,0 +1,120 @@ +// Copyright 2024 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: GPL-3.0-only + +use human_repr::HumanCount; +use human_repr::HumanThroughput; +use nym_node_metrics::NymNodeMetrics; +use nym_task::TaskClient; +use std::time::Duration; +use time::OffsetDateTime; +use tokio::task::JoinHandle; +use tokio::time::{interval_at, Instant}; +use tracing::{info, trace}; + +struct AtLastUpdate { + time: OffsetDateTime, + + // INGRESS + forward_hop_packets_received: usize, + + // INGRESS + final_hop_packets_received: usize, + + // EGRESS + forward_hop_packets_sent: usize, + + // EGRESS + ack_packets_sent: usize, +} + +impl AtLastUpdate { + fn new() -> Self { + Self { + time: OffsetDateTime::now_utc(), + forward_hop_packets_received: 0, + final_hop_packets_received: 0, + forward_hop_packets_sent: 0, + ack_packets_sent: 0, + } + } +} + +// replicate behaviour from old mixnode to log number of mixed packets +pub(crate) struct ConsoleLogger { + logging_delay: Duration, + at_last_update: AtLastUpdate, + metrics: NymNodeMetrics, + shutdown: TaskClient, +} + +impl ConsoleLogger { + pub(crate) fn new( + logging_delay: Duration, + metrics: NymNodeMetrics, + shutdown: TaskClient, + ) -> Self { + ConsoleLogger { + logging_delay, + at_last_update: AtLastUpdate::new(), + metrics, + shutdown, + } + } + + async fn log_running_stats(&mut self) { + let now = OffsetDateTime::now_utc(); + let delta_secs = (now - self.at_last_update.time).as_seconds_f64(); + + let forward_received = self.metrics.mixnet.ingress.forward_hop_packets_received(); + let final_received = self.metrics.mixnet.ingress.final_hop_packets_received(); + let forward_sent = self.metrics.mixnet.egress.forward_hop_packets_sent(); + let acks = self.metrics.mixnet.egress.ack_packets_sent(); + + let forward_received_rate = + (forward_received - self.at_last_update.forward_hop_packets_received) as f64 + / delta_secs; + let final_rate = + (final_received - self.at_last_update.final_hop_packets_received) as f64 / delta_secs; + let forward_sent_rate = + (forward_sent - self.at_last_update.forward_hop_packets_sent) as f64 / delta_secs; + let acks_rate = (acks - self.at_last_update.ack_packets_sent) as f64 / delta_secs; + + info!("↑↓ Packets sent [total] / sent [acks] / received [mix] / received [gw]: {} ({}) / {} ({}) / {} ({}) / {} ({})", + forward_sent.human_count_bare(), + forward_sent_rate.human_throughput_bare(), + acks.human_count_bare(), + acks_rate.human_throughput_bare(), + forward_received.human_count_bare(), + forward_received_rate.human_throughput_bare(), + final_received.human_count_bare(), + final_rate.human_throughput_bare(), + ); + + self.at_last_update.time = now; + self.at_last_update.forward_hop_packets_received = forward_received; + self.at_last_update.final_hop_packets_received = final_received; + self.at_last_update.forward_hop_packets_sent = forward_sent; + self.at_last_update.ack_packets_sent = acks; + + // TODO: add websocket-client traffic + } + + async fn run(&mut self) { + trace!("Starting ConsoleLogger"); + let mut interval = interval_at(Instant::now() + self.logging_delay, self.logging_delay); + while !self.shutdown.is_shutdown() { + tokio::select! { + biased; + _ = self.shutdown.recv() => { + trace!("ConsoleLogger: Received shutdown"); + } + _ = interval.tick() => self.log_running_stats().await, + }; + } + trace!("ConsoleLogger: Exiting"); + } + + pub(crate) fn start(mut self) -> JoinHandle<()> { + tokio::spawn(async move { self.run().await }) + } +} diff --git a/nym-node/src/node/metrics/events_listener.rs b/nym-node/src/node/metrics/events_listener.rs new file mode 100644 index 00000000000..755fb6cc8b8 --- /dev/null +++ b/nym-node/src/node/metrics/events_listener.rs @@ -0,0 +1,2 @@ +// Copyright 2024 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: Apache-2.0 diff --git a/gateway/src/node/statistics/sessions.rs b/nym-node/src/node/metrics/handler/client_sessions.rs similarity index 55% rename from gateway/src/node/statistics/sessions.rs rename to nym-node/src/node/metrics/handler/client_sessions.rs index eca940a21d5..5f9e870a2a0 100644 --- a/gateway/src/node/statistics/sessions.rs +++ b/nym-node/src/node/metrics/handler/client_sessions.rs @@ -1,52 +1,37 @@ -// Copyright 2022 - Nym Technologies SA <contact@nymtech.net> -// SPDX-License-Identifier: GPL-3.0-only - -use nym_credentials_interface::TicketType; -use nym_gateway_stats_storage::models::FinishedSession; -use nym_gateway_stats_storage::PersistentStatsStorage; -use nym_gateway_stats_storage::{error::StatsStorageError, models::ActiveSession}; -use nym_node_http_api::state::metrics::SharedSessionStats; -use nym_sphinx::DestinationAddressBytes; -use sha2::{Digest, Sha256}; +// Copyright 2024 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: Apache-2.0 + +use crate::node::metrics::handler::{ + MetricsHandler, OnStartMetricsHandler, OnUpdateMetricsHandler, +}; +use async_trait::async_trait; +use nym_gateway::node::PersistentStatsStorage; +use nym_gateway_stats_storage::error::StatsStorageError; +use nym_gateway_stats_storage::models::{TicketType, ToSessionType}; +use nym_node_metrics::entry::{ActiveSession, ClientSessions, FinishedSession}; +use nym_node_metrics::events::GatewaySessionEvent; +use nym_node_metrics::NymNodeMetrics; +use nym_sphinx_types::DestinationAddressBytes; use time::{Date, Duration, OffsetDateTime}; +use tracing::error; +use tracing::log::trace; -use nym_statistics_common::gateways::SessionEvent; - -pub(crate) struct SessionStatsHandler { +pub(crate) struct GatewaySessionStatsHandler { storage: PersistentStatsStorage, current_day: Date, - shared_session_stats: SharedSessionStats, + metrics: NymNodeMetrics, } -impl SessionStatsHandler { - pub fn new(shared_session_stats: SharedSessionStats, storage: PersistentStatsStorage) -> Self { - SessionStatsHandler { +impl GatewaySessionStatsHandler { + pub(crate) fn new(metrics: NymNodeMetrics, storage: PersistentStatsStorage) -> Self { + GatewaySessionStatsHandler { storage, current_day: OffsetDateTime::now_utc().date(), - shared_session_stats, + metrics, } } - pub(crate) async fn handle_event( - &mut self, - event: SessionEvent, - ) -> Result<(), StatsStorageError> { - match event { - SessionEvent::SessionStart { start_time, client } => { - self.handle_session_start(start_time, client).await - } - - SessionEvent::SessionStop { stop_time, client } => { - self.handle_session_stop(stop_time, client).await - } - - SessionEvent::EcashTicket { - ticket_type, - client, - } => self.handle_ecash_ticket(ticket_type, client).await, - } - } async fn handle_session_start( &mut self, start_time: OffsetDateTime, @@ -83,62 +68,38 @@ impl SessionStatsHandler { client: DestinationAddressBytes, ) -> Result<(), StatsStorageError> { self.storage - .update_active_session_type(client, ticket_type.into()) + .update_active_session_type(client, ticket_type.to_session_type()) .await?; Ok(()) } - pub(crate) async fn on_start(&mut self) -> Result<(), StatsStorageError> { - let yesterday = OffsetDateTime::now_utc().date() - Duration::DAY; - //publish yesterday's data if any - self.publish_stats(yesterday).await?; - //store "active" sessions as duration 0 - for active_session in self.storage.get_all_active_sessions().await? { - self.storage - .insert_finished_session( - self.current_day, - FinishedSession { - duration: Duration::ZERO, - typ: active_session.typ, - }, - ) - .await? - } - //cleanup active sessions - self.storage.cleanup_active_sessions().await?; + async fn handle_session_event( + &mut self, + event: GatewaySessionEvent, + ) -> Result<(), StatsStorageError> { + match event { + GatewaySessionEvent::SessionStart { start_time, client } => { + self.handle_session_start(start_time, client).await + } - //delete old entries - self.delete_old_stats(yesterday - Duration::DAY).await?; - Ok(()) - } + GatewaySessionEvent::SessionStop { stop_time, client } => { + self.handle_session_stop(stop_time, client).await + } - //update shared state once a day has passed, with data from the previous day - async fn publish_stats(&mut self, stats_date: Date) -> Result<(), StatsStorageError> { - let finished_sessions = self.storage.get_finished_sessions(stats_date).await?; - let unique_users = self.storage.get_unique_users(stats_date).await?; - let unique_users_hash = unique_users - .into_iter() - .map(|address| format!("{:x}", Sha256::digest(address))) - .collect::<Vec<_>>(); - let session_started = self.storage.get_started_sessions_count(stats_date).await? as u32; - { - let mut shared_state = self.shared_session_stats.write().await; - shared_state.update_time = stats_date; - shared_state.unique_active_users_count = unique_users_hash.len() as u32; - shared_state.unique_active_users_hashes = unique_users_hash; - shared_state.session_started = session_started; - shared_state.sessions = finished_sessions.iter().map(|s| s.serialize()).collect(); + GatewaySessionEvent::EcashTicket { + ticket_type, + client, + } => self.handle_ecash_ticket(ticket_type, client).await, } - - Ok(()) } - pub(crate) async fn maybe_update_shared_state( + + async fn maybe_update_shared_state( &mut self, update_time: OffsetDateTime, ) -> Result<(), StatsStorageError> { let update_date = update_time.date(); if update_date != self.current_day { - self.publish_stats(self.current_day).await?; + self.update_shared_stats(self.current_day).await?; self.delete_old_stats(self.current_day - Duration::DAY) .await?; self.reset_stats(update_date).await?; @@ -147,6 +108,26 @@ impl SessionStatsHandler { Ok(()) } + // update shared state once a day has passed, with data from the previous day + async fn update_shared_stats(&mut self, stats_date: Date) -> Result<(), StatsStorageError> { + let finished_sessions = self.storage.get_finished_sessions(stats_date).await?; + let unique_users = self.storage.get_unique_users(stats_date).await?; + let session_started = self.storage.get_started_sessions_count(stats_date).await? as u32; + + let new_sessions = ClientSessions::new( + stats_date, + unique_users, + session_started, + finished_sessions.into_iter().map(Into::into).collect(), + ); + self.metrics + .entry + .update_client_sessions(new_sessions) + .await; + + Ok(()) + } + async fn reset_stats(&mut self, reset_day: Date) -> Result<(), StatsStorageError> { //active users reset let new_active_users = self.storage.get_active_users().await?; @@ -162,4 +143,56 @@ impl SessionStatsHandler { self.storage.delete_unique_users(delete_before).await?; Ok(()) } + + async fn cleanup(&mut self) -> Result<(), StatsStorageError> { + let yesterday = OffsetDateTime::now_utc().date() - Duration::DAY; + //publish yesterday's data if any + self.update_shared_stats(yesterday).await?; + //store "active" sessions as duration 0 + for active_session in self.storage.get_all_active_sessions().await? { + self.storage + .insert_finished_session( + self.current_day, + FinishedSession::new(Default::default(), active_session.typ), + ) + .await? + } + //cleanup active sessions + self.storage.cleanup_active_sessions().await?; + + //delete old entries + self.delete_old_stats(yesterday - Duration::DAY).await?; + Ok(()) + } +} + +#[async_trait] +impl OnStartMetricsHandler for GatewaySessionStatsHandler { + async fn on_start(&mut self) { + if let Err(err) = self.cleanup().await { + error!("failed to cleanup gateway session stats handler: {err}"); + } + } +} + +#[async_trait] +impl OnUpdateMetricsHandler for GatewaySessionStatsHandler { + async fn on_update(&mut self) { + let now = OffsetDateTime::now_utc(); + if let Err(err) = self.maybe_update_shared_state(now).await { + error!("failed to update session stats: {err}"); + } + } +} + +#[async_trait] +impl MetricsHandler for GatewaySessionStatsHandler { + type Events = GatewaySessionEvent; + + async fn handle_event(&mut self, event: Self::Events) { + trace!("event: {event:?}"); + if let Err(err) = self.handle_session_event(event).await { + error!("failed to handle client session event '{event:?}': {err}") + } + } } diff --git a/nym-node/src/node/metrics/handler/legacy_packet_data.rs b/nym-node/src/node/metrics/handler/legacy_packet_data.rs new file mode 100644 index 00000000000..4b649e831a1 --- /dev/null +++ b/nym-node/src/node/metrics/handler/legacy_packet_data.rs @@ -0,0 +1,68 @@ +// Copyright 2024 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: Apache-2.0 + +use crate::node::metrics::handler::{ + MetricsHandler, OnStartMetricsHandler, OnUpdateMetricsHandler, +}; +use async_trait::async_trait; +use nym_node_metrics::NymNodeMetrics; +use time::OffsetDateTime; + +// it can be anything, we just need a unique type_id to register our handler +pub struct LegacyMixingData; + +pub struct LegacyMixingStatsUpdater { + received_total_at_last_update: usize, + dropped_total_at_last_update: usize, + sent_total_at_last_update: usize, + + metrics: NymNodeMetrics, +} + +impl LegacyMixingStatsUpdater { + pub(crate) fn new(metrics: NymNodeMetrics) -> Self { + LegacyMixingStatsUpdater { + received_total_at_last_update: 0, + dropped_total_at_last_update: 0, + sent_total_at_last_update: 0, + metrics, + } + } +} + +#[async_trait] +impl OnStartMetricsHandler for LegacyMixingStatsUpdater {} + +#[async_trait] +impl OnUpdateMetricsHandler for LegacyMixingStatsUpdater { + async fn on_update(&mut self) { + let total_received = self.metrics.mixnet.ingress.forward_hop_packets_received(); + let total_dropped = self.metrics.mixnet.egress.forward_hop_packets_dropped(); + let total_sent = self.metrics.mixnet.egress.forward_hop_packets_sent(); + + let received_since_update = + total_received.saturating_sub(self.received_total_at_last_update); + let dropped_since_update = total_dropped.saturating_sub(self.dropped_total_at_last_update); + let sent_since_update = total_sent.saturating_sub(self.sent_total_at_last_update); + + self.received_total_at_last_update = total_received; + self.sent_total_at_last_update = total_sent; + self.dropped_total_at_last_update = total_dropped; + + self.metrics.mixnet.update_legacy_stats( + received_since_update, + sent_since_update, + dropped_since_update, + OffsetDateTime::now_utc().unix_timestamp(), + ); + } +} + +#[async_trait] +impl MetricsHandler for LegacyMixingStatsUpdater { + type Events = LegacyMixingData; + + async fn handle_event(&mut self, _event: Self::Events) { + panic!("this should have never been called! MetricsHandler has been incorrectly called on LegacyMixingStatsUpdater") + } +} diff --git a/nym-node/src/node/metrics/handler/mixnet_data_cleaner.rs b/nym-node/src/node/metrics/handler/mixnet_data_cleaner.rs new file mode 100644 index 00000000000..83c2041b5b3 --- /dev/null +++ b/nym-node/src/node/metrics/handler/mixnet_data_cleaner.rs @@ -0,0 +1,107 @@ +// Copyright 2024 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: GPL-3.0-only + +use crate::node::metrics::handler::{ + MetricsHandler, OnStartMetricsHandler, OnUpdateMetricsHandler, +}; +use async_trait::async_trait; +use nym_node_metrics::mixnet::{EgressRecipientStats, IngressRecipientStats}; +use nym_node_metrics::NymNodeMetrics; +use std::collections::HashMap; +use std::net::{IpAddr, SocketAddr}; + +// it can be anything, we just need a unique type_id to register our handler +pub struct StaleMixnetMetrics; + +#[derive(Default)] +pub struct LastSeen { + ingress_senders: HashMap<IpAddr, IngressRecipientStats>, + egress_forward_recipients: HashMap<SocketAddr, EgressRecipientStats>, +} + +pub struct MixnetMetricsCleaner { + metrics: NymNodeMetrics, + last_seen: LastSeen, +} + +impl MixnetMetricsCleaner { + pub(crate) fn new(metrics: NymNodeMetrics) -> Self { + MixnetMetricsCleaner { + metrics, + last_seen: LastSeen::default(), + } + } +} + +#[async_trait] +impl OnStartMetricsHandler for MixnetMetricsCleaner {} + +#[async_trait] +impl OnUpdateMetricsHandler for MixnetMetricsCleaner { + async fn on_update(&mut self) { + let mut senders_to_remove = Vec::new(); + let mut recipients_to_remove = Vec::new(); + + for sender_entry in self.metrics.mixnet.ingress.senders().iter() { + if let Some(last_seen) = self.last_seen.ingress_senders.get(sender_entry.key()) { + if sender_entry.value() == last_seen { + senders_to_remove.push(*sender_entry.key()); + } + } + } + + for recipient_entry in self.metrics.mixnet.egress.forward_recipients().iter() { + if let Some(last_seen) = self + .last_seen + .egress_forward_recipients + .get(recipient_entry.key()) + { + if recipient_entry.value() == last_seen { + recipients_to_remove.push(*recipient_entry.key()); + } + } + } + + // no need to make copies if data hasn't changed + if !senders_to_remove.is_empty() { + let mut new_ingress_senders = HashMap::new(); + + for sender in senders_to_remove { + self.metrics.mixnet.ingress.remove_stale_sender(sender) + } + + for sender_entry in self.metrics.mixnet.ingress.senders() { + new_ingress_senders.insert(*sender_entry.key(), *sender_entry.value()); + } + + self.last_seen.ingress_senders = new_ingress_senders; + } + + if !recipients_to_remove.is_empty() { + let mut new_egress_forward_recipients = HashMap::new(); + + for recipient in recipients_to_remove { + self.metrics + .mixnet + .egress + .remove_stale_forward_recipient(recipient) + } + + for recipient_entry in self.metrics.mixnet.egress.forward_recipients() { + new_egress_forward_recipients + .insert(*recipient_entry.key(), *recipient_entry.value()); + } + + self.last_seen.egress_forward_recipients = new_egress_forward_recipients; + } + } +} + +#[async_trait] +impl MetricsHandler for MixnetMetricsCleaner { + type Events = StaleMixnetMetrics; + + async fn handle_event(&mut self, _event: Self::Events) { + panic!("this should have never been called! MetricsHandler has been incorrectly called on MixnetMetricsCleaner") + } +} diff --git a/nym-node/src/node/metrics/handler/mod.rs b/nym-node/src/node/metrics/handler/mod.rs new file mode 100644 index 00000000000..025b1419b7f --- /dev/null +++ b/nym-node/src/node/metrics/handler/mod.rs @@ -0,0 +1,122 @@ +// Copyright 2024 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: Apache-2.0 + +use async_trait::async_trait; +use std::any; +use std::any::Any; +use std::time::Duration; +use tokio::time::Instant; +use tracing::trace; + +pub(crate) mod client_sessions; +pub(crate) mod legacy_packet_data; +pub(crate) mod mixnet_data_cleaner; + +pub(crate) trait RegistrableHandler: + Downcast + OnStartMetricsHandler + OnUpdateMetricsHandler + Send + Sync + 'static +{ +} + +impl<T> RegistrableHandler for T where + T: Downcast + OnStartMetricsHandler + OnUpdateMetricsHandler + Send + Sync + 'static +{ +} + +pub trait Downcast { + #[allow(dead_code)] + fn as_any(&'_ self) -> &'_ dyn Any + where + Self: 'static; + + fn as_any_mut(&'_ mut self) -> &mut dyn Any + where + Self: 'static; +} + +impl<T> Downcast for T { + fn as_any(&'_ self) -> &'_ dyn Any + where + Self: 'static, + { + self + } + + fn as_any_mut(&'_ mut self) -> &'_ mut dyn Any + where + Self: 'static, + { + self + } +} + +#[async_trait] +pub(crate) trait MetricsHandler: RegistrableHandler { + type Events; + + async fn handle_event(&mut self, event: Self::Events); +} + +#[async_trait] +pub(crate) trait OnStartMetricsHandler { + async fn on_start(&mut self) {} +} + +#[async_trait] +pub(crate) trait OnUpdateMetricsHandler { + async fn on_update(&mut self); +} + +pub(crate) struct HandlerWrapper<T> { + handler: Box<dyn MetricsHandler<Events = T>>, + update_interval: Duration, + last_updated: Instant, +} + +impl<T> HandlerWrapper<T> { + pub fn new<U>(update_interval: Duration, handler: U) -> Self + where + U: MetricsHandler<Events = T>, + { + HandlerWrapper { + handler: Box::new(handler), + update_interval, + last_updated: Instant::now(), + } + } +} + +impl<T> HandlerWrapper<T> +where + T: 'static, +{ + pub(crate) async fn handle_event(&mut self, event: T) { + self.handler.handle_event(event).await + } +} + +#[async_trait] +impl<T> OnStartMetricsHandler for HandlerWrapper<T> { + async fn on_start(&mut self) { + let name = any::type_name::<T>(); + trace!("on start for handler for events of type {name}"); + + self.handler.on_start().await; + } +} + +#[async_trait] +impl<T> OnUpdateMetricsHandler for HandlerWrapper<T> { + async fn on_update(&mut self) { + let name = any::type_name::<T>(); + trace!("on update for handler for events of type {name}"); + + let elapsed = self.last_updated.elapsed(); + if elapsed < self.update_interval { + trace!("too soon for updates"); + return; + } + + self.handler.on_update().await; + self.last_updated = Instant::now(); + } +} diff --git a/nym-node/src/node/metrics/mod.rs b/nym-node/src/node/metrics/mod.rs new file mode 100644 index 00000000000..d8f119efeaa --- /dev/null +++ b/nym-node/src/node/metrics/mod.rs @@ -0,0 +1,7 @@ +// Copyright 2024 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: GPL-3.0-only + +pub(crate) mod aggregator; +pub(crate) mod console_logger; +pub(crate) mod events_listener; +pub(crate) mod handler; diff --git a/nym-node/src/node/mixnet/handler.rs b/nym-node/src/node/mixnet/handler.rs new file mode 100644 index 00000000000..6cfb7d50d11 --- /dev/null +++ b/nym-node/src/node/mixnet/handler.rs @@ -0,0 +1,184 @@ +// Copyright 2024 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: GPL-3.0-only + +use crate::node::mixnet::shared::SharedData; +use futures::StreamExt; +use nym_metrics::nanos; +use nym_sphinx_forwarding::packet::MixPacket; +use nym_sphinx_framing::codec::NymCodec; +use nym_sphinx_framing::packet::FramedNymPacket; +use nym_sphinx_framing::processing::{ + process_framed_packet, MixProcessingResult, ProcessedFinalHop, +}; +use nym_sphinx_types::Delay; +use std::net::SocketAddr; +use tokio::net::TcpStream; +use tokio::time::Instant; +use tokio_util::codec::Framed; +use tracing::{debug, error, instrument, trace}; + +pub(crate) struct ConnectionHandler { + shared: SharedData, + mixnet_connection: Framed<TcpStream, NymCodec>, + remote_address: SocketAddr, +} + +impl Drop for ConnectionHandler { + fn drop(&mut self) { + self.shared + .metrics + .network + .disconnected_ingress_mixnet_client() + } +} + +impl ConnectionHandler { + pub(crate) fn new( + shared: &SharedData, + tcp_stream: TcpStream, + remote_address: SocketAddr, + ) -> Self { + let mut task_client = shared.task_client.fork(remote_address.to_string()); + // we don't want dropped connections to cause global shutdown + task_client.disarm(); + + shared.metrics.network.new_active_ingress_mixnet_client(); + + ConnectionHandler { + shared: SharedData { + processing_config: shared.processing_config, + sphinx_key: shared.sphinx_key.clone(), + mixnet_forwarder: shared.mixnet_forwarder.clone(), + final_hop: shared.final_hop.clone(), + metrics: shared.metrics.clone(), + task_client, + }, + remote_address, + mixnet_connection: Framed::new(tcp_stream, NymCodec), + } + } + + /// Determine instant at which packet should get forwarded to the next hop. + /// By using [`Instant`] rather than explicit [`Duration`] we minimise effects of + /// the skew caused by being stuck in the channel queue. + /// This method also clamps the maximum allowed delay so that nobody could send a bunch of packets + /// with, for example, delays of 1 year thus causing denial of service + fn create_delay_target(&self, delay: Option<Delay>) -> Option<Instant> { + let delay = delay?.to_duration(); + let now = Instant::now(); + + let delay = if delay > self.shared.processing_config.maximum_packet_delay { + self.shared.processing_config.maximum_packet_delay + } else { + delay + }; + trace!( + "received packet will be delayed for {}ms", + delay.as_millis() + ); + + Some(now + delay) + } + + fn handle_forward_packet(&self, mix_packet: MixPacket, delay: Option<Delay>) { + if !self.shared.processing_config.forward_hop_processing_enabled { + trace!("this nym-node does not support forward hop packets"); + self.shared.dropped_forward_packet(self.remote_address.ip()); + return; + } + + let forward_instant = self.create_delay_target(delay); + self.shared.forward_mix_packet(mix_packet, forward_instant); + } + + async fn handle_final_hop(&self, final_hop_data: ProcessedFinalHop) { + if !self.shared.processing_config.final_hop_processing_enabled { + trace!("this nym-node does not support final hop packets"); + self.shared + .dropped_final_hop_packet(self.remote_address.ip()); + return; + } + + let client = final_hop_data.destination; + let message = final_hop_data.message; + + // if possible attempt to push message directly to the client + match self.shared.try_push_message_to_client(client, message) { + Err(unsent_plaintext) => { + // if that failed, store it on disk (to be 🔥 soon...) + match self + .shared + .store_processed_packet_payload(client, unsent_plaintext) + .await + { + Err(err) => error!("Failed to store client data - {err}"), + Ok(_) => trace!("Stored packet for {client}"), + } + } + Ok(_) => trace!("Pushed received packet to {client}"), + } + + // if we managed to either push message directly to the [online] client or store it at + // its inbox, it means that it must exist at this gateway, hence we can send the + // received ack back into the network + self.shared.forward_ack_packet(final_hop_data.forward_ack); + } + + #[instrument(skip(self, packet), level = "debug")] + async fn handle_received_nym_packet(&self, packet: FramedNymPacket) { + // TODO: here be replay attack detection with bloomfilters and all the fancy stuff + // + + nanos!("handle_received_nym_packet", { + // 1. attempt to unwrap the packet + let unwrapped_packet = process_framed_packet(packet, &self.shared.sphinx_key); + + // 2. increment our favourite metrics stats + self.shared + .update_metrics(&unwrapped_packet, self.remote_address.ip()); + + // 3. forward the packet to the relevant sink (if enabled) + match unwrapped_packet { + Err(err) => trace!("failed to process received mix packet: {err}"), + Ok(MixProcessingResult::ForwardHop(forward_packet, delay)) => { + self.handle_forward_packet(forward_packet, delay); + } + Ok(MixProcessingResult::FinalHop(final_hop_data)) => { + self.handle_final_hop(final_hop_data).await; + } + } + }) + } + + #[instrument( + skip(self), + level = "debug", + fields( + remote = %self.remote_address + ) + )] + pub(crate) async fn handle_stream(&mut self) { + while !self.shared.task_client.is_shutdown() { + tokio::select! { + biased; + _ = self.shared.task_client.recv() => { + trace!("connection handler: received shutdown"); + } + maybe_framed_nym_packet = self.mixnet_connection.next() => { + match maybe_framed_nym_packet { + Some(Ok(packet)) => self.handle_received_nym_packet(packet).await, + Some(Err(err)) => { + debug!("connection got corrupted with: {err}"); + return + } + None => { + debug!("connection got closed by the remote"); + return + } + } + } + } + } + debug!("exiting and closing connection"); + } +} diff --git a/nym-node/src/node/mixnet/listener.rs b/nym-node/src/node/mixnet/listener.rs new file mode 100644 index 00000000000..34ae2918fe9 --- /dev/null +++ b/nym-node/src/node/mixnet/listener.rs @@ -0,0 +1,57 @@ +// Copyright 2024 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: GPL-3.0-only + +use crate::node::mixnet::SharedData; +use nym_task::TaskClient; +use std::net::SocketAddr; +use tokio::task::JoinHandle; +use tracing::{error, info, trace}; + +pub(crate) struct Listener { + bind_address: SocketAddr, + shutdown: TaskClient, + shared_data: SharedData, +} + +impl Listener { + pub(crate) fn new(bind_address: SocketAddr, shared_data: SharedData) -> Self { + Listener { + bind_address, + shutdown: shared_data.task_client.fork("socket-listener"), + shared_data, + } + } + + pub(crate) async fn run(&mut self) { + info!("attempting to run mixnet listener on {}", self.bind_address); + + let tcp_listener = match tokio::net::TcpListener::bind(self.bind_address).await { + Ok(listener) => listener, + Err(err) => { + error!("Failed to bind to {}: {err}. Are you sure nothing else is running on the specified port and your user has sufficient permission to bind to the requested address?", self.bind_address); + + // that's a bit gnarly, but we need to make sure we trigger shutdown + let mut shutdown_bomb = self.shutdown.fork("shutdown-bomb"); + shutdown_bomb.rearm(); + drop(shutdown_bomb); + return; + } + }; + + while !self.shutdown.is_shutdown() { + tokio::select! { + biased; + _ = self.shutdown.recv() => { + trace!("mixnet listener: received shutdown"); + } + connection = tcp_listener.accept() => { + self.shared_data.try_handle_connection(connection); + } + } + } + } + + pub(crate) fn start(mut self) -> JoinHandle<()> { + tokio::spawn(async move { self.run().await }) + } +} diff --git a/nym-node/src/node/mixnet/mod.rs b/nym-node/src/node/mixnet/mod.rs new file mode 100644 index 00000000000..b4f9f7e4f6d --- /dev/null +++ b/nym-node/src/node/mixnet/mod.rs @@ -0,0 +1,10 @@ +// Copyright 2024 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: GPL-3.0-only + +pub(crate) mod handler; +pub(crate) mod listener; +pub(crate) mod packet_forwarding; +pub(crate) mod shared; + +pub(crate) use listener::Listener; +pub(crate) use shared::{final_hop::SharedFinalHopData, SharedData}; diff --git a/nym-node/src/node/mixnet/packet_forwarding.rs b/nym-node/src/node/mixnet/packet_forwarding.rs new file mode 100644 index 00000000000..bcd51a52a9e --- /dev/null +++ b/nym-node/src/node/mixnet/packet_forwarding.rs @@ -0,0 +1,143 @@ +// Copyright 2024 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: Apache-2.0 + +use futures::StreamExt; +use nym_mixnet_client::forwarder::{ + mix_forwarding_channels, MixForwardingReceiver, MixForwardingSender, PacketToForward, +}; +use nym_mixnet_client::SendWithoutResponse; +use nym_node_metrics::NymNodeMetrics; +use nym_nonexhaustive_delayqueue::{Expired, NonExhaustiveDelayQueue}; +use nym_sphinx_forwarding::packet::MixPacket; +use std::io; +use tokio::time::Instant; +use tracing::{debug, error, trace, warn}; + +pub struct PacketForwarder<C> { + delay_queue: NonExhaustiveDelayQueue<MixPacket>, + mixnet_client: C, + + metrics: NymNodeMetrics, + + packet_sender: MixForwardingSender, + packet_receiver: MixForwardingReceiver, + shutdown: nym_task::TaskClient, +} + +impl<C> PacketForwarder<C> { + pub fn new(client: C, metrics: NymNodeMetrics, shutdown: nym_task::TaskClient) -> Self { + let (packet_sender, packet_receiver) = mix_forwarding_channels(); + + PacketForwarder { + delay_queue: NonExhaustiveDelayQueue::new(), + mixnet_client: client, + metrics, + packet_sender, + packet_receiver, + shutdown, + } + } + + pub fn sender(&self) -> MixForwardingSender { + self.packet_sender.clone() + } + + fn forward_packet(&mut self, packet: MixPacket) + where + C: SendWithoutResponse, + { + let next_hop = packet.next_hop(); + let packet_type = packet.packet_type(); + let packet = packet.into_packet(); + + if let Err(err) = self + .mixnet_client + .send_without_response(next_hop, packet, packet_type) + { + if err.kind() == io::ErrorKind::WouldBlock { + // we only know for sure if we dropped a packet if our sending queue was full + // in any other case the connection might still be re-established (or created for the first time) + // and the packet might get sent, but we won't know about it + self.metrics + .mixnet + .egress_dropped_forward_packet(next_hop.into()) + } else if err.kind() == io::ErrorKind::NotConnected { + // let's give the benefit of the doubt and assume we manage to establish connection + self.metrics + .mixnet + .egress_sent_forward_packet(next_hop.into()) + } + } else { + self.metrics + .mixnet + .egress_sent_forward_packet(next_hop.into()) + } + } + + /// Upon packet being finished getting delayed, forward it to the mixnet. + fn handle_done_delaying(&mut self, packet: Expired<MixPacket>) + where + C: SendWithoutResponse, + { + let delayed_packet = packet.into_inner(); + self.forward_packet(delayed_packet) + } + + fn handle_new_packet(&mut self, new_packet: PacketToForward) + where + C: SendWithoutResponse, + { + // in case of a zero delay packet, don't bother putting it in the delay queue, + // just forward it immediately + if let Some(instant) = new_packet.forward_delay_target { + // check if the delay has already expired, if so, don't bother putting it through + // the delay queue only to retrieve it immediately. Just forward it. + if instant.checked_duration_since(Instant::now()).is_none() { + self.forward_packet(new_packet.packet) + } else { + self.delay_queue.insert_at(new_packet.packet, instant); + } + } else { + self.forward_packet(new_packet.packet) + } + } + + pub async fn run(&mut self) + where + C: SendWithoutResponse, + { + let mut processed = 0; + trace!("starting PacketForwarder"); + loop { + tokio::select! { + biased; + _ = self.shutdown.recv() => { + debug!("PacketForwarder: Received shutdown"); + break; + } + delayed = self.delay_queue.next() => { + // SAFETY: `stream` implementation of `NonExhaustiveDelayQueue` never returns `None` + #[allow(clippy::unwrap_used)] + self.handle_done_delaying(delayed.unwrap()); + } + new_packet = self.packet_receiver.next() => { + // this one is impossible to ever panic - the struct itself contains a sender + // and hence it can't happen that ALL senders are dropped + #[allow(clippy::unwrap_used)] + self.handle_new_packet(new_packet.unwrap()); + if processed % 1000 == 0 { + let queue_len = self.packet_sender.len(); + match queue_len { + n if n > 200 => error!("there are currently {n} mix packets waiting to get forwarded!"), + n if n > 50 => warn!("there are currently {n} mix packets waiting to get forwarded"), + n => trace!("there are currently {n} mix packets waiting to get forwarded"), + } + } + + processed += 1; + } + } + } + trace!("PacketForwarder: Exiting"); + } +} diff --git a/nym-node/src/node/mixnet/shared/final_hop.rs b/nym-node/src/node/mixnet/shared/final_hop.rs new file mode 100644 index 00000000000..d38fc99d9fc --- /dev/null +++ b/nym-node/src/node/mixnet/shared/final_hop.rs @@ -0,0 +1,51 @@ +// Copyright 2024 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: GPL-3.0-only + +use nym_gateway::node::{ActiveClientsStore, GatewayStorage, GatewayStorageError}; +use nym_sphinx_types::DestinationAddressBytes; +use tracing::debug; + +#[derive(Clone)] +pub(crate) struct SharedFinalHopData { + active_clients: ActiveClientsStore, + storage: GatewayStorage, +} + +impl SharedFinalHopData { + pub fn new(active_clients: ActiveClientsStore, storage: GatewayStorage) -> Self { + Self { + active_clients, + storage, + } + } + + pub(crate) fn try_push_message_to_client( + &self, + client_address: DestinationAddressBytes, + message: Vec<u8>, + ) -> Result<(), Vec<u8>> { + match self.active_clients.get_sender(client_address) { + None => Err(message), + Some(sender_channel) => { + if let Err(unsent) = sender_channel.unbounded_send(vec![message]) { + // the unwrap here is fine as the original message got returned; + // plus we're only ever sending 1 message at the time (for now) + #[allow(clippy::unwrap_used)] + Err(unsent.into_inner().pop().unwrap()) + } else { + Ok(()) + } + } + } + } + + pub(crate) async fn store_processed_packet_payload( + &self, + client_address: DestinationAddressBytes, + message: Vec<u8>, + ) -> Result<(), GatewayStorageError> { + debug!("Storing received message for {client_address} on the disk...",); + + self.storage.store_message(client_address, message).await + } +} diff --git a/nym-node/src/node/mixnet/shared/mod.rs b/nym-node/src/node/mixnet/shared/mod.rs new file mode 100644 index 00000000000..9387704a53b --- /dev/null +++ b/nym-node/src/node/mixnet/shared/mod.rs @@ -0,0 +1,181 @@ +// Copyright 2024 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: GPL-3.0-only + +use crate::config::Config; +use crate::node::mixnet::handler::ConnectionHandler; +use crate::node::mixnet::SharedFinalHopData; +use nym_crypto::asymmetric::x25519; +use nym_gateway::node::GatewayStorageError; +use nym_mixnet_client::forwarder::{MixForwardingSender, PacketToForward}; +use nym_node_metrics::NymNodeMetrics; +use nym_sphinx_forwarding::packet::MixPacket; +use nym_sphinx_framing::processing::{MixProcessingResult, PacketProcessingError}; +use nym_sphinx_types::DestinationAddressBytes; +use nym_task::TaskClient; +use std::io; +use std::net::{IpAddr, SocketAddr}; +use std::sync::Arc; +use std::time::Duration; +use tokio::net::TcpStream; +use tokio::task::JoinHandle; +use tokio::time::Instant; +use tracing::{debug, error}; + +pub(crate) mod final_hop; + +#[derive(Clone, Copy)] +pub(crate) struct ProcessingConfig { + pub(crate) maximum_packet_delay: Duration, + + pub(crate) forward_hop_processing_enabled: bool, + pub(crate) final_hop_processing_enabled: bool, +} + +impl ProcessingConfig { + pub(crate) fn new(config: &Config) -> Self { + ProcessingConfig { + maximum_packet_delay: config.mixnet.debug.maximum_forward_packet_delay, + forward_hop_processing_enabled: config.modes.mixnode, + final_hop_processing_enabled: config.modes.expects_final_hop_traffic() + || config.wireguard.enabled, + } + } +} + +// explicitly do NOT derive clone as we want to manually apply relevant suffixes to the task clients +// as well as immediately disarm them +pub(crate) struct SharedData { + pub(super) processing_config: ProcessingConfig, + // TODO: this type is not `Zeroize` : ( + pub(super) sphinx_key: Arc<nym_sphinx_types::PrivateKey>, + + // used for FORWARD mix packets and FINAL ack packets + pub(super) mixnet_forwarder: MixForwardingSender, + + // data specific to the final hop (gateway) processing + pub(super) final_hop: SharedFinalHopData, + + pub(super) metrics: NymNodeMetrics, + pub(super) task_client: TaskClient, +} + +impl SharedData { + pub(crate) fn new( + processing_config: ProcessingConfig, + x25519_key: &x25519::PrivateKey, + mixnet_forwarder: MixForwardingSender, + final_hop: SharedFinalHopData, + metrics: NymNodeMetrics, + task_client: TaskClient, + ) -> Self { + SharedData { + processing_config, + sphinx_key: Arc::new(x25519_key.into()), + mixnet_forwarder, + final_hop, + metrics, + task_client, + } + } + + pub(super) fn log_connected_clients(&self) { + debug!( + "there are currently {} connected clients on the mixnet socket", + self.metrics + .network + .active_ingress_mixnet_connections_count() + ) + } + + pub(super) fn dropped_forward_packet(&self, source: IpAddr) { + self.metrics.mixnet.ingress_dropped_forward_packet(source) + } + + pub(super) fn dropped_final_hop_packet(&self, source: IpAddr) { + self.metrics.mixnet.ingress_dropped_final_hop_packet(source) + } + + pub(super) fn update_metrics( + &self, + processing_result: &Result<MixProcessingResult, PacketProcessingError>, + source: IpAddr, + ) { + match processing_result { + Err(_) => self.metrics.mixnet.ingress_malformed_packet(source), + Ok(MixProcessingResult::ForwardHop(_, delay)) => { + self.metrics.mixnet.ingress_received_forward_packet(source); + + // check if the delay wasn't excessive + if let Some(delay) = delay { + if delay.to_duration() > self.processing_config.maximum_packet_delay { + self.metrics.mixnet.ingress_excessive_delay_packet() + } + } + } + Ok(MixProcessingResult::FinalHop(_)) => { + self.metrics + .mixnet + .ingress_received_final_hop_packet(source); + } + } + } + + pub(super) fn try_handle_connection( + &self, + accepted: io::Result<(TcpStream, SocketAddr)>, + ) -> Option<JoinHandle<()>> { + match accepted { + Ok((socket, remote_addr)) => { + debug!("accepted incoming mixnet connection from: {remote_addr}"); + let mut handler = ConnectionHandler::new(self, socket, remote_addr); + let join_handle = tokio::spawn(async move { handler.handle_stream().await }); + self.log_connected_clients(); + Some(join_handle) + } + Err(err) => { + debug!("failed to accept incoming mixnet connection: {err}"); + None + } + } + } + + pub(super) fn forward_mix_packet(&self, packet: MixPacket, delay_until: Option<Instant>) { + if self + .mixnet_forwarder + .forward_packet(PacketToForward::new(packet, delay_until)) + .is_err() + && !self.task_client.is_shutdown() + { + error!("failed to forward sphinx packet on the channel while the process is not going through the shutdown!"); + // this is a critical error, we're in uncharted lands, we have to shut down + let mut shutdown_bomb = self.task_client.fork("shutdown bomb"); + shutdown_bomb.rearm(); + drop(shutdown_bomb) + } + } + + pub(super) fn forward_ack_packet(&self, forward_ack: Option<MixPacket>) { + if let Some(forward_ack) = forward_ack { + self.forward_mix_packet(forward_ack, None); + self.metrics.mixnet.egress_sent_ack(); + } + } + + pub(super) fn try_push_message_to_client( + &self, + client: DestinationAddressBytes, + message: Vec<u8>, + ) -> Result<(), Vec<u8>> { + self.final_hop.try_push_message_to_client(client, message) + } + + pub(crate) async fn store_processed_packet_payload( + &self, + client_address: DestinationAddressBytes, + message: Vec<u8>, + ) -> Result<(), GatewayStorageError> { + self.final_hop + .store_processed_packet_payload(client_address, message) + .await + } +} diff --git a/nym-node/src/node/mod.rs b/nym-node/src/node/mod.rs index bebefa0aef9..1311bb4f574 100644 --- a/nym-node/src/node/mod.rs +++ b/nym-node/src/node/mod.rs @@ -1,116 +1,117 @@ // Copyright 2024 - Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: GPL-3.0-only +use self::helpers::load_x25519_wireguard_keypair; +use crate::config::helpers::gateway_tasks_config; +use crate::config::{ + Config, GatewayTasksConfig, NodeModes, ServiceProvidersConfig, Wireguard, DEFAULT_MIXNET_PORT, +}; +use crate::error::{EntryGatewayError, NymNodeError, ServiceProvidersError}; use crate::node::description::{load_node_description, save_node_description}; use crate::node::helpers::{ load_ed25519_identity_keypair, load_key, load_x25519_noise_keypair, load_x25519_sphinx_keypair, store_ed25519_identity_keypair, store_key, store_keypair, store_x25519_noise_keypair, store_x25519_sphinx_keypair, DisplayDetails, }; -use crate::node::http::{sign_host_details, system_info::get_system_info}; -use nym_bin_common::{bin_info, bin_info_owned}; +use crate::node::http::api::api_requests; +use crate::node::http::helpers::sign_host_details; +use crate::node::http::helpers::system_info::get_system_info; +use crate::node::http::state::AppState; +use crate::node::http::{HttpServerConfig, NymNodeHttpServer, NymNodeRouter}; +use crate::node::metrics::aggregator::MetricsAggregator; +use crate::node::metrics::console_logger::ConsoleLogger; +use crate::node::metrics::handler::client_sessions::GatewaySessionStatsHandler; +use crate::node::metrics::handler::legacy_packet_data::LegacyMixingStatsUpdater; +use crate::node::metrics::handler::mixnet_data_cleaner::MixnetMetricsCleaner; +use crate::node::mixnet::packet_forwarding::PacketForwarder; +use crate::node::mixnet::shared::ProcessingConfig; +use crate::node::mixnet::SharedFinalHopData; +use crate::node::shared_topology::NymNodeTopologyProvider; +use nym_bin_common::bin_info; use nym_crypto::asymmetric::{ed25519, x25519}; -use nym_gateway::Gateway; -use nym_mixnode::MixNode; +use nym_gateway::node::{ActiveClientsStore, GatewayTasksBuilder}; +use nym_mixnet_client::forwarder::MixForwardingSender; use nym_network_requester::{ set_active_gateway, setup_fs_gateways_storage, store_gateway_details, CustomGatewayDetails, GatewayDetails, GatewayRegistration, }; -use nym_node::config::entry_gateway::ephemeral_entry_gateway_config; -use nym_node::config::exit_gateway::ephemeral_exit_gateway_config; -use nym_node::config::mixnode::ephemeral_mixnode_config; -use nym_node::config::persistence::AuthenticatorPaths; -use nym_node::config::{ - Config, EntryGatewayConfig, ExitGatewayConfig, MixnodeConfig, NodeMode, Wireguard, -}; -use nym_node::error::{EntryGatewayError, ExitGatewayError, MixnodeError, NymNodeError}; -use nym_node_http_api::api::api_requests; -use nym_node_http_api::api::api_requests::v1::node::models::{AnnouncePorts, NodeDescription}; -use nym_node_http_api::state::metrics::{SharedMixingStats, SharedSessionStats, SharedVerlocStats}; -use nym_node_http_api::state::AppState; -use nym_node_http_api::{NymNodeHTTPServer, NymNodeRouter}; +use nym_node_metrics::events::MetricEventsSender; +use nym_node_metrics::NymNodeMetrics; +use nym_node_requests::api::v1::node::models::{AnnouncePorts, NodeDescription}; use nym_sphinx_acknowledgements::AckKey; use nym_sphinx_addressing::Recipient; use nym_task::{TaskClient, TaskManager}; +use nym_topology::NetworkAddress; use nym_validator_client::client::NymApiClientExt; use nym_validator_client::models::NodeRefreshBody; -use nym_validator_client::NymApiClient; +use nym_validator_client::{NymApiClient, UserAgent}; +use nym_verloc::measurements::SharedVerlocStats; +use nym_verloc::{self, measurements::VerlocMeasurer}; use nym_wireguard::{peer_controller::PeerControlRequest, WireguardGatewayData}; use rand::rngs::OsRng; use rand::{CryptoRng, RngCore}; +use std::net::SocketAddr; use std::path::Path; use std::sync::Arc; use std::time::Duration; use tokio::sync::mpsc; use tokio::time::timeout; -use tracing::{debug, error, info, trace, warn}; +use tracing::{debug, info, trace, warn}; use zeroize::Zeroizing; -use self::helpers::load_x25519_wireguard_keypair; - pub mod bonding_information; pub mod description; pub mod helpers; pub(crate) mod http; +pub(crate) mod metrics; +pub(crate) mod mixnet; +mod shared_topology; -pub struct MixnodeData { - mixing_stats: SharedMixingStats, -} - -impl MixnodeData { - pub fn initialise(_config: &MixnodeConfig) -> Result<(), MixnodeError> { - Ok(()) - } - - fn new(_config: &MixnodeConfig) -> Result<MixnodeData, MixnodeError> { - Ok(MixnodeData { - mixing_stats: SharedMixingStats::new(), - }) - } -} - -pub struct EntryGatewayData { - mnemonic: Zeroizing<bip39::Mnemonic>, - client_storage: nym_gateway::node::PersistentStorage, +pub struct GatewayTasksData { + mnemonic: Arc<Zeroizing<bip39::Mnemonic>>, + client_storage: nym_gateway::node::GatewayStorage, stats_storage: nym_gateway::node::PersistentStatsStorage, - sessions_stats: SharedSessionStats, } -impl EntryGatewayData { +impl GatewayTasksData { pub fn initialise( - config: &EntryGatewayConfig, + config: &GatewayTasksConfig, custom_mnemonic: Option<Zeroizing<bip39::Mnemonic>>, ) -> Result<(), EntryGatewayError> { // SAFETY: // this unwrap is fine as 24 word count is a valid argument for generating entropy for a new bip39 mnemonic #[allow(clippy::unwrap_used)] - let mnemonic = custom_mnemonic - .unwrap_or_else(|| Zeroizing::new(bip39::Mnemonic::generate(24).unwrap())); + let mnemonic = Arc::new( + custom_mnemonic + .unwrap_or_else(|| Zeroizing::new(bip39::Mnemonic::generate(24).unwrap())), + ); config.storage_paths.save_mnemonic_to_file(&mnemonic)?; Ok(()) } - async fn new(config: &EntryGatewayConfig) -> Result<EntryGatewayData, EntryGatewayError> { - Ok(EntryGatewayData { - mnemonic: config.storage_paths.load_mnemonic_from_file()?, - client_storage: nym_gateway::node::PersistentStorage::init( - &config.storage_paths.clients_storage, - config.debug.message_retrieval_limit, - ) - .await - .map_err(nym_gateway::GatewayError::from)?, - stats_storage: nym_gateway::node::PersistentStatsStorage::init( - &config.storage_paths.stats_storage, - ) - .await - .map_err(nym_gateway::GatewayError::from)?, - sessions_stats: SharedSessionStats::new(), + async fn new(config: &GatewayTasksConfig) -> Result<GatewayTasksData, EntryGatewayError> { + let client_storage = nym_gateway::node::GatewayStorage::init( + &config.storage_paths.clients_storage, + config.debug.message_retrieval_limit, + ) + .await + .map_err(nym_gateway::GatewayError::from)?; + + let stats_storage = + nym_gateway::node::PersistentStatsStorage::init(&config.storage_paths.stats_storage) + .await + .map_err(nym_gateway::GatewayError::from)?; + + Ok(GatewayTasksData { + mnemonic: Arc::new(config.storage_paths.load_mnemonic_from_file()?), + client_storage, + stats_storage, }) } } -pub struct ExitGatewayData { +pub struct ServiceProvidersData { // ideally we'd be storing all the keys here, but unfortunately due to how the service providers // are currently implemented, they will be loading the data themselves from the provided paths @@ -121,21 +122,19 @@ pub struct ExitGatewayData { ipr_ed25519: ed25519::PublicKey, ipr_x25519: x25519::PublicKey, + // TODO: those should be moved to WG section auth_ed25519: ed25519::PublicKey, auth_x25519: x25519::PublicKey, - - client_storage: nym_gateway::node::PersistentStorage, - stats_storage: nym_gateway::node::PersistentStatsStorage, } -impl ExitGatewayData { +impl ServiceProvidersData { fn initialise_client_keys<R: RngCore + CryptoRng>( rng: &mut R, typ: &str, ed25519_paths: nym_pemstore::KeyPairPath, x25519_paths: nym_pemstore::KeyPairPath, ack_key_path: &Path, - ) -> Result<(), ExitGatewayError> { + ) -> Result<(), ServiceProvidersError> { let ed25519_keys = ed25519::KeyPair::new(rng); let x25519_keys = x25519::KeyPair::new(rng); let aes128ctr_key = AckKey::new(rng); @@ -154,7 +153,7 @@ impl ExitGatewayData { async fn initialise_client_gateway_storage( storage_path: &Path, registration: &GatewayRegistration, - ) -> Result<(), ExitGatewayError> { + ) -> Result<(), ServiceProvidersError> { // insert all required information into the gateways store // (I hate that we have to do it, but that's currently the simplest thing to do) let storage = setup_fs_gateways_storage(storage_path).await?; @@ -165,9 +164,9 @@ impl ExitGatewayData { pub async fn initialise_network_requester<R: RngCore + CryptoRng>( rng: &mut R, - config: &ExitGatewayConfig, + config: &ServiceProvidersConfig, registration: &GatewayRegistration, - ) -> Result<(), ExitGatewayError> { + ) -> Result<(), ServiceProvidersError> { trace!("initialising network requester keys"); Self::initialise_client_keys( rng, @@ -191,9 +190,9 @@ impl ExitGatewayData { pub async fn initialise_ip_packet_router_requester<R: RngCore + CryptoRng>( rng: &mut R, - config: &ExitGatewayConfig, + config: &ServiceProvidersConfig, registration: &GatewayRegistration, - ) -> Result<(), ExitGatewayError> { + ) -> Result<(), ServiceProvidersError> { trace!("initialising ip packet router keys"); Self::initialise_client_keys( rng, @@ -215,10 +214,37 @@ impl ExitGatewayData { .await } + pub async fn initialise_authenticator<R: RngCore + CryptoRng>( + rng: &mut R, + config: &ServiceProvidersConfig, + registration: &GatewayRegistration, + ) -> Result<(), ServiceProvidersError> { + trace!("initialising authenticator keys"); + Self::initialise_client_keys( + rng, + "authenticator", + config + .storage_paths + .authenticator + .ed25519_identity_storage_paths(), + config + .storage_paths + .authenticator + .x25519_diffie_hellman_storage_paths(), + &config.storage_paths.authenticator.ack_key_file, + )?; + Self::initialise_client_gateway_storage( + &config.storage_paths.authenticator.gateway_registrations, + registration, + ) + .await?; + Ok(()) + } + pub async fn initialise( - config: &ExitGatewayConfig, + config: &ServiceProvidersConfig, public_key: ed25519::PublicKey, - ) -> Result<(), ExitGatewayError> { + ) -> Result<(), ServiceProvidersError> { // generate all the keys for NR, IPR and AUTH let mut rng = OsRng; @@ -230,10 +256,13 @@ impl ExitGatewayData { // IPR: Self::initialise_ip_packet_router_requester(&mut rng, config, &gateway_details).await?; + // Authenticator + Self::initialise_authenticator(&mut rng, config, &gateway_details).await?; + Ok(()) } - async fn new(config: &ExitGatewayConfig) -> Result<ExitGatewayData, ExitGatewayError> { + fn new(config: &ServiceProvidersConfig) -> Result<ServiceProvidersData, ServiceProvidersError> { let nr_paths = &config.storage_paths.network_requester; let nr_ed25519 = load_key( &nr_paths.public_ed25519_identity_key_file, @@ -267,27 +296,13 @@ impl ExitGatewayData { "authenticator x25519", )?; - let client_storage = nym_gateway::node::PersistentStorage::init( - &config.storage_paths.clients_storage, - config.debug.message_retrieval_limit, - ) - .await - .map_err(nym_gateway::GatewayError::from)?; - - let stats_storage = - nym_gateway::node::PersistentStatsStorage::init(&config.storage_paths.stats_storage) - .await - .map_err(nym_gateway::GatewayError::from)?; - - Ok(ExitGatewayData { + Ok(ServiceProvidersData { nr_ed25519, nr_x25519, ipr_ed25519, ipr_x25519, auth_ed25519, auth_x25519, - client_storage, - stats_storage, }) } } @@ -308,7 +323,7 @@ impl WireguardData { Ok(WireguardData { inner, peer_rx }) } - pub(crate) fn initialise(config: &Wireguard) -> Result<(), ExitGatewayError> { + pub(crate) fn initialise(config: &Wireguard) -> Result<(), ServiceProvidersError> { let mut rng = OsRng; let x25519_keys = x25519::KeyPair::new(&mut rng); @@ -337,18 +352,16 @@ pub(crate) struct NymNode { description: NodeDescription, - // TODO: currently we're only making measurements in 'mixnode' mode; this should be changed - verloc_stats: SharedVerlocStats, + metrics: NymNodeMetrics, - #[allow(dead_code)] - mixnode: MixnodeData, + verloc_stats: SharedVerlocStats, - entry_gateway: EntryGatewayData, + entry_gateway: GatewayTasksData, #[allow(dead_code)] - exit_gateway: ExitGatewayData, + service_providers: ServiceProvidersData, - wireguard: WireguardData, + wireguard: Option<WireguardData>, ed25519_identity_keys: Arc<ed25519::KeyPair>, x25519_sphinx_keys: Arc<x25519::KeyPair>, @@ -359,57 +372,6 @@ pub(crate) struct NymNode { } impl NymNode { - fn initialise_client_keys<R: RngCore + CryptoRng>( - rng: &mut R, - typ: &str, - ed25519_paths: nym_pemstore::KeyPairPath, - x25519_paths: nym_pemstore::KeyPairPath, - ack_key_path: &Path, - ) -> Result<(), EntryGatewayError> { - let ed25519_keys = ed25519::KeyPair::new(rng); - let x25519_keys = x25519::KeyPair::new(rng); - let aes128ctr_key = AckKey::new(rng); - - store_keypair( - &ed25519_keys, - ed25519_paths, - format!("{typ}-ed25519-identity"), - )?; - store_keypair(&x25519_keys, x25519_paths, format!("{typ}-x25519-dh"))?; - store_key(&aes128ctr_key, ack_key_path, format!("{typ}-ack-key"))?; - - Ok(()) - } - - async fn initialise_client_gateway_storage( - storage_path: &Path, - registration: &GatewayRegistration, - ) -> Result<(), EntryGatewayError> { - // insert all required information into the gateways store - // (I hate that we have to do it, but that's currently the simplest thing to do) - let storage = setup_fs_gateways_storage(storage_path).await?; - store_gateway_details(&storage, registration).await?; - set_active_gateway(&storage, ®istration.gateway_id().to_base58_string()).await?; - Ok(()) - } - - pub async fn initialise_authenticator<R: RngCore + CryptoRng>( - rng: &mut R, - paths: &AuthenticatorPaths, - registration: &GatewayRegistration, - ) -> Result<(), NymNodeError> { - trace!("initialising authenticator keys"); - Self::initialise_client_keys( - rng, - "authenticator", - paths.ed25519_identity_storage_paths(), - paths.x25519_diffie_hellman_storage_paths(), - &paths.ack_key_file, - )?; - Self::initialise_client_gateway_storage(&paths.gateway_registrations, registration).await?; - Ok(()) - } - pub(crate) async fn initialise( config: &Config, custom_mnemonic: Option<Zeroizing<bip39::Mnemonic>>, @@ -446,24 +408,13 @@ impl NymNode { &NodeDescription::default(), )?; - // mixnode initialisation - MixnodeData::initialise(&config.mixnode)?; - // entry gateway initialisation - EntryGatewayData::initialise(&config.entry_gateway, custom_mnemonic)?; - - // exit gateway initialisation - ExitGatewayData::initialise(&config.exit_gateway, *ed25519_identity_keys.public_key()) - .await?; - - // authenticator initialization: - Self::initialise_authenticator( - &mut rng, - &config.entry_gateway.storage_paths.authenticator, - &GatewayDetails::Custom(CustomGatewayDetails::new( - *ed25519_identity_keys.public_key(), - )) - .into(), + GatewayTasksData::initialise(&config.gateway_tasks, custom_mnemonic)?; + + // service providers initialisation + ServiceProvidersData::initialise( + &config.service_providers, + *ed25519_identity_keys.public_key(), ) .await?; @@ -475,6 +426,7 @@ impl NymNode { pub(crate) async fn new(config: Config) -> Result<Self, NymNodeError> { let wireguard_data = WireguardData::new(&config.wireguard)?; + Ok(NymNode { ed25519_identity_keys: Arc::new(load_ed25519_identity_keypair( config.storage_paths.keys.ed25519_identity_storage_paths(), @@ -486,16 +438,20 @@ impl NymNode { config.storage_paths.keys.x25519_noise_storage_paths(), )?), description: load_node_description(&config.storage_paths.description)?, + metrics: NymNodeMetrics::new(), verloc_stats: Default::default(), - mixnode: MixnodeData::new(&config.mixnode)?, - entry_gateway: EntryGatewayData::new(&config.entry_gateway).await?, - exit_gateway: ExitGatewayData::new(&config.exit_gateway).await?, - wireguard: wireguard_data, + entry_gateway: GatewayTasksData::new(&config.gateway_tasks).await?, + service_providers: ServiceProvidersData::new(&config.service_providers)?, + wireguard: Some(wireguard_data), config, accepted_operator_terms_and_conditions: false, }) } + pub(crate) fn config(&self) -> &Config { + &self.config + } + pub(crate) fn with_accepted_operator_terms_and_conditions( mut self, accepted_operator_terms_and_conditions: bool, @@ -506,48 +462,53 @@ impl NymNode { fn exit_network_requester_address(&self) -> Recipient { Recipient::new( - self.exit_gateway.nr_ed25519, - self.exit_gateway.nr_x25519, + self.service_providers.nr_ed25519, + self.service_providers.nr_x25519, *self.ed25519_identity_keys.public_key(), ) } fn exit_ip_packet_router_address(&self) -> Recipient { Recipient::new( - self.exit_gateway.ipr_ed25519, - self.exit_gateway.ipr_x25519, + self.service_providers.ipr_ed25519, + self.service_providers.ipr_x25519, *self.ed25519_identity_keys.public_key(), ) } fn exit_authenticator_address(&self) -> Recipient { Recipient::new( - self.exit_gateway.auth_ed25519, - self.exit_gateway.auth_x25519, + self.service_providers.auth_ed25519, + self.service_providers.auth_x25519, *self.ed25519_identity_keys.public_key(), ) } - fn x25519_wireguard_key(&self) -> &x25519::PublicKey { - self.wireguard.inner.keypair().public_key() + fn x25519_wireguard_key(&self) -> Result<x25519::PublicKey, NymNodeError> { + let wg_data = self + .wireguard + .as_ref() + .ok_or(NymNodeError::WireguardDataUnavailable)?; + + Ok(*wg_data.inner.keypair().public_key()) } - pub(crate) fn display_details(&self) -> DisplayDetails { - DisplayDetails { - current_mode: self.config.mode, + pub(crate) fn display_details(&self) -> Result<DisplayDetails, NymNodeError> { + Ok(DisplayDetails { + current_modes: self.config.modes, description: self.description.clone(), ed25519_identity_key: self.ed25519_identity_key().to_base58_string(), x25519_sphinx_key: self.x25519_sphinx_key().to_base58_string(), x25519_noise_key: self.x25519_noise_key().to_base58_string(), - x25519_wireguard_key: self.x25519_wireguard_key().to_base58_string(), + x25519_wireguard_key: self.x25519_wireguard_key()?.to_base58_string(), exit_network_requester_address: self.exit_network_requester_address().to_string(), exit_ip_packet_router_address: self.exit_ip_packet_router_address().to_string(), exit_authenticator_address: self.exit_authenticator_address().to_string(), - } + }) } - pub(crate) fn mode(&self) -> NodeMode { - self.config.mode + pub(crate) fn modes(&self) -> NodeModes { + self.config.modes } pub(crate) fn ed25519_identity_key(&self) -> &ed25519::PublicKey { @@ -562,85 +523,140 @@ impl NymNode { self.x25519_noise_keys.public_key() } - fn start_mixnode(self, task_client: TaskClient) -> Result<(), NymNodeError> { - info!("going to start the nym-node in MIXNODE mode"); + // the reason it's here as opposed to in the gateway directly, + // is that other nym-node tasks will also eventually need it + // (such as the ones for obtaining noise keys of other nodes) + fn build_topology_provider(&self) -> Result<NymNodeTopologyProvider, NymNodeError> { + Ok(NymNodeTopologyProvider::new( + self.as_gateway_topology_node()?, + self.config.debug.topology_cache_ttl, + self.user_agent(), + self.config.mixnet.nym_api_urls.clone(), + )) + } - let config = ephemeral_mixnode_config(self.config.clone())?; - let mut mixnode = MixNode::new_loaded( - config, - self.ed25519_identity_keys.clone(), - self.x25519_sphinx_keys.clone(), - ); - mixnode.set_task_client(task_client); - mixnode.set_mixing_stats(self.mixnode.mixing_stats.clone()); - mixnode.set_verloc_stats(self.verloc_stats.clone()); + fn as_gateway_topology_node(&self) -> Result<nym_topology::gateway::LegacyNode, NymNodeError> { + let Some(ip) = self.config.host.public_ips.first() else { + return Err(NymNodeError::NoPublicIps); + }; - tokio::spawn(async move { mixnode.run().await }); - Ok(()) + let mix_port = self + .config + .mixnet + .announce_port + .unwrap_or(DEFAULT_MIXNET_PORT); + let mix_host = SocketAddr::new(*ip, mix_port); + + let clients_ws_port = self + .config + .gateway_tasks + .announce_ws_port + .unwrap_or(self.config.gateway_tasks.bind_address.port()); + + Ok(nym_topology::gateway::LegacyNode { + node_id: u32::MAX, + mix_host, + host: NetworkAddress::IpAddr(*ip), + clients_ws_port, + clients_wss_port: self.config.gateway_tasks.announce_wss_port, + sphinx_key: *self.x25519_sphinx_key(), + identity_key: *self.ed25519_identity_key(), + version: env!("CARGO_PKG_VERSION").into(), + }) } - fn start_entry_gateway(self, task_client: TaskClient) -> Result<(), NymNodeError> { - info!("going to start the nym-node in ENTRY GATEWAY mode"); + async fn start_gateway_tasks( + &mut self, + metrics_sender: MetricEventsSender, + active_clients_store: ActiveClientsStore, + mix_packet_sender: MixForwardingSender, + task_client: TaskClient, + ) -> Result<(), NymNodeError> { + let config = gateway_tasks_config(&self.config); + let topology_provider = Box::new(self.build_topology_provider()?); - let config = - ephemeral_entry_gateway_config(self.config.clone(), &self.entry_gateway.mnemonic)?; - let mut entry_gateway = Gateway::new_loaded( + let mut gateway_tasks_builder = GatewayTasksBuilder::new( config.gateway, - config.nr_opts, - config.ipr_opts, - Some(config.auth_opts), self.ed25519_identity_keys.clone(), - self.x25519_sphinx_keys.clone(), self.entry_gateway.client_storage.clone(), - bin_info!().into(), - self.entry_gateway.stats_storage.clone(), + mix_packet_sender, + metrics_sender, + self.entry_gateway.mnemonic.clone(), + task_client, ); - entry_gateway.set_task_client(task_client); - entry_gateway.set_session_stats(self.entry_gateway.sessions_stats.clone()); - if self.config.wireguard.enabled { - entry_gateway.set_wireguard_data(self.wireguard.into()); + + // if we're running in entry mode, start the websocket + if self.modes().entry { + info!( + "starting the clients websocket... on {}", + self.config.gateway_tasks.bind_address + ); + let websocket = gateway_tasks_builder + .build_websocket_listener(active_clients_store.clone()) + .await?; + websocket.start(); + } else { + info!("node not running in entry mode: the websocket will remain closed"); } - tokio::spawn(async move { - if let Err(err) = entry_gateway.run().await { - error!("the entry gateway subtask has failed with the following message: {err}") - } - }); - Ok(()) - } + // if we're running in exit mode, start the IPR and NR + if self.modes().exit { + info!("starting the exit service providers: NR + IPR"); + gateway_tasks_builder.set_network_requester_opts(config.nr_opts); + gateway_tasks_builder.set_ip_packet_router_opts(config.ipr_opts); + + let exit_sps = gateway_tasks_builder.build_exit_service_providers( + topology_provider.clone(), + topology_provider.clone(), + )?; + + // note, this has all the joinhandles for when we want to use joinset + let (started_nr, started_ipr) = exit_sps.start_service_providers().await?; + active_clients_store.insert_embedded(started_nr.handle); + active_clients_store.insert_embedded(started_ipr.handle); + info!("started NR at: {}", started_nr.on_start_data.address); + info!("started IPR at: {}", started_ipr.on_start_data.address); + } else { + info!("node not running in exit mode: the exit service providers (NR + IPR) will remain unavailable"); + } - fn start_exit_gateway(self, task_client: TaskClient) -> Result<(), NymNodeError> { - info!("going to start the nym-node in EXIT GATEWAY mode"); + // if we're running wireguard, start the authenticator + // and the actual wireguard listener + if self.config.wireguard.enabled { + info!("starting the wireguard tasks: authenticator service provider + wireguard peer controller"); - let config = - ephemeral_exit_gateway_config(self.config.clone(), &self.entry_gateway.mnemonic)?; + gateway_tasks_builder.set_authenticator_opts(config.auth_opts); - let mut exit_gateway = Gateway::new_loaded( - config.gateway, - config.nr_opts, - config.ipr_opts, - Some(config.auth_opts), - self.ed25519_identity_keys.clone(), - self.x25519_sphinx_keys.clone(), - self.exit_gateway.client_storage.clone(), - bin_info!().into(), - self.exit_gateway.stats_storage.clone(), - ); - exit_gateway.set_task_client(task_client); - exit_gateway.set_session_stats(self.entry_gateway.sessions_stats.clone()); //Weird naming I'll give you that, but Andrew is gonna rework it anyway - if self.config.wireguard.enabled { - exit_gateway.set_wireguard_data(self.wireguard.into()); + // that's incredibly nasty, but unfortunately to change it, would require some refactoring... + let Some(wg_data) = self.wireguard.take() else { + return Err(NymNodeError::WireguardDataUnavailable); + }; + + gateway_tasks_builder.set_wireguard_data(wg_data.into()); + + let authenticator = gateway_tasks_builder + .build_wireguard_authenticator(topology_provider) + .await?; + let started_authenticator = authenticator.start_service_provider().await?; + active_clients_store.insert_embedded(started_authenticator.handle); + + info!( + "started authenticator at: {}", + started_authenticator.on_start_data.address + ); + + gateway_tasks_builder + .try_start_wireguard() + .await + .map_err(NymNodeError::GatewayTasksStartupFailure)?; + } else { + info!("node not running with wireguard: authenticator service provider and wireguard will remain unavailable"); } - tokio::spawn(async move { - if let Err(err) = exit_gateway.run().await { - error!("the exit gateway subtask has failed with the following message: {err}") - } - }); Ok(()) } - pub(crate) async fn build_http_server(&self) -> Result<NymNodeHTTPServer, NymNodeError> { + pub(crate) async fn build_http_server(&self) -> Result<NymNodeHttpServer, NymNodeError> { let host_details = sign_host_details( &self.config, self.x25519_sphinx_keys.public_key(), @@ -651,7 +667,7 @@ impl NymNode { let auxiliary_details = api_requests::v1::node::models::AuxiliaryDetails { location: self.config.host.location, announce_ports: AnnouncePorts { - verloc_port: self.config.mixnode.verloc.announce_port, + verloc_port: self.config.verloc.announce_port, mix_port: self.config.mixnet.announce_port, }, accepted_operator_terms_and_conditions: self.accepted_operator_terms_and_conditions, @@ -664,7 +680,7 @@ impl NymNode { let wireguard = if self.config.wireguard.enabled { Some(api_requests::v1::gateway::models::Wireguard { port: self.config.wireguard.announced_port, - public_key: self.wireguard.inner.keypair().public_key().to_string(), + public_key: self.x25519_wireguard_key()?.to_string(), }) } else { None @@ -672,13 +688,13 @@ impl NymNode { let mixnet_websockets = Some(api_requests::v1::gateway::models::WebSockets { ws_port: self .config - .entry_gateway + .gateway_tasks .announce_ws_port - .unwrap_or(self.config.entry_gateway.bind_address.port()), - wss_port: self.config.entry_gateway.announce_wss_port, + .unwrap_or(self.config.gateway_tasks.bind_address.port()), + wss_port: self.config.gateway_tasks.announce_wss_port, }); let gateway_details = api_requests::v1::gateway::models::Gateway { - enforces_zk_nyms: self.config.entry_gateway.enforce_zk_nyms, + enforces_zk_nyms: self.config.gateway_tasks.enforce_zk_nyms, client_interfaces: api_requests::v1::gateway::models::ClientInterfaces { wireguard, mixnet_websockets, @@ -687,20 +703,20 @@ impl NymNode { // exit gateway info let nr_details = api_requests::v1::network_requester::models::NetworkRequester { - encoded_identity_key: self.exit_gateway.nr_ed25519.to_base58_string(), - encoded_x25519_key: self.exit_gateway.nr_x25519.to_base58_string(), + encoded_identity_key: self.service_providers.nr_ed25519.to_base58_string(), + encoded_x25519_key: self.service_providers.nr_x25519.to_base58_string(), address: self.exit_network_requester_address().to_string(), }; let ipr_details = api_requests::v1::ip_packet_router::models::IpPacketRouter { - encoded_identity_key: self.exit_gateway.ipr_ed25519.to_base58_string(), - encoded_x25519_key: self.exit_gateway.ipr_x25519.to_base58_string(), + encoded_identity_key: self.service_providers.ipr_ed25519.to_base58_string(), + encoded_x25519_key: self.service_providers.ipr_x25519.to_base58_string(), address: self.exit_ip_packet_router_address().to_string(), }; let auth_details = api_requests::v1::authenticator::models::Authenticator { - encoded_identity_key: self.exit_gateway.auth_ed25519.to_base58_string(), - encoded_x25519_key: self.exit_gateway.auth_x25519.to_base58_string(), + encoded_identity_key: self.service_providers.auth_ed25519.to_base58_string(), + encoded_x25519_key: self.service_providers.auth_x25519.to_base58_string(), address: self.exit_authenticator_address().to_string(), }; @@ -709,7 +725,7 @@ impl NymNode { enabled: true, upstream_source: self .config - .exit_gateway + .service_providers .upstream_exit_policy_url .to_string(), last_updated: 0, @@ -717,7 +733,7 @@ impl NymNode { policy: None, }; - let mut config = nym_node_http_api::Config::new(bin_info_owned!(), host_details) + let mut config = HttpServerConfig::new(host_details) .with_landing_page_assets(self.config.http.landing_page_assets_path.as_ref()) .with_mixnode_details(mixnode_details) .with_gateway_details(gateway_details) @@ -734,29 +750,33 @@ impl NymNode { self.config.http.expose_crypto_hardware, )) } - match self.config.mode { - NodeMode::Mixnode => config.api.v1_config.node.roles.mixnode_enabled = true, - NodeMode::EntryGateway => config.api.v1_config.node.roles.gateway_enabled = true, - NodeMode::ExitGateway => { - config.api.v1_config.node.roles.gateway_enabled = true; - config.api.v1_config.node.roles.network_requester_enabled = true; - config.api.v1_config.node.roles.ip_packet_router_enabled = true; - } + if self.config.modes.mixnode { + config.api.v1_config.node.roles.mixnode_enabled = true; + } + + if self.config.modes.entry { + config.api.v1_config.node.roles.gateway_enabled = true } - let app_state = AppState::new() - .with_mixing_stats(self.mixnode.mixing_stats.clone()) - .with_sessions_stats(self.entry_gateway.sessions_stats.clone()) - .with_verloc_stats(self.verloc_stats.clone()) + if self.config.modes.exit { + config.api.v1_config.node.roles.network_requester_enabled = true; + config.api.v1_config.node.roles.ip_packet_router_enabled = true; + } + + let app_state = AppState::new(self.metrics.clone(), self.verloc_stats.clone()) .with_metrics_key(self.config.http.access_token.clone()); - Ok(NymNodeRouter::new(config, Some(app_state)) + Ok(NymNodeRouter::new(config, app_state) .build_server(&self.config.http.bind_address) .await?) } + fn user_agent(&self) -> UserAgent { + bin_info!().into() + } + async fn try_refresh_remote_nym_api_cache(&self) { - info!("attempting to request described cache request from nym-api..."); + info!("attempting to request described cache refresh from nym-api..."); if self.config.mixnet.nym_api_urls.is_empty() { warn!("no nym-api urls available"); return; @@ -764,7 +784,7 @@ impl NymNode { for nym_api in &self.config.mixnet.nym_api_urls { info!("trying {nym_api}..."); - let client = NymApiClient::new_with_user_agent(nym_api.clone(), bin_info_owned!()); + let client = NymApiClient::new_with_user_agent(nym_api.clone(), self.user_agent()); // make new request every time in case previous one takes longer and invalidates the signature let request = NodeRefreshBody::new(self.ed25519_identity_keys.private_key()); @@ -787,7 +807,154 @@ impl NymNode { } } - pub(crate) async fn run(self) -> Result<(), NymNodeError> { + pub(crate) fn start_verloc_measurements(&self, shutdown: TaskClient) { + info!( + "Starting the [verloc] round-trip-time measurer on {} ...", + self.config.verloc.bind_address + ); + + let mut base_agent = self.user_agent(); + base_agent.application = format!("{}-verloc", base_agent.application); + let config = nym_verloc::measurements::ConfigBuilder::new( + self.config.mixnet.nym_api_urls.clone(), + base_agent, + ) + .listening_address(self.config.verloc.bind_address) + .packets_per_node(self.config.verloc.debug.packets_per_node) + .connection_timeout(self.config.verloc.debug.connection_timeout) + .packet_timeout(self.config.verloc.debug.packet_timeout) + .delay_between_packets(self.config.verloc.debug.delay_between_packets) + .tested_nodes_batch_size(self.config.verloc.debug.tested_nodes_batch_size) + .testing_interval(self.config.verloc.debug.testing_interval) + .retry_timeout(self.config.verloc.debug.retry_timeout) + .build(); + + let mut verloc_measurer = + VerlocMeasurer::new(config, self.ed25519_identity_keys.clone(), shutdown); + verloc_measurer.set_shared_state(self.verloc_stats.clone()); + tokio::spawn(async move { verloc_measurer.run().await }); + } + + pub(crate) fn setup_metrics_backend(&self, shutdown: TaskClient) -> MetricEventsSender { + info!("setting up node metrics..."); + + // aggregator (to listen for any metrics events) + let mut metrics_aggregator = MetricsAggregator::new( + self.config.metrics.debug.aggregator_update_rate, + shutdown.fork("aggregator"), + ); + + // >>>> START: register all relevant handlers for custom events + + // legacy metrics updater on the deprecated endpoint + metrics_aggregator.register_handler( + LegacyMixingStatsUpdater::new(self.metrics.clone()), + self.config.metrics.debug.legacy_mixing_metrics_update_rate, + ); + + // stats for gateway client sessions (websocket-related information) + metrics_aggregator.register_handler( + GatewaySessionStatsHandler::new( + self.metrics.clone(), + self.entry_gateway.stats_storage.clone(), + ), + self.config.metrics.debug.clients_sessions_update_rate, + ); + + // handler for periodically cleaning up stale recipient/sender darta + metrics_aggregator.register_handler( + MixnetMetricsCleaner::new(self.metrics.clone()), + self.config.metrics.debug.stale_mixnet_metrics_cleaner_rate, + ); + + // note: we're still measuring things such as number of mixed packets, + // but since they're stored as atomic integers, they are incremented directly at source + // rather than going through event pipeline + // should we need custom mixnet events, we can add additional handler for that. that's not a problem + + // >>>> END: register all relevant handlers + + // console logger to preserve old mixnode functionalities + // if self.config.logging.debug.log_to_console { + if self.config.metrics.debug.log_stats_to_console { + ConsoleLogger::new( + self.config.metrics.debug.console_logging_update_interval, + self.metrics.clone(), + shutdown.named("metrics-console-logger"), + ) + .start(); + } else { + let mut shutdown = shutdown; + shutdown.disarm() + } + + let events_sender = metrics_aggregator.sender(); + + // spawn the aggregator task + metrics_aggregator.start(); + + events_sender + } + + pub(crate) fn start_mixnet_listener( + &self, + active_clients_store: &ActiveClientsStore, + shutdown: TaskClient, + ) -> MixForwardingSender { + let processing_config = ProcessingConfig::new(&self.config); + + // we're ALWAYS listening for mixnet packets, either for forward or final hops (or both) + info!( + "Starting the mixnet listener... on {} (forward: {}, final hop: {}))", + self.config.mixnet.bind_address, + processing_config.forward_hop_processing_enabled, + processing_config.final_hop_processing_enabled + ); + + let mixnet_client_config = nym_mixnet_client::Config::new( + self.config.mixnet.debug.packet_forwarding_initial_backoff, + self.config.mixnet.debug.packet_forwarding_maximum_backoff, + self.config.mixnet.debug.initial_connection_timeout, + self.config.mixnet.debug.maximum_connection_buffer_size, + ); + let mixnet_client = nym_mixnet_client::Client::new(mixnet_client_config); + + let mut packet_forwarder = PacketForwarder::new( + mixnet_client, + self.metrics.clone(), + shutdown.fork("mix-packet-forwarder"), + ); + let mix_packet_sender = packet_forwarder.sender(); + tokio::spawn(async move { packet_forwarder.run().await }); + + let final_hop_data = SharedFinalHopData::new( + active_clients_store.clone(), + self.entry_gateway.client_storage.clone(), + ); + + let shared = mixnet::SharedData::new( + processing_config, + self.x25519_sphinx_keys.private_key(), + mix_packet_sender.clone(), + final_hop_data, + self.metrics.clone(), + shutdown, + ); + + mixnet::Listener::new(self.config.mixnet.bind_address, shared).start(); + mix_packet_sender + } + + pub(crate) async fn run(mut self) -> Result<(), NymNodeError> { + info!("starting Nym Node {} with the following modes: mixnode: {}, entry: {}, exit: {}, wireguard: {}", + self.ed25519_identity_key(), + self.config.modes.mixnode, + self.config.modes.entry, + self.config.modes.exit, + self.config.wireguard.enabled + ); + debug!("config: {:#?}", self.config); + let mut task_manager = TaskManager::default().named("NymNode"); let http_server = self .build_http_server() @@ -796,29 +963,32 @@ impl NymNode { let bind_address = self.config.http.bind_address; tokio::spawn(async move { { - info!("Started NymNodeHTTPServer on {bind_address}"); + info!("started NymNodeHTTPServer on {bind_address}"); http_server.run().await } }); self.try_refresh_remote_nym_api_cache().await; - match self.config.mode { - NodeMode::Mixnode => { - self.start_mixnode(task_manager.subscribe_named("mixnode"))?; - let _ = task_manager.catch_interrupt().await; - Ok(()) - } - NodeMode::EntryGateway => { - self.start_entry_gateway(task_manager.subscribe_named("entry-gateway"))?; - let _ = task_manager.catch_interrupt().await; - Ok(()) - } - NodeMode::ExitGateway => { - self.start_exit_gateway(task_manager.subscribe_named("exit-gateway"))?; - let _ = task_manager.catch_interrupt().await; - Ok(()) - } - } + self.start_verloc_measurements(task_manager.subscribe_named("verloc-measurements")); + + let metrics_sender = self.setup_metrics_backend(task_manager.subscribe_named("metrics")); + let active_clients_store = ActiveClientsStore::new(); + + let mix_packet_sender = self.start_mixnet_listener( + &active_clients_store, + task_manager.subscribe_named("mixnet-traffic"), + ); + + self.start_gateway_tasks( + metrics_sender, + active_clients_store, + mix_packet_sender, + task_manager.subscribe_named("gateway-tasks"), + ) + .await?; + + let _ = task_manager.catch_interrupt().await; + Ok(()) } } diff --git a/nym-node/src/node/shared_topology.rs b/nym-node/src/node/shared_topology.rs new file mode 100644 index 00000000000..594fb5f4fb3 --- /dev/null +++ b/nym-node/src/node/shared_topology.rs @@ -0,0 +1,98 @@ +// Copyright 2024 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: GPL-3.0-only + +use async_trait::async_trait; +use nym_gateway::node::{NymApiTopologyProvider, NymApiTopologyProviderConfig, UserAgent}; +use nym_topology::{gateway, NymTopology, TopologyProvider}; +use std::sync::Arc; +use std::time::Duration; +use time::OffsetDateTime; +use tokio::sync::Mutex; +use tracing::debug; +use url::Url; + +// I wouldn't be surprised if this became the start of the node topology cache + +#[derive(Clone)] +pub struct NymNodeTopologyProvider { + inner: Arc<Mutex<NymNodeTopologyProviderInner>>, +} + +impl NymNodeTopologyProvider { + pub fn new( + gateway_node: gateway::LegacyNode, + cache_ttl: Duration, + user_agent: UserAgent, + nym_api_url: Vec<Url>, + ) -> NymNodeTopologyProvider { + NymNodeTopologyProvider { + inner: Arc::new(Mutex::new(NymNodeTopologyProviderInner { + inner: NymApiTopologyProvider::new( + NymApiTopologyProviderConfig { + min_mixnode_performance: 50, + min_gateway_performance: 0, + }, + nym_api_url, + Some(user_agent), + ), + cache_ttl, + cached_at: OffsetDateTime::UNIX_EPOCH, + cached: None, + gateway_node, + })), + } + } +} + +struct NymNodeTopologyProviderInner { + inner: NymApiTopologyProvider, + cache_ttl: Duration, + cached_at: OffsetDateTime, + cached: Option<NymTopology>, + gateway_node: gateway::LegacyNode, +} + +impl NymNodeTopologyProviderInner { + fn cached_topology(&self) -> Option<NymTopology> { + if let Some(cached_topology) = &self.cached { + if self.cached_at + self.cache_ttl > OffsetDateTime::now_utc() { + return Some(cached_topology.clone()); + } + } + + None + } + + async fn update_cache(&mut self) -> Option<NymTopology> { + let updated_cache = match self.inner.get_new_topology().await { + None => None, + Some(mut base) => { + if !base.gateway_exists(&self.gateway_node.identity_key) { + debug!( + "{} didn't exist in topology. inserting it.", + self.gateway_node.identity_key + ); + base.insert_gateway(self.gateway_node.clone()); + } + Some(base) + } + }; + + self.cached_at = OffsetDateTime::now_utc(); + self.cached = updated_cache.clone(); + + updated_cache + } +} + +#[async_trait] +impl TopologyProvider for NymNodeTopologyProvider { + async fn get_new_topology(&mut self) -> Option<NymTopology> { + let mut guard = self.inner.lock().await; + // check the cache + if let Some(cached) = guard.cached_topology() { + return Some(cached); + } + guard.update_cache().await + } +} diff --git a/nym-node/src/wireguard/error.rs b/nym-node/src/wireguard/error.rs index 3682017e37c..6a0cd7a09e5 100644 --- a/nym-node/src/wireguard/error.rs +++ b/nym-node/src/wireguard/error.rs @@ -4,10 +4,4 @@ use thiserror::Error; #[derive(Debug, Error)] -pub enum WireguardError { - #[error("the client is currently not in the process of being registered")] - RegistrationNotInProgress, - - #[error("the client mac failed to get verified correctly")] - MacVerificationFailure, -} +pub enum WireguardError {} diff --git a/nym-wallet/Cargo.lock b/nym-wallet/Cargo.lock index 81fa6e2a7b2..6d1ec90fd49 100644 --- a/nym-wallet/Cargo.lock +++ b/nym-wallet/Cargo.lock @@ -3104,7 +3104,6 @@ dependencies = [ "log", "pretty_env_logger", "schemars", - "semver", "serde", "utoipa", "vergen", diff --git a/package.json b/package.json index 12d3f610462..d89f2f48a49 100644 --- a/package.json +++ b/package.json @@ -15,10 +15,7 @@ "explorer", "explorer-nextjs", "types", - "clients/validator", - "sdk/typescript/packages/**", - "sdk/typescript/examples/**", - "sdk/typescript/codegen/**" + "clients/validator" ], "scripts": { "nuke": "npx rimraf **/node_modules node_modules", diff --git a/scripts/network_tunnel_manager.sh b/scripts/network_tunnel_manager.sh index c135864d901..93e6f73ada2 100644 --- a/scripts/network_tunnel_manager.sh +++ b/scripts/network_tunnel_manager.sh @@ -27,17 +27,57 @@ fetch_and_display_ipv6() { ipv6_address=$(ip -6 addr show "$network_device" scope global | grep inet6 | awk '{print $2}') if [[ -z "$ipv6_address" ]]; then echo "no global IPv6 address found on $network_device." - elsen + else echo "IPv6 address on $network_device: $ipv6_address" fi } +remove_duplicate_rules() { + local interface=$1 + local script_name=$(basename "$0") + + if [[ -z "$interface" ]]; then + echo "error: no interface specified. please enter the interface (nymwg or nymtun0):" + read -r interface + fi + + if [[ "$interface" != "nymwg" && "$interface" != "nymtun0" ]]; then + echo "error: invalid interface '$interface'. allowed values are 'nymwg' or 'nymtun0'." >&2 + exit 1 + fi + + echo "removing duplicate rules for $interface..." + + iptables-save | grep "$interface" | while read -r line; do + sudo iptables -D ${line#-A } || echo "Failed to delete rule: $line" + done + + ip6tables-save | grep "$interface" | while read -r line; do + sudo ip6tables -D ${line#-A } || echo "Failed to delete rule: $line" + done + + echo "duplicates removed for $interface." + echo "!!-important-!! you need to now reapply the iptables rules for $interface." + if [ "$interface" == "nymwg" ]; then + echo "run: ./$script_name apply_iptables_rules_wg" + else + echo "run: ./$script_name apply_iptables_rules" + fi +} + adjust_ip_forwarding() { ipv6_forwarding_setting="net.ipv6.conf.all.forwarding=1" ipv4_forwarding_setting="net.ipv4.ip_forward=1" + + # remove duplicate entries for these settings from the file + sudo sed -i "/^net.ipv6.conf.all.forwarding=/d" /etc/sysctl.conf + sudo sed -i "/^net.ipv4.ip_forward=/d" /etc/sysctl.conf + echo "$ipv6_forwarding_setting" | sudo tee -a /etc/sysctl.conf echo "$ipv4_forwarding_setting" | sudo tee -a /etc/sysctl.conf - sysctl -p /etc/sysctl.conf + + sudo sysctl -p /etc/sysctl.conf + } apply_iptables_rules() { @@ -45,22 +85,10 @@ apply_iptables_rules() { echo "applying IPtables rules for $interface..." sleep 2 - # remove duplicates for IPv4 - sudo iptables -D FORWARD -i "$interface" -o "$network_device" -j ACCEPT 2>/dev/null || true - sudo iptables -D FORWARD -i "$network_device" -o "$interface" -m state --state RELATED,ESTABLISHED -j ACCEPT 2>/dev/null || true - sudo iptables -t nat -D POSTROUTING -o "$network_device" -j MASQUERADE 2>/dev/null || true - - # remove duplicates for IPv6 - sudo ip6tables -D FORWARD -i "$interface" -o "$network_device" -j ACCEPT 2>/dev/null || true - sudo ip6tables -D FORWARD -i "$network_device" -o "$interface" -m state --state RELATED,ESTABLISHED -j ACCEPT 2>/dev/null || true - sudo ip6tables -t nat -D POSTROUTING -o "$network_device" -j MASQUERADE 2>/dev/null || true - - # add new rules for IPv4 sudo iptables -t nat -A POSTROUTING -o "$network_device" -j MASQUERADE sudo iptables -A FORWARD -i "$interface" -o "$network_device" -j ACCEPT sudo iptables -A FORWARD -i "$network_device" -o "$interface" -m state --state RELATED,ESTABLISHED -j ACCEPT - # add new rules for IPv6 sudo ip6tables -t nat -A POSTROUTING -o "$network_device" -j MASQUERADE sudo ip6tables -A FORWARD -i "$interface" -o "$network_device" -j ACCEPT sudo ip6tables -A FORWARD -i "$network_device" -o "$interface" -m state --state RELATED,ESTABLISHED -j ACCEPT @@ -69,36 +97,6 @@ apply_iptables_rules() { sudo ip6tables-save | sudo tee /etc/iptables/rules.v6 } -apply_iptables_rules_wg() { - local interface=$wg_tunnel_interface - echo "applying IPtables rules for WireGuard ($interface)..." - sleep 2 - - # remove duplicates for IPv4 - sudo iptables -D FORWARD -i "$interface" -o "$network_device" -j ACCEPT 2>/dev/null || true - sudo iptables -D FORWARD -i "$network_device" -o "$interface" -m state --state RELATED,ESTABLISHED -j ACCEPT 2>/dev/null || true - sudo iptables -t nat -D POSTROUTING -o "$network_device" -j MASQUERADE 2>/dev/null || true - - # remove duplicates for IPv6 - sudo ip6tables -D FORWARD -i "$interface" -o "$network_device" -j ACCEPT 2>/dev/null || true - sudo ip6tables -D FORWARD -i "$network_device" -o "$interface" -m state --state RELATED,ESTABLISHED -j ACCEPT 2>/dev/null || true - sudo ip6tables -t nat -D POSTROUTING -o "$network_device" -j MASQUERADE 2>/dev/null || true - - # add new rules for IPv4 - sudo iptables -t nat -A POSTROUTING -o "$network_device" -j MASQUERADE - sudo iptables -A FORWARD -i "$interface" -o "$network_device" -j ACCEPT - sudo iptables -A FORWARD -i "$network_device" -o "$interface" -m state --state RELATED,ESTABLISHED -j ACCEPT - - # add new rules for IPv6 - sudo ip6tables -t nat -A POSTROUTING -o "$network_device" -j MASQUERADE - sudo ip6tables -A FORWARD -i "$interface" -o "$network_device" -j ACCEPT - sudo ip6tables -A FORWARD -i "$network_device" -o "$interface" -m state --state RELATED,ESTABLISHED -j ACCEPT - - sudo iptables-save | sudo tee /etc/iptables/rules.v4 - sudo ip6tables-save | sudo tee /etc/iptables/rules.v6 -} - - check_tunnel_iptables() { local interface=$1 echo "inspecting IPtables rules for $interface..." @@ -135,26 +133,75 @@ perform_pings() { joke_through_tunnel() { local interface=$1 - echo "checking tunnel connectivity and fetching a joke for $interface..." - ipv4_address=$(ip addr show "$interface" | grep 'inet ' | awk '{print $2}' | cut -d'/' -f1) - ipv6_address=$(ip addr show "$interface" | grep 'inet6 ' | awk '{print $2}' | grep -v '^fe80:' | cut -d'/' -f1) + local green="\033[0;32m" + local reset="\033[0m" + local red="\033[0;31m" + local yellow="\033[0;33m" + + sleep 1 + echo + echo -e "${yellow}checking tunnel connectivity and fetching a joke for $interface...${reset}" + echo -e "${yellow}if these test succeeds, it confirms your machine can reach the outside world via IPv4 and IPv6.${reset}" + echo -e "${yellow}however, probes and external clients may experience different connectivity to your nym-node.${reset}" + + ipv4_address=$(ip addr show "$interface" | awk '/inet / {print $2}' | cut -d'/' -f1) + ipv6_address=$(ip addr show "$interface" | awk '/inet6 / && $2 !~ /^fe80/ {print $2}' | cut -d'/' -f1) if [[ -z "$ipv4_address" && -z "$ipv6_address" ]]; then - echo "no IP address found on $interface. Unable to fetch a joke." - return + echo -e "${red}no IP address found on $interface. unable to fetch a joke.${reset}" + echo -e "${red}please verify your tunnel configuration and ensure the interface is up.${reset}" + return 1 fi - + if [[ -n "$ipv4_address" ]]; then - joke=$(curl -s -H "Accept: application/json" --interface "$ipv4_address" https://icanhazdadjoke.com/ | jq -r .joke) - [[ -n "$joke" && "$joke" != "null" ]] && echo "IPv4 joke: $joke" || echo "Failed to fetch a joke via IPv4." + echo + echo -e "------------------------------------" + echo -e "detected IPv4 address: $ipv4_address" + echo -e "testing IPv4 connectivity..." + echo + + if ping -c 1 -I "$ipv4_address" google.com >/dev/null 2>&1; then + echo -e "${green}IPv4 connectivity is working. fetching a joke...${reset}" + joke=$(curl -s -H "Accept: application/json" --interface "$ipv4_address" https://icanhazdadjoke.com/ | jq -r .joke) + [[ -n "$joke" && "$joke" != "null" ]] && echo -e "${green}IPv4 joke: $joke${reset}" || echo -e "failed to fetch a joke via IPv4." + else + echo -e "${red}IPv4 connectivity is not working for $interface. verify your routing and NAT settings.${reset}" + fi fi if [[ -n "$ipv6_address" ]]; then - joke=$(curl -s -H "Accept: application/json" --interface "$ipv6_address" https://icanhazdadjoke.com/ | jq -r .joke) - [[ -n "$joke" && "$joke" != "null" ]] && echo "IPv6 joke: $joke" || echo "Failed to fetch a joke via IPv6." + echo + echo -e "------------------------------------" + echo -e "detected IPv6 address: $ipv6_address" + echo -e "testing IPv6 connectivity..." + echo + + if ping6 -c 1 -I "$ipv6_address" google.com >/dev/null 2>&1; then + echo -e "${green}IPv6 connectivity is working. fetching a joke...${reset}" + joke=$(curl -s -H "Accept: application/json" --interface "$ipv6_address" https://icanhazdadjoke.com/ | jq -r .joke) + [[ -n "$joke" && "$joke" != "null" ]] && echo -e "${green}IPv6 joke: $joke${reset}" || echo -e "${red}failed to fetch a joke via IPv6.${reset}" + else + echo -e "${red}IPv6 connectivity is not working for $interface. verify your routing and NAT settings.${reset}" + fi fi + + echo -e "${green}joke fetching processes completed for $interface.${reset}" + echo -e "------------------------------------" + + sleep 3 + echo + echo + echo -e "${yellow}### connectivity testing recommendations ###${reset}" + echo -e "${yellow}- use the following command to test WebSocket connectivity from an external client:${reset}" + echo -e "${yellow} wscat -c wss://<your-ip-address/ hostname>:9001 ${reset}" + echo -e "${yellow}- test UDP connectivity on port 51822 (commonly used for nym wireguard) ${reset}" + echo -e "${yellow} from another machine, use tools like nc or socat to send UDP packets ${reset}" + echo -e "${yellow} echo 'test message' | nc -u <your-ip-address> 51822 ${reset}" + echo -e "${yellow}if connectivity issues persist, ensure port forwarding and firewall rules are correctly configured ${reset}" + echo } + configure_dns_and_icmp_wg() { echo "allowing icmp (ping)..." sudo iptables -A INPUT -p icmp --icmp-type echo-request -j ACCEPT @@ -167,7 +214,7 @@ configure_dns_and_icmp_wg() { sudo iptables -A INPUT -p tcp --dport 53 -j ACCEPT echo "saving iptables rules..." - sudo iptables-save > /etc/iptables/rules.v4 + sudo iptables-save >/etc/iptables/rules.v4 echo "dns and icmp configuration completed." } @@ -212,6 +259,9 @@ configure_dns_and_icmp_wg) adjust_ip_forwarding) adjust_ip_forwarding ;; +remove_duplicate_rules) + remove_duplicate_rules "$2" + ;; *) echo "Usage: $0 [command]" echo "Commands:" @@ -228,6 +278,7 @@ adjust_ip_forwarding) echo " joke_through_wg_tunnel - Fetch a joke via nymwg." echo " configure_dns_and_icmp_wg - Allows icmp ping tests for probes alongside configuring dns" echo " adjust_ip_forwarding - Enable IPV6 and IPV4 forwarding" + echo " remove_duplicate_rules <interface> - Remove duplicate iptables rules. Valid interfaces: nymwg, nymtun0" exit 1 ;; esac diff --git a/sdk/ffi/go/src/lib.rs b/sdk/ffi/go/src/lib.rs index a5c930a2ec8..a0a690c6913 100644 --- a/sdk/ffi/go/src/lib.rs +++ b/sdk/ffi/go/src/lib.rs @@ -1,6 +1,9 @@ // Copyright 2023-2024 - Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: Apache-2.0 +// due to autogenerated code +#![allow(clippy::empty_line_after_doc_comments)] + use nym_sdk::mixnet::Recipient; use nym_sphinx_anonymous_replies::requests::AnonymousSenderTag; uniffi::include_scaffolding!("bindings"); diff --git a/sdk/rust/nym-sdk/examples/custom_topology_provider.rs b/sdk/rust/nym-sdk/examples/custom_topology_provider.rs index 3a617b6b781..df5f4ef5a65 100644 --- a/sdk/rust/nym-sdk/examples/custom_topology_provider.rs +++ b/sdk/rust/nym-sdk/examples/custom_topology_provider.rs @@ -21,7 +21,7 @@ impl MyTopologyProvider { async fn get_topology(&self) -> NymTopology { let mixnodes = self .validator_client - .get_all_basic_active_mixing_assigned_nodes(None) + .get_all_basic_active_mixing_assigned_nodes() .await .unwrap(); @@ -35,7 +35,7 @@ impl MyTopologyProvider { let gateways = self .validator_client - .get_all_basic_entry_assigned_nodes(None) + .get_all_basic_entry_assigned_nodes() .await .unwrap(); diff --git a/sdk/rust/nym-sdk/examples/geo_topology_provider.rs b/sdk/rust/nym-sdk/examples/geo_topology_provider.rs index e57de3f1ada..3b327d7377d 100644 --- a/sdk/rust/nym-sdk/examples/geo_topology_provider.rs +++ b/sdk/rust/nym-sdk/examples/geo_topology_provider.rs @@ -22,7 +22,6 @@ async fn main() { // We filter on the version of the mixnodes. Be prepared to manually update // this to keep this example working, as we can't (currently) fetch to current // latest version. - "1.1.31".to_string(), group_by, ); diff --git a/sdk/rust/nym-sdk/src/lib.rs b/sdk/rust/nym-sdk/src/lib.rs index 545090e3ae6..5b8afd246c3 100644 --- a/sdk/rust/nym-sdk/src/lib.rs +++ b/sdk/rust/nym-sdk/src/lib.rs @@ -10,12 +10,15 @@ pub mod mixnet; pub mod tcp_proxy; pub use error::{Error, Result}; -pub use nym_client_core::client::{ - mix_traffic::transceiver::*, - topology_control::{ - GeoAwareTopologyProvider, NymApiTopologyProvider, NymApiTopologyProviderConfig, - TopologyProvider, +pub use nym_client_core::{ + client::{ + mix_traffic::transceiver::*, + topology_control::{ + GeoAwareTopologyProvider, NymApiTopologyProvider, NymApiTopologyProviderConfig, + TopologyProvider, + }, }, + config::DebugConfig, }; pub use nym_network_defaults::{ ChainDetails, DenomDetails, DenomDetailsOwned, NymContracts, NymNetworkDetails, diff --git a/service-providers/authenticator/src/authenticator.rs b/service-providers/authenticator/src/authenticator.rs index 1023059d35f..268d96c1efa 100644 --- a/service-providers/authenticator/src/authenticator.rs +++ b/service-providers/authenticator/src/authenticator.rs @@ -7,7 +7,6 @@ use futures::channel::oneshot; use ipnetwork::IpNetwork; use nym_client_core::{HardcodedTopologyProvider, TopologyProvider}; use nym_credential_verification::ecash::EcashManager; -use nym_gateway_storage::Storage; use nym_sdk::{mixnet::Recipient, GatewayTransceiver}; use nym_task::{TaskClient, TaskHandle}; use nym_wireguard::WireguardGatewayData; @@ -25,20 +24,20 @@ impl OnStartData { } } -pub struct Authenticator<S> { +pub struct Authenticator { #[allow(unused)] config: Config, wait_for_gateway: bool, custom_topology_provider: Option<Box<dyn TopologyProvider + Send + Sync>>, custom_gateway_transceiver: Option<Box<dyn GatewayTransceiver + Send + Sync>>, wireguard_gateway_data: WireguardGatewayData, - ecash_verifier: Option<Arc<EcashManager<S>>>, + ecash_verifier: Option<Arc<EcashManager>>, used_private_network_ips: Vec<IpAddr>, shutdown: Option<TaskClient>, on_start: Option<oneshot::Sender<OnStartData>>, } -impl<S: Storage + Clone + 'static> Authenticator<S> { +impl Authenticator { pub fn new( config: Config, wireguard_gateway_data: WireguardGatewayData, @@ -59,7 +58,7 @@ impl<S: Storage + Clone + 'static> Authenticator<S> { #[must_use] #[allow(unused)] - pub fn with_ecash_verifier(mut self, ecash_verifier: Arc<EcashManager<S>>) -> Self { + pub fn with_ecash_verifier(mut self, ecash_verifier: Arc<EcashManager>) -> Self { self.ecash_verifier = Some(ecash_verifier); self } diff --git a/service-providers/authenticator/src/cli/mod.rs b/service-providers/authenticator/src/cli/mod.rs index e8d6e1d1230..08aca6f720e 100644 --- a/service-providers/authenticator/src/cli/mod.rs +++ b/service-providers/authenticator/src/cli/mod.rs @@ -8,8 +8,8 @@ use nym_authenticator::{ config::{helpers::try_upgrade_config, BaseClientConfig, Config}, error::AuthenticatorError, }; +use nym_bin_common::bin_info; use nym_bin_common::completions::{fig_generate, ArgShell}; -use nym_bin_common::{bin_info, version_checker}; use nym_client_core::cli_helpers::CliClient; use std::sync::OnceLock; @@ -171,31 +171,3 @@ async fn try_load_current_config(id: &str) -> Result<Config, AuthenticatorError> Ok(config) } - -// this only checks compatibility between config the binary. It does not take into consideration -// network version. It might do so in the future. -fn version_check(cfg: &Config) -> bool { - let binary_version = env!("CARGO_PKG_VERSION"); - let config_version = &cfg.base.client.version; - if binary_version == config_version { - true - } else { - log::warn!( - "The native-client binary has different version than what is specified \ - in config file! {binary_version} and {config_version}", - ); - if version_checker::is_minor_version_compatible(binary_version, config_version) { - log::info!( - "but they are still semver compatible. \ - However, consider running the `upgrade` command" - ); - true - } else { - log::error!( - "and they are semver incompatible! - \ - please run the `upgrade` command before attempting `run` again" - ); - false - } - } -} diff --git a/service-providers/authenticator/src/cli/request.rs b/service-providers/authenticator/src/cli/request.rs index eca46a1892b..0a8dfe3cd55 100644 --- a/service-providers/authenticator/src/cli/request.rs +++ b/service-providers/authenticator/src/cli/request.rs @@ -1,9 +1,9 @@ // Copyright 2024 - Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: Apache-2.0 +use crate::cli::try_load_current_config; use crate::cli::AuthenticatorError; use crate::cli::{override_config, OverrideConfig}; -use crate::cli::{try_load_current_config, version_check}; use clap::{Args, Subcommand}; use nym_authenticator_requests::latest::{ registration::{ClientMac, FinalMessage, GatewayClient, InitMessage, IpPair}, @@ -96,11 +96,6 @@ pub(crate) async fn execute(args: &Request) -> Result<(), AuthenticatorError> { let mut config = try_load_current_config(&args.common_args.id).await?; config = override_config(config, OverrideConfig::from(args.clone())); - if !version_check(&config) { - log::error!("failed the local version check"); - return Err(AuthenticatorError::FailedLocalVersionCheck); - } - let shutdown = TaskHandle::default(); let mixnet_client = nym_authenticator::mixnet_client::create_mixnet_client( &config.base, diff --git a/service-providers/authenticator/src/cli/run.rs b/service-providers/authenticator/src/cli/run.rs index 42f9e9ab0e0..b190556269a 100644 --- a/service-providers/authenticator/src/cli/run.rs +++ b/service-providers/authenticator/src/cli/run.rs @@ -1,20 +1,17 @@ // Copyright 2024 - Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: Apache-2.0 -use std::sync::Arc; - use crate::cli::peer_handler::DummyHandler; +use crate::cli::try_load_current_config; use crate::cli::{override_config, OverrideConfig}; -use crate::cli::{try_load_current_config, version_check}; use clap::Args; -use log::error; use nym_authenticator::error::AuthenticatorError; use nym_client_core::cli_helpers::client_run::CommonClientRunArgs; use nym_crypto::asymmetric::x25519::KeyPair; -use nym_gateway_storage::PersistentStorage; use nym_task::TaskHandle; use nym_wireguard::WireguardGatewayData; use rand::rngs::OsRng; +use std::sync::Arc; #[allow(clippy::struct_excessive_bools)] #[derive(Args, Clone)] @@ -38,11 +35,6 @@ pub(crate) async fn execute(args: &Run) -> Result<(), AuthenticatorError> { config = override_config(config, OverrideConfig::from(args.clone())); log::debug!("Using config: {:#?}", config); - if !version_check(&config) { - error!("failed the local version check"); - return Err(AuthenticatorError::FailedLocalVersionCheck); - } - log::info!("Starting authenticator service provider"); let (wireguard_gateway_data, peer_rx) = WireguardGatewayData::new( config.authenticator.clone().into(), @@ -54,11 +46,7 @@ pub(crate) async fn execute(args: &Run) -> Result<(), AuthenticatorError> { handler.run().await; }); - let mut server = nym_authenticator::Authenticator::<PersistentStorage>::new( - config, - wireguard_gateway_data, - vec![], - ); + let mut server = nym_authenticator::Authenticator::new(config, wireguard_gateway_data, vec![]); if let Some(custom_mixnet) = &args.common_args.custom_mixnet { server = server.with_stored_topology(custom_mixnet)? } diff --git a/service-providers/authenticator/src/cli/sign.rs b/service-providers/authenticator/src/cli/sign.rs index 5d8306ca840..9e1de18bf05 100644 --- a/service-providers/authenticator/src/cli/sign.rs +++ b/service-providers/authenticator/src/cli/sign.rs @@ -1,7 +1,7 @@ // Copyright 2024 - Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: Apache-2.0 -use crate::cli::{try_load_current_config, version_check}; +use crate::cli::try_load_current_config; use clap::Args; use nym_authenticator::error::AuthenticatorError; use nym_bin_common::output_format::OutputFormat; @@ -57,11 +57,6 @@ fn print_signed_contract_msg( pub(crate) async fn execute(args: &Sign) -> Result<(), AuthenticatorError> { let config = try_load_current_config(&args.id).await?; - if !version_check(&config) { - log::error!("Failed the local version check"); - return Err(AuthenticatorError::FailedLocalVersionCheck); - } - let key_store = OnDiskKeys::new(config.storage_paths.common_paths.keys); let identity_keypair = key_store.load_identity_keypair().map_err(|source| { AuthenticatorError::ClientCoreError(ClientCoreError::KeyStoreError { diff --git a/service-providers/authenticator/src/error.rs b/service-providers/authenticator/src/error.rs index 2f272e4d165..0430a0805ba 100644 --- a/service-providers/authenticator/src/error.rs +++ b/service-providers/authenticator/src/error.rs @@ -23,9 +23,6 @@ pub enum AuthenticatorError { #[error("received too short packet")] ShortPacket, - #[error("failed local version check, client and config mismatch")] - FailedLocalVersionCheck, - #[error("failed to connect to mixnet: {source}")] FailedToConnectToMixnet { source: nym_sdk::Error }, @@ -45,7 +42,7 @@ pub enum AuthenticatorError { FailedToSetupMixnetClient { source: nym_sdk::Error }, #[error("{0}")] - GatewayStorageError(#[from] nym_gateway_storage::error::StorageError), + GatewayStorageError(#[from] nym_gateway_storage::error::GatewayStorageError), #[error("internal error: {0}")] InternalError(String), diff --git a/service-providers/authenticator/src/mixnet_listener.rs b/service-providers/authenticator/src/mixnet_listener.rs index 8fbd3e2b294..4235a9da151 100644 --- a/service-providers/authenticator/src/mixnet_listener.rs +++ b/service-providers/authenticator/src/mixnet_listener.rs @@ -27,7 +27,6 @@ use nym_credential_verification::{ use nym_credentials_interface::CredentialSpendingData; use nym_crypto::asymmetric::x25519::KeyPair; use nym_gateway_requests::models::CredentialSpendingRequest; -use nym_gateway_storage::Storage; use nym_sdk::mixnet::{InputMessage, MixnetMessageSender, Recipient, TransmissionLane}; use nym_service_provider_requests_common::{Protocol, ServiceProviderType}; use nym_sphinx::receiver::ReconstructedMessage; @@ -55,7 +54,7 @@ impl RegistredAndFree { } } -pub(crate) struct MixnetListener<S> { +pub(crate) struct MixnetListener { // The configuration for the mixnet listener pub(crate) config: Config, @@ -70,19 +69,19 @@ pub(crate) struct MixnetListener<S> { pub(crate) peer_manager: PeerManager, - pub(crate) ecash_verifier: Option<Arc<EcashManager<S>>>, + pub(crate) ecash_verifier: Option<Arc<EcashManager>>, pub(crate) timeout_check_interval: IntervalStream, } -impl<S: Storage + Clone + 'static> MixnetListener<S> { +impl MixnetListener { pub fn new( config: Config, free_private_network_ips: PrivateIPs, wireguard_gateway_data: WireguardGatewayData, mixnet_client: nym_sdk::mixnet::MixnetClient, task_handle: TaskHandle, - ecash_verifier: Option<Arc<EcashManager<S>>>, + ecash_verifier: Option<Arc<EcashManager>>, ) -> Self { let timeout_check_interval = IntervalStream::new(tokio::time::interval(DEFAULT_REGISTRATION_TIMEOUT_CHECK)); @@ -316,6 +315,7 @@ impl<S: Storage + Clone + 'static> MixnetListener<S> { .filter(|r| r.1.is_none()) .choose(&mut thread_rng()) .ok_or(AuthenticatorError::NoFreeIp)?; + let private_ips = *private_ip_ref.0; // mark it as used, even though it's not final *private_ip_ref.1 = Some(SystemTime::now()); let gateway_data = GatewayClient::new( @@ -337,11 +337,12 @@ impl<S: Storage + Clone + 'static> MixnetListener<S> { v1::response::AuthenticatorResponse::new_pending_registration_success( v1::registration::RegistrationData { nonce: registration_data.nonce, - gateway_data: v1::GatewayClient { - pub_key: gateway_data.pub_key, - private_ip: gateway_data.private_ips.ipv4.into(), - mac: v1::ClientMac::new(gateway_data.mac.to_vec()), - }, + gateway_data: v1::registration::GatewayClient::new( + self.keypair().private_key(), + remote_public.inner(), + private_ips.ipv4.into(), + nonce, + ), wg_port: registration_data.wg_port, }, request_id, @@ -356,7 +357,12 @@ impl<S: Storage + Clone + 'static> MixnetListener<S> { v2::response::AuthenticatorResponse::new_pending_registration_success( v2::registration::RegistrationData { nonce: registration_data.nonce, - gateway_data: registration_data.gateway_data.into(), + gateway_data: v2::registration::GatewayClient::new( + self.keypair().private_key(), + remote_public.inner(), + private_ips.ipv4.into(), + nonce, + ), wg_port: registration_data.wg_port, }, request_id, @@ -371,7 +377,12 @@ impl<S: Storage + Clone + 'static> MixnetListener<S> { v3::response::AuthenticatorResponse::new_pending_registration_success( v3::registration::RegistrationData { nonce: registration_data.nonce, - gateway_data: registration_data.gateway_data.into(), + gateway_data: v3::registration::GatewayClient::new( + self.keypair().private_key(), + remote_public.inner(), + private_ips.ipv4.into(), + nonce, + ), wg_port: registration_data.wg_port, }, request_id, @@ -519,7 +530,7 @@ impl<S: Storage + Clone + 'static> MixnetListener<S> { } async fn credential_verification( - ecash_verifier: Arc<EcashManager<S>>, + ecash_verifier: Arc<EcashManager>, credential: CredentialSpendingData, client_id: i64, ) -> Result<i64> { diff --git a/service-providers/ip-packet-router/src/cli/mod.rs b/service-providers/ip-packet-router/src/cli/mod.rs index ee746f48c96..11c2a0e049f 100644 --- a/service-providers/ip-packet-router/src/cli/mod.rs +++ b/service-providers/ip-packet-router/src/cli/mod.rs @@ -1,8 +1,8 @@ use crate::cli::ecash::Ecash; use clap::{CommandFactory, Parser, Subcommand}; use log::error; +use nym_bin_common::bin_info; use nym_bin_common::completions::{fig_generate, ArgShell}; -use nym_bin_common::{bin_info, version_checker}; use nym_client_core::cli_helpers::CliClient; use nym_ip_packet_router::config::helpers::try_upgrade_config; use nym_ip_packet_router::config::{BaseClientConfig, Config}; @@ -167,31 +167,3 @@ async fn try_load_current_config(id: &str) -> Result<Config, IpPacketRouterError Ok(config) } - -// this only checks compatibility between config the binary. It does not take into consideration -// network version. It might do so in the future. -fn version_check(cfg: &Config) -> bool { - let binary_version = env!("CARGO_PKG_VERSION"); - let config_version = &cfg.base.client.version; - if binary_version == config_version { - true - } else { - log::warn!( - "The native-client binary has different version than what is specified \ - in config file! {binary_version} and {config_version}", - ); - if version_checker::is_minor_version_compatible(binary_version, config_version) { - log::info!( - "but they are still semver compatible. \ - However, consider running the `upgrade` command" - ); - true - } else { - log::error!( - "and they are semver incompatible! - \ - please run the `upgrade` command before attempting `run` again" - ); - false - } - } -} diff --git a/service-providers/ip-packet-router/src/cli/run.rs b/service-providers/ip-packet-router/src/cli/run.rs index 32d97efddcc..3e4010cb326 100644 --- a/service-providers/ip-packet-router/src/cli/run.rs +++ b/service-providers/ip-packet-router/src/cli/run.rs @@ -1,7 +1,6 @@ +use crate::cli::try_load_current_config; use crate::cli::{override_config, OverrideConfig}; -use crate::cli::{try_load_current_config, version_check}; use clap::Args; -use log::error; use nym_client_core::cli_helpers::client_run::CommonClientRunArgs; use nym_ip_packet_router::error::IpPacketRouterError; @@ -27,11 +26,6 @@ pub(crate) async fn execute(args: &Run) -> Result<(), IpPacketRouterError> { config = override_config(config, OverrideConfig::from(args.clone())); log::debug!("Using config: {:#?}", config); - if !version_check(&config) { - error!("failed the local version check"); - return Err(IpPacketRouterError::FailedLocalVersionCheck); - } - log::info!("Starting ip packet router service provider"); let mut server = nym_ip_packet_router::IpPacketRouter::new(config); if let Some(custom_mixnet) = &args.common_args.custom_mixnet { diff --git a/service-providers/ip-packet-router/src/cli/sign.rs b/service-providers/ip-packet-router/src/cli/sign.rs index 968ce83536f..cb4aea63bd5 100644 --- a/service-providers/ip-packet-router/src/cli/sign.rs +++ b/service-providers/ip-packet-router/src/cli/sign.rs @@ -1,4 +1,4 @@ -use crate::cli::{try_load_current_config, version_check}; +use crate::cli::try_load_current_config; use clap::Args; use nym_bin_common::output_format::OutputFormat; use nym_client_core::client::key_manager::persistence::OnDiskKeys; @@ -54,11 +54,6 @@ fn print_signed_contract_msg( pub(crate) async fn execute(args: &Sign) -> Result<(), IpPacketRouterError> { let config = try_load_current_config(&args.id).await?; - if !version_check(&config) { - log::error!("Failed the local version check"); - return Err(IpPacketRouterError::FailedLocalVersionCheck); - } - let key_store = OnDiskKeys::new(config.storage_paths.common_paths.keys); let identity_keypair = key_store.load_identity_keypair().map_err(|source| { IpPacketRouterError::ClientCoreError(ClientCoreError::KeyStoreError { diff --git a/service-providers/ip-packet-router/src/error.rs b/service-providers/ip-packet-router/src/error.rs index 6a7e5393d7f..28e21e6edd1 100644 --- a/service-providers/ip-packet-router/src/error.rs +++ b/service-providers/ip-packet-router/src/error.rs @@ -23,9 +23,6 @@ pub enum IpPacketRouterError { #[error("failed to validate the loaded config")] ConfigValidationFailure, - #[error("failed local version check, client and config mismatch")] - FailedLocalVersionCheck, - #[error("failed to setup mixnet client: {source}")] FailedToSetupMixnetClient { source: nym_sdk::Error }, diff --git a/service-providers/ip-packet-router/src/ip_packet_router.rs b/service-providers/ip-packet-router/src/ip_packet_router.rs index e002559ca14..cff86d6fb0f 100644 --- a/service-providers/ip-packet-router/src/ip_packet_router.rs +++ b/service-providers/ip-packet-router/src/ip_packet_router.rs @@ -119,7 +119,7 @@ impl IpPacketRouter { log::error!("ip packet router service provider is not yet supported on this platform"); Ok(()) } else { - todo!("service provider is not yet supported on this platform") + unimplemented!("service provider is not yet supported on this platform") } } diff --git a/service-providers/network-requester/Cargo.toml b/service-providers/network-requester/Cargo.toml index a419abd4f6a..1508c7d7755 100644 --- a/service-providers/network-requester/Cargo.toml +++ b/service-providers/network-requester/Cargo.toml @@ -4,7 +4,7 @@ [package] name = "nym-network-requester" license = "GPL-3.0" -version = "1.1.45" +version = "1.1.46" authors.workspace = true edition.workspace = true rust-version = "1.70" diff --git a/service-providers/network-requester/src/cli/mod.rs b/service-providers/network-requester/src/cli/mod.rs index 750575f2fbe..c647913ea6e 100644 --- a/service-providers/network-requester/src/cli/mod.rs +++ b/service-providers/network-requester/src/cli/mod.rs @@ -11,7 +11,6 @@ use clap::{CommandFactory, Parser, Subcommand}; use log::error; use nym_bin_common::bin_info; use nym_bin_common::completions::{fig_generate, ArgShell}; -use nym_bin_common::version_checker; use nym_client_core::cli_helpers::CliClient; use nym_config::OptionalSet; use std::sync::OnceLock; @@ -187,34 +186,6 @@ async fn try_load_current_config(id: &str) -> Result<Config, NetworkRequesterErr Ok(config) } -// this only checks compatibility between config the binary. It does not take into consideration -// network version. It might do so in the future. -fn version_check(cfg: &Config) -> bool { - let binary_version = env!("CARGO_PKG_VERSION"); - let config_version = &cfg.base.client.version; - if binary_version == config_version { - true - } else { - log::warn!( - "The native-client binary has different version than what is specified \ - in config file! {binary_version} and {config_version}", - ); - if version_checker::is_minor_version_compatible(binary_version, config_version) { - log::info!( - "but they are still semver compatible. \ - However, consider running the `upgrade` command" - ); - true - } else { - log::error!( - "and they are semver incompatible! - \ - please run the `upgrade` command before attempting `run` again" - ); - false - } - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/service-providers/network-requester/src/cli/run.rs b/service-providers/network-requester/src/cli/run.rs index 19806b13b82..6a061dd7114 100644 --- a/service-providers/network-requester/src/cli/run.rs +++ b/service-providers/network-requester/src/cli/run.rs @@ -1,13 +1,12 @@ // Copyright 2023 - Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: GPL-3.0-only -use crate::cli::{try_load_current_config, version_check}; +use crate::cli::try_load_current_config; use crate::{ cli::{override_config, OverrideConfig}, error::NetworkRequesterError, }; use clap::Args; -use log::error; use nym_client_core::cli_helpers::client_run::CommonClientRunArgs; #[allow(clippy::struct_excessive_bools)] @@ -58,11 +57,6 @@ pub(crate) async fn execute(args: &Run) -> Result<(), NetworkRequesterError> { ); } - if !version_check(&config) { - error!("failed the local version check"); - return Err(NetworkRequesterError::FailedLocalVersionCheck); - } - log::info!("Starting socks5 service provider"); let mut server = crate::core::NRServiceProviderBuilder::new(config); if let Some(custom_mixnet) = &args.common_args.custom_mixnet { diff --git a/service-providers/network-requester/src/cli/sign.rs b/service-providers/network-requester/src/cli/sign.rs index 9e43de6009c..769c5c5b138 100644 --- a/service-providers/network-requester/src/cli/sign.rs +++ b/service-providers/network-requester/src/cli/sign.rs @@ -1,7 +1,7 @@ // Copyright 2023 - Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: GPL-3.0-only -use crate::cli::{try_load_current_config, version_check}; +use crate::cli::try_load_current_config; use crate::error::NetworkRequesterError; use clap::Args; use nym_bin_common::output_format::OutputFormat; @@ -57,11 +57,6 @@ fn print_signed_contract_msg( pub(crate) async fn execute(args: &Sign) -> Result<(), NetworkRequesterError> { let config = try_load_current_config(&args.id).await?; - if !version_check(&config) { - log::error!("Failed the local version check"); - return Err(NetworkRequesterError::FailedLocalVersionCheck); - } - let key_store = OnDiskKeys::new(config.storage_paths.common_paths.keys); let identity_keypair = key_store.load_identity_keypair().map_err(|source| { NetworkRequesterError::ClientCoreError(ClientCoreError::KeyStoreError { diff --git a/service-providers/network-requester/src/error.rs b/service-providers/network-requester/src/error.rs index 52a3cd8108b..8b15743f2bf 100644 --- a/service-providers/network-requester/src/error.rs +++ b/service-providers/network-requester/src/error.rs @@ -29,9 +29,6 @@ pub enum NetworkRequesterError { #[error("Failed to validate the loaded config")] ConfigValidationFailure, - #[error("failed local version check, client and config mismatch")] - FailedLocalVersionCheck, - #[error("failed to setup mixnet client: {source}")] FailedToSetupMixnetClient { source: nym_sdk::Error }, diff --git a/testnet-faucet/yarn.lock b/testnet-faucet/yarn.lock index 9c97a04c98d..176b837def5 100644 --- a/testnet-faucet/yarn.lock +++ b/testnet-faucet/yarn.lock @@ -1541,7 +1541,7 @@ brace-expansion@^1.1.7: balanced-match "^1.0.0" concat-map "0.0.1" -braces@^3.0.1: +braces@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== @@ -1794,9 +1794,9 @@ create-hmac@^1.1.0, create-hmac@^1.1.4, create-hmac@^1.1.7: sha.js "^2.4.8" cross-spawn@^7.0.2: - version "7.0.3" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" - integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + version "7.0.6" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" + integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== dependencies: path-key "^3.1.0" shebang-command "^2.0.0" @@ -2792,12 +2792,12 @@ merge2@^1.3.0, merge2@^1.4.1: integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== micromatch@^4.0.4: - version "4.0.4" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.4.tgz#896d519dfe9db25fce94ceb7a500919bf881ebf9" - integrity sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg== + version "4.0.8" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202" + integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== dependencies: - braces "^3.0.1" - picomatch "^2.2.3" + braces "^3.0.3" + picomatch "^2.3.1" miller-rabin@^4.0.0: version "4.0.1" @@ -3075,10 +3075,10 @@ picocolors@^1.0.0: resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== -picomatch@^2.2.3: - version "2.3.0" - resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.0.tgz#f1f061de8f6a4bf022892e2d128234fb98302972" - integrity sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw== +picomatch@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== postcss-value-parser@^4.2.0: version "4.2.0" diff --git a/tools/internal/mixnet-connectivity-check/Cargo.toml b/tools/internal/mixnet-connectivity-check/Cargo.toml new file mode 100644 index 00000000000..af55790aa49 --- /dev/null +++ b/tools/internal/mixnet-connectivity-check/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "mixnet-connectivity-check" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true + +[dependencies] +anyhow = { workspace = true } +clap = { workspace = true, features = ["cargo", "derive"] } +futures = { workspace = true } +tracing = { workspace = true } +tokio = { workspace = true, features = ["rt-multi-thread", "signal", "macros"] } + +nym-network-defaults = { path = "../../../common/network-defaults" } +nym-bin-common = { path = "../../../common/bin-common", features = ["basic_tracing", "output_format"] } +nym-crypto = { path = "../../../common/crypto", features = ["asymmetric"] } +nym-sdk = { path = "../../../sdk/rust/nym-sdk" } diff --git a/tools/internal/mixnet-connectivity-check/src/main.rs b/tools/internal/mixnet-connectivity-check/src/main.rs new file mode 100644 index 00000000000..be66b4f443c --- /dev/null +++ b/tools/internal/mixnet-connectivity-check/src/main.rs @@ -0,0 +1,156 @@ +// Copyright 2024 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: GPL-3.0-only + +use clap::{Args, Parser, Subcommand}; +use futures::stream::StreamExt; +use nym_bin_common::output_format::OutputFormat; +use nym_bin_common::{bin_info, bin_info_owned}; +use nym_crypto::asymmetric::ed25519; +use nym_network_defaults::setup_env; +use nym_sdk::mixnet::MixnetMessageSender; +use nym_sdk::{mixnet, DebugConfig}; +use std::sync::OnceLock; +use std::time::Duration; +use tokio::time::timeout; + +fn pretty_build_info_static() -> &'static str { + static PRETTY_BUILD_INFORMATION: OnceLock<String> = OnceLock::new(); + PRETTY_BUILD_INFORMATION.get_or_init(|| bin_info!().pretty_print()) +} + +#[derive(Parser)] +#[clap(author = "Nymtech", version, long_version = pretty_build_info_static(), about)] +pub(crate) struct Cli { + /// Path pointing to an env file that configures the client. + #[clap(short, long)] + pub(crate) config_env_file: Option<std::path::PathBuf>, + + #[clap(subcommand)] + command: Commands, +} + +impl Cli { + async fn execute(self) -> anyhow::Result<()> { + match self.command { + Commands::CheckConnectivity(args) => connectivity_test(args).await?, + Commands::BuildInfo(args) => build_info(args), + } + Ok(()) + } +} + +#[derive(Subcommand)] +#[allow(clippy::large_enum_variant)] +pub(crate) enum Commands { + /// Attempt to run a simple connectivity test + CheckConnectivity(ConnectivityArgs), + + /// Show build information of this binary + BuildInfo(BuildInfoArgs), +} + +#[derive(Args, Clone, Debug)] +struct ConnectivityArgs { + #[clap(long)] + gateway: Option<ed25519::PublicKey>, + + #[clap(long)] + ignore_performance: bool, +} + +#[derive(clap::Args, Debug)] +pub(crate) struct BuildInfoArgs { + #[clap(short, long, default_value_t = OutputFormat::default())] + output: OutputFormat, +} + +fn build_info(args: BuildInfoArgs) { + println!("{}", args.output.format(&bin_info_owned!())) +} + +async fn connectivity_test(args: ConnectivityArgs) -> anyhow::Result<()> { + let env = mixnet::NymNetworkDetails::new_from_env(); + let mut debug_config = DebugConfig::default(); + debug_config.cover_traffic.disable_loop_cover_traffic_stream = true; + debug_config + .traffic + .disable_main_poisson_packet_distribution = true; + + if args.ignore_performance { + debug_config.topology.minimum_mixnode_performance = 0; + debug_config.topology.minimum_gateway_performance = 0; + }; + + let client_builder = mixnet::MixnetClientBuilder::new_ephemeral() + .network_details(env) + .debug_config(debug_config); + + let mixnet_client = if let Some(gateway) = args.gateway { + client_builder + .request_gateway(gateway.to_string()) + .build()? + } else { + client_builder.build()? + }; + + print!("connecting to mixnet... "); + let mut client = match mixnet_client.connect_to_mixnet().await { + Ok(client) => { + println!("✅"); + client + } + Err(err) => { + println!("❌"); + println!("failed to connect: {err}"); + return Err(err.into()); + } + }; + let our_address = client.nym_address(); + + println!("attempting to send a message to ourselves ({our_address})"); + + client + .send_plain_message(*our_address, "hello there") + .await?; + + print!("awaiting response... "); + + match timeout(Duration::from_secs(5), client.next()).await { + Err(_timeout) => { + println!("❌"); + println!("timed out while waiting for the response..."); + } + Ok(Some(received)) => match String::from_utf8(received.message) { + Ok(message) => { + println!("✅"); + println!("received '{message}' back!"); + } + Err(err) => { + println!("❌"); + println!("the received message got malformed on the way to us: {err}"); + } + }, + Ok(None) => { + println!("❌"); + println!("failed to receive any message back..."); + } + } + + println!("disconnecting the client before shutting down..."); + client.disconnect().await; + Ok(()) +} + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + // std::env::set_var( + // "RUST_LOG", + // "debug,handlebars=warn,tendermint_rpc=warn,h2=warn,hyper=warn,rustls=warn,reqwest=warn,tungstenite=warn,async_tungstenite=warn,tokio_util=warn,tokio_tungstenite=warn,tokio-util=warn", + // ); + + let args = Cli::parse(); + setup_env(args.config_env_file.as_ref()); + // setup_tracing_logger(); + + args.execute().await +} diff --git a/tools/internal/testnet-manager/dkg-bypass-contract/src/contract.rs b/tools/internal/testnet-manager/dkg-bypass-contract/src/contract.rs index c04fae8ef29..df70e604f47 100644 --- a/tools/internal/testnet-manager/dkg-bypass-contract/src/contract.rs +++ b/tools/internal/testnet-manager/dkg-bypass-contract/src/contract.rs @@ -33,7 +33,7 @@ pub(crate) struct VkShareIndex<'a> { pub(crate) epoch_id: MultiIndex<'a, EpochId, ContractVKShare, VKShareKey<'a>>, } -impl<'a> IndexList<ContractVKShare> for VkShareIndex<'a> { +impl IndexList<ContractVKShare> for VkShareIndex<'_> { fn get_indexes(&'_ self) -> Box<dyn Iterator<Item = &'_ dyn Index<ContractVKShare>> + '_> { let v: Vec<&dyn Index<ContractVKShare>> = vec![&self.epoch_id]; Box::new(v.into_iter()) @@ -87,7 +87,7 @@ pub fn query(_: Deps<'_>, _: Env, _: EmptyMessage) -> Result<QueryResponse, StdE #[cfg_attr(not(feature = "library"), cosmwasm_std::entry_point)] pub fn migrate(deps: DepsMut<'_>, env: Env, msg: MigrateMsg) -> Result<Response, StdError> { // on migration immediately attempt to rewrite the storage - let threshold = (2 * msg.dealers.len() as u64 + 3 - 1) / 3; + let threshold = (2 * msg.dealers.len() as u64).div_ceil(3); let epoch = CURRENT_EPOCH.load(deps.storage)?; assert_eq!(0, epoch.epoch_id); diff --git a/tools/internal/testnet-manager/src/manager/dkg_skip.rs b/tools/internal/testnet-manager/src/manager/dkg_skip.rs index a132ea69505..77202026652 100644 --- a/tools/internal/testnet-manager/src/manager/dkg_skip.rs +++ b/tools/internal/testnet-manager/src/manager/dkg_skip.rs @@ -58,7 +58,7 @@ impl<'a> FakeDkgKey<'a> { } } -impl<'a> PemStorableKey for FakeDkgKey<'a> { +impl PemStorableKey for FakeDkgKey<'_> { type Error = NetworkManagerError; fn pem_type() -> &'static str { @@ -84,7 +84,7 @@ struct DkgSkipCtx<'a> { ecash_signers: Vec<EcashSignerWithPaths>, } -impl<'a> ProgressCtx for DkgSkipCtx<'a> { +impl ProgressCtx for DkgSkipCtx<'_> { fn progress_tracker(&self) -> &ProgressTracker { &self.progress } @@ -138,7 +138,7 @@ impl NetworkManager { // generate required materials let n = api_endpoints.len(); - let threshold = (2 * n + 3 - 1) / 3; + let threshold = (2 * n).div_ceil(3); let ecash_keys = ttp_keygen(threshold as u64, n as u64)?; diff --git a/tools/internal/testnet-manager/src/manager/local_apis.rs b/tools/internal/testnet-manager/src/manager/local_apis.rs index 631f6117f2f..8ad28e87433 100644 --- a/tools/internal/testnet-manager/src/manager/local_apis.rs +++ b/tools/internal/testnet-manager/src/manager/local_apis.rs @@ -24,7 +24,7 @@ struct LocalApisCtx<'a> { signers: Vec<EcashSignerWithPaths>, } -impl<'a> ProgressCtx for LocalApisCtx<'a> { +impl ProgressCtx for LocalApisCtx<'_> { fn progress_tracker(&self) -> &ProgressTracker { &self.progress } @@ -168,7 +168,7 @@ impl NetworkManager { let id = ctx.signer_id(signer); cmds.push(format!( - "{bin_canon_display} -c {env_canon_display} run --id {id}" + "{bin_canon_display} -c {env_canon_display} run --id {id} --allow-illegal-ips" )); } Ok(RunCommands(cmds)) diff --git a/tools/internal/testnet-manager/src/manager/local_client.rs b/tools/internal/testnet-manager/src/manager/local_client.rs index c037f53719c..f5c8ba173a9 100644 --- a/tools/internal/testnet-manager/src/manager/local_client.rs +++ b/tools/internal/testnet-manager/src/manager/local_client.rs @@ -30,7 +30,7 @@ struct LocalClientCtx<'a> { network: &'a LoadedNetwork, } -impl<'a> ProgressCtx for LocalClientCtx<'a> { +impl ProgressCtx for LocalClientCtx<'_> { fn progress_tracker(&self) -> &ProgressTracker { &self.progress } @@ -96,7 +96,7 @@ impl NetworkManager { let wait_fut = async { let inner_fut = async { loop { - let mut nodes = match api_client.get_all_basic_nodes(None).await { + let nodes = match api_client.get_all_basic_nodes().await { Ok(nodes) => nodes, Err(err) => { ctx.println(format!( @@ -121,9 +121,13 @@ impl NetworkManager { } // otherwise look for ANY node - if let Some(node) = nodes.pop() { - return SocketAddr::new(node.ip_addresses[0], node.entry.unwrap().ws_port); + if let Some(node) = nodes.iter().find(|n| n.supported_roles.entry) { + return SocketAddr::new( + node.ip_addresses[0], + node.entry.as_ref().unwrap().ws_port, + ); } + sleep(Duration::from_secs(10)).await; } }; diff --git a/tools/internal/testnet-manager/src/manager/local_nodes.rs b/tools/internal/testnet-manager/src/manager/local_nodes.rs index dc19d444362..792c67e8fe2 100644 --- a/tools/internal/testnet-manager/src/manager/local_nodes.rs +++ b/tools/internal/testnet-manager/src/manager/local_nodes.rs @@ -7,6 +7,7 @@ use crate::manager::network::LoadedNetwork; use crate::manager::node::NymNode; use crate::manager::NetworkManager; use console::style; +use nym_crypto::asymmetric::ed25519; use nym_mixnet_contract_common::nym_node::Role; use nym_mixnet_contract_common::RoleAssignment; use nym_validator_client::nyxd::contract_traits::MixnetSigningClient; @@ -30,7 +31,7 @@ struct LocalNodesCtx<'a> { gateways: Vec<NymNode>, } -impl<'a> ProgressCtx for LocalNodesCtx<'a> { +impl ProgressCtx for LocalNodesCtx<'_> { fn progress_tracker(&self) -> &ProgressTracker { &self.progress } @@ -89,28 +90,10 @@ impl<'a> LocalNodesCtx<'a> { } } -#[derive(Serialize, Deserialize, Debug)] -#[serde(tag = "node_type")] -pub enum BondingInformationV1 { - Mixnode(MixnodeBondingInformation), - Gateway(GatewayBondingInformation), -} - -#[derive(Serialize, Deserialize, Debug)] -pub struct MixnodeBondingInformation { - pub(crate) version: String, - pub(crate) host: String, - pub(crate) identity_key: String, - pub(crate) sphinx_key: String, -} - -#[derive(Serialize, Deserialize, Debug)] -pub struct GatewayBondingInformation { - pub(crate) version: String, - pub(crate) host: String, - pub(crate) location: String, - pub(crate) identity_key: String, - pub(crate) sphinx_key: String, +#[derive(Debug, Deserialize, Serialize)] +pub struct BondingInformation { + host: String, + identity_key: ed25519::PublicKey, } #[derive(Deserialize)] @@ -168,6 +151,7 @@ impl NetworkManager { "--mnemonic", &Zeroizing::new(node.owner.mnemonic.to_string()), "--local", + "--accept-operator-terms-and-conditions", "--output", "json", "--bonding-information-output", @@ -180,6 +164,9 @@ impl NetworkManager { if is_gateway { cmd.args(["--mode", "entry"]); + } else { + // be explicit about it, even though we don't have to be + cmd.args(["--mode", "mixnode"]); } let mut child = cmd.spawn()?; @@ -190,20 +177,9 @@ impl NetworkManager { } let output_file = fs::File::open(&output_file_path)?; - let bonding_info: BondingInformationV1 = serde_json::from_reader(&output_file)?; - - match bonding_info { - BondingInformationV1::Mixnode(bonding_info) => { - node.identity_key = bonding_info.identity_key; - node.sphinx_key = bonding_info.sphinx_key; - node.version = bonding_info.version; - } - BondingInformationV1::Gateway(bonding_info) => { - node.identity_key = bonding_info.identity_key; - node.sphinx_key = bonding_info.sphinx_key; - node.version = bonding_info.version; - } - } + let bonding_info: BondingInformation = serde_json::from_reader(&output_file)?; + + node.identity_key = bonding_info.identity_key.to_string(); ctx.set_pb_message(format!("generating bonding signature for node {id}...")); @@ -225,6 +201,7 @@ impl NetworkManager { .stderr(Stdio::null()) .kill_on_drop(true) .output(); + let out = ctx.async_with_progress(child).await?; if !out.status.success() { return Err(NetworkManagerError::NymNodeExecutionFailure); diff --git a/tools/internal/testnet-manager/src/manager/node.rs b/tools/internal/testnet-manager/src/manager/node.rs index d744057f329..ec5eeae06d1 100644 --- a/tools/internal/testnet-manager/src/manager/node.rs +++ b/tools/internal/testnet-manager/src/manager/node.rs @@ -14,9 +14,7 @@ pub(crate) struct NymNode { pub(crate) verloc_port: u16, pub(crate) http_port: u16, pub(crate) clients_port: u16, - pub(crate) sphinx_key: String, pub(crate) identity_key: String, - pub(crate) version: String, pub(crate) owner: Account, pub(crate) bonding_signature: String, @@ -29,9 +27,7 @@ impl NymNode { verloc_port: 0, http_port: 0, clients_port: 0, - sphinx_key: "".to_string(), identity_key: "".to_string(), - version: "".to_string(), owner: Account::new(), bonding_signature: "".to_string(), } diff --git a/tools/nym-cli/Cargo.toml b/tools/nym-cli/Cargo.toml index 2a4bdf01861..c156e286797 100644 --- a/tools/nym-cli/Cargo.toml +++ b/tools/nym-cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nym-cli" -version = "1.1.44" +version = "1.1.45" authors.workspace = true edition = "2021" license.workspace = true diff --git a/tools/nymvisor/Cargo.toml b/tools/nymvisor/Cargo.toml index a1f0590c8dc..d44e9fc12b7 100644 --- a/tools/nymvisor/Cargo.toml +++ b/tools/nymvisor/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nymvisor" -version = "0.1.9" +version = "0.1.10" authors.workspace = true repository.workspace = true homepage.workspace = true diff --git a/yarn.lock b/yarn.lock index 40513f47f47..7f26c95e1cb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1219,6 +1219,13 @@ dependencies: regenerator-runtime "^0.14.0" +"@babel/runtime@^7.25.7", "@babel/runtime@^7.26.0": + version "7.26.0" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.26.0.tgz#8600c2f595f277c60815256418b85356a65173c1" + integrity sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw== + dependencies: + regenerator-runtime "^0.14.0" + "@babel/runtime@~7.5.4": version "7.5.5" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.5.5.tgz#74fba56d35efbeca444091c7850ccd494fd2f132" @@ -3095,6 +3102,11 @@ resolved "https://registry.yarnpkg.com/@mui/types/-/types-7.2.15.tgz#dadd232fe9a70be0d526630675dff3b110f30b53" integrity sha512-nbo7yPhtKJkdf9kcVOF8JZHPZTmqXjJ/tI0bdWgHg5tp9AnIN4Y7f7wm9T+0SyGYJk76+GYZ8Q5XaTYAsUHN0Q== +"@mui/types@^7.2.19": + version "7.2.19" + resolved "https://registry.yarnpkg.com/@mui/types/-/types-7.2.19.tgz#c941954dd24393fdce5f07830d44440cf4ab6c80" + integrity sha512-6XpZEM/Q3epK9RN8ENoXuygnqUQxE+siN/6rGRi2iwJPgBUR25mphYQ9ZI87plGh58YoZ5pp40bFvKYOCDJ3tA== + "@mui/utils@^5.10.3", "@mui/utils@^5.15.14", "@mui/utils@^5.7.0": version "5.15.14" resolved "https://registry.yarnpkg.com/@mui/utils/-/utils-5.15.14.tgz#e414d7efd5db00bfdc875273a40c0a89112ade3a" @@ -3117,6 +3129,53 @@ prop-types "^15.8.1" react-is "^18.3.1" +"@mui/utils@^5.16.6 || ^6.0.0": + version "6.1.8" + resolved "https://registry.yarnpkg.com/@mui/utils/-/utils-6.1.8.tgz#ae07cad4f6099eeb43dbc71267b4a96304ba3982" + integrity sha512-O2DWb1kz8hiANVcR7Z4gOB3SvPPsSQGUmStpyBDzde6dJIfBzgV9PbEQOBZd3EBsd1pB+Uv1z5LAJAbymmawrA== + dependencies: + "@babel/runtime" "^7.26.0" + "@mui/types" "^7.2.19" + "@types/prop-types" "^15.7.13" + clsx "^2.1.1" + prop-types "^15.8.1" + react-is "^18.3.1" + +"@mui/x-charts-vendor@7.20.0": + version "7.20.0" + resolved "https://registry.yarnpkg.com/@mui/x-charts-vendor/-/x-charts-vendor-7.20.0.tgz#b5858b91da0bde4f9c31f5360d05ade0b6eb5e31" + integrity sha512-pzlh7z/7KKs5o0Kk0oPcB+sY0+Dg7Q7RzqQowDQjpy5Slz6qqGsgOB5YUzn0L+2yRmvASc4Pe0914Ao3tMBogg== + dependencies: + "@babel/runtime" "^7.25.7" + "@types/d3-color" "^3.1.3" + "@types/d3-delaunay" "^6.0.4" + "@types/d3-interpolate" "^3.0.4" + "@types/d3-scale" "^4.0.8" + "@types/d3-shape" "^3.1.6" + "@types/d3-time" "^3.0.3" + d3-color "^3.1.0" + d3-delaunay "^6.0.4" + d3-interpolate "^3.0.1" + d3-scale "^4.0.2" + d3-shape "^3.2.0" + d3-time "^3.1.0" + delaunator "^5.0.1" + robust-predicates "^3.0.2" + +"@mui/x-charts@^7.22.3": + version "7.22.3" + resolved "https://registry.yarnpkg.com/@mui/x-charts/-/x-charts-7.22.3.tgz#bb8cb23ab368147634e7b9deae5225b9b93869ef" + integrity sha512-w23+AwIK86bpNWkuHewyQwOKi1wYbLDzrvUEqvZ9KVYzZvnqpJmbTKideX1pLVgSNt0On8NDXytzCntV48Nobw== + dependencies: + "@babel/runtime" "^7.25.7" + "@mui/utils" "^5.16.6 || ^6.0.0" + "@mui/x-charts-vendor" "7.20.0" + "@mui/x-internals" "7.21.0" + "@react-spring/rafz" "^9.7.5" + "@react-spring/web" "^9.7.5" + clsx "^2.1.1" + prop-types "^15.8.1" + "@mui/x-data-grid@7.1.1": version "7.1.1" resolved "https://registry.yarnpkg.com/@mui/x-data-grid/-/x-data-grid-7.1.1.tgz#ed4b852bf03c86d39bb4d35eacc35d5d0312f7ed" @@ -3162,6 +3221,14 @@ "@babel/runtime" "^7.24.8" "@mui/utils" "^5.16.5" +"@mui/x-internals@7.21.0": + version "7.21.0" + resolved "https://registry.yarnpkg.com/@mui/x-internals/-/x-internals-7.21.0.tgz#daca984059015b27efdb47bb44dc7ff4a6816673" + integrity sha512-94YNyZ0BhK5Z+Tkr90RKf47IVCW8R/1MvdUhh6MCQg6sZa74jsX+x+gEZ4kzuCqOsuyTyxikeQ8vVuCIQiP7UQ== + dependencies: + "@babel/runtime" "^7.25.7" + "@mui/utils" "^5.16.6 || ^6.0.0" + "@mui/x-tree-view@^7.11.1": version "7.11.1" resolved "https://registry.yarnpkg.com/@mui/x-tree-view/-/x-tree-view-7.11.1.tgz#77748013f368a9bd5f1e5e03adf3d6a788fb0f76" @@ -4306,6 +4373,51 @@ resolved "https://registry.yarnpkg.com/@react-icons/all-files/-/all-files-4.1.0.tgz#477284873a0821928224b6fc84c62d2534d6650b" integrity sha512-hxBI2UOuVaI3O/BhQfhtb4kcGn9ft12RWAFVMUeNjqqhLsHvFtzIkFaptBJpFDANTKoDfdVoHTKZDlwKCACbMQ== +"@react-spring/animated@~9.7.5": + version "9.7.5" + resolved "https://registry.yarnpkg.com/@react-spring/animated/-/animated-9.7.5.tgz#eb0373aaf99b879736b380c2829312dae3b05f28" + integrity sha512-Tqrwz7pIlsSDITzxoLS3n/v/YCUHQdOIKtOJf4yL6kYVSDTSmVK1LI1Q3M/uu2Sx4X3pIWF3xLUhlsA6SPNTNg== + dependencies: + "@react-spring/shared" "~9.7.5" + "@react-spring/types" "~9.7.5" + +"@react-spring/core@~9.7.5": + version "9.7.5" + resolved "https://registry.yarnpkg.com/@react-spring/core/-/core-9.7.5.tgz#72159079f52c1c12813d78b52d4f17c0bf6411f7" + integrity sha512-rmEqcxRcu7dWh7MnCcMXLvrf6/SDlSokLaLTxiPlAYi11nN3B5oiCUAblO72o+9z/87j2uzxa2Inm8UbLjXA+w== + dependencies: + "@react-spring/animated" "~9.7.5" + "@react-spring/shared" "~9.7.5" + "@react-spring/types" "~9.7.5" + +"@react-spring/rafz@^9.7.5", "@react-spring/rafz@~9.7.5": + version "9.7.5" + resolved "https://registry.yarnpkg.com/@react-spring/rafz/-/rafz-9.7.5.tgz#ee7959676e7b5d6a3813e8c17d5e50df98b95df9" + integrity sha512-5ZenDQMC48wjUzPAm1EtwQ5Ot3bLIAwwqP2w2owG5KoNdNHpEJV263nGhCeKKmuA3vG2zLLOdu3or6kuDjA6Aw== + +"@react-spring/shared@~9.7.5": + version "9.7.5" + resolved "https://registry.yarnpkg.com/@react-spring/shared/-/shared-9.7.5.tgz#6d513622df6ad750bbbd4dedb4ca0a653ec92073" + integrity sha512-wdtoJrhUeeyD/PP/zo+np2s1Z820Ohr/BbuVYv+3dVLW7WctoiN7std8rISoYoHpUXtbkpesSKuPIw/6U1w1Pw== + dependencies: + "@react-spring/rafz" "~9.7.5" + "@react-spring/types" "~9.7.5" + +"@react-spring/types@~9.7.5": + version "9.7.5" + resolved "https://registry.yarnpkg.com/@react-spring/types/-/types-9.7.5.tgz#e5dd180f3ed985b44fd2cd2f32aa9203752ef3e8" + integrity sha512-HVj7LrZ4ReHWBimBvu2SKND3cDVUPWKLqRTmWe/fNY6o1owGOX0cAHbdPDTMelgBlVbrTKrre6lFkhqGZErK/g== + +"@react-spring/web@^9.7.5": + version "9.7.5" + resolved "https://registry.yarnpkg.com/@react-spring/web/-/web-9.7.5.tgz#7d7782560b3a6fb9066b52824690da738605de80" + integrity sha512-lmvqGwpe+CSttsWNZVr+Dg62adtKhauGwLyGE/RRyZ8AAMLgb9x3NDMA5RMElXo+IMyTkPp7nxTB8ZQlmhb6JQ== + dependencies: + "@react-spring/animated" "~9.7.5" + "@react-spring/core" "~9.7.5" + "@react-spring/shared" "~9.7.5" + "@react-spring/types" "~9.7.5" + "@react-stately/calendar@^3.4.4": version "3.4.4" resolved "https://registry.yarnpkg.com/@react-stately/calendar/-/calendar-3.4.4.tgz#6e926058ddc8bc9f506c35eaf8a8a04859cb81df" @@ -6330,7 +6442,7 @@ resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-3.2.1.tgz#1f6658e3d2006c4fceac53fde464166859f8b8c5" integrity sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg== -"@types/d3-color@*": +"@types/d3-color@*", "@types/d3-color@^3.1.3": version "3.1.3" resolved "https://registry.yarnpkg.com/@types/d3-color/-/d3-color-3.1.3.tgz#368c961a18de721da8200e80bf3943fb53136af2" integrity sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A== @@ -6340,6 +6452,11 @@ resolved "https://registry.yarnpkg.com/@types/d3-color/-/d3-color-1.4.5.tgz#23bb1afda325549c6314ab60aa2aa28c4c6b1c37" integrity sha512-5sNP3DmtSnSozxcjqmzQKsDOuVJXZkceo1KJScDc1982kk/TS9mTPc6lpli1gTu1MIBF1YWutpHpjucNWcIj5g== +"@types/d3-delaunay@^6.0.4": + version "6.0.4" + resolved "https://registry.yarnpkg.com/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz#185c1a80cc807fdda2a3fe960f7c11c4a27952e1" + integrity sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw== + "@types/d3-dsv@*": version "3.0.7" resolved "https://registry.yarnpkg.com/@types/d3-dsv/-/d3-dsv-3.0.7.tgz#0a351f996dc99b37f4fa58b492c2d1c04e3dac17" @@ -6378,7 +6495,7 @@ dependencies: "@types/d3-color" "^1" -"@types/d3-interpolate@^3.0.1": +"@types/d3-interpolate@^3.0.1", "@types/d3-interpolate@^3.0.4": version "3.0.4" resolved "https://registry.yarnpkg.com/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz#412b90e84870285f2ff8a846c6eb60344f12a41c" integrity sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA== @@ -6390,7 +6507,7 @@ resolved "https://registry.yarnpkg.com/@types/d3-path/-/d3-path-3.1.0.tgz#2b907adce762a78e98828f0b438eaca339ae410a" integrity sha512-P2dlU/q51fkOc/Gfl3Ul9kicV7l+ra934qBFXCFhrZMOL6du1TM0pm1ThYvENukyOn5h9v+yMJ9Fn5JK4QozrQ== -"@types/d3-scale@^4.0.1", "@types/d3-scale@^4.0.2": +"@types/d3-scale@^4.0.1", "@types/d3-scale@^4.0.2", "@types/d3-scale@^4.0.8": version "4.0.8" resolved "https://registry.yarnpkg.com/@types/d3-scale/-/d3-scale-4.0.8.tgz#d409b5f9dcf63074464bf8ddfb8ee5a1f95945bb" integrity sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ== @@ -6402,7 +6519,7 @@ resolved "https://registry.yarnpkg.com/@types/d3-selection/-/d3-selection-1.4.6.tgz#3e6056117b19d8bb6c729c872ca7234622099fb6" integrity sha512-0MhJ/LzJe6/vQVxiYJnvNq5CD/MF6Qy0dLp4BEQ6Dz8oOaB0EMXfx1GGeBFSW+3VzgjaUrxK6uECDQj9VLa/Mg== -"@types/d3-shape@^3.1.0": +"@types/d3-shape@^3.1.0", "@types/d3-shape@^3.1.6": version "3.1.6" resolved "https://registry.yarnpkg.com/@types/d3-shape/-/d3-shape-3.1.6.tgz#65d40d5a548f0a023821773e39012805e6e31a72" integrity sha512-5KKk5aKGu2I+O6SONMYSNflgiP0WfZIQvVUMan50wHsLG1G94JlxEVnCpQARfTtzytuY0p/9PXXZb3I7giofIA== @@ -6414,6 +6531,11 @@ resolved "https://registry.yarnpkg.com/@types/d3-time/-/d3-time-3.0.3.tgz#3c186bbd9d12b9d84253b6be6487ca56b54f88be" integrity sha512-2p6olUZ4w3s+07q3Tm2dbiMZy5pCDfYwtLXXHUnVzXgQlZ/OyPtUz6OL382BkOuGlLXqfT+wqv8Fw2v8/0geBw== +"@types/d3-time@^3.0.3": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@types/d3-time/-/d3-time-3.0.4.tgz#8472feecd639691450dd8000eb33edd444e1323f" + integrity sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g== + "@types/d3-timer@^3.0.0": version "3.0.2" resolved "https://registry.yarnpkg.com/@types/d3-timer/-/d3-timer-3.0.2.tgz#70bbda77dc23aa727413e22e214afa3f0e852f70" @@ -6703,6 +6825,11 @@ resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.12.tgz#12bb1e2be27293c1406acb6af1c3f3a1481d98c6" integrity sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q== +"@types/prop-types@^15.7.13": + version "15.7.13" + resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.13.tgz#2af91918ee12d9d32914feb13f5326658461b451" + integrity sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA== + "@types/qrcode.react@^1.0.2": version "1.0.5" resolved "https://registry.yarnpkg.com/@types/qrcode.react/-/qrcode.react-1.0.5.tgz#d4ddcacee8f34d22a663029a230c5f0ab908cfb7" @@ -10107,11 +10234,18 @@ d3-array@^2.5.0: resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-2.0.0.tgz#8d625cab42ed9b8f601a1760a389f7ea9189d62e" integrity sha512-SPXi0TSKPD4g9tw0NMZFnR95XVgUZiBH+uUTqQuDu1OsE2zomHU7ho0FISciaPvosimixwHFl3WHLGabv6dDgQ== -"d3-color@1 - 3": +"d3-color@1 - 3", d3-color@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-3.1.0.tgz#395b2833dfac71507f12ac2f7af23bf819de24e2" integrity sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA== +d3-delaunay@^6.0.4: + version "6.0.4" + resolved "https://registry.yarnpkg.com/d3-delaunay/-/d3-delaunay-6.0.4.tgz#98169038733a0a5babbeda55054f795bb9e4a58b" + integrity sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A== + dependencies: + delaunator "5" + "d3-dispatch@1 - 2": version "2.0.0" resolved "https://registry.yarnpkg.com/d3-dispatch/-/d3-dispatch-2.0.0.tgz#8a18e16f76dd3fcaef42163c97b926aa9b55e7cf" @@ -10182,7 +10316,7 @@ d3-selection@2, d3-selection@^2.0.0: resolved "https://registry.yarnpkg.com/d3-selection/-/d3-selection-2.0.0.tgz#94a11638ea2141b7565f883780dabc7ef6a61066" integrity sha512-XoGGqhLUN/W14NmaqcO/bb1nqjDAw5WtSYb2X8wiuQWvSZUsUVYsOSkOybUrNvcBjaywBdYPy03eXHMXjk9nZA== -d3-shape@^3.1.0: +d3-shape@^3.1.0, d3-shape@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-3.2.0.tgz#a1a839cbd9ba45f28674c69d7f855bcf91dfc6a5" integrity sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA== @@ -10196,7 +10330,7 @@ d3-shape@^3.1.0: dependencies: d3-time "1 - 3" -"d3-time@1 - 3", "d3-time@2.1.1 - 3", d3-time@^3.0.0: +"d3-time@1 - 3", "d3-time@2.1.1 - 3", d3-time@^3.0.0, d3-time@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-3.1.0.tgz#9310db56e992e3c0175e1ef385e545e48a9bb5c7" integrity sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q== @@ -10489,6 +10623,13 @@ del@^4.1.1: pify "^4.0.1" rimraf "^2.6.3" +delaunator@5, delaunator@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/delaunator/-/delaunator-5.0.1.tgz#39032b08053923e924d6094fe2cde1a99cc51278" + integrity sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw== + dependencies: + robust-predicates "^3.0.2" + delay@^4.4.0: version "4.4.1" resolved "https://registry.yarnpkg.com/delay/-/delay-4.4.1.tgz#6e02d02946a1b6ab98b39262ced965acba2ac4d1" @@ -15438,6 +15579,11 @@ lru-cache@^7.4.4, lru-cache@^7.5.1, lru-cache@^7.7.1: resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.18.3.tgz#f793896e0fd0e954a59dfdd82f0773808df6aa89" integrity sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA== +lucide-react@^0.453.0: + version "0.453.0" + resolved "https://registry.yarnpkg.com/lucide-react/-/lucide-react-0.453.0.tgz#d37909a45a29d89680383a202ee861224b05ba6a" + integrity sha512-kL+RGZCcJi9BvJtzg2kshO192Ddy9hv3ij+cPrVPWSRzgCWCVazoQJxOjAwgK53NomL07HB7GPHW120FimjNhQ== + lunr@^2.3.9: version "2.3.9" resolved "https://registry.yarnpkg.com/lunr/-/lunr-2.3.9.tgz#18b123142832337dd6e964df1a5a7707b25d35e1" @@ -19067,6 +19213,11 @@ ripemd160@^2.0.0, ripemd160@^2.0.1, ripemd160@^2.0.2: hash-base "^3.0.0" inherits "^2.0.1" +robust-predicates@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/robust-predicates/-/robust-predicates-3.0.2.tgz#d5b28528c4824d20fc48df1928d41d9efa1ad771" + integrity sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg== + rollup-plugin-dts@^5.0.0, rollup-plugin-dts@^5.2.0: version "5.3.1" resolved "https://registry.yarnpkg.com/rollup-plugin-dts/-/rollup-plugin-dts-5.3.1.tgz#c2841269a3a5cb986b7791b0328e6a178eba108f"