diff --git a/.github/workflows/ci-check-ns-api-version.yml b/.github/workflows/ci-check-ns-api-version.yml index 00ef062fc3..6eb6508899 100644 --- a/.github/workflows/ci-check-ns-api-version.yml +++ b/.github/workflows/ci-check-ns-api-version.yml @@ -16,7 +16,7 @@ jobs: uses: actions/checkout@v4 - name: Get version from cargo.toml - uses: mikefarah/yq@v4.45.4 + uses: mikefarah/yq@v4.47.1 id: get_version with: cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml diff --git a/.github/workflows/ci-check-nym-stats-api-version.yml b/.github/workflows/ci-check-nym-stats-api-version.yml index 6f3eb75770..cfdddf2b25 100644 --- a/.github/workflows/ci-check-nym-stats-api-version.yml +++ b/.github/workflows/ci-check-nym-stats-api-version.yml @@ -16,7 +16,7 @@ jobs: uses: actions/checkout@v4 - name: Get version from cargo.toml - uses: mikefarah/yq@v4.45.4 + uses: mikefarah/yq@v4.47.1 id: get_version with: cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml diff --git a/.github/workflows/greetings.yml b/.github/workflows/greetings.yml index 6b31063c73..de25d37594 100644 --- a/.github/workflows/greetings.yml +++ b/.github/workflows/greetings.yml @@ -6,7 +6,7 @@ jobs: greeting: runs-on: ubuntu-latest steps: - - uses: actions/first-interaction@v1 + - uses: actions/first-interaction@v3 with: repo-token: ${{ secrets.GITHUB_TOKEN }} issue-message: 'Thank you for raising this issue' diff --git a/.github/workflows/nightly-security-audit.yml b/.github/workflows/nightly-security-audit.yml index db975e2384..aeb9fa08c5 100644 --- a/.github/workflows/nightly-security-audit.yml +++ b/.github/workflows/nightly-security-audit.yml @@ -31,7 +31,7 @@ jobs: - name: Check out repository code uses: actions/checkout@v4 - name: Download report from previous job - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v5 with: name: report path: .github/workflows/support-files/notifications diff --git a/.github/workflows/publish-nyms5-android-apk.yml b/.github/workflows/publish-nyms5-android-apk.yml index 2bdaf2b762..9302f3402c 100644 --- a/.github/workflows/publish-nyms5-android-apk.yml +++ b/.github/workflows/publish-nyms5-android-apk.yml @@ -25,7 +25,7 @@ jobs: uses: actions/checkout@v4 - name: Install Java - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: "temurin" java-version: "17" @@ -91,7 +91,7 @@ jobs: - name: Checkout uses: actions/checkout@v4 - name: Download binary artifact - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v5 with: name: nyms5-apk-arch64 path: apk diff --git a/.github/workflows/push-credential-proxy.yaml b/.github/workflows/push-credential-proxy.yaml index 6cb974030f..8b433b1d62 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.45.4 + uses: mikefarah/yq@v4.47.1 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 4ac48c8603..bd999f45d5 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.45.4 + uses: mikefarah/yq@v4.47.1 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 4c28bf0800..8baa371411 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.45.4 + uses: mikefarah/yq@v4.47.1 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-api.yaml b/.github/workflows/push-node-status-api.yaml index 9087fda7aa..765e28e2e1 100644 --- a/.github/workflows/push-node-status-api.yaml +++ b/.github/workflows/push-node-status-api.yaml @@ -34,18 +34,22 @@ jobs: - name: Get version from cargo.toml id: get_version run: | - yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml + VERSION=$(yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml) + echo "result=$VERSION" >> $GITHUB_OUTPUT - name: Set GIT_TAG variable run: echo "GIT_TAG=${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }}" >> $GITHUB_ENV - - name: Set RELEASE_TAG variable + - name: Initialise RELEASE_TAG + run: echo "RELEASE_TAG=" >> $GITHUB_ENV + + - name: Set RELEASE_TAG for release if: github.event.inputs.release_image == 'true' run: echo "RELEASE_TAG=golden-" >> $GITHUB_ENV - + - name: Set IMAGE_NAME_AND_TAGS variable run: echo "IMAGE_NAME_AND_TAGS=${{ env.CONTAINER_NAME }}:${{ env.RELEASE_TAG }}${{ steps.get_version.outputs.result }}" >> $GITHUB_ENV - + - name: New env vars run: echo "RELEASE_TAG='$RELEASE_TAG' GIT_TAG='$GIT_TAG' IMAGE_NAME_AND_TAGS='$IMAGE_NAME_AND_TAGS'" @@ -65,6 +69,6 @@ jobs: - name: BuildAndPushImageOnHarbor run: | - docker build -f ${{ env.WORKING_DIRECTORY }}/Dockerfile . -t harbor.nymte.ch/nym/${{ env.IMAGE_NAME_AND_TAGS }} -t harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }}:latest + docker build -f ${{ env.WORKING_DIRECTORY }}/Dockerfile-pg . -t harbor.nymte.ch/nym/${{ env.IMAGE_NAME_AND_TAGS }} -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-nym-api.yaml b/.github/workflows/push-nym-api.yaml index 594332b344..79f5fe9ade 100644 --- a/.github/workflows/push-nym-api.yaml +++ b/.github/workflows/push-nym-api.yaml @@ -26,7 +26,7 @@ jobs: git config --global user.name "Lawrence Stalder" - name: Get version from cargo.toml - uses: mikefarah/yq@v4.45.4 + uses: mikefarah/yq@v4.47.1 id: get_version with: cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/nym-api/Cargo.toml diff --git a/.github/workflows/push-nym-node.yaml b/.github/workflows/push-nym-node.yaml index 8ca806af69..05ad829163 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.45.4 + uses: mikefarah/yq@v4.47.1 id: get_version with: cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml diff --git a/.github/workflows/push-nym-statistics-api.yaml b/.github/workflows/push-nym-statistics-api.yaml index 4dd78acc60..93e804c305 100644 --- a/.github/workflows/push-nym-statistics-api.yaml +++ b/.github/workflows/push-nym-statistics-api.yaml @@ -26,7 +26,7 @@ jobs: git config --global user.name "Lawrence Stalder" - name: Get version from cargo.toml - uses: mikefarah/yq@v4.45.4 + uses: mikefarah/yq@v4.47.1 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 index aface6357b..c156a6ecde 100644 --- a/.github/workflows/push-nyx-chain-watcher.yaml +++ b/.github/workflows/push-nyx-chain-watcher.yaml @@ -26,7 +26,7 @@ jobs: git config --global user.name "Lawrence Stalder" - name: Get version from cargo.toml - uses: mikefarah/yq@v4.45.4 + uses: mikefarah/yq@v4.47.1 id: get_version with: cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml diff --git a/.github/workflows/push-validator-rewarder.yaml b/.github/workflows/push-validator-rewarder.yaml index 87a48334f8..7fcb4b166e 100644 --- a/.github/workflows/push-validator-rewarder.yaml +++ b/.github/workflows/push-validator-rewarder.yaml @@ -26,7 +26,7 @@ jobs: git config --global user.name "Lawrence Stalder" - name: Get version from cargo.toml - uses: mikefarah/yq@v4.45.4 + uses: mikefarah/yq@v4.47.1 id: get_version with: cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml diff --git a/Cargo.lock b/Cargo.lock index fe857a3867..50b104fe5d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,7 +11,7 @@ dependencies = [ "macroific", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -408,7 +408,7 @@ dependencies = [ "rustc-hash", "serde", "serde_derive", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -490,7 +490,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -501,7 +501,7 @@ checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -709,7 +709,7 @@ checksum = "57d123550fa8d071b7255cb0cc04dc302baa6c8c4a79f55701552684d8399bce" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -847,6 +847,12 @@ dependencies = [ "serde", ] +[[package]] +name = "binstring" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0669d5a35b64fdb5ab7fb19cae13148b6b5cbdf4b8247faf54ece47f699c8cef" + [[package]] name = "bip32" version = "0.5.3" @@ -942,6 +948,17 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "blake2b_simd" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06e903a20b159e944f91ec8499fe1e55651480c541ea0a584f5d967c49ad9d99" +dependencies = [ + "arrayref", + "arrayvec", + "constant_time_eq", +] + [[package]] name = "blake3" version = "1.8.2" @@ -1323,7 +1340,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -1332,6 +1349,17 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" +[[package]] +name = "coarsetime" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91849686042de1b41cd81490edc83afbcb0abe5a9b6f2c4114f23ce8cca1bcf4" +dependencies = [ + "libc", + "wasix", + "wasm-bindgen", +] + [[package]] name = "colorchoice" version = "1.0.4" @@ -1368,19 +1396,6 @@ dependencies = [ "crossbeam-utils", ] -[[package]] -name = "console" -version = "0.15.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8" -dependencies = [ - "encode_unicode", - "libc", - "once_cell", - "unicode-width 0.2.1", - "windows-sys 0.59.0", -] - [[package]] name = "console" version = "0.16.0" @@ -1576,7 +1591,7 @@ checksum = "a782b93fae93e57ca8ad3e9e994e784583f5933aeaaa5c80a545c4b437be2047" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -1600,7 +1615,7 @@ checksum = "e01c9214319017f6ebd8e299036e1f717fa9bb6724e758f7d6fb2477599d1a29" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -1844,7 +1859,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" dependencies = [ "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -1868,6 +1883,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "ct-codecs" +version = "1.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b10589d1a5e400d61f9f38f12f884cfd080ff345de8f17efda36fe0e4a02aa8" + [[package]] name = "ctr" version = "0.9.2" @@ -1898,7 +1919,7 @@ dependencies = [ "openssl-probe", "openssl-sys", "schannel", - "socket2", + "socket2 0.5.10", "windows-sys 0.59.0", ] @@ -1942,7 +1963,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -2094,7 +2115,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -2105,7 +2126,7 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -2158,7 +2179,7 @@ dependencies = [ "macroific", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -2201,7 +2222,7 @@ checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -2230,7 +2251,7 @@ checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", "unicode-xid", ] @@ -2242,7 +2263,7 @@ checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", "unicode-xid", ] @@ -2311,7 +2332,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -2331,12 +2352,6 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" -[[package]] -name = "dotenv" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" - [[package]] name = "dotenvy" version = "0.15.7" @@ -2370,7 +2385,7 @@ version = "0.1.0" dependencies = [ "cosmwasm-std", "quote", - "syn 1.0.109", + "syn 2.0.106", ] [[package]] @@ -2424,6 +2439,16 @@ dependencies = [ "signature", ] +[[package]] +name = "ed25519-compact" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9b3460f44bea8cd47f45a0c70892f1eff856d97cd55358b2f73f663789f6190" +dependencies = [ + "ct-codecs", + "getrandom 0.2.16", +] + [[package]] name = "ed25519-consensus" version = "2.1.0" @@ -2488,6 +2513,8 @@ dependencies = [ "ff", "generic-array 0.14.7", "group", + "hkdf", + "pem-rfc7468", "pkcs8", "rand_core 0.6.4", "sec1", @@ -2520,7 +2547,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -2650,7 +2677,7 @@ dependencies = [ "macroific", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -2880,7 +2907,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -3349,6 +3376,30 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "hmac-sha1-compact" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18492c9f6f9a560e0d346369b665ad2bdbc89fa9bceca75796584e79042694c3" + +[[package]] +name = "hmac-sha256" +version = "1.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad6880c8d4a9ebf39c6e8b77007ce223f646a4d21ce29d99f70cb16420545425" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "hmac-sha512" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89e8d20b3799fa526152a5301a771eaaad80857f83e01b23216ceaafb2d9280" +dependencies = [ + "digest 0.10.7", +] + [[package]] name = "home" version = "0.5.11" @@ -3492,7 +3543,7 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", - "socket2", + "socket2 0.5.10", "tokio", "tower-service", "tracing", @@ -3582,7 +3633,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2", + "socket2 0.5.10", "tokio", "tower-service", "tracing", @@ -3733,7 +3784,7 @@ checksum = "0ab604ee7085efba6efc65e4ebca0e9533e3aff6cb501d7d77b211e3a781c6d5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -3802,7 +3853,7 @@ dependencies = [ "macroific", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -3827,26 +3878,13 @@ dependencies = [ "serde", ] -[[package]] -name = "indicatif" -version = "0.17.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "183b3088984b400f4cfac3620d5e076c84da5364016b4f49473de574b2586235" -dependencies = [ - "console 0.15.11", - "number_prefix", - "portable-atomic", - "unicode-width 0.2.1", - "web-time", -] - [[package]] name = "indicatif" version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70a646d946d06bedbbc4cac4c218acf4bbf2d87757a784857025f4d447e4e1cd" dependencies = [ - "console 0.16.0", + "console", "portable-atomic", "unicode-width 0.2.1", "unit-prefix", @@ -3917,9 +3955,9 @@ checksum = "8bb03732005da905c88227371639bf1ad885cc712789c011c31c5fb3ab3ccf02" [[package]] name = "inventory" -version = "0.3.20" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab08d7cd2c5897f2c949e5383ea7c7db03fb19130ffcfbf7eda795137ae3cb83" +checksum = "bc61209c082fbeb19919bee74b176221b27223e27b65d781eb91af24eb1fb46e" dependencies = [ "rustversion", ] @@ -3947,7 +3985,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f" dependencies = [ - "socket2", + "socket2 0.5.10", "widestring", "windows-sys 0.48.0", "winreg", @@ -4065,7 +4103,7 @@ checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -4088,6 +4126,32 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "jwt-simple" +version = "0.12.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "731011e9647a71ff4f8474176ff6ce6e0d2de87a0173f15613af3a84c3e3401a" +dependencies = [ + "anyhow", + "binstring", + "blake2b_simd", + "coarsetime", + "ct-codecs", + "ed25519-compact", + "hmac-sha1-compact", + "hmac-sha256", + "hmac-sha512", + "k256", + "p256", + "p384", + "rand 0.8.5", + "serde", + "serde_json", + "superboring", + "thiserror 2.0.12", + "zeroize", +] + [[package]] name = "k256" version = "0.13.4" @@ -4311,7 +4375,7 @@ dependencies = [ "proc-macro2", "quote", "sealed", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -4323,7 +4387,7 @@ dependencies = [ "proc-macro2", "quote", "sealed", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -4336,7 +4400,7 @@ dependencies = [ "macroific_core", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -4364,7 +4428,7 @@ checksum = "ac84fd3f360fcc43dc5f5d186f02a94192761a080e8bc58621ad4d12296a58cf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -4519,9 +4583,9 @@ dependencies = [ [[package]] name = "mock_instant" -version = "0.5.3" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e1d4c44418358edcac6e1d9ce59cea7fb38052429c7704033f1196f0c179e6a" +checksum = "dce6dd36094cac388f119d2e9dc82dc730ef91c32a6222170d630e5414b956e6" [[package]] name = "moka" @@ -4817,12 +4881,6 @@ dependencies = [ "libc", ] -[[package]] -name = "number_prefix" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" - [[package]] name = "nym-api" version = "1.1.65" @@ -4843,7 +4901,7 @@ dependencies = [ "cw3", "cw4", "dashmap", - "dotenv", + "dotenvy", "futures", "humantime-serde", "moka", @@ -4863,6 +4921,7 @@ dependencies = [ "nym-ecash-signer-check", "nym-ecash-time", "nym-gateway-client", + "nym-http-api-client", "nym-http-api-common", "nym-mixnet-contract-common", "nym-node-requests", @@ -4888,6 +4947,7 @@ dependencies = [ "sqlx", "tempfile", "tendermint", + "test-with", "thiserror 2.0.12", "time", "tokio", @@ -4951,6 +5011,25 @@ dependencies = [ "tokio", ] +[[package]] +name = "nym-authenticator-client" +version = "0.1.0" +dependencies = [ + "bincode", + "futures", + "nym-authenticator-requests", + "nym-credentials-interface", + "nym-crypto", + "nym-sdk", + "nym-service-provider-requests-common", + "nym-wireguard-types", + "semver 1.0.26", + "thiserror 2.0.12", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "nym-authenticator-requests" version = "0.1.0" @@ -5074,6 +5153,7 @@ dependencies = [ "nym-crypto", "nym-ecash-contract-common", "nym-ecash-time", + "nym-http-api-client", "nym-id", "nym-mixnet-contract-common", "nym-multisig-contract-common", @@ -5162,6 +5242,7 @@ dependencies = [ "nym-http-api-client", "nym-id", "nym-mixnet-client", + "nym-mixnet-contract-common", "nym-network-defaults", "nym-nonexhaustive-delayqueue", "nym-pemstore", @@ -5211,6 +5292,7 @@ dependencies = [ name = "nym-client-core-gateways-storage" version = "0.1.0" dependencies = [ + "anyhow", "async-trait", "cosmrs", "nym-crypto", @@ -5229,6 +5311,7 @@ dependencies = [ name = "nym-client-core-surb-storage" version = "0.1.0" dependencies = [ + "anyhow", "async-trait", "dashmap", "nym-crypto", @@ -5371,7 +5454,7 @@ dependencies = [ [[package]] name = "nym-credential-proxy" -version = "0.1.7" +version = "0.2.0" dependencies = [ "anyhow", "axum 0.7.9", @@ -5384,11 +5467,13 @@ dependencies = [ "nym-bin-common", "nym-compact-ecash", "nym-config", + "nym-credential-proxy-lib", "nym-credential-proxy-requests", "nym-credentials", "nym-credentials-interface", "nym-crypto", "nym-ecash-contract-common", + "nym-ecash-signer-check", "nym-http-api-common", "nym-network-defaults", "nym-validator-client", @@ -5413,6 +5498,43 @@ dependencies = [ "zeroize", ] +[[package]] +name = "nym-credential-proxy-lib" +version = "0.1.0" +dependencies = [ + "anyhow", + "axum 0.7.9", + "bip39", + "bs58", + "futures", + "humantime", + "nym-compact-ecash", + "nym-credential-proxy-requests", + "nym-credentials", + "nym-credentials-interface", + "nym-crypto", + "nym-ecash-contract-common", + "nym-ecash-signer-check", + "nym-network-defaults", + "nym-validator-client", + "rand 0.8.5", + "reqwest 0.12.22", + "serde", + "serde_json", + "sqlx", + "strum", + "strum_macros", + "tempfile", + "thiserror 2.0.12", + "time", + "tokio", + "tokio-util", + "tracing", + "url", + "uuid", + "zeroize", +] + [[package]] name = "nym-credential-proxy-requests" version = "0.1.0" @@ -5439,6 +5561,7 @@ dependencies = [ name = "nym-credential-storage" version = "0.1.0" dependencies = [ + "anyhow", "async-trait", "bincode", "log", @@ -5510,6 +5633,7 @@ dependencies = [ "nym-crypto", "nym-ecash-contract-common", "nym-ecash-time", + "nym-http-api-client", "nym-network-defaults", "nym-serde-helpers", "nym-validator-client", @@ -5544,6 +5668,7 @@ dependencies = [ "aead", "aes", "aes-gcm-siv", + "base64 0.22.1", "blake3", "bs58", "cipher", @@ -5553,6 +5678,7 @@ dependencies = [ "generic-array 0.14.7", "hkdf", "hmac", + "jwt-simple", "nym-pemstore", "nym-sphinx-types", "rand 0.8.5", @@ -5609,6 +5735,7 @@ version = "0.1.0" dependencies = [ "futures", "nym-ecash-signer-check-types", + "nym-http-api-client", "nym-network-defaults", "nym-validator-client", "semver 1.0.26", @@ -5641,14 +5768,6 @@ dependencies = [ "time", ] -[[package]] -name = "nym-execute" -version = "0.1.0" -dependencies = [ - "quote", - "syn 1.0.109", -] - [[package]] name = "nym-exit-policy" version = "0.1.0" @@ -5718,6 +5837,7 @@ dependencies = [ "nym-types", "nym-validator-client", "nym-wireguard", + "nym-wireguard-private-metadata-server", "nym-wireguard-types", "rand 0.8.5", "serde", @@ -5807,6 +5927,7 @@ dependencies = [ name = "nym-gateway-stats-storage" version = "0.1.0" dependencies = [ + "anyhow", "nym-node-metrics", "nym-sphinx", "nym-statistics-common", @@ -5822,6 +5943,7 @@ dependencies = [ name = "nym-gateway-storage" version = "0.1.0" dependencies = [ + "anyhow", "async-trait", "bincode", "defguard_wireguard_rs", @@ -5871,17 +5993,23 @@ dependencies = [ "async-trait", "bincode", "bytes", + "cfg-if", "encoding_rs", "hickory-resolver", "http 1.3.1", + "inventory", "itertools 0.14.0", "mime", "nym-bin-common", + "nym-http-api-client-macro", "nym-http-api-common", + "nym-network-defaults", "once_cell", "reqwest 0.12.22", "serde", "serde_json", + "serde_plain", + "serde_yaml", "thiserror 2.0.12", "tokio", "tracing", @@ -5889,6 +6017,19 @@ dependencies = [ "wasmtimer", ] +[[package]] +name = "nym-http-api-client-macro" +version = "0.1.0" +dependencies = [ + "nym-http-api-client", + "proc-macro-crate", + "proc-macro2", + "quote", + "reqwest 0.12.22", + "syn 2.0.106", + "uuid", +] + [[package]] name = "nym-http-api-common" version = "0.1.0" @@ -5946,6 +6087,20 @@ dependencies = [ "thiserror 2.0.12", ] +[[package]] +name = "nym-ip-packet-client" +version = "0.1.0" +dependencies = [ + "bytes", + "futures", + "nym-ip-packet-requests", + "nym-sdk", + "thiserror 2.0.12", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "nym-ip-packet-requests" version = "0.1.0" @@ -6138,6 +6293,8 @@ dependencies = [ "nym-client-core", "nym-crypto", "nym-gateway-requests", + "nym-http-api-client", + "nym-mixnet-contract-common", "nym-network-defaults", "nym-sdk", "nym-sphinx", @@ -6234,7 +6391,7 @@ dependencies = [ "hkdf", "human-repr", "humantime-serde", - "indicatif 0.17.11", + "indicatif", "ipnetwork", "lioness", "nym-bin-common", @@ -6279,6 +6436,7 @@ dependencies = [ "thiserror 2.0.12", "time", "tokio", + "tokio-stream", "tokio-util", "toml 0.8.23", "tower-http 0.5.2", @@ -6350,7 +6508,7 @@ dependencies = [ [[package]] name = "nym-node-status-api" -version = "3.3.0" +version = "3.3.2" dependencies = [ "ammonia", "anyhow", @@ -6605,6 +6763,7 @@ dependencies = [ "nym-credentials-interface", "nym-crypto", "nym-gateway-requests", + "nym-http-api-client", "nym-network-defaults", "nym-ordered-buffer", "nym-service-providers-common", @@ -6672,6 +6831,26 @@ dependencies = [ "tokio", ] +[[package]] +name = "nym-signers-monitor" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap", + "humantime", + "itertools 0.14.0", + "nym-bin-common", + "nym-ecash-signer-check", + "nym-network-defaults", + "nym-task", + "nym-validator-client", + "time", + "tokio", + "tracing", + "url", + "zulip-client", +] + [[package]] name = "nym-socks5-client" version = "1.1.62" @@ -6815,10 +6994,12 @@ dependencies = [ name = "nym-sphinx-addressing" version = "0.1.0" dependencies = [ + "bincode", "nym-crypto", "nym-sphinx-types", "rand 0.8.5", "serde", + "serde_json", "thiserror 2.0.12", ] @@ -7004,9 +7185,11 @@ dependencies = [ name = "nym-task" version = "0.1.0" dependencies = [ + "anyhow", "cfg-if", "futures", "log", + "nym-test-utils", "thiserror 2.0.12", "tokio", "tokio-util", @@ -7108,6 +7291,22 @@ dependencies = [ "x25519-dalek", ] +[[package]] +name = "nym-upgrade-mode-check" +version = "0.1.0" +dependencies = [ + "anyhow", + "jwt-simple", + "nym-crypto", + "nym-http-api-client", + "reqwest 0.12.22", + "serde", + "serde_json", + "thiserror 2.0.12", + "time", + "tracing", +] + [[package]] name = "nym-validator-client" version = "0.1.0" @@ -7178,6 +7377,7 @@ dependencies = [ "nym-credentials-interface", "nym-crypto", "nym-ecash-time", + "nym-http-api-client", "nym-network-defaults", "nym-pemstore", "nym-serde-helpers", @@ -7207,7 +7407,9 @@ dependencies = [ "bytes", "futures", "humantime", + "nym-api-requests", "nym-crypto", + "nym-http-api-client", "nym-task", "nym-validator-client", "rand 0.8.5", @@ -7278,6 +7480,26 @@ dependencies = [ "ts-rs", ] +[[package]] +name = "nym-wg-gateway-client" +version = "0.1.0" +dependencies = [ + "nym-authenticator-client", + "nym-authenticator-requests", + "nym-bandwidth-controller", + "nym-credentials-interface", + "nym-crypto", + "nym-node-requests", + "nym-pemstore", + "nym-sdk", + "nym-statistics-common", + "nym-validator-client", + "rand 0.8.5", + "thiserror 2.0.12", + "tracing", + "url", +] + [[package]] name = "nym-wireguard" version = "0.1.0" @@ -7310,6 +7532,68 @@ dependencies = [ "x25519-dalek", ] +[[package]] +name = "nym-wireguard-private-metadata-client" +version = "1.0.0" +dependencies = [ + "async-trait", + "nym-http-api-client", + "nym-wireguard-private-metadata-shared", + "tracing", +] + +[[package]] +name = "nym-wireguard-private-metadata-server" +version = "1.0.0" +dependencies = [ + "anyhow", + "async-trait", + "axum 0.7.9", + "futures", + "nym-credential-verification", + "nym-credentials-interface", + "nym-http-api-common", + "nym-wireguard", + "nym-wireguard-private-metadata-shared", + "tokio", + "tokio-util", + "tower-http 0.5.2", + "utoipa", + "utoipa-swagger-ui", +] + +[[package]] +name = "nym-wireguard-private-metadata-shared" +version = "1.0.0" +dependencies = [ + "axum 0.7.9", + "bincode", + "nym-credentials-interface", + "schemars 0.8.22", + "serde", + "thiserror 2.0.12", + "utoipa", +] + +[[package]] +name = "nym-wireguard-private-metadata-tests" +version = "1.0.0" +dependencies = [ + "async-trait", + "axum 0.7.9", + "nym-credential-verification", + "nym-credentials-interface", + "nym-http-api-client", + "nym-http-api-common", + "nym-wireguard", + "nym-wireguard-private-metadata-client", + "nym-wireguard-private-metadata-server", + "nym-wireguard-private-metadata-shared", + "tokio", + "tower-http 0.5.2", + "utoipa", +] + [[package]] name = "nym-wireguard-types" version = "0.1.0" @@ -7390,6 +7674,7 @@ dependencies = [ name = "nyxd-scraper" version = "0.1.0" dependencies = [ + "anyhow", "async-trait", "const_format", "cosmrs", @@ -7613,6 +7898,18 @@ dependencies = [ "sha2 0.10.9", ] +[[package]] +name = "p384" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2 0.10.9", +] + [[package]] name = "pairing" version = "0.23.0" @@ -7752,7 +8049,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -7815,7 +8112,7 @@ dependencies = [ "phf_shared", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -7844,7 +8141,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -8052,6 +8349,37 @@ dependencies = [ "elliptic-curve", ] +[[package]] +name = "proc-macro-crate" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "proc-macro2" version = "1.0.95" @@ -8105,7 +8433,7 @@ dependencies = [ "itertools 0.14.0", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -8181,7 +8509,7 @@ dependencies = [ "quinn-udp", "rustc-hash", "rustls 0.23.29", - "socket2", + "socket2 0.5.10", "thiserror 2.0.12", "tokio", "tracing", @@ -8218,7 +8546,7 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2", + "socket2 0.5.10", "tracing", "windows-sys 0.59.0", ] @@ -8370,7 +8698,7 @@ checksum = "1165225c21bff1f3bbce98f5a1f889949bc902d3575308cc7b0de30b4f6d27c7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -8593,6 +8921,7 @@ dependencies = [ "pkcs1", "pkcs8", "rand_core 0.6.4", + "sha2 0.10.9", "signature", "spki", "subtle 2.6.1", @@ -8619,7 +8948,7 @@ dependencies = [ "proc-macro2", "quote", "rust-embed-utils", - "syn 2.0.104", + "syn 2.0.106", "walkdir", ] @@ -8923,7 +9252,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals 0.29.1", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -8955,7 +9284,7 @@ checksum = "1783eabc414609e28a5ba76aee5ddd52199f7107a0b24c2e9746a1ecc34a683d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -8976,7 +9305,7 @@ checksum = "22f968c5ea23d555e670b449c1c5e7b2fc399fdaec1d304a17cd48e288abc107" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -9116,7 +9445,7 @@ checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -9127,7 +9456,7 @@ checksum = "e578a843d40b4189a4d66bba51d7684f57da5bd7c304c64e14bd63efbef49509" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -9138,7 +9467,7 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -9200,7 +9529,7 @@ checksum = "aafbefbe175fa9bf03ca83ef89beecff7d2a95aaacd5732325b90ac8c3bd7b90" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -9213,6 +9542,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_plain" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce1fc6db65a611022b23a0dec6975d63fb80a302cb3388835ff02c097258d50" +dependencies = [ + "serde", +] + [[package]] name = "serde_repr" version = "0.1.20" @@ -9221,7 +9559,7 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -9274,7 +9612,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -9426,9 +9764,9 @@ checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" [[package]] name = "slab" -version = "0.4.10" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" [[package]] name = "sluice" @@ -9504,6 +9842,16 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "socket2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + [[package]] name = "sphinx-packet" version = "0.6.0" @@ -9610,7 +9958,7 @@ dependencies = [ "quote", "sqlx-core", "sqlx-macros-core", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -9633,7 +9981,7 @@ dependencies = [ "sqlx-mysql", "sqlx-postgres", "sqlx-sqlite", - "syn 2.0.104", + "syn 2.0.106", "tokio", "url", ] @@ -9842,7 +10190,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -9872,6 +10220,19 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "734676eb262c623cec13c3155096e08d1f8f29adce39ba17948b18dad1e54142" +[[package]] +name = "superboring" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "515cce34a781d7250b8a65706e0f2a5b99236ea605cb235d4baed6685820478f" +dependencies = [ + "getrandom 0.2.16", + "hmac-sha256", + "hmac-sha512", + "rand 0.8.5", + "rsa", +] + [[package]] name = "syn" version = "1.0.109" @@ -9885,9 +10246,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.104" +version = "2.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" dependencies = [ "proc-macro2", "quote", @@ -9917,7 +10278,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -10104,6 +10465,19 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "test-with" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0f370b9efbfbbc5f057cbce9888373eaeb146a3095bb8cc869b199c94d15559" +dependencies = [ + "proc-macro-error2", + "proc-macro2", + "quote", + "regex", + "syn 2.0.106", +] + [[package]] name = "testnet-manager" version = "0.1.0" @@ -10112,11 +10486,11 @@ dependencies = [ "bip39", "bs58", "clap", - "console 0.15.11", + "console", "cw-utils", "dkg-bypass-contract", "humantime", - "indicatif 0.17.11", + "indicatif", "nym-bin-common", "nym-coconut-dkg-common", "nym-compact-ecash", @@ -10125,6 +10499,7 @@ dependencies = [ "nym-crypto", "nym-ecash-contract-common", "nym-group-contract-common", + "nym-http-api-client", "nym-mixnet-contract-common", "nym-multisig-contract-common", "nym-pemstore", @@ -10180,7 +10555,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -10191,7 +10566,7 @@ checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -10296,9 +10671,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.46.1" +version = "1.47.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cc3a2344dafbe23a245241fe8b09735b521110d30fcefbbd5feb1797ca35d17" +checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" dependencies = [ "backtrace", "bytes", @@ -10309,10 +10684,10 @@ dependencies = [ "pin-project-lite", "signal-hook-registry", "slab", - "socket2", + "socket2 0.6.0", "tokio-macros", "tracing", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -10323,7 +10698,7 @@ checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -10346,7 +10721,7 @@ dependencies = [ "postgres-protocol", "postgres-types", "rand 0.9.2", - "socket2", + "socket2 0.5.10", "tokio", "tokio-util", "whoami", @@ -10522,7 +10897,7 @@ dependencies = [ "percent-encoding", "pin-project", "prost", - "socket2", + "socket2 0.5.10", "tokio", "tokio-stream", "tower 0.4.13", @@ -10644,7 +11019,7 @@ checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -10673,7 +11048,7 @@ version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c714cc8fc46db04fcfddbd274c6ef59bebb1b435155984e7c6e89c3ce66f200" dependencies = [ - "indicatif 0.18.0", + "indicatif", "tracing", "tracing-core", "tracing-subscriber", @@ -10814,7 +11189,7 @@ checksum = "0e9d8656589772eeec2cf7a8264d9cda40fb28b9bc53118ceb9e8c07f8f38730" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", "termcolor", ] @@ -10841,7 +11216,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals 0.28.0", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -11031,7 +11406,7 @@ dependencies = [ "indexmap 2.10.0", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -11046,7 +11421,7 @@ dependencies = [ "proc-macro2", "quote", "serde", - "syn 2.0.104", + "syn 2.0.106", "toml 0.5.11", "uniffi_meta", ] @@ -11173,7 +11548,7 @@ dependencies = [ "proc-macro2", "quote", "regex", - "syn 2.0.104", + "syn 2.0.106", "uuid", ] @@ -11212,7 +11587,7 @@ checksum = "268d76aaebb80eba79240b805972e52d7d410d4bcc52321b951318b0f440cd60" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -11223,7 +11598,7 @@ checksum = "382673bda1d05c85b4550d32fd4192ccd4cffe9a908543a0795d1e7682b36246" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", "utoipauto-core", ] @@ -11247,6 +11622,7 @@ dependencies = [ "clap", "comfy-table", "nym-bin-common", + "nym-http-api-client", "nym-network-defaults", "nym-validator-client", "serde", @@ -11369,6 +11745,15 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" +[[package]] +name = "wasix" +version = "0.12.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1fbb4ef9bbca0c1170e0b00dd28abc9e3b68669821600cad1caaed606583c6d" +dependencies = [ + "wasi 0.11.1+wasi-snapshot-preview1", +] + [[package]] name = "wasm-bindgen" version = "0.2.100" @@ -11391,7 +11776,7 @@ dependencies = [ "log", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", "wasm-bindgen-shared", ] @@ -11426,7 +11811,7 @@ checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -11461,7 +11846,7 @@ checksum = "17d5042cc5fa009658f9a7333ef24291b1291a25b6382dd68862a7f3b969f69b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -11477,6 +11862,7 @@ dependencies = [ "nym-credential-storage", "nym-crypto", "nym-gateway-client", + "nym-http-api-client", "nym-sphinx", "nym-sphinx-acknowledgements", "nym-statistics-common", @@ -11732,7 +12118,7 @@ checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -11743,7 +12129,7 @@ checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -12167,7 +12553,7 @@ checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", "synstructure", ] @@ -12188,7 +12574,7 @@ checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -12208,7 +12594,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", "synstructure", ] @@ -12229,7 +12615,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] @@ -12262,7 +12648,7 @@ checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.106", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 6cf7625af6..627a1c29de 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,6 +43,7 @@ members = [ "common/cosmwasm-smart-contracts/nym-performance-contract", "common/cosmwasm-smart-contracts/nym-pool-contract", "common/cosmwasm-smart-contracts/vesting-contract", + "common/credential-proxy", "common/credential-storage", "common/credential-utils", "common/credential-verification", @@ -53,12 +54,11 @@ members = [ "common/ecash-signer-check", "common/ecash-signer-check-types", "common/ecash-time", - "common/execute", "common/exit-policy", "common/gateway-requests", "common/gateway-stats-storage", "common/gateway-storage", - "common/http-api-client", + "common/http-api-client", "common/http-api-client-macro", "common/http-api-common", "common/inclusion-probability", "common/ip-packet-requests", @@ -96,22 +96,28 @@ members = [ "common/ticketbooks-merkle", "common/topology", "common/tun", - "common/types", + "common/types", "common/upgrade-mode-check", "common/verloc", "common/wasm/client-core", "common/wasm/storage", "common/wasm/utils", "common/wireguard", + "common/wireguard-private-metadata/client", + "common/wireguard-private-metadata/server", + "common/wireguard-private-metadata/shared", + "common/wireguard-private-metadata/tests", "common/wireguard-types", "common/zulip-client", "documentation/autodoc", "gateway", "nym-api", "nym-api/nym-api-requests", + "nym-authenticator-client", "nym-browser-extension/storage", "nym-credential-proxy/nym-credential-proxy", "nym-credential-proxy/nym-credential-proxy-requests", "nym-credential-proxy/vpn-api-lib-wasm", + "nym-ip-packet-client", "nym-network-monitor", "nym-node", "nym-node-status-api/nym-node-status-agent", @@ -119,9 +125,10 @@ members = [ "nym-node-status-api/nym-node-status-client", "nym-node/nym-node-metrics", "nym-node/nym-node-requests", - "nym-outfox", + "nym-outfox", "nym-signers-monitor", "nym-statistics-api", "nym-validator-rewarder", + "nym-wg-gateway-client", "nyx-chain-watcher", "sdk/ffi/cpp", "sdk/ffi/go", @@ -178,7 +185,7 @@ homepage = "https://nymtech.net" documentation = "https://nymtech.net" edition = "2021" license = "Apache-2.0" -rust-version = "1.80" +rust-version = "1.81" readme = "README.md" [workspace.dependencies] @@ -219,7 +226,7 @@ clap_complete = "4.5" clap_complete_fig = "4.5" colored = "2.2" comfy-table = "7.1.4" -console = "0.15.11" +console = "0.16.0" console-subscriber = "0.4.1" console_error_panic_hook = "0.1" const-str = "0.5.6" @@ -266,11 +273,13 @@ humantime = "2.2.0" humantime-serde = "1.1.1" hyper = "1.6.0" hyper-util = "0.1" -indicatif = "0.17.11" +indicatif = "0.18.0" inquire = "0.6.2" +inventory = "0.3.21" ip_network = "0.4.1" ipnetwork = "0.20" itertools = "0.14.0" +jwt-simple = { version = "0.12.12", default-features = false, features = ["pure-rust"] } k256 = "0.13" lazy_static = "1.5.0" ledger-transport = "0.10.0" @@ -314,6 +323,7 @@ serde_json_path = "0.7.2" serde_repr = "0.1" serde_with = "3.9.0" serde_yaml = "0.9.25" +serde_plain = "1.0.2" sha2 = "0.10.9" si-scale = "0.2.3" snow = "0.9.6" @@ -322,14 +332,15 @@ sqlx = "0.8.6" strum = "0.27.2" strum_macros = "0.27.2" subtle-encoding = "0.5" -syn = "1" +syn = "2" sysinfo = "0.37.0" tap = "1.0.1" tar = "0.4.44" +test-with = { version = "0.15.4", default-features = false } tempfile = "3.20" thiserror = "2.0" time = "0.3.41" -tokio = "1.45" +tokio = "1.47" tokio-postgres = "0.7" tokio-stream = "0.1.17" tokio-test = "0.4.4" diff --git a/clients/native/src/client/mod.rs b/clients/native/src/client/mod.rs index ce36a5eb84..ffe4daa9fb 100644 --- a/clients/native/src/client/mod.rs +++ b/clients/native/src/client/mod.rs @@ -11,7 +11,7 @@ use nym_client_core::client::base_client::{ BaseClientBuilder, ClientInput, ClientOutput, ClientState, }; use nym_sphinx::params::PacketType; -use nym_task::TaskHandle; +use nym_task::ShutdownManager; use nym_validator_client::QueryHttpRpcNyxdClient; use std::error::Error; use std::path::PathBuf; @@ -29,6 +29,8 @@ pub struct SocketClient { /// Optional path to a .json file containing standalone network details. custom_mixnet: Option, + + shutdown_manager: ShutdownManager, } impl SocketClient { @@ -40,6 +42,7 @@ impl SocketClient { SocketClient { config, custom_mixnet, + shutdown_manager: Default::default(), } } @@ -49,7 +52,7 @@ impl SocketClient { client_output: ClientOutput, client_state: ClientState, self_address: &Recipient, - task_client: nym_task::TaskClient, + shutdown_token: nym_task::ShutdownToken, packet_type: PacketType, ) { info!("Starting websocket listener..."); @@ -77,24 +80,24 @@ impl SocketClient { shared_lane_queue_lengths, reply_controller_sender, Some(packet_type), - task_client.fork("websocket_handler"), + shutdown_token.clone(), ); websocket::Listener::new( config.socket.host, config.socket.listening_port, - task_client.with_suffix("websocket_listener"), + shutdown_token.child_token(), ) .start(websocket_handler); } /// blocking version of `start_socket` method. Will run forever (or until SIGINT is sent) pub async fn run_socket_forever(self) -> Result<(), Box> { - let shutdown = self.start_socket().await?; + let mut shutdown = self.start_socket().await?; - let res = shutdown.wait_for_shutdown().await; + shutdown.run_until_shutdown().await; log::info!("Stopping nym-client"); - res + Ok(()) } async fn initialise_storage(&self) -> Result { @@ -119,6 +122,7 @@ impl SocketClient { let mut base_client = BaseClientBuilder::new(self.config().base(), storage, dkg_query_client) + .with_shutdown(self.shutdown_manager.shutdown_tracker_owned()) .with_user_agent(user_agent); if let Some(custom_mixnet) = &self.custom_mixnet { @@ -128,7 +132,7 @@ impl SocketClient { Ok(base_client) } - pub async fn start_socket(self) -> Result { + pub async fn start_socket(self) -> Result { if !self.config.socket.socket_type.is_websocket() { return Err(ClientError::InvalidSocketMode); } @@ -147,13 +151,13 @@ impl SocketClient { client_output, client_state, &self_address, - started_client.task_handle.get_handle(), + self.shutdown_manager.child_shutdown_token(), packet_type, ); info!("Client startup finished!"); info!("The address of this client is: {self_address}"); - Ok(started_client.task_handle) + Ok(self.shutdown_manager) } } diff --git a/clients/native/src/websocket/handler.rs b/clients/native/src/websocket/handler.rs index 761e2b9531..2ea7cac609 100644 --- a/clients/native/src/websocket/handler.rs +++ b/clients/native/src/websocket/handler.rs @@ -19,7 +19,7 @@ use nym_sphinx::receiver::ReconstructedMessage; use nym_task::connections::{ ConnectionCommand, ConnectionCommandSender, ConnectionId, LaneQueueLengths, TransmissionLane, }; -use nym_task::TaskClient; +use nym_task::ShutdownToken; use std::time::Duration; use tokio::net::TcpStream; use tokio::time::Instant; @@ -44,7 +44,7 @@ pub(crate) struct HandlerBuilder { lane_queue_lengths: LaneQueueLengths, reply_controller_sender: ReplyControllerSender, packet_type: Option, - task_client: TaskClient, + shutdown_token: ShutdownToken, } impl HandlerBuilder { @@ -57,7 +57,7 @@ impl HandlerBuilder { lane_queue_lengths: LaneQueueLengths, reply_controller_sender: ReplyControllerSender, packet_type: Option, - task_client: TaskClient, + shutdown_token: ShutdownToken, ) -> Self { Self { msg_input, @@ -67,14 +67,13 @@ impl HandlerBuilder { lane_queue_lengths, reply_controller_sender, packet_type, - task_client, + shutdown_token, } } // TODO: make sure we only ever have one active handler pub fn create_active_handler(&self) -> Handler { - let mut task_client = self.task_client.fork("active_handler"); - task_client.disarm(); + let shutdown_token = self.shutdown_token.clone(); Handler { msg_input: self.msg_input.clone(), client_connection_tx: self.client_connection_tx.clone(), @@ -85,7 +84,7 @@ impl HandlerBuilder { lane_queue_lengths: self.lane_queue_lengths.clone(), reply_controller_sender: self.reply_controller_sender.clone(), packet_type: self.packet_type, - task_client, + shutdown_token, } } } @@ -100,19 +99,14 @@ pub(crate) struct Handler { lane_queue_lengths: LaneQueueLengths, reply_controller_sender: ReplyControllerSender, packet_type: Option, - task_client: TaskClient, + shutdown_token: ShutdownToken, } impl Drop for Handler { fn drop(&mut self) { - if let Err(err) = self + let _ = self .buffer_requester - .unbounded_send(ReceivedBufferMessage::ReceiverDisconnect) - { - if !self.task_client.is_shutdown_poll() { - error!("failed to disconnect the receiver from the buffer: {err}"); - } - } + .unbounded_send(ReceivedBufferMessage::ReceiverDisconnect); } } @@ -142,7 +136,7 @@ impl Handler { { Ok(length) => length, Err(err) => { - if !self.task_client.is_shutdown_poll() { + if !self.shutdown_token.is_cancelled() { error!( "Failed to get reply queue length for connection {connection_id}: {err}" ); @@ -192,7 +186,7 @@ impl Handler { // the ack control is now responsible for chunking, etc. let input_msg = InputMessage::new_regular(recipient, message, lane, self.packet_type); if let Err(err) = self.msg_input.send(input_msg).await { - if !self.task_client.is_shutdown_poll() { + if !self.shutdown_token.is_cancelled() { error!("Failed to send message to the input buffer: {err}"); } } @@ -225,7 +219,7 @@ impl Handler { let input_msg = InputMessage::new_anonymous(recipient, message, reply_surbs, lane, self.packet_type); if let Err(err) = self.msg_input.send(input_msg).await { - if !self.task_client.is_shutdown_poll() { + if !self.shutdown_token.is_cancelled() { error!("Failed to send anonymous message to the input buffer: {err}"); } } @@ -253,7 +247,7 @@ impl Handler { let input_msg = InputMessage::new_reply(recipient_tag, message, lane, self.packet_type); if let Err(err) = self.msg_input.send(input_msg).await { - if !self.task_client.is_shutdown_poll() { + if !self.shutdown_token.is_cancelled() { error!("Failed to send reply message to the input buffer: {err}"); } } @@ -275,7 +269,7 @@ impl Handler { .client_connection_tx .unbounded_send(ConnectionCommand::Close(connection_id)) { - if !self.task_client.is_shutdown_poll() { + if !self.shutdown_token.is_cancelled() { error!("Failed to send close connection command: {err}"); } } @@ -394,11 +388,14 @@ impl Handler { } async fn listen_for_requests(&mut self, mut msg_receiver: ReconstructedMessagesReceiver) { - let mut task_client = self.task_client.fork("select"); - task_client.disarm(); + let shutdown_token = self.shutdown_token.clone(); - while !task_client.is_shutdown() { + loop { tokio::select! { + _ = shutdown_token.cancelled() => { + log::trace!("Websocket handler: Received shutdown"); + break; + } // we can either get a client request from the websocket socket_msg = self.next_websocket_request() => { if socket_msg.is_none() { @@ -436,9 +433,6 @@ impl Handler { break; } } - _ = task_client.recv() => { - log::trace!("Websocket handler: Received shutdown"); - } } } log::debug!("Websocket handler: Exiting"); @@ -464,7 +458,7 @@ impl Handler { reconstructed_sender, )) { - if !self.task_client.is_shutdown_poll() { + if !self.shutdown_token.is_cancelled() { error!("failed to announce the receiver to the buffer: {err}"); } } diff --git a/clients/native/src/websocket/listener.rs b/clients/native/src/websocket/listener.rs index a1b430a930..f0723c1824 100644 --- a/clients/native/src/websocket/listener.rs +++ b/clients/native/src/websocket/listener.rs @@ -3,7 +3,7 @@ use super::handler::HandlerBuilder; use log::*; -use nym_task::TaskClient; +use nym_task::ShutdownToken; use std::net::IpAddr; use std::{net::SocketAddr, process, sync::Arc}; use tokio::io::AsyncWriteExt; @@ -23,15 +23,15 @@ impl State { pub(crate) struct Listener { address: SocketAddr, state: State, - task_client: TaskClient, + shutdown_token: ShutdownToken, } impl Listener { - pub(crate) fn new(host: IpAddr, port: u16, task_client: TaskClient) -> Self { + pub(crate) fn new(host: IpAddr, port: u16, shutdown_token: ShutdownToken) -> Self { Listener { address: SocketAddr::new(host, port), state: State::AwaitingConnection, - task_client, + shutdown_token, } } @@ -46,11 +46,11 @@ impl Listener { let notify = Arc::new(Notify::new()); - while !self.task_client.is_shutdown() { + while !self.shutdown_token.is_cancelled() { tokio::select! { // When the handler finishes we check if shutdown is signalled _ = notify.notified() => { - if self.task_client.is_shutdown() { + if self.shutdown_token.is_cancelled() { log::trace!("Websocket listener: detected shutdown after connection closed"); break; } @@ -59,7 +59,7 @@ impl Listener { } // ... but when there is no connected client at the time of shutdown being // signalled, we handle it here. - _ = self.task_client.recv() => { + _ = self.shutdown_token.cancelled() => { if !self.state.is_connected() { log::trace!("Not connected: shutting down"); break; diff --git a/common/bandwidth-controller/src/utils.rs b/common/bandwidth-controller/src/utils.rs index 4502238c91..c353755192 100644 --- a/common/bandwidth-controller/src/utils.rs +++ b/common/bandwidth-controller/src/utils.rs @@ -13,7 +13,7 @@ use nym_credentials_interface::{ }; use nym_ecash_time::Date; use nym_validator_client::coconut::all_ecash_api_clients; -use nym_validator_client::nym_api::EpochId; +use nym_validator_client::nym_api::{EpochId, NymApiClientExt}; use nym_validator_client::nyxd::contract_traits::DkgQueryClient; use nym_validator_client::EcashApiClient; use rand::prelude::SliceRandom; diff --git a/common/client-core/Cargo.toml b/common/client-core/Cargo.toml index 74663fe807..d3e2409eec 100644 --- a/common/client-core/Cargo.toml +++ b/common/client-core/Cargo.toml @@ -53,6 +53,7 @@ nym-client-core-config-types = { path = "./config-types", features = [ nym-client-core-surb-storage = { path = "./surb-storage" } nym-client-core-gateways-storage = { path = "./gateways-storage" } nym-ecash-time = { path = "../ecash-time" } +nym-mixnet-contract-common = { path = "../cosmwasm-smart-contracts/mixnet-contract" } [target."cfg(not(target_arch = \"wasm32\"))".dependencies] nym-mixnet-client = { path = "../client-libs/mixnet-client", default-features = false } diff --git a/common/client-core/gateways-storage/Cargo.toml b/common/client-core/gateways-storage/Cargo.toml index ef2a1b5fdf..938a6db5dc 100644 --- a/common/client-core/gateways-storage/Cargo.toml +++ b/common/client-core/gateways-storage/Cargo.toml @@ -3,6 +3,7 @@ name = "nym-client-core-gateways-storage" version = "0.1.0" edition = "2021" license.workspace = true +rust-version.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -26,6 +27,7 @@ features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate", "time"] optional = true [build-dependencies] +anyhow = { workspace = true } tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } sqlx = { workspace = true, features = [ "runtime-tokio-rustls", diff --git a/common/client-core/gateways-storage/build.rs b/common/client-core/gateways-storage/build.rs index f8b85cbb4a..e8fc127e07 100644 --- a/common/client-core/gateways-storage/build.rs +++ b/common/client-core/gateways-storage/build.rs @@ -2,23 +2,30 @@ // SPDX-License-Identifier: Apache-2.0 #[tokio::main] -async fn main() { +async fn main() -> anyhow::Result<()> { #[cfg(feature = "fs-gateways-storage")] { + use anyhow::Context; use sqlx::{Connection, SqliteConnection}; use std::env; - let out_dir = env::var("OUT_DIR").unwrap(); + let out_dir = env::var("OUT_DIR")?; let database_path = format!("{out_dir}/gateways-storage-example.sqlite"); + // remove the db file if it already existed from previous build + // in case it was from a different branch + if std::fs::exists(&database_path)? { + std::fs::remove_file(&database_path)?; + } + let mut conn = SqliteConnection::connect(&format!("sqlite://{database_path}?mode=rwc")) .await - .expect("Failed to create SQLx database connection"); + .context("Failed to create SQLx database connection")?; sqlx::migrate!("./fs_gateways_migrations") .run(&mut conn) .await - .expect("Failed to perform SQLx migrations"); + .context("Failed to perform SQLx migrations")?; #[cfg(target_family = "unix")] println!("cargo:rustc-env=DATABASE_URL=sqlite://{}", &database_path); @@ -28,4 +35,6 @@ async fn main() { // not a valid windows path... but hey, it works... println!("cargo:rustc-env=DATABASE_URL=sqlite:///{}", &database_path); } + + Ok(()) } diff --git a/common/client-core/src/client/base_client/helpers.rs b/common/client-core/src/client/base_client/helpers.rs index 4259ead046..68c0cb61bc 100644 --- a/common/client-core/src/client/base_client/helpers.rs +++ b/common/client-core/src/client/base_client/helpers.rs @@ -1,7 +1,9 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use crate::error::ClientCoreError; use crate::{client::replies::reply_storage, config::DebugConfig}; +use nym_task::{ShutdownManager, ShutdownToken, ShutdownTracker}; pub fn setup_empty_reply_surb_backend(debug_config: &DebugConfig) -> reply_storage::Empty { reply_storage::Empty { @@ -13,3 +15,49 @@ pub fn setup_empty_reply_surb_backend(debug_config: &DebugConfig) -> reply_stora .maximum_reply_surb_storage_threshold, } } + +// old 'TaskHandle' +pub(crate) enum ShutdownHelper { + Internal(ShutdownManager), + External(ShutdownTracker), +} + +fn new_shutdown_manager() -> Result { + cfg_if::cfg_if! { + if #[cfg(not(target_arch = "wasm32"))] { + Ok(ShutdownManager::build_new_default()?) + } else { + Ok(ShutdownManager::new_without_signals()) + } + } +} + +impl ShutdownHelper { + pub(crate) fn new(shutdown_tracker: Option) -> Result { + match shutdown_tracker { + None => Ok(ShutdownHelper::Internal(new_shutdown_manager()?)), + Some(shutdown_tracker) => Ok(ShutdownHelper::External(shutdown_tracker)), + } + } + + pub(crate) fn into_internal(self) -> Option { + match self { + ShutdownHelper::Internal(manager) => Some(manager), + ShutdownHelper::External(_) => None, + } + } + + pub(crate) fn shutdown_token(&self) -> ShutdownToken { + match self { + ShutdownHelper::External(shutdown) => shutdown.clone_shutdown_token(), + ShutdownHelper::Internal(shutdown) => shutdown.clone_shutdown_token(), + } + } + + pub(crate) fn tracker(&self) -> &ShutdownTracker { + match self { + ShutdownHelper::External(shutdown) => shutdown, + ShutdownHelper::Internal(shutdown) => shutdown.shutdown_tracker(), + } + } +} diff --git a/common/client-core/src/client/base_client/mod.rs b/common/client-core/src/client/base_client/mod.rs index 36cb427780..2b187ca05c 100644 --- a/common/client-core/src/client/base_client/mod.rs +++ b/common/client-core/src/client/base_client/mod.rs @@ -4,6 +4,7 @@ use super::mix_traffic::ClientRequestSender; use super::received_buffer::ReceivedBufferMessage; use super::statistics_control::StatisticsControl; +use crate::client::base_client::helpers::ShutdownHelper; use crate::client::base_client::storage::helpers::store_client_keys; use crate::client::base_client::storage::MixnetClientStorage; use crate::client::cover_traffic_stream::LoopCoverTrafficStream; @@ -27,13 +28,13 @@ use crate::client::topology_control::nym_api_provider::NymApiTopologyProvider; use crate::client::topology_control::{ TopologyAccessor, TopologyRefresher, TopologyRefresherConfig, }; +use crate::config; use crate::config::{Config, DebugConfig}; use crate::error::ClientCoreError; use crate::init::{ setup_gateway, types::{GatewaySetup, InitialisationResult}, }; -use crate::{config, spawn_future}; use futures::channel::mpsc; use nym_bandwidth_controller::BandwidthController; use nym_client_core_config_types::{ForgetMe, RememberMe}; @@ -48,16 +49,15 @@ use nym_gateway_client::{ use nym_sphinx::acknowledgements::AckKey; use nym_sphinx::addressing::clients::Recipient; use nym_sphinx::addressing::nodes::NodeIdentity; -use nym_sphinx::params::PacketType; use nym_sphinx::receiver::{ReconstructedMessage, SphinxMessageReceiver}; use nym_statistics_common::clients::ClientStatsSender; use nym_statistics_common::generate_client_stats_id; use nym_task::connections::{ConnectionCommandReceiver, ConnectionCommandSender, LaneQueueLengths}; -use nym_task::{TaskClient, TaskHandle}; +use nym_task::{ShutdownManager, ShutdownTracker}; use nym_topology::provider_trait::TopologyProvider; use nym_topology::HardcodedTopologyProvider; use nym_validator_client::nym_api::NymApiClientExt; -use nym_validator_client::{nyxd::contract_traits::DkgQueryClient, NymApiClient, UserAgent}; +use nym_validator_client::{nyxd::contract_traits::DkgQueryClient, UserAgent}; use rand::prelude::SliceRandom; use rand::rngs::OsRng; use rand::thread_rng; @@ -95,7 +95,6 @@ impl ClientInput { } } -#[derive(Clone)] pub struct ClientOutput { pub received_buffer_request_sender: ReceivedBufferRequestSender, } @@ -195,7 +194,7 @@ pub struct BaseClientBuilder { wait_for_gateway: bool, custom_topology_provider: Option>, custom_gateway_transceiver: Option>, - shutdown: Option, + shutdown: Option, user_agent: Option, setup_method: GatewaySetup, @@ -281,7 +280,7 @@ where } #[must_use] - pub fn with_shutdown(mut self, shutdown: TaskClient) -> Self { + pub fn with_shutdown(mut self, shutdown: ShutdownTracker) -> Self { self.shutdown = Some(shutdown); self } @@ -325,11 +324,11 @@ where topology_accessor: TopologyAccessor, mix_tx: BatchMixMessageSender, stats_tx: ClientStatsSender, - task_client: TaskClient, + shutdown_tracker: &ShutdownTracker, ) { info!("Starting loop cover traffic stream..."); - let stream = LoopCoverTrafficStream::new( + let mut stream = LoopCoverTrafficStream::new( ack_key, debug_config.acknowledgements.average_ack_delay, mix_tx, @@ -338,10 +337,9 @@ where debug_config.traffic, debug_config.cover_traffic, stats_tx, - task_client, ); - - stream.start(); + shutdown_tracker + .try_spawn_named_with_shutdown(async move { stream.run().await }, "CoverTrafficStream"); } #[allow(clippy::too_many_arguments)] @@ -357,13 +355,12 @@ where reply_controller_receiver: ReplyControllerReceiver, lane_queue_lengths: LaneQueueLengths, client_connection_rx: ConnectionCommandReceiver, - task_client: TaskClient, - packet_type: PacketType, stats_tx: ClientStatsSender, + shutdown_tracker: &ShutdownTracker, ) { info!("Starting real traffic stream..."); - RealMessagesController::new( + let real_messages_controller = RealMessagesController::new( controller_config, key_rotation_config, ack_receiver, @@ -376,9 +373,63 @@ where lane_queue_lengths, client_connection_rx, stats_tx, - task_client, - ) - .start(packet_type); + shutdown_tracker.clone_shutdown_token(), + ); + + // break out all the subtasks + let (mut out_queue_control, mut reply_controller, ack_controller) = + real_messages_controller.into_tasks(); + let ( + mut ack_listener, + mut input_listener, + mut retransmission_listener, + mut sent_notification_listener, + mut ack_action_controller, + ) = ack_controller.into_tasks(); + + shutdown_tracker.try_spawn_named( + async move { out_queue_control.run().await }, + "RealMessagesController::OutQueueControl", + ); + + let shutdown_token = shutdown_tracker.clone_shutdown_token(); + shutdown_tracker.try_spawn_named( + async move { reply_controller.run(shutdown_token).await }, + "RealMessagesController::ReplyController", + ); + + let shutdown_token = shutdown_tracker.clone_shutdown_token(); + shutdown_tracker.try_spawn_named( + async move { ack_listener.run(shutdown_token).await }, + "AcknowledgementController::AcknowledgementListener", + ); + + let shutdown_token = shutdown_tracker.clone_shutdown_token(); + shutdown_tracker.try_spawn_named( + async move { input_listener.run(shutdown_token).await }, + "AcknowledgementController::InputMessageListener", + ); + + let shutdown_token = shutdown_tracker.clone_shutdown_token(); + shutdown_tracker.try_spawn_named( + async move { retransmission_listener.run(shutdown_token).await }, + "AcknowledgementController::RetransmissionRequestListener", + ); + + shutdown_tracker.try_spawn_named_with_shutdown( + async move { + sent_notification_listener.run().await; + }, + "AcknowledgementController::SentNotificationListener", + ); + + let shutdown_token = shutdown_tracker.clone_shutdown_token(); + shutdown_tracker.try_spawn_named( + async move { ack_action_controller.run(shutdown_token).await }, + "AcknowledgementController::ActionController", + ); + + // .start(packet_type); } // buffer controlling all messages fetched from provider @@ -389,21 +440,29 @@ where mixnet_receiver: MixnetMessageReceiver, reply_key_storage: SentReplyKeys, reply_controller_sender: ReplyControllerSender, - shutdown: TaskClient, metrics_reporter: ClientStatsSender, + shutdown_tracker: &ShutdownTracker, ) { info!("Starting received messages buffer controller..."); - let controller: ReceivedMessagesBufferController = - ReceivedMessagesBufferController::new( - local_encryption_keypair, - query_receiver, - mixnet_receiver, - reply_key_storage, - reply_controller_sender, - metrics_reporter, - shutdown, - ); - controller.start() + let controller = ReceivedMessagesBufferController::::new( + local_encryption_keypair, + query_receiver, + mixnet_receiver, + reply_key_storage, + reply_controller_sender, + metrics_reporter, + shutdown_tracker.clone_shutdown_token(), + ); + let (mut msg_receiver, mut req_receiver) = controller.into_tasks(); + + shutdown_tracker.try_spawn_named( + async move { msg_receiver.run().await }, + "ReceivedMessagesBufferController::FragmentedMessageReceiver", + ); + shutdown_tracker.try_spawn_named( + async move { req_receiver.run().await }, + "ReceivedMessagesBufferController::RequestReceiver", + ); } #[allow(clippy::too_many_arguments)] @@ -415,7 +474,7 @@ where packet_router: PacketRouter, stats_reporter: ClientStatsSender, #[cfg(unix)] connection_fd_callback: Option>, - shutdown: TaskClient, + shutdown_tracker: &ShutdownTracker, ) -> Result, ClientCoreError> where ::StorageError: Send + Sync + 'static, @@ -434,7 +493,7 @@ where packet_router, bandwidth_controller, stats_reporter, - shutdown, + shutdown_tracker.clone_shutdown_token(), ) } else { let cfg = GatewayConfig::new( @@ -459,7 +518,7 @@ where stats_reporter, #[cfg(unix)] connection_fd_callback, - shutdown, + shutdown_tracker.clone_shutdown_token(), ) }; @@ -522,7 +581,7 @@ where packet_router: PacketRouter, stats_reporter: ClientStatsSender, #[cfg(unix)] connection_fd_callback: Option>, - mut shutdown: TaskClient, + shutdown_tracker: &ShutdownTracker, ) -> Result, ClientCoreError> where ::StorageError: Send + Sync + 'static, @@ -539,7 +598,6 @@ where Err(ClientCoreError::CustomGatewaySelectionExpected) } else { // and make sure to invalidate the task client, so we wouldn't cause premature shutdown - shutdown.disarm(); custom_gateway_transceiver.set_packet_router(packet_router)?; Ok(custom_gateway_transceiver) }; @@ -555,7 +613,7 @@ where stats_reporter, #[cfg(unix)] connection_fd_callback, - shutdown, + shutdown_tracker, ) .await?; @@ -566,7 +624,7 @@ where custom_provider: Option>, config_topology: config::Topology, nym_api_urls: Vec, - nym_api_client: NymApiClient, + nym_api_client: nym_http_api_client::Client, ) -> Box { // if no custom provider was ... provided ..., create one using nym-api custom_provider.unwrap_or_else(|| { @@ -586,22 +644,20 @@ where topology_accessor: TopologyAccessor, local_gateway: NodeIdentity, wait_for_gateway: bool, - mut task_client: TaskClient, + shutdown_tracker: &ShutdownTracker, ) -> Result<(), ClientCoreError> { let topology_refresher_config = TopologyRefresherConfig::new(topology_config.topology_refresh_rate); if topology_config.disable_refreshing { // if we're not spawning the refresher, don't cause shutdown immediately - info!("The background topology refesher is not going to be started"); - task_client.disarm(); + info!("The background topology refresher is not going to be started"); } let mut topology_refresher = TopologyRefresher::new( topology_refresher_config, topology_accessor, topology_provider, - task_client, ); // before returning, block entire runtime to refresh the current network view so that any // components depending on topology would see a non-empty view @@ -646,7 +702,10 @@ where // don't spawn the refresher if we don't want to be refreshing the topology. // only use the initial values obtained info!("Starting topology refresher..."); - topology_refresher.start(); + shutdown_tracker.try_spawn_named_with_shutdown( + async move { topology_refresher.run().await }, + "TopologyRefresher", + ); } Ok(()) @@ -657,7 +716,7 @@ where user_agent: Option, client_stats_id: String, input_sender: Sender, - task_client: TaskClient, + shutdown_tracker: &ShutdownTracker, ) -> ClientStatsSender { info!("Starting statistics control..."); StatisticsControl::create_and_start( @@ -667,18 +726,23 @@ where .unwrap_or("unknown".to_string()), client_stats_id, input_sender.clone(), - task_client, + shutdown_tracker, ) } fn start_mix_traffic_controller( gateway_transceiver: Box, - shutdown: TaskClient, + shutdown_tracker: &ShutdownTracker, ) -> (BatchMixMessageSender, ClientRequestSender) { info!("Starting mix traffic controller..."); - let (mix_traffic_controller, mix_tx, client_tx) = - MixTrafficController::new(gateway_transceiver, shutdown); - mix_traffic_controller.start(); + let (mut mix_traffic_controller, mix_tx, client_tx) = + MixTrafficController::new(gateway_transceiver, shutdown_tracker.clone_shutdown_token()); + + shutdown_tracker.try_spawn_named( + async move { mix_traffic_controller.run().await }, + "MixTrafficController", + ); + (mix_tx, client_tx) } @@ -686,7 +750,7 @@ where async fn setup_persistent_reply_storage( backend: S::ReplyStore, key_rotation_config: KeyRotationConfig, - shutdown: TaskClient, + shutdown_tracker: &ShutdownTracker, ) -> Result where ::StorageError: Sync + Send, @@ -711,13 +775,14 @@ where })?; let store_clone = mem_store.clone(); - spawn_future!( + let shutdown_token = shutdown_tracker.clone_shutdown_token(); + shutdown_tracker.try_spawn_named( async move { persistent_storage - .flush_on_shutdown(store_clone, shutdown) + .flush_on_shutdown(store_clone, shutdown_token) .await }, - "PersistentReplyStorage::flush_on_shutdown" + "PersistentReplyStorage::flush_on_shutdown", ); Ok(mem_store) @@ -749,21 +814,29 @@ where setup_gateway(setup_method, key_store, details_store).await } - fn construct_nym_api_client(config: &Config, user_agent: Option) -> NymApiClient { + fn construct_nym_api_client( + config: &Config, + user_agent: Option, + ) -> Result { let mut nym_api_urls = config.get_nym_api_endpoints(); nym_api_urls.shuffle(&mut thread_rng()); + let mut builder = nym_http_api_client::Client::builder(nym_api_urls[0].clone()) + .map_err(ClientCoreError::from)?; + if let Some(user_agent) = user_agent { - NymApiClient::new_with_user_agent(nym_api_urls[0].clone(), user_agent) - } else { - NymApiClient::new(nym_api_urls[0].clone()) + builder = builder.with_user_agent(user_agent); } + + builder = builder.with_bincode(); + + builder.build().map_err(ClientCoreError::from) } async fn determine_key_rotation_state( - client: &NymApiClient, + client: &nym_http_api_client::Client, ) -> Result { - Ok(client.nym_api.get_key_rotation_info().await?.into()) + Ok(client.get_key_rotation_info().await?.into()) } pub async fn start_base(mut self) -> Result @@ -809,11 +882,7 @@ where TopologyAccessor::new(self.config.debug.topology.ignore_egress_epoch_role); // Shutdown notifier for signalling tasks to stop - let shutdown = self - .shutdown - .map(Into::::into) - .unwrap_or_default() - .name_if_unnamed("BaseNymClient"); + let shutdown = ShutdownHelper::new(self.shutdown)?; // channels responsible for dealing with reply-related fun let (reply_controller_sender, reply_controller_receiver) = @@ -830,7 +899,7 @@ where .dkg_query_client .map(|client| BandwidthController::new(credential_store, client)); - let nym_api_client = Self::construct_nym_api_client(&self.config, self.user_agent.clone()); + let nym_api_client = Self::construct_nym_api_client(&self.config, self.user_agent.clone())?; let key_rotation_config = Self::determine_key_rotation_state(&nym_api_client).await?; let topology_provider = Self::setup_topology_provider( @@ -845,7 +914,7 @@ where self.user_agent.clone(), generate_client_stats_id(*self_address.identity()), input_sender.clone(), - shutdown.fork("statistics_control"), + shutdown.tracker(), ); // needs to be started as the first thing to block if required waiting for the gateway @@ -855,14 +924,14 @@ where shared_topology_accessor.clone(), self_address.gateway(), self.wait_for_gateway, - shutdown.fork("topology_refresher"), + shutdown.tracker(), ) .await?; let gateway_packet_router = PacketRouter::new( ack_sender, mixnet_messages_sender, - shutdown.get_handle().named("gateway-packet-router"), + shutdown.shutdown_token(), ); let gateway_transceiver = Self::setup_gateway_transceiver( @@ -875,7 +944,7 @@ where stats_reporter.clone(), #[cfg(unix)] self.connection_fd_callback, - shutdown.fork("gateway_transceiver"), + shutdown.tracker(), ) .await?; let gateway_ws_fd = gateway_transceiver.ws_fd(); @@ -883,7 +952,7 @@ where let reply_storage = Self::setup_persistent_reply_storage( reply_storage_backend, key_rotation_config, - shutdown.fork("persistent_reply_storage"), + shutdown.tracker(), ) .await?; @@ -893,8 +962,8 @@ where mixnet_messages_receiver, reply_storage.key_storage(), reply_controller_sender.clone(), - shutdown.fork("received_messages_buffer"), stats_reporter.clone(), + shutdown.tracker(), ); // The message_sender is the transmitter for any component generating sphinx packets @@ -902,10 +971,8 @@ where // traffic stream. // The MixTrafficController then sends the actual traffic - let (message_sender, client_request_sender) = Self::start_mix_traffic_controller( - gateway_transceiver, - shutdown.fork("mix_traffic_controller"), - ); + let (message_sender, client_request_sender) = + Self::start_mix_traffic_controller(gateway_transceiver, shutdown.tracker()); // Channels that the websocket listener can use to signal downstream to the real traffic // controller that connections are closed. @@ -933,9 +1000,8 @@ where reply_controller_receiver, shared_lane_queue_lengths.clone(), client_connection_rx, - shutdown.fork("real_traffic_controller"), - self.config.debug.traffic.packet_type, stats_reporter.clone(), + shutdown.tracker(), ); if !self @@ -951,7 +1017,7 @@ where shared_topology_accessor.clone(), message_sender, stats_reporter.clone(), - shutdown.fork("cover_traffic_stream"), + shutdown.tracker(), ); } @@ -979,7 +1045,7 @@ where gateway_connection: GatewayConnection { gateway_ws_fd }, }, stats_reporter, - task_handle: shutdown, + shutdown_handle: shutdown.into_internal(), client_request_sender, forget_me: self.config.debug.forget_me, remember_me: self.config.debug.remember_me, @@ -995,7 +1061,7 @@ pub struct BaseClient { pub client_state: ClientState, pub stats_reporter: ClientStatsSender, pub client_request_sender: ClientRequestSender, - pub task_handle: TaskHandle, + pub shutdown_handle: Option, pub forget_me: ForgetMe, pub remember_me: RememberMe, } diff --git a/common/client-core/src/client/cover_traffic_stream.rs b/common/client-core/src/client/cover_traffic_stream.rs index d635fc20c6..d10a6b04dc 100644 --- a/common/client-core/src/client/cover_traffic_stream.rs +++ b/common/client-core/src/client/cover_traffic_stream.rs @@ -3,7 +3,7 @@ use crate::client::mix_traffic::BatchMixMessageSender; use crate::client::topology_control::TopologyAccessor; -use crate::{config, spawn_future}; +use crate::config; use futures::task::{Context, Poll}; use futures::{Future, Stream, StreamExt}; use nym_sphinx::acknowledgements::AckKey; @@ -12,7 +12,6 @@ use nym_sphinx::cover::generate_loop_cover_packet; use nym_sphinx::params::{PacketSize, PacketType}; use nym_sphinx::utils::sample_poisson_duration; use nym_statistics_common::clients::{packet_statistics::PacketStatisticsEvent, ClientStatsSender}; -use nym_task::TaskClient; use rand::{rngs::OsRng, CryptoRng, Rng}; use std::pin::Pin; use std::sync::Arc; @@ -69,8 +68,6 @@ where packet_type: PacketType, stats_tx: ClientStatsSender, - - task_client: TaskClient, } impl Stream for LoopCoverTrafficStream @@ -117,7 +114,6 @@ impl LoopCoverTrafficStream { traffic_config: config::Traffic, cover_config: config::CoverTraffic, stats_tx: ClientStatsSender, - task_client: TaskClient, ) -> Self { let rng = OsRng; @@ -137,7 +133,6 @@ impl LoopCoverTrafficStream { use_legacy_sphinx_format: traffic_config.use_legacy_sphinx_format, packet_type: traffic_config.packet_type, stats_tx, - task_client, } } @@ -235,12 +230,13 @@ impl LoopCoverTrafficStream { tokio::task::yield_now().await; } + // it's fine if cover traffic stream task gets killed whilst processing next message #[allow(clippy::panic)] - pub fn start(mut self) { + pub async fn run(&mut self) { if self.cover_traffic.disable_loop_cover_traffic_stream { // we should have never got here in the first place - the task should have never been created to begin with // so panic and review the code that lead to this branch - panic!("attempted to start LoopCoverTrafficStream while config explicitly disabled it.") + panic!("attempted to run LoopCoverTrafficStream while config explicitly disabled it.") } // we should set initial delay only when we actually start the stream @@ -250,32 +246,11 @@ impl LoopCoverTrafficStream { ); self.set_next_delay(sampled); - let mut shutdown = self.task_client.fork("select"); + while self.next().await.is_some() { + self.on_new_message().await; + } - spawn_future!( - async move { - debug!("Started LoopCoverTrafficStream with graceful shutdown support"); - - while !shutdown.is_shutdown() { - tokio::select! { - biased; - _ = shutdown.recv() => { - tracing::trace!("LoopCoverTrafficStream: Received shutdown"); - } - next = self.next() => { - if next.is_some() { - self.on_new_message().await; - } else { - tracing::trace!("LoopCoverTrafficStream: Stopping since channel closed"); - break; - } - } - } - } - shutdown.recv_timeout().await; - tracing::debug!("LoopCoverTrafficStream: Exiting"); - }, - "LoopCoverTrafficStream" - ) + // this should never get triggered + error!("cover traffic stream has been exhausted!") } } diff --git a/common/client-core/src/client/mix_traffic/mod.rs b/common/client-core/src/client/mix_traffic/mod.rs index 9839d74473..33688e9067 100644 --- a/common/client-core/src/client/mix_traffic/mod.rs +++ b/common/client-core/src/client/mix_traffic/mod.rs @@ -2,11 +2,9 @@ // SPDX-License-Identifier: Apache-2.0 use crate::client::mix_traffic::transceiver::GatewayTransceiver; -use crate::error::ClientCoreError; -use crate::spawn_future; use nym_gateway_requests::ClientRequest; use nym_sphinx::forwarding::packet::MixPacket; -use nym_task::TaskClient; +use nym_task::ShutdownToken; use tracing::*; use transceiver::ErasedGatewayError; @@ -34,13 +32,13 @@ pub struct MixTrafficController { // in long run `gateway_client` will be moved away from `MixTrafficController` anyway. consecutive_gateway_failure_count: usize, - task_client: TaskClient, + shutdown_token: ShutdownToken, } impl MixTrafficController { pub fn new( gateway_transceiver: T, - task_client: TaskClient, + shutdown_token: ShutdownToken, ) -> ( MixTrafficController, BatchMixMessageSender, @@ -60,7 +58,7 @@ impl MixTrafficController { mix_rx: message_receiver, client_rx: client_receiver, consecutive_gateway_failure_count: 0, - task_client, + shutdown_token, }, message_sender, client_sender, @@ -69,7 +67,7 @@ impl MixTrafficController { pub fn new_dynamic( gateway_transceiver: Box, - task_client: TaskClient, + shutdown_token: ShutdownToken, ) -> ( MixTrafficController, BatchMixMessageSender, @@ -84,7 +82,7 @@ impl MixTrafficController { mix_rx: message_receiver, client_rx: client_receiver, consecutive_gateway_failure_count: 0, - task_client, + shutdown_token, }, message_sender, client_sender, @@ -107,7 +105,7 @@ impl MixTrafficController { tokio::select! { biased; - _ = self.task_client.recv() => { + _ = self.shutdown_token.cancelled() => { trace!("received shutdown while handling messages"); Ok(()) } @@ -127,7 +125,7 @@ impl MixTrafficController { async fn on_client_request(&mut self, client_request: ClientRequest) { tokio::select! { biased; - _ = self.task_client.recv() => { + _ = self.shutdown_token.cancelled() => { trace!("received shutdown while handling client request"); } result = self.gateway_transceiver.send_client_request(client_request) => { @@ -138,51 +136,44 @@ impl MixTrafficController { } } - pub fn start(mut self) { - spawn_future!( - async move { - debug!("Started MixTrafficController with graceful shutdown support"); - while !self.task_client.is_shutdown() { - tokio::select! { - biased; - _ = self.task_client.recv() => { - tracing::trace!("MixTrafficController: Received shutdown"); - break; - } - mix_packets = self.mix_rx.recv() => match mix_packets { - Some(mix_packets) => { - if let Err(err) = self.on_messages(mix_packets).await { - error!("Failed to send sphinx packet(s) to the gateway: {err}"); - if self.consecutive_gateway_failure_count == MAX_FAILURE_COUNT { - // Disconnect from the gateway. If we should try to re-connect - // is handled at a higher layer. - error!("Failed to send sphinx packet to the gateway {MAX_FAILURE_COUNT} times in a row - assuming the gateway is dead"); - // Do we need to handle the embedded mixnet client case - // separately? - self.task_client.send_we_stopped(Box::new(ClientCoreError::GatewayFailedToForwardMessages)); - break; - } - } - }, - None => { - tracing::trace!("MixTrafficController: Stopping since channel closed"); + pub async fn run(&mut self) { + debug!("Started MixTrafficController with graceful shutdown support"); + loop { + tokio::select! { + biased; + _ = self.shutdown_token.cancelled() => { + trace!("MixTrafficController: Received shutdown"); + break; + } + mix_packets = self.mix_rx.recv() => match mix_packets { + Some(mix_packets) => { + if let Err(err) = self.on_messages(mix_packets).await { + error!("Failed to send sphinx packet(s) to the gateway: {err}"); + if self.consecutive_gateway_failure_count == MAX_FAILURE_COUNT { + // Disconnect from the gateway. If we should try to re-connect + // is handled at a higher layer. + error!("Failed to send sphinx packet to the gateway {MAX_FAILURE_COUNT} times in a row - assuming the gateway is dead"); + // Do we need to handle the embedded mixnet client case + // separately? break; } - }, - client_request = self.client_rx.recv() => match client_request { - Some(client_request) => { - self.on_client_request(client_request).await; - }, - None => { - tracing::trace!("MixTrafficController, client request channel closed"); - } - }, + } + }, + None => { + trace!("MixTrafficController: Stopping since channel closed"); + break; } - } - self.task_client.recv_timeout().await; - tracing::debug!("MixTrafficController: Exiting"); - }, - "MixTrafficController" - ); + }, + client_request = self.client_rx.recv() => match client_request { + Some(client_request) => { + self.on_client_request(client_request).await; + }, + None => { + trace!("MixTrafficController, client request channel closed"); + break} + }, + } + } + debug!("MixTrafficController: Exiting"); } } diff --git a/common/client-core/src/client/real_messages_control/acknowledgement_control/acknowledgement_listener.rs b/common/client-core/src/client/real_messages_control/acknowledgement_control/acknowledgement_listener.rs index 7c7000c3b3..2167718e57 100644 --- a/common/client-core/src/client/real_messages_control/acknowledgement_control/acknowledgement_listener.rs +++ b/common/client-core/src/client/real_messages_control/acknowledgement_control/acknowledgement_listener.rs @@ -10,18 +10,17 @@ use nym_sphinx::{ acknowledgements::{identifier::recover_identifier, AckKey}, chunking::fragment::{FragmentIdentifier, COVER_FRAG_ID}, }; -use nym_task::TaskClient; +use nym_task::ShutdownToken; use std::sync::Arc; use tracing::*; /// Module responsible for listening for any data resembling acknowledgements from the network /// and firing actions to remove them from the 'Pending' state. -pub(super) struct AcknowledgementListener { +pub(crate) struct AcknowledgementListener { ack_key: Arc, ack_receiver: AcknowledgementReceiver, action_sender: AckActionSender, stats_tx: ClientStatsSender, - task_client: TaskClient, } impl AcknowledgementListener { @@ -30,14 +29,12 @@ impl AcknowledgementListener { ack_receiver: AcknowledgementReceiver, action_sender: AckActionSender, stats_tx: ClientStatsSender, - task_client: TaskClient, ) -> Self { AcknowledgementListener { ack_key, ack_receiver, action_sender, stats_tx, - task_client, } } @@ -68,14 +65,9 @@ impl AcknowledgementListener { trace!("Received {frag_id} from the mix network"); self.stats_tx .report(PacketStatisticsEvent::RealAckReceived(ack_content.len()).into()); - if let Err(err) = self + let _ = self .action_sender - .unbounded_send(Action::new_remove(frag_id)) - { - if !self.task_client.is_shutdown_poll() { - error!("Failed to send remove action to action controller: {err}"); - } - } + .unbounded_send(Action::new_remove(frag_id)); } async fn handle_ack_receiver_item(&mut self, item: Vec>) { @@ -85,11 +77,16 @@ impl AcknowledgementListener { } } - pub(super) async fn run(&mut self) { + pub(crate) async fn run(&mut self, shutdown_token: ShutdownToken) { debug!("Started AcknowledgementListener with graceful shutdown support"); - while !self.task_client.is_shutdown() { + loop { tokio::select! { + biased; + _ = shutdown_token.cancelled() => { + tracing::trace!("AcknowledgementListener: Received shutdown"); + break; + } acks = self.ack_receiver.next() => match acks { Some(acks) => self.handle_ack_receiver_item(acks).await, None => { @@ -97,12 +94,9 @@ impl AcknowledgementListener { break; } }, - _ = self.task_client.recv() => { - tracing::trace!("AcknowledgementListener: Received shutdown"); - } + } } - self.task_client.recv_timeout().await; tracing::debug!("AcknowledgementListener: Exiting"); } } diff --git a/common/client-core/src/client/real_messages_control/acknowledgement_control/action_controller.rs b/common/client-core/src/client/real_messages_control/acknowledgement_control/action_controller.rs index 350b400e46..6262a37e23 100644 --- a/common/client-core/src/client/real_messages_control/acknowledgement_control/action_controller.rs +++ b/common/client-core/src/client/real_messages_control/acknowledgement_control/action_controller.rs @@ -8,7 +8,7 @@ use futures::StreamExt; use nym_nonexhaustive_delayqueue::{Expired, NonExhaustiveDelayQueue, QueueKey}; use nym_sphinx::chunking::fragment::FragmentIdentifier; use nym_sphinx::Delay as SphinxDelay; -use nym_task::TaskClient; +use nym_task::ShutdownToken; use std::collections::HashMap; use std::sync::Arc; use std::time::Duration; @@ -82,7 +82,7 @@ impl Config { } } -pub(super) struct ActionController { +pub(crate) struct ActionController { /// Configurable parameters of the `ActionController` config: Config, @@ -102,8 +102,6 @@ pub(super) struct ActionController { /// Channel for notifying `RetransmissionRequestListener` about expired acknowledgements. retransmission_sender: RetransmissionRequestSender, - - task_client: TaskClient, } impl ActionController { @@ -111,7 +109,6 @@ impl ActionController { config: Config, retransmission_sender: RetransmissionRequestSender, incoming_actions: AckActionReceiver, - task_client: TaskClient, ) -> Self { ActionController { config, @@ -119,7 +116,6 @@ impl ActionController { pending_acks_timers: NonExhaustiveDelayQueue::new(), incoming_actions, retransmission_sender, - task_client, } } @@ -226,14 +222,9 @@ impl ActionController { // downgrading an arc and then upgrading vs cloning is difference of 30ns vs 15ns // so it's literally a NO difference while it might prevent us from unnecessarily // resending data (in maybe 1 in 1 million cases, but it's something) - if let Err(err) = self + let _ = self .retransmission_sender - .unbounded_send(Arc::downgrade(pending_ack_data)) - { - if !self.task_client.is_shutdown_poll() { - tracing::error!("Failed to send pending ack for retransmission: {err}"); - } - } + .unbounded_send(Arc::downgrade(pending_ack_data)); } else { // this shouldn't cause any issues but shouldn't have happened to begin with! error!("An already removed pending ack has expired") @@ -251,11 +242,16 @@ impl ActionController { } } - pub(super) async fn run(&mut self) { + pub(crate) async fn run(&mut self, shutdown_token: ShutdownToken) { debug!("Started ActionController with graceful shutdown support"); - while !self.task_client.is_shutdown() { + loop { tokio::select! { + biased; + _ = shutdown_token.cancelled() => { + tracing::trace!("ActionController: Received shutdown"); + break; + } action = self.incoming_actions.next() => match action { Some(action) => self.process_action(action), None => { @@ -272,13 +268,8 @@ impl ActionController { break; } }, - _ = self.task_client.recv() => { - tracing::trace!("ActionController: Received shutdown"); - break; - } } } - self.task_client.recv_timeout().await; tracing::debug!("ActionController: Exiting"); } } diff --git a/common/client-core/src/client/real_messages_control/acknowledgement_control/input_message_listener.rs b/common/client-core/src/client/real_messages_control/acknowledgement_control/input_message_listener.rs index f47f050b3b..69ca92709f 100644 --- a/common/client-core/src/client/real_messages_control/acknowledgement_control/input_message_listener.rs +++ b/common/client-core/src/client/real_messages_control/acknowledgement_control/input_message_listener.rs @@ -10,21 +10,20 @@ use nym_sphinx::anonymous_replies::requests::AnonymousSenderTag; use nym_sphinx::forwarding::packet::MixPacket; use nym_sphinx::params::PacketType; use nym_task::connections::TransmissionLane; -use nym_task::TaskClient; +use nym_task::ShutdownToken; use rand::{CryptoRng, Rng}; use tracing::*; /// Module responsible for dealing with the received messages: splitting them, creating acknowledgements, /// putting everything into sphinx packets, etc. /// It also makes an initial sending attempt for said messages. -pub(super) struct InputMessageListener +pub(crate) struct InputMessageListener where R: CryptoRng + Rng, { input_receiver: InputMessageReceiver, message_handler: MessageHandler, reply_controller_sender: ReplyControllerSender, - task_client: TaskClient, } impl InputMessageListener @@ -38,13 +37,11 @@ where input_receiver: InputMessageReceiver, message_handler: MessageHandler, reply_controller_sender: ReplyControllerSender, - task_client: TaskClient, ) -> Self { InputMessageListener { input_receiver, message_handler, reply_controller_sender, - task_client, } } @@ -68,14 +65,9 @@ where max_retransmissions: Option, ) { // offload reply handling to the dedicated task - if let Err(err) = + let _ = self.reply_controller_sender - .send_reply(recipient_tag, data, lane, max_retransmissions) - { - if !self.task_client.is_shutdown_poll() { - error!("failed to send a reply - {err}"); - } - } + .send_reply(recipient_tag, data, lane, max_retransmissions); } async fn handle_plain_message( @@ -221,13 +213,13 @@ where }; } - pub(super) async fn run(&mut self) { + pub(crate) async fn run(&mut self, shutdown_token: ShutdownToken) { debug!("Started InputMessageListener with graceful shutdown support"); - while !self.task_client.is_shutdown() { + loop { tokio::select! { biased; - _ = self.task_client.recv() => { + _ = shutdown_token.cancelled() => { tracing::trace!("InputMessageListener: Received shutdown"); break; } @@ -243,7 +235,6 @@ where } } - self.task_client.recv_timeout().await; tracing::debug!("InputMessageListener: Exiting"); } } diff --git a/common/client-core/src/client/real_messages_control/acknowledgement_control/mod.rs b/common/client-core/src/client/real_messages_control/acknowledgement_control/mod.rs index ea332ebdaf..fd17348ee9 100644 --- a/common/client-core/src/client/real_messages_control/acknowledgement_control/mod.rs +++ b/common/client-core/src/client/real_messages_control/acknowledgement_control/mod.rs @@ -10,7 +10,6 @@ use self::{ use crate::client::inbound_messages::InputMessageReceiver; use crate::client::real_messages_control::message_handler::MessageHandler; use crate::client::replies::reply_controller::ReplyControllerSender; -use crate::spawn_future; use action_controller::AckActionReceiver; use futures::channel::mpsc; use nym_gateway_client::AcknowledgementReceiver; @@ -23,13 +22,11 @@ use nym_sphinx::{ Delay as SphinxDelay, }; use nym_statistics_common::clients::ClientStatsSender; -use nym_task::TaskClient; use rand::{CryptoRng, Rng}; use std::{ sync::{Arc, Weak}, time::Duration, }; -use tracing::*; pub(crate) use action_controller::{AckActionSender, Action}; @@ -190,6 +187,9 @@ pub(super) struct Config { /// Predefined packet size used for the encapsulated messages. packet_size: PacketSize, + + /// Type of packets used for retransmissions + packet_type: PacketType, } impl Config { @@ -197,12 +197,14 @@ impl Config { maximum_retransmissions: Option, ack_wait_addition: Duration, ack_wait_multiplier: f64, + packet_type: PacketType, ) -> Self { Config { maximum_retransmissions, ack_wait_addition, ack_wait_multiplier, packet_size: Default::default(), + packet_type, } } @@ -212,7 +214,7 @@ impl Config { } } -pub(super) struct AcknowledgementController +pub(crate) struct AcknowledgementController where R: CryptoRng + Rng, { @@ -234,7 +236,6 @@ where message_handler: MessageHandler, reply_controller_sender: ReplyControllerSender, stats_tx: ClientStatsSender, - task_client: TaskClient, ) -> Self { let (retransmission_tx, retransmission_rx) = mpsc::unbounded(); @@ -244,7 +245,6 @@ where action_config, retransmission_tx, connectors.ack_action_receiver, - task_client.fork("action_controller"), ); // will listen for any acks coming from the network @@ -253,7 +253,6 @@ where connectors.ack_receiver, connectors.ack_action_sender.clone(), stats_tx, - task_client.fork("acknowledgement_listener"), ); // will listen for any new messages from the client @@ -261,7 +260,6 @@ where connectors.input_receiver, message_handler.clone(), reply_controller_sender.clone(), - task_client.fork("input_message_listener"), ); // will listen for any ack timeouts and trigger retransmission @@ -271,16 +269,13 @@ where message_handler, retransmission_rx, reply_controller_sender, - task_client.fork("retransmission_request_listener"), + config.packet_type, ); // will listen for events indicating the packet was sent through the network so that // the retransmission timer should be started. - let sent_notification_listener = SentNotificationListener::new( - connectors.sent_notifier, - connectors.ack_action_sender, - task_client.with_suffix("sent_notification_listener"), - ); + let sent_notification_listener = + SentNotificationListener::new(connectors.sent_notifier, connectors.ack_action_sender); AcknowledgementController { acknowledgement_listener, @@ -291,51 +286,21 @@ where } } - pub(super) fn start(self, packet_type: PacketType) { - let mut acknowledgement_listener = self.acknowledgement_listener; - let mut input_message_listener = self.input_message_listener; - let mut retransmission_request_listener = self.retransmission_request_listener; - let mut sent_notification_listener = self.sent_notification_listener; - let mut action_controller = self.action_controller; - - spawn_future!( - async move { - acknowledgement_listener.run().await; - debug!("The acknowledgement listener has finished execution!"); - }, - "AcknowledgementController::AcknowledgementListener" - ); - - spawn_future!( - async move { - input_message_listener.run().await; - debug!("The input listener has finished execution!"); - }, - "AcknowledgementController::InputMessageListener" - ); - - spawn_future!( - async move { - retransmission_request_listener.run(packet_type).await; - debug!("The retransmission request listener has finished execution!"); - }, - "AcknowledgementController::RetransmissionRequestListener" - ); - - spawn_future!( - async move { - sent_notification_listener.run().await; - debug!("The sent notification listener has finished execution!"); - }, - "AcknowledgementController::SentNotificationListener" - ); - - spawn_future!( - async move { - action_controller.run().await; - debug!("The controller has finished execution!"); - }, - "AcknowledgementController::ActionController" - ); + pub(crate) fn into_tasks( + self, + ) -> ( + AcknowledgementListener, + InputMessageListener, + RetransmissionRequestListener, + SentNotificationListener, + ActionController, + ) { + ( + self.acknowledgement_listener, + self.input_message_listener, + self.retransmission_request_listener, + self.sent_notification_listener, + self.action_controller, + ) } } diff --git a/common/client-core/src/client/real_messages_control/acknowledgement_control/retransmission_request_listener.rs b/common/client-core/src/client/real_messages_control/acknowledgement_control/retransmission_request_listener.rs index 0b066d0cc0..597bfecf56 100644 --- a/common/client-core/src/client/real_messages_control/acknowledgement_control/retransmission_request_listener.rs +++ b/common/client-core/src/client/real_messages_control/acknowledgement_control/retransmission_request_listener.rs @@ -13,19 +13,19 @@ use futures::StreamExt; use nym_sphinx::chunking::fragment::Fragment; use nym_sphinx::preparer::PreparedFragment; use nym_sphinx::{addressing::clients::Recipient, params::PacketType}; -use nym_task::{connections::TransmissionLane, TaskClient}; +use nym_task::{connections::TransmissionLane, ShutdownToken}; use rand::{CryptoRng, Rng}; use std::sync::{Arc, Weak}; use tracing::*; // responsible for packet retransmission upon fired timer -pub(super) struct RetransmissionRequestListener { +pub(crate) struct RetransmissionRequestListener { maximum_retransmissions: Option, action_sender: AckActionSender, message_handler: MessageHandler, request_receiver: RetransmissionRequestReceiver, reply_controller_sender: ReplyControllerSender, - task_client: TaskClient, + packet_type: PacketType, } impl RetransmissionRequestListener @@ -38,7 +38,7 @@ where message_handler: MessageHandler, request_receiver: RetransmissionRequestReceiver, reply_controller_sender: ReplyControllerSender, - task_client: TaskClient, + packet_type: PacketType, ) -> Self { RetransmissionRequestListener { maximum_retransmissions, @@ -46,7 +46,7 @@ where message_handler, request_receiver, reply_controller_sender, - task_client, + packet_type, } } @@ -67,7 +67,6 @@ where async fn on_retransmission_request( &mut self, weak_timed_out_ack: Weak, - packet_type: PacketType, ) { let timed_out_ack = match weak_timed_out_ack.upgrade() { Some(timed_out_ack) => timed_out_ack, @@ -97,22 +96,18 @@ where } => { // if this is retransmission for reply, offload it to the dedicated task // that deals with all the surbs - if let Err(err) = self.reply_controller_sender.send_retransmission_data( + let _ = self.reply_controller_sender.send_retransmission_data( *recipient_tag, weak_timed_out_ack, *extra_surb_request, - ) { - if !self.task_client.is_shutdown_poll() { - error!("Failed to send retransmission data to the reply controller: {err}"); - } - } + ); return; } PacketDestination::KnownRecipient(recipient) => { self.prepare_normal_retransmission_chunk( **recipient, timed_out_ack.message_chunk.clone(), - packet_type, + self.packet_type, ) .await } @@ -153,14 +148,9 @@ where // is sent to the `OutQueueControl` and has gone through its internal queue // with the additional poisson delay. // And since Actions are executed in order `UpdateTimer` will HAVE TO be executed before `StartTimer` - if let Err(err) = self + let _ = self .action_sender - .unbounded_send(Action::new_update_pending_ack(frag_id, new_delay)) - { - if !self.task_client.is_shutdown_poll() { - error!("Failed to send update pending ack action to the controller: {err}"); - } - } + .unbounded_send(Action::new_update_pending_ack(frag_id, new_delay)); // send to `OutQueueControl` to eventually send to the mix network self.message_handler @@ -174,18 +164,18 @@ where .await } - pub(super) async fn run(&mut self, packet_type: PacketType) { + pub(crate) async fn run(&mut self, shutdown_token: ShutdownToken) { debug!("Started RetransmissionRequestListener with graceful shutdown support"); - while !self.task_client.is_shutdown() { + loop { tokio::select! { biased; - _ = self.task_client.recv() => { + _ = shutdown_token.cancelled() => { tracing::trace!("RetransmissionRequestListener: Received shutdown"); break; } timed_out_ack = self.request_receiver.next() => match timed_out_ack { - Some(timed_out_ack) => self.on_retransmission_request(timed_out_ack, packet_type).await, + Some(timed_out_ack) => self.on_retransmission_request(timed_out_ack).await, None => { tracing::trace!("RetransmissionRequestListener: Stopping since channel closed"); break; @@ -194,7 +184,6 @@ where } } - self.task_client.recv_timeout().await; tracing::debug!("RetransmissionRequestListener: Exiting"); } } diff --git a/common/client-core/src/client/real_messages_control/acknowledgement_control/sent_notification_listener.rs b/common/client-core/src/client/real_messages_control/acknowledgement_control/sent_notification_listener.rs index 9ee9cdf461..02560805a8 100644 --- a/common/client-core/src/client/real_messages_control/acknowledgement_control/sent_notification_listener.rs +++ b/common/client-core/src/client/real_messages_control/acknowledgement_control/sent_notification_listener.rs @@ -5,29 +5,25 @@ use super::action_controller::{AckActionSender, Action}; use super::SentPacketNotificationReceiver; use futures::StreamExt; use nym_sphinx::chunking::fragment::{FragmentIdentifier, COVER_FRAG_ID}; -use nym_task::TaskClient; use tracing::*; /// Module responsible for starting up retransmission timers. /// It is required because when we send our packet to the `real traffic stream` controlled /// by a poisson timer, there's no guarantee the message will be sent immediately, so we might /// accidentally fire retransmission way quicker than we should have. -pub(super) struct SentNotificationListener { +pub(crate) struct SentNotificationListener { sent_notifier: SentPacketNotificationReceiver, action_sender: AckActionSender, - task_client: TaskClient, } impl SentNotificationListener { pub(super) fn new( sent_notifier: SentPacketNotificationReceiver, action_sender: AckActionSender, - task_client: TaskClient, ) -> Self { SentNotificationListener { sent_notifier, action_sender, - task_client, } } @@ -36,37 +32,18 @@ impl SentNotificationListener { trace!("sent off a cover message - no need to start retransmission timer!"); return; } - if let Err(err) = self + let _ = self .action_sender - .unbounded_send(Action::new_start_timer(frag_id)) - { - if !self.task_client.is_shutdown_poll() { - error!("Failed to send start timer action to action controller: {err}"); - } - } + .unbounded_send(Action::new_start_timer(frag_id)); } - pub(super) async fn run(&mut self) { + pub(crate) async fn run(&mut self) { debug!("Started SentNotificationListener with graceful shutdown support"); - while !self.task_client.is_shutdown() { - tokio::select! { - frag_id = self.sent_notifier.next() => match frag_id { - Some(frag_id) => { - self.on_sent_message(frag_id).await; - } - None => { - tracing::trace!("SentNotificationListener: Stopping since channel closed"); - break; - } - }, - _ = self.task_client.recv() => { - tracing::trace!("SentNotificationListener: Received shutdown"); - break; - } - } + while let Some(frag_id) = self.sent_notifier.next().await { + self.on_sent_message(frag_id).await; } - assert!(self.task_client.is_shutdown_poll()); + tracing::debug!("SentNotificationListener: Exiting"); } } diff --git a/common/client-core/src/client/real_messages_control/message_handler.rs b/common/client-core/src/client/real_messages_control/message_handler.rs index 1dca40cf7e..366b266090 100644 --- a/common/client-core/src/client/real_messages_control/message_handler.rs +++ b/common/client-core/src/client/real_messages_control/message_handler.rs @@ -20,7 +20,7 @@ use nym_sphinx::params::{PacketSize, PacketType}; use nym_sphinx::preparer::{MessagePreparer, PreparedFragment}; use nym_sphinx::Delay; use nym_task::connections::TransmissionLane; -use nym_task::TaskClient; +use nym_task::ShutdownToken; use nym_topology::{NymRouteProvider, NymTopologyError}; use rand::{CryptoRng, Rng}; use std::collections::HashMap; @@ -189,7 +189,7 @@ pub(crate) struct MessageHandler { topology_access: TopologyAccessor, reply_key_storage: SentReplyKeys, tag_storage: UsedSenderTags, - task_client: TaskClient, + shutdown_token: ShutdownToken, } impl MessageHandler @@ -205,7 +205,7 @@ where topology_access: TopologyAccessor, reply_key_storage: SentReplyKeys, tag_storage: UsedSenderTags, - task_client: TaskClient, + shutdown_token: ShutdownToken, ) -> Self where R: Copy, @@ -228,7 +228,7 @@ where topology_access, reply_key_storage, tag_storage, - task_client, + shutdown_token, } } @@ -712,7 +712,7 @@ where .action_sender .unbounded_send(Action::UpdatePendingAck(id, new_delay)) { - if !self.task_client.is_shutdown_poll() { + if !self.shutdown_token.is_cancelled() { error!("Failed to send update action to the controller: {err}"); } } @@ -723,7 +723,7 @@ where .action_sender .unbounded_send(Action::new_insert(pending_acks)) { - if !self.task_client.is_shutdown_poll() { + if !self.shutdown_token.is_cancelled() { error!("Failed to send insert action to the controller: {err}"); } } @@ -737,7 +737,7 @@ where ) { tokio::select! { biased; - _ = self.task_client.recv() => { + _ = self.shutdown_token.cancelled() => { trace!("received shutdown while attempting to forward mixnet messages"); } sending_res = self.real_message_sender.send((messages, transmission_lane)) => { diff --git a/common/client-core/src/client/real_messages_control/mod.rs b/common/client-core/src/client/real_messages_control/mod.rs index b169a9bff8..9b852535fa 100644 --- a/common/client-core/src/client/real_messages_control/mod.rs +++ b/common/client-core/src/client/real_messages_control/mod.rs @@ -14,26 +14,21 @@ use crate::client::replies::reply_controller::{ ReplyController, ReplyControllerReceiver, ReplyControllerSender, }; use crate::client::replies::reply_storage::CombinedReplyStorage; -use crate::config; -use crate::{ - client::{ - inbound_messages::InputMessageReceiver, mix_traffic::BatchMixMessageSender, - real_messages_control::acknowledgement_control::AcknowledgementControllerConnectors, - topology_control::TopologyAccessor, - }, - spawn_future, +use crate::client::{ + inbound_messages::InputMessageReceiver, mix_traffic::BatchMixMessageSender, + real_messages_control::acknowledgement_control::AcknowledgementControllerConnectors, + topology_control::TopologyAccessor, }; +use crate::config; use futures::channel::mpsc; use nym_gateway_client::AcknowledgementReceiver; use nym_sphinx::acknowledgements::AckKey; use nym_sphinx::addressing::clients::Recipient; -use nym_sphinx::params::PacketType; use nym_statistics_common::clients::ClientStatsSender; use nym_task::connections::{ConnectionCommandReceiver, LaneQueueLengths}; -use nym_task::TaskClient; +use nym_task::ShutdownToken; use rand::{rngs::OsRng, CryptoRng, Rng}; use std::sync::Arc; -use tracing::*; use crate::client::replies::reply_controller::key_rotation_helpers::KeyRotationConfig; pub(crate) use acknowledgement_control::{AckActionSender, Action}; @@ -69,6 +64,7 @@ impl<'a> From<&'a Config> for acknowledgement_control::Config { cfg.traffic.maximum_number_of_retransmissions, cfg.acks.ack_wait_addition, cfg.acks.ack_wait_multiplier, + cfg.traffic.packet_type, ) .with_custom_packet_size(cfg.traffic.primary_packet_size) } @@ -146,7 +142,7 @@ impl RealMessagesController { lane_queue_lengths: LaneQueueLengths, client_connection_rx: ConnectionCommandReceiver, stats_tx: ClientStatsSender, - task_client: TaskClient, + shutdown_token: ShutdownToken, ) -> Self { let rng = OsRng; @@ -178,7 +174,7 @@ impl RealMessagesController { topology_access.clone(), reply_storage.key_storage(), reply_storage.tags_storage(), - task_client.fork("message_handler"), + shutdown_token.clone(), ); let ack_control = AcknowledgementController::new( @@ -188,7 +184,6 @@ impl RealMessagesController { message_handler.clone(), reply_controller_sender, stats_tx.clone(), - task_client.fork("ack_control"), ); let reply_control = ReplyController::new( @@ -196,7 +191,6 @@ impl RealMessagesController { message_handler, reply_storage, reply_controller_receiver, - task_client.fork("reply_controller"), ); let out_queue_control = OutQueueControl::new( @@ -209,7 +203,7 @@ impl RealMessagesController { lane_queue_lengths, client_connection_rx, stats_tx, - task_client.with_suffix("out_queue_control"), + shutdown_token.clone(), ); RealMessagesController { @@ -219,26 +213,13 @@ impl RealMessagesController { } } - pub fn start(self, packet_type: PacketType) { - let mut out_queue_control = self.out_queue_control; - let ack_control = self.ack_control; - let mut reply_control = self.reply_control; - - spawn_future!( - async move { - out_queue_control.run().await; - debug!("The out queue controller has finished execution!"); - }, - "RealMessagesController::OutQueueControl)" - ); - spawn_future!( - async move { - reply_control.run().await; - debug!("The reply controller has finished execution!"); - }, - "RealMessagesController::ReplyController" - ); - - ack_control.start(packet_type); + pub fn into_tasks( + self, + ) -> ( + OutQueueControl, + ReplyController, + AcknowledgementController, + ) { + (self.out_queue_control, self.reply_control, self.ack_control) } } diff --git a/common/client-core/src/client/real_messages_control/real_traffic_stream.rs b/common/client-core/src/client/real_messages_control/real_traffic_stream.rs index 7cbf3b6ad6..92faffdbf2 100644 --- a/common/client-core/src/client/real_messages_control/real_traffic_stream.rs +++ b/common/client-core/src/client/real_messages_control/real_traffic_stream.rs @@ -21,7 +21,7 @@ use nym_statistics_common::clients::{packet_statistics::PacketStatisticsEvent, C use nym_task::connections::{ ConnectionCommand, ConnectionCommandReceiver, ConnectionId, LaneQueueLengths, TransmissionLane, }; -use nym_task::TaskClient; +use nym_task::ShutdownToken; use rand::{CryptoRng, Rng}; use std::pin::Pin; use std::sync::Arc; @@ -119,7 +119,7 @@ where /// Channel used for sending metrics events (specifically `PacketStatistics` events) to the metrics tracker. stats_tx: ClientStatsSender, - task_client: TaskClient, + shutdown_token: ShutdownToken, } #[derive(Debug)] @@ -179,7 +179,7 @@ where lane_queue_lengths: LaneQueueLengths, client_connection_rx: ConnectionCommandReceiver, stats_tx: ClientStatsSender, - task_client: TaskClient, + shutdown_token: ShutdownToken, ) -> Self { OutQueueControl { config, @@ -194,7 +194,7 @@ where client_connection_rx, lane_queue_lengths, stats_tx, - task_client, + shutdown_token, } } @@ -282,7 +282,7 @@ where let sending_res = tokio::select! { biased; - _ = self.task_client.recv() => { + _ = self.shutdown_token.cancelled() => { trace!("received shutdown signal while attempting to send mix message"); return } @@ -293,7 +293,7 @@ where match sending_res { Err(_) => { - if !self.task_client.is_shutdown_poll() { + if !self.shutdown_token.is_cancelled() { tracing::error!( "failed to send mixnet packet due to closed channel (outside of shutdown!)" ); @@ -536,9 +536,7 @@ where } #[cfg(not(target_arch = "wasm32"))] - fn log_status(&self, shutdown: &mut TaskClient) { - use crate::error::ClientCoreStatusMessage; - + fn log_status(&self) { let packets = self.transmission_buffer.total_size(); let lanes = self.transmission_buffer.lanes(); let mult = self.sending_delay_controller.current_multiplier(); @@ -567,32 +565,33 @@ where tracing::debug!("{status_str}"); } - // Send status message to whoever is listening (possibly UI) - if mult == self.sending_delay_controller.max_multiplier() { - shutdown.send_status_msg(Box::new(ClientCoreStatusMessage::GatewayIsVerySlow)); - } else if mult > self.sending_delay_controller.min_multiplier() { - shutdown.send_status_msg(Box::new(ClientCoreStatusMessage::GatewayIsSlow)); - } + // leave the code commented in case somebody wanted to restore this logic with a different channel + // // Send status message to whoever is listening (possibly UI) + // if mult == self.sending_delay_controller.max_multiplier() { + // shutdown.send_status_msg(Box::new(ClientCoreStatusMessage::GatewayIsVerySlow)); + // } else if mult > self.sending_delay_controller.min_multiplier() { + // shutdown.send_status_msg(Box::new(ClientCoreStatusMessage::GatewayIsSlow)); + // } } - pub(super) async fn run(&mut self) { + pub(crate) async fn run(&mut self) { debug!("Started OutQueueControl with graceful shutdown support"); - let mut shutdown = self.task_client.fork("select"); - + // avoid borrow on self + let shutdown_token = self.shutdown_token.clone(); #[cfg(not(target_arch = "wasm32"))] { let mut status_timer = tokio::time::interval(Duration::from_secs(5)); - while !shutdown.is_shutdown() { + loop { tokio::select! { biased; - _ = shutdown.recv() => { + _ = shutdown_token.cancelled() => { tracing::trace!("OutQueueControl: Received shutdown"); break; } _ = status_timer.tick() => { - self.log_status(&mut shutdown); + self.log_status(); } next_message = self.next() => if let Some(next_message) = next_message { self.on_message(next_message).await; @@ -602,16 +601,16 @@ where } } } - shutdown.recv_timeout().await; } #[cfg(target_arch = "wasm32")] { - while !shutdown.is_shutdown() { + loop { tokio::select! { biased; - _ = shutdown.recv() => { + _ = shutdown_token.cancelled() => { tracing::trace!("OutQueueControl: Received shutdown"); + break; } next_message = self.next() => if let Some(next_message) = next_message { self.on_message(next_message).await; diff --git a/common/client-core/src/client/real_messages_control/real_traffic_stream/sending_delay_controller.rs b/common/client-core/src/client/real_messages_control/real_traffic_stream/sending_delay_controller.rs index 7c147bed8d..fa9898e42b 100644 --- a/common/client-core/src/client/real_messages_control/real_traffic_stream/sending_delay_controller.rs +++ b/common/client-core/src/client/real_messages_control/real_traffic_stream/sending_delay_controller.rs @@ -83,11 +83,13 @@ impl SendingDelayController { self.current_multiplier } + #[allow(dead_code)] #[cfg(not(target_arch = "wasm32"))] pub(crate) fn min_multiplier(&self) -> u32 { self.lower_bound } + #[allow(dead_code)] #[cfg(not(target_arch = "wasm32"))] pub(crate) fn max_multiplier(&self) -> u32 { self.upper_bound diff --git a/common/client-core/src/client/received_buffer.rs b/common/client-core/src/client/received_buffer.rs index 9ed1101867..b31cccb419 100644 --- a/common/client-core/src/client/received_buffer.rs +++ b/common/client-core/src/client/received_buffer.rs @@ -1,10 +1,10 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use crate::client::helpers::get_time_now; use crate::client::replies::{ reply_controller::ReplyControllerSender, reply_storage::SentReplyKeys, }; -use crate::spawn_future; use futures::channel::mpsc; use futures::lock::Mutex; use futures::StreamExt; @@ -19,10 +19,10 @@ use nym_sphinx::message::{NymMessage, PlainMessage}; use nym_sphinx::params::ReplySurbKeyDigestAlgorithm; use nym_sphinx::receiver::{MessageReceiver, MessageRecoveryError, ReconstructedMessage}; use nym_statistics_common::clients::{packet_statistics::PacketStatisticsEvent, ClientStatsSender}; -use nym_task::TaskClient; +use nym_task::ShutdownToken; use std::collections::HashSet; use std::sync::Arc; -use std::time::{Duration, Instant}; +use std::time::Duration; use tracing::*; // The interval at which we check for stale buffers @@ -54,7 +54,7 @@ struct ReceivedMessagesBufferInner { stats_tx: ClientStatsSender, // Periodically check for stale buffers to clean up - last_stale_check: Instant, + last_stale_check: crate::client::helpers::Instant, } impl ReceivedMessagesBufferInner { @@ -154,7 +154,7 @@ impl ReceivedMessagesBufferInner { } fn cleanup_stale_buffers(&mut self) { - let now = Instant::now(); + let now = get_time_now(); if now - self.last_stale_check > STALE_BUFFER_CHECK_INTERVAL { self.last_stale_check = now; self.message_receiver @@ -171,7 +171,7 @@ struct ReceivedMessagesBuffer { inner: Arc>>, reply_key_storage: SentReplyKeys, reply_controller_sender: ReplyControllerSender, - task_client: TaskClient, + shutdown_token: ShutdownToken, } impl ReceivedMessagesBuffer { @@ -180,7 +180,7 @@ impl ReceivedMessagesBuffer { reply_key_storage: SentReplyKeys, reply_controller_sender: ReplyControllerSender, stats_tx: ClientStatsSender, - task_client: TaskClient, + shutdown_token: ShutdownToken, ) -> Self { ReceivedMessagesBuffer { inner: Arc::new(Mutex::new(ReceivedMessagesBufferInner { @@ -190,11 +190,11 @@ impl ReceivedMessagesBuffer { message_sender: None, recently_reconstructed: HashSet::new(), stats_tx, - last_stale_check: Instant::now(), + last_stale_check: get_time_now(), })), reply_key_storage, reply_controller_sender, - task_client, + shutdown_token, } } @@ -315,7 +315,7 @@ impl ReceivedMessagesBuffer { reply_surbs, from_surb_request, ) { - if !self.task_client.is_shutdown_poll() { + if !self.shutdown_token.is_cancelled() { error!("{err}"); } } @@ -338,7 +338,7 @@ impl ReceivedMessagesBuffer { .reply_controller_sender .send_additional_surbs_request(*recipient, amount) { - if !self.task_client.is_shutdown_poll() { + if !self.shutdown_token.is_cancelled() { error!("{err}"); } } @@ -465,22 +465,22 @@ pub enum ReceivedBufferMessage { ReceiverDisconnect, } -struct RequestReceiver { +pub(crate) struct RequestReceiver { received_buffer: ReceivedMessagesBuffer, query_receiver: ReceivedBufferRequestReceiver, - task_client: TaskClient, + shutdown_token: ShutdownToken, } impl RequestReceiver { fn new( received_buffer: ReceivedMessagesBuffer, query_receiver: ReceivedBufferRequestReceiver, - task_client: TaskClient, + shutdown_token: ShutdownToken, ) -> Self { RequestReceiver { received_buffer, query_receiver, - task_client, + shutdown_token, } } @@ -495,66 +495,70 @@ impl RequestReceiver { } } - async fn run(&mut self) { + pub(crate) async fn run(&mut self) { debug!("Started RequestReceiver with graceful shutdown support"); - while !self.task_client.is_shutdown() { + loop { tokio::select! { biased; - _ = self.task_client.recv() => { + _ = self.shutdown_token.cancelled() => { tracing::trace!("RequestReceiver: Received shutdown"); + break; } request = self.query_receiver.next() => { if let Some(message) = request { self.handle_message(message).await } else { tracing::trace!("RequestReceiver: Stopping since channel closed"); + self.shutdown_token.cancelled().await; break; } }, } } - self.task_client.recv().await; tracing::debug!("RequestReceiver: Exiting"); } } -struct FragmentedMessageReceiver { +pub(crate) struct FragmentedMessageReceiver { received_buffer: ReceivedMessagesBuffer, mixnet_packet_receiver: MixnetMessageReceiver, - task_client: TaskClient, + shutdown_token: ShutdownToken, } impl FragmentedMessageReceiver { fn new( received_buffer: ReceivedMessagesBuffer, mixnet_packet_receiver: MixnetMessageReceiver, - task_client: TaskClient, + shutdown_token: ShutdownToken, ) -> Self { FragmentedMessageReceiver { received_buffer, mixnet_packet_receiver, - task_client, + shutdown_token, } } - async fn run(&mut self) -> Result<(), MessageRecoveryError> { + pub(crate) async fn run(&mut self) -> Result<(), MessageRecoveryError> { debug!("Started FragmentedMessageReceiver with graceful shutdown support"); - while !self.task_client.is_shutdown() { + loop { tokio::select! { + biased; + _ = self.shutdown_token.cancelled() => { + tracing::trace!("FragmentedMessageReceiver: Received shutdown"); + break; + } new_messages = self.mixnet_packet_receiver.next() => { if let Some(new_messages) = new_messages { self.received_buffer.handle_new_received(new_messages).await?; } else { tracing::trace!("FragmentedMessageReceiver: Stopping since channel closed"); + self.shutdown_token.cancelled().await; break; } }, - _ = self.task_client.recv_with_delay() => { - tracing::trace!("FragmentedMessageReceiver: Received shutdown"); - } + } } - self.task_client.recv_timeout().await; tracing::debug!("FragmentedMessageReceiver: Exiting"); Ok(()) } @@ -573,48 +577,31 @@ impl ReceivedMessagesBufferControll reply_key_storage: SentReplyKeys, reply_controller_sender: ReplyControllerSender, metrics_reporter: ClientStatsSender, - task_client: TaskClient, + shutdown_token: ShutdownToken, ) -> Self { let received_buffer = ReceivedMessagesBuffer::new( local_encryption_keypair, reply_key_storage, reply_controller_sender, metrics_reporter, - task_client.fork("received_messages_buffer"), + shutdown_token.clone(), ); ReceivedMessagesBufferController { fragmented_message_receiver: FragmentedMessageReceiver::new( received_buffer.clone(), mixnet_packet_receiver, - task_client.fork("fragmented_message_receiver"), + shutdown_token.clone(), ), request_receiver: RequestReceiver::new( received_buffer, query_receiver, - task_client.with_suffix("request_receiver"), + shutdown_token.clone(), ), } } - pub fn start(self) { - let mut fragmented_message_receiver = self.fragmented_message_receiver; - let mut request_receiver = self.request_receiver; - - spawn_future!( - async move { - match fragmented_message_receiver.run().await { - Ok(_) => {} - Err(e) => error!("{e}"), - } - }, - "ReceivedMessagesBufferController::FragmentedMessageReceiver" - ); - spawn_future!( - async move { - request_receiver.run().await; - }, - "ReceivedMessagesBufferController::RequestReceiver" - ); + pub(crate) fn into_tasks(self) -> (FragmentedMessageReceiver, RequestReceiver) { + (self.fragmented_message_receiver, self.request_receiver) } } diff --git a/common/client-core/src/client/replies/reply_controller/mod.rs b/common/client-core/src/client/replies/reply_controller/mod.rs index 60004c2011..63ead1c0be 100644 --- a/common/client-core/src/client/replies/reply_controller/mod.rs +++ b/common/client-core/src/client/replies/reply_controller/mod.rs @@ -7,7 +7,7 @@ use crate::client::replies::reply_controller::key_rotation_helpers::KeyRotationC use crate::client::replies::reply_storage::CombinedReplyStorage; use crate::config; use futures::StreamExt; -use nym_task::TaskClient; +use nym_task::ShutdownToken; use rand::rngs::OsRng; use rand::{CryptoRng, Rng}; use std::time::Duration; @@ -60,9 +60,6 @@ pub struct ReplyController { receiver_controller: ReceiverReplyController, request_receiver: ReplyControllerReceiver, - - // Listen for shutdown signals - task_client: TaskClient, } impl ReplyController { @@ -71,7 +68,6 @@ impl ReplyController { message_handler: MessageHandler, full_reply_storage: CombinedReplyStorage, request_receiver: ReplyControllerReceiver, - task_client: TaskClient, ) -> Self { ReplyController { config, @@ -86,7 +82,6 @@ impl ReplyController { message_handler, ), request_receiver, - task_client, } } } @@ -148,22 +143,21 @@ where self.sender_controller.inspect_and_clear_stale_data(now) } - pub(crate) async fn run(&mut self) { + pub(crate) async fn run(&mut self, shutdown_token: ShutdownToken) { debug!("Started ReplyController with graceful shutdown support"); - let mut shutdown = self.task_client.fork("reply-controller"); - let polling_rate = Duration::from_secs(5); let mut stale_inspection = new_interval_stream(polling_rate); let polling_rate = self.config.key_rotation.epoch_duration / 8; let mut invalidation_inspection = new_interval_stream(polling_rate); - while !shutdown.is_shutdown() { + loop { tokio::select! { biased; - _ = shutdown.recv() => { + _ = shutdown_token.cancelled() => { tracing::trace!("ReplyController: Received shutdown"); + break; }, req = self.request_receiver.next() => match req { Some(req) => self.handle_request(req).await, @@ -181,7 +175,6 @@ where } } } - assert!(shutdown.is_shutdown_poll()); tracing::debug!("ReplyController: Exiting"); } } diff --git a/common/client-core/src/client/statistics_control.rs b/common/client-core/src/client/statistics_control.rs index 6d315f4eb2..dcfbd2e19c 100644 --- a/common/client-core/src/client/statistics_control.rs +++ b/common/client-core/src/client/statistics_control.rs @@ -16,21 +16,17 @@ #![warn(clippy::todo)] #![warn(clippy::dbg_macro)] +use crate::client::inbound_messages::{InputMessage, InputMessageSender}; use futures::StreamExt; use nym_client_core_config_types::StatsReporting; use nym_sphinx::addressing::Recipient; use nym_statistics_common::clients::{ ClientStatsController, ClientStatsReceiver, ClientStatsSender, }; -use nym_task::{connections::TransmissionLane, TaskClient}; +use nym_task::{connections::TransmissionLane, ShutdownToken, ShutdownTracker}; use std::time::Duration; -use crate::{ - client::inbound_messages::{InputMessage, InputMessageSender}, - spawn_future, -}; - -/// Time interval between reporting statistics locally (logging/task_client) +/// Time interval between reporting statistics locally (logging/shutdown_token) const LOCAL_REPORT_INTERVAL: Duration = Duration::from_secs(2); /// Interval for taking snapshots of the statistics const SNAPSHOT_INTERVAL: Duration = Duration::from_millis(500); @@ -51,9 +47,6 @@ pub(crate) struct StatisticsControl { /// Config for stats reporting (enabled, address, interval) reporting_config: StatsReporting, - - /// Task client for listening for shutdown - task_client: TaskClient, } impl StatisticsControl { @@ -62,24 +55,20 @@ impl StatisticsControl { client_type: String, client_stats_id: String, report_tx: InputMessageSender, - task_client: TaskClient, + shutdown_token: ShutdownToken, ) -> (Self, ClientStatsSender) { let (stats_tx, stats_rx) = tokio::sync::mpsc::unbounded_channel(); let stats = ClientStatsController::new(client_stats_id, client_type); - let mut task_client_stats_sender = task_client.fork("stats_sender"); - task_client_stats_sender.disarm(); - ( StatisticsControl { stats, stats_rx, report_tx, reporting_config, - task_client, }, - ClientStatsSender::new(Some(stats_tx), task_client_stats_sender), + ClientStatsSender::new(Some(stats_tx), shutdown_token), ) } @@ -99,7 +88,8 @@ impl StatisticsControl { } } - async fn run(&mut self) { + // manually control the shutdown mechanism as we don't want to get interrupted mid-snapshot + pub async fn run(&mut self, shutdown_token: ShutdownToken) { tracing::debug!("Started StatisticsControl with graceful shutdown support"); #[cfg(not(target_arch = "wasm32"))] @@ -129,10 +119,10 @@ impl StatisticsControl { let mut snapshot_interval = gloo_timers::future::IntervalStream::new(SNAPSHOT_INTERVAL.as_millis() as u32); - while !self.task_client.is_shutdown() { + loop { tokio::select! { biased; - _ = self.task_client.recv() => { + _ = shutdown_token.cancelled() => { tracing::trace!("StatisticsControl: Received shutdown"); break; }, @@ -157,37 +147,34 @@ impl StatisticsControl { } _ = local_report_interval.next() => { - self.stats.local_report(&mut self.task_client); + self.stats.local_report(); } } } tracing::debug!("StatisticsControl: Exiting"); } - pub(crate) fn start(mut self) { - spawn_future!( - async move { - self.run().await; - }, - "StatisticsControl" - ) - } - pub(crate) fn create_and_start( reporting_config: StatsReporting, client_type: String, client_stats_id: String, report_tx: InputMessageSender, - task_client: TaskClient, + shutdown_tracker: &ShutdownTracker, ) -> ClientStatsSender { - let (controller, sender) = Self::create( + let (mut controller, sender) = Self::create( reporting_config, client_type, client_stats_id, report_tx, - task_client, + shutdown_tracker.child_shutdown_token(), + ); + let shutdown_token = shutdown_tracker.clone_shutdown_token(); + shutdown_tracker.try_spawn_named( + async move { + controller.run(shutdown_token).await; + }, + "StatisticsControl", ); - controller.start(); sender } } diff --git a/common/client-core/src/client/topology_control/mod.rs b/common/client-core/src/client/topology_control/mod.rs index b3c4d61f3f..c083aa7378 100644 --- a/common/client-core/src/client/topology_control/mod.rs +++ b/common/client-core/src/client/topology_control/mod.rs @@ -1,11 +1,9 @@ // Copyright 2021-2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::spawn_future; pub(crate) use accessor::{TopologyAccessor, TopologyReadPermit}; use futures::StreamExt; use nym_sphinx::addressing::nodes::NodeIdentity; -use nym_task::TaskClient; use nym_topology::NymTopologyError; use std::time::Duration; use tracing::*; @@ -41,8 +39,6 @@ pub struct TopologyRefresher { refresh_rate: Duration, consecutive_failure_count: usize, - - task_client: TaskClient, } impl TopologyRefresher { @@ -50,14 +46,12 @@ impl TopologyRefresher { cfg: TopologyRefresherConfig, topology_accessor: TopologyAccessor, topology_provider: Box, - task_client: TaskClient, ) -> Self { TopologyRefresher { topology_provider, topology_accessor, refresh_rate: cfg.refresh_rate, consecutive_failure_count: 0, - task_client, } } @@ -144,40 +138,30 @@ impl TopologyRefresher { } } - pub fn start(mut self) { - spawn_future!( - async move { - debug!("Started TopologyRefresher with graceful shutdown support"); + // it's perfectly fine if task is interrupted mid-refresh + // there's no data to persist or send over + pub async fn run(&mut self) { + debug!("Started TopologyRefresher with graceful shutdown support"); - #[cfg(not(target_arch = "wasm32"))] - let mut interval = tokio_stream::wrappers::IntervalStream::new( - tokio::time::interval(self.refresh_rate), - ); + #[cfg(not(target_arch = "wasm32"))] + let mut interval = + tokio_stream::wrappers::IntervalStream::new(tokio::time::interval(self.refresh_rate)); - #[cfg(target_arch = "wasm32")] - let mut interval = - gloo_timers::future::IntervalStream::new(self.refresh_rate.as_millis() as u32); + #[cfg(target_arch = "wasm32")] + let mut interval = + gloo_timers::future::IntervalStream::new(self.refresh_rate.as_millis() as u32); - // We already have an initial topology, so no need to refresh it immediately. - // My understanding is that js setInterval does not fire immediately, so it's not - // needed there. - #[cfg(not(target_arch = "wasm32"))] - interval.next().await; + // We already have an initial topology, so no need to refresh it immediately. + // My understanding is that js setInterval does not fire immediately, so it's not + // needed there. + #[cfg(not(target_arch = "wasm32"))] + interval.next().await; - while !self.task_client.is_shutdown() { - tokio::select! { - _ = interval.next() => { - self.try_refresh().await; - }, - _ = self.task_client.recv() => { - tracing::trace!("TopologyRefresher: Received shutdown"); - }, - } - } - self.task_client.recv_timeout().await; - tracing::debug!("TopologyRefresher: Exiting"); - }, - "TopologyRefresher" - ) + while interval.next().await.is_some() { + self.try_refresh().await; + } + + // this should never get triggered + error!("topology refresher interval has been exhausted!") } } 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 339cd12653..a81118665c 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 @@ -2,8 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 use async_trait::async_trait; +use nym_mixnet_contract_common::EpochRewardedSet; use nym_topology::provider_trait::{ToTopologyMetadata, TopologyProvider}; use nym_topology::NymTopology; +use nym_validator_client::nym_api::NymApiClientExt; use rand::prelude::SliceRandom; use rand::thread_rng; use std::cmp::min; @@ -39,30 +41,43 @@ impl Config { pub struct NymApiTopologyProvider { config: Config, - validator_client: nym_validator_client::client::NymApiClient, + validator_client: nym_http_api_client::Client, nym_api_urls: Vec, currently_used_api: usize, + use_bincode: bool, } impl NymApiTopologyProvider { pub fn new( config: impl Into, mut nym_api_urls: Vec, - mut validator_client: nym_validator_client::client::NymApiClient, + validator_client: nym_http_api_client::Client, ) -> Self { nym_api_urls.shuffle(&mut thread_rng()); - validator_client.change_nym_api(nym_api_urls[0].clone()); - - NymApiTopologyProvider { + let mut provider = NymApiTopologyProvider { config: config.into(), validator_client, nym_api_urls, currently_used_api: 0, - } + use_bincode: true, + }; + // Set all API URLs - the client will try them in order with automatic failover + provider.validator_client.change_base_urls( + provider + .nym_api_urls + .iter() + .map(|u| u.clone().into()) + .collect(), + ); + provider } pub fn disable_bincode(&mut self) { - self.validator_client.use_bincode = false; + self.use_bincode = false; + // Note: The unified client doesn't support toggling bincode after creation. + // This would require recreating the client without bincode. + // For now, we'll track the preference but it won't take effect. + warn!("Disabling bincode on existing client is not currently supported"); } fn use_next_nym_api(&mut self) { @@ -72,8 +87,19 @@ impl NymApiTopologyProvider { } self.currently_used_api = (self.currently_used_api + 1) % self.nym_api_urls.len(); - self.validator_client - .change_nym_api(self.nym_api_urls[self.currently_used_api].clone()) + + // Provide all URLs starting from the next one in rotation order + // This enables automatic failover to other endpoints + let rotated_urls: Vec<_> = self + .nym_api_urls + .iter() + .cycle() + .skip(self.currently_used_api) + .take(self.nym_api_urls.len()) + .map(|u| u.clone().into()) + .collect(); + + self.validator_client.change_base_urls(rotated_urls) } async fn get_current_compatible_topology(&mut self) -> Option { @@ -99,8 +125,13 @@ impl NymApiTopologyProvider { .filter(|n| n.performance.round_to_integer() >= self.config.min_node_performance()) .collect::>(); - NymTopology::new(metadata.to_topology_metadata(), rewarded_set, Vec::new()) - .with_skimmed_nodes(&nodes_filtered) + let epoch_rewarded_set: EpochRewardedSet = rewarded_set.into(); + NymTopology::new( + metadata.to_topology_metadata(), + epoch_rewarded_set, + Vec::new(), + ) + .with_skimmed_nodes(&nodes_filtered) } else { // if we're not using extended topology, we're only getting active set mixnodes and gateways @@ -148,8 +179,13 @@ impl NymApiTopologyProvider { } } - NymTopology::new(metadata.to_topology_metadata(), rewarded_set, Vec::new()) - .with_skimmed_nodes(&nodes) + let epoch_rewarded_set: EpochRewardedSet = rewarded_set.into(); + NymTopology::new( + metadata.to_topology_metadata(), + epoch_rewarded_set, + Vec::new(), + ) + .with_skimmed_nodes(&nodes) }; if !topology.is_minimally_routable() { diff --git a/common/client-core/src/error.rs b/common/client-core/src/error.rs index 1c26e96dfb..c76b8a3c88 100644 --- a/common/client-core/src/error.rs +++ b/common/client-core/src/error.rs @@ -56,10 +56,7 @@ pub enum ClientCoreError { ListOfNymApisIsEmpty, #[error("failed to resolve a query to nym API: {source}")] - NymApiQueryFailure { - #[from] - source: NymAPIError, - }, + NymApiQueryFailure { source: Box }, #[error( "the current network topology seem to be insufficient to route any packets through:\n\t{0}" @@ -255,6 +252,14 @@ impl From for ClientCoreError { } } +impl From for ClientCoreError { + fn from(err: NymAPIError) -> ClientCoreError { + ClientCoreError::NymApiQueryFailure { + source: Box::new(err), + } + } +} + /// Set of messages that the client can send to listeners via the task manager #[derive(Debug)] pub enum ClientCoreStatusMessage { diff --git a/common/client-core/src/init/helpers.rs b/common/client-core/src/init/helpers.rs index 1ca4b68191..f4e34f5ffd 100644 --- a/common/client-core/src/init/helpers.rs +++ b/common/client-core/src/init/helpers.rs @@ -7,7 +7,8 @@ use futures::{SinkExt, StreamExt}; use nym_crypto::asymmetric::ed25519; use nym_gateway_client::GatewayClient; use nym_topology::node::RoutingNode; -use nym_validator_client::client::IdentityKeyRef; +use nym_validator_client::client::{IdentityKeyRef, NymApiClientExt}; +use nym_validator_client::nym_nodes::SkimmedNodesWithMetadata; use nym_validator_client::UserAgent; use rand::{seq::SliceRandom, Rng}; #[cfg(unix)] @@ -83,6 +84,48 @@ struct GatewayWithLatency<'a, G: ConnectableGateway> { latency: Duration, } +// Helper to collect all pages of entry nodes - replicates NymApiClient's convenience method +async fn get_all_basic_entry_nodes_with_metadata( + client: &nym_http_api_client::Client, + use_bincode: bool, +) -> Result { + // Get first page to obtain metadata + let mut page = 0; + let res = client + .get_basic_entry_assigned_nodes_v2(false, Some(page), None, use_bincode) + .await?; + let mut nodes = res.nodes.data; + let metadata = res.metadata; + + if res.nodes.pagination.total == nodes.len() { + return Ok(SkimmedNodesWithMetadata::new(nodes, metadata)); + } + + page += 1; + + // Collect remaining pages + loop { + let mut res = client + .get_basic_entry_assigned_nodes_v2(false, Some(page), None, use_bincode) + .await?; + + if !metadata.consistency_check(&res.metadata) { + return Err(ClientCoreError::ValidatorClientError( + nym_validator_client::ValidatorClientError::InconsistentPagedMetadata, + )); + } + + nodes.append(&mut res.nodes.data); + if nodes.len() < res.nodes.pagination.total { + page += 1 + } else { + break; + } + } + + Ok(SkimmedNodesWithMetadata::new(nodes, metadata)) +} + impl<'a, G: ConnectableGateway> GatewayWithLatency<'a, G> { fn new(gateway: &'a G, latency: Duration) -> Self { GatewayWithLatency { gateway, latency } @@ -99,16 +142,28 @@ pub async fn gateways_for_init( let nym_api = nym_apis .choose(rng) .ok_or(ClientCoreError::ListOfNymApisIsEmpty)?; - let client = if let Some(user_agent) = user_agent { - nym_validator_client::client::NymApiClient::new_with_user_agent(nym_api.clone(), user_agent) - } else { - nym_validator_client::client::NymApiClient::new(nym_api.clone()) - }; + + // Use the unified HTTP client directly with optional user agent + let mut builder = nym_http_api_client::Client::builder(nym_api.clone()) + .map_err(|e| { + ClientCoreError::ValidatorClientError(nym_validator_client::ValidatorClientError::from( + e, + )) + })? + .with_bincode(); // Use bincode for better performance + + if let Some(user_agent) = user_agent { + builder = builder.with_user_agent(user_agent); + } + + let client = builder.build().map_err(|e| { + ClientCoreError::ValidatorClientError(nym_validator_client::ValidatorClientError::from(e)) + })?; tracing::debug!("Fetching list of gateways from: {nym_api}"); - let gateways = client - .get_all_basic_entry_assigned_nodes_with_metadata() + // Use our helper to handle pagination + let gateways = get_all_basic_entry_nodes_with_metadata(&client, true) .await? .nodes; info!("nym api reports {} gateways", gateways.len()); diff --git a/common/client-core/src/lib.rs b/common/client-core/src/lib.rs index 8be8b10133..072bac7e9f 100644 --- a/common/client-core/src/lib.rs +++ b/common/client-core/src/lib.rs @@ -17,7 +17,9 @@ pub use nym_topology::{ HardcodedTopologyProvider, NymRouteProvider, NymTopology, NymTopologyError, TopologyProvider, }; +#[deprecated(note = "use spawn_future from nym_task crate instead")] #[cfg(target_arch = "wasm32")] +#[track_caller] pub fn spawn_future(future: F) where F: Future + 'static, @@ -25,9 +27,7 @@ where wasm_bindgen_futures::spawn_local(future); } -// TODO: expose similar API to the rest of the codebase, -// perhaps with some simple trait for a task to define its name - +#[deprecated(note = "use spawn_future from nym_task crate instead")] #[cfg(not(target_arch = "wasm32"))] #[track_caller] pub fn spawn_future(future: F) @@ -37,35 +37,3 @@ where { tokio::spawn(future); } - -#[cfg(not(target_arch = "wasm32"))] -#[track_caller] -pub fn spawn_named_future(future: F, name: &str) -where - F: Future + Send + 'static, - F::Output: Send + 'static, -{ - cfg_if::cfg_if! {if #[cfg(tokio_unstable)] { - #[allow(clippy::expect_used)] - tokio::task::Builder::new().name(name).spawn(future).expect("failed to spawn future"); - } else { - let _ = name; - tracing::debug!(r#"the underlying binary hasn't been built with `RUSTFLAGS="--cfg tokio_unstable"` - the future naming won't do anything"#); - spawn_future(future); - }} -} - -#[macro_export] -macro_rules! spawn_future { - ($future:expr) => {{ - $crate::spawn_future($future) - }}; - ($future:expr, $name:expr) => {{ - cfg_if::cfg_if! {if #[cfg(not(target_arch = "wasm32"))] { - $crate::spawn_named_future($future, $name) - } else { - let _ = $name; - $crate::spawn_future($future) - }} - }}; -} diff --git a/common/client-core/surb-storage/Cargo.toml b/common/client-core/surb-storage/Cargo.toml index 551bfb74ec..be03487d95 100644 --- a/common/client-core/surb-storage/Cargo.toml +++ b/common/client-core/surb-storage/Cargo.toml @@ -30,6 +30,7 @@ optional = true path = "../../../sqlx-pool-guard" [build-dependencies] +anyhow = { workspace = true } tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } sqlx = { workspace = true, features = [ "runtime-tokio-rustls", diff --git a/common/client-core/surb-storage/build.rs b/common/client-core/surb-storage/build.rs index 62c02c3968..7f77560668 100644 --- a/common/client-core/surb-storage/build.rs +++ b/common/client-core/surb-storage/build.rs @@ -2,23 +2,24 @@ // SPDX-License-Identifier: Apache-2.0 #[tokio::main] -async fn main() { +async fn main() -> anyhow::Result<()> { #[cfg(feature = "fs-surb-storage")] { + use anyhow::Context; use sqlx::{Connection, SqliteConnection}; use std::env; - let out_dir = env::var("OUT_DIR").unwrap(); + let out_dir = env::var("OUT_DIR")?; let database_path = format!("{out_dir}/fs-surbs-example.sqlite"); let mut conn = SqliteConnection::connect(&format!("sqlite://{database_path}?mode=rwc")) .await - .expect("Failed to create SQLx database connection"); + .context("Failed to create SQLx database connection")?; sqlx::migrate!("./fs_surbs_migrations") .run(&mut conn) .await - .expect("Failed to perform SQLx migrations"); + .context("Failed to perform SQLx migrations")?; #[cfg(target_family = "unix")] println!("cargo:rustc-env=DATABASE_URL=sqlite://{}", &database_path); @@ -28,4 +29,6 @@ async fn main() { // not a valid windows path... but hey, it works... println!("cargo:rustc-env=DATABASE_URL=sqlite:///{}", &database_path); } + + Ok(()) } diff --git a/common/client-core/surb-storage/src/lib.rs b/common/client-core/surb-storage/src/lib.rs index b7ac924706..079c213bbc 100644 --- a/common/client-core/surb-storage/src/lib.rs +++ b/common/client-core/surb-storage/src/lib.rs @@ -40,7 +40,7 @@ where pub async fn flush_on_shutdown( mut self, mem_state: CombinedReplyStorage, - mut shutdown: nym_task::TaskClient, + shutdown: nym_task::ShutdownToken, ) { use tracing::{debug, error, info}; @@ -50,7 +50,7 @@ where return; } - shutdown.recv().await; + shutdown.cancelled().await; info!("PersistentReplyStorage is flushing all reply-related data to underlying storage"); if let Err(err) = self.backend.flush_surb_storage(&mem_state).await { diff --git a/common/client-libs/gateway-client/src/client/mod.rs b/common/client-libs/gateway-client/src/client/mod.rs index de90b3ad35..52e5833eb2 100644 --- a/common/client-libs/gateway-client/src/client/mod.rs +++ b/common/client-libs/gateway-client/src/client/mod.rs @@ -12,7 +12,7 @@ use crate::socket_state::{ws_fd, PartiallyDelegatedHandle, SocketState}; use crate::traits::GatewayPacketRouter; use crate::{cleanup_socket_message, try_decrypt_binary_message}; use futures::{SinkExt, StreamExt}; -use nym_bandwidth_controller::{BandwidthController, BandwidthStatusMessage}; +use nym_bandwidth_controller::BandwidthController; use nym_credential_storage::ephemeral_storage::EphemeralStorage as EphemeralCredentialStorage; use nym_credential_storage::storage::Storage as CredentialStorage; use nym_credentials::CredentialSpendingData; @@ -27,7 +27,7 @@ use nym_gateway_requests::{ use nym_sphinx::forwarding::packet::MixPacket; use nym_statistics_common::clients::connection::ConnectionStatsEvent; use nym_statistics_common::clients::ClientStatsSender; -use nym_task::TaskClient; +use nym_task::ShutdownToken; use nym_validator_client::nyxd::contract_traits::DkgQueryClient; use rand::rngs::OsRng; use std::sync::Arc; @@ -109,7 +109,7 @@ pub struct GatewayClient { connection_fd_callback: Option>, /// Listen to shutdown messages and send notifications back to the task manager - task_client: TaskClient, + shutdown_token: ShutdownToken, } impl GatewayClient { @@ -124,7 +124,7 @@ impl GatewayClient { bandwidth_controller: Option>, stats_reporter: ClientStatsSender, #[cfg(unix)] connection_fd_callback: Option>, - task_client: TaskClient, + shutdown_token: ShutdownToken, ) -> Self { GatewayClient { cfg, @@ -141,7 +141,7 @@ impl GatewayClient { negotiated_protocol: None, #[cfg(unix)] connection_fd_callback, - task_client, + shutdown_token, } } @@ -293,7 +293,7 @@ impl GatewayClient { loop { tokio::select! { - _ = self.task_client.recv() => { + _ = self.shutdown_token.cancelled() => { log::trace!("GatewayClient control response: Received shutdown"); log::debug!("GatewayClient control response: Exiting"); break Err(GatewayClientError::ConnectionClosedGatewayShutdown); @@ -514,7 +514,7 @@ impl GatewayClient { self.cfg.bandwidth.require_tickets, derive_aes256_gcm_siv_key, #[cfg(not(target_arch = "wasm32"))] - self.task_client.clone(), + self.shutdown_token.clone(), ) .await .map_err(GatewayClientError::RegistrationFailure), @@ -631,9 +631,6 @@ impl GatewayClient { self.negotiated_protocol = protocol_version; log::debug!("authenticated: {status}, bandwidth remaining: {bandwidth_remaining}"); - self.task_client.send_status_msg(Box::new( - BandwidthStatusMessage::RemainingBandwidth(bandwidth_remaining), - )); Ok(()) } ServerResponse::Error { message } => Err(GatewayClientError::GatewayError(message)), @@ -1069,7 +1066,7 @@ impl GatewayClient { .expect("no shared key present even though we're authenticated!"), ), self.bandwidth.clone(), - self.task_client.clone(), + self.shutdown_token.clone(), ) } _ => unreachable!(), @@ -1143,8 +1140,8 @@ impl GatewayClient { // perfectly fine here, because it's not meant to be used let (ack_tx, _) = mpsc::unbounded(); let (mix_tx, _) = mpsc::unbounded(); - let task_client = TaskClient::dummy(); - let packet_router = PacketRouter::new(ack_tx, mix_tx, task_client.clone()); + let shutdown_token = ShutdownToken::default(); + let packet_router = PacketRouter::new(ack_tx, mix_tx, shutdown_token.clone()); GatewayClient { cfg: GatewayClientConfig::default().with_disabled_credentials_mode(true), @@ -1157,11 +1154,11 @@ impl GatewayClient { connection: SocketState::NotConnected, packet_router, bandwidth_controller: None, - stats_reporter: ClientStatsSender::new(None, task_client.clone()), + stats_reporter: ClientStatsSender::new(None, shutdown_token.clone()), negotiated_protocol: None, #[cfg(unix)] connection_fd_callback, - task_client, + shutdown_token, } } @@ -1170,7 +1167,7 @@ impl GatewayClient { packet_router: PacketRouter, bandwidth_controller: Option>, stats_reporter: ClientStatsSender, - task_client: TaskClient, + shutdown_token: ShutdownToken, ) -> GatewayClient { // invariants that can't be broken // (unless somebody decided to expose some field that wasn't meant to be exposed) @@ -1193,7 +1190,7 @@ impl GatewayClient { negotiated_protocol: self.negotiated_protocol, #[cfg(unix)] connection_fd_callback: self.connection_fd_callback, - task_client, + shutdown_token, } } } diff --git a/common/client-libs/gateway-client/src/packet_router.rs b/common/client-libs/gateway-client/src/packet_router.rs index 36168f4ab8..7fb863947f 100644 --- a/common/client-libs/gateway-client/src/packet_router.rs +++ b/common/client-libs/gateway-client/src/packet_router.rs @@ -7,7 +7,7 @@ use crate::error::GatewayClientError; use crate::GatewayPacketRouter; use futures::channel::mpsc; -use nym_task::TaskClient; +use nym_task::ShutdownToken; pub type MixnetMessageSender = mpsc::UnboundedSender>>; pub type MixnetMessageReceiver = mpsc::UnboundedReceiver>>; @@ -19,14 +19,14 @@ pub type AcknowledgementReceiver = mpsc::UnboundedReceiver>>; pub struct PacketRouter { ack_sender: AcknowledgementSender, mixnet_message_sender: MixnetMessageSender, - shutdown: TaskClient, + shutdown: ShutdownToken, } impl PacketRouter { pub fn new( ack_sender: AcknowledgementSender, mixnet_message_sender: MixnetMessageSender, - shutdown: TaskClient, + shutdown: ShutdownToken, ) -> Self { PacketRouter { ack_sender, @@ -42,7 +42,7 @@ impl PacketRouter { if let Err(err) = self.mixnet_message_sender.unbounded_send(received_messages) { // check if the failure is due to the shutdown being in progress and thus the receiver channel // having already been dropped - if self.shutdown.is_shutdown_poll() || self.shutdown.is_dummy() { + if self.shutdown.is_cancelled() { // This should ideally not happen, but it's ok tracing::warn!("Failed to send mixnet messages due to receiver task shutdown"); return Err(GatewayClientError::ShutdownInProgress); @@ -58,7 +58,7 @@ impl PacketRouter { if let Err(err) = self.ack_sender.unbounded_send(received_acks) { // check if the failure is due to the shutdown being in progress and thus the receiver channel // having already been dropped - if self.shutdown.is_shutdown_poll() || self.shutdown.is_dummy() { + if self.shutdown.is_cancelled() { // This should ideally not happen, but it's ok tracing::warn!("Failed to send acks due to receiver task shutdown"); return Err(GatewayClientError::ShutdownInProgress); @@ -69,10 +69,6 @@ impl PacketRouter { } Ok(()) } - - pub fn disarm(&mut self) { - self.shutdown.disarm(); - } } impl GatewayPacketRouter for PacketRouter { diff --git a/common/client-libs/gateway-client/src/socket_state.rs b/common/client-libs/gateway-client/src/socket_state.rs index 5489ec3628..4f3009e389 100644 --- a/common/client-libs/gateway-client/src/socket_state.rs +++ b/common/client-libs/gateway-client/src/socket_state.rs @@ -11,7 +11,7 @@ use futures::stream::{SplitSink, SplitStream}; use futures::{SinkExt, StreamExt}; use nym_gateway_requests::shared_key::SharedGatewayKey; use nym_gateway_requests::{SensitiveServerResponse, ServerResponse, SimpleGatewayRequestsError}; -use nym_task::TaskClient; +use nym_task::ShutdownToken; use si_scale::helpers::bibytes2; use std::os::raw::c_int as RawFd; use std::sync::Arc; @@ -87,13 +87,13 @@ impl PartiallyDelegatedRouter { } } - async fn run(mut self, mut split_stream: SplitStream, mut task_client: TaskClient) { + async fn run(mut self, mut split_stream: SplitStream, shutdown_token: ShutdownToken) { let mut chunked_stream = (&mut split_stream).ready_chunks(8); let ret: Result<_, GatewayClientError> = loop { tokio::select! { biased; // received system-wide shutdown - _ = task_client.recv() => { + _ = shutdown_token.cancelled() => { log::trace!("GatewayClient listener: Received shutdown"); log::debug!("GatewayClient listener: Exiting"); return; @@ -118,11 +118,7 @@ impl PartiallyDelegatedRouter { let return_res = match ret { Err(err) => self.stream_return.send(Err(err)), - Ok(_) => { - self.packet_router.disarm(); - task_client.disarm(); - self.stream_return.send(Ok(split_stream)) - } + Ok(_) => self.stream_return.send(Ok(split_stream)), }; if return_res.is_err() { @@ -266,8 +262,8 @@ impl PartiallyDelegatedRouter { Ok(plaintexts) } - fn spawn(self, split_stream: SplitStream, task_client: TaskClient) { - let fut = async move { self.run(split_stream, task_client).await }; + fn spawn(self, split_stream: SplitStream, shutdown_token: ShutdownToken) { + let fut = async move { self.run(split_stream, shutdown_token).await }; #[cfg(target_arch = "wasm32")] wasm_bindgen_futures::spawn_local(fut); @@ -283,7 +279,7 @@ impl PartiallyDelegatedHandle { packet_router: PacketRouter, shared_key: Arc, client_bandwidth: ClientBandwidth, - shutdown: TaskClient, + shutdown: ShutdownToken, ) -> Self { // when called for, it NEEDS TO yield back the stream so that we could merge it and // read control request responses. diff --git a/common/client-libs/validator-client/src/client.rs b/common/client-libs/validator-client/src/client.rs index 2e6d19d9ea..2758369ede 100644 --- a/common/client-libs/validator-client/src/client.rs +++ b/common/client-libs/validator-client/src/client.rs @@ -5,8 +5,8 @@ use crate::nyxd::{self, NyxdClient}; use crate::signing::direct_wallet::DirectSecp256k1HdWallet; use crate::signing::signer::{NoSigner, OfflineSigner}; use crate::{ - nym_api, DirectSigningReqwestRpcValidatorClient, QueryReqwestRpcValidatorClient, - ReqwestRpcClient, ValidatorClientError, + DirectSigningReqwestRpcValidatorClient, QueryReqwestRpcValidatorClient, ReqwestRpcClient, + ValidatorClientError, }; use nym_api_requests::ecash::models::{ AggregatedCoinIndicesSignatureResponse, AggregatedExpirationDateSignatureResponse, @@ -20,11 +20,9 @@ use nym_api_requests::ecash::{ PartialExpirationDateSignatureResponse, VerificationKeyResponse, }; use nym_api_requests::models::{ - ApiHealthResponse, GatewayBondAnnotated, GatewayCoreStatusResponse, - HistoricalPerformanceResponse, MixnodeCoreStatusResponse, MixnodeStatusResponse, - NymNodeDescription, RewardEstimationResponse, StakeSaturationResponse, + ApiHealthResponse, GatewayCoreStatusResponse, HistoricalPerformanceResponse, + MixnodeCoreStatusResponse, NymNodeDescription, }; -use nym_api_requests::models::{LegacyDescribedGateway, MixNodeBondAnnotated}; use nym_api_requests::nym_nodes::{ NodesByAddressesResponse, SemiSkimmedNodesWithMetadata, SkimmedNode, SkimmedNodesWithMetadata, }; @@ -153,7 +151,7 @@ impl Config { pub struct Client { // ideally they would have been read-only, but unfortunately rust doesn't have such features // #[deprecated(note = "please use `nym_api_client` instead")] - pub nym_api: nym_api::Client, + pub nym_api: nym_http_api_client::Client, // pub nym_api_client: NymApiClient, pub nyxd: NyxdClient, } @@ -214,7 +212,7 @@ impl Client { impl Client { pub fn new_with_rpc_client(config: Config, rpc_client: C) -> Self { - let nym_api_client = nym_api::Client::new(config.api_url.clone(), None); + let nym_api_client = nym_http_api_client::Client::new(config.api_url.clone(), None); Client { nym_api: nym_api_client, @@ -228,7 +226,7 @@ impl Client { where S: OfflineSigner, { - let nym_api_client = nym_api::Client::new(config.api_url.clone(), None); + let nym_api_client = nym_http_api_client::Client::new(config.api_url.clone(), None); Client { nym_api: nym_api_client, @@ -249,65 +247,6 @@ impl Client { self.nym_api.change_base_urls(vec![new_endpoint.into()]) } - #[deprecated] - pub async fn get_cached_mixnodes(&self) -> Result, ValidatorClientError> { - Ok(self.nym_api.get_mixnodes().await?) - } - - #[deprecated] - pub async fn get_cached_mixnodes_detailed( - &self, - ) -> Result, ValidatorClientError> { - Ok(self.nym_api.get_mixnodes_detailed().await?) - } - - #[deprecated] - pub async fn get_cached_mixnodes_detailed_unfiltered( - &self, - ) -> Result, ValidatorClientError> { - Ok(self.nym_api.get_mixnodes_detailed_unfiltered().await?) - } - - #[deprecated] - pub async fn get_cached_rewarded_mixnodes( - &self, - ) -> Result, ValidatorClientError> { - Ok(self.nym_api.get_rewarded_mixnodes().await?) - } - - #[deprecated] - pub async fn get_cached_rewarded_mixnodes_detailed( - &self, - ) -> Result, ValidatorClientError> { - Ok(self.nym_api.get_rewarded_mixnodes_detailed().await?) - } - - #[deprecated] - pub async fn get_cached_active_mixnodes( - &self, - ) -> Result, ValidatorClientError> { - Ok(self.nym_api.get_active_mixnodes().await?) - } - - #[deprecated] - pub async fn get_cached_active_mixnodes_detailed( - &self, - ) -> Result, ValidatorClientError> { - Ok(self.nym_api.get_active_mixnodes_detailed().await?) - } - - #[deprecated] - pub async fn get_cached_gateways(&self) -> Result, ValidatorClientError> { - 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?) - } - pub async fn get_full_node_performance_history( &self, node_id: NodeId, @@ -385,38 +324,25 @@ impl Client { } } +/// DEPRECATED: Use nym_http_api_client::Client with from_network() or with_bincode() instead +#[deprecated( + since = "1.2.0", + note = "Use nym_http_api_client::Client::from_network() or ClientBuilder::with_bincode() instead" +)] #[derive(Clone)] pub struct NymApiClient { pub use_bincode: bool, - pub nym_api: nym_api::Client, + pub nym_api: nym_http_api_client::Client, // TODO: perhaps if we really need it at some (currently I don't see any reasons for it) // we could re-implement the communication with the REST API on port 1317 } -impl From for NymApiClient { - fn from(nym_api: nym_api::Client) -> Self { - NymApiClient { - use_bincode: false, - nym_api, - } - } -} - // we have to allow the use of deprecated method here as they're calling the deprecated trait methods #[allow(deprecated)] impl NymApiClient { - pub fn new(api_url: Url) -> Self { - let nym_api = nym_api::Client::new(api_url, None); - - NymApiClient { - use_bincode: true, - nym_api, - } - } - #[cfg(not(target_arch = "wasm32"))] pub fn new_with_timeout(api_url: Url, timeout: std::time::Duration) -> Self { - let nym_api = nym_api::Client::new(api_url, Some(timeout)); + let nym_api = nym_http_api_client::Client::new(api_url, Some(timeout)); NymApiClient { use_bincode: true, @@ -431,10 +357,10 @@ impl NymApiClient { } pub fn new_with_user_agent(api_url: Url, user_agent: impl Into) -> Self { - let nym_api = nym_api::Client::builder::<_, ValidatorClientError>(api_url) + let nym_api = nym_http_api_client::Client::builder(api_url) .expect("invalid api url") .with_user_agent(user_agent.into()) - .build::() + .build() .expect("failed to build nym api client"); NymApiClient { @@ -571,37 +497,6 @@ impl NymApiClient { Ok(self.nym_api.health().await?) } - #[deprecated] - pub async fn get_cached_active_mixnodes( - &self, - ) -> Result, ValidatorClientError> { - Ok(self.nym_api.get_active_mixnodes().await?) - } - - #[deprecated] - pub async fn get_cached_rewarded_mixnodes( - &self, - ) -> Result, ValidatorClientError> { - Ok(self.nym_api.get_rewarded_mixnodes().await?) - } - - #[deprecated] - pub async fn get_cached_mixnodes(&self) -> Result, ValidatorClientError> { - Ok(self.nym_api.get_mixnodes().await?) - } - - #[deprecated] - pub async fn get_cached_gateways(&self) -> Result, ValidatorClientError> { - Ok(self.nym_api.get_gateways().await?) - } - - #[deprecated] - pub async fn get_cached_described_gateways( - &self, - ) -> Result, ValidatorClientError> { - Ok(self.nym_api.get_gateways_described().await?) - } - pub async fn get_all_described_nodes( &self, ) -> Result, ValidatorClientError> { @@ -668,30 +563,6 @@ impl NymApiClient { .await?) } - #[deprecated] - pub async fn get_mixnode_status( - &self, - mix_id: NodeId, - ) -> Result { - Ok(self.nym_api.get_mixnode_status(mix_id).await?) - } - - #[deprecated] - pub async fn get_mixnode_reward_estimation( - &self, - mix_id: NodeId, - ) -> Result { - Ok(self.nym_api.get_mixnode_reward_estimation(mix_id).await?) - } - - #[deprecated] - pub async fn get_mixnode_stake_saturation( - &self, - mix_id: NodeId, - ) -> Result { - Ok(self.nym_api.get_mixnode_stake_saturation(mix_id).await?) - } - pub async fn blind_sign( &self, request_body: &BlindSignRequestBody, diff --git a/common/client-libs/validator-client/src/coconut/mod.rs b/common/client-libs/validator-client/src/coconut/mod.rs index 67974c0170..35748dd1de 100644 --- a/common/client-libs/validator-client/src/coconut/mod.rs +++ b/common/client-libs/validator-client/src/coconut/mod.rs @@ -3,7 +3,6 @@ use crate::nyxd::contract_traits::{DkgQueryClient, PagedDkgQueryClient}; use crate::nyxd::error::NyxdError; -use crate::NymApiClient; use nym_coconut_dkg_common::types::{EpochId, NodeIndex}; use nym_coconut_dkg_common::verification_key::ContractVKShare; use nym_compact_ecash::error::CompactEcashError; @@ -15,7 +14,7 @@ use url::Url; // TODO: it really doesn't feel like this should live in this crate. #[derive(Clone)] pub struct EcashApiClient { - pub api_client: NymApiClient, + pub api_client: nym_http_api_client::Client, pub verification_key: VerificationKeyAuth, pub node_id: NodeIndex, pub cosmos_address: cosmrs::AccountId, @@ -25,10 +24,15 @@ impl Display for EcashApiClient { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!( f, - "[id: {}] {} @ {}", + "[id: {}] {} @ ({})", self.node_id, self.cosmos_address, - self.api_client.api_url() + self.api_client + .base_urls() + .iter() + .map(|url| url.to_string()) + .collect::>() + .join(", ") ) } } @@ -60,6 +64,9 @@ pub enum EcashApiError { source: CompactEcashError, }, + #[error("failed to create API client: {0}")] + ClientError(String), + #[error("the provided account address is malformed: {source}")] MalformedAccountAddress { #[from] @@ -89,8 +96,13 @@ impl TryFrom for EcashApiClient { // In non-client applications this resolver can cause warning logs about H2 connection // failure. This indicates that the long lived https connection was closed by the remote // peer and the resolver will have to reconnect. It should not impact actual functionality + let api_client = nym_http_api_client::Client::builder(url_address) + .map_err(|e| EcashApiError::ClientError(e.to_string()))? + .build() + .map_err(|e| EcashApiError::ClientError(e.to_string()))?; + Ok(EcashApiClient { - api_client: NymApiClient::new(url_address), + api_client, verification_key: VerificationKeyAuth::try_from_bs58(&share.share)?, node_id: share.node_index, cosmos_address: share.owner.as_str().parse()?, diff --git a/common/client-libs/validator-client/src/connection_tester.rs b/common/client-libs/validator-client/src/connection_tester.rs index 3e71a3c000..3cd9e36729 100644 --- a/common/client-libs/validator-client/src/connection_tester.rs +++ b/common/client-libs/validator-client/src/connection_tester.rs @@ -1,7 +1,8 @@ +use crate::nym_api::NymApiClientExt; use crate::nyxd::contract_traits::MixnetQueryClient; use crate::nyxd::error::NyxdError; use crate::nyxd::Config as ClientConfig; -use crate::{NymApiClient, QueryHttpRpcNyxdClient, ValidatorClientError}; +use crate::{QueryHttpRpcNyxdClient, ValidatorClientError}; use colored::Colorize; use core::fmt; use itertools::Itertools; @@ -87,8 +88,17 @@ fn setup_connection_tests( } }); - let api_connection_test_clients = api_urls.map(|(network, url)| { - ClientForConnectionTest::Api(network, url.clone(), NymApiClient::new(url)) + let api_connection_test_clients = api_urls.filter_map(|(network, url)| { + match nym_http_api_client::Client::builder(url.clone()).and_then(|b| b.build()) { + Ok(client) => Some(ClientForConnectionTest::Api(network, url, client)), + Err(err) => { + eprintln!( + "Failed to create API client for {}: {err}", + network.network_name + ); + None + } + } }); nyxd_connection_test_clients.chain(api_connection_test_clients) @@ -160,7 +170,7 @@ async fn test_nyxd_connection( async fn test_nym_api_connection( network: NymNetworkDetails, url: &Url, - client: &NymApiClient, + client: &nym_http_api_client::Client, ) -> ConnectionResult { let result = match timeout( Duration::from_secs(CONNECTION_TEST_TIMEOUT_SEC), @@ -186,7 +196,7 @@ async fn test_nym_api_connection( enum ClientForConnectionTest { Nyxd(NymNetworkDetails, Url, Box), - Api(NymNetworkDetails, Url, NymApiClient), + Api(NymNetworkDetails, Url, nym_http_api_client::Client), } impl ClientForConnectionTest { diff --git a/common/client-libs/validator-client/src/error.rs b/common/client-libs/validator-client/src/error.rs index 6acbc73c78..c3979c0784 100644 --- a/common/client-libs/validator-client/src/error.rs +++ b/common/client-libs/validator-client/src/error.rs @@ -9,8 +9,7 @@ use thiserror::Error; pub enum ValidatorClientError { #[error("nym api request failed: {source}")] NymAPIError { - #[from] - source: nym_api::error::NymAPIError, + source: Box, }, #[error("Tendermint RPC request failure: {0}")] @@ -28,3 +27,11 @@ pub enum ValidatorClientError { #[error("No validator API url has been provided")] NoAPIUrlAvailable, } + +impl From for ValidatorClientError { + fn from(source: nym_api::error::NymAPIError) -> Self { + ValidatorClientError::NymAPIError { + source: Box::new(source), + } + } +} diff --git a/common/client-libs/validator-client/src/lib.rs b/common/client-libs/validator-client/src/lib.rs index 725b86450b..db86658047 100644 --- a/common/client-libs/validator-client/src/lib.rs +++ b/common/client-libs/validator-client/src/lib.rs @@ -14,7 +14,6 @@ pub mod signing; pub use crate::error::ValidatorClientError; pub use crate::rpc::reqwest::ReqwestRpcClient; pub use crate::signing::direct_wallet::DirectSecp256k1HdWallet; -pub use client::NymApiClient; pub use client::{Client, Config, EcashApiClient}; pub use nym_api_requests::*; pub use nym_http_api_client::UserAgent; diff --git a/common/client-libs/validator-client/src/nym_api/error.rs b/common/client-libs/validator-client/src/nym_api/error.rs index 3f4e9c65a4..457e4ecfd7 100644 --- a/common/client-libs/validator-client/src/nym_api/error.rs +++ b/common/client-libs/validator-client/src/nym_api/error.rs @@ -1,7 +1,6 @@ // Copyright 2022-2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use nym_api_requests::models::RequestError; use nym_http_api_client::HttpClientError; -pub type NymAPIError = HttpClientError; +pub type NymAPIError = HttpClientError; 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 aa13634503..8c549491e9 100644 --- a/common/client-libs/validator-client/src/nym_api/mod.rs +++ b/common/client-libs/validator-client/src/nym_api/mod.rs @@ -3,6 +3,7 @@ use crate::nym_api::error::NymAPIError; use crate::nym_api::routes::{ecash, CORE_STATUS_COUNT, SINCE_ARG}; +use crate::nym_nodes::SkimmedNodesWithMetadata; use async_trait::async_trait; use nym_api_requests::ecash::models::{ AggregatedCoinIndicesSignatureResponse, AggregatedExpirationDateSignatureResponse, @@ -15,9 +16,8 @@ use nym_api_requests::ecash::models::{ use nym_api_requests::ecash::VerificationKeyResponse; use nym_api_requests::models::{ AnnotationResponse, ApiHealthResponse, BinaryBuildInformationOwned, ChainBlocksStatusResponse, - ChainStatusResponse, KeyRotationInfoResponse, LegacyDescribedMixNode, NodePerformanceResponse, - NodeRefreshBody, NymNodeDescription, PerformanceHistoryResponse, RewardedSetResponse, - SignerInformationResponse, + ChainStatusResponse, KeyRotationInfoResponse, NodePerformanceResponse, NodeRefreshBody, + NymNodeDescription, PerformanceHistoryResponse, RewardedSetResponse, SignerInformationResponse, }; use nym_api_requests::nym_nodes::{ NodesByAddressesRequestBody, NodesByAddressesResponse, PaginatedCachedNodesResponseV1, @@ -31,26 +31,22 @@ pub use nym_api_requests::{ VerifyEcashCredentialBody, }, models::{ - ComputeRewardEstParam, GatewayBondAnnotated, GatewayCoreStatusResponse, - GatewayStatusReportResponse, GatewayUptimeHistoryResponse, LegacyDescribedGateway, - MixNodeBondAnnotated, MixnodeCoreStatusResponse, MixnodeStatusReportResponse, - MixnodeStatusResponse, MixnodeUptimeHistoryResponse, RewardEstimationResponse, - StakeSaturationResponse, UptimeResponse, + GatewayCoreStatusResponse, GatewayStatusReportResponse, GatewayUptimeHistoryResponse, + MixnodeCoreStatusResponse, MixnodeStatusReportResponse, MixnodeStatusResponse, + MixnodeUptimeHistoryResponse, StakeSaturationResponse, UptimeResponse, }, - nym_nodes::{CachedNodesResponse, SemiSkimmedNode, SkimmedNode}, + nym_nodes::{CachedNodesResponse, SemiSkimmedNode, SemiSkimmedNodesWithMetadata, SkimmedNode}, NymNetworkDetailsResponse, }; -use nym_contracts_common::IdentityKey; use nym_http_api_client::{ApiClient, NO_PARAMS}; -use nym_mixnet_contract_common::mixnode::MixNodeDetails; -use nym_mixnet_contract_common::{GatewayBond, IdentityKeyRef, NodeId, NymNodeDetails}; +use nym_mixnet_contract_common::{IdentityKeyRef, NodeId, NymNodeDetails}; use std::net::IpAddr; use time::format_description::BorrowedFormatItem; use time::Date; use tracing::instrument; +use crate::ValidatorClientError; pub use nym_coconut_dkg_common::types::EpochId; -pub use nym_http_api_client::Client; pub mod error; pub mod routes; @@ -62,6 +58,9 @@ pub fn rfc_3339_date() -> Vec> { #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] pub trait NymApiClientExt: ApiClient { + /// Get the current API URL being used by the client + fn api_url(&self) -> &url::Url; + async fn health(&self) -> Result { self.get_json( &[ @@ -87,104 +86,6 @@ pub trait NymApiClientExt: ApiClient { .await } - #[deprecated] - #[instrument(level = "debug", skip(self))] - async fn get_mixnodes(&self) -> Result, NymAPIError> { - self.get_json(&[routes::V1_API_VERSION, routes::MIXNODES], NO_PARAMS) - .await - } - - #[deprecated] - #[instrument(level = "debug", skip(self))] - async fn get_mixnodes_detailed(&self) -> Result, NymAPIError> { - self.get_json( - &[ - routes::V1_API_VERSION, - routes::STATUS, - routes::MIXNODES, - routes::DETAILED, - ], - NO_PARAMS, - ) - .await - } - - #[deprecated] - #[instrument(level = "debug", skip(self))] - async fn get_gateways_detailed(&self) -> Result, NymAPIError> { - self.get_json( - &[ - routes::V1_API_VERSION, - routes::STATUS, - routes::GATEWAYS, - routes::DETAILED, - ], - NO_PARAMS, - ) - .await - } - - #[deprecated] - #[instrument(level = "debug", skip(self))] - async fn get_gateways_detailed_unfiltered( - &self, - ) -> Result, NymAPIError> { - self.get_json( - &[ - routes::V1_API_VERSION, - routes::STATUS, - routes::GATEWAYS, - routes::DETAILED_UNFILTERED, - ], - NO_PARAMS, - ) - .await - } - - #[deprecated] - #[instrument(level = "debug", skip(self))] - async fn get_mixnodes_detailed_unfiltered( - &self, - ) -> Result, NymAPIError> { - self.get_json( - &[ - routes::V1_API_VERSION, - routes::STATUS, - routes::MIXNODES, - routes::DETAILED_UNFILTERED, - ], - NO_PARAMS, - ) - .await - } - - #[deprecated] - #[instrument(level = "debug", skip(self))] - async fn get_gateways(&self) -> Result, NymAPIError> { - self.get_json(&[routes::V1_API_VERSION, routes::GATEWAYS], NO_PARAMS) - .await - } - - #[deprecated] - #[instrument(level = "debug", skip(self))] - async fn get_gateways_described(&self) -> Result, NymAPIError> { - self.get_json( - &[routes::V1_API_VERSION, routes::GATEWAYS, routes::DESCRIBED], - NO_PARAMS, - ) - .await - } - - #[deprecated] - #[instrument(level = "debug", skip(self))] - async fn get_mixnodes_described(&self) -> Result, NymAPIError> { - self.get_json( - &[routes::V1_API_VERSION, routes::MIXNODES, routes::DESCRIBED], - NO_PARAMS, - ) - .await - } - #[tracing::instrument(level = "debug", skip_all)] async fn get_node_performance_history( &self, @@ -241,6 +142,156 @@ pub trait NymApiClientExt: ApiClient { .await } + async fn get_current_rewarded_set(&self) -> Result { + self.get_rewarded_set().await + } + + async fn get_all_basic_nodes_with_metadata( + &self, + ) -> Result { + // unroll first loop iteration in order to obtain the metadata + let mut page = 0; + let res = self + .get_basic_nodes_v2(false, Some(page), None, true) + .await?; + let mut nodes = res.nodes.data; + let metadata = res.metadata; + + if res.nodes.pagination.total == nodes.len() { + return Ok(SkimmedNodesWithMetadata::new(nodes, metadata)); + } + + page += 1; + + loop { + let mut res = self + .get_basic_nodes_v2(false, Some(page), None, true) + .await?; + + if !metadata.consistency_check(&res.metadata) { + // Create a custom error for inconsistent metadata + return Err(NymAPIError::InternalResponseInconsistency { + url: self.api_url().clone(), + details: "Inconsistent paged metadata".to_string(), + }); + } + + nodes.append(&mut res.nodes.data); + if nodes.len() >= res.nodes.pagination.total { + break; + } else { + page += 1 + } + } + + Ok(SkimmedNodesWithMetadata::new(nodes, metadata)) + } + + async fn get_all_basic_active_mixing_assigned_nodes_with_metadata( + &self, + ) -> Result { + // Get all mixing nodes that are in the active/rewarded set + let mut page = 0; + let res = self + .get_basic_active_mixing_assigned_nodes_v2(false, Some(page), None, false) + .await?; + + let metadata = res.metadata; + let mut nodes = res.nodes.data; + + if res.nodes.pagination.total == nodes.len() { + return Ok(SkimmedNodesWithMetadata::new(nodes, metadata)); + } + + page += 1; + + loop { + let res = self + .get_basic_active_mixing_assigned_nodes_v2(false, Some(page), None, false) + .await?; + + if !metadata.consistency_check(&res.metadata) { + return Err(NymAPIError::InternalResponseInconsistency { + url: self.api_url().clone(), + details: "Inconsistent paged metadata".to_string(), + }); + } + + nodes.append(&mut res.nodes.data.clone()); + + // Check if we've got all nodes + if nodes.len() >= res.nodes.pagination.total { + break; + } else { + page += 1; + } + } + + Ok(SkimmedNodesWithMetadata::new(nodes, metadata)) + } + + async fn get_all_basic_entry_assigned_nodes_with_metadata( + &self, + ) -> Result { + // Get all nodes that can act as entry gateways + let mut page = 0; + let res = self + .get_basic_entry_assigned_nodes_v2(false, Some(page), None, false) + .await?; + + let metadata = res.metadata; + let mut nodes = res.nodes.data; + + if res.nodes.pagination.total == nodes.len() { + return Ok(SkimmedNodesWithMetadata::new(nodes, metadata)); + } + + page += 1; + + loop { + let res = self + .get_basic_entry_assigned_nodes_v2(false, Some(page), None, false) + .await?; + + if !metadata.consistency_check(&res.metadata) { + return Err(NymAPIError::InternalResponseInconsistency { + url: self.api_url().clone(), + details: "Inconsistent paged metadata".to_string(), + }); + } + + nodes.append(&mut res.nodes.data.clone()); + + // Check if we've got all nodes + if nodes.len() >= res.nodes.pagination.total { + break; + } else { + page += 1; + } + } + + Ok(SkimmedNodesWithMetadata::new(nodes, metadata)) + } + + async fn get_all_described_nodes(&self) -> Result, NymAPIError> { + // TODO: deal with paging in macro or some helper function or something, because it's the same pattern everywhere + let mut page = 0; + let mut descriptions = Vec::new(); + + loop { + let mut res = self.get_nodes_described(Some(page), None).await?; + + descriptions.append(&mut res.data); + if descriptions.len() < res.pagination.total { + page += 1 + } else { + break; + } + } + + Ok(descriptions) + } + #[tracing::instrument(level = "debug", skip_all)] async fn get_nym_nodes( &self, @@ -268,6 +319,25 @@ pub trait NymApiClientExt: ApiClient { .await } + async fn get_all_bonded_nym_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 bonds = Vec::new(); + + loop { + let mut res = self.get_nym_nodes(Some(page), None).await?; + + bonds.append(&mut res.data); + if bonds.len() < res.pagination.total { + page += 1 + } else { + break; + } + } + + Ok(bonds) + } + #[deprecated] #[tracing::instrument(level = "debug", skip_all)] async fn get_basic_mixnodes(&self) -> Result, NymAPIError> { @@ -677,42 +747,6 @@ pub trait NymApiClientExt: ApiClient { .await } - #[deprecated] - #[instrument(level = "debug", skip(self))] - async fn get_active_mixnodes(&self) -> Result, NymAPIError> { - self.get_json( - &[routes::V1_API_VERSION, routes::MIXNODES, routes::ACTIVE], - NO_PARAMS, - ) - .await - } - - #[deprecated] - #[instrument(level = "debug", skip(self))] - async fn get_active_mixnodes_detailed(&self) -> Result, NymAPIError> { - self.get_json( - &[ - routes::V1_API_VERSION, - routes::STATUS, - routes::MIXNODES, - routes::ACTIVE, - routes::DETAILED, - ], - NO_PARAMS, - ) - .await - } - - #[deprecated] - #[instrument(level = "debug", skip(self))] - async fn get_rewarded_mixnodes(&self) -> Result, NymAPIError> { - self.get_json( - &[routes::V1_API_VERSION, routes::MIXNODES, routes::REWARDED], - NO_PARAMS, - ) - .await - } - #[deprecated] #[instrument(level = "debug", skip(self))] async fn get_mixnode_report( @@ -789,24 +823,6 @@ pub trait NymApiClientExt: ApiClient { .await } - #[deprecated] - #[instrument(level = "debug", skip(self))] - async fn get_rewarded_mixnodes_detailed( - &self, - ) -> Result, NymAPIError> { - self.get_json( - &[ - routes::V1_API_VERSION, - routes::STATUS, - routes::MIXNODES, - routes::REWARDED, - routes::DETAILED, - ], - NO_PARAMS, - ) - .await - } - #[deprecated] #[instrument(level = "debug", skip(self))] async fn get_gateway_core_status_count( @@ -874,104 +890,6 @@ pub trait NymApiClientExt: ApiClient { } } - #[deprecated] - #[instrument(level = "debug", skip(self))] - async fn get_mixnode_status( - &self, - mix_id: NodeId, - ) -> Result { - self.get_json( - &[ - routes::V1_API_VERSION, - routes::STATUS_ROUTES, - routes::MIXNODE, - &mix_id.to_string(), - routes::STATUS, - ], - NO_PARAMS, - ) - .await - } - - #[deprecated] - #[instrument(level = "debug", skip(self))] - async fn get_mixnode_reward_estimation( - &self, - mix_id: NodeId, - ) -> Result { - self.get_json( - &[ - routes::V1_API_VERSION, - routes::STATUS_ROUTES, - routes::MIXNODE, - &mix_id.to_string(), - routes::REWARD_ESTIMATION, - ], - NO_PARAMS, - ) - .await - } - - #[deprecated] - #[instrument(level = "debug", skip(self))] - async fn compute_mixnode_reward_estimation( - &self, - mix_id: NodeId, - request_body: &ComputeRewardEstParam, - ) -> Result { - self.post_json( - &[ - routes::V1_API_VERSION, - routes::STATUS_ROUTES, - routes::MIXNODE, - &mix_id.to_string(), - routes::COMPUTE_REWARD_ESTIMATION, - ], - NO_PARAMS, - request_body, - ) - .await - } - - #[deprecated] - #[instrument(level = "debug", skip(self))] - async fn get_mixnode_stake_saturation( - &self, - mix_id: NodeId, - ) -> Result { - self.get_json( - &[ - routes::V1_API_VERSION, - routes::STATUS_ROUTES, - routes::MIXNODE, - &mix_id.to_string(), - routes::STAKE_SATURATION, - ], - NO_PARAMS, - ) - .await - } - - #[deprecated] - #[allow(deprecated)] - #[instrument(level = "debug", skip(self))] - async fn get_mixnode_inclusion_probability( - &self, - mix_id: NodeId, - ) -> Result { - self.get_json( - &[ - routes::V1_API_VERSION, - routes::STATUS_ROUTES, - routes::MIXNODE, - &mix_id.to_string(), - routes::INCLUSION_CHANCE, - ], - NO_PARAMS, - ) - .await - } - #[instrument(level = "debug", skip(self))] async fn get_current_node_performance( &self, @@ -1020,34 +938,6 @@ pub trait NymApiClientExt: ApiClient { .await } - #[deprecated] - #[instrument(level = "debug", skip(self))] - async fn get_mixnodes_blacklisted(&self) -> Result, NymAPIError> { - self.get_json( - &[ - routes::V1_API_VERSION, - routes::MIXNODES, - routes::BLACKLISTED, - ], - NO_PARAMS, - ) - .await - } - - #[deprecated] - #[instrument(level = "debug", skip(self))] - async fn get_gateways_blacklisted(&self) -> Result, NymAPIError> { - self.get_json( - &[ - routes::V1_API_VERSION, - routes::GATEWAYS, - routes::BLACKLISTED, - ], - NO_PARAMS, - ) - .await - } - #[instrument(level = "debug", skip(self, request_body))] async fn blind_sign( &self, @@ -1371,8 +1261,49 @@ pub trait NymApiClientExt: ApiClient { ) .await } + + /// Method to change the base API URLs being used by the client + fn change_base_urls(&mut self, urls: Vec); + + /// Retrieve expanded information for all bonded nodes on the network + async fn get_all_expanded_nodes(&self) -> Result { + // Unroll the first iteration to get the metadata + let mut page = 0; + + let res = self.get_expanded_nodes(false, Some(page), None).await?; + let mut nodes = res.nodes.data; + let metadata = res.metadata; + + if res.nodes.pagination.total == nodes.len() { + return Ok(SemiSkimmedNodesWithMetadata::new(nodes, metadata)); + } + + page += 1; + + loop { + let mut res = self.get_expanded_nodes(false, Some(page), None).await?; + + nodes.append(&mut res.nodes.data); + if nodes.len() < res.nodes.pagination.total { + page += 1 + } else { + break; + } + } + + Ok(SemiSkimmedNodesWithMetadata::new(nodes, metadata)) + } } +// Client is already nym_http_api_client::Client (re-exported above), so just one impl needed #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] -impl NymApiClientExt for Client {} +impl NymApiClientExt for nym_http_api_client::Client { + fn api_url(&self) -> &url::Url { + self.current_url().as_ref() + } + + fn change_base_urls(&mut self, urls: Vec) { + self.change_base_urls(urls.into_iter().map(|u| u.into()).collect()); + } +} diff --git a/common/client-libs/validator-client/src/nyxd/cosmwasm_client/client_traits/query_client.rs b/common/client-libs/validator-client/src/nyxd/cosmwasm_client/client_traits/query_client.rs index 0a13537643..80af295644 100644 --- a/common/client-libs/validator-client/src/nyxd/cosmwasm_client/client_traits/query_client.rs +++ b/common/client-libs/validator-client/src/nyxd/cosmwasm_client/client_traits/query_client.rs @@ -28,7 +28,7 @@ use cosmrs::proto::cosmwasm::wasm::v1::{ QueryRawContractStateResponse, QuerySmartContractStateRequest, QuerySmartContractStateResponse, }; use cosmrs::tendermint::{block, chain, Hash}; -use cosmrs::{AccountId, Coin as CosmosCoin, Tx}; +use cosmrs::{AccountId, Coin as CosmosCoin}; use prost::Message; use serde::{Deserialize, Serialize}; @@ -556,23 +556,12 @@ pub trait CosmWasmClient: TendermintRpcClient { Ok(serde_json::from_slice(&res.data)?) } - // deprecation warning is due to the fact the protobuf files built were based on cosmos-sdk 0.44, - // where they prefer using tx_bytes directly. However, in 0.42, which we are using at the time - // of writing this, the option does not work - // TODO: we should really stop using the `tx` argument here and use `tx_bytes` exlusively, - // however, at the time of writing this update, while our QA and mainnet networks do support it, - // sandbox is still running old version of wasmd that lacks support for `tx_bytes` - #[allow(deprecated)] - async fn query_simulate( - &self, - tx: Option, - tx_bytes: Vec, - ) -> Result { + async fn query_simulate(&self, tx_bytes: Vec) -> Result { let path = Some("/cosmos.tx.v1beta1.Service/Simulate".to_owned()); let req = SimulateRequest { - tx: tx.map(Into::into), tx_bytes, + ..Default::default() }; let res = self diff --git a/common/client-libs/validator-client/src/nyxd/cosmwasm_client/client_traits/signing_client.rs b/common/client-libs/validator-client/src/nyxd/cosmwasm_client/client_traits/signing_client.rs index cec29e9c50..e59046af7b 100644 --- a/common/client-libs/validator-client/src/nyxd/cosmwasm_client/client_traits/signing_client.rs +++ b/common/client-libs/validator-client/src/nyxd/cosmwasm_client/client_traits/signing_client.rs @@ -81,17 +81,14 @@ where auth_info: single_unspecified_signer_auth(public_key, sequence_response.sequence), signatures: vec![Vec::new()], }; - self.query_simulate(Some(partial_tx), Vec::new()).await - // for completion sake, once we're able to transition into using `tx_bytes`, - // we might want to use something like this instead: - // let tx_raw: tx::Raw = cosmrs::proto::cosmos::tx::v1beta1::TxRaw { - // body_bytes: partial_tx.body.into_bytes().unwrap(), - // auth_info_bytes: partial_tx.auth_info.into_bytes().unwrap(), - // signatures: partial_tx.signatures, - // } - // .into(); - // self.query_simulate(None, tx_raw.to_bytes().unwrap()).await + let tx_raw: tx::Raw = cosmrs::proto::cosmos::tx::v1beta1::TxRaw { + body_bytes: partial_tx.body.into_bytes()?, + auth_info_bytes: partial_tx.auth_info.into_bytes()?, + signatures: partial_tx.signatures, + } + .into(); + self.query_simulate(tx_raw.to_bytes()?).await } async fn upload( diff --git a/common/commands/Cargo.toml b/common/commands/Cargo.toml index d382cb696f..4398342d4c 100644 --- a/common/commands/Cargo.toml +++ b/common/commands/Cargo.toml @@ -38,6 +38,7 @@ cosmrs = { workspace = true } cosmwasm-std = { workspace = true } nym-validator-client = { path = "../client-libs/validator-client" } +nym-http-api-client = { path = "../http-api-client" } nym-bin-common = { path = "../../common/bin-common", features = ["output_format"] } nym-crypto = { path = "../../common/crypto", features = ["asymmetric"] } nym-network-defaults = { path = "../network-defaults" } diff --git a/common/commands/src/context/mod.rs b/common/commands/src/context/mod.rs index db20ed0ac3..daca52df77 100644 --- a/common/commands/src/context/mod.rs +++ b/common/commands/src/context/mod.rs @@ -2,12 +2,12 @@ // SPDX-License-Identifier: Apache-2.0 use crate::context::errors::ContextError; +pub use nym_http_api_client::Client as NymApiClient; use nym_network_defaults::{ setup_env, var_names::{MIXNET_CONTRACT_ADDRESS, NYM_API, NYXD, VESTING_CONTRACT_ADDRESS}, NymNetworkDetails, }; -pub use nym_validator_client::nym_api::Client as NymApiClient; use nym_validator_client::nyxd::{self, AccountId, NyxdClient}; use nym_validator_client::{ DirectSigningHttpRpcNyxdClient, DirectSigningHttpRpcValidatorClient, QueryHttpRpcNyxdClient, diff --git a/common/commands/src/internal/mod.rs b/common/commands/src/internal/mod.rs index 44fa4d2659..fc929a2e4e 100644 --- a/common/commands/src/internal/mod.rs +++ b/common/commands/src/internal/mod.rs @@ -4,6 +4,7 @@ use clap::{Args, Subcommand}; pub mod ecash; +pub mod nyx; #[derive(Debug, Args)] #[clap(args_conflicts_with_subcommands = true, subcommand_required = true)] @@ -16,4 +17,6 @@ pub struct Internal { pub enum InternalCommands { /// Ecash related internal commands Ecash(ecash::InternalEcash), + + Nyx(nyx::InternalNyx), } diff --git a/common/commands/src/internal/nyx/force_advance_epoch.rs b/common/commands/src/internal/nyx/force_advance_epoch.rs new file mode 100644 index 0000000000..3c797ff01c --- /dev/null +++ b/common/commands/src/internal/nyx/force_advance_epoch.rs @@ -0,0 +1,116 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::context::SigningClient; +use anyhow::bail; +use clap::Parser; +use nym_mixnet_contract_common::nym_node::Role; +use nym_mixnet_contract_common::reward_params::NodeRewardingParameters; +use nym_mixnet_contract_common::{ + EpochRewardedSet, EpochState, NodeId, RewardingParams, RoleAssignment, +}; +use nym_validator_client::nyxd::contract_traits::mixnet_query_client::MixnetQueryClientExt; +use nym_validator_client::nyxd::contract_traits::{MixnetQueryClient, MixnetSigningClient}; +use rand::prelude::*; +use rand::thread_rng; + +#[derive(Debug, Parser)] +pub struct Args {} + +fn choose_new_nodes( + params: &RewardingParams, + rewarded_set: &EpochRewardedSet, + role: Role, +) -> Vec { + let mut rng = thread_rng(); + + match role { + Role::EntryGateway => rewarded_set + .assignment + .entry_gateways + .choose_multiple(&mut rng, params.rewarded_set.entry_gateways as usize) + .copied() + .collect(), + Role::Layer1 => rewarded_set + .assignment + .layer1 + .choose_multiple(&mut rng, params.rewarded_set.mixnodes as usize / 3) + .copied() + .collect(), + Role::Layer2 => rewarded_set + .assignment + .layer2 + .choose_multiple(&mut rng, params.rewarded_set.mixnodes as usize / 3) + .copied() + .collect(), + Role::Layer3 => rewarded_set + .assignment + .layer3 + .choose_multiple(&mut rng, params.rewarded_set.mixnodes as usize / 3) + .copied() + .collect(), + Role::ExitGateway => rewarded_set + .assignment + .exit_gateways + .choose_multiple(&mut rng, params.rewarded_set.exit_gateways as usize) + .copied() + .collect(), + Role::Standby => rewarded_set + .assignment + .standby + .choose_multiple(&mut rng, params.rewarded_set.standby as usize) + .copied() + .collect(), + } +} + +pub async fn force_advance_epoch(_: Args, client: SigningClient) -> anyhow::Result<()> { + let current_epoch = client.get_current_interval_details().await?; + let epoch_status = client.get_current_epoch_status().await?; + if epoch_status.being_advanced_by.as_str() != client.address().to_string() { + bail!( + "this client is not authorised to perform any epoch operations. we need {}", + client.address() + ) + } + + let rewarding_params = client.get_rewarding_parameters().await?; + let current_rewarded_set = client.get_rewarded_set().await?; + + if !current_epoch.is_current_epoch_over { + println!("the current epoch is not over yet - there's nothing to do") + } + + // is this most efficient? no. but it's simple + loop { + let epoch_status = client.get_current_epoch_status().await?; + + match epoch_status.state { + EpochState::InProgress => break, + EpochState::Rewarding { final_node_id, .. } => { + println!("rewarding {final_node_id} with big fat 0..."); + client + .reward_node( + final_node_id, + NodeRewardingParameters::new(Default::default(), Default::default()), + None, + ) + .await?; + } + EpochState::ReconcilingEvents => { + println!("trying to reconcile events..."); + client.reconcile_epoch_events(None, None).await?; + } + EpochState::RoleAssignment { next } => { + let nodes = choose_new_nodes(&rewarding_params, ¤t_rewarded_set, next); + println!("assigning {nodes:?} as {next}"); + + client + .assign_roles(RoleAssignment { role: next, nodes }, None) + .await?; + } + } + } + + Ok(()) +} diff --git a/common/commands/src/internal/nyx/mod.rs b/common/commands/src/internal/nyx/mod.rs new file mode 100644 index 0000000000..0ffb24f3a3 --- /dev/null +++ b/common/commands/src/internal/nyx/mod.rs @@ -0,0 +1,19 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use clap::{Args, Subcommand}; + +pub mod force_advance_epoch; + +#[derive(Debug, Args)] +#[clap(args_conflicts_with_subcommands = true, subcommand_required = true)] +pub struct InternalNyx { + #[clap(subcommand)] + pub command: InternalNyxCommands, +} + +#[derive(Debug, Subcommand)] +pub enum InternalNyxCommands { + /// Attempt to force advance the current epoch + ForceAdvanceEpoch(force_advance_epoch::Args), +} diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/bindings/ts-packages/types/src/types/rust/Role.ts b/common/cosmwasm-smart-contracts/mixnet-contract/bindings/ts-packages/types/src/types/rust/Role.ts index 3bac0196bd..ab0a836612 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/bindings/ts-packages/types/src/types/rust/Role.ts +++ b/common/cosmwasm-smart-contracts/mixnet-contract/bindings/ts-packages/types/src/types/rust/Role.ts @@ -1,3 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type Role = "EntryGateway" | "Layer1" | "Layer2" | "Layer3" | "ExitGateway" | "Standby"; +export type Role = "entry_gateway" | "layer1" | "layer2" | "layer3" | "exit_gateway" | "standby"; diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/mixnode.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/mixnode.rs index 91b4222fbe..ea45cd0414 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/mixnode.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/mixnode.rs @@ -23,6 +23,7 @@ use serde_repr::{Deserialize_repr, Serialize_repr}; /// Full details associated with given mixnode. #[cw_serde] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] pub struct MixNodeDetails { /// Basic bond information of this mixnode, such as owner address, original pledge, etc. pub bond_information: MixNodeBond, @@ -695,6 +696,7 @@ impl From for u8 { Copy, )] #[schemars(crate = "::cosmwasm_schema::schemars")] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] pub struct PendingMixNodeChanges { pub pledge_change: Option, diff --git a/common/credential-proxy/Cargo.toml b/common/credential-proxy/Cargo.toml new file mode 100644 index 0000000000..9fb4248a05 --- /dev/null +++ b/common/credential-proxy/Cargo.toml @@ -0,0 +1,55 @@ +[package] +name = "nym-credential-proxy-lib" +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 } +axum = { workspace = true } +bip39 = { workspace = true, features = ["zeroize"] } +bs58 = { workspace = true } +futures = { workspace = true } +humantime = { workspace = true } +rand = { workspace = true } +reqwest = { workspace = true, features = ["rustls-tls"] } +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } +strum = { workspace = true, features = ["derive"] } +strum_macros = { workspace = true } +sqlx = { workspace = true, features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate", "time"] } +time = { workspace = true } +thiserror = { workspace = true } +tokio = { workspace = true, features = ["sync"] } +tokio-util = { workspace = true, features = ["rt"] } +tracing = { workspace = true } +uuid = { workspace = true, features = ["serde"] } +url = { workspace = true } +zeroize = { workspace = true } + +nym-credentials = { path = "../credentials" } +nym-crypto = { path = "../crypto", features = ["asymmetric", "rand", "serde"] } +nym-credentials-interface = { path = "../credentials-interface" } +nym-credential-proxy-requests = { path = "../../nym-credential-proxy/nym-credential-proxy-requests" } +nym-ecash-signer-check = { path = "../ecash-signer-check" } +nym-ecash-contract-common = { path = "../cosmwasm-smart-contracts/ecash-contract" } +nym-compact-ecash = { path = "../nym_offline_compact_ecash" } +nym-validator-client = { path = "../client-libs/validator-client" } +nym-network-defaults = { path = "../network-defaults" } + +[dev-dependencies] +tempfile = { workspace = true } + +[build-dependencies] +anyhow = { workspace = true } +tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } +sqlx = { workspace = true, features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"] } + +[lints] +workspace = true diff --git a/nym-credential-proxy/nym-credential-proxy/build.rs b/common/credential-proxy/build.rs similarity index 54% rename from nym-credential-proxy/nym-credential-proxy/build.rs rename to common/credential-proxy/build.rs index bdbdcf0d22..bedf911da7 100644 --- a/nym-credential-proxy/nym-credential-proxy/build.rs +++ b/common/credential-proxy/build.rs @@ -1,22 +1,31 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only +use anyhow::Context; + #[tokio::main] -async fn main() { +async fn main() -> anyhow::Result<()> { use sqlx::{Connection, SqliteConnection}; use std::env; - let out_dir = env::var("OUT_DIR").unwrap(); + let out_dir = env::var("OUT_DIR")?; let database_path = format!("{out_dir}/nym-credential-proxy-example.sqlite"); + // remove the db file if it already existed from previous build + // in case it was from a different branch + if std::fs::exists(&database_path)? { + std::fs::remove_file(&database_path)?; + } + let mut conn = SqliteConnection::connect(&format!("sqlite://{database_path}?mode=rwc")) .await - .expect("Failed to create SQLx database connection"); + .context("Failed to create SQLx database connection")?; sqlx::migrate!("./migrations") .run(&mut conn) .await - .expect("Failed to perform SQLx migrations"); + .context("Failed to perform SQLx migrations")?; println!("cargo:rustc-env=DATABASE_URL=sqlite://{}", &database_path); + Ok(()) } diff --git a/nym-credential-proxy/nym-credential-proxy/migrations/01_initial.sql b/common/credential-proxy/migrations/01_initial.sql similarity index 100% rename from nym-credential-proxy/nym-credential-proxy/migrations/01_initial.sql rename to common/credential-proxy/migrations/01_initial.sql diff --git a/nym-credential-proxy/nym-credential-proxy/migrations/02_cherry_picking_chaos.sql b/common/credential-proxy/migrations/02_cherry_picking_chaos.sql similarity index 100% rename from nym-credential-proxy/nym-credential-proxy/migrations/02_cherry_picking_chaos.sql rename to common/credential-proxy/migrations/02_cherry_picking_chaos.sql diff --git a/nym-credential-proxy/nym-credential-proxy/migrations/03_blinded_shares_no_foreign_table.sql b/common/credential-proxy/migrations/03_blinded_shares_no_foreign_table.sql similarity index 100% rename from nym-credential-proxy/nym-credential-proxy/migrations/03_blinded_shares_no_foreign_table.sql rename to common/credential-proxy/migrations/03_blinded_shares_no_foreign_table.sql diff --git a/nym-credential-proxy/nym-credential-proxy/migrations/04_global_expiration_date_signatures_epoch_fix.sql b/common/credential-proxy/migrations/04_global_expiration_date_signatures_epoch_fix.sql similarity index 100% rename from nym-credential-proxy/nym-credential-proxy/migrations/04_global_expiration_date_signatures_epoch_fix.sql rename to common/credential-proxy/migrations/04_global_expiration_date_signatures_epoch_fix.sql diff --git a/common/credential-proxy/migrations/05_buffered_deposits.sql b/common/credential-proxy/migrations/05_buffered_deposits.sql new file mode 100644 index 0000000000..88b875a2a9 --- /dev/null +++ b/common/credential-proxy/migrations/05_buffered_deposits.sql @@ -0,0 +1,81 @@ +/* + * Copyright 2025 - Nym Technologies SA + * SPDX-License-Identifier: GPL-3.0-only + */ + +CREATE TABLE ecash_deposit +( + -- id assigned [by the contract] to the deposit + deposit_id INTEGER PRIMARY KEY NOT NULL, + + -- associated tx hash + deposit_tx_hash TEXT NOT NULL, + + -- indication of when the deposit request has been created + -- (so that based on block timestamp we could potentially determine latency) + requested_on TIMESTAMP WITHOUT TIME ZONE NOT NULL, + + -- the amount put in the deposit (informative, as we expect this to change in the future) + deposit_amount TEXT NOT NULL, + + -- the private key generated for the purposes of the deposit (the public component has been put in the transaction) + ed25519_deposit_private_key BLOB NOT NULL +); + + +INSERT INTO ecash_deposit(deposit_id, deposit_tx_hash, requested_on, deposit_amount, ed25519_deposit_private_key) +SELECT deposit_id, deposit_tx_hash, requested_on, deposit_amount, ed25519_deposit_private_key +FROM ticketbook_deposit; + + +CREATE TABLE ecash_deposit_usage +( + deposit_id INTEGER PRIMARY KEY REFERENCES ecash_deposit (deposit_id), + ticketbooks_requested_on TIMESTAMP WITHOUT TIME ZONE NOT NULL, + client_pubkey BLOB NOT NULL, + request_uuid TEXT UNIQUE NOT NULL, + + -- this has to be improved later on to resume issuance or something, but for now it's fine + ticketbook_request_error TEXT +); + +INSERT INTO ecash_deposit_usage(deposit_id, ticketbooks_requested_on, client_pubkey, request_uuid) +SELECT deposit_id, 0, client_pubkey, request_uuid +FROM ticketbook_deposit; + + +CREATE TABLE partial_blinded_wallet_new +( + corresponding_deposit INTEGER NOT NULL REFERENCES ecash_deposit_usage (deposit_id), + epoch_id INTEGER NOT NULL, + expiration_date DATE NOT NULL, + node_id INTEGER NOT NULL, + created TIMESTAMP WITHOUT TIME ZONE NOT NULL, + blinded_signature BLOB NOT NULL +); + +CREATE TABLE partial_blinded_wallet_failure_new +( + corresponding_deposit INTEGER NOT NULL REFERENCES ecash_deposit_usage (deposit_id), + epoch_id INTEGER NOT NULL, + expiration_date DATE NOT NULL, + node_id INTEGER NOT NULL, + created TIMESTAMP WITHOUT TIME ZONE NOT NULL, + failure_message TEXT NOT NULL +); + +INSERT INTO partial_blinded_wallet_new +SELECT * +FROM partial_blinded_wallet; +INSERT INTO partial_blinded_wallet_failure_new +SELECT * +FROM partial_blinded_wallet_failure; + +DROP TABLE partial_blinded_wallet; +DROP TABLE partial_blinded_wallet_failure; +DROP TABLE ticketbook_deposit; + +ALTER TABLE partial_blinded_wallet_new + RENAME TO partial_blinded_wallet; +ALTER TABLE partial_blinded_wallet_failure_new + RENAME TO partial_blinded_wallet_failure; diff --git a/common/credential-proxy/src/deposits_buffer/helpers.rs b/common/credential-proxy/src/deposits_buffer/helpers.rs new file mode 100644 index 0000000000..d2d0814e01 --- /dev/null +++ b/common/credential-proxy/src/deposits_buffer/helpers.rs @@ -0,0 +1,101 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::error::CredentialProxyError; +use crate::storage::models::StorableEcashDeposit; +use nym_compact_ecash::WithdrawalRequest; +use nym_credentials::IssuanceTicketBook; +use nym_crypto::asymmetric::ed25519; +use nym_validator_client::nyxd::{Coin, Hash}; +use time::OffsetDateTime; +use zeroize::Zeroizing; + +pub struct BufferedDeposit { + pub deposit_id: u32, + + // note: this type implements `ZeroizeOnDrop` + pub ed25519_private_key: ed25519::PrivateKey, +} + +impl TryFrom for BufferedDeposit { + type Error = CredentialProxyError; + + fn try_from(deposit: StorableEcashDeposit) -> Result { + let ed25519_private_key = ed25519::PrivateKey::from_bytes( + deposit.ed25519_deposit_private_key.as_ref(), + ) + .map_err(|err| CredentialProxyError::DatabaseInconsistency { + reason: format!("one of the stored deposit ed25519 private keys is malformed: {err}"), + })?; + + Ok(BufferedDeposit { + deposit_id: deposit.deposit_id, + ed25519_private_key, + }) + } +} + +impl BufferedDeposit { + pub fn new(deposit_id: u32, ed25519_private_key: ed25519::PrivateKey) -> Self { + BufferedDeposit { + deposit_id, + ed25519_private_key, + } + } + + pub fn sign_ticketbook_plaintext( + &self, + withdrawal_request: &WithdrawalRequest, + ) -> ed25519::Signature { + let plaintext = IssuanceTicketBook::request_plaintext(withdrawal_request, self.deposit_id); + self.ed25519_private_key.sign(plaintext) + } +} + +pub struct PerformedDeposits { + pub deposits_data: Vec, + + // shared by all performed deposits as they were included in the same tx + pub tx_hash: Hash, + pub requested_on: OffsetDateTime, + pub deposit_amount: Coin, +} + +impl PerformedDeposits { + pub(crate) fn to_storable(&self) -> Vec { + self.deposits_data + .iter() + .map(|d| StorableEcashDeposit { + deposit_id: d.deposit_id, + deposit_tx_hash: self.tx_hash.to_string(), + requested_on: self.requested_on, + deposit_amount: self.deposit_amount.to_string(), + ed25519_deposit_private_key: Zeroizing::new(d.ed25519_private_key.to_bytes()), + }) + .collect() + } +} + +pub(super) fn request_sizes(total: usize, max_request_size: usize) -> impl Iterator { + (0..total) + .step_by(max_request_size) + .map(move |start| std::cmp::min(max_request_size, total - start)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn request_sizes_test() { + assert_eq!( + request_sizes(100, 32).collect::>(), + vec![32, 32, 32, 4] + ); + + assert_eq!(request_sizes(10, 32).collect::>(), vec![10]); + assert_eq!(request_sizes(32, 32).collect::>(), vec![32]); + assert_eq!(request_sizes(33, 32).collect::>(), vec![32, 1]); + assert_eq!(request_sizes(1, 32).collect::>(), vec![1]); + } +} diff --git a/common/credential-proxy/src/deposits_buffer/mod.rs b/common/credential-proxy/src/deposits_buffer/mod.rs new file mode 100644 index 0000000000..59cbe51e7b --- /dev/null +++ b/common/credential-proxy/src/deposits_buffer/mod.rs @@ -0,0 +1,308 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::deposits_buffer::helpers::request_sizes; +use crate::deposits_buffer::refill_task::RefillTask; +use crate::error::CredentialProxyError; +use crate::shared_state::nyxd_client::ChainClient; +use crate::shared_state::required_deposit_cache::RequiredDepositCache; +use crate::storage::CredentialProxyStorage; +use nym_compact_ecash::PublicKeyUser; +use nym_crypto::asymmetric::ed25519; +use nym_ecash_contract_common::deposit::DepositId; +use nym_validator_client::nyxd::cosmwasm_client::ContractResponseData; +use nym_validator_client::nyxd::Coin; +use rand::rngs::OsRng; +use std::sync::Arc; +use std::time::Duration; +use time::OffsetDateTime; +use tokio::sync::Mutex as AsyncMutex; +use tokio_util::sync::CancellationToken; +use tracing::{debug, error, info, instrument, warn}; +use uuid::Uuid; + +pub use helpers::{BufferedDeposit, PerformedDeposits}; + +pub(crate) mod helpers; +mod refill_task; + +// TODO: I guess make it configurable +const DEPOSITS_THRESHOLD_P: f32 = 0.1; + +struct DepositsBufferInner { + client: ChainClient, + + required_deposit_cache: RequiredDepositCache, + + storage: CredentialProxyStorage, + target_amount: usize, + max_concurrent_deposits: usize, + unused_deposits: AsyncMutex>, + + deposits_refill_task: RefillTask, + short_sha: &'static str, + cancellation_token: CancellationToken, +} + +#[derive(Clone)] +pub struct DepositsBuffer { + inner: Arc, +} + +impl DepositsBuffer { + pub async fn new( + storage: CredentialProxyStorage, + client: ChainClient, + required_deposit_cache: RequiredDepositCache, + short_sha: &'static str, + target_amount: usize, + max_concurrent_deposits: usize, + cancellation_token: CancellationToken, + ) -> Result { + let unused_deposits = storage.load_unused_deposits().await?; + info!("managed to load {} deposits", unused_deposits.len()); + + Ok(DepositsBuffer { + inner: Arc::new(DepositsBufferInner { + client, + required_deposit_cache, + storage, + target_amount, + max_concurrent_deposits, + unused_deposits: AsyncMutex::new(unused_deposits), + deposits_refill_task: RefillTask::default(), + short_sha, + cancellation_token, + }), + }) + } + + async fn deposit_amount(&self) -> Result { + self.inner + .required_deposit_cache + .get_or_update(&self.inner.client) + .await + } + + #[instrument(skip(self), err(Display))] + async fn make_deposits_request( + &self, + amount: usize, + ) -> Result { + let requested_on = OffsetDateTime::now_utc(); + let chain_write_permit = self.inner.client.start_chain_tx().await; + let mut rng = OsRng; + + let deposit_amount = self.deposit_amount().await?; + let keys = (0..amount) + .map(|_| ed25519::PrivateKey::new(&mut rng)) + .collect::>(); + + info!("starting {amount} deposits"); + let mut contents = Vec::new(); + for key in &keys { + let public_key: ed25519::PublicKey = key.into(); + contents.push((public_key.to_base58_string(), deposit_amount.clone())); + } + + let execute_res = chain_write_permit + .make_deposits(self.inner.short_sha, contents) + .await?; + + let tx_hash = execute_res.transaction_hash; + info!("{amount} deposits made in transaction: {tx_hash}"); + + let contract_data = match execute_res.to_contract_data() { + Ok(contract_data) => contract_data, + Err(err) => { + // that one is tricky. deposits technically got made, but we somehow failed to parse response, + // in this case terminate the proxy with 0 exit code so it wouldn't get automatically restarted + // because it requires some serious MANUAL intervention + error!("CRITICAL FAILURE: failed to parse out deposit information from the contract transaction. either the chain got upgraded and the schema changed or the ecash contract got changed! terminating the process. it has to be inspected manually. error was: {err}"); + self.inner.cancellation_token.cancel(); + return Err(CredentialProxyError::DepositFailure); + } + }; + + if contract_data.len() != amount { + // another critical failure, that one should be quite impossible and thus has to be manually inspected + error!("CRITICAL FAILURE: failed to parse out all deposit information from the contract transaction. got {} responses while we sent {amount} deposits! either the chain got upgraded and the schema changed or the ecash contract got changed! terminating the process. it has to be inspected manually", contract_data.len()); + self.inner.cancellation_token.cancel(); + return Err(CredentialProxyError::DepositFailure); + } + + let mut deposits_data = Vec::new(); + for (key, response) in keys.into_iter().zip(contract_data) { + let response_index = response.message_index; + let deposit_id = match response.parse_singleton_u32_contract_data() { + Ok(deposit_id) => deposit_id, + Err(err) => { + // another impossibility + error!("CRITICAL FAILURE: failed to parse out deposit id out of the response at index {response_index}: {err}. either the chain got upgraded and the schema changed or the ecash contract got changed! terminating the process. it has to be inspected manually"); + self.inner.cancellation_token.cancel(); + return Err(CredentialProxyError::DepositFailure); + } + }; + + deposits_data.push(BufferedDeposit::new(deposit_id, key)); + } + + Ok(PerformedDeposits { + deposits_data, + tx_hash, + requested_on, + deposit_amount, + }) + } + + async fn insert_new_deposits( + &self, + mut deposits: PerformedDeposits, + ) -> Result<(), CredentialProxyError> { + // 1. insert into the db + self.inner.storage.insert_new_deposits(&deposits).await?; + + // 2. update the buffer + self.inner + .unused_deposits + .lock() + .await + .append(&mut deposits.deposits_data); + Ok(()) + } + + /// Start refilling our deposit buffer. + /// It chunks the amount required based on the configured maximum request size + /// and updates global state after each successful transaction. + async fn refill_deposits(&self) -> Result<(), CredentialProxyError> { + let available = self.inner.unused_deposits.lock().await.len(); + + let target = self.deposits_upper_threshold(); + let to_request = target - available; + + for request_chunk in request_sizes(to_request, self.inner.max_concurrent_deposits) { + // note: we check for cancellation between individual requests + // as opposed to wrapping that in tokio::select! so that we would never abandon chain operations + // as we wouldn't want to lose funds + if self.inner.cancellation_token.is_cancelled() { + info!("received cancellation during deposits refilling"); + return Ok(()); + } + + // make sure to insert deposits into db/vec as we get them so on initial run, + // we'd start trickling down data as soon as possible + let deposits = self.make_deposits_request(request_chunk).await?; + self.insert_new_deposits(deposits).await?; + } + + Ok(()) + } + + // if we're here, we know we're below the threshold + fn maybe_refill_deposits(&self) { + if let Some(mut guard) = self.inner.deposits_refill_task.try_get_new_task_guard() { + let this = self.clone(); + *guard = Some(tokio::spawn(async move { this.refill_deposits().await })); + } + } + + fn deposits_lower_threshold(&self) -> usize { + self.inner.target_amount - (self.inner.target_amount as f32 * DEPOSITS_THRESHOLD_P) as usize + } + + fn deposits_upper_threshold(&self) -> usize { + self.inner.target_amount + (self.inner.target_amount as f32 * DEPOSITS_THRESHOLD_P) as usize + } + + async fn mark_deposit_as_used( + &self, + deposit_id: DepositId, + requested_on: OffsetDateTime, + client_pubkey: PublicKeyUser, + request_uuid: Uuid, + ) -> Result<(), CredentialProxyError> { + self.inner + .storage + .insert_deposit_usage(deposit_id, requested_on, client_pubkey, request_uuid) + .await + } + + async fn wait_for_deposit( + &self, + request_uuid: Uuid, + requested_on: OffsetDateTime, + client_pubkey: PublicKeyUser, + ) -> Result { + loop { + tokio::time::sleep(Duration::from_millis(500)).await; + if let Some(buffered_deposit) = self.inner.unused_deposits.lock().await.pop() { + // if the db call fails, we technically don't lose the deposit (we'll 'recover' it on restart) + self.mark_deposit_as_used( + buffered_deposit.deposit_id, + requested_on, + client_pubkey, + request_uuid, + ) + .await?; + return Ok(buffered_deposit); + } else { + // make sure there's always a task working in the background in case deposits get used up too quickly + self.maybe_refill_deposits() + } + } + } + + pub async fn get_valid_deposit( + &self, + request_uuid: Uuid, + requested_on: OffsetDateTime, + client_pubkey: PublicKeyUser, + ) -> Result { + let mut deposits_guard = self.inner.unused_deposits.lock().await; + let deposits_available = deposits_guard.len(); + + debug!("we have {deposits_available} unused deposits available"); + + let maybe_deposit = deposits_guard.pop(); + drop(deposits_guard); + + if deposits_available < self.deposits_lower_threshold() { + // if we're below threshold, start refill task + self.maybe_refill_deposits() + } + + match maybe_deposit { + None => { + warn!("we currently don't have any usable deposits! are we using them up faster than we request them?"); + + // we have to wait until refill task has completed (either initiated by this or another fn call) + self.wait_for_deposit(request_uuid, requested_on, client_pubkey) + .await + } + Some(buffered_deposit) => { + self.mark_deposit_as_used( + buffered_deposit.deposit_id, + requested_on, + client_pubkey, + request_uuid, + ) + .await?; + Ok(buffered_deposit) + } + } + } + + pub async fn wait_for_shutdown(&self) { + let task_handle = self.inner.deposits_refill_task.take_task_join_handle(); + if let Some(task_handle) = task_handle { + if !task_handle.is_finished() { + info!("the deposit refill task is currently in progress - waiting for the current transaction to finish before concluding shutdown"); + let _ = task_handle.await; + } + } + } +} + +impl DepositsBufferInner { + // +} diff --git a/common/credential-proxy/src/deposits_buffer/refill_task.rs b/common/credential-proxy/src/deposits_buffer/refill_task.rs new file mode 100644 index 0000000000..4f09df55b0 --- /dev/null +++ b/common/credential-proxy/src/deposits_buffer/refill_task.rs @@ -0,0 +1,56 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::error::CredentialProxyError; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Mutex as StdMutex, MutexGuard}; +use tokio::task::JoinHandle; +use tracing::{debug, error}; + +pub(super) type RefillTaskResult = Result<(), CredentialProxyError>; + +#[derive(Default)] +pub(super) struct RefillTask { + // note that we can only have a single transaction in progress (or it'd mess up with our sequence numbers) + // if we find that we're using up deposits more quickly than we're refilling them, + // we'll have to increase the number of deposits per transaction + join_handle: StdMutex>>, + + in_progress: AtomicBool, +} + +impl RefillTask { + /// Attempt to set the `in_progress` value to `true` if it's not already `true`. + /// Returns boolean indicating whether it was successful + fn try_set_in_progress(&self) -> bool { + self.in_progress + .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst) + .is_ok() + } + + pub(super) fn try_get_new_task_guard( + &self, + ) -> Option>>> { + // sanity check for concurrent request + if !self.try_set_in_progress() { + debug!("another task has already started deposit refill request"); + return None; + } + + #[allow(clippy::expect_used)] + let guard = self.join_handle.lock().expect("mutex got poisoned"); + + if let Some(existing_handle) = guard.as_ref() { + if !existing_handle.is_finished() { + error!("CRITICAL BUG: there was already a deposit refill task spawned that hasn't yet finished") + } + } + + Some(guard) + } + + pub(super) fn take_task_join_handle(&self) -> Option> { + #[allow(clippy::expect_used)] + self.join_handle.lock().expect("mutex got poisoned").take() + } +} diff --git a/nym-credential-proxy/nym-credential-proxy/src/error.rs b/common/credential-proxy/src/error.rs similarity index 70% rename from nym-credential-proxy/nym-credential-proxy/src/error.rs rename to common/credential-proxy/src/error.rs index 56e18a0673..d50264cdc7 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/error.rs +++ b/common/credential-proxy/src/error.rs @@ -1,8 +1,9 @@ -// Copyright 2024 Nym Technologies SA +// Copyright 2025 Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only +use nym_ecash_signer_check::SignerCheckError; use nym_validator_client::coconut::EcashApiError; -use nym_validator_client::nym_api::EpochId; +use nym_validator_client::nym_api::{error::NymAPIError, EpochId}; use nym_validator_client::nyxd::error::NyxdError; use std::io; use std::net::SocketAddr; @@ -10,7 +11,7 @@ use thiserror::Error; use time::OffsetDateTime; #[derive(Debug, Error)] -pub enum VpnApiError { +pub enum CredentialProxyError { #[error("encountered an internal io error: {source}")] IoError { #[from] @@ -70,6 +71,9 @@ pub enum VpnApiError { source: EcashApiError, }, + #[error("Nym API request failed: {source}")] + NymApiFailure { source: Box }, + #[error("Compact ecash internal error: {0}")] CompactEcashInternalError(#[from] nym_compact_ecash::error::CompactEcashError), @@ -118,11 +122,52 @@ pub enum VpnApiError { #[error("failed to create deposit")] DepositFailure, + + #[error("can't obtain sufficient number of credential shares due to unavailable quorum")] + UnavailableSigningQuorum, + + #[error("failed to perform quorum check: {source}")] + QuorumCheckFailure { + #[from] + source: SignerCheckError, + }, + + #[error( + "this operation couldn't be completed as the program is in the process of shutting down" + )] + ShutdownInProgress, + + #[error("failed to obtain wallet shares with id {id}: {message}")] + ShareByIdLoadError { message: String, id: i64 }, + + #[error("failed to obtain wallet shares with device_id {device_id} and credential_id: {credential_id}: {message}")] + ShareByDeviceLoadError { + message: String, + device_id: String, + credential_id: String, + }, + + #[error("could not find shares with id {id}")] + SharesByIdNotFound { id: i64 }, + + #[error("could not find shares with device_id {device_id} and credential_id: {credential_id}")] + SharesByDeviceNotFound { + device_id: String, + credential_id: String, + }, } -impl VpnApiError { - pub fn database_inconsistency>(reason: S) -> VpnApiError { - VpnApiError::DatabaseInconsistency { +impl From for CredentialProxyError { + fn from(source: NymAPIError) -> Self { + CredentialProxyError::NymApiFailure { + source: Box::new(source), + } + } +} + +impl CredentialProxyError { + pub fn database_inconsistency>(reason: S) -> CredentialProxyError { + CredentialProxyError::DatabaseInconsistency { reason: reason.into(), } } diff --git a/common/credential-proxy/src/helpers.rs b/common/credential-proxy/src/helpers.rs new file mode 100644 index 0000000000..8f45ea99e4 --- /dev/null +++ b/common/credential-proxy/src/helpers.rs @@ -0,0 +1,67 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use rand::rngs::OsRng; +use rand::RngCore; +use time::OffsetDateTime; +use tracing::{debug, info, warn}; +use uuid::Uuid; + +pub fn random_uuid() -> Uuid { + let mut bytes = [0u8; 16]; + let mut rng = OsRng; + rng.fill_bytes(&mut bytes); + Uuid::from_bytes(bytes) +} + +pub struct LockTimer { + created: OffsetDateTime, + message: String, +} + +impl LockTimer { + pub fn new>(message: S) -> Self { + LockTimer { + message: message.into(), + ..Default::default() + } + } +} + +impl Drop for LockTimer { + fn drop(&mut self) { + let time_taken = OffsetDateTime::now_utc() - self.created; + let time_taken_formatted = humantime::format_duration(time_taken.unsigned_abs()); + if time_taken > time::Duration::SECOND * 10 { + warn!(time_taken = %time_taken_formatted, "{}", self.message) + } else if time_taken > time::Duration::SECOND * 5 { + info!(time_taken = %time_taken_formatted, "{}", self.message) + } else { + debug!(time_taken = %time_taken_formatted, "{}", self.message) + }; + } +} + +impl Default for LockTimer { + fn default() -> Self { + LockTimer { + created: OffsetDateTime::now_utc(), + message: "released the lock".to_string(), + } + } +} + +// #[allow(clippy::panic)] +// 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] +// } diff --git a/nym-credential-proxy/nym-credential-proxy/src/http/types.rs b/common/credential-proxy/src/http_helpers.rs similarity index 68% rename from nym-credential-proxy/nym-credential-proxy/src/http/types.rs rename to common/credential-proxy/src/http_helpers.rs index 6538c5a9a7..129fcfa86b 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/http/types.rs +++ b/common/credential-proxy/src/http_helpers.rs @@ -1,11 +1,12 @@ -// Copyright 2024 Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 -use crate::error::VpnApiError; +use crate::error::CredentialProxyError; use axum::http::StatusCode; use axum::response::{IntoResponse, Response}; use axum::Json; use nym_credential_proxy_requests::api::v1::ErrorResponse; +use tracing::warn; use uuid::Uuid; #[derive(Debug, Clone)] @@ -35,7 +36,11 @@ impl RequestError { } } - pub fn new_server_error(err: VpnApiError, uuid: Uuid) -> Self { + pub fn new_plain_error(err: CredentialProxyError) -> Self { + Self::from_err(err, StatusCode::INTERNAL_SERVER_ERROR) + } + + pub fn new_server_error(err: CredentialProxyError, uuid: Uuid) -> Self { RequestError::new_with_uuid(err.to_string(), uuid, StatusCode::INTERNAL_SERVER_ERROR) } @@ -59,3 +64,12 @@ impl IntoResponse for RequestError { (self.status, Json(self.inner)).into_response() } } + +pub fn db_failure(err: CredentialProxyError, uuid: Uuid) -> Result { + warn!("db failure: {err}"); + Err(RequestError::new_with_uuid( + format!("oh no, something went wrong {err}"), + uuid, + StatusCode::INTERNAL_SERVER_ERROR, + )) +} diff --git a/common/credential-proxy/src/lib.rs b/common/credential-proxy/src/lib.rs new file mode 100644 index 0000000000..fedd68dfb4 --- /dev/null +++ b/common/credential-proxy/src/lib.rs @@ -0,0 +1,13 @@ +// Copyright 2025 Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +pub mod deposits_buffer; +pub mod error; +pub mod helpers; +pub mod http_helpers; +pub mod nym_api_helpers; +pub mod quorum_checker; +pub mod shared_state; +pub mod storage; +pub mod ticketbook_manager; +pub mod webhook; diff --git a/nym-credential-proxy/nym-credential-proxy/src/nym_api_helpers.rs b/common/credential-proxy/src/nym_api_helpers.rs similarity index 84% rename from nym-credential-proxy/nym-credential-proxy/src/nym_api_helpers.rs rename to common/credential-proxy/src/nym_api_helpers.rs index 72d0e2a467..c762b33f98 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/nym_api_helpers.rs +++ b/common/credential-proxy/src/nym_api_helpers.rs @@ -4,7 +4,7 @@ // TODO: this was just copied from nym-api; // it should have been therefore extracted to a common crate instead and imported as dependency -use crate::error::VpnApiError; +use crate::error::CredentialProxyError; use futures::{stream, StreamExt}; use nym_credentials::ecash::utils::{cred_exp_date, ecash_today, EcashTime}; use nym_validator_client::nym_api::EpochId; @@ -19,9 +19,9 @@ use time::{Date, OffsetDateTime}; use tokio::sync::{Mutex, RwLock, RwLockReadGuard}; use tracing::warn; -pub(crate) struct CachedEpoch { +pub struct CachedEpoch { valid_until: OffsetDateTime, - pub(crate) current_epoch: Epoch, + pub current_epoch: Epoch, } impl Default for CachedEpoch { @@ -34,11 +34,11 @@ impl Default for CachedEpoch { } impl CachedEpoch { - pub(crate) fn is_valid(&self) -> bool { + pub fn is_valid(&self) -> bool { self.valid_until > OffsetDateTime::now_utc() } - pub(crate) fn update(&mut self, epoch: Epoch) { + pub fn update(&mut self, epoch: Epoch) { let now = OffsetDateTime::now_utc(); let validity_duration = if let Some(epoch_finish) = epoch.deadline { @@ -58,13 +58,13 @@ impl CachedEpoch { } // a map of items that never change for given key -pub(crate) struct CachedImmutableItems { +pub struct CachedImmutableItems { // I wonder if there's a more efficient structure with OnceLock or OnceCell or something inner: RwLock>, } // an item that stays constant throughout given epoch -pub(crate) type CachedImmutableEpochItem = CachedImmutableItems; +pub type CachedImmutableEpochItem = CachedImmutableItems; impl Default for CachedImmutableItems { fn default() -> Self { @@ -86,11 +86,7 @@ impl CachedImmutableItems where K: Eq + Hash, { - pub(crate) async fn get_or_init( - &self, - key: K, - f: F, - ) -> Result, E> + pub async fn get_or_init(&self, key: K, f: F) -> Result, E> where F: FnOnce() -> U, U: Future>, @@ -129,29 +125,29 @@ where } } -pub(crate) fn ensure_sane_expiration_date(expiration_date: Date) -> Result<(), VpnApiError> { +pub fn ensure_sane_expiration_date(expiration_date: Date) -> Result<(), CredentialProxyError> { let today = ecash_today(); if expiration_date < today.date() { // what's the point of signatures with expiration in the past? - return Err(VpnApiError::ExpirationDateTooEarly); + return Err(CredentialProxyError::ExpirationDateTooEarly); } if expiration_date > cred_exp_date().ecash_date() { - return Err(VpnApiError::ExpirationDateTooLate); + return Err(CredentialProxyError::ExpirationDateTooLate); } Ok(()) } -pub(crate) async fn query_all_threshold_apis( +pub async fn query_all_threshold_apis( all_apis: Vec, threshold: u64, f: F, -) -> Result, VpnApiError> +) -> Result, CredentialProxyError> where F: Fn(EcashApiClient) -> U, - U: Future>, + U: Future>, { let shares = Mutex::new(Vec::with_capacity(all_apis.len())); @@ -172,7 +168,7 @@ where let shares = shares.into_inner(); if shares.len() < threshold as usize { - return Err(VpnApiError::InsufficientNumberOfSigners { + return Err(CredentialProxyError::InsufficientNumberOfSigners { threshold, available: shares.len(), }); diff --git a/common/credential-proxy/src/quorum_checker.rs b/common/credential-proxy/src/quorum_checker.rs new file mode 100644 index 0000000000..6940a90749 --- /dev/null +++ b/common/credential-proxy/src/quorum_checker.rs @@ -0,0 +1,102 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::error::CredentialProxyError; +use crate::shared_state::nyxd_client::ChainClient; +use nym_ecash_signer_check::{check_known_dealers, dkg_details_with_client}; +use std::ops::Deref; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; +use std::time::Duration; +use tokio_util::sync::CancellationToken; +use tracing::{error, info, warn}; + +#[derive(Clone)] +pub struct QuorumState { + available: Arc, +} + +impl QuorumState { + pub fn available(&self) -> bool { + self.available.load(Ordering::Acquire) + } +} + +pub struct QuorumStateChecker { + client: ChainClient, + cancellation_token: CancellationToken, + check_interval: Duration, + quorum_state: QuorumState, +} + +impl QuorumStateChecker { + pub async fn new( + client: ChainClient, + check_interval: Duration, + cancellation_token: CancellationToken, + ) -> Result { + let this = QuorumStateChecker { + client, + cancellation_token, + check_interval, + quorum_state: QuorumState { + available: Arc::new(Default::default()), + }, + }; + + // first check MUST succeed, otherwise we shouldn't start + let quorum_available = this.check_quorum_state().await?; + this.quorum_state + .available + .store(quorum_available, Ordering::Relaxed); + Ok(this) + } + + pub fn quorum_state_ref(&self) -> QuorumState { + self.quorum_state.clone() + } + + async fn check_quorum_state(&self) -> Result { + let client_guard = self.client.query_chain().await; + + // split the operation as we only need to hold the reference to chain client for the first part + // and the second half doesn't rely on it (and takes way longer) + let dkg_details = dkg_details_with_client(client_guard.deref()).await?; + drop(client_guard); + + let res = check_known_dealers(dkg_details).await?; + + let Some(signing_threshold) = res.threshold else { + warn!("signing threshold is currently unavailable and we have not yet implemented credential issuance during DKG transition"); + return Ok(false); + }; + + let mut working_issuer = 0; + + for result in res.results { + if result.chain_available() && result.signing_available() { + working_issuer += 1; + } + } + + Ok((working_issuer as u64) >= signing_threshold) + } + + pub async fn run_forever(self) { + info!("starting quorum state checker"); + loop { + tokio::select! { + biased; + _ = self.cancellation_token.cancelled() => { + break + } + _ = tokio::time::sleep(self.check_interval) => { + match self.check_quorum_state().await { + Ok(available) => self.quorum_state.available.store(available, Ordering::SeqCst), + Err(err) => error!("failed to check current quorum state: {err}"), + } + } + } + } + } +} diff --git a/common/credential-proxy/src/shared_state/ecash_state.rs b/common/credential-proxy/src/shared_state/ecash_state.rs new file mode 100644 index 0000000000..011643216f --- /dev/null +++ b/common/credential-proxy/src/shared_state/ecash_state.rs @@ -0,0 +1,49 @@ +// Copyright 2025 Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::nym_api_helpers::{CachedEpoch, CachedImmutableEpochItem, CachedImmutableItems}; +use crate::quorum_checker::QuorumState; +use crate::shared_state::required_deposit_cache::RequiredDepositCache; +use nym_compact_ecash::VerificationKeyAuth; +use nym_credentials::{AggregatedCoinIndicesSignatures, AggregatedExpirationDateSignatures}; +use nym_validator_client::nym_api::EpochId; +use nym_validator_client::EcashApiClient; +use time::Date; +use tokio::sync::RwLock; + +pub struct EcashState { + pub required_deposit_cache: RequiredDepositCache, + + pub quorum_state: QuorumState, + + pub cached_epoch: RwLock, + + pub master_verification_key: CachedImmutableEpochItem, + + pub threshold_values: CachedImmutableEpochItem, + + pub epoch_clients: CachedImmutableEpochItem>, + + pub coin_index_signatures: CachedImmutableEpochItem, + + pub expiration_date_signatures: + CachedImmutableItems<(EpochId, Date), AggregatedExpirationDateSignatures>, +} + +impl EcashState { + pub fn new( + required_deposit_cache: RequiredDepositCache, + quorum_state: QuorumState, + ) -> EcashState { + EcashState { + required_deposit_cache, + quorum_state, + cached_epoch: Default::default(), + master_verification_key: Default::default(), + threshold_values: Default::default(), + epoch_clients: Default::default(), + coin_index_signatures: Default::default(), + expiration_date_signatures: Default::default(), + } + } +} diff --git a/common/credential-proxy/src/shared_state/mod.rs b/common/credential-proxy/src/shared_state/mod.rs new file mode 100644 index 0000000000..750b199d52 --- /dev/null +++ b/common/credential-proxy/src/shared_state/mod.rs @@ -0,0 +1,496 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::deposits_buffer::{BufferedDeposit, DepositsBuffer}; +use crate::error::CredentialProxyError; +use crate::nym_api_helpers::{ensure_sane_expiration_date, query_all_threshold_apis}; +use crate::shared_state::ecash_state::EcashState; +use crate::shared_state::nyxd_client::ChainClient; +use crate::storage::CredentialProxyStorage; +use nym_compact_ecash::scheme::coin_indices_signatures::{ + aggregate_annotated_indices_signatures, CoinIndexSignatureShare, +}; +use nym_compact_ecash::scheme::expiration_date_signatures::{ + aggregate_annotated_expiration_signatures, ExpirationDateSignatureShare, +}; +use nym_compact_ecash::{Base58, PublicKeyUser, VerificationKeyAuth}; +use nym_credential_proxy_requests::api::v1::ticketbook::models::{ + AggregatedCoinIndicesSignaturesResponse, AggregatedExpirationDateSignaturesResponse, + GlobalDataParams, MasterVerificationKeyResponse, +}; +use nym_credentials::ecash::utils::EcashTime; +use nym_credentials::{ + AggregatedCoinIndicesSignatures, AggregatedExpirationDateSignatures, EpochVerificationKey, +}; +use nym_ecash_contract_common::deposit::DepositId; +use nym_validator_client::client::NymApiClientExt; +use nym_validator_client::coconut::EcashApiError; +use nym_validator_client::nym_api::EpochId; +use nym_validator_client::nyxd::contract_traits::dkg_query_client::Epoch; +use nym_validator_client::nyxd::contract_traits::{DkgQueryClient, PagedDkgQueryClient}; +use nym_validator_client::nyxd::Coin; +use nym_validator_client::{DirectSigningHttpRpcNyxdClient, EcashApiClient}; +use std::sync::Arc; +use std::time::Duration; +use time::{Date, OffsetDateTime}; +use tokio::sync::RwLockReadGuard; +use tokio::time::Instant; +use tracing::{debug, error, info, warn}; +use uuid::Uuid; + +pub mod ecash_state; +pub mod nyxd_client; +pub mod required_deposit_cache; + +#[derive(Clone)] +pub struct CredentialProxyState { + inner: Arc, +} + +impl CredentialProxyState { + pub fn new( + storage: CredentialProxyStorage, + client: ChainClient, + deposits_buffer: DepositsBuffer, + ecash_state: EcashState, + ) -> Self { + CredentialProxyState { + inner: Arc::new(CredentialProxyStateInner { + storage, + client, + deposits_buffer, + ecash_state, + }), + } + } + + pub fn storage(&self) -> &CredentialProxyStorage { + &self.inner.storage + } + + pub async fn deposit_amount(&self) -> Result { + self.ecash_state() + .required_deposit_cache + .get_or_update(self.client()) + .await + } + + pub fn client(&self) -> &ChainClient { + &self.inner.client + } + + pub fn deposits_buffer(&self) -> &DepositsBuffer { + &self.inner.deposits_buffer + } + + pub fn ecash_state(&self) -> &EcashState { + &self.inner.ecash_state + } + + pub(crate) async fn query_chain(&self) -> RwLockReadGuard<'_, DirectSigningHttpRpcNyxdClient> { + self.inner.client.query_chain().await + } + + pub async fn ensure_credentials_issuable(&self) -> Result<(), CredentialProxyError> { + let epoch = self.current_epoch().await?; + + if epoch.state.is_final() { + Ok(()) + } else if let Some(final_timestamp) = epoch.final_timestamp_secs() { + // SAFETY: the timestamp values in our DKG contract should be valid timestamps, + // otherwise it means the chain is seriously misbehaving + #[allow(clippy::unwrap_used)] + let finish_dt = OffsetDateTime::from_unix_timestamp(final_timestamp as i64).unwrap(); + + Err(CredentialProxyError::CredentialsNotYetIssuable { + availability: finish_dt, + }) + } else if epoch.state.is_waiting_initialisation() { + Err(CredentialProxyError::UninitialisedDkg) + } else { + Err(CredentialProxyError::UnknownEcashFailure) + } + } + + pub async fn get_deposit( + &self, + request_uuid: Uuid, + requested_on: OffsetDateTime, + client_pubkey: PublicKeyUser, + ) -> Result { + let start = Instant::now(); + let deposit = self + .deposits_buffer() + .get_valid_deposit(request_uuid, requested_on, client_pubkey) + .await; + + let time_taken = start.elapsed(); + let formatted = humantime::format_duration(time_taken); + if time_taken > Duration::from_secs(10) { + warn!("attempting to get buffered deposit took {formatted}. perhaps the buffer is too small or the process/chain is overloaded?") + } else { + debug!("attempting to get buffered deposit took {formatted}") + }; + + deposit + } + + pub async fn insert_deposit_usage_error(&self, deposit_id: DepositId, error: String) { + if let Err(err) = self + .storage() + .insert_deposit_usage_error(deposit_id, error) + .await + { + error!("failed to insert information about deposit (id: {deposit_id}) usage failure: {err}") + } + } + + pub async fn current_epoch_id(&self) -> Result { + let read_guard = self.inner.ecash_state.cached_epoch.read().await; + if read_guard.is_valid() { + return Ok(read_guard.current_epoch.epoch_id); + } + + // update cache + drop(read_guard); + let mut write_guard = self.inner.ecash_state.cached_epoch.write().await; + let epoch = self.query_chain().await.get_current_epoch().await?; + + write_guard.update(epoch); + Ok(epoch.epoch_id) + } + + pub async fn current_epoch(&self) -> Result { + let read_guard = self.ecash_state().cached_epoch.read().await; + if read_guard.is_valid() { + return Ok(read_guard.current_epoch); + } + + // update cache + drop(read_guard); + let mut write_guard = self.ecash_state().cached_epoch.write().await; + let epoch = self.query_chain().await.get_current_epoch().await?; + + write_guard.update(epoch); + Ok(epoch) + } + + pub async fn global_data( + &self, + global_data: GlobalDataParams, + epoch_id: EpochId, + expiration_date: Date, + ) -> Result< + ( + Option, + Option, + Option, + ), + CredentialProxyError, + > { + let master_verification_key = if global_data.include_master_verification_key { + debug!("including master verification key in the response"); + Some( + self.master_verification_key(Some(epoch_id)) + .await + .map(|key| MasterVerificationKeyResponse { + epoch_id, + bs58_encoded_key: key.to_bs58(), + }) + .inspect_err(|err| warn!("request failure: {err}"))?, + ) + } else { + None + }; + + let aggregated_expiration_date_signatures = + if global_data.include_expiration_date_signatures { + debug!("including expiration date signatures in the response"); + Some( + self.master_expiration_date_signatures(epoch_id, expiration_date) + .await + .map(|signatures| AggregatedExpirationDateSignaturesResponse { + signatures: signatures.clone(), + }) + .inspect_err(|err| warn!("request failure: {err}"))?, + ) + } else { + None + }; + + let aggregated_coin_index_signatures = if global_data.include_coin_index_signatures { + debug!("including coin index signatures in the response"); + Some( + self.master_coin_index_signatures(Some(epoch_id)) + .await + .map(|signatures| AggregatedCoinIndicesSignaturesResponse { + signatures: signatures.clone(), + }) + .inspect_err(|err| warn!("request failure: {err}"))?, + ) + } else { + None + }; + + Ok(( + master_verification_key, + aggregated_expiration_date_signatures, + aggregated_coin_index_signatures, + )) + } + + pub async fn master_verification_key( + &self, + epoch_id: Option, + ) -> Result, CredentialProxyError> { + let epoch_id = match epoch_id { + Some(id) => id, + None => self.current_epoch_id().await?, + }; + + self.inner + .ecash_state + .master_verification_key + .get_or_init(epoch_id, || async { + // 1. check the storage + if let Some(stored) = self + .inner + .storage + .get_master_verification_key(epoch_id) + .await? + { + return Ok(stored.key); + } + + info!("attempting to establish master verification key for epoch {epoch_id}..."); + + // 2. perform actual aggregation + let all_apis = self.ecash_clients(epoch_id).await?; + let threshold = self.ecash_threshold(epoch_id).await?; + + if all_apis.len() < threshold as usize { + return Err(CredentialProxyError::InsufficientNumberOfSigners { + threshold, + available: all_apis.len(), + }); + } + + let master_key = nym_credentials::aggregate_verification_keys(&all_apis)?; + + let epoch = EpochVerificationKey { + epoch_id, + key: master_key, + }; + + // 3. save the key in the storage for when we reboot + self.inner + .storage + .insert_master_verification_key(&epoch) + .await?; + + Ok(epoch.key) + }) + .await + } + + pub async fn master_coin_index_signatures( + &self, + epoch_id: Option, + ) -> Result, CredentialProxyError> { + let epoch_id = match epoch_id { + Some(id) => id, + None => self.current_epoch_id().await?, + }; + + self.inner + .ecash_state + .coin_index_signatures + .get_or_init(epoch_id, || async { + // 1. check the storage + if let Some(master_sigs) = self + .inner + .storage + .get_master_coin_index_signatures(epoch_id) + .await? + { + return Ok(master_sigs); + } + + info!( + "attempting to establish master coin index signatures for epoch {epoch_id}..." + ); + + // 2. go around APIs and attempt to aggregate the data + let master_vk = self.master_verification_key(Some(epoch_id)).await?; + let all_apis = self.ecash_clients(epoch_id).await?; + let threshold = self.ecash_threshold(epoch_id).await?; + + let get_partial_signatures = |api: EcashApiClient| async { + // move the api into the closure + let api = api; + let node_index = api.node_id; + let partial_vk = api.verification_key; + + let partial = api + .api_client + .partial_coin_indices_signatures(Some(epoch_id)) + .await? + .signatures; + Ok(CoinIndexSignatureShare { + index: node_index, + key: partial_vk, + signatures: partial, + }) + }; + + let shares = + query_all_threshold_apis(all_apis.clone(), threshold, get_partial_signatures) + .await?; + + let aggregated = aggregate_annotated_indices_signatures( + nym_credentials_interface::ecash_parameters(), + &master_vk, + &shares, + )?; + + let sigs = AggregatedCoinIndicesSignatures { + epoch_id, + signatures: aggregated, + }; + + // 3. save the signatures in the storage for when we reboot + self.inner + .storage + .insert_master_coin_index_signatures(&sigs) + .await?; + + Ok(sigs) + }) + .await + } + + pub async fn master_expiration_date_signatures( + &self, + epoch_id: EpochId, + expiration_date: Date, + ) -> Result, CredentialProxyError> { + self.inner.ecash_state + .expiration_date_signatures + .get_or_init((epoch_id, expiration_date), || async { + // 1. sanity check to see if the expiration_date is not nonsense + ensure_sane_expiration_date(expiration_date)?; + + // 2. check the storage + if let Some(master_sigs) = self + .storage() + .get_master_expiration_date_signatures(expiration_date, epoch_id) + .await? + { + return Ok(master_sigs); + } + + + info!( + "attempting to establish master expiration date signatures for {expiration_date} and epoch {epoch_id}..." + ); + + // 3. go around APIs and attempt to aggregate the data + let epoch_id = self.current_epoch_id().await?; + let master_vk = self.master_verification_key(Some(epoch_id)).await?; + let all_apis = self.ecash_clients(epoch_id).await?; + let threshold = self.ecash_threshold(epoch_id).await?; + + let get_partial_signatures = |api: EcashApiClient| async { + // move the api into the closure + let api = api; + let node_index = api.node_id; + let partial_vk = api.verification_key; + + let partial = api + .api_client + .partial_expiration_date_signatures(Some(expiration_date), Some(epoch_id)) + .await? + .signatures; + Ok(ExpirationDateSignatureShare { + index: node_index, + key: partial_vk, + signatures: partial, + }) + }; + + let shares = + query_all_threshold_apis(all_apis.clone(), threshold, get_partial_signatures) + .await?; + + let aggregated = aggregate_annotated_expiration_signatures( + &master_vk, + expiration_date.ecash_unix_timestamp(), + &shares, + )?; + + let sigs = AggregatedExpirationDateSignatures { + epoch_id, + expiration_date, + signatures: aggregated, + }; + + // 4. save the signatures in the storage for when we reboot + self.inner.storage + .insert_master_expiration_date_signatures(&sigs) + .await?; + + Ok(sigs) + }) + .await + } + + pub async fn ecash_clients( + &self, + epoch_id: EpochId, + ) -> Result>, CredentialProxyError> { + self.inner + .ecash_state + .epoch_clients + .get_or_init(epoch_id, || async { + Ok(self + .query_chain() + .await + .get_all_verification_key_shares(epoch_id) + .await? + .into_iter() + .map(TryInto::try_into) + .collect::, EcashApiError>>()?) + }) + .await + } + + pub async fn ecash_threshold(&self, epoch_id: EpochId) -> Result { + self.inner + .ecash_state + .threshold_values + .get_or_init(epoch_id, || async { + if let Some(threshold) = self + .query_chain() + .await + .get_epoch_threshold(epoch_id) + .await? + { + Ok(threshold) + } else { + Err(CredentialProxyError::UnavailableThreshold { epoch_id }) + } + }) + .await + .map(|t| *t) + } +} + +struct CredentialProxyStateInner { + storage: CredentialProxyStorage, + + client: ChainClient, + + deposits_buffer: DepositsBuffer, + + ecash_state: EcashState, +} diff --git a/common/credential-proxy/src/shared_state/nyxd_client.rs b/common/credential-proxy/src/shared_state/nyxd_client.rs new file mode 100644 index 0000000000..fee9d4333e --- /dev/null +++ b/common/credential-proxy/src/shared_state/nyxd_client.rs @@ -0,0 +1,126 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::error::CredentialProxyError; +use crate::helpers::LockTimer; +use nym_ecash_contract_common::msg::ExecuteMsg; +use nym_validator_client::nyxd::contract_traits::NymContractsProvider; +use nym_validator_client::nyxd::cosmwasm_client::types::ExecuteResult; +use nym_validator_client::nyxd::{Coin, CosmWasmClient, NyxdClient}; +use nym_validator_client::{nyxd, DirectSigningHttpRpcNyxdClient}; +use std::ops::Deref; +use std::sync::Arc; +use std::time::Duration; +use tokio::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard}; +use tracing::{instrument, warn}; + +#[derive(Clone)] +pub struct ChainClient(Arc>); + +impl ChainClient { + pub fn new(mnemonic: bip39::Mnemonic) -> Result { + 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(|| CredentialProxyError::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(CredentialProxyError::UnavailableEcashContract); + } + + if client.dkg_contract_address().is_none() { + return Err(CredentialProxyError::UnavailableDKGContract); + } + + Ok(ChainClient(Arc::new(RwLock::new(client)))) + } + + pub async fn query_chain(&self) -> ChainReadPermit<'_> { + let _acquire_timer = LockTimer::new("acquire chain query permit"); + self.0.read().await + } + + pub 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, + } + } +} + +pub type ChainReadPermit<'a> = RwLockReadGuard<'a, DirectSigningHttpRpcNyxdClient>; + +// explicitly wrap the WriteGuard for extra information regarding time taken +pub struct ChainWritePermit<'a> { + // it's not really dead, we only care about it being dropped + #[allow(dead_code)] + lock_timer: LockTimer, + inner: RwLockWriteGuard<'a, DirectSigningHttpRpcNyxdClient>, +} + +impl ChainWritePermit<'_> { + #[instrument(skip(self, short_sha, info), err(Display))] + pub async fn make_deposits( + self, + short_sha: &'static str, + info: Vec<(String, Coin)>, + ) -> Result { + 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(CredentialProxyError::UnavailableEcashContract)?; + let deposit_messages = info + .into_iter() + .map(|(identity_key, amount)| { + ( + ExecuteMsg::DepositTicketBookFunds { identity_key }, + vec![amount], + ) + }) + .collect::>(); + + 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 { + self.inner.deref() + } +} diff --git a/common/credential-proxy/src/shared_state/required_deposit_cache.rs b/common/credential-proxy/src/shared_state/required_deposit_cache.rs new file mode 100644 index 0000000000..9b45f5da4b --- /dev/null +++ b/common/credential-proxy/src/shared_state/required_deposit_cache.rs @@ -0,0 +1,71 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::error::CredentialProxyError; +use crate::shared_state::nyxd_client::ChainClient; +use nym_validator_client::nyxd::contract_traits::EcashQueryClient; +use nym_validator_client::nyxd::Coin; +use std::sync::Arc; +use time::OffsetDateTime; +use tokio::sync::RwLock; + +pub struct CachedDeposit { + valid_until: OffsetDateTime, + required_amount: Coin, +} + +impl CachedDeposit { + const MAX_VALIDITY: time::Duration = time::Duration::MINUTE; + + fn is_valid(&self) -> bool { + self.valid_until > OffsetDateTime::now_utc() + } + + fn update(&mut self, required_amount: Coin) { + self.valid_until = OffsetDateTime::now_utc() + Self::MAX_VALIDITY; + self.required_amount = required_amount; + } +} + +impl Default for CachedDeposit { + fn default() -> Self { + CachedDeposit { + valid_until: OffsetDateTime::UNIX_EPOCH, + required_amount: Coin { + amount: u128::MAX, + denom: "unym".to_string(), + }, + } + } +} + +#[derive(Clone, Default)] +pub struct RequiredDepositCache { + inner: Arc>, +} + +impl RequiredDepositCache { + pub async fn get_or_update( + &self, + chain_client: &ChainClient, + ) -> Result { + let read_guard = self.inner.read().await; + if read_guard.is_valid() { + return Ok(read_guard.required_amount.clone()); + } + + // update cache + drop(read_guard); + let mut write_guard = self.inner.write().await; + let deposit_amount = chain_client + .query_chain() + .await + .get_required_deposit_amount() + .await?; + + let nym_coin: Coin = deposit_amount.into(); + + write_guard.update(nym_coin.clone()); + Ok(nym_coin) + } +} diff --git a/nym-credential-proxy/nym-credential-proxy/src/storage/manager.rs b/common/credential-proxy/src/storage/manager.rs similarity index 81% rename from nym-credential-proxy/nym-credential-proxy/src/storage/manager.rs rename to common/credential-proxy/src/storage/manager.rs index b5fefae449..f72341cd08 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/storage/manager.rs +++ b/common/credential-proxy/src/storage/manager.rs @@ -1,13 +1,13 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::error::VpnApiError; use crate::storage::models::{ BlindedShares, BlindedSharesStatus, MinimalWalletShare, RawCoinIndexSignatures, - RawExpirationDateSignatures, RawVerificationKey, + RawExpirationDateSignatures, RawVerificationKey, StorableEcashDeposit, }; use nym_validator_client::nyxd::contract_traits::ecash_query_client::DepositId; use time::{Date, OffsetDateTime}; +use tracing::error; #[derive(Clone)] pub(crate) struct SqliteStorageManager { @@ -42,7 +42,7 @@ impl SqliteStorageManager { r#" SELECT t1.node_id, t1.blinded_signature, t1.epoch_id, t1.expiration_date as "expiration_date!: Date" FROM partial_blinded_wallet as t1 - JOIN ticketbook_deposit as t2 + JOIN ecash_deposit_usage as t2 on t1.corresponding_deposit = t2.deposit_id JOIN blinded_shares as t3 ON t2.request_uuid = t3.request_uuid @@ -106,7 +106,7 @@ impl SqliteStorageManager { t1.epoch_id as "epoch_id!", t1.expiration_date as "expiration_date!: Date" FROM partial_blinded_wallet as t1 - JOIN ticketbook_deposit as t2 + JOIN ecash_deposit_usage as t2 on t1.corresponding_deposit = t2.deposit_id JOIN blinded_shares as t3 ON t2.request_uuid = t3.request_uuid @@ -169,7 +169,7 @@ impl SqliteStorageManager { available_shares: i64, device_id: &str, credential_id: &str, - ) -> Result { + ) -> Result { let now = OffsetDateTime::now_utc(); let res = sqlx::query_as( r#" @@ -196,7 +196,7 @@ impl SqliteStorageManager { device_id: &str, credential_id: &str, error: &str, - ) -> Result { + ) -> Result { let now = time::OffsetDateTime::now_utc(); let res = sqlx::query_as( r#" @@ -221,7 +221,7 @@ impl SqliteStorageManager { pub(crate) async fn prune_old_blinded_shares( &self, delete_after: OffsetDateTime, - ) -> Result<(), VpnApiError> { + ) -> Result<(), sqlx::Error> { sqlx::query!( r#" DELETE FROM blinded_shares WHERE created < ? @@ -236,7 +236,7 @@ impl SqliteStorageManager { pub(crate) async fn prune_old_partial_blinded_wallets( &self, delete_after: OffsetDateTime, - ) -> Result<(), VpnApiError> { + ) -> Result<(), sqlx::Error> { sqlx::query!( r#" DELETE FROM partial_blinded_wallet WHERE created < ? @@ -251,7 +251,7 @@ impl SqliteStorageManager { pub(crate) async fn prune_old_partial_blinded_wallet_failures( &self, delete_after: OffsetDateTime, - ) -> Result<(), VpnApiError> { + ) -> Result<(), sqlx::Error> { sqlx::query!( r#" DELETE FROM partial_blinded_wallet_failure WHERE created < ? @@ -370,32 +370,87 @@ impl SqliteStorageManager { Ok(()) } - #[allow(clippy::too_many_arguments)] - pub(crate) async fn insert_deposit_data( + pub(crate) async fn insert_new_deposits( + &self, + deposits: Vec, + ) -> Result<(), sqlx::Error> { + if deposits.is_empty() { + // this should NEVER happen + error!("attempted to insert empty list of deposits"); + return Ok(()); + } + + let mut query_builder = + sqlx::QueryBuilder::new("INSERT INTO ecash_deposit (deposit_id, deposit_tx_hash, requested_on, deposit_amount, ed25519_deposit_private_key) "); + + query_builder.push_values(&deposits, |mut b, deposit| { + b.push_bind(deposit.deposit_id) + .push_bind(deposit.deposit_tx_hash.clone()) + .push_bind(deposit.requested_on) + .push_bind(deposit.deposit_amount.clone()) + .push_bind(deposit.ed25519_deposit_private_key.as_ref()); + }); + + query_builder.build().execute(&self.connection_pool).await?; + Ok(()) + } + + pub(crate) async fn load_unused_deposits( + &self, + ) -> Result, sqlx::Error> { + // select all entries from ecash_deposit where there is NO associated marked usage + sqlx::query_as( + r#" + SELECT ecash_deposit.* + FROM ecash_deposit ecash_deposit + LEFT JOIN ecash_deposit_usage deposit_usage + ON ecash_deposit.deposit_id = deposit_usage.deposit_id + WHERE deposit_usage.deposit_id IS NULL; + "#, + ) + .fetch_all(&self.connection_pool) + .await + } + + pub(crate) async fn insert_deposit_usage( &self, deposit_id: DepositId, - deposit_tx_hash: String, requested_on: OffsetDateTime, + client_pubkey: Vec, request_uuid: String, - deposit_amount: String, - client_pubkey: &[u8], - deposit_ed25519_private_key: &[u8], ) -> Result<(), sqlx::Error> { sqlx::query!( r#" - INSERT INTO ticketbook_deposit(deposit_id, deposit_tx_hash, requested_on, request_uuid, deposit_amount, client_pubkey, ed25519_deposit_private_key) - VALUES (?, ?, ?, ?, ?, ?, ?) + INSERT INTO ecash_deposit_usage (deposit_id, ticketbooks_requested_on, client_pubkey, request_uuid) + VALUES (?, ?, ?, ?) "#, - deposit_id, - deposit_tx_hash, - requested_on, - request_uuid, - deposit_amount, - client_pubkey, - deposit_ed25519_private_key, + deposit_id, + requested_on, + client_pubkey, + request_uuid ) - .execute(&self.connection_pool) - .await?; + .execute(&self.connection_pool) + .await?; + + Ok(()) + } + + pub(crate) async fn insert_deposit_usage_error( + &self, + deposit_id: DepositId, + error: String, + ) -> Result<(), sqlx::Error> { + sqlx::query!( + r#" + UPDATE ecash_deposit_usage + SET ticketbook_request_error = ? + WHERE deposit_id = ? + "#, + error, + deposit_id + ) + .execute(&self.connection_pool) + .await?; Ok(()) } diff --git a/nym-credential-proxy/nym-credential-proxy/src/storage/mod.rs b/common/credential-proxy/src/storage/mod.rs similarity index 69% rename from nym-credential-proxy/nym-credential-proxy/src/storage/mod.rs rename to common/credential-proxy/src/storage/mod.rs index 437e62c870..bc20fc85cc 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/storage/mod.rs +++ b/common/credential-proxy/src/storage/mod.rs @@ -1,21 +1,18 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::credentials::ticketbook::NodeId; -use crate::error::VpnApiError; +use crate::deposits_buffer::helpers::{BufferedDeposit, PerformedDeposits}; +use crate::error::CredentialProxyError; use crate::storage::manager::SqliteStorageManager; use crate::storage::models::{BlindedShares, MinimalWalletShare}; use nym_compact_ecash::PublicKeyUser; -use nym_credentials::ecash::bandwidth::issuance::Hash; use nym_credentials::ecash::bandwidth::serialiser::VersionedSerialise; use nym_credentials::{ AggregatedCoinIndicesSignatures, AggregatedExpirationDateSignatures, EpochVerificationKey, }; -use nym_crypto::asymmetric::ed25519; use nym_validator_client::ecash::BlindedSignatureResponse; use nym_validator_client::nym_api::EpochId; use nym_validator_client::nyxd::contract_traits::ecash_query_client::DepositId; -use nym_validator_client::nyxd::Coin; use sqlx::sqlite::{SqliteAutoVacuum, SqliteSynchronous}; use sqlx::ConnectOptions; use std::fmt::Debug; @@ -25,19 +22,24 @@ use time::{Date, OffsetDateTime}; use tracing::log::LevelFilter; use tracing::{debug, error, info, instrument}; use uuid::Uuid; -use zeroize::Zeroizing; mod manager; pub mod models; +pub(crate) mod pruner; + +// TODO: proper import +type NodeId = u64; #[derive(Clone)] -pub struct VpnApiStorage { +pub struct CredentialProxyStorage { pub(crate) storage_manager: SqliteStorageManager, } -impl VpnApiStorage { +impl CredentialProxyStorage { #[instrument] - pub async fn init + Debug>(database_path: P) -> Result { + pub async fn init + Debug>( + database_path: P, + ) -> Result { debug!("Attempting to connect to database"); let opts = sqlx::sqlite::SqliteConnectOptions::new() @@ -69,36 +71,36 @@ impl VpnApiStorage { info!("Database migration finished!"); - Ok(VpnApiStorage { + Ok(CredentialProxyStorage { storage_manager: SqliteStorageManager { connection_pool }, }) } #[allow(dead_code)] - pub(crate) async fn load_blinded_shares_status_by_shares_id( + pub async fn load_blinded_shares_status_by_shares_id( &self, id: i64, - ) -> Result, VpnApiError> { + ) -> Result, CredentialProxyError> { Ok(self .storage_manager .load_blinded_shares_status_by_shares_id(id) .await?) } - pub(crate) async fn load_wallet_shares_by_shares_id( + pub async fn load_wallet_shares_by_shares_id( &self, id: i64, - ) -> Result, VpnApiError> { + ) -> Result, CredentialProxyError> { Ok(self .storage_manager .load_wallet_shares_by_shares_id(id) .await?) } - pub(crate) async fn load_shares_error_by_shares_id( + pub async fn load_shares_error_by_shares_id( &self, id: i64, - ) -> Result, VpnApiError> { + ) -> Result, CredentialProxyError> { Ok(self .storage_manager .load_shares_error_by_device_by_shares_id(id) @@ -106,84 +108,86 @@ impl VpnApiStorage { } #[allow(dead_code)] - pub(crate) async fn load_blinded_shares_status_by_device_and_credential_id( + pub async fn load_blinded_shares_status_by_device_and_credential_id( &self, device_id: &str, credential_id: &str, - ) -> Result, VpnApiError> { + ) -> Result, CredentialProxyError> { Ok(self .storage_manager .load_blinded_shares_status_by_device_and_credential_id(device_id, credential_id) .await?) } - pub(crate) async fn load_wallet_shares_by_device_and_credential_id( + pub async fn load_wallet_shares_by_device_and_credential_id( &self, device_id: &str, credential_id: &str, - ) -> Result, VpnApiError> { + ) -> Result, CredentialProxyError> { Ok(self .storage_manager .load_wallet_shares_by_device_and_credential_id(device_id, credential_id) .await?) } - pub(crate) async fn load_shares_error_by_device_and_credential_id( + pub async fn load_shares_error_by_device_and_credential_id( &self, device_id: &str, credential_id: &str, - ) -> Result, VpnApiError> { + ) -> Result, CredentialProxyError> { Ok(self .storage_manager .load_shares_error_by_device_and_credential_id(device_id, credential_id) .await?) } - pub(crate) async fn insert_new_pending_async_shares_request( + pub async fn insert_new_pending_async_shares_request( &self, request: Uuid, device_id: &str, credential_id: &str, - ) -> Result { + ) -> Result { Ok(self .storage_manager .insert_new_pending_async_shares_request(request.to_string(), device_id, credential_id) .await?) } - pub(crate) async fn update_pending_async_blinded_shares_issued( + pub async fn update_pending_async_blinded_shares_issued( &self, available_shares: usize, device_id: &str, credential_id: &str, - ) -> Result { - self.storage_manager + ) -> Result { + Ok(self + .storage_manager .update_pending_async_blinded_shares_issued( available_shares as i64, device_id, credential_id, ) - .await + .await?) } - pub(crate) async fn update_pending_async_blinded_shares_error( + pub async fn update_pending_async_blinded_shares_error( &self, available_shares: usize, device_id: &str, credential_id: &str, error: &str, - ) -> Result { - self.storage_manager + ) -> Result { + Ok(self + .storage_manager .update_pending_async_blinded_shares_error( available_shares as i64, device_id, credential_id, error, ) - .await + .await?) } - pub(crate) async fn prune_old_blinded_shares(&self) -> Result<(), VpnApiError> { + pub async fn prune_old_blinded_shares(&self) -> Result<(), CredentialProxyError> { let max_age = OffsetDateTime::now_utc() - time::Duration::days(31); self.storage_manager @@ -192,46 +196,70 @@ impl VpnApiStorage { self.storage_manager .prune_old_partial_blinded_wallet_failures(max_age) .await?; - self.storage_manager.prune_old_blinded_shares(max_age).await + self.storage_manager + .prune_old_blinded_shares(max_age) + .await?; + Ok(()) } - #[allow(clippy::too_many_arguments)] - pub(crate) async fn insert_deposit_data( + pub async fn insert_new_deposits( &self, - deposit_id: DepositId, - deposit_tx_hash: Hash, - requested_on: OffsetDateTime, - request: Uuid, - deposit_amount: Coin, - client_ecash_pubkey: &PublicKeyUser, - ed22519_keypair: &ed25519::KeyPair, - ) -> Result<(), VpnApiError> { - debug!("inserting deposit data"); - - let private_key_bytes = Zeroizing::new(ed22519_keypair.private_key().to_bytes()); + deposits: &PerformedDeposits, + ) -> Result<(), CredentialProxyError> { + debug!("inserting {} deposits data", deposits.deposits_data.len()); self.storage_manager - .insert_deposit_data( + .insert_new_deposits(deposits.to_storable()) + .await?; + Ok(()) + } + + pub async fn load_unused_deposits(&self) -> Result, CredentialProxyError> { + self.storage_manager + .load_unused_deposits() + .await? + .into_iter() + .map(|deposit| deposit.try_into()) + .collect() + } + + pub async fn insert_deposit_usage( + &self, + deposit_id: DepositId, + requested_on: OffsetDateTime, + client_pubkey: PublicKeyUser, + request_uuid: Uuid, + ) -> Result<(), CredentialProxyError> { + self.storage_manager + .insert_deposit_usage( deposit_id, - deposit_tx_hash.to_string(), requested_on, - request.to_string(), - deposit_amount.to_string(), - &client_ecash_pubkey.to_bytes(), - private_key_bytes.as_ref(), + client_pubkey.to_bytes(), + request_uuid.to_string(), ) .await?; Ok(()) } - pub(crate) async fn insert_partial_wallet_share( + pub async fn insert_deposit_usage_error( + &self, + deposit_id: DepositId, + error: String, + ) -> Result<(), CredentialProxyError> { + self.storage_manager + .insert_deposit_usage_error(deposit_id, error) + .await?; + Ok(()) + } + + pub async fn insert_partial_wallet_share( &self, deposit_id: DepositId, epoch_id: EpochId, expiration_date: Date, node_id: NodeId, - res: &Result, - ) -> Result<(), VpnApiError> { + res: &Result, + ) -> Result<(), CredentialProxyError> { debug!("inserting partial wallet share"); let now = OffsetDateTime::now_utc(); @@ -264,10 +292,10 @@ impl VpnApiStorage { Ok(()) } - pub(crate) async fn get_master_verification_key( + pub async fn get_master_verification_key( &self, epoch_id: EpochId, - ) -> Result, VpnApiError> { + ) -> Result, CredentialProxyError> { let Some(raw) = self .storage_manager .get_master_verification_key(epoch_id as i64) @@ -278,14 +306,14 @@ impl VpnApiStorage { let deserialised = EpochVerificationKey::try_unpack(&raw.serialised_key, raw.serialization_revision) - .map_err(|err| VpnApiError::database_inconsistency(err.to_string()))?; + .map_err(|err| CredentialProxyError::database_inconsistency(err.to_string()))?; Ok(Some(deserialised)) } - pub(crate) async fn insert_master_verification_key( + pub async fn insert_master_verification_key( &self, key: &EpochVerificationKey, - ) -> Result<(), VpnApiError> { + ) -> Result<(), CredentialProxyError> { let packed = key.pack(); Ok(self .storage_manager @@ -293,10 +321,10 @@ impl VpnApiStorage { .await?) } - pub(crate) async fn get_master_coin_index_signatures( + pub async fn get_master_coin_index_signatures( &self, epoch_id: EpochId, - ) -> Result, VpnApiError> { + ) -> Result, CredentialProxyError> { let Some(raw) = self .storage_manager .get_master_coin_index_signatures(epoch_id as i64) @@ -309,14 +337,14 @@ impl VpnApiStorage { &raw.serialised_signatures, raw.serialization_revision, ) - .map_err(|err| VpnApiError::database_inconsistency(err.to_string()))?; + .map_err(|err| CredentialProxyError::database_inconsistency(err.to_string()))?; Ok(Some(deserialised)) } - pub(crate) async fn insert_master_coin_index_signatures( + pub async fn insert_master_coin_index_signatures( &self, signatures: &AggregatedCoinIndicesSignatures, - ) -> Result<(), VpnApiError> { + ) -> Result<(), CredentialProxyError> { let packed = signatures.pack(); self.storage_manager .insert_master_coin_index_signatures( @@ -328,11 +356,11 @@ impl VpnApiStorage { Ok(()) } - pub(crate) async fn get_master_expiration_date_signatures( + pub async fn get_master_expiration_date_signatures( &self, expiration_date: Date, epoch_id: EpochId, - ) -> Result, VpnApiError> { + ) -> Result, CredentialProxyError> { let Some(raw) = self .storage_manager .get_master_expiration_date_signatures(expiration_date, epoch_id as i64) @@ -345,14 +373,14 @@ impl VpnApiStorage { &raw.serialised_signatures, raw.serialization_revision, ) - .map_err(|err| VpnApiError::database_inconsistency(err.to_string()))?; + .map_err(|err| CredentialProxyError::database_inconsistency(err.to_string()))?; Ok(Some(deserialised)) } - pub(crate) async fn insert_master_expiration_date_signatures( + pub async fn insert_master_expiration_date_signatures( &self, signatures: &AggregatedExpirationDateSignatures, - ) -> Result<(), VpnApiError> { + ) -> Result<(), CredentialProxyError> { let packed = signatures.pack(); self.storage_manager .insert_master_expiration_date_signatures( @@ -371,9 +399,11 @@ impl VpnApiStorage { #[cfg(test)] mod tests { use super::*; - use crate::http::helpers; + use crate::helpers::random_uuid; use crate::storage::models::BlindedSharesStatus; use nym_compact_ecash::scheme::keygen::KeyPairUser; + use nym_crypto::asymmetric::ed25519; + use nym_validator_client::nyxd::{Coin, Hash}; use rand::rngs::OsRng; use rand::RngCore; use std::ops::Deref; @@ -381,7 +411,7 @@ mod tests { // create the wrapper so the underlying file gets deleted when it's no longer needed struct StorageTestWrapper { - inner: VpnApiStorage, + inner: CredentialProxyStorage, _path: TempPath, } @@ -393,12 +423,12 @@ mod tests { println!("Creating database at {path:?}..."); Ok(StorageTestWrapper { - inner: VpnApiStorage::init(&path).await?, + inner: CredentialProxyStorage::init(&path).await?, _path: path, }) } - async fn insert_dummy_deposit(&self, uuid: Uuid) -> anyhow::Result { + async fn insert_dummy_used_deposit(&self, uuid: Uuid) -> anyhow::Result { let mut rng = OsRng; let deposit_id = rng.next_u32(); let tx_hash = Hash::Sha256(Default::default()); @@ -407,18 +437,21 @@ mod tests { let client_keypair = KeyPairUser::new(); let client_ecash_pubkey = &client_keypair.public_key(); - let deposit_keypair = ed25519::KeyPair::new(&mut rng); + let deposit_key = ed25519::PrivateKey::new(&mut rng); self.inner - .insert_deposit_data( - deposit_id, + .insert_new_deposits(&PerformedDeposits { + deposits_data: vec![BufferedDeposit { + deposit_id, + ed25519_private_key: deposit_key, + }], tx_hash, requested_on, - uuid, deposit_amount, - client_ecash_pubkey, - &deposit_keypair, - ) + }) + .await?; + self.inner + .insert_deposit_usage(deposit_id, requested_on, *client_ecash_pubkey, uuid) .await?; Ok(deposit_id) @@ -426,7 +459,7 @@ mod tests { } impl Deref for StorageTestWrapper { - type Target = VpnApiStorage; + type Target = CredentialProxyStorage; fn deref(&self) -> &Self::Target { &self.inner } @@ -448,10 +481,10 @@ mod tests { async fn test_add() -> anyhow::Result<()> { let storage = get_storage().await?; - let dummy_uuid = helpers::random_uuid(); + let dummy_uuid = random_uuid(); println!("🚀 insert_pending_blinded_share..."); - storage.insert_dummy_deposit(dummy_uuid).await?; + storage.insert_dummy_used_deposit(dummy_uuid).await?; let res = storage .insert_new_pending_async_shares_request(dummy_uuid, "1234", "1234") .await; @@ -459,7 +492,7 @@ mod tests { println!("❌ {e}"); } assert!(res.is_ok()); - let res = res.unwrap(); + let res = res?; println!("res = {res:?}"); assert_eq!(res.status, BlindedSharesStatus::Pending); @@ -471,7 +504,7 @@ mod tests { println!("❌ {e}"); } assert!(res.is_ok()); - let res = res.unwrap(); + let res = res?; println!("res = {res:?}"); assert!(res.error_message.is_some()); assert_eq!(res.status, BlindedSharesStatus::Error); @@ -484,7 +517,7 @@ mod tests { println!("❌ {e}"); } assert!(res.is_ok()); - let res = res.unwrap(); + let res = res?; println!("res = {res:?}"); assert_eq!(res.status, BlindedSharesStatus::Issued); assert!(res.error_message.is_none()); diff --git a/nym-credential-proxy/nym-credential-proxy/src/storage/models.rs b/common/credential-proxy/src/storage/models.rs similarity index 57% rename from nym-credential-proxy/nym-credential-proxy/src/storage/models.rs rename to common/credential-proxy/src/storage/models.rs index 8118890b07..2c369d199d 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/storage/models.rs +++ b/common/credential-proxy/src/storage/models.rs @@ -1,11 +1,49 @@ // Copyright 2024 Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only +use nym_crypto::asymmetric::ed25519; use serde::{Deserialize, Serialize}; -use sqlx::{FromRow, Type}; -use std::convert::Into; +use sqlx::sqlite::SqliteRow; +use sqlx::{FromRow, Row, Type}; use strum_macros::{Display, EnumString}; use time::{Date, OffsetDateTime}; +use zeroize::Zeroizing; + +pub(crate) struct StorableEcashDeposit { + pub(crate) deposit_id: u32, + pub(crate) deposit_tx_hash: String, + pub(crate) requested_on: OffsetDateTime, + pub(crate) deposit_amount: String, + pub(crate) ed25519_deposit_private_key: Zeroizing<[u8; ed25519::SECRET_KEY_LENGTH]>, +} + +impl<'r> FromRow<'r, SqliteRow> for StorableEcashDeposit { + fn from_row(row: &'r SqliteRow) -> Result { + let deposit_id = row.try_get("deposit_id")?; + let deposit_tx_hash = row.try_get("deposit_tx_hash")?; + let requested_on = row.try_get("requested_on")?; + let deposit_amount = row.try_get("deposit_amount")?; + let ed25519_deposit_private_key: Vec = row.try_get("ed25519_deposit_private_key")?; + if ed25519_deposit_private_key.len() != ed25519::SECRET_KEY_LENGTH { + return Err(sqlx::Error::decode( + "stored ed25519_deposit_private_key has invalid length", + )); + } + + // SAFETY: we just checked the length is correct + #[allow(clippy::unwrap_used)] + let ed25519_deposit_private_key: [u8; ed25519::SECRET_KEY_LENGTH] = + ed25519_deposit_private_key.try_into().unwrap(); + + Ok(StorableEcashDeposit { + deposit_id, + deposit_tx_hash, + requested_on, + deposit_amount, + ed25519_deposit_private_key: Zeroizing::new(ed25519_deposit_private_key), + }) + } +} #[derive(Serialize, Deserialize, Debug, Clone, EnumString, Type, PartialEq, Display)] #[sqlx(rename_all = "snake_case")] @@ -29,11 +67,6 @@ pub struct BlindedShares { pub updated: OffsetDateTime, } -pub struct FullBlindedShares { - pub status: BlindedShares, - pub shares: (), -} - #[derive(FromRow)] pub struct RawExpirationDateSignatures { pub serialised_signatures: Vec, diff --git a/nym-credential-proxy/nym-credential-proxy/src/tasks.rs b/common/credential-proxy/src/storage/pruner.rs similarity index 74% rename from nym-credential-proxy/nym-credential-proxy/src/tasks.rs rename to common/credential-proxy/src/storage/pruner.rs index f886b7fd44..6ed8d3b6c0 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/tasks.rs +++ b/common/credential-proxy/src/storage/pruner.rs @@ -1,17 +1,17 @@ -// Copyright 2024 Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 -use crate::storage::VpnApiStorage; +use crate::storage::CredentialProxyStorage; use tokio_util::sync::CancellationToken; use tracing::{error, info}; pub struct StoragePruner { cancellation_token: CancellationToken, - storage: VpnApiStorage, + storage: CredentialProxyStorage, } impl StoragePruner { - pub fn new(cancellation_token: CancellationToken, storage: VpnApiStorage) -> Self { + pub fn new(cancellation_token: CancellationToken, storage: CredentialProxyStorage) -> Self { Self { cancellation_token, storage, @@ -22,6 +22,7 @@ impl StoragePruner { info!("starting the storage pruner task"); loop { tokio::select! { + biased; _ = self.cancellation_token.cancelled() => { break } diff --git a/common/credential-proxy/src/ticketbook_manager/mod.rs b/common/credential-proxy/src/ticketbook_manager/mod.rs new file mode 100644 index 0000000000..7159cd279b --- /dev/null +++ b/common/credential-proxy/src/ticketbook_manager/mod.rs @@ -0,0 +1,163 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::deposits_buffer::DepositsBuffer; +use crate::error::CredentialProxyError; +use crate::quorum_checker::QuorumStateChecker; +use crate::shared_state::ecash_state::EcashState; +use crate::shared_state::nyxd_client::ChainClient; +use crate::shared_state::required_deposit_cache::RequiredDepositCache; +use crate::shared_state::CredentialProxyState; +use crate::storage::pruner::StoragePruner; +use crate::storage::CredentialProxyStorage; +use crate::webhook::ZkNymWebhook; +use nym_credentials::ecash::utils::ecash_today; +use nym_validator_client::nym_api::EpochId; +use std::future::Future; +use std::time::Duration; +use time::Date; +use tokio::task::JoinHandle; +use tokio_util::sync::CancellationToken; +use tokio_util::task::TaskTracker; + +mod shares_handlers; +pub mod ticketbook_handlers; +pub mod wallet_shares; + +#[derive(Clone, Default)] +pub struct ShutdownTracker { + pub shutdown_token: CancellationToken, + pub tracker: TaskTracker, +} + +#[derive(Clone)] +pub struct TicketbookManager { + pub(crate) state: CredentialProxyState, + pub(crate) webhook: ZkNymWebhook, + pub(crate) shutdown_tracker: ShutdownTracker, +} + +impl TicketbookManager { + pub async fn new( + build_sha: &'static str, + quorum_check_interval: Duration, + deposits_buffer_size: usize, + max_concurrent_deposits: usize, + storage: CredentialProxyStorage, + mnemonic: bip39::Mnemonic, + webhook: ZkNymWebhook, + ) -> Result { + let chain_client = ChainClient::new(mnemonic)?; + let shutdown_tracker = ShutdownTracker::default(); + + let quorum_state_checker = QuorumStateChecker::new( + chain_client.clone(), + quorum_check_interval, + shutdown_tracker.shutdown_token.clone(), + ) + .await?; + + let required_deposit_cache = RequiredDepositCache::default(); + + let deposits_buffer = DepositsBuffer::new( + storage.clone(), + chain_client.clone(), + required_deposit_cache.clone(), + build_sha, + deposits_buffer_size, + max_concurrent_deposits, + shutdown_tracker.shutdown_token.clone(), + ) + .await?; + + let storage_pruner = + StoragePruner::new(shutdown_tracker.shutdown_token.clone(), storage.clone()); + + let this = TicketbookManager { + state: CredentialProxyState::new( + storage.clone(), + chain_client, + deposits_buffer, + EcashState::new( + required_deposit_cache, + quorum_state_checker.quorum_state_ref(), + ), + ), + webhook, + shutdown_tracker, + }; + + // since this is startup, + // might as well do all the needed network queries to establish needed global signatures + // if we don't already have them + this.build_initial_cache().await?; + + // spawn the background tasks + this.try_spawn_in_background(quorum_state_checker.run_forever()); + this.try_spawn_in_background(storage_pruner.run_forever()); + + Ok(this) + } + + async fn build_initial_cache(&self) -> Result<(), CredentialProxyError> { + let today = ecash_today().date(); + + let epoch_id = self.state.current_epoch_id().await?; + let _ = self.state.deposit_amount().await?; + let _ = self.state.master_verification_key(Some(epoch_id)).await?; + let _ = self.state.ecash_threshold(epoch_id).await?; + let _ = self.state.ecash_clients(epoch_id).await?; + let _ = self + .state + .master_coin_index_signatures(Some(epoch_id)) + .await?; + let _ = self + .state + .master_expiration_date_signatures(epoch_id, today) + .await?; + + Ok(()) + } + + pub async fn cancel_and_wait(&self) { + self.shutdown_tracker.shutdown_token.cancel(); + self.state.deposits_buffer().wait_for_shutdown().await; + self.shutdown_tracker.tracker.wait().await + } + + pub fn shutdown_token(&self) -> CancellationToken { + self.shutdown_tracker.shutdown_token.clone() + } + + /// Ensure the required global data for the specified epoch and expiration date exists in our cache (and storage) + async fn ensure_global_data_cached( + &self, + epoch: EpochId, + expiration_date: Date, + ) -> Result<(), CredentialProxyError> { + let _ = self.state.master_verification_key(Some(epoch)).await?; + let _ = self.state.master_coin_index_signatures(Some(epoch)).await?; + let _ = self + .state + .master_expiration_date_signatures(epoch, expiration_date) + .await?; + Ok(()) + } + + pub fn try_spawn_in_background(&self, task: F) -> Option> + where + F: Future + Send + 'static, + F::Output: Send + 'static, + { + // don't spawn new task if we've received cancellation token + if self.shutdown_tracker.shutdown_token.is_cancelled() { + None + } else { + self.shutdown_tracker.tracker.reopen(); + // TODO: later use a task queue since most requests will be blocked waiting on chain permit anyway + let join_handle = self.shutdown_tracker.tracker.spawn(task); + self.shutdown_tracker.tracker.close(); + Some(join_handle) + } + } +} diff --git a/common/credential-proxy/src/ticketbook_manager/shares_handlers.rs b/common/credential-proxy/src/ticketbook_manager/shares_handlers.rs new file mode 100644 index 0000000000..4890c7608b --- /dev/null +++ b/common/credential-proxy/src/ticketbook_manager/shares_handlers.rs @@ -0,0 +1,145 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::error::CredentialProxyError; +use crate::storage::models::MinimalWalletShare; +use crate::ticketbook_manager::TicketbookManager; +use nym_credential_proxy_requests::api::v1::ticketbook::models::{ + GlobalDataParams, TicketbookWalletSharesResponse, +}; +use nym_validator_client::nym_api::EpochId; +use tracing::{debug, span, Instrument, Level}; +use uuid::Uuid; + +impl TicketbookManager { + async fn shares_to_response( + &self, + shares: Vec, + params: GlobalDataParams, + ) -> Result { + // in all calls we ensured the shares are non-empty + #[allow(clippy::unwrap_used)] + let first = shares.first().unwrap(); + let expiration_date = first.expiration_date; + let epoch_id = first.epoch_id as EpochId; + + let threshold = self.state.ecash_threshold(epoch_id).await?; + if shares.len() < threshold as usize { + return Err(CredentialProxyError::InsufficientNumberOfCredentials { + available: shares.len(), + threshold, + }); + } + + // grab any requested additional data + let ( + master_verification_key, + aggregated_expiration_date_signatures, + aggregated_coin_index_signatures, + ) = self + .state + .global_data(params, epoch_id, expiration_date) + .await?; + + // finally produce a response + Ok(TicketbookWalletSharesResponse { + epoch_id, + shares: shares.into_iter().map(Into::into).collect(), + master_verification_key, + aggregated_coin_index_signatures, + aggregated_expiration_date_signatures, + }) + } + + /// Query by id for blinded shares of a bandwidth voucher + pub async fn query_for_shares_by_id( + &self, + uuid: Uuid, + params: GlobalDataParams, + share_id: i64, + ) -> Result { + let span = span!(Level::INFO, "query shares by id", uuid = %uuid, share_id = %share_id); + 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 = self + .state + .storage() + .load_wallet_shares_by_shares_id(share_id) + .await?; + if shares.is_empty() { + debug!("shares not found"); + + // check for explicit error + if let Some(error_message) = self + .state + .storage() + .load_shares_error_by_shares_id(share_id) + .await? + { + return Err(CredentialProxyError::ShareByIdLoadError { + message: error_message, + id: share_id, + }); + } + + return Err(CredentialProxyError::SharesByIdNotFound { id: share_id }); + } + + self.shares_to_response(shares, params).await + } + .instrument(span) + .await + } + + /// Query by id for blinded wallet shares of a ticketbook + pub async fn query_for_shares_by_device_id_and_credential_id( + &self, + uuid: Uuid, + params: GlobalDataParams, + device_id: String, + credential_id: String, + ) -> Result { + let span = span!(Level::INFO, "query shares by device and credential ids", uuid = %uuid, device_id = %device_id, credential_id = %credential_id); + 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 = self + .state + .storage() + .load_wallet_shares_by_device_and_credential_id(&device_id, &credential_id) + .await?; + + if shares.is_empty() { + debug!("shares not found"); + + // check for explicit error + if let Some(error_message) = self + .state + .storage() + .load_shares_error_by_device_and_credential_id(&device_id, &credential_id) + .await? + { + return Err(CredentialProxyError::ShareByDeviceLoadError { + message: error_message, + device_id, + credential_id, + }); + } + + return Err(CredentialProxyError::SharesByDeviceNotFound { + device_id, + credential_id, + }); + } + + self.shares_to_response(shares, params).await + } + .instrument(span) + .await + } +} diff --git a/common/credential-proxy/src/ticketbook_manager/ticketbook_handlers.rs b/common/credential-proxy/src/ticketbook_manager/ticketbook_handlers.rs new file mode 100644 index 0000000000..b812878e22 --- /dev/null +++ b/common/credential-proxy/src/ticketbook_manager/ticketbook_handlers.rs @@ -0,0 +1,164 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::error::CredentialProxyError; +use crate::nym_api_helpers::ensure_sane_expiration_date; +use crate::ticketbook_manager::TicketbookManager; +use nym_compact_ecash::Base58; +use nym_credential_proxy_requests::api::v1::ticketbook::models::{ + CurrentEpochResponse, DepositResponse, GlobalDataParams, MasterVerificationKeyResponse, + PartialVerificationKey, PartialVerificationKeysResponse, TicketbookAsyncRequest, + TicketbookObtainParams, TicketbookRequest, TicketbookWalletSharesAsyncResponse, + TicketbookWalletSharesResponse, +}; +use time::OffsetDateTime; +use tracing::{error, info, span, warn, Instrument, Level}; +use uuid::Uuid; + +impl TicketbookManager { + pub async fn obtain_ticketbook_shares( + &self, + uuid: Uuid, + request: TicketbookRequest, + params: GlobalDataParams, + ) -> Result { + let requested_on = OffsetDateTime::now_utc(); + let span = span!(Level::INFO, "obtain ticketboook", uuid = %uuid); + + async move { + info!(""); + + self.state.ensure_credentials_issuable().await?; + let epoch_id = self.state.current_epoch_id().await?; + ensure_sane_expiration_date(request.expiration_date)?; + + // 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, + ) = self + .state + .global_data(params, epoch_id, request.expiration_date) + .await?; + + let shares = self + .try_obtain_wallet_shares(uuid, requested_on, request) + .await + .inspect_err(|err| warn!("shares request failure: {err}"))?; + + info!("request was successful!"); + Ok(TicketbookWalletSharesResponse { + epoch_id, + shares, + master_verification_key, + aggregated_coin_index_signatures, + aggregated_expiration_date_signatures, + }) + } + .instrument(span) + .await + } + + pub async fn obtain_ticketbook_shares_async( + &self, + uuid: Uuid, + request: TicketbookAsyncRequest, + params: TicketbookObtainParams, + ) -> Result { + let requested_on = OffsetDateTime::now_utc(); + let span = span!(Level::INFO, "[async] obtain ticketboook", uuid = %uuid); + async move { + info!(""); + + // 1. perform basic validation + self.state.ensure_credentials_issuable().await?; + + ensure_sane_expiration_date(request.inner.expiration_date)?; + + // 2. store the request to retrieve the id + let pending = self + .state + .storage() + .insert_new_pending_async_shares_request( + uuid, + &request.device_id, + &request.credential_id, + ) + .await + .inspect_err(|err| error!("failed to insert new pending async shares: {err}"))?; + + let id = pending.id; + + // 3. try to spawn a new task attempting to resolve the request + let this = self.clone(); + if self + .try_spawn_in_background(async move { + this.try_obtain_blinded_ticketbook_async( + uuid, + requested_on, + request, + params, + pending, + ) + .await + }) + .is_none() + { + warn!("could not start async ticketbook issuance due to shutdown in progress"); + return Err(CredentialProxyError::ShutdownInProgress); + } + + // 4. in the meantime, return the id to the user + Ok(TicketbookWalletSharesAsyncResponse { id, uuid }) + } + .instrument(span) + .await + } + + pub async fn current_deposit(&self) -> Result { + let current_deposit = self.state.deposit_amount().await?; + Ok(DepositResponse { + current_deposit_amount: current_deposit.amount, + current_deposit_denom: current_deposit.denom, + }) + } + + pub async fn partial_verification_keys( + &self, + ) -> Result { + self.state.ensure_credentials_issuable().await?; + + let epoch_id = self.state.current_epoch_id().await?; + let signers = self.state.ecash_clients(epoch_id).await?; + Ok(PartialVerificationKeysResponse { + epoch_id, + keys: signers + .iter() + .map(|signer| PartialVerificationKey { + node_index: signer.node_id, + bs58_encoded_key: signer.verification_key.to_bs58(), + }) + .collect(), + }) + } + + pub async fn master_verification_key( + &self, + ) -> Result { + self.state.ensure_credentials_issuable().await?; + + let epoch_id = self.state.current_epoch_id().await?; + let key = self.state.master_verification_key(Some(epoch_id)).await?; + Ok(MasterVerificationKeyResponse { + epoch_id, + bs58_encoded_key: key.to_bs58(), + }) + } + + pub async fn current_epoch(&self) -> Result { + self.state.ensure_credentials_issuable().await?; + let epoch_id = self.state.current_epoch_id().await?; + Ok(CurrentEpochResponse { epoch_id }) + } +} diff --git a/common/credential-proxy/src/ticketbook_manager/wallet_shares.rs b/common/credential-proxy/src/ticketbook_manager/wallet_shares.rs new file mode 100644 index 0000000000..d6508ce2fe --- /dev/null +++ b/common/credential-proxy/src/ticketbook_manager/wallet_shares.rs @@ -0,0 +1,344 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::error::CredentialProxyError; +use crate::storage::models::BlindedShares; +use crate::ticketbook_manager::TicketbookManager; +use futures::{stream, StreamExt}; +use nym_compact_ecash::Base58; +use nym_credential_proxy_requests::api::v1::ticketbook::models::{ + TicketbookAsyncRequest, TicketbookObtainParams, TicketbookRequest, + TicketbookWalletSharesResponse, WalletShare, WebhookTicketbookWalletShares, + WebhookTicketbookWalletSharesRequest, +}; +use nym_validator_client::client::NymApiClientExt; +use nym_validator_client::ecash::BlindSignRequestBody; +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 uuid::Uuid; + +impl TicketbookManager { + #[instrument( + skip(self, request_data, request, requested_on), + fields( + expiration_date = %request_data.expiration_date, + ticketbook_type = %request_data.ticketbook_type + ) + )] + pub async fn try_obtain_wallet_shares( + &self, + request: Uuid, + requested_on: OffsetDateTime, + request_data: TicketbookRequest, + ) -> Result, CredentialProxyError> { + // don't proceed if we don't have quorum available as the request will definitely fail + if !self.state.ecash_state().quorum_state.available() { + return Err(CredentialProxyError::UnavailableSigningQuorum); + } + + let epoch = self.state.current_epoch_id().await?; + let threshold = self.state.ecash_threshold(epoch).await?; + let expiration_date = request_data.expiration_date; + + // before we commit to making the deposit, ensure we have required signatures cached and stored + self.ensure_global_data_cached(epoch, expiration_date) + .await?; + let ecash_api_clients = self.state.ecash_clients(epoch).await?.clone(); + + let deposit_data = self + .state + .get_deposit(request, requested_on, request_data.ecash_pubkey) + .await?; + let deposit_id = deposit_data.deposit_id; + let signature = deposit_data.sign_ticketbook_plaintext(&request_data.withdrawal_request); + + let credential_request = BlindSignRequestBody::new( + request_data.withdrawal_request.into(), + deposit_id, + signature, + request_data.ecash_pubkey, + request_data.expiration_date, + request_data.ticketbook_type, + ); + + let wallet_shares = Arc::new(Mutex::new(HashMap::new())); + + info!("attempting to contract all nym-apis for the partial wallets..."); + stream::iter(ecash_api_clients) + .for_each_concurrent(None, |client| async { + // move the client into the block + let client = client; + + debug!("contacting {client} for blinded partial wallet"); + let res = timeout( + Duration::from_secs(5), + client.api_client.blind_sign(&credential_request), + ) + .await + .map_err(|_| CredentialProxyError::EcashApiRequestTimeout { + client_repr: client.to_string(), + }) + .and_then(|res| res.map_err(Into::into)); + + // 1. try to store it + if let Err(err) = self + .state + .storage() + .insert_partial_wallet_share( + deposit_id, + epoch, + expiration_date, + client.node_id, + &res, + ) + .await + { + error!("failed to persist issued partial share: {err}") + } + + // 2. add it to the map + match res { + Ok(share) => { + wallet_shares + .lock() + .await + .insert(client.node_id, share.blinded_signature); + } + Err(err) => { + error!("failed to obtain partial blinded wallet share from {client}: {err}") + } + } + }) + .await; + + // SAFETY: the futures have completed, so we MUST have the only arc reference + #[allow(clippy::unwrap_used)] + let wallet_shares = Arc::into_inner(wallet_shares).unwrap().into_inner(); + let shares = wallet_shares.len(); + + if shares < threshold as usize { + let err = CredentialProxyError::InsufficientNumberOfCredentials { + available: shares, + threshold, + }; + self.state + .insert_deposit_usage_error(deposit_id, err.to_string()) + .await; + return Err(err); + } + + Ok(wallet_shares + .into_iter() + .map(|(node_index, share)| WalletShare { + node_index, + bs58_encoded_share: share.to_bs58(), + }) + .collect()) + } + + pub async fn try_obtain_wallet_shares_async( + &self, + request: Uuid, + requested_on: OffsetDateTime, + request_data: TicketbookRequest, + device_id: &str, + credential_id: &str, + ) -> Result, CredentialProxyError> { + let shares = match self + .try_obtain_wallet_shares(request, requested_on, request_data) + .await + { + Ok(shares) => shares, + Err(err) => { + let obtained = match err { + CredentialProxyError::InsufficientNumberOfCredentials { available, .. } => { + available + } + _ => 0, + }; + + // currently there's no retry mechanisms, but, who knows, that might change + if let Err(err) = self + .state + .storage() + .update_pending_async_blinded_shares_error( + obtained, + device_id, + credential_id, + &err.to_string(), + ) + .await + { + error!("failed to update database with the error information: {err}") + } + return Err(err); + } + }; + + Ok(shares) + } + + async fn try_obtain_blinded_ticketbook_async_inner( + &self, + request: Uuid, + requested_on: OffsetDateTime, + request_data: TicketbookAsyncRequest, + params: TicketbookObtainParams, + pending: &BlindedShares, + ) -> Result<(), CredentialProxyError> { + let epoch_id = self.state.current_epoch_id().await?; + + let device_id = &request_data.device_id; + let credential_id = &request_data.credential_id; + let secret = request_data.secret.clone(); + + // 1. try to obtain global data + let ( + master_verification_key, + aggregated_expiration_date_signatures, + aggregated_coin_index_signatures, + ) = self + .state + .global_data(params.global, epoch_id, request_data.inner.expiration_date) + .await?; + + // 2. try to obtain shares (failures are written to the DB) + let shares = self + .try_obtain_wallet_shares_async( + request, + requested_on, + request_data.inner, + device_id, + credential_id, + ) + .await?; + + // 3. update the storage, if possible + // (as long as we can trigger webhook, we should still be good) + if let Err(err) = self + .state + .storage() + .update_pending_async_blinded_shares_issued(shares.len(), device_id, credential_id) + .await + { + error!(uuid = %request, "failed to update db with issued information: {err}") + } + + // 4. build the webhook request body + let data = Some(TicketbookWalletSharesResponse { + epoch_id, + shares, + master_verification_key, + aggregated_coin_index_signatures, + aggregated_expiration_date_signatures, + }); + + let ticketbook_wallet_shares = WebhookTicketbookWalletShares { + id: pending.id, + status: pending.status.to_string(), + device_id: device_id.clone(), + credential_id: credential_id.clone(), + data, + error_message: None, + created: pending.created, + updated: pending.updated, + }; + + let webhook_request = WebhookTicketbookWalletSharesRequest { + ticketbook_wallet_shares, + secret, + }; + + // 5. call the webhook + self.webhook.try_trigger(request, &webhook_request).await; + + Ok(()) + } + + async fn try_trigger_webhook_request_for_error( + &self, + request: Uuid, + request_data: TicketbookAsyncRequest, + pending: &BlindedShares, + error_message: String, + ) -> Result<(), CredentialProxyError> { + let device_id = &request_data.device_id; + let credential_id = &request_data.credential_id; + let secret = request_data.secret.clone(); + + let ticketbook_wallet_shares = WebhookTicketbookWalletShares { + id: pending.id, + status: "error".to_string(), + device_id: device_id.clone(), + credential_id: credential_id.clone(), + data: None, + error_message: Some(error_message), + created: pending.created, + updated: pending.updated, + }; + + let webhook_request = WebhookTicketbookWalletSharesRequest { + ticketbook_wallet_shares, + secret, + }; + + self.webhook.try_trigger(request, &webhook_request).await; + + Ok(()) + } + + #[instrument( + skip_all, + fields( + credential_id = %request_data.credential_id, + device_id = %request_data.device_id) + ) + ] + #[allow(clippy::too_many_arguments)] + pub(crate) async fn try_obtain_blinded_ticketbook_async( + &self, + request: Uuid, + requested_on: OffsetDateTime, + request_data: TicketbookAsyncRequest, + params: TicketbookObtainParams, + pending: BlindedShares, + ) { + let skip_webhook = params.skip_webhook; + if let Err(err) = self + .try_obtain_blinded_ticketbook_async_inner( + request, + requested_on, + request_data.clone(), + params, + &pending, + ) + .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) = self + .try_trigger_webhook_request_for_error( + request, + request_data, + &pending, + format!("Failed to get ticketbook: {err}"), + ) + .await + { + error!(uuid = %request, "failed to make webhook request to report error: {webhook_err}") + } + error!(uuid = %request, "failed to resolve the blinded ticketbook issuance: {err}") + } else { + info!(uuid = %request, "managed to resolve the blinded ticketbook issuance") + } + } +} diff --git a/nym-credential-proxy/nym-credential-proxy/src/webhook.rs b/common/credential-proxy/src/webhook.rs similarity index 58% rename from nym-credential-proxy/nym-credential-proxy/src/webhook.rs rename to common/credential-proxy/src/webhook.rs index d32f27c929..256a6e76fb 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/webhook.rs +++ b/common/credential-proxy/src/webhook.rs @@ -1,57 +1,34 @@ -// Copyright 2024 Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 -use crate::error::VpnApiError; -use clap::Args; use reqwest::header::AUTHORIZATION; use serde::Serialize; use tracing::{debug, error, instrument, span, Instrument, Level}; use url::Url; use uuid::Uuid; -#[derive(Args, Debug, Clone)] -pub struct ZkNymWebHookConfig { - #[clap(long, env = "WEBHOOK_ZK_NYMS_URL")] - pub webhook_url: Url, +#[derive(Debug, Clone)] +pub struct ZkNymWebhook { + pub webhook_client_url: Url, - #[clap(long, env = "WEBHOOK_ZK_NYMS_CLIENT_ID")] - pub webhook_client_id: String, - - #[clap(long, env = "WEBHOOK_ZK_NYMS_CLIENT_SECRET")] pub webhook_client_secret: String, } -impl ZkNymWebHookConfig { - pub fn ensure_valid_client_url(&self) -> Result<(), VpnApiError> { - self.client_url() - .map_err(|_| VpnApiError::InvalidWebhookUrl) - .map(|_| ()) - } - - fn client_url(&self) -> Result { - self.webhook_url.join(&self.webhook_client_id) - } - - fn unchecked_client_url(&self) -> Url { - // we ensured we have valid url on startup - #[allow(clippy::unwrap_used)] - self.client_url().unwrap() - } - +impl ZkNymWebhook { fn bearer_token(&self) -> String { format!("Bearer {}", self.webhook_client_secret) } #[instrument(skip_all)] pub async fn try_trigger(&self, original_uuid: Uuid, payload: &T) { - let url = self.unchecked_client_url(); + let url = self.webhook_client_url.clone(); let span = span!(Level::DEBUG, "webhook", uuid = %original_uuid, url = %url); async move { debug!("🕸️ about to trigger the webhook"); match reqwest::Client::new() - .post(url.clone()) + .post(url) .header(AUTHORIZATION, self.bearer_token()) .json(payload) .send() diff --git a/common/credential-storage/Cargo.toml b/common/credential-storage/Cargo.toml index aa9b615805..9220c21b1c 100644 --- a/common/credential-storage/Cargo.toml +++ b/common/credential-storage/Cargo.toml @@ -3,6 +3,7 @@ name = "nym-credential-storage" version = "0.1.0" edition = "2021" license.workspace = true +rust-version.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -33,6 +34,7 @@ features = ["rt-multi-thread", "net", "signal", "fs"] [build-dependencies] +anyhow = { workspace = true } sqlx = { workspace = true, features = [ "runtime-tokio-rustls", "sqlite", diff --git a/common/credential-storage/build.rs b/common/credential-storage/build.rs index 4716c65d0d..cc713b8c6f 100644 --- a/common/credential-storage/build.rs +++ b/common/credential-storage/build.rs @@ -3,22 +3,29 @@ * SPDX-License-Identifier: Apache-2.0 */ +use anyhow::Context; use sqlx::{Connection, SqliteConnection}; use std::env; #[tokio::main] -async fn main() { - let out_dir = env::var("OUT_DIR").unwrap(); +async fn main() -> anyhow::Result<()> { + let out_dir = env::var("OUT_DIR")?; let database_path = format!("{out_dir}/coconut-credential-example.sqlite"); + // remove the db file if it already existed from previous build + // in case it was from a different branch + if std::fs::exists(&database_path)? { + std::fs::remove_file(&database_path)?; + } + let mut conn = SqliteConnection::connect(&format!("sqlite://{database_path}?mode=rwc")) .await - .expect("Failed to create SQLx database connection"); + .context("Failed to create SQLx database connection")?; sqlx::migrate!("./migrations") .run(&mut conn) .await - .expect("Failed to perform SQLx migrations"); + .context("Failed to perform SQLx migrations")?; #[cfg(target_family = "unix")] println!("cargo:rustc-env=DATABASE_URL=sqlite://{}", &database_path); @@ -27,4 +34,6 @@ async fn main() { // for some strange reason we need to add a leading `/` to the windows path even though it's // not a valid windows path... but hey, it works... println!("cargo:rustc-env=DATABASE_URL=sqlite:///{}", &database_path); + + Ok(()) } diff --git a/common/credential-verification/src/ecash/credential_sender.rs b/common/credential-verification/src/ecash/credential_sender.rs index 4d4a0b8fe6..98706f37ac 100644 --- a/common/credential-verification/src/ecash/credential_sender.rs +++ b/common/credential-verification/src/ecash/credential_sender.rs @@ -14,7 +14,7 @@ use nym_api_requests::ecash::models::{BatchRedeemTicketsBody, VerifyEcashTicketB use nym_credentials_interface::Bandwidth; use nym_credentials_interface::{ClientTicket, TicketType}; use nym_validator_client::coconut::EcashApiError; -use nym_validator_client::nym_api::EpochId; +use nym_validator_client::nym_api::{EpochId, NymApiClientExt}; use nym_validator_client::nyxd::contract_traits::{ EcashSigningClient, MultisigQueryClient, MultisigSigningClient, PagedMultisigQueryClient, }; @@ -126,7 +126,7 @@ pub struct CredentialHandlerConfig { pub maximum_time_between_redemption: Duration, } -pub(crate) struct CredentialHandler { +pub struct CredentialHandler { config: CredentialHandlerConfig, multisig_threshold: f32, ticket_receiver: UnboundedReceiver, @@ -354,7 +354,7 @@ impl CredentialHandler { Err(err) => { error!("failed to send ticket {ticket_id} for verification to ecash signer '{client}': {err}. if we don't reach quorum, we'll retry later"); Err(EcashTicketError::ApiFailure(EcashApiError::NymApi { - source: err, + source: nym_validator_client::ValidatorClientError::from(err), })) } } @@ -907,7 +907,7 @@ impl CredentialHandler { Ok(()) } - async fn run(mut self, mut shutdown: nym_task::TaskClient) { + pub async fn run(mut self, shutdown: nym_task::ShutdownToken) { info!("Starting Ecash CredentialSender"); // attempt to clear any pending operations @@ -919,11 +919,12 @@ impl CredentialHandler { let start = Instant::now() + self.config.pending_poller; let mut resolver_interval = interval_at(start, self.config.pending_poller); - while !shutdown.is_shutdown() { + loop { tokio::select! { biased; - _ = shutdown.recv() => { + _ = shutdown.cancelled() => { trace!("client_handling::credentialSender : received shutdown"); + break }, Some(ticket) = self.ticket_receiver.next() => { let (queued_up, _) = self.ticket_receiver.size_hint(); @@ -946,8 +947,4 @@ impl CredentialHandler { } } } - - pub(crate) fn start(self, shutdown: nym_task::TaskClient) { - tokio::spawn(async move { self.run(shutdown).await }); - } } diff --git a/common/credential-verification/src/ecash/mod.rs b/common/credential-verification/src/ecash/mod.rs index 5586ef5ab1..71d1198988 100644 --- a/common/credential-verification/src/ecash/mod.rs +++ b/common/credential-verification/src/ecash/mod.rs @@ -82,9 +82,8 @@ impl EcashManager { credential_handler_cfg: CredentialHandlerConfig, nyxd_client: DirectSigningHttpRpcNyxdClient, pk_bytes: [u8; 32], - shutdown: nym_task::TaskClient, storage: GatewayStorage, - ) -> Result { + ) -> Result<(Self, CredentialHandler), Error> { let shared_state = SharedState::new(nyxd_client, Box::new(storage)).await?; let (cred_sender, cred_receiver) = mpsc::unbounded(); @@ -92,14 +91,16 @@ impl EcashManager { let cs = CredentialHandler::new(credential_handler_cfg, cred_receiver, shared_state.clone()) .await?; - cs.start(shutdown); - Ok(EcashManager { - shared_state, - pk_bytes, - pay_infos: Default::default(), - cred_sender, - }) + Ok(( + EcashManager { + shared_state, + pk_bytes, + pay_infos: Default::default(), + cred_sender, + }, + cs, + )) } pub async fn verify_pay_info(&self, pay_info: NymPayInfo) -> Result { diff --git a/common/credential-verification/src/lib.rs b/common/credential-verification/src/lib.rs index 2164c25514..61f9a1b30c 100644 --- a/common/credential-verification/src/lib.rs +++ b/common/credential-verification/src/lib.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use crate::ecash::traits::EcashManager; +use async_trait::async_trait; use bandwidth_storage_manager::BandwidthStorageManager; use nym_credentials::ecash::utils::{cred_exp_date, ecash_today, EcashTime}; use nym_credentials_interface::{Bandwidth, ClientTicket, TicketType}; @@ -139,3 +140,18 @@ impl CredentialVerifier { .await) } } + +#[async_trait] +pub trait TicketVerifier { + /// Verify that the ticket is valid and cryptographically correct. + /// If the verification succeeds, also increase the bandwidth with the ticket's + /// amount and return the latest available bandwidth + async fn verify(&mut self) -> Result; +} + +#[async_trait] +impl TicketVerifier for CredentialVerifier { + async fn verify(&mut self) -> Result { + self.verify().await + } +} diff --git a/common/credentials/Cargo.toml b/common/credentials/Cargo.toml index 4240718a37..e3cb06ec40 100644 --- a/common/credentials/Cargo.toml +++ b/common/credentials/Cargo.toml @@ -22,6 +22,7 @@ nym-ecash-time = { path = "../ecash-time", features = ["expiration"] } nym-credentials-interface = { path = "../credentials-interface" } nym-crypto = { path = "../crypto" } nym-api-requests = { path = "../../nym-api/nym-api-requests" } +nym-http-api-client = { path = "../http-api-client" } nym-validator-client = { path = "../client-libs/validator-client", default-features = false } nym-ecash-contract-common = { path = "../cosmwasm-smart-contracts/ecash-contract" } nym-network-defaults = { path = "../network-defaults" } diff --git a/common/credentials/src/ecash/bandwidth/issuance.rs b/common/credentials/src/ecash/bandwidth/issuance.rs index 3d1cacc72d..366eff02f3 100644 --- a/common/credentials/src/ecash/bandwidth/issuance.rs +++ b/common/credentials/src/ecash/bandwidth/issuance.rs @@ -15,7 +15,7 @@ use nym_credentials_interface::{ use nym_crypto::asymmetric::ed25519; use nym_ecash_contract_common::deposit::DepositId; use nym_ecash_time::{ecash_default_expiration_date, ecash_today, EcashTime}; -use nym_validator_client::nym_api::EpochId; +use nym_validator_client::nym_api::{EpochId, NymApiClientExt}; use serde::{Deserialize, Serialize}; use time::Date; @@ -108,7 +108,7 @@ impl IssuanceTicketBook { signing_request.withdrawal_request.clone(), self.deposit_id, request_signature, - signing_request.ecash_pub_key.clone(), + signing_request.ecash_pub_key, signing_request.expiration_date, signing_request.ticketbook_type, ) @@ -116,7 +116,7 @@ impl IssuanceTicketBook { pub async fn obtain_blinded_credential( &self, - client: &nym_validator_client::client::NymApiClient, + client: &nym_http_api_client::Client, request_body: &BlindSignRequestBody, ) -> Result { let server_response = client.blind_sign(request_body).await?; @@ -179,7 +179,7 @@ impl IssuanceTicketBook { // ideally this would have been generic over credential type, but we really don't need secp256k1 keys for bandwidth vouchers pub async fn obtain_partial_ticketbook_credential( &self, - client: &nym_validator_client::client::NymApiClient, + client: &nym_http_api_client::Client, signer_index: u64, validator_vk: &VerificationKeyAuth, signing_data: CredentialSigningData, diff --git a/common/credentials/src/ecash/utils.rs b/common/credentials/src/ecash/utils.rs index 305aafd532..497987e3da 100644 --- a/common/credentials/src/ecash/utils.rs +++ b/common/credentials/src/ecash/utils.rs @@ -10,6 +10,7 @@ use nym_credentials_interface::{ VerificationKeyAuth, WalletSignatures, }; use nym_validator_client::client::EcashApiClient; +use nym_validator_client::nym_api::NymApiClientExt; // so we wouldn't break all the existing imports pub use nym_ecash_time::{cred_exp_date, ecash_date_offset, ecash_today, EcashTime}; diff --git a/common/credentials/src/error.rs b/common/credentials/src/error.rs index fa751451dc..daabbcdb1b 100644 --- a/common/credentials/src/error.rs +++ b/common/credentials/src/error.rs @@ -4,7 +4,7 @@ use crate::ecash::bandwidth::issued::CURRENT_SERIALIZATION_REVISION; use nym_credentials_interface::CompactEcashError; use nym_crypto::asymmetric::x25519::KeyRecoveryError; -use nym_validator_client::ValidatorClientError; +use nym_validator_client::{nym_api::error::NymAPIError, ValidatorClientError}; use thiserror::Error; #[derive(Debug, Error)] @@ -37,6 +37,9 @@ pub enum Error { #[error("Ran into a validator client error - {0}")] ValidatorClientError(#[from] ValidatorClientError), + #[error("Nym API request failed - {0}")] + NymAPIError(Box), + #[error("Bandwidth operation overflowed. {0}")] BandwidthOverflow(String), @@ -61,3 +64,9 @@ pub enum Error { #[error("failed to create a secp256k1 signature")] Secp256k1SignFailure, } + +impl From for Error { + fn from(e: NymAPIError) -> Self { + Error::NymAPIError(Box::new(e)) + } +} diff --git a/common/crypto/Cargo.toml b/common/crypto/Cargo.toml index 6e1f8cfb12..96549a448d 100644 --- a/common/crypto/Cargo.toml +++ b/common/crypto/Cargo.toml @@ -11,6 +11,7 @@ repository = { workspace = true } aes-gcm-siv = { workspace = true, optional = true } aes = { workspace = true, optional = true } aead = { workspace = true, optional = true } +base64.workspace = true bs58 = { workspace = true } blake3 = { workspace = true, features = ["traits-preview"], optional = true } ctr = { workspace = true, optional = true } @@ -18,6 +19,7 @@ digest = { workspace = true, optional = true } generic-array = { workspace = true, optional = true } hkdf = { workspace = true, optional = true } hmac = { workspace = true, optional = true } +jwt-simple = { workspace = true, optional = true } cipher = { workspace = true, optional = true } x25519-dalek = { workspace = true, features = ["static_secrets"], optional = true } ed25519-dalek = { workspace = true, features = ["rand_core"], optional = true } @@ -39,6 +41,7 @@ rand_chacha = { workspace = true } [features] default = [] aead = ["dep:aead", "aead/std", "aes-gcm-siv", "generic-array"] +naive_jwt = ["asymmetric", "jwt-simple"] serde = ["dep:serde", "serde_bytes", "ed25519-dalek/serde", "x25519-dalek/serde"] asymmetric = ["x25519-dalek", "ed25519-dalek", "zeroize"] hashing = ["blake3", "digest", "hkdf", "hmac", "generic-array", "sha2"] diff --git a/common/crypto/src/asymmetric/ed25519/mod.rs b/common/crypto/src/asymmetric/ed25519/mod.rs index 387034a8d1..8d23ee7d8c 100644 --- a/common/crypto/src/asymmetric/ed25519/mod.rs +++ b/common/crypto/src/asymmetric/ed25519/mod.rs @@ -2,8 +2,9 @@ // SPDX-License-Identifier: Apache-2.0 pub use ed25519_dalek::SignatureError; -use ed25519_dalek::{SecretKey, Signer, SigningKey}; pub use ed25519_dalek::{Verifier, PUBLIC_KEY_LENGTH, SECRET_KEY_LENGTH, SIGNATURE_LENGTH}; + +use ed25519_dalek::Signer; use nym_pemstore::traits::{PemStorableKey, PemStorableKeyPair}; use std::fmt::{self, Debug, Display, Formatter}; use std::str::FromStr; @@ -13,6 +14,9 @@ use zeroize::{Zeroize, ZeroizeOnDrop}; #[cfg(feature = "serde")] pub mod serde_helpers; +#[cfg(feature = "serde")] +pub use serde_helpers::*; + #[cfg(feature = "sphinx")] use nym_sphinx_types::{DestinationAddressBytes, DESTINATION_ADDRESS_LENGTH}; @@ -81,8 +85,8 @@ impl KeyPair { } } - pub fn from_secret(secret: SecretKey, index: u32) -> Self { - let ed25519_signing_key = SigningKey::from(secret); + pub fn from_secret(secret: ed25519_dalek::SecretKey, index: u32) -> Self { + let ed25519_signing_key = ed25519_dalek::SigningKey::from(secret); KeyPair { private_key: PrivateKey(ed25519_signing_key.to_bytes()), @@ -276,7 +280,7 @@ impl Display for PrivateKey { impl<'a> From<&'a PrivateKey> for PublicKey { fn from(pk: &'a PrivateKey) -> Self { - PublicKey(SigningKey::from_bytes(&pk.0).verifying_key()) + PublicKey(ed25519_dalek::SigningKey::from_bytes(&pk.0).verifying_key()) } } @@ -320,7 +324,7 @@ impl PrivateKey { } pub fn sign>(&self, message: M) -> Signature { - let signing_key: SigningKey = self.0.into(); + let signing_key: ed25519_dalek::SigningKey = self.0.into(); let sig = signing_key.sign(message.as_ref()); Signature(sig) } @@ -425,9 +429,57 @@ impl<'d> Deserialize<'d> for Signature { } } +#[cfg(feature = "naive_jwt")] +impl PublicKey { + pub fn to_jwt_compatible_key(&self) -> jwt_simple::algorithms::Ed25519PublicKey { + (*self).into() + } +} + +#[cfg(feature = "naive_jwt")] +impl From for jwt_simple::algorithms::Ed25519PublicKey { + fn from(value: PublicKey) -> Self { + // SAFETY: we have a valid ed25519 pubkey, we're just changing to a different library wrapper + #[allow(clippy::unwrap_used)] + jwt_simple::algorithms::Ed25519PublicKey::from_bytes(&value.to_bytes()).unwrap() + } +} + +#[cfg(feature = "naive_jwt")] +impl PrivateKey { + pub fn to_jwt_compatible_keys(&self) -> jwt_simple::algorithms::Ed25519KeyPair { + let pub_key = self.public_key(); + let mut bytes = zeroize::Zeroizing::new([0u8; 64]); + + bytes[..SECRET_KEY_LENGTH] + .copy_from_slice(zeroize::Zeroizing::new(self.to_bytes()).as_ref()); + bytes[SECRET_KEY_LENGTH..].copy_from_slice(&pub_key.to_bytes()); + + // SAFETY: we have a valid ed25519 keys, we're just changing to a different library wrapper + #[allow(clippy::unwrap_used)] + jwt_simple::algorithms::Ed25519KeyPair::from_bytes(bytes.as_ref()).unwrap() + } +} + +#[cfg(feature = "naive_jwt")] +impl KeyPair { + pub fn to_jwt_compatible_keys(&self) -> jwt_simple::algorithms::Ed25519KeyPair { + let mut bytes = zeroize::Zeroizing::new([0u8; 64]); + + bytes[..SECRET_KEY_LENGTH] + .copy_from_slice(zeroize::Zeroizing::new(self.private_key.to_bytes()).as_ref()); + bytes[SECRET_KEY_LENGTH..].copy_from_slice(&self.public_key.to_bytes()); + + // SAFETY: we have a valid ed25519 keys, we're just changing to a different library wrapper + #[allow(clippy::unwrap_used)] + jwt_simple::algorithms::Ed25519KeyPair::from_bytes(bytes.as_ref()).unwrap() + } +} + #[cfg(test)] mod tests { use super::*; + use rand::thread_rng; fn assert_zeroize_on_drop() {} @@ -438,4 +490,29 @@ mod tests { assert_zeroize::(); assert_zeroize_on_drop::(); } + + #[test] + #[cfg(all(feature = "naive_jwt", feature = "rand"))] + fn check_jwt_key_compat_conversion() { + let mut rng = thread_rng(); + let keys = KeyPair::new(&mut rng); + let jwt_keys = keys.to_jwt_compatible_keys(); + + // internally they're represented by hidden `Edwards25519KeyPair` (plus key_id) + // which has way nicer API for assertions + let jwt_keys_inner = + jwt_simple::algorithms::Edwards25519KeyPair::from_bytes(&jwt_keys.to_bytes()).unwrap(); + + let compact_ed25519 = jwt_keys_inner.as_ref(); + assert!(compact_ed25519 + .sk + .validate_public_key(&compact_ed25519.pk) + .is_ok()); + + let dummy_message = "hello world"; + let sig1 = keys.private_key.sign(dummy_message).to_bytes(); + let sig2 = compact_ed25519.sk.sign(dummy_message, None).to_vec(); + + assert_eq!(sig1.to_vec(), sig2); + } } diff --git a/common/crypto/src/asymmetric/x25519/mod.rs b/common/crypto/src/asymmetric/x25519/mod.rs index 9b73adb910..3f41319a97 100644 --- a/common/crypto/src/asymmetric/x25519/mod.rs +++ b/common/crypto/src/asymmetric/x25519/mod.rs @@ -1,6 +1,7 @@ // Copyright 2021-2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use base64::Engine; use nym_pemstore::traits::{PemStorableKey, PemStorableKeyPair}; use std::fmt::{self, Debug, Display, Formatter}; use std::str::FromStr; @@ -158,6 +159,15 @@ impl PublicKey { .map_err(|source| KeyRecoveryError::MalformedPublicKeyString { source })?; Self::from_bytes(&bytes) } + + pub fn from_base64(s: &str) -> Option { + let bytes = base64::engine::general_purpose::STANDARD.decode(s).ok()?; + Self::from_bytes(&bytes).ok() + } + + pub fn to_base64(&self) -> String { + base64::engine::general_purpose::STANDARD.encode(self.as_bytes()) + } } impl FromStr for PublicKey { diff --git a/common/ecash-signer-check-types/src/status.rs b/common/ecash-signer-check-types/src/status.rs index 2a506d6c96..ea07862633 100644 --- a/common/ecash-signer-check-types/src/status.rs +++ b/common/ecash-signer-check-types/src/status.rs @@ -157,6 +157,14 @@ impl SignerResult { pub fn malformed_details(&self) -> bool { self.information.parse().is_err() } + + pub fn try_get_test_result(&self) -> Option<&SignerTestResult> { + if let SignerStatus::Tested { result } = &self.status { + Some(result) + } else { + None + } + } } impl SignerResult diff --git a/common/ecash-signer-check/Cargo.toml b/common/ecash-signer-check/Cargo.toml index be22872ad8..cc8fb6ec29 100644 --- a/common/ecash-signer-check/Cargo.toml +++ b/common/ecash-signer-check/Cargo.toml @@ -22,6 +22,7 @@ url = { workspace = true } nym-validator-client = { path = "../client-libs/validator-client" } nym-network-defaults = { path = "../network-defaults" } nym-ecash-signer-check-types = { path = "../ecash-signer-check-types" } +nym-http-api-client = { path = "../http-api-client" } [lints] workspace = true diff --git a/common/ecash-signer-check/src/client_check.rs b/common/ecash-signer-check/src/client_check.rs index b5c367794d..aff2db6725 100644 --- a/common/ecash-signer-check/src/client_check.rs +++ b/common/ecash-signer-check/src/client_check.rs @@ -1,15 +1,14 @@ // Copyright 2025 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::{LocalChainStatus, SigningStatus, TypedSignerResult}; +use crate::{LocalChainStatus, SignerCheckError, SigningStatus, TypedSignerResult}; use nym_ecash_signer_check_types::dealer_information::RawDealerInformation; use nym_ecash_signer_check_types::status::{SignerStatus, SignerTestResult}; -use nym_validator_client::client::NymApiClientExt; use nym_validator_client::models::BinaryBuildInformationOwned; +use nym_validator_client::nym_api::NymApiClientExt; use nym_validator_client::nyxd::contract_traits::dkg_query_client::{ ContractVKShare, DealerDetails, }; -use nym_validator_client::NymApiClient; use std::time::Duration; use tracing::{error, warn}; use url::Url; @@ -32,37 +31,38 @@ pub(crate) mod signing_status { } struct ClientUnderTest { - api_client: NymApiClient, + api_client: nym_http_api_client::Client, build_info: Option, } impl ClientUnderTest { - pub(crate) fn new(api_url: &Url) -> Self { - ClientUnderTest { - api_client: NymApiClient::new(api_url.clone()), + pub(crate) fn new(api_url: &Url) -> Result { + // The builder should not fail with a valid URL that's already parsed + // If it does fail, it's an internal error that we can't recover from + let api_client = nym_http_api_client::Client::builder(api_url.clone())?.build()?; + + Ok(ClientUnderTest { + api_client, build_info: None, - } + }) } pub(crate) async fn try_retrieve_build_information(&mut self) -> bool { - match tokio::time::timeout( - Duration::from_secs(5), - self.api_client.nym_api.build_information(), - ) - .await + match tokio::time::timeout(Duration::from_secs(5), self.api_client.build_information()) + .await { Ok(Ok(build_information)) => { self.build_info = Some(build_information); true } Ok(Err(err)) => { - warn!("{}: failed to retrieve build information: {err}. the signer is most likely down", self.api_client.api_url()); + warn!("{}: failed to retrieve build information: {err}. the signer is most likely down", self.api_client.current_url()); false } Err(_timeout) => { warn!( "{}: timed out while attempting to retrieve build information", - self.api_client.api_url() + self.api_client.current_url() ); false } @@ -77,7 +77,7 @@ impl ClientUnderTest { .inspect_err(|err| { error!( "ecash signer '{}' reports invalid version {}: {err}", - self.api_client.api_url(), + self.api_client.current_url(), build_info.build_version ) }) @@ -121,14 +121,14 @@ impl ClientUnderTest { // check if it supports the current query if self.supports_chain_status_query() { - return match self.api_client.nym_api.get_chain_blocks_status().await { + return match self.api_client.get_chain_blocks_status().await { Ok(status) => LocalChainStatus::Reachable { response: Box::new(status), }, Err(err) => { warn!( "{}: failed to retrieve local chain status: {err}", - self.api_client.api_url() + self.api_client.current_url() ); LocalChainStatus::Unreachable } @@ -136,14 +136,14 @@ impl ClientUnderTest { } // fallback to the legacy query - match self.api_client.nym_api.get_chain_status().await { + match self.api_client.get_chain_status().await { Ok(status) => LocalChainStatus::ReachableLegacy { response: Box::new(status), }, Err(err) => { warn!( "{}: failed to retrieve [legacy] local chain status: {err}", - self.api_client.api_url() + self.api_client.current_url() ); LocalChainStatus::Unreachable } @@ -158,14 +158,14 @@ impl ClientUnderTest { // check if it supports the current query if self.supports_signing_status_query() { - return match self.api_client.nym_api.get_signer_status().await { + return match self.api_client.get_signer_status().await { Ok(response) => SigningStatus::Reachable { response: Box::new(response), }, Err(err) => { warn!( "{}: failed to retrieve signer chain status: {err}", - self.api_client.api_url() + self.api_client.current_url() ); SigningStatus::Unreachable } @@ -173,14 +173,14 @@ impl ClientUnderTest { } // fallback to the legacy query - match self.api_client.nym_api.get_signer_information().await { + match self.api_client.get_signer_information().await { Ok(status) => SigningStatus::ReachableLegacy { response: Box::new(status), }, Err(err) => { warn!( "{}: failed to retrieve [legacy] signer chain status: {err}", - self.api_client.api_url() + self.api_client.current_url() ); // NOTE: this might equally mean the signing is disabled SigningStatus::Unreachable @@ -201,7 +201,13 @@ pub(crate) async fn check_client( return SignerStatus::ProvidedInvalidDetails.with_details(dealer_information, dkg_epoch); }; - let mut client = ClientUnderTest::new(&parsed_information.announce_address); + let mut client = match ClientUnderTest::new(&parsed_information.announce_address) { + Ok(client) => client, + Err(err) => { + error!("failed to create client instance: {err}"); + return SignerStatus::Unreachable.with_details(dealer_information, dkg_epoch); + } + }; // 8. check basic connection status - can you retrieve build information? if !client.try_retrieve_build_information().await { diff --git a/common/ecash-signer-check/src/error.rs b/common/ecash-signer-check/src/error.rs index bb7c3c719b..705ce9f3bf 100644 --- a/common/ecash-signer-check/src/error.rs +++ b/common/ecash-signer-check/src/error.rs @@ -1,6 +1,7 @@ // Copyright 2025 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use nym_http_api_client::HttpClientError; use nym_validator_client::nyxd::error::NyxdError; use thiserror::Error; @@ -11,6 +12,17 @@ pub enum SignerCheckError { #[error("failed to query the DKG contract: {source}")] DKGContractQueryFailure { source: NyxdError }, + + #[error("failed to build client: {source}")] + HttpClient { source: Box }, +} + +impl From for SignerCheckError { + fn from(e: HttpClientError) -> Self { + SignerCheckError::HttpClient { + source: Box::new(e), + } + } } impl SignerCheckError { diff --git a/common/ecash-signer-check/src/lib.rs b/common/ecash-signer-check/src/lib.rs index 2915119a27..c7ddb07113 100644 --- a/common/ecash-signer-check/src/lib.rs +++ b/common/ecash-signer-check/src/lib.rs @@ -15,6 +15,9 @@ use nym_validator_client::ecash::models::EcashSignerStatusResponse; use nym_validator_client::models::{ ChainBlocksStatusResponse, ChainStatusResponse, SignerInformationResponse, }; +use nym_validator_client::nyxd::contract_traits::dkg_query_client::{ + ContractVKShare, DealerDetails, Epoch, +}; mod client_check; pub mod error; @@ -48,7 +51,22 @@ pub async fn check_signers( check_signers_with_client(&client).await } +pub struct DkgDetails { + pub dkg_epoch: Epoch, + pub threshold: Option, + pub network_dealers: Vec, + pub submitted_shared: HashMap, +} + pub async fn check_signers_with_client(client: &C) -> Result +where + C: DkgQueryClient + Sync, +{ + let dkg_details = dkg_details_with_client(client).await?; + check_known_dealers(dkg_details).await +} + +pub async fn dkg_details_with_client(client: &C) -> Result where C: DkgQueryClient + Sync, { @@ -79,16 +97,31 @@ where .map(|share| (share.node_index, share)) .collect(); + Ok(DkgDetails { + dkg_epoch, + threshold, + network_dealers: dealers, + submitted_shared: shares, + }) +} + +pub async fn check_known_dealers( + dkg_details: DkgDetails, +) -> Result { // 6. for each dealer attempt to perform the checks - let results = dealers + let results = dkg_details + .network_dealers .into_iter() .map(|d| { - let share = shares.get(&d.assigned_index); - check_client(d, dkg_epoch.epoch_id, share) + let share = dkg_details.submitted_shared.get(&d.assigned_index); + check_client(d, dkg_details.dkg_epoch.epoch_id, share) }) .collect::>() .collect::>() .await; - Ok(SignersTestResult { threshold, results }) + Ok(SignersTestResult { + threshold: dkg_details.threshold, + results, + }) } diff --git a/common/execute/Cargo.toml b/common/execute/Cargo.toml deleted file mode 100644 index 613d5ebb5f..0000000000 --- a/common/execute/Cargo.toml +++ /dev/null @@ -1,12 +0,0 @@ -[package] -name = "nym-execute" -version = "0.1.0" -edition = "2021" -license.workspace = true - -[lib] -proc-macro = true - -[dependencies] -syn = { workspace = true, features = ["full"] } -quote = { workspace = true } diff --git a/common/execute/src/lib.rs b/common/execute/src/lib.rs deleted file mode 100644 index c05a2e8748..0000000000 --- a/common/execute/src/lib.rs +++ /dev/null @@ -1,110 +0,0 @@ -use proc_macro::TokenStream; -use quote::quote; -use syn::{ - parse_macro_input, Block, ExprMethodCall, FnArg, Ident, ItemFn, LitStr, ReturnType, Token, - VisPublic, Visibility, -}; - -#[proc_macro_attribute] -pub fn execute(attr: TokenStream, item: TokenStream) -> TokenStream { - let f = parse_macro_input!(item as ItemFn); - let target = parse_macro_input!(attr as LitStr).value(); - - let cl = if target == "mixnet" { - quote! {self.mixnet_contract_address()} - } else if target == "vesting" { - quote! {self.vesting_contract_address()} - } else { - panic!("Only `mixnet` and `vesting` targets are supported!") - }; - let cl = proc_macro::TokenStream::from(cl); - let cl = parse_macro_input!(cl as ExprMethodCall); - - let orig_f = f.clone(); - let mut execute_f = f.clone(); - let mut simulate_f = f.clone(); - let name = f.sig.ident; - let name_str = name.to_string(); - let call_args = f.sig.inputs.into_iter().filter_map(|arg| match arg { - FnArg::Receiver(_) => None, - FnArg::Typed(arg) => Some(arg.pat), - }); - let execute_args = call_args.clone(); - let simulate_args = call_args; - - execute_f.sig.asyncness = Some(Token![async](execute_f.sig.ident.span())); - simulate_f.sig.asyncness = Some(Token![async](simulate_f.sig.ident.span())); - - execute_f.vis = Visibility::Public(VisPublic { - pub_token: Token![pub](execute_f.sig.ident.span()), - }); - simulate_f.vis = Visibility::Public(VisPublic { - pub_token: Token![pub](simulate_f.sig.ident.span()), - }); - - execute_f.sig.ident = Ident::new( - &format!("execute{}", execute_f.sig.ident), - execute_f.sig.ident.span(), - ); - - simulate_f.sig.ident = Ident::new( - &format!("simulate{}", simulate_f.sig.ident), - simulate_f.sig.ident.span(), - ); - - let execute_output = quote! { - -> Result - }; - let o_ts = proc_macro::TokenStream::from(execute_output); - execute_f.sig.output = parse_macro_input!(o_ts as ReturnType); - - let simulate_output = quote! { - -> Result - }; - let o_ts = proc_macro::TokenStream::from(simulate_output); - simulate_f.sig.output = parse_macro_input!(o_ts as ReturnType); - - let simulate_block = quote! { - { - let (msg, _fee) = self.#name(#(#simulate_args),*); - let msg = self.wrap_contract_execute_message( - #cl, - &msg, - vec![], - )?; - - self.simulate(vec![msg]).await - } - }; - - let ts = proc_macro::TokenStream::from(simulate_block); - simulate_f.block = Box::new(parse_macro_input!(ts as Block)); - - let execute_block = quote! { - { - let (req, fee) = self.#name(#(#execute_args),*); - let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier))); - self.client - .execute( - self.address(), - #cl, - &req, - fee, - #name_str, - vec![], - ) - .await - } - }; - - let ts = proc_macro::TokenStream::from(execute_block); - execute_f.block = Box::new(parse_macro_input!(ts as Block)); - - let out = quote! { - #orig_f - #execute_f - #simulate_f - }; - - out.into() -} diff --git a/common/gateway-requests/src/models.rs b/common/gateway-requests/src/models.rs index 32b2a9f812..f7e79d7685 100644 --- a/common/gateway-requests/src/models.rs +++ b/common/gateway-requests/src/models.rs @@ -89,7 +89,7 @@ mod tests { .unwrap(); let blind_sig = issue( keypair.secret_key(), - sig_req.ecash_pub_key.clone(), + sig_req.ecash_pub_key, &sig_req.withdrawal_request, expiration_date.ecash_unix_timestamp(), issuance.ticketbook_type().encode(), diff --git a/common/gateway-requests/src/registration/handshake/mod.rs b/common/gateway-requests/src/registration/handshake/mod.rs index f6ef8a4de4..2daed81b48 100644 --- a/common/gateway-requests/src/registration/handshake/mod.rs +++ b/common/gateway-requests/src/registration/handshake/mod.rs @@ -14,7 +14,7 @@ use std::task::{Context, Poll}; use tungstenite::{Error as WsError, Message as WsMessage}; #[cfg(not(target_arch = "wasm32"))] -use nym_task::TaskClient; +use nym_task::ShutdownToken; pub(crate) type WsItem = Result; @@ -52,7 +52,7 @@ pub fn client_handshake<'a, S, R>( gateway_pubkey: ed25519::PublicKey, expects_credential_usage: bool, derive_aes256_gcm_siv_key: bool, - #[cfg(not(target_arch = "wasm32"))] shutdown: TaskClient, + #[cfg(not(target_arch = "wasm32"))] shutdown_token: ShutdownToken, ) -> GatewayHandshake<'a> where S: Stream + Sink + Unpin + Send + 'a, @@ -64,7 +64,7 @@ where identity, Some(gateway_pubkey), #[cfg(not(target_arch = "wasm32"))] - shutdown, + shutdown_token, ) .with_credential_usage(expects_credential_usage) .with_aes256_gcm_siv_key(derive_aes256_gcm_siv_key); @@ -80,13 +80,13 @@ pub fn gateway_handshake<'a, S, R>( ws_stream: &'a mut S, identity: &'a ed25519::KeyPair, received_init_payload: Vec, - shutdown: TaskClient, + shutdown_token: ShutdownToken, ) -> GatewayHandshake<'a> where S: Stream + Sink + Unpin + Send + 'a, R: CryptoRng + RngCore + Send, { - let state = State::new(rng, ws_stream, identity, None, shutdown); + let state = State::new(rng, ws_stream, identity, None, shutdown_token); GatewayHandshake { handshake_future: Box::pin(state.perform_gateway_handshake(received_init_payload)), } @@ -149,7 +149,7 @@ mod tests { *gateway_keys.public_key(), false, true, - TaskClient::dummy(), + ShutdownToken::default(), ); let client_fut = handshake_client.spawn_timeboxed(); @@ -176,7 +176,7 @@ mod tests { gateway_ws, gateway_keys, init_msg, - TaskClient::dummy(), + ShutdownToken::default(), ); let gateway_fut = handshake_gateway.spawn_timeboxed(); diff --git a/common/gateway-requests/src/registration/handshake/state.rs b/common/gateway-requests/src/registration/handshake/state.rs index 62e9eba9e8..3d7b29e3f9 100644 --- a/common/gateway-requests/src/registration/handshake/state.rs +++ b/common/gateway-requests/src/registration/handshake/state.rs @@ -24,7 +24,7 @@ use tracing::log::*; use tungstenite::Message as WsMessage; #[cfg(not(target_arch = "wasm32"))] -use nym_task::TaskClient; +use nym_task::ShutdownToken; #[cfg(not(target_arch = "wasm32"))] use tokio::time::timeout; @@ -63,7 +63,7 @@ pub(crate) struct State<'a, S, R> { // channel to receive shutdown signal #[cfg(not(target_arch = "wasm32"))] - shutdown: TaskClient, + shutdown_token: ShutdownToken, } impl<'a, S, R> State<'a, S, R> { @@ -72,7 +72,7 @@ impl<'a, S, R> State<'a, S, R> { ws_stream: &'a mut S, identity: &'a ed25519::KeyPair, remote_pubkey: Option, - #[cfg(not(target_arch = "wasm32"))] shutdown: TaskClient, + #[cfg(not(target_arch = "wasm32"))] shutdown_token: ShutdownToken, ) -> Self where R: CryptoRng + RngCore, @@ -89,7 +89,7 @@ impl<'a, S, R> State<'a, S, R> { expects_credential_usage: false, derive_aes256_gcm_siv_key: false, #[cfg(not(target_arch = "wasm32"))] - shutdown, + shutdown_token, } } @@ -306,7 +306,7 @@ impl<'a, S, R> State<'a, S, R> { loop { tokio::select! { biased; - _ = self.shutdown.recv() => return Err(HandshakeError::ReceivedShutdown), + _ = self.shutdown_token.cancelled() => return Err(HandshakeError::ReceivedShutdown), msg = self.ws_stream.next() => { let Some(ret) = Self::on_wg_msg(msg)? else { continue; diff --git a/common/gateway-stats-storage/Cargo.toml b/common/gateway-stats-storage/Cargo.toml index dc479e4cd2..e958e25526 100644 --- a/common/gateway-stats-storage/Cargo.toml +++ b/common/gateway-stats-storage/Cargo.toml @@ -7,6 +7,7 @@ homepage.workspace = true documentation.workspace = true edition.workspace = true license.workspace = true +rust-version.workspace = true [dependencies] sqlx = { workspace = true, features = [ @@ -27,6 +28,7 @@ nym-statistics-common = { path = "../statistics" } [build-dependencies] +anyhow = { workspace = true } tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } sqlx = { workspace = true, features = [ "runtime-tokio-rustls", diff --git a/common/gateway-stats-storage/build.rs b/common/gateway-stats-storage/build.rs index cef40420c9..6c665678e6 100644 --- a/common/gateway-stats-storage/build.rs +++ b/common/gateway-stats-storage/build.rs @@ -1,22 +1,29 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only +use anyhow::Context; use sqlx::{Connection, SqliteConnection}; use std::env; #[tokio::main] -async fn main() { - let out_dir = env::var("OUT_DIR").unwrap(); +async fn main() -> anyhow::Result<()> { + let out_dir = env::var("OUT_DIR")?; let database_path = format!("{out_dir}/gateway-stats-example.sqlite"); + // remove the db file if it already existed from previous build + // in case it was from a different branch + if std::fs::exists(&database_path)? { + std::fs::remove_file(&database_path)?; + } + let mut conn = SqliteConnection::connect(&format!("sqlite://{database_path}?mode=rwc")) .await - .expect("Failed to create SQLx database connection"); + .context("Failed to create SQLx database connection")?; sqlx::migrate!("./migrations") .run(&mut conn) .await - .expect("Failed to perform SQLx migrations"); + .context("Failed to perform SQLx migrations")?; #[cfg(target_family = "unix")] println!("cargo:rustc-env=DATABASE_URL=sqlite://{}", &database_path); @@ -25,4 +32,6 @@ async fn main() { // for some strange reason we need to add a leading `/` to the windows path even though it's // not a valid windows path... but hey, it works... println!("cargo:rustc-env=DATABASE_URL=sqlite:///{}", &database_path); + + Ok(()) } diff --git a/common/gateway-storage/Cargo.toml b/common/gateway-storage/Cargo.toml index 4ead02b939..c7878511be 100644 --- a/common/gateway-storage/Cargo.toml +++ b/common/gateway-storage/Cargo.toml @@ -7,6 +7,7 @@ homepage.workspace = true documentation.workspace = true edition.workspace = true license.workspace = true +rust-version.workspace = true [dependencies] async-trait = { workspace = true } @@ -31,6 +32,7 @@ nym-gateway-requests = { path = "../gateway-requests" } nym-sphinx = { path = "../nymsphinx" } [build-dependencies] +anyhow = { workspace = true } tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } sqlx = { workspace = true, features = [ "runtime-tokio-rustls", diff --git a/common/gateway-storage/build.rs b/common/gateway-storage/build.rs index 9b46e07840..bfdaf4fca9 100644 --- a/common/gateway-storage/build.rs +++ b/common/gateway-storage/build.rs @@ -1,22 +1,29 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only +use anyhow::Context; use sqlx::{Connection, SqliteConnection}; use std::env; #[tokio::main] -async fn main() { - let out_dir = env::var("OUT_DIR").unwrap(); +async fn main() -> anyhow::Result<()> { + let out_dir = env::var("OUT_DIR")?; let database_path = format!("{out_dir}/gateway-example.sqlite"); + // remove the db file if it already existed from previous build + // in case it was from a different branch + if std::fs::exists(&database_path)? { + std::fs::remove_file(&database_path)?; + } + let mut conn = SqliteConnection::connect(&format!("sqlite://{database_path}?mode=rwc")) .await - .expect("Failed to create SQLx database connection"); + .context("Failed to create SQLx database connection")?; sqlx::migrate!("./migrations") .run(&mut conn) .await - .expect("Failed to perform SQLx migrations"); + .context("Failed to perform SQLx migrations")?; #[cfg(target_family = "unix")] println!("cargo:rustc-env=DATABASE_URL=sqlite://{}", &database_path); @@ -25,4 +32,6 @@ async fn main() { // for some strange reason we need to add a leading `/` to the windows path even though it's // not a valid windows path... but hey, it works... println!("cargo:rustc-env=DATABASE_URL=sqlite:///{}", &database_path); + + Ok(()) } diff --git a/common/http-api-client-macro/Cargo.toml b/common/http-api-client-macro/Cargo.toml new file mode 100644 index 0000000000..ea23bbd23a --- /dev/null +++ b/common/http-api-client-macro/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "nym-http-api-client-macro" +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 + +[lib] +proc-macro = true + +[dependencies] +proc-macro2 = "1" +syn = { workspace = true, features = ["full"] } +quote = "1.0.40" +proc-macro-crate = "3" +uuid = { version = "1.0", features = ["v4"] } + +[dev-dependencies] +nym-http-api-client = { path = "../http-api-client" } +reqwest = { workspace = true } + +[features] +debug-inventory = [] + +[lints] +workspace = true diff --git a/common/http-api-client-macro/build.rs b/common/http-api-client-macro/build.rs new file mode 100644 index 0000000000..69b80f56e8 --- /dev/null +++ b/common/http-api-client-macro/build.rs @@ -0,0 +1,13 @@ +use std::env; + +fn main() { + // Enable debug output during build + if env::var("CARGO_FEATURE_DEBUG_INVENTORY").is_ok() || env::var("DEBUG_HTTP_INVENTORY").is_ok() + { + println!("cargo:warning=HTTP Client Inventory Debug Enabled"); + println!("cargo:rustc-cfg=debug_inventory"); + } + + // Force rebuild when this environment variable changes + println!("cargo:rerun-if-env-changed=DEBUG_HTTP_INVENTORY"); +} diff --git a/common/http-api-client-macro/src/lib.rs b/common/http-api-client-macro/src/lib.rs new file mode 100644 index 0000000000..cd1c1dd179 --- /dev/null +++ b/common/http-api-client-macro/src/lib.rs @@ -0,0 +1,388 @@ +//! Proc-macros for configuring HTTP clients globally via the `inventory` crate. +//! +//! This crate provides macros that allow any crate in the workspace to contribute +//! configuration modifications to `reqwest::ClientBuilder` instances through a +//! compile-time registry pattern. +//! +//! # Overview +//! +//! The macros work by: +//! 1. Collecting configuration functions from across all crates at compile time +//! 2. Sorting them by priority (lower numbers run first) +//! 3. Applying them sequentially to build HTTP clients with consistent settings +//! +//! # Examples +//! +//! ## Basic Usage with `client_defaults!` +//! +//! ```ignore +//! use nym_http_api_client_macro::client_defaults; +//! +//! // Register default configurations with priority +//! client_defaults!( +//! priority = 10; // Optional, defaults to 0 +//! timeout = std::time::Duration::from_secs(30), +//! gzip = true, +//! user_agent = "MyApp/1.0" +//! ); +//! ``` +//! +//! ## Using `client_cfg!` for one-off configurations +//! +//! ```ignore +//! use nym_http_api_client_macro::client_cfg; +//! +//! let configure = client_cfg!( +//! timeout = std::time::Duration::from_secs(60), +//! default_headers { +//! "X-Custom-Header" => "value", +//! "Authorization" => "auth_token" +//! } +//! ); +//! +//! let builder = reqwest::ClientBuilder::new(); +//! let configured = configure(builder); +//! ``` +//! +//! # DSL Reference +//! +//! The macro DSL supports several patterns: +//! - `key = value` - Calls `builder.key(value)` +//! - `key(arg1, arg2)` - Calls `builder.key(arg1, arg2)` +//! - `flag` - Calls `builder.flag()` with no arguments +//! - `default_headers { "name" => "value", ... }` - Sets default headers +//! +//! # Priority System +//! +//! Configurations are applied in priority order (lower numbers first): +//! - Negative priorities: Early configuration (e.g., -100 for base settings) +//! - Zero (default): Standard configuration +//! - Positive priorities: Late configuration (e.g., 100 for overrides) + +use proc_macro::TokenStream; +use proc_macro2::{Span, TokenStream as TokenStream2}; +use proc_macro_crate::{crate_name, FoundCrate}; +use quote::{format_ident, quote}; +use syn::{ + braced, + parse::{Parse, ParseStream}, + parse_macro_input, + punctuated::Punctuated, + token, Expr, Ident, LitInt, Result, Token, +}; + +// ------------------ core crate path resolution ------------------ + +fn core_path() -> TokenStream2 { + match crate_name("nym-http-api-client") { + Ok(FoundCrate::Itself) => quote!(crate), + Ok(FoundCrate::Name(name)) => { + let ident = Ident::new(&name, Span::call_site()); + quote!( ::#ident ) + } + Err(_) => { + // Fallback if the crate is not found by name (unlikely if deps set up correctly) + quote!(::nym_http_api_client) + } + } +} + +// ------------------ DSL parsing ------------------ + +struct Items(Punctuated); +impl Parse for Items { + fn parse(input: ParseStream<'_>) -> Result { + Ok(Self(Punctuated::parse_terminated(input)?)) + } +} + +enum Item { + Assign { + key: Ident, + _eq: Token![=], + value: Expr, + }, // foo = EXPR + Call { + key: Ident, + args: Punctuated, + _p: token::Paren, + }, // foo(a,b) + DefaultHeaders { + _key: Ident, + map: HeaderMapInit, + }, // default_headers { ... } + Flag { + key: Ident, + }, // foo +} + +impl Parse for Item { + fn parse(input: ParseStream<'_>) -> Result { + let key: Ident = input.parse()?; + + if input.peek(Token![=]) { + let _eq: Token![=] = input.parse()?; + let value: Expr = input.parse()?; + return Ok(Self::Assign { key, _eq, value }); + } + if input.peek(token::Paren) { + let content; + let _p = syn::parenthesized!(content in input); + let args = Punctuated::::parse_terminated(&content)?; + return Ok(Self::Call { key, args, _p }); + } + if input.peek(token::Brace) && key == format_ident!("default_headers") { + let map = input.parse::()?; + return Ok(Self::DefaultHeaders { _key: key, map }); + } + Ok(Self::Flag { key }) + } +} + +struct HeaderPair { + k: Expr, + _arrow: Token![=>], + v: Expr, +} +impl Parse for HeaderPair { + fn parse(input: ParseStream<'_>) -> Result { + Ok(Self { + k: input.parse()?, + _arrow: input.parse()?, + v: input.parse()?, + }) + } +} + +struct HeaderMapInit { + _brace: token::Brace, + pairs: Punctuated, +} +impl Parse for HeaderMapInit { + fn parse(input: ParseStream<'_>) -> Result { + let content; + let _brace = braced!(content in input); + let pairs = Punctuated::::parse_terminated(&content)?; + Ok(Self { _brace, pairs }) + } +} + +// Generate statements that mutate a builder named `b` using the resolved core path. +fn to_stmts(items: Items, core: &TokenStream2) -> TokenStream2 { + let mut stmts = Vec::new(); + for it in items.0 { + match it { + Item::Assign { key, value, .. } => { + let m = key; + stmts.push(quote! { b = b.#m(#value); }); + } + Item::Call { key, args, .. } => { + let m = key; + let args = args.iter(); + stmts.push(quote! { b = b.#m( #( #args ),* ); }); + } + Item::DefaultHeaders { map, .. } => { + let (ks, vs): (Vec<_>, Vec<_>) = map.pairs.into_iter().map(|p| (p.k, p.v)).unzip(); + stmts.push(quote! { + let mut __cm = #core::reqwest::header::HeaderMap::new(); + #( + { + use #core::reqwest::header::{HeaderName, HeaderValue}; + let __k = HeaderName::try_from(#ks) + .unwrap_or_else(|e| panic!("Invalid header name: {}", e)); + let __v = HeaderValue::try_from(#vs) + .unwrap_or_else(|e| panic!("Invalid header value: {}", e)); + __cm.insert(__k, __v); + } + )* + b = b.default_headers(__cm); + }); + } + Item::Flag { key } => { + let m = key; + stmts.push(quote! { b = b.#m(); }); + } + } + } + quote! { #(#stmts)* } +} + +// ------------------ client_cfg! ------------------ + +/// Creates a closure that configures a `ReqwestClientBuilder`. +/// +/// This macro generates a closure that can be used to configure a single +/// `reqwest::ClientBuilder` instance without affecting global defaults. +/// +/// # Example +/// +/// ```ignore +/// use nym_http_api_client_macro::client_cfg; +/// +/// let config = client_cfg!( +/// timeout = std::time::Duration::from_secs(30), +/// gzip = true +/// ); +/// let client = config(reqwest::ClientBuilder::new()).build().unwrap(); +/// ``` +#[proc_macro] +pub fn client_cfg(input: TokenStream) -> TokenStream { + let items = parse_macro_input!(input as Items); + let core = core_path(); + let body = to_stmts(items, &core); + let out = quote! { + |mut b: #core::ReqwestClientBuilder| { #body b } + }; + out.into() +} + +// ------------------ client_defaults! with optional priority header ------------------ + +struct MaybePrioritized { + priority: i32, + items: Items, +} +impl Parse for MaybePrioritized { + fn parse(input: ParseStream<'_>) -> Result { + // Optional header: `priority = ;` + let fork = input.fork(); + let mut priority = 0i32; + if fork.peek(Ident) && fork.parse::()? == "priority" && fork.peek(Token![=]) { + // commit + let _ = input.parse::()?; // priority + let _ = input.parse::()?; + let lit: LitInt = input.parse()?; + priority = lit.base10_parse()?; + let _ = input.parse::()?; + } + let items = input.parse::()?; + Ok(Self { priority, items }) + } +} + +/// Registers global default configurations for HTTP clients. +/// +/// This macro submits a configuration record to the global registry that will +/// be applied to all HTTP clients created with `default_builder()`. +/// +/// # Parameters +/// +/// - `priority` (optional): Integer priority for ordering (lower runs first, default: 0) +/// - Configuration items: Any valid `reqwest::ClientBuilder` method calls +/// +/// # Example +/// +/// ```ignore +/// use nym_http_api_client_macro::client_defaults; +/// +/// client_defaults!( +/// priority = -50; // Run early in the configuration chain +/// connect_timeout = std::time::Duration::from_secs(10), +/// pool_max_idle_per_host = 32, +/// default_headers { +/// "User-Agent" => "MyApp/1.0", +/// "Accept" => "application/json" +/// } +/// ); +/// ``` +#[proc_macro] +pub fn client_defaults(input: TokenStream) -> TokenStream { + let MaybePrioritized { priority, items } = parse_macro_input!(input as MaybePrioritized); + let core = core_path(); + + // Generate a description of what this config does (before consuming items) + let config_description = if cfg!(feature = "debug-inventory") { + let descriptions = items + .0 + .iter() + .map(|item| match item { + Item::Assign { key, value, .. } => { + format!("{}={:?}", quote!(#key), quote!(#value).to_string()) + } + Item::Call { key, args, .. } => { + let args_str = args + .iter() + .map(|a| quote!(#a).to_string()) + .collect::>() + .join(", "); + format!("{}({})", quote!(#key), args_str) + } + Item::Flag { key } => { + format!("{}()", quote!(#key)) + } + Item::DefaultHeaders { .. } => "default_headers{{...}}".to_string(), + }) + .collect::>() + .join(", "); + + quote! { + pub const __CONFIG_DESC: &str = #descriptions; + } + } else { + quote! {} + }; + + // Now consume items to generate the body + let body = to_stmts(items, &core); + + // Generate a unique identifier for this submission + let submission_id = format!("__client_defaults_{}", uuid::Uuid::new_v4().simple()); + let submission_ident = syn::Ident::new(&submission_id, proc_macro2::Span::call_site()); + + // Debug output at compile time if enabled + if std::env::var("DEBUG_HTTP_INVENTORY").is_ok() { + eprintln!( + "cargo:warning=[HTTP-INVENTORY] Registering config with priority={} from {}", + priority, + std::env::var("CARGO_PKG_NAME").unwrap_or_else(|_| "unknown".to_string()) + ); + } + + // Add debug_print_inventory call if the feature is enabled + let debug_call = if cfg!(feature = "debug-inventory") { + quote! { + #config_description + + // Ensure the debug function gets called when config is applied + pub fn __cfg_with_debug( + b: #core::ReqwestClientBuilder + ) -> #core::ReqwestClientBuilder { + eprintln!("[HTTP-INVENTORY] Applying: {} (priority={})", __CONFIG_DESC, #priority); + __cfg(b) + } + } + } else { + quote! {} + }; + + // Use the debug wrapper if feature is enabled + let apply_fn = if cfg!(feature = "debug-inventory") { + quote! { __cfg_with_debug } + } else { + quote! { __cfg } + }; + + let out = quote! { + #[allow(non_snake_case)] + mod #submission_ident { + use super::*; + #[allow(unused)] + pub fn __cfg( + mut b: #core::ReqwestClientBuilder + ) -> #core::ReqwestClientBuilder { + #body + b + } + + #debug_call + + #core::inventory::submit! { + #core::registry::ConfigRecord { + priority: #priority, + apply: #apply_fn, + } + } + } + }; + out.into() +} diff --git a/common/http-api-client-macro/tests/macro_tests.rs b/common/http-api-client-macro/tests/macro_tests.rs new file mode 100644 index 0000000000..2696abb0f8 --- /dev/null +++ b/common/http-api-client-macro/tests/macro_tests.rs @@ -0,0 +1,64 @@ +use nym_http_api_client_macro::{client_cfg, client_defaults}; +use std::time::Duration; + +#[test] +fn test_client_cfg_basic() { + // Test that the macro compiles with basic configuration + let _config = client_cfg!(timeout = Duration::from_secs(30), gzip = true); +} + +#[test] +fn test_client_cfg_with_headers() { + // Test that the macro compiles with default headers + let _config = client_cfg!( + timeout = Duration::from_secs(30), + default_headers { + "User-Agent" => "TestApp/1.0", + "Accept" => "application/json" + } + ); +} + +#[test] +fn test_client_cfg_with_method_calls() { + // Test that the macro compiles with method calls + let _config = client_cfg!( + pool_max_idle_per_host = 32, + tcp_nodelay = true, + danger_accept_invalid_certs = true + ); +} + +#[test] +fn test_client_defaults_with_priority() { + // Test that client_defaults macro compiles with priority + client_defaults!( + priority = -100; + gzip = true, + deflate = true + ); +} + +#[test] +fn test_client_defaults_without_priority() { + // Test that client_defaults macro compiles without priority (defaults to 0) + client_defaults!(brotli = true, zstd = true); +} + +#[test] +fn test_empty_client_cfg() { + // Test that empty configuration compiles + let _config = client_cfg!(); +} + +// Integration test to verify the closure actually works +#[test] +fn test_client_cfg_closure_application() { + let config = client_cfg!(gzip = true); + + // Apply the configuration to a new builder + let builder = reqwest::ClientBuilder::new(); + let _configured_builder = config(builder); + // Note: We can't easily test the internal state of the builder, + // but we verify it compiles and runs without panic +} diff --git a/common/http-api-client/Cargo.toml b/common/http-api-client/Cargo.toml index 853b79b9a5..35da8e0e11 100644 --- a/common/http-api-client/Cargo.toml +++ b/common/http-api-client/Cargo.toml @@ -13,19 +13,25 @@ license.workspace = true [features] default=["tunneling"] tunneling=[] +network-defaults = ["dep:nym-network-defaults"] +debug-inventory = ["nym-http-api-client-macro/debug-inventory"] [dependencies] async-trait = { workspace = true } bincode = { workspace = true } +cfg-if = { workspace = true} reqwest = { workspace = true, features = ["json", "gzip", "deflate", "brotli", "zstd", "rustls-tls"] } http.workspace = true url = { workspace = true } once_cell = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } +serde_yaml = { workspace = true} +serde_plain = { workspace = true } thiserror = { workspace = true } tracing = { workspace = true } itertools = { workspace = true } +inventory = { workspace = true } # used for decoding text responses (they were already implicitly included) bytes = { workspace = true } @@ -34,6 +40,8 @@ mime = { workspace = true } nym-http-api-common = { path = "../http-api-common", default-features = false } nym-bin-common = { path = "../bin-common" } +nym-network-defaults = { path = "../network-defaults", optional = true } +nym-http-api-client-macro = { path = "../http-api-client-macro" } [target."cfg(not(target_arch = \"wasm32\"))".dependencies] hickory-resolver = { workspace = true, features = ["https-ring", "tls-ring", "webpki-roots"] } diff --git a/common/http-api-client/examples/debug_inventory.rs b/common/http-api-client/examples/debug_inventory.rs new file mode 100644 index 0000000000..e19a9c05dd --- /dev/null +++ b/common/http-api-client/examples/debug_inventory.rs @@ -0,0 +1,26 @@ +use nym_http_api_client::registry; + +fn main() { + println!("Debugging HTTP Client Inventory"); + println!("================================"); + + // Print all registered configurations + registry::debug_print_inventory(); + + // Also print the count + println!( + "\nTotal registered configs: {}", + registry::registered_config_count() + ); + + // Show the detailed breakdown + println!("\nDetailed configuration list:"); + for (i, (priority, ptr)) in registry::inspect_registered_configs().iter().enumerate() { + println!( + " Config #{}: priority={}, function=0x{:x}", + i + 1, + priority, + ptr + ); + } +} diff --git a/common/http-api-client/examples/test_timeout.rs b/common/http-api-client/examples/test_timeout.rs new file mode 100644 index 0000000000..fe3658e34b --- /dev/null +++ b/common/http-api-client/examples/test_timeout.rs @@ -0,0 +1,99 @@ +use nym_http_api_client::registry; +use nym_http_api_client::{inventory, ReqwestClientBuilder}; +use nym_http_api_client_macro::client_defaults; +use std::time::{Duration, Instant}; + +#[tokio::main] +async fn main() { + println!("Testing HTTP Client Timeout Configuration"); + println!("==========================================\n"); + + client_defaults!(timeout = std::time::Duration::from_secs(300),); + + // Build a client using the registry (should have 300s timeout) + let client = registry::build_client().expect("Failed to build client"); + + println!("Testing timeout behavior..."); + println!("The inventory should have set timeout to 300 seconds"); + + // Test 1: Try a request to a slow endpoint that delays for 5 seconds + // This should succeed since timeout is 300s + println!("\nTest 1: Request with 5 second delay (should succeed)"); + let start = Instant::now(); + match client.get("https://httpbin.org/delay/5").send().await { + Ok(_) => { + let elapsed = start.elapsed(); + println!("✓ Request succeeded after {:?}", elapsed); + } + Err(e) => { + let elapsed = start.elapsed(); + if e.is_timeout() { + println!( + "✗ Request timed out after {:?} - timeout might be shorter than expected!", + elapsed + ); + } else { + println!("✗ Request failed after {:?}: {}", elapsed, e); + } + } + } + + // Test 2: Try to inspect the client's actual configuration + println!("\nTest 2: Client debug information"); + println!("Client Debug: {:?}", client); + + // Test 3: Create a client with explicit short timeout to compare behavior + println!("\nTest 3: Control test with 2 second timeout"); + let short_timeout_client = reqwest::Client::builder() + .timeout(Duration::from_secs(2)) + .build() + .expect("Failed to build short timeout client"); + + let start = Instant::now(); + match short_timeout_client + .get("https://httpbin.org/delay/5") + .send() + .await + { + Ok(_) => { + let elapsed = start.elapsed(); + println!( + "✗ Request succeeded after {:?} - timeout not working!", + elapsed + ); + } + Err(e) => { + let elapsed = start.elapsed(); + if e.is_timeout() { + println!("✓ Request timed out as expected after {:?}", elapsed); + } else { + println!("? Request failed after {:?}: {}", elapsed, e); + } + } + } + + // Test 4: Create a client through the registry and verify timeout on a hanging connection + println!("\nTest 4: Testing with a connection that hangs"); + println!("Making request to an endpoint that will hang..."); + + let start = Instant::now(); + // This IP is reserved for documentation and will hang + match client.get("http://192.0.2.1:81").send().await { + Ok(_) => { + println!("✗ Request succeeded - unexpected!"); + } + Err(e) => { + let elapsed = start.elapsed(); + if e.is_timeout() { + println!("✓ Request timed out after {:?}", elapsed); + if elapsed < Duration::from_secs(290) { + println!(" Note: Timeout occurred faster than 300s, might be connection timeout not total timeout"); + } + } else if e.is_connect() { + println!("✓ Connection failed after {:?} (connect timeout)", elapsed); + } else { + println!("? Request failed after {:?}: {}", elapsed, e); + } + } + } +} diff --git a/common/http-api-client/src/fronted.rs b/common/http-api-client/src/fronted.rs index f3ff92b129..f41ce8f3cf 100644 --- a/common/http-api-client/src/fronted.rs +++ b/common/http-api-client/src/fronted.rs @@ -54,10 +54,14 @@ impl Front { #[derive(Debug, Default, PartialEq, Clone)] #[cfg(feature = "tunneling")] +/// Policy for when to use domain fronting for HTTP requests. pub enum FrontPolicy { + /// Always use domain fronting for all requests. Always, + /// Only use domain fronting when retrying failed requests. OnRetry, #[default] + /// Never use domain fronting. Off, } @@ -96,14 +100,14 @@ mod tests { // Some(vec!["https://cdn77.com"]), // ).unwrap(); // cdn77 - let client = ClientBuilder::new::<_, &str>(url1) + let client = ClientBuilder::new(url1) .expect("bad url") .with_fronting(FrontPolicy::Always) - .build::<&str>() + .build() .expect("failed to build client"); let response = client - .send_request::<_, (), &str, &str, &str>( + .send_request::<_, (), &str, &str>( reqwest::Method::GET, &["api", "v1", "network", "details"], NO_PARAMS, diff --git a/common/http-api-client/src/lib.rs b/common/http-api-client/src/lib.rs index 64359c96e7..1e05f514fb 100644 --- a/common/http-api-client/src/lib.rs +++ b/common/http-api-client/src/lib.rs @@ -15,7 +15,7 @@ //! # use url::Url; //! # use nym_http_api_client::{ApiClient, NO_PARAMS, HttpClientError}; //! -//! # type Err = HttpClientError; +//! # type Err = HttpClientError; //! # async fn run() -> Result<(), Err> { //! let url: Url = "https://nymvpn.com".parse()?; //! let client = nym_http_api_client::Client::new(url, None); @@ -51,7 +51,7 @@ //! Up, //! } //! -//! # type Err = HttpClientError; +//! # type Err = HttpClientError; //! # async fn run() -> Result<(), Err> { //! // This will POST a body of `{"lang":"rust","body":"json"}` //! let mut map = HashMap::new(); @@ -114,7 +114,7 @@ //! } //! } //! -//! pub type SpecificAPIError = HttpClientError; +//! pub type SpecificAPIError = HttpClientError; //! //! #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] //! #[cfg_attr(not(target_arch = "wasm32"), async_trait)] @@ -136,12 +136,18 @@ //! ``` #![warn(missing_docs)] +pub use inventory; +pub use reqwest; +pub use reqwest::ClientBuilder as ReqwestClientBuilder; pub use reqwest::StatusCode; +use std::error::Error; + +pub mod registry; use crate::path::RequestPath; use async_trait::async_trait; use bytes::Bytes; -use http::header::CONTENT_TYPE; +use http::header::{ACCEPT, CONTENT_TYPE}; use http::HeaderMap; use itertools::Itertools; use mime::Mime; @@ -161,6 +167,8 @@ use std::sync::Arc; #[cfg(feature = "tunneling")] mod fronted; +#[cfg(feature = "tunneling")] +pub use fronted::FrontPolicy; mod url; pub use url::{IntoUrl, Url}; mod user_agent; @@ -174,8 +182,12 @@ mod path; pub use dns::{HickoryDnsError, HickoryDnsResolver}; // helper for generating user agent based on binary information +#[cfg(not(target_arch = "wasm32"))] +use crate::registry::default_builder; #[doc(hidden)] pub use nym_bin_common::bin_info; +#[cfg(not(target_arch = "wasm32"))] +use nym_http_api_client_macro::client_defaults; /// Default HTTP request connection timeout. /// @@ -183,6 +195,17 @@ pub use nym_bin_common::bin_info; /// high and chatty protocols take a while to complete. pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30); +#[cfg(not(target_arch = "wasm32"))] +client_defaults!( + priority = -100; + gzip = true, + deflate = true, + brotli = true, + zstd = true, + timeout = DEFAULT_TIMEOUT, + user_agent = format!("nym-http-api-client/{}", env!("CARGO_PKG_VERSION")) +); + /// Collection of URL Path Segments pub type PathSegments<'a> = &'a [&'a str]; /// Collection of HTTP Request Parameters @@ -191,53 +214,206 @@ pub type Params<'a, K, V> = &'a [(K, V)]; /// Empty collection of HTTP Request Parameters. pub const NO_PARAMS: Params<'_, &'_ str, &'_ str> = &[]; +/// Serialization format for API requests and responses +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SerializationFormat { + /// Use JSON serialization (default, always works) + Json, + /// Use bincode serialization (must be explicitly opted into) + Bincode, + /// Use YAML serialization + Yaml, + /// Use Text serialization + Text, +} + +impl Display for SerializationFormat { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + SerializationFormat::Json => write!(f, "json"), + SerializationFormat::Bincode => write!(f, "bincode"), + SerializationFormat::Yaml => write!(f, "yaml"), + SerializationFormat::Text => write!(f, "text"), + } + } +} + +impl SerializationFormat { + #[allow(missing_docs)] + pub fn content_type(&self) -> String { + match self { + SerializationFormat::Json => "application/json".to_string(), + SerializationFormat::Bincode => "application/bincode".to_string(), + SerializationFormat::Yaml => "application/yaml".to_string(), + SerializationFormat::Text => "text/plain".to_string(), + } + } +} + +#[allow(missing_docs)] +#[derive(Debug)] +pub struct ReqwestErrorWrapper(reqwest::Error); + +impl Display for ReqwestErrorWrapper { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + cfg_if::cfg_if! { + if #[cfg(not(target_arch = "wasm32"))] { + if self.0.is_connect() { + write!(f, "failed to connect: ")?; + } + } + } + + if self.0.is_timeout() { + write!(f, "timed out: ")?; + } + if self.0.is_redirect() { + if let Some(final_stop) = self.0.url() { + write!(f, "redirect loop at {final_stop}: ")?; + } + } + + self.0.fmt(f)?; + if let Some(status_code) = self.0.status() { + write!(f, " status: {status_code}")?; + } else { + write!(f, " unknown status code")?; + } + + if let Some(source) = self.0.source() { + write!(f, " source: {source}")?; + } else { + write!(f, " unknown lower-level error source")?; + } + + Ok(()) + } +} + +impl std::error::Error for ReqwestErrorWrapper {} + /// The Errors that may occur when creating or using an HTTP client. #[derive(Debug, Error)] #[allow(missing_docs)] -pub enum HttpClientError { +pub enum HttpClientError { + #[error("failed to construct inner reqwest client: {source}")] + ReqwestBuildError { + #[source] + source: reqwest::Error, + }, + + #[deprecated( + note = "use another more strongly typed variant - this variant is only left for compatibility reasons" + )] + #[error("request failed with error message: {0}")] + GenericRequestFailure(String), + + #[deprecated( + note = "use another more strongly typed variant - this variant is only left for compatibility reasons" + )] #[error("there was an issue with the REST request: {source}")] ReqwestClientError { #[from] source: reqwest::Error, }, + #[error("failed to parse {raw} as a valid URL: {source}")] + MalformedUrl { + raw: String, + #[source] + source: reqwest::Error, + }, + + #[error("failed to send request for {url}: {source}")] + RequestSendFailure { + url: reqwest::Url, + #[source] + source: ReqwestErrorWrapper, + }, + + #[error("failed to read response body from {url}: {source}")] + ResponseReadFailure { + url: reqwest::Url, + headers: Box, + status: StatusCode, + #[source] + source: ReqwestErrorWrapper, + }, + #[error("failed to deserialize received response: {source}")] ResponseDeserialisationFailure { source: serde_json::Error }, #[error("provided url is malformed: {source}")] - MalformedUrl { + UrlParseFailure { #[from] source: url::ParseError, }, - #[error("the requested resource could not be found")] - NotFound, + #[error("the requested resource could not be found at {url}")] + NotFound { url: reqwest::Url }, - #[error("request failed with error message: {0}")] - GenericRequestFailure(String), + #[error("attempted to use domain fronting and clone a request containing stream data")] + AttemptedToCloneStreamRequest, - #[error("the request failed with status '{status}'. no additional error message provided")] - RequestFailure { status: StatusCode }, + // #[error("request failed with error message: {0}")] + // GenericRequestFailure(String), + // + #[error("the request for {url} failed with status '{status}'. no additional error message provided. response headers: {headers:?}")] + RequestFailure { + url: reqwest::Url, + status: StatusCode, + headers: Box, + }, - #[error("the returned response was empty. status: '{status}'")] - EmptyResponse { status: StatusCode }, + #[error( + "the returned response from {url} was empty. status: '{status}'. response headers: {headers:?}" + )] + EmptyResponse { + url: reqwest::Url, + status: StatusCode, + headers: Box, + }, - #[error("failed to resolve request. status: '{status}', additional error message: {error}")] - EndpointFailure { status: StatusCode, error: E }, + #[error("failed to resolve request for {url}. status: '{status}'. response headers: {headers:?}. additional error message: {error}")] + EndpointFailure { + url: reqwest::Url, + status: StatusCode, + headers: Box, + error: String, + }, #[error("failed to decode response body: {message} from {content}")] ResponseDecodeFailure { message: String, content: String }, + #[error("failed to resolve request to {url} due to data inconsistency: {details}")] + InternalResponseInconsistency { url: ::url::Url, details: String }, + + #[error("Failed to encode bincode: {0}")] + Bincode(#[from] bincode::Error), + + #[error("Failed to json: {0}")] + Json(#[from] serde_json::Error), + + #[error("Failed to yaml: {0}")] + Yaml(#[from] serde_yaml::Error), + + #[error("Failed to plain: {0}")] + Plain(#[from] serde_plain::Error), + #[cfg(target_arch = "wasm32")] #[error("the request has timed out")] RequestTimeout, } +#[allow(missing_docs)] +#[allow(deprecated)] impl HttpClientError { /// Returns true if the error is a timeout. pub fn is_timeout(&self) -> bool { match self { HttpClientError::ReqwestClientError { source } => source.is_timeout(), + HttpClientError::RequestSendFailure { source, .. } => source.0.is_timeout(), + HttpClientError::ResponseReadFailure { source, .. } => source.0.is_timeout(), #[cfg(target_arch = "wasm32")] HttpClientError::RequestTimeout => true, _ => false, @@ -247,12 +423,24 @@ impl HttpClientError { /// Returns the HTTP status code if available. pub fn status_code(&self) -> Option { match self { - HttpClientError::RequestFailure { status } => Some(*status), - HttpClientError::EmptyResponse { status } => Some(*status), + HttpClientError::ResponseReadFailure { status, .. } => Some(*status), + HttpClientError::RequestFailure { status, .. } => Some(*status), + HttpClientError::EmptyResponse { status, .. } => Some(*status), HttpClientError::EndpointFailure { status, .. } => Some(*status), _ => None, } } + + pub fn reqwest_client_build_error(source: reqwest::Error) -> Self { + HttpClientError::ReqwestBuildError { source } + } + + pub fn request_send_error(url: reqwest::Url, source: reqwest::Error) -> Self { + HttpClientError::RequestSendFailure { + url, + source: ReqwestErrorWrapper(source), + } + } } /// Core functionality required for types acting as API clients. @@ -269,8 +457,8 @@ pub trait ApiClientCore { method: reqwest::Method, path: P, params: Params<'_, K, V>, - json_body: Option<&B>, - ) -> RequestBuilder + body: Option<&B>, + ) -> Result where P: RequestPath, B: Serialize + ?Sized, @@ -296,8 +484,8 @@ pub trait ApiClientCore { &self, method: reqwest::Method, endpoint: S, - json_body: Option<&B>, - ) -> RequestBuilder + body: Option<&B>, + ) -> Result where B: Serialize + ?Sized, S: AsRef, @@ -323,7 +511,7 @@ pub trait ApiClientCore { }; let params: Vec<(String, String)> = standin_url.query_pairs().into_owned().collect(); - self.create_request(method, path.as_slice(), ¶ms, json_body) + self.create_request(method, path.as_slice(), ¶ms, body) } /// Send a created HTTP request. @@ -331,26 +519,23 @@ pub trait ApiClientCore { /// A [`RequestBuilder`] can be created with [`ApiClientCore::create_request`] or /// [`ApiClientCore::create_request_endpoint`] or if absolutely necessary, using reqwest /// tooling directly. - async fn send(&self, request: RequestBuilder) -> Result> - where - E: Display; + async fn send(&self, request: RequestBuilder) -> Result; /// Create and send a created HTTP request. - async fn send_request( + async fn send_request( &self, method: reqwest::Method, path: P, params: Params<'_, K, V>, json_body: Option<&B>, - ) -> Result> + ) -> Result where P: RequestPath + Send + Sync, B: Serialize + ?Sized + Sync, K: AsRef + Sync, V: AsRef + Sync, - E: Display, { - let req = self.create_request(method, path, params, json_body); + let req = self.create_request(method, path, params, json_body)?; self.send(req).await } } @@ -370,16 +555,16 @@ pub struct ClientBuilder { front: Option, retry_limit: usize, + serialization: SerializationFormat, } impl ClientBuilder { /// Constructs a new `ClientBuilder`. /// /// This is the same as `Client::builder()`. - pub fn new(url: U) -> Result> + pub fn new(url: U) -> Result where U: IntoUrl, - E: Display, { let str_url = url.as_str(); @@ -395,6 +580,50 @@ impl ClientBuilder { } } + /// Create a client builder from network details with sensible defaults + #[cfg(feature = "network-defaults")] + pub fn from_network( + network: &nym_network_defaults::NymNetworkDetails, + ) -> Result { + let urls = network + .nym_api_urls + .as_ref() + .ok_or_else(|| { + HttpClientError::GenericRequestFailure( + "No API URLs configured in network details".to_string(), + ) + })? + .iter() + .map(|api_url| { + // Convert ApiUrl to our Url type with fronting support + let mut url = Url::parse(&api_url.url)?; + + // Add fronting domains if available + #[cfg(feature = "tunneling")] + if let Some(ref front_hosts) = api_url.front_hosts { + let fronts: Vec = front_hosts + .iter() + .map(|host| format!("https://{}", host)) + .collect(); + url = Url::new(api_url.url.clone(), Some(fronts)) + .map_err(|e| HttpClientError::GenericRequestFailure(e.to_string()))?; + } + + Ok(url) + }) + .collect::, HttpClientError>>()?; + + let mut builder = Self::new_with_urls(urls); + + // Enable domain fronting by default (on retry) + #[cfg(feature = "tunneling")] + { + builder = builder.with_fronting(FrontPolicy::OnRetry); + } + + Ok(builder) + } + /// Constructs a new http `ClientBuilder` from a valid url. pub fn new_with_urls(urls: Vec) -> Self { let urls = Self::check_urls(urls); @@ -403,20 +632,7 @@ impl ClientBuilder { let reqwest_client_builder = reqwest::ClientBuilder::new(); #[cfg(not(target_arch = "wasm32"))] - let reqwest_client_builder = { - // Note: I believe the manual enable calls for the compression methods are extra - // as the various compression features for `reqwest` crate should be enabled - // just by including the feature which: - // `"Enable[s] auto decompression by checking the Content-Encoding response header."` - // - // I am going to leave these here anyways so that removing a decompression method - // from the features list will throw an error if it is not also removed here. - reqwest::ClientBuilder::new() - .gzip(true) - .deflate(true) - .brotli(true) - .zstd(true) - }; + let reqwest_client_builder = default_builder(); ClientBuilder { urls, @@ -428,6 +644,7 @@ impl ClientBuilder { front: None, retry_limit: 0, + serialization: SerializationFormat::Json, } } @@ -500,11 +717,19 @@ impl ClientBuilder { self } + /// Set the serialization format for API requests and responses + pub fn with_serialization(mut self, format: SerializationFormat) -> Self { + self.serialization = format; + self + } + + /// Configure the client to use bincode serialization + pub fn with_bincode(self) -> Self { + self.with_serialization(SerializationFormat::Bincode) + } + /// Returns a Client that uses this ClientBuilder configuration. - pub fn build(self) -> Result> - where - E: Display, - { + pub fn build(self) -> Result { #[cfg(target_arch = "wasm32")] let reqwest_client = self.reqwest_client_builder.build()?; @@ -512,22 +737,16 @@ impl ClientBuilder { // but that'd break bunch of things due to type changes #[cfg(not(target_arch = "wasm32"))] let reqwest_client = { - let mut builder = self - .reqwest_client_builder - .timeout(self.timeout.unwrap_or(DEFAULT_TIMEOUT)); - - // if no custom user agent was set, use a default - if !self.custom_user_agent { - builder = - builder.user_agent(format!("nym-http-api-client/{}", env!("CARGO_PKG_VERSION"))) - } + let mut builder = self.reqwest_client_builder; // unless explicitly disabled use the DoT/DoH enabled resolver if self.use_secure_dns { builder = builder.dns_resolver(Arc::new(HickoryDnsResolver::default())); } - builder.build()? + builder + .build() + .map_err(HttpClientError::reqwest_client_build_error)? }; let client = Client { @@ -541,6 +760,7 @@ impl ClientBuilder { #[cfg(target_arch = "wasm32")] request_timeout: self.timeout.unwrap_or(DEFAULT_TIMEOUT), retry_limit: self.retry_limit, + serialization: self.serialization, }; Ok(client) @@ -561,6 +781,7 @@ pub struct Client { request_timeout: Duration, retry_limit: usize, + serialization: SerializationFormat, } impl Client { @@ -570,16 +791,15 @@ impl Client { // In order to prevent interference in API requests at the DNS phase we default to a resolver // that uses DoT and DoH. pub fn new(base_url: ::url::Url, timeout: Option) -> Self { - Self::new_url::<_, String>(base_url, timeout).expect( + Self::new_url(base_url, timeout).expect( "we provided valid url and we were unwrapping previous construction errors anyway", ) } /// Attempt to create a new http client from a something that can be converted to a URL - pub fn new_url(url: U, timeout: Option) -> Result> + pub fn new_url(url: U, timeout: Option) -> Result where U: IntoUrl, - E: Display, { let builder = Self::builder(url)?; match timeout { @@ -591,10 +811,9 @@ impl Client { /// Creates a [`ClientBuilder`] to configure a [`Client`]. /// /// This is the same as [`ClientBuilder::new()`]. - pub fn builder(url: U) -> Result> + pub fn builder(url: U) -> Result where U: IntoUrl, - E: Display, { ClientBuilder::new(url) } @@ -618,6 +837,7 @@ impl Client { #[cfg(target_arch = "wasm32")] request_timeout: self.request_timeout, + serialization: self.serialization, } } @@ -693,26 +913,42 @@ impl Client { /// this method. For example, if the client is configured to rotate hosts after each error, this /// method should be called after the host has been updated -- i.e. as part of the subsequent /// send. - fn apply_hosts_to_req(&self, r: &mut reqwest::Request) { + fn apply_hosts_to_req(&self, r: &mut reqwest::Request) -> (&str, Option<&str>) { let url = self.current_url(); r.url_mut().set_host(url.host_str()).unwrap(); #[cfg(feature = "tunneling")] if let Some(ref front) = self.front { if front.is_enabled() { - // this should never fail as we are transplanting the host from one url to another - r.url_mut().set_host(url.front_str()).unwrap(); + if let Some(front_host) = url.front_str() { + if let Some(actual_host) = url.host_str() { + tracing::debug!( + "Domain fronting enabled: routing via CDN {} to actual host {}", + front_host, + actual_host + ); - let actual_host: HeaderValue = url - .host_str() - .unwrap_or("") - .parse() - .unwrap_or(HeaderValue::from_static("")); - // If the map did have this key present, the new value is associated with the key - // and all previous values are removed. (reqwest HeaderMap docs) - _ = r.headers_mut().insert(reqwest::header::HOST, actual_host); + // this should never fail as we are transplanting the host from one url to another + r.url_mut().set_host(Some(front_host)).unwrap(); + + let actual_host_header: HeaderValue = + actual_host.parse().unwrap_or(HeaderValue::from_static("")); + // If the map did have this key present, the new value is associated with the key + // and all previous values are removed. (reqwest HeaderMap docs) + _ = r + .headers_mut() + .insert(reqwest::header::HOST, actual_host_header); + + return (url.as_str(), url.front_str()); + } else { + warn!("Domain fronting is enabled, but no host_url is defined! Domain fronting WILL NOT WORK") + } + } else { + warn!("Domain fronting is enabled, but no front_url is defined! Domain fronting WILL NOT WORK") + } } } + (url.as_str(), None) } } @@ -725,8 +961,8 @@ impl ApiClientCore for Client { method: reqwest::Method, path: P, params: Params<'_, K, V>, - json_body: Option<&B>, - ) -> RequestBuilder + body: Option<&B>, + ) -> Result where P: RequestPath, B: Serialize + ?Sized, @@ -742,33 +978,53 @@ impl ApiClientCore for Client { let mut rb = RequestBuilder::from_parts(self.reqwest_client.clone(), req); - if let Some(body) = json_body { - rb = rb.json(body); + rb = rb + .header(ACCEPT, self.serialization.content_type()) + .header(CONTENT_TYPE, self.serialization.content_type()); + + if let Some(body) = body { + match self.serialization { + SerializationFormat::Json => { + rb = rb.json(body); + } + SerializationFormat::Bincode => { + let body = bincode::serialize(body)?; + rb = rb.body(body); + } + SerializationFormat::Yaml => { + let mut body_bytes = Vec::new(); + serde_yaml::to_writer(&mut body_bytes, &body)?; + rb = rb.body(body_bytes); + } + SerializationFormat::Text => { + let body = serde_plain::to_string(&body)?.as_bytes().to_vec(); + rb = rb.body(body); + } + } } - rb + Ok(rb) } - async fn send(&self, request: RequestBuilder) -> Result> - where - E: Display, - { + async fn send(&self, request: RequestBuilder) -> Result { let mut attempts = 0; loop { // try_clone may fail if the body is a stream in which case using retries is not advised. let r = request .try_clone() - .ok_or(HttpClientError::GenericRequestFailure( - "failed to send request".to_string(), - ))?; + .ok_or(HttpClientError::AttemptedToCloneStreamRequest)?; // apply any changes based on the current state of the client wrt. hosts, // fronting domains, etc. - let mut req = r.build()?; + let mut req = r + .build() + .map_err(HttpClientError::reqwest_client_build_error)?; self.apply_hosts_to_req(&mut req); + #[cfg(not(target_arch = "wasm32"))] + let url = req.url().clone(); #[cfg(target_arch = "wasm32")] - let response: Result> = { + let response: Result = { Ok(wasmtimer::tokio::timeout( self.request_timeout, self.reqwest_client.execute(req), @@ -782,7 +1038,7 @@ impl ApiClientCore for Client { match response { Ok(resp) => return Ok(resp), - Err(e) => { + Err(err) => { // if we have multiple urls, update to the next self.update_host(); @@ -790,18 +1046,29 @@ impl ApiClientCore for Client { if let Some(ref front) = self.front { // If fronting is set to be enabled on error, enable domain fronting as we // have encountered an error. + let was_enabled = front.is_enabled(); front.retry_enable(); + if !was_enabled && front.is_enabled() { + tracing::info!( + "Domain fronting activated after connection failure: {err}", + ); + } } if attempts < self.retry_limit { - warn!("Retrying request due to http error: {}", e); + warn!("Retrying request due to http error: {err}"); attempts += 1; continue; } // if we have exhausted our attempts, return the error - #[allow(clippy::useless_conversion)] // conversion considered useless in wasm - return Err(e.into()); + cfg_if::cfg_if! { + if #[cfg(target_arch = "wasm32")] { + return Err(err); + } else { + return Err(HttpClientError::request_send_error(url, err)); + } + } } } } @@ -815,7 +1082,11 @@ impl ApiClientCore for Client { #[cfg_attr(not(target_arch = "wasm32"), async_trait)] pub trait ApiClient: ApiClientCore { /// Create an HTTP GET Request with the provided path and parameters - fn create_get_request(&self, path: P, params: Params<'_, K, V>) -> RequestBuilder + fn create_get_request( + &self, + path: P, + params: Params<'_, K, V>, + ) -> Result where P: RequestPath, K: AsRef, @@ -830,7 +1101,7 @@ pub trait ApiClient: ApiClientCore { path: P, params: Params<'_, K, V>, json_body: &B, - ) -> RequestBuilder + ) -> Result where P: RequestPath, B: Serialize + ?Sized, @@ -841,7 +1112,11 @@ pub trait ApiClient: ApiClientCore { } /// Create an HTTP DELETE Request with the provided path and parameters - fn create_delete_request(&self, path: P, params: Params<'_, K, V>) -> RequestBuilder + fn create_delete_request( + &self, + path: P, + params: Params<'_, K, V>, + ) -> Result where P: RequestPath, K: AsRef, @@ -856,7 +1131,7 @@ pub trait ApiClient: ApiClientCore { path: P, params: Params<'_, K, V>, json_body: &B, - ) -> RequestBuilder + ) -> Result where P: RequestPath, B: Serialize + ?Sized, @@ -868,68 +1143,64 @@ pub trait ApiClient: ApiClientCore { /// Create and send an HTTP GET Request with the provided path and parameters #[instrument(level = "debug", skip_all, fields(path=?path))] - async fn send_get_request( + async fn send_get_request( &self, path: P, params: Params<'_, K, V>, - ) -> Result> + ) -> Result where P: RequestPath + Send + Sync, K: AsRef + Sync, V: AsRef + Sync, - E: Display, { self.send_request(reqwest::Method::GET, path, params, None::<&()>) .await } /// Create and send an HTTP POST Request with the provided path, parameters, and json data - async fn send_post_request( + async fn send_post_request( &self, path: P, params: Params<'_, K, V>, json_body: &B, - ) -> Result> + ) -> Result where P: RequestPath + Send + Sync, B: Serialize + ?Sized + Sync, K: AsRef + Sync, V: AsRef + Sync, - E: Display, { self.send_request(reqwest::Method::POST, path, params, Some(json_body)) .await } /// Create and send an HTTP DELETE Request with the provided path and parameters - async fn send_delete_request( + async fn send_delete_request( &self, path: P, params: Params<'_, K, V>, - ) -> Result> + ) -> Result where P: RequestPath + Send + Sync, K: AsRef + Sync, V: AsRef + Sync, - E: Display, { self.send_request(reqwest::Method::DELETE, path, params, None::<&()>) .await } /// Create and send an HTTP PATCH Request with the provided path, parameters, and json data - async fn send_patch_request( + async fn send_patch_request( &self, path: P, params: Params<'_, K, V>, json_body: &B, - ) -> Result> + ) -> Result where P: RequestPath + Send + Sync, B: Serialize + ?Sized + Sync, K: AsRef + Sync, V: AsRef + Sync, - E: Display, { self.send_request(reqwest::Method::PATCH, path, params, Some(json_body)) .await @@ -940,17 +1211,16 @@ pub trait ApiClient: ApiClientCore { /// into the provided type `T`. #[instrument(level = "debug", skip_all, fields(path=?path))] // TODO: deprecate in favour of get_response that works based on mime type in the response - async fn get_json( + async fn get_json( &self, path: P, params: Params<'_, K, V>, - ) -> Result> + ) -> Result where P: RequestPath + Send + Sync, for<'a> T: Deserialize<'a>, K: AsRef + Sync, V: AsRef + Sync, - E: Display + DeserializeOwned, { self.get_response(path, params).await } @@ -958,17 +1228,16 @@ pub trait ApiClient: ApiClientCore { /// 'get' data from the segment-defined path, e.g. `["api", "v1", "mixnodes"]`, with tuple /// defined key-value parameters, e.g. `[("since", "12345")]`. Attempt to parse the response /// into the provided type `T` based on the content type header - async fn get_response( + async fn get_response( &self, path: P, params: Params<'_, K, V>, - ) -> Result> + ) -> Result where P: RequestPath + Send + Sync, for<'a> T: Deserialize<'a>, K: AsRef + Sync, V: AsRef + Sync, - E: Display + DeserializeOwned, { let res = self .send_request(reqwest::Method::GET, path, params, None::<&()>) @@ -979,19 +1248,18 @@ pub trait ApiClient: ApiClientCore { /// 'post' json data to the segment-defined path, e.g. `["api", "v1", "mixnodes"]`, with tuple /// defined key-value parameters, e.g. `[("since", "12345")]`. Attempt to parse the response /// into the provided type `T`. - async fn post_json( + async fn post_json( &self, path: P, params: Params<'_, K, V>, json_body: &B, - ) -> Result> + ) -> Result where P: RequestPath + Send + Sync, B: Serialize + ?Sized + Sync, for<'a> T: Deserialize<'a>, K: AsRef + Sync, V: AsRef + Sync, - E: Display + DeserializeOwned, { let res = self .send_request(reqwest::Method::POST, path, params, Some(json_body)) @@ -1002,17 +1270,16 @@ pub trait ApiClient: ApiClientCore { /// 'delete' json data from the segment-defined path, e.g. `["api", "v1", "mixnodes"]`, with /// tuple defined key-value parameters, e.g. `[("since", "12345")]`. Attempt to parse the /// response into the provided type `T`. - async fn delete_json( + async fn delete_json( &self, path: P, params: Params<'_, K, V>, - ) -> Result> + ) -> Result where P: RequestPath + Send + Sync, for<'a> T: Deserialize<'a>, K: AsRef + Sync, V: AsRef + Sync, - E: Display + DeserializeOwned, { let res = self .send_request(reqwest::Method::DELETE, path, params, None::<&()>) @@ -1023,19 +1290,18 @@ pub trait ApiClient: ApiClientCore { /// 'patch' json data at the segment-defined path, e.g. `["api", "v1", "mixnodes"]`, with tuple /// defined key-value parameters, e.g. `[("since", "12345")]`. Attempt to parse the response /// into the provided type `T`. - async fn patch_json( + async fn patch_json( &self, path: P, params: Params<'_, K, V>, json_body: &B, - ) -> Result> + ) -> Result where P: RequestPath + Send + Sync, B: Serialize + ?Sized + Sync, for<'a> T: Deserialize<'a>, K: AsRef + Sync, V: AsRef + Sync, - E: Display + DeserializeOwned, { let res = self .send_request(reqwest::Method::PATCH, path, params, Some(json_body)) @@ -1045,62 +1311,59 @@ pub trait ApiClient: ApiClientCore { /// `get` json data from the provided absolute endpoint, e.g. `"/api/v1/mixnodes?since=12345"`. /// Attempt to parse the response into the provided type `T`. - async fn get_json_from(&self, endpoint: S) -> Result> + async fn get_json_from(&self, endpoint: S) -> Result where for<'a> T: Deserialize<'a>, - E: Display + DeserializeOwned, S: AsRef + Sync + Send, { - let req = self.create_request_endpoint(reqwest::Method::GET, endpoint, None::<&()>); + let req = self.create_request_endpoint(reqwest::Method::GET, endpoint, None::<&()>)?; let res = self.send(req).await?; parse_response(res, false).await } /// `post` json data to the provided absolute endpoint, e.g. `"/api/v1/mixnodes?since=12345"`. /// Attempt to parse the response into the provided type `T`. - async fn post_json_data_to( + async fn post_json_data_to( &self, endpoint: S, json_body: &B, - ) -> Result> + ) -> Result where B: Serialize + ?Sized + Sync, for<'a> T: Deserialize<'a>, - E: Display + DeserializeOwned, S: AsRef + Sync + Send, { - let req = self.create_request_endpoint(reqwest::Method::POST, endpoint, Some(json_body)); + let req = self.create_request_endpoint(reqwest::Method::POST, endpoint, Some(json_body))?; let res = self.send(req).await?; parse_response(res, false).await } /// `delete` json data from the provided absolute endpoint, e.g. /// `"/api/v1/mixnodes?since=12345"`. Attempt to parse the response into the provided type `T`. - async fn delete_json_from(&self, endpoint: S) -> Result> + async fn delete_json_from(&self, endpoint: S) -> Result where for<'a> T: Deserialize<'a>, - E: Display + DeserializeOwned, S: AsRef + Sync + Send, { - let req = self.create_request_endpoint(reqwest::Method::DELETE, endpoint, None::<&()>); + let req = self.create_request_endpoint(reqwest::Method::DELETE, endpoint, None::<&()>)?; let res = self.send(req).await?; parse_response(res, false).await } /// `patch` json data at the provided absolute endpoint, e.g. `"/api/v1/mixnodes?since=12345"`. /// Attempt to parse the response into the provided type `T`. - async fn patch_json_data_at( + async fn patch_json_data_at( &self, endpoint: S, json_body: &B, - ) -> Result> + ) -> Result where B: Serialize + ?Sized + Sync, for<'a> T: Deserialize<'a>, - E: Display + DeserializeOwned, S: AsRef + Sync + Send, { - let req = self.create_request_endpoint(reqwest::Method::PATCH, endpoint, Some(json_body)); + let req = + self.create_request_endpoint(reqwest::Method::PATCH, endpoint, Some(json_body))?; let res = self.send(req).await?; parse_response(res, false).await } @@ -1156,49 +1419,63 @@ fn decode_as_text(bytes: &bytes::Bytes, headers: &HeaderMap) -> String { /// Attempt to parse a response object from an HTTP response #[instrument(level = "debug", skip_all)] -pub async fn parse_response(res: Response, allow_empty: bool) -> Result> +pub async fn parse_response(res: Response, allow_empty: bool) -> Result where T: DeserializeOwned, - E: DeserializeOwned + Display, { let status = res.status(); - tracing::trace!("Status: {} (success: {})", &status, status.is_success()); + let headers = res.headers().clone(); + let url = res.url().clone(); + + tracing::trace!("status: {status} (success: {})", status.is_success()); + tracing::trace!("headers: {headers:?}"); if !allow_empty { if let Some(0) = res.content_length() { - return Err(HttpClientError::EmptyResponse { status }); + return Err(HttpClientError::EmptyResponse { + url, + status, + headers: Box::new(headers), + }); } } - let headers = res.headers().clone(); - tracing::trace!("headers: {:?}", headers); if res.status().is_success() { // internally reqwest is first retrieving bytes and then performing parsing via serde_json // (and similarly does the same thing for text()) - let full = res.bytes().await?; + let full = res + .bytes() + .await + .map_err(|source| HttpClientError::ResponseReadFailure { + url, + headers: Box::new(headers.clone()), + status, + source: ReqwestErrorWrapper(source), + })?; decode_raw_response(&headers, full) } else if res.status() == StatusCode::NOT_FOUND { - Err(HttpClientError::NotFound) + Err(HttpClientError::NotFound { url }) } else { let Ok(plaintext) = res.text().await else { - return Err(HttpClientError::RequestFailure { status }); + return Err(HttpClientError::RequestFailure { + url, + status, + headers: Box::new(headers), + }); }; - if let Ok(request_error) = serde_json::from_str(&plaintext) { - Err(HttpClientError::EndpointFailure { - status, - error: request_error, - }) - } else { - Err(HttpClientError::GenericRequestFailure(plaintext)) - } + Err(HttpClientError::EndpointFailure { + url, + status, + headers: Box::new(headers), + error: plaintext, + }) } } -fn decode_as_json(headers: &HeaderMap, content: Bytes) -> Result> +fn decode_as_json(headers: &HeaderMap, content: Bytes) -> Result where T: DeserializeOwned, - E: DeserializeOwned + Display, { match serde_json::from_slice(&content) { Ok(data) => Ok(data), @@ -1212,10 +1489,9 @@ where } } -fn decode_as_bincode(headers: &HeaderMap, content: Bytes) -> Result> +fn decode_as_bincode(headers: &HeaderMap, content: Bytes) -> Result where T: DeserializeOwned, - E: DeserializeOwned + Display, { use bincode::Options; @@ -1232,10 +1508,9 @@ where } } -fn decode_raw_response(headers: &HeaderMap, content: Bytes) -> Result> +fn decode_raw_response(headers: &HeaderMap, content: Bytes) -> Result where T: DeserializeOwned, - E: DeserializeOwned + Display, { // if content type header is missing, fallback to our old default, json let mime = try_get_mime_type(headers).unwrap_or(mime::APPLICATION_JSON); diff --git a/common/http-api-client/src/registry.rs b/common/http-api-client/src/registry.rs new file mode 100644 index 0000000000..05ad9a5daa --- /dev/null +++ b/common/http-api-client/src/registry.rs @@ -0,0 +1,101 @@ +//! Global registry for HTTP client configurations. +//! +//! This module provides a compile-time registry system that allows any crate +//! in the workspace to contribute configuration modifications to HTTP clients. + +use crate::ReqwestClientBuilder; + +/// A configuration record that modifies a `ReqwestClientBuilder`. +/// +/// Records are collected at compile-time via the `inventory` crate and +/// applied in priority order when building HTTP clients. +pub struct ConfigRecord { + /// Lower numbers run earlier. + pub priority: i32, + /// A function that takes a builder and returns a mutated builder. + pub apply: fn(ReqwestClientBuilder) -> ReqwestClientBuilder, +} + +inventory::collect!(ConfigRecord); + +/// Returns the default builder with all registered configurations applied. +pub fn default_builder() -> ReqwestClientBuilder { + let mut b = ReqwestClientBuilder::new(); + let mut records: Vec<&'static ConfigRecord> = + inventory::iter::.into_iter().collect(); + records.sort_by_key(|r| r.priority); // lower runs first + + #[cfg(feature = "debug-inventory")] + { + eprintln!( + "[HTTP-INVENTORY] Building client with {} registered configurations", + records.len() + ); + } + + for r in records { + b = (r.apply)(b); + } + + #[cfg(feature = "debug-inventory")] + { + eprintln!("[HTTP-INVENTORY] Final builder state (Debug):"); + eprintln!("{:#?}", b); + eprintln!( + "[HTTP-INVENTORY] Note: reqwest::ClientBuilder doesn't expose all internal state" + ); + eprintln!("[HTTP-INVENTORY] Building test client to verify configuration..."); + + // Try to build a client to see if it works + match b.try_clone().unwrap().build() { + Ok(client) => { + eprintln!("[HTTP-INVENTORY] ✓ Client built successfully"); + eprintln!("[HTTP-INVENTORY] Client debug info: {:#?}", client); + } + Err(e) => { + eprintln!("[HTTP-INVENTORY] ✗ Failed to build client: {}", e); + } + } + } + + b +} + +/// Builds a client using the default builder with all registered configurations. +pub fn build_client() -> reqwest::Result { + default_builder().build() +} + +/// Debug function to inspect registered configurations. +/// Returns a vector of (priority, function_pointer) tuples for debugging. +pub fn inspect_registered_configs() -> Vec<(i32, usize)> { + let mut configs: Vec<(i32, usize)> = inventory::iter:: + .into_iter() + .map(|record| (record.priority, record.apply as usize)) + .collect(); + configs.sort_by_key(|(priority, _)| *priority); + configs +} + +/// Print all registered configurations to stderr for debugging. +/// This shows the priority and function pointer address of each registered config. +pub fn debug_print_inventory() { + eprintln!("[HTTP-INVENTORY] Registered configurations:"); + let configs = inspect_registered_configs(); + if configs.is_empty() { + eprintln!(" (none)"); + } else { + for (i, (priority, ptr)) in configs.iter().enumerate() { + eprintln!( + " [{:2}] Priority: {:4}, Function: 0x{:016x}", + i, priority, ptr + ); + } + eprintln!(" Total: {} configurations", configs.len()); + } +} + +/// Returns the count of registered configuration records. +pub fn registered_config_count() -> usize { + inventory::iter::.into_iter().count() +} diff --git a/common/http-api-client/src/tests.rs b/common/http-api-client/src/tests.rs index 6f295cd845..98f6fec79f 100644 --- a/common/http-api-client/src/tests.rs +++ b/common/http-api-client/src/tests.rs @@ -95,10 +95,10 @@ async fn api_client_retry() -> Result<(), Box> { "http://example.com/".parse()?, ]) .with_retries(3) - .build::()?; + .build()?; - let req = client.create_get_request(&["/"], NO_PARAMS); - let resp = client.send::(req).await?; + let req = client.create_get_request(&["/"], NO_PARAMS).unwrap(); + let resp = client.send(req).await?; assert_eq!(resp.status(), 200); @@ -111,10 +111,7 @@ async fn api_client_retry() -> Result<(), Box> { #[test] fn host_updating() { let url = Url::new("http://example.com", None).unwrap(); - let mut client = ClientBuilder::new::<_, &str>(url) - .unwrap() - .build::<&str>() - .unwrap(); + let mut client = ClientBuilder::new(url).unwrap().build().unwrap(); // check that the url is set correctly let current_url = client.current_url(); @@ -171,10 +168,10 @@ fn host_updating() { #[cfg(feature = "tunneling")] fn fronted_host_updating() { let url = Url::new("http://example.com", Some(vec!["http://front1.com"])).unwrap(); - let mut client = ClientBuilder::new::<_, &str>(url) + let mut client = ClientBuilder::new(url) .unwrap() .with_fronting(crate::fronted::FrontPolicy::Always) - .build::<&str>() + .build() .unwrap(); // check that the url is set correctly diff --git a/common/http-api-client/tests/registry_tests.rs b/common/http-api-client/tests/registry_tests.rs new file mode 100644 index 0000000000..1f878b9444 --- /dev/null +++ b/common/http-api-client/tests/registry_tests.rs @@ -0,0 +1,70 @@ +use nym_http_api_client::registry; + +// Create separate modules to avoid name conflicts +mod config_early { + use nym_http_api_client_macro::client_defaults; + + client_defaults!( + priority = -200; + tcp_nodelay = true + ); +} + +mod config_late { + use nym_http_api_client_macro::client_defaults; + + client_defaults!( + priority = 100; + pool_idle_timeout = std::time::Duration::from_secs(90) + ); +} + +#[test] +fn test_registry_collects_configs() { + // Verify that configurations are being registered + let count = registry::registered_config_count(); + // Should have at least the ones we registered above plus the default from lib.rs + assert!( + count >= 3, + "Expected at least 3 registered configs, got {}", + count + ); +} + +#[test] +fn test_default_builder_applies_configs() { + // Test that default_builder returns a configured builder + let _builder = registry::default_builder(); + // The builder should have all configurations applied + // We can't easily inspect the internals, but we verify it doesn't panic +} + +#[test] +fn test_build_client_works() { + // Test that we can successfully build a client with all configurations + let result = registry::build_client(); + assert!(result.is_ok(), "Failed to build client: {:?}", result.err()); +} + +#[cfg(debug_assertions)] +#[test] +fn test_inspect_configs() { + // In debug mode, test that we can inspect registered configurations + let configs = registry::inspect_registered_configs(); + + // Verify configs are sorted by priority + for window in configs.windows(2) { + assert!(window[0].0 <= window[1].0, "Configs not sorted by priority"); + } + + // Verify we have configs at different priority levels + let priorities: Vec = configs.iter().map(|(p, _)| *p).collect(); + assert!( + priorities.iter().any(|&p| p < 0), + "Expected negative priority configs" + ); + assert!( + priorities.iter().any(|&p| p >= 0), + "Expected non-negative priority configs" + ); +} diff --git a/common/network-defaults/src/constants.rs b/common/network-defaults/src/constants.rs index e9b70f54e3..1153fd902d 100644 --- a/common/network-defaults/src/constants.rs +++ b/common/network-defaults/src/constants.rs @@ -47,7 +47,8 @@ pub mod nyx { pub mod wireguard { use std::net::{Ipv4Addr, Ipv6Addr}; - pub const WG_PORT: u16 = 51822; + pub const WG_TUNNEL_PORT: u16 = 51822; + pub const WG_METADATA_PORT: u16 = 51830; // The interface used to route traffic pub const WG_TUN_BASE_NAME: &str = "nymwg"; diff --git a/common/network-defaults/src/network.rs b/common/network-defaults/src/network.rs index e8fd8fcfa8..3fdf03671e 100644 --- a/common/network-defaults/src/network.rs +++ b/common/network-defaults/src/network.rs @@ -55,6 +55,7 @@ pub struct ApiUrl { pub front_hosts: Option>, } +#[derive(Copy, Clone)] pub struct ApiUrlConst<'a> { pub url: &'a str, pub front_hosts: Option<&'a [&'a str]>, @@ -188,8 +189,14 @@ impl NymNetworkDetails { ), }, nym_vpn_api_url: parse_optional_str(mainnet::NYM_VPN_API), - nym_api_urls: None, - nym_vpn_api_urls: None, + nym_api_urls: Some(mainnet::NYM_APIS.iter().copied().map(Into::into).collect()), + nym_vpn_api_urls: Some( + mainnet::NYM_VPN_APIS + .iter() + .copied() + .map(Into::into) + .collect(), + ), } } diff --git a/common/node-tester-utils/src/receiver.rs b/common/node-tester-utils/src/receiver.rs index fd764d56dc..f927e23632 100644 --- a/common/node-tester-utils/src/receiver.rs +++ b/common/node-tester-utils/src/receiver.rs @@ -9,7 +9,7 @@ use futures::StreamExt; use nym_crypto::asymmetric::x25519; use nym_sphinx::acknowledgements::AckKey; use nym_sphinx::receiver::{MessageReceiver, SphinxMessageReceiver}; -use nym_task::TaskClient; +use nym_task::ShutdownToken; use serde::de::DeserializeOwned; use std::sync::Arc; @@ -24,7 +24,7 @@ pub struct SimpleMessageReceiver acks_receiver: mpsc::UnboundedReceiver>>, received_sender: ReceivedSender, - shutdown: TaskClient, + shutdown: ShutdownToken, } impl SimpleMessageReceiver { @@ -34,7 +34,7 @@ impl SimpleMessageReceiver { mixnet_message_receiver: mpsc::UnboundedReceiver>>, acks_receiver: mpsc::UnboundedReceiver>>, received_sender: ReceivedSender, - shutdown: TaskClient, + shutdown: ShutdownToken, ) -> Self { Self::new( local_encryption_keypair, @@ -54,7 +54,7 @@ impl SimpleMessageReceiver { mixnet_message_receiver: mpsc::UnboundedReceiver>>, acks_receiver: mpsc::UnboundedReceiver>>, received_sender: ReceivedSender, - shutdown: TaskClient, + shutdown: ShutdownToken, ) -> Self { SimpleMessageReceiver { message_processor: TestPacketProcessor::new(local_encryption_keypair, ack_key), @@ -91,11 +91,12 @@ impl SimpleMessageReceiver { where T: DeserializeOwned, { - while !self.shutdown.is_shutdown() { + loop { tokio::select! { biased; - _ = self.shutdown.recv() => { - log_info!("SimpleMessageReceiver: received shutdown") + _ = self.shutdown.cancelled() => { + log_info!("SimpleMessageReceiver: received shutdown"); + break } mixnet_messages = self.mixnet_message_receiver.next() => { let Some(mixnet_messages) = mixnet_messages else { diff --git a/common/nym_offline_compact_ecash/src/scheme/identify.rs b/common/nym_offline_compact_ecash/src/scheme/identify.rs index 38489e28c9..06e51e151a 100644 --- a/common/nym_offline_compact_ecash/src/scheme/identify.rs +++ b/common/nym_offline_compact_ecash/src/scheme/identify.rs @@ -319,9 +319,9 @@ mod tests { let sk = grp.random_scalar(); let sk_user = SecretKeyUser { sk }; let pk_user = sk_user.public_key(); - public_keys.push(pk_user.clone()); + public_keys.push(pk_user); } - public_keys.push(user_keypair.public_key().clone()); + public_keys.push(user_keypair.public_key()); let (req, req_info) = withdrawal_request(user_keypair.secret_key(), expiration_date, t_type).unwrap(); @@ -462,9 +462,9 @@ mod tests { let sk = grp.random_scalar(); let sk_user = SecretKeyUser { sk }; let pk_user = sk_user.public_key(); - public_keys.push(pk_user.clone()); + public_keys.push(pk_user); } - public_keys.push(user_keypair.public_key().clone()); + public_keys.push(user_keypair.public_key()); let (req, req_info) = withdrawal_request(user_keypair.secret_key(), expiration_date, t_type).unwrap(); diff --git a/common/nym_offline_compact_ecash/src/scheme/keygen.rs b/common/nym_offline_compact_ecash/src/scheme/keygen.rs index 8db1597e61..faa5db6b59 100644 --- a/common/nym_offline_compact_ecash/src/scheme/keygen.rs +++ b/common/nym_offline_compact_ecash/src/scheme/keygen.rs @@ -401,7 +401,7 @@ impl Bytable for SecretKeyUser { impl Base58 for SecretKeyUser {} -#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)] +#[derive(Debug, Eq, PartialEq, Clone, Copy, Serialize, Deserialize)] pub struct PublicKeyUser { pub(crate) pk: G1Projective, } @@ -554,7 +554,7 @@ impl KeyPairUser { } pub fn public_key(&self) -> PublicKeyUser { - self.public_key.clone() + self.public_key } pub fn to_bytes(&self) -> Vec { diff --git a/common/nymsphinx/addressing/Cargo.toml b/common/nymsphinx/addressing/Cargo.toml index 03ec4d94af..c7f756cb5c 100644 --- a/common/nymsphinx/addressing/Cargo.toml +++ b/common/nymsphinx/addressing/Cargo.toml @@ -16,3 +16,6 @@ thiserror = { workspace = true } [dev-dependencies] rand = { workspace = true } nym-crypto = { path = "../../crypto", features = ["rand"] } +bincode = { workspace = true } +serde_json = { workspace = true } +serde = { workspace = true, features = ["derive"] } \ No newline at end of file diff --git a/common/nymsphinx/addressing/src/clients.rs b/common/nymsphinx/addressing/src/clients.rs index 0b1bcf7623..aa475dc401 100644 --- a/common/nymsphinx/addressing/src/clients.rs +++ b/common/nymsphinx/addressing/src/clients.rs @@ -7,7 +7,7 @@ use crate::nodes::{NodeIdentity, NODE_IDENTITY_SIZE}; use nym_crypto::asymmetric::{ed25519, x25519}; use nym_sphinx_types::Destination; -use serde::de::{Error as SerdeError, Unexpected, Visitor}; +use serde::de::{Error as SerdeError, SeqAccess, Unexpected, Visitor}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use std::fmt::{self, Formatter}; use std::str::FromStr; @@ -64,7 +64,7 @@ impl<'de> Deserialize<'de> for Recipient { { struct RecipientVisitor; - impl Visitor<'_> for RecipientVisitor { + impl<'de> Visitor<'de> for RecipientVisitor { type Value = Recipient; fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result { @@ -90,6 +90,42 @@ impl<'de> Deserialize<'de> for Recipient { ) }) } + + fn visit_seq(self, mut seq: A) -> Result + where + A: SeqAccess<'de>, + { + // if we know the size hint, check if it matches expectation, + // otherwise return an error + if let Some(size_hint) = seq.size_hint() { + if size_hint != Recipient::LEN { + return Err(SerdeError::invalid_length(size_hint, &self)); + } + } + + let mut recipient_bytes = [0u8; Recipient::LEN]; + + // clippy's suggestion is completely wrong and it iterates wrong sequence + #[allow(clippy::needless_range_loop)] + for i in 0..Recipient::LEN { + let Some(elem) = seq.next_element::()? else { + return Err(SerdeError::invalid_length(i + 1, &self)); + }; + recipient_bytes[i] = elem; + } + + // make sure there are no trailing bytes + if seq.next_element::()?.is_some() { + return Err(SerdeError::invalid_length(Recipient::LEN + 1, &self)); + } + + Recipient::try_from_bytes(recipient_bytes).map_err(|_| { + SerdeError::invalid_value( + Unexpected::Other("At least one of the curve points was malformed"), + &self, + ) + }) + } } deserializer.deserialize_bytes(RecipientVisitor) @@ -245,6 +281,18 @@ impl FromStr for Recipient { mod tests { use super::*; + fn mock_recipient() -> Recipient { + Recipient::try_from_bytes([ + 67, 5, 132, 146, 3, 236, 116, 89, 254, 57, 131, 159, 69, 181, 55, 208, 12, 108, 136, + 83, 58, 76, 171, 195, 31, 98, 92, 64, 68, 53, 156, 184, 100, 189, 73, 3, 238, 103, 156, + 108, 124, 199, 42, 79, 172, 98, 81, 177, 182, 100, 167, 164, 74, 183, 199, 213, 162, + 173, 102, 112, 30, 159, 148, 66, 44, 75, 230, 182, 138, 114, 170, 163, 209, 82, 204, + 100, 118, 91, 57, 150, 212, 147, 151, 135, 148, 16, 213, 223, 182, 164, 242, 37, 40, + 73, 137, 228, + ]) + .unwrap() + } + #[test] fn string_conversion_works() { let mut rng = rand::thread_rng(); @@ -308,4 +356,40 @@ mod tests { recovered_recipient.gateway.to_bytes() ); } + + // calls `visit_bytes` + #[test] + fn bincode_serialisation_works() { + let recipient = mock_recipient(); + + #[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq)] + struct MyStruct { + recipient: Recipient, + } + let a = MyStruct { recipient }; + let s = bincode::serialize(&a).unwrap(); + + let b = bincode::deserialize(&s).unwrap(); + + assert_eq!(a, b); + } + + // calls `visit_seq` + #[test] + fn json_serialisation_works() { + use serde::{Deserialize, Serialize}; + + let recipient = mock_recipient(); + + #[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq)] + struct MyStruct { + recipient: Recipient, + } + let a = MyStruct { recipient }; + let s = serde_json::to_string(&a).unwrap(); + + let b = serde_json::from_str(&s).unwrap(); + + assert_eq!(a, b); + } } diff --git a/common/nymsphinx/types/src/lib.rs b/common/nymsphinx/types/src/lib.rs index 5ecd5c3fb1..aa1a82b848 100644 --- a/common/nymsphinx/types/src/lib.rs +++ b/common/nymsphinx/types/src/lib.rs @@ -180,6 +180,7 @@ impl NymPacket { } #[cfg(feature = "sphinx")] + #[allow(unreachable_patterns)] pub fn sphinx_packet_ref(&self) -> Option<&SphinxPacket> { match self { NymPacket::Sphinx(packet) => Some(packet), @@ -188,6 +189,7 @@ impl NymPacket { } #[cfg(feature = "sphinx")] + #[allow(unreachable_patterns)] pub fn to_sphinx_packet(self) -> Option { match self { NymPacket::Sphinx(packet) => Some(packet), diff --git a/common/nyxd-scraper/Cargo.toml b/common/nyxd-scraper/Cargo.toml index 9eee598af7..025e906d56 100644 --- a/common/nyxd-scraper/Cargo.toml +++ b/common/nyxd-scraper/Cargo.toml @@ -7,6 +7,7 @@ homepage.workspace = true documentation.workspace = true edition.workspace = true license.workspace = true +rust-version.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -36,5 +37,6 @@ url.workspace = true [build-dependencies] +anyhow = { workspace = true } sqlx = { workspace = true, features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"] } tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } diff --git a/common/nyxd-scraper/build.rs b/common/nyxd-scraper/build.rs index cfe3b9c079..89a7681b98 100644 --- a/common/nyxd-scraper/build.rs +++ b/common/nyxd-scraper/build.rs @@ -1,22 +1,30 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use anyhow::Context; + #[tokio::main] -async fn main() { +async fn main() -> anyhow::Result<()> { use sqlx::{Connection, SqliteConnection}; use std::env; - let out_dir = env::var("OUT_DIR").unwrap(); + let out_dir = env::var("OUT_DIR")?; let database_path = format!("{out_dir}/scraper-example.sqlite"); + // remove the db file if it already existed from previous build + // in case it was from a different branch + if std::fs::exists(&database_path)? { + std::fs::remove_file(&database_path)?; + } + let mut conn = SqliteConnection::connect(&format!("sqlite://{database_path}?mode=rwc")) .await - .expect("Failed to create SQLx database connection"); + .context("Failed to create SQLx database connection")?; sqlx::migrate!("./sql_migrations") .run(&mut conn) .await - .expect("Failed to perform SQLx migrations"); + .context("Failed to perform SQLx migrations")?; #[cfg(target_family = "unix")] println!("cargo:rustc-env=DATABASE_URL=sqlite://{}", &database_path); @@ -25,4 +33,6 @@ async fn main() { // for some strange reason we need to add a leading `/` to the windows path even though it's // not a valid windows path... but hey, it works... println!("cargo:rustc-env=DATABASE_URL=sqlite:///{}", &database_path); + + Ok(()) } diff --git a/common/socks5-client-core/src/lib.rs b/common/socks5-client-core/src/lib.rs index 3c7a4b658d..034aa5f4f0 100644 --- a/common/socks5-client-core/src/lib.rs +++ b/common/socks5-client-core/src/lib.rs @@ -23,9 +23,7 @@ use nym_client_core::init::types::GatewaySetup; use nym_credential_storage::storage::Storage as CredentialStorage; use nym_sphinx::addressing::clients::Recipient; use nym_sphinx::params::PacketType; -use nym_task::{TaskClient, TaskHandle, TaskStatus}; - -use anyhow::anyhow; +use nym_task::{ShutdownManager, ShutdownTracker}; use nym_validator_client::UserAgent; use std::error::Error; use std::path::PathBuf; @@ -46,7 +44,7 @@ pub enum Socks5ControlMessage { pub struct StartedSocks5Client { /// Handle for managing graceful shutdown of this client. If dropped, the client will be stopped. - pub shutdown_handle: TaskHandle, + pub shutdown_handle: ShutdownManager, /// Address of the started client pub address: Recipient, @@ -65,6 +63,8 @@ pub struct NymClient { /// Optional path to a .json file containing standalone network details. custom_mixnet: Option, + + shutdown_manager: ShutdownManager, } impl NymClient @@ -92,6 +92,7 @@ where setup_method: GatewaySetup::MustLoad { gateway_id: None }, user_agent, custom_mixnet, + shutdown_manager: Default::default(), } } @@ -108,7 +109,7 @@ where client_output: ClientOutput, client_status: ClientState, self_address: Recipient, - shutdown: TaskClient, + shutdown: ShutdownTracker, packet_type: PacketType, ) { info!("Starting socks5 listener..."); @@ -148,51 +149,39 @@ where socks5_config.send_anonymously, socks5_config.socks5_debug, ), - shutdown.clone(), + shutdown, packet_type, ); - nym_task::spawn_with_report_error( - async move { - sphinx_socks - .serve( - input_sender, - received_buffer_request_sender, - connection_command_sender, - ) - .await - }, - shutdown, - ); + nym_task::spawn_future(async move { + sphinx_socks + .serve( + input_sender, + received_buffer_request_sender, + connection_command_sender, + ) + .await + }); } /// blocking version of `start` method. Will run forever (or until SIGINT is sent) pub async fn run_forever(self) -> Result<(), Box> { - let started = self.start().await?; + let mut started = self.start().await?; - let res = started.shutdown_handle.wait_for_shutdown().await; + started.shutdown_handle.run_until_shutdown().await; log::info!("Stopping nym-socks5-client"); - res + Ok(()) } // Variant of `run_forever` that listens for remote control messages pub async fn run_and_listen( self, mut receiver: Socks5ControlMessageReceiver, - sender: nym_task::StatusSender, ) -> Result<(), Box> { // Start the main task let started = self.start().await?; - let mut shutdown = started - .shutdown_handle - .try_into_task_manager() - .ok_or(anyhow!( - "attempted to use `run_and_listen` without owning shutdown handle" - ))?; + let mut task_manager = started.shutdown_handle; - // Listen to status messages from task, that we forward back to the caller - shutdown - .start_status_listener(sender, TaskStatus::Ready) - .await; + let mut shutdown_signals = task_manager.detach_shutdown_signals(); let res = tokio::select! { biased; @@ -207,22 +196,20 @@ where } } Ok(()) - } - Some(msg) = shutdown.wait_for_error() => { - log::info!("Task error: {msg:?}"); - Err(msg) - } - _ = tokio::signal::ctrl_c() => { - log::info!("Received SIGINT"); + }, + _ = shutdown_signals.wait_for_signal() => { + log::info!("Received shutdown signal"); Ok(()) }, }; - log::info!("Sending shutdown"); - shutdown.signal_shutdown().ok(); + if !task_manager.is_cancelled() { + log::info!("Sending shutdown"); + task_manager.send_cancellation(); + } log::info!("Waiting for tasks to finish... (Press ctrl-c to force)"); - shutdown.wait_for_shutdown().await; + task_manager.perform_shutdown().await; log::info!("Stopping nym-socks5-client"); res @@ -238,6 +225,7 @@ where let mut base_builder = BaseClientBuilder::new(self.config.base(), self.storage, dkg_query_client) + .with_shutdown(self.shutdown_manager.shutdown_tracker_owned()) .with_gateway_setup(self.setup_method) .with_user_agent(self.user_agent); @@ -261,7 +249,7 @@ where client_output, client_state, self_address, - started_client.task_handle.get_handle(), + self.shutdown_manager.shutdown_tracker_owned(), packet_type, ); @@ -269,7 +257,7 @@ where info!("The address of this client is: {self_address}"); Ok(StartedSocks5Client { - shutdown_handle: started_client.task_handle, + shutdown_handle: self.shutdown_manager, address: self_address, }) } diff --git a/common/socks5-client-core/src/socks/client.rs b/common/socks5-client-core/src/socks/client.rs index 5d79d66380..ffdca82a70 100644 --- a/common/socks5-client-core/src/socks/client.rs +++ b/common/socks5-client-core/src/socks/client.rs @@ -21,7 +21,7 @@ use nym_sphinx::addressing::clients::Recipient; use nym_sphinx::params::PacketSize; use nym_sphinx::params::PacketType; use nym_task::connections::{LaneQueueLengths, TransmissionLane}; -use nym_task::TaskClient; +use nym_task::ShutdownTracker; use pin_project::pin_project; use rand::RngCore; use std::io; @@ -185,7 +185,7 @@ pub(crate) struct SocksClient { self_address: Recipient, started_proxy: bool, lane_queue_lengths: LaneQueueLengths, - shutdown_listener: TaskClient, + shutdown_listener: ShutdownTracker, packet_type: Option, } @@ -214,12 +214,9 @@ impl SocksClient { controller_sender: ControllerSender, self_address: &Recipient, lane_queue_lengths: LaneQueueLengths, - mut shutdown_listener: TaskClient, + shutdown_listener: ShutdownTracker, packet_type: Option, ) -> Self { - // If this task fails and exits, we don't want to send shutdown signal - shutdown_listener.disarm(); - let connection_id = Self::generate_random(); SocksClient { @@ -294,7 +291,6 @@ impl SocksClient { .shutdown() .await .map_err(|source| SocksProxyError::SocketShutdownFailure { source })?; - self.shutdown_listener.disarm(); Ok(()) } diff --git a/common/socks5-client-core/src/socks/mixnet_responses.rs b/common/socks5-client-core/src/socks/mixnet_responses.rs index f74681c8de..0a9e5e154d 100644 --- a/common/socks5-client-core/src/socks/mixnet_responses.rs +++ b/common/socks5-client-core/src/socks/mixnet_responses.rs @@ -13,13 +13,13 @@ use nym_service_providers_common::interface::{ControlResponse, ResponseContent}; use nym_socks5_proxy_helpers::connection_controller::{ControllerCommand, ControllerSender}; use nym_socks5_requests::{Socks5ProviderResponse, Socks5Response, Socks5ResponseContent}; use nym_sphinx::receiver::ReconstructedMessage; -use nym_task::TaskClient; +use nym_task::ShutdownToken; pub(crate) struct MixnetResponseListener { buffer_requester: ReceivedBufferRequestSender, mix_response_receiver: ReconstructedMessagesReceiver, controller_sender: ControllerSender, - shutdown: TaskClient, + shutdown: ShutdownToken, } impl Drop for MixnetResponseListener { @@ -28,7 +28,7 @@ impl Drop for MixnetResponseListener { .buffer_requester .unbounded_send(ReceivedBufferMessage::ReceiverDisconnect) { - if self.shutdown.is_shutdown_poll() { + if self.shutdown.is_cancelled() { log::debug!("The buffer request failed: {err}"); } else { log::error!("The buffer request failed: {err}"); @@ -41,7 +41,7 @@ impl MixnetResponseListener { pub(crate) fn new( buffer_requester: ReceivedBufferRequestSender, controller_sender: ControllerSender, - shutdown: TaskClient, + shutdown: ShutdownToken, ) -> Self { let (mix_response_sender, mix_response_receiver) = mpsc::unbounded(); buffer_requester @@ -130,13 +130,18 @@ impl MixnetResponseListener { } pub(crate) async fn run(&mut self) { - while !self.shutdown.is_shutdown() { + loop { tokio::select! { + biased; + _ = self.shutdown.cancelled() => { + log::trace!("MixnetResponseListener: Received shutdown"); + break; + } received_responses = self.mix_response_receiver.next() => { if let Some(received_responses) = received_responses { for reconstructed_message in received_responses { if let Err(err) = self.on_message(reconstructed_message) { - self.shutdown.send_status_msg(Box::new(err)); + debug!("message handling error: {err}") } } } else { @@ -144,12 +149,8 @@ impl MixnetResponseListener { break; } }, - _ = self.shutdown.recv() => { - log::trace!("MixnetResponseListener: Received shutdown"); - } } } - self.shutdown.recv_timeout().await; log::debug!("MixnetResponseListener: Exiting"); } } diff --git a/common/socks5-client-core/src/socks/server.rs b/common/socks5-client-core/src/socks/server.rs index 1a3fc22f3f..278435a9df 100644 --- a/common/socks5-client-core/src/socks/server.rs +++ b/common/socks5-client-core/src/socks/server.rs @@ -12,7 +12,7 @@ use nym_socks5_proxy_helpers::connection_controller::Controller; use nym_sphinx::addressing::clients::Recipient; use nym_sphinx::params::PacketType; use nym_task::connections::{ConnectionCommandSender, LaneQueueLengths}; -use nym_task::TaskClient; +use nym_task::ShutdownTracker; use std::net::SocketAddr; use tap::TapFallible; use tokio::net::TcpListener; @@ -25,7 +25,7 @@ pub struct NymSocksServer { self_address: Recipient, client_config: client::Config, lane_queue_lengths: LaneQueueLengths, - shutdown: TaskClient, + shutdown: ShutdownTracker, packet_type: PacketType, } @@ -39,7 +39,7 @@ impl NymSocksServer { self_address: Recipient, lane_queue_lengths: LaneQueueLengths, client_config: client::Config, - shutdown: TaskClient, + shutdown: ShutdownTracker, packet_type: PacketType, ) -> Self { info!("Listening on {bind_address}"); @@ -72,7 +72,7 @@ impl NymSocksServer { let (mut active_streams_controller, controller_sender) = Controller::new( client_connection_tx, //BroadcastActiveConnections::Off, - self.shutdown.clone(), + self.shutdown.clone_shutdown_token(), ); tokio::spawn(async move { active_streams_controller.run().await; @@ -82,20 +82,30 @@ impl NymSocksServer { let mut mixnet_response_listener = MixnetResponseListener::new( buffer_requester, controller_sender.clone(), - self.shutdown.clone(), + self.shutdown.clone_shutdown_token(), + ); + self.shutdown.try_spawn_named( + async move { + mixnet_response_listener.run().await; + }, + "Socks5MixnetListener", ); - tokio::spawn(async move { - mixnet_response_listener.run().await; - }); // TODO:, if required, there should be another task here responsible for control requests. // it should get `input_sender` to send actual requests into the mixnet // and some channel that connects it from `MixnetResponseListener` to receive // any control responses + let shutdown = self.shutdown.clone_shutdown_token(); loop { tokio::select! { - Ok((stream, _remote)) = listener.accept() => { + biased; + _ = shutdown.cancelled() => { + log::trace!("NymSocksServer: Received shutdown"); + log::debug!("NymSocksServer: Exiting"); + return Ok(()); + } + Ok((stream, remote)) = listener.accept() => { let mut client = SocksClient::new( self.client_config, stream, @@ -109,23 +119,20 @@ impl NymSocksServer { Some(self.packet_type) ); - tokio::spawn(async move { - if let Err(err) = client.run().await { - error!("Error! {err}"); - if client.send_error(err).await.is_err() { - warn!("Failed to send error code"); + self.shutdown.try_spawn_named( + async move { + if let Err(err) = client.run().await { + error!("Error! {err}"); + if client.send_error(err).await.is_err() { + warn!("Failed to send error code"); + }; + if client.shutdown().await.is_err() { + warn!("Failed to shutdown TcpStream"); + }; }; - if client.shutdown().await.is_err() { - warn!("Failed to shutdown TcpStream"); - }; - } - }); + }, &format!("Socks5Client::{remote}") + ); }, - _ = self.shutdown.recv() => { - log::trace!("NymSocksServer: Received shutdown"); - log::debug!("NymSocksServer: Exiting"); - return Ok(()); - } } } } diff --git a/common/socks5/proxy-helpers/src/connection_controller.rs b/common/socks5/proxy-helpers/src/connection_controller.rs index 5c2e2f22c1..f8b7ca783b 100644 --- a/common/socks5/proxy-helpers/src/connection_controller.rs +++ b/common/socks5/proxy-helpers/src/connection_controller.rs @@ -7,7 +7,7 @@ use log::*; use nym_ordered_buffer::{OrderedMessageBuffer, ReadContiguousData}; use nym_socks5_requests::{ConnectionId, SocketData}; use nym_task::connections::{ConnectionCommand, ConnectionCommandSender}; -use nym_task::TaskClient; +use nym_task::ShutdownToken; use std::collections::{HashMap, HashSet}; /// A generic message produced after reading from a socket/connection. @@ -101,13 +101,13 @@ pub struct Controller { // un-order messages. Note we don't ever expect to have more than 1-2 messages per connection here pending_messages: HashMap>, - shutdown: TaskClient, + shutdown: ShutdownToken, } impl Controller { pub fn new( client_connection_tx: ConnectionCommandSender, - shutdown: TaskClient, + shutdown: ShutdownToken, ) -> (Self, ControllerSender) { let (sender, receiver) = mpsc::unbounded(); ( @@ -155,7 +155,7 @@ impl Controller { .client_connection_tx .unbounded_send(ConnectionCommand::Close(conn_id)) { - if self.shutdown.is_shutdown_poll() { + if self.shutdown.is_cancelled() { log::debug!("Failed to send: {err}"); } else { log::error!("Failed to send: {err}"); @@ -230,7 +230,6 @@ impl Controller { }, } } - self.shutdown.recv_timeout().await; log::debug!("SOCKS5 Controller: Exiting"); } } diff --git a/common/socks5/proxy-helpers/src/proxy_runner/inbound.rs b/common/socks5/proxy-helpers/src/proxy_runner/inbound.rs index 6cdf0efa9c..2e3087bbc0 100644 --- a/common/socks5/proxy-helpers/src/proxy_runner/inbound.rs +++ b/common/socks5/proxy-helpers/src/proxy_runner/inbound.rs @@ -11,7 +11,7 @@ use log::*; use nym_socks5_requests::{ConnectionId, SocketData}; use nym_task::connections::LaneQueueLengths; use nym_task::connections::TransmissionLane; -use nym_task::TaskClient; +use nym_task::ShutdownToken; use std::sync::Arc; use std::time::Duration; use tokio::select; @@ -81,7 +81,7 @@ pub(super) async fn run_inbound( available_plaintext_per_mix_packet: usize, shutdown_notify: Arc, lane_queue_lengths: Option, - mut shutdown_listener: TaskClient, + shutdown_listener: ShutdownToken, ) -> OwnedReadHalf where F: Fn(SocketData) -> S + Send + 'static, @@ -129,7 +129,7 @@ where message_sender.send_empty_close().await; break; } - _ = shutdown_listener.recv() => { + _ = shutdown_listener.cancelled() => { log::trace!("ProxyRunner inbound: Received shutdown"); break; } @@ -171,6 +171,5 @@ where trace!("{connection_id} - inbound closed"); shutdown_notify.notify_one(); - shutdown_listener.disarm(); reader } diff --git a/common/socks5/proxy-helpers/src/proxy_runner/mod.rs b/common/socks5/proxy-helpers/src/proxy_runner/mod.rs index 970378d811..75a51e366a 100644 --- a/common/socks5/proxy-helpers/src/proxy_runner/mod.rs +++ b/common/socks5/proxy-helpers/src/proxy_runner/mod.rs @@ -5,7 +5,7 @@ use crate::connection_controller::ConnectionReceiver; use crate::ordered_sender::OrderedMessageSender; use nym_socks5_requests::{ConnectionId, SocketData}; use nym_task::connections::LaneQueueLengths; -use nym_task::TaskClient; +use nym_task::ShutdownTracker; use std::fmt::Debug; use std::{sync::Arc, time::Duration}; use tokio::{net::TcpStream, sync::Notify}; @@ -57,7 +57,8 @@ pub struct ProxyRunner { available_plaintext_per_mix_packet: usize, // Listens to shutdown commands from higher up - shutdown_listener: TaskClient, + // and spawn new tracked tasks + shutdown_tracker: ShutdownTracker, } impl ProxyRunner @@ -74,7 +75,7 @@ where available_plaintext_per_mix_packet: usize, connection_id: ConnectionId, lane_queue_lengths: Option, - shutdown_listener: TaskClient, + shutdown_tracker: ShutdownTracker, ) -> Self { ProxyRunner { mix_receiver: Some(mix_receiver), @@ -85,7 +86,7 @@ where connection_id, lane_queue_lengths, available_plaintext_per_mix_packet, - shutdown_listener, + shutdown_tracker, } } @@ -113,7 +114,7 @@ where self.available_plaintext_per_mix_packet, Arc::clone(&shutdown_notify), self.lane_queue_lengths.clone(), - self.shutdown_listener.clone(), + self.shutdown_tracker.clone_shutdown_token(), ); let outbound_future = outbound::run_outbound( @@ -123,14 +124,26 @@ where self.mix_receiver.take().unwrap(), self.connection_id, shutdown_notify, - self.shutdown_listener.clone(), + self.shutdown_tracker.clone_shutdown_token(), ); // TODO: this shouldn't really have to spawn tasks inside "library" code, but // if we used join directly, stuff would have been executed on the same thread // (it's not bad, but an unnecessary slowdown) - let handle_inbound = tokio::spawn(inbound_future); - let handle_outbound = tokio::spawn(outbound_future); + let handle_inbound = self.shutdown_tracker.try_spawn_named( + inbound_future, + &format!( + "Socks5Inbound::{}::{}", + self.remote_source_address, self.connection_id + ), + ); + let handle_outbound = self.shutdown_tracker.try_spawn_named( + outbound_future, + &format!( + "Socks5Outbound::{}::{}", + self.remote_source_address, self.connection_id + ), + ); let (inbound_result, outbound_result) = futures::future::join(handle_inbound, handle_outbound).await; @@ -148,7 +161,6 @@ where } pub fn into_inner(mut self) -> (TcpStream, ConnectionReceiver) { - self.shutdown_listener.disarm(); ( self.socket.take().unwrap(), self.mix_receiver.take().unwrap(), diff --git a/common/socks5/proxy-helpers/src/proxy_runner/outbound.rs b/common/socks5/proxy-helpers/src/proxy_runner/outbound.rs index dddae8d894..dda46c5442 100644 --- a/common/socks5/proxy-helpers/src/proxy_runner/outbound.rs +++ b/common/socks5/proxy-helpers/src/proxy_runner/outbound.rs @@ -7,7 +7,7 @@ use futures::FutureExt; use futures::StreamExt; use log::*; use nym_socks5_requests::ConnectionId; -use nym_task::TaskClient; +use nym_task::ShutdownToken; use std::{sync::Arc, time::Duration}; use tokio::io::AsyncWriteExt; use tokio::select; @@ -51,7 +51,7 @@ pub(super) async fn run_outbound( mut mix_receiver: ConnectionReceiver, connection_id: ConnectionId, shutdown_notify: Arc, - mut shutdown_listener: TaskClient, + shutdown_listener: ShutdownToken, ) -> (OwnedWriteHalf, ConnectionReceiver) { let shutdown_future = shutdown_notify.notified().then(|_| sleep(SHUTDOWN_TIMEOUT)); tokio::pin!(shutdown_future); @@ -60,6 +60,11 @@ pub(super) async fn run_outbound( loop { select! { + biased; + _ = shutdown_listener.cancelled() => { + log::trace!("ProxyRunner outbound: Received shutdown"); + break; + } connection_message = mix_receiver.next() => { if let Some(connection_message) = connection_message { if deal_with_message(connection_message, &mut writer, &local_destination_address, &remote_source_address, connection_id).await { @@ -80,16 +85,11 @@ pub(super) async fn run_outbound( debug!("closing outbound proxy after inbound was closed {SHUTDOWN_TIMEOUT:?} ago"); break; } - _ = shutdown_listener.recv() => { - log::trace!("ProxyRunner outbound: Received shutdown"); - break; - } } } trace!("{connection_id} - outbound closed"); shutdown_notify.notify_one(); - shutdown_listener.disarm(); (writer, mix_receiver) } diff --git a/common/statistics/src/clients/mod.rs b/common/statistics/src/clients/mod.rs index 2ca1fa006d..3c2badcdfe 100644 --- a/common/statistics/src/clients/mod.rs +++ b/common/statistics/src/clients/mod.rs @@ -3,7 +3,7 @@ use crate::report::client::{ClientStatsReport, OsInformation}; -use nym_task::TaskClient; +use nym_task::ShutdownToken; use time::{OffsetDateTime, Time}; use tokio::sync::mpsc::UnboundedSender; @@ -25,18 +25,18 @@ pub type ClientStatsReceiver = tokio::sync::mpsc::UnboundedReceiver>, - task_client: TaskClient, + shutdown_token: ShutdownToken, } impl ClientStatsSender { /// Create a new statistics Sender pub fn new( stats_tx: Option>, - task_client: TaskClient, + shutdown_token: ShutdownToken, ) -> Self { ClientStatsSender { stats_tx, - task_client, + shutdown_token, } } @@ -44,7 +44,7 @@ impl ClientStatsSender { pub fn report(&self, event: ClientStatsEvents) { if let Some(tx) = &self.stats_tx { if let Err(err) = tx.send(event) { - if !self.task_client.is_shutdown_poll() { + if !self.shutdown_token.is_cancelled() { log::error!("Failed to send stats event: {err}"); } } @@ -137,8 +137,8 @@ impl ClientStatsController { self.packet_stats.snapshot(); } - pub fn local_report(&mut self, task_client: &mut TaskClient) { - self.packet_stats.local_report(task_client); + pub fn local_report(&mut self) { + self.packet_stats.local_report(); self.gateway_conn_stats.local_report(); self.nym_api_stats.local_report(); } diff --git a/common/statistics/src/clients/packet_statistics.rs b/common/statistics/src/clients/packet_statistics.rs index b2c6f56d5c..81de7e35f6 100644 --- a/common/statistics/src/clients/packet_statistics.rs +++ b/common/statistics/src/clients/packet_statistics.rs @@ -449,15 +449,16 @@ impl PacketStatisticsControl { self.stats.clone() } - pub(crate) fn local_report(&mut self, task_client: &mut nym_task::TaskClient) { - let rates = self.report_rates(); + pub(crate) fn local_report(&mut self) { + let _rates = self.report_rates(); self.check_for_notable_events(); self.report_counters(); - // Report our current bandwidth used to e.g a GUI client - if let Some(rates) = rates { - task_client.send_status_msg(Box::new(MixnetBandwidthStatisticsEvent::new(rates))); - } + // leave the code commented in case somebody wanted to restore this logic with a different channel + // // Report our current bandwidth used to e.g a GUI client + // if let Some(rates) = rates { + // task_client.send_status_msg(Box::new(MixnetBandwidthStatisticsEvent::new(rates))); + // } } // Add the current stats to the history, and remove old ones. diff --git a/common/task/Cargo.toml b/common/task/Cargo.toml index 6047373c53..d96fcbe068 100644 --- a/common/task/Cargo.toml +++ b/common/task/Cargo.toml @@ -30,5 +30,13 @@ workspace = true workspace = true features = ["tokio"] +[features] +tokio-tracing = ["tokio/tracing"] + [dev-dependencies] +anyhow = { workspace = true } tokio = { workspace = true, features = ["rt-multi-thread", "net", "signal", "test-util", "macros"] } +nym-test-utils = { path = "../test-utils" } + +[lints] +workspace = true \ No newline at end of file diff --git a/common/task/src/cancellation.rs b/common/task/src/cancellation.rs deleted file mode 100644 index d45296caca..0000000000 --- a/common/task/src/cancellation.rs +++ /dev/null @@ -1,402 +0,0 @@ -// Copyright 2025 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::{TaskClient, TaskManager}; -use futures::stream::FuturesUnordered; -use futures::StreamExt; -use std::future::Future; -use std::mem; -use std::ops::Deref; -use std::pin::Pin; -use std::time::Duration; -use tokio::task::JoinSet; -use tokio::time::sleep; -use tokio_util::sync::{CancellationToken, DropGuard}; -use tokio_util::task::TaskTracker; -use tracing::{debug, info, trace}; - -#[cfg(unix)] -use tokio::signal::unix::{signal, SignalKind}; - -pub const DEFAULT_MAX_SHUTDOWN_DURATION: Duration = Duration::from_secs(5); - -pub fn token_name(name: &Option) -> String { - name.clone().unwrap_or_else(|| "unknown".to_string()) -} - -// a wrapper around tokio's CancellationToken that adds optional `name` information to more easily -// track down sources of shutdown -#[derive(Debug, Default)] -pub struct ShutdownToken { - name: Option, - inner: CancellationToken, -} - -impl Clone for ShutdownToken { - fn clone(&self) -> Self { - // make sure to not accidentally overflow the stack if we keep cloning the handle - let name = if let Some(name) = &self.name { - if name != Self::OVERFLOW_NAME && name.len() < Self::MAX_NAME_LENGTH { - Some(format!("{name}-child")) - } else { - Some(Self::OVERFLOW_NAME.to_string()) - } - } else { - None - }; - - ShutdownToken { - name, - inner: self.inner.clone(), - } - } -} - -impl Deref for ShutdownToken { - type Target = CancellationToken; - - fn deref(&self) -> &Self::Target { - &self.inner - } -} - -impl ShutdownToken { - const MAX_NAME_LENGTH: usize = 128; - const OVERFLOW_NAME: &'static str = "reached maximum ShutdownToken children name depth"; - - pub fn new(name: impl Into) -> Self { - ShutdownToken { - name: Some(name.into()), - inner: CancellationToken::new(), - } - } - - pub fn ephemeral() -> Self { - ShutdownToken::new("ephemeral-token") - } - - // Creates a ShutdownToken which will get cancelled whenever the current token gets cancelled. - // Unlike a cloned/forked ShutdownToken, cancelling a child token does not cancel the parent token. - #[must_use] - pub fn child_token>(&self, child_suffix: S) -> Self { - let suffix = child_suffix.into(); - let child_name = if let Some(base) = &self.name { - format!("{base}-{suffix}") - } else { - format!("unknown-{suffix}") - }; - - ShutdownToken { - name: Some(child_name), - inner: self.inner.child_token(), - } - } - - // Creates a clone of the ShutdownToken which will get cancelled whenever the current token gets cancelled, and vice versa. - #[must_use] - pub fn clone_with_suffix>(&self, child_suffix: S) -> Self { - let mut child = self.clone(); - let suffix = child_suffix.into(); - let child_name = if let Some(base) = &self.name { - format!("{base}-{suffix}") - } else { - format!("unknown-{suffix}") - }; - - child.name = Some(child_name); - child - } - - // exposed method with the old name for easier migration - // it will eventually be removed so please try to use `.clone_with_suffix` instead - #[must_use] - pub fn fork>(&self, child_suffix: S) -> Self { - self.clone_with_suffix(child_suffix) - } - - // exposed method with the old name for easier migration - // it will eventually be removed so please try to use `.clone().named(name)` instead - #[must_use] - pub fn fork_named>(&self, name: S) -> Self { - self.clone().named(name) - } - - #[must_use] - pub fn named>(mut self, name: S) -> Self { - self.name = Some(name.into()); - self - } - - #[must_use] - pub fn add_suffix>(self, suffix: S) -> Self { - let suffix = suffix.into(); - let name = if let Some(base) = &self.name { - format!("{base}-{suffix}") - } else { - format!("unknown-{suffix}") - }; - self.named(name) - } - - // Returned guard will cancel this token (and all its children) on drop unless disarmed. - pub fn drop_guard(self) -> ShutdownDropGuard { - ShutdownDropGuard { - name: self.name, - inner: self.inner.drop_guard(), - } - } - - pub fn name(&self) -> String { - token_name(&self.name) - } - - pub async fn run_until_cancelled(&self, fut: F) -> Option - where - F: Future, - { - let res = self.inner.run_until_cancelled(fut).await; - trace!("'{}' got cancelled", self.name()); - res - } -} - -pub struct ShutdownDropGuard { - name: Option, - inner: DropGuard, -} - -impl Deref for ShutdownDropGuard { - type Target = DropGuard; - - fn deref(&self) -> &Self::Target { - &self.inner - } -} - -impl ShutdownDropGuard { - pub fn disarm(self) -> ShutdownToken { - ShutdownToken { - name: self.name, - inner: self.inner.disarm(), - } - } - - pub fn name(&self) -> String { - token_name(&self.name) - } -} - -#[derive(Default)] -pub struct ShutdownSignals(JoinSet<()>); - -impl ShutdownSignals { - pub async fn wait_for_signal(&mut self) { - self.0.join_next().await; - } -} - -pub struct ShutdownManager { - pub root_token: ShutdownToken, - - legacy_task_manager: Option, - - shutdown_signals: ShutdownSignals, - - // the reason I'm not using a `JoinSet` is because it forces us to use futures with the same `::Output` type - tracker: TaskTracker, - - max_shutdown_duration: Duration, -} - -impl Deref for ShutdownManager { - type Target = TaskTracker; - - fn deref(&self) -> &Self::Target { - &self.tracker - } -} - -impl ShutdownManager { - pub fn new(root_token_name: impl Into) -> Self { - let manager = ShutdownManager { - root_token: ShutdownToken::new(root_token_name), - legacy_task_manager: None, - shutdown_signals: Default::default(), - tracker: Default::default(), - max_shutdown_duration: Duration::from_secs(10), - }; - - // we need to add an explicit watcher for the cancellation token being cancelled - // so that we could cancel all legacy tasks - let cancel_watcher = manager.root_token.clone(); - manager.with_shutdown(async move { cancel_watcher.cancelled().await }) - } - - pub fn with_legacy_task_manager(mut self) -> Self { - let mut legacy_manager = - TaskManager::default().named(format!("{}-legacy", self.root_token.name())); - let mut legacy_error_rx = legacy_manager.task_return_error_rx(); - let mut legacy_drop_rx = legacy_manager.task_drop_rx(); - - self.legacy_task_manager = Some(legacy_manager); - - // add a task that listens for legacy task clients being dropped to trigger cancellation - self.with_shutdown(async move { - tokio::select! { - _ = legacy_error_rx.recv() => (), - _ = legacy_drop_rx.recv() => (), - } - - info!("received legacy shutdown signal"); - }) - } - - #[cfg(not(target_arch = "wasm32"))] - pub fn with_default_shutdown_signals(self) -> std::io::Result { - cfg_if::cfg_if! { - if #[cfg(unix)] { - self.with_interrupt_signal() - .with_terminate_signal()? - .with_quit_signal() - } else { - Ok(self.with_interrupt_signal()) - } - } - } - - #[must_use] - #[track_caller] - pub fn with_shutdown(mut self, shutdown: F) -> Self - where - F: Future, - F: Send + 'static, - { - let shutdown_token = self.root_token.clone(); - self.shutdown_signals.0.spawn(async move { - shutdown.await; - - info!("sending cancellation after receiving shutdown signal"); - shutdown_token.cancel(); - }); - self - } - - #[cfg(unix)] - #[track_caller] - pub fn with_shutdown_signal(self, signal_kind: SignalKind) -> std::io::Result { - let mut sig = signal(signal_kind)?; - Ok(self.with_shutdown(async move { - sig.recv().await; - })) - } - - #[cfg(not(target_arch = "wasm32"))] - #[track_caller] - pub fn with_interrupt_signal(self) -> Self { - self.with_shutdown(async move { - let _ = tokio::signal::ctrl_c().await; - }) - } - - #[cfg(unix)] - #[track_caller] - pub fn with_terminate_signal(self) -> std::io::Result { - self.with_shutdown_signal(SignalKind::terminate()) - } - - #[cfg(unix)] - #[track_caller] - pub fn with_quit_signal(self) -> std::io::Result { - self.with_shutdown_signal(SignalKind::quit()) - } - - #[must_use] - pub fn with_shutdown_duration(mut self, duration: Duration) -> Self { - self.max_shutdown_duration = duration; - self - } - - pub fn child_token>(&self, child_suffix: S) -> ShutdownToken { - self.root_token.child_token(child_suffix) - } - - pub fn clone_token>(&self, child_suffix: S) -> ShutdownToken { - self.root_token.clone_with_suffix(child_suffix) - } - - #[must_use] - pub fn subscribe_legacy>(&self, child_suffix: S) -> TaskClient { - // alternatively we could have set self.legacy_task_manager = Some(TaskManager::default()); - // on demand if it wasn't unavailable, but then we'd have to use mutable reference - #[allow(clippy::expect_used)] - self.legacy_task_manager - .as_ref() - .expect("did not enable legacy shutdown support") - .subscribe_named(child_suffix) - } - - async fn finish_shutdown(mut self) { - let mut wait_futures = FuturesUnordered::>>>::new(); - - // force shutdown via ctrl-c - wait_futures.push(Box::pin(async move { - #[cfg(not(target_arch = "wasm32"))] - let interrupt_future = tokio::signal::ctrl_c(); - - #[cfg(target_arch = "wasm32")] - let interrupt_future = futures::future::pending::<()>(); - - let _ = interrupt_future.await; - info!("received interrupt - forcing shutdown"); - })); - - // timeout - wait_futures.push(Box::pin(async move { - sleep(self.max_shutdown_duration).await; - info!("timeout reached, forcing shutdown"); - })); - - // graceful - wait_futures.push(Box::pin(async move { - self.tracker.wait().await; - debug!("migrated tasks successfully shutdown"); - if let Some(legacy) = self.legacy_task_manager.as_mut() { - legacy.wait_for_graceful_shutdown().await; - debug!("legacy tasks successfully shutdown"); - } - - info!("all registered tasks successfully shutdown") - })); - - wait_futures.next().await; - } - - pub fn detach_shutdown_signals(&mut self) -> ShutdownSignals { - mem::take(&mut self.shutdown_signals) - } - - pub fn replace_shutdown_signals(&mut self, signals: ShutdownSignals) { - self.shutdown_signals = signals; - } - - // cancellation safe - pub async fn wait_for_shutdown_signal(&mut self) { - self.shutdown_signals.0.join_next().await; - } - - pub async fn perform_shutdown(mut self) { - if let Some(legacy_manager) = self.legacy_task_manager.as_mut() { - info!("attempting to shutdown legacy tasks"); - let _ = legacy_manager.signal_shutdown(); - } - - info!("waiting for tasks to finish... (press ctrl-c to force)"); - self.finish_shutdown().await; - } - - pub async fn run_until_shutdown(mut self) { - self.wait_for_shutdown_signal().await; - - self.perform_shutdown().await; - } -} diff --git a/common/task/src/cancellation/manager.rs b/common/task/src/cancellation/manager.rs new file mode 100644 index 0000000000..f2cf5a9d02 --- /dev/null +++ b/common/task/src/cancellation/manager.rs @@ -0,0 +1,744 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::cancellation::tracker::{Cancelled, ShutdownTracker}; +use crate::spawn::JoinHandle; +use crate::ShutdownToken; +use futures::stream::FuturesUnordered; +use futures::StreamExt; +use log::error; +use std::future::Future; +use std::mem; +use std::pin::Pin; +use std::time::Duration; +use tracing::info; + +#[cfg(not(target_arch = "wasm32"))] +use tokio::time::sleep; + +#[cfg(target_arch = "wasm32")] +use wasmtimer::tokio::sleep; + +#[cfg(unix)] +use tokio::signal::unix::{signal, SignalKind}; +use tokio::task::JoinSet; + +/// A top level structure responsible for controlling process shutdown by listening to +/// the underlying registered signals and issuing cancellation to tasks derived from its root cancellation token. +#[allow(deprecated)] +pub struct ShutdownManager { + /// Optional reference to the legacy [TaskManager](crate::TaskManager) to allow easier + /// transition to the new system. + pub(crate) legacy_task_manager: Option, + + /// Registered [ShutdownSignals](ShutdownSignals) that will trigger process shutdown if detected. + pub(crate) shutdown_signals: ShutdownSignals, + + /// Combined [TaskTracker](tokio_util::task::TaskTracker) and [ShutdownToken](ShutdownToken) + /// for spawning and tracking tasks associated with this ShutdownManager. + pub(crate) tracker: ShutdownTracker, + + /// The maximum shutdown duration when tracked tasks could gracefully exit + /// before forcing the shutdown. + pub(crate) max_shutdown_duration: Duration, +} + +/// Wrapper behind futures that upon completion will trigger binary shutdown. +#[derive(Default)] +pub struct ShutdownSignals(JoinSet<()>); + +impl ShutdownSignals { + /// Wait for any of the registered signals to be ready + pub async fn wait_for_signal(&mut self) { + self.0.join_next().await; + } +} + +// note: default implementation will ONLY listen for SIGINT and will ignore SIGTERM and SIGQUIT +// this is due to result type when registering the signal +#[cfg(not(target_arch = "wasm32"))] +impl Default for ShutdownManager { + fn default() -> Self { + ShutdownManager::new_without_signals() + .with_interrupt_signal() + .with_cancel_on_panic() + } +} + +#[cfg(not(target_arch = "wasm32"))] +impl ShutdownManager { + /// Create new instance of ShutdownManager with the most sensible defaults, so that: + /// - shutdown will be triggered upon either SIGINT, SIGTERM (unix only) or SIGQUIT (unix only) being sent + /// - shutdown will be triggered upon any task panicking + pub fn build_new_default() -> std::io::Result { + Ok(ShutdownManager::new_without_signals() + .with_default_shutdown_signals()? + .with_cancel_on_panic()) + } + + /// Register a new shutdown signal that upon completion will trigger system shutdown. + #[must_use] + #[track_caller] + pub fn with_shutdown(mut self, shutdown: F) -> Self + where + F: Future, + F: Send + 'static, + { + let shutdown_token = self.tracker.clone_shutdown_token(); + self.shutdown_signals.0.spawn(async move { + shutdown.await; + + info!("sending cancellation after receiving shutdown signal"); + shutdown_token.cancel(); + }); + self + } + + /// Include support for the legacy [TaskManager](TaskManager) to this instance of the ShutdownManager. + /// This will allow issuing [TaskClient](TaskClient) for tasks that still require them. + #[allow(deprecated)] + pub fn with_legacy_task_manager(mut self) -> Self { + let mut legacy_manager = crate::TaskManager::default().named("legacy-task-manager"); + let mut legacy_error_rx = legacy_manager.task_return_error_rx(); + let mut legacy_drop_rx = legacy_manager.task_drop_rx(); + + self.legacy_task_manager = Some(legacy_manager); + + // add a task that listens for legacy task clients being dropped to trigger cancellation + self.with_shutdown(async move { + tokio::select! { + _ = legacy_error_rx.recv() => (), + _ = legacy_drop_rx.recv() => (), + } + + info!("received legacy shutdown signal"); + }) + } + + /// Add the specified signal to the currently registered shutdown signals that will trigger + /// cancellation of all registered tasks. + #[cfg(unix)] + #[track_caller] + pub fn with_shutdown_signal(self, signal_kind: SignalKind) -> std::io::Result { + let mut sig = signal(signal_kind)?; + Ok(self.with_shutdown(async move { + sig.recv().await; + })) + } + + /// Add the SIGTERM signal to the currently registered shutdown signals that will trigger + /// cancellation of all registered tasks. + #[cfg(unix)] + #[track_caller] + pub fn with_terminate_signal(self) -> std::io::Result { + self.with_shutdown_signal(SignalKind::terminate()) + } + + /// Add the SIGQUIT signal to the currently registered shutdown signals that will trigger + /// cancellation of all registered tasks. + #[cfg(unix)] + #[track_caller] + pub fn with_quit_signal(self) -> std::io::Result { + self.with_shutdown_signal(SignalKind::quit()) + } + + /// Add default signals to the set of the currently registered shutdown signals that will trigger + /// cancellation of all registered tasks. + /// This includes SIGINT, SIGTERM and SIGQUIT for unix-based platforms and SIGINT for other targets (such as windows)/ + pub fn with_default_shutdown_signals(self) -> std::io::Result { + cfg_if::cfg_if! { + if #[cfg(unix)] { + self.with_interrupt_signal() + .with_terminate_signal()? + .with_quit_signal() + } else { + Ok(self.with_interrupt_signal()) + } + } + } + + /// Add the SIGINT (ctrl-c) signal to the currently registered shutdown signals that will trigger + /// cancellation of all registered tasks. + #[track_caller] + pub fn with_interrupt_signal(self) -> Self { + self.with_shutdown(async move { + let _ = tokio::signal::ctrl_c().await; + }) + } + + /// Spawn the provided future on the current Tokio runtime, and track it in the underlying [TaskTracker](tokio_util::task::TaskTracker). + #[track_caller] + pub fn spawn(&self, task: F) -> JoinHandle + where + F: Future + Send + 'static, + F::Output: Send + 'static, + { + self.tracker.spawn(task) + } + + /// Spawn the provided future on the current Tokio runtime, + /// and track it in the underlying [TaskTracker](tokio_util::task::TaskTracker). + /// Furthermore, attach a name to the spawned task to more easily track it within a [tokio console](https://github.com/tokio-rs/console) + /// + /// Note that is no different from [spawn](Self::spawn) if the underlying binary + /// has not been built with `RUSTFLAGS="--cfg tokio_unstable"` and `--features="tokio-tracing"` + #[track_caller] + pub fn try_spawn_named(&self, task: F, name: &str) -> JoinHandle + where + F: Future + Send + 'static, + F::Output: Send + 'static, + { + self.tracker.try_spawn_named(task, name) + } + + /// Spawn the provided future on the provided Tokio runtime, + /// and track it in the underlying [TaskTracker](tokio_util::task::TaskTracker). + #[track_caller] + pub fn spawn_on(&self, task: F, handle: &tokio::runtime::Handle) -> JoinHandle + where + F: Future + Send + 'static, + F::Output: Send + 'static, + { + self.tracker.spawn_on(task, handle) + } + + /// Spawn the provided future on the current [LocalSet](tokio::task::LocalSet), + /// and track it in the underlying [TaskTracker](tokio_util::task::TaskTracker). + #[track_caller] + pub fn spawn_local(&self, task: F) -> JoinHandle + where + F: Future + 'static, + F::Output: 'static, + { + self.tracker.spawn_local(task) + } + + /// Spawn the provided blocking task on the current Tokio runtime, + /// and track it in the underlying [TaskTracker](tokio_util::task::TaskTracker). + #[track_caller] + pub fn spawn_blocking(&self, task: F) -> JoinHandle + where + F: FnOnce() -> T, + F: Send + 'static, + T: Send + 'static, + { + self.tracker.spawn_blocking(task) + } + + /// Spawn the provided blocking task on the provided Tokio runtime, + /// and track it in the underlying [TaskTracker](tokio_util::task::TaskTracker). + #[track_caller] + pub fn spawn_blocking_on(&self, task: F, handle: &tokio::runtime::Handle) -> JoinHandle + where + F: FnOnce() -> T, + F: Send + 'static, + T: Send + 'static, + { + self.tracker.spawn_blocking_on(task, handle) + } + + /// Spawn the provided future on the current Tokio runtime + /// that will get cancelled once a global shutdown signal is detected, + /// and track it in the underlying [TaskTracker](tokio_util::task::TaskTracker). + /// + /// Note that to fully use the naming feature, such as tracking within a [tokio console](https://github.com/tokio-rs/console), + /// the underlying binary has to be built with `RUSTFLAGS="--cfg tokio_unstable"` and `--features="tokio-tracing"` + #[track_caller] + pub fn try_spawn_named_with_shutdown( + &self, + task: F, + name: &str, + ) -> JoinHandle> + where + F: Future + Send + 'static, + F::Output: Send + 'static, + { + self.tracker.try_spawn_named_with_shutdown(task, name) + } + + /// Spawn the provided future on the current Tokio runtime + /// that will get cancelled once a global shutdown signal is detected, + /// and track it in the underlying [TaskTracker](tokio_util::task::TaskTracker). + #[track_caller] + pub fn spawn_with_shutdown(&self, task: F) -> JoinHandle> + where + F: Future + Send + 'static, + F::Output: Send + 'static, + { + self.tracker.spawn_with_shutdown(task) + } +} + +#[cfg(target_arch = "wasm32")] +impl ShutdownManager { + /// Run the provided future on the current thread, and track it in the underlying [TaskTracker](tokio_util::task::TaskTracker). + #[track_caller] + pub fn spawn(&self, task: F) -> JoinHandle + where + F: Future + 'static, + { + self.tracker.spawn(task) + } + + /// Run the provided future on the current thread, and track it in the underlying [TaskTracker](tokio_util::task::TaskTracker). + /// It has exactly the same behaviour as [spawn](Self::spawn) and it only exists to provide + /// the same interface as non-wasm32 targets. + #[track_caller] + pub fn try_spawn_named(&self, task: F, name: &str) -> JoinHandle + where + F: Future + 'static, + { + self.tracker.try_spawn_named(task, name) + } + + /// Run the provided future on the current thread + /// that will get cancelled once a global shutdown signal is detected, + /// and track it in the underlying [TaskTracker](tokio_util::task::TaskTracker). + /// It has exactly the same behaviour as [spawn_with_shutdown](Self::spawn_with_shutdown) and it only exists to provide + /// the same interface as non-wasm32 targets. + #[track_caller] + pub fn try_spawn_named_with_shutdown( + &self, + task: F, + name: &str, + ) -> JoinHandle> + where + F: Future + Send + 'static, + { + self.tracker.try_spawn_named_with_shutdown(task, name) + } + + /// Run the provided future on the current thread + /// that will get cancelled once a global shutdown signal is detected, + /// and track it in the underlying [TaskTracker](tokio_util::task::TaskTracker). + #[track_caller] + pub fn spawn_with_shutdown(&self, task: F) -> JoinHandle> + where + F: Future + Send + 'static, + { + self.tracker.spawn_with_shutdown(task) + } +} + +impl ShutdownManager { + /// Create new instance of ShutdownManager without any external shutdown signals registered, + /// meaning it will only attempt to wait for all tasks spawned on its tracker to gracefully finish execution. + pub fn new_without_signals() -> Self { + Self::new_from_external_shutdown_token(ShutdownToken::new()) + } + + /// Create new instance of the ShutdownManager using an external shutdown token. + /// + /// Note: it will not listen to any external shutdown signals! + /// You might want further customise it with [shutdown signals](Self::with_shutdown) + /// (or just use [the default set](Self::with_default_shutdown_signals). + /// Similarly, you might want to include [cancellation on panic](Self::with_cancel_on_panic) + /// to make sure everything gets cancelled if one of the tasks panics. + pub fn new_from_external_shutdown_token(shutdown_token: ShutdownToken) -> Self { + let manager = ShutdownManager { + legacy_task_manager: None, + shutdown_signals: Default::default(), + tracker: ShutdownTracker::new_from_external_shutdown_token(shutdown_token), + max_shutdown_duration: Duration::from_secs(10), + }; + + // we need to add an explicit watcher for the cancellation token being cancelled + // so that we could cancel all legacy tasks + cfg_if::cfg_if! {if #[cfg(not(target_arch = "wasm32"))] { + let cancel_watcher = manager.tracker.clone_shutdown_token(); + manager.with_shutdown(async move { cancel_watcher.cancelled().await }) + } else { + manager + }} + } + + /// Create an empty testing mock of the ShutdownManager with no signals registered. + pub fn empty_mock() -> Self { + ShutdownManager { + legacy_task_manager: None, + shutdown_signals: Default::default(), + tracker: Default::default(), + max_shutdown_duration: Default::default(), + } + } + + /// Add additional panic hook such that upon triggering, the root [ShutdownToken](ShutdownToken) gets cancelled. + /// Note: an unfortunate limitation of this is that graceful shutdown will no longer be possible + /// since that task that has panicked will not exit and thus all shutdowns will have to be either forced + /// or will have to time out. + #[must_use] + pub fn with_cancel_on_panic(self) -> Self { + let current_hook = std::panic::take_hook(); + + let shutdown_token = self.clone_shutdown_token(); + std::panic::set_hook(Box::new(move |panic_info| { + // 1. call existing hook + current_hook(panic_info); + + let location = panic_info + .location() + .map(|l| l.to_string()) + .unwrap_or_else(|| "".to_string()); + + let payload = if let Some(payload) = panic_info.payload().downcast_ref::<&str>() { + payload + } else { + "" + }; + + // 2. issue cancellation + error!("panicked at {location}: {payload}. issuing global cancellation"); + shutdown_token.cancel(); + })); + self + } + + /// Change the maximum shutdown duration when tracked tasks could gracefully exit + /// before forcing the shutdown. + #[must_use] + pub fn with_shutdown_duration(mut self, duration: Duration) -> Self { + self.max_shutdown_duration = duration; + self + } + + /// Returns true if the root [ShutdownToken](ShutdownToken) has been cancelled. + pub fn is_cancelled(&self) -> bool { + self.tracker.root_cancellation_token.is_cancelled() + } + + /// Get a reference to the used [ShutdownTracker](ShutdownTracker) + pub fn shutdown_tracker(&self) -> &ShutdownTracker { + &self.tracker + } + + /// Get a cloned instance of the used [ShutdownTracker](ShutdownTracker) + pub fn shutdown_tracker_owned(&self) -> ShutdownTracker { + self.tracker.clone() + } + + /// Waits until the underlying [TaskTracker](tokio_util::task::TaskTracker) is both closed and empty. + /// + /// If the underlying [TaskTracker](tokio_util::task::TaskTracker) is already closed and empty when this method is called, then it + /// returns immediately. + pub async fn wait_for_tracker(&self) { + self.tracker.wait_for_tracker().await; + } + + /// Close the underlying [TaskTracker](tokio_util::task::TaskTracker). + /// + /// This allows [`wait_for_tracker`] futures to complete. It does not prevent you from spawning new tasks. + /// + /// Returns `true` if this closed the underlying [TaskTracker](tokio_util::task::TaskTracker), or `false` if it was already closed. + /// + /// [`wait_for_tracker`]: ShutdownTracker::wait_for_tracker + pub fn close_tracker(&self) -> bool { + self.tracker.close_tracker() + } + + /// Reopen the underlying [TaskTracker](tokio_util::task::TaskTracker). + /// + /// This prevents [`wait_for_tracker`] futures from completing even if the underlying [TaskTracker](tokio_util::task::TaskTracker) is empty. + /// + /// Returns `true` if this reopened the underlying [TaskTracker](tokio_util::task::TaskTracker), or `false` if it was already open. + /// + /// [`wait_for_tracker`]: ShutdownTracker::wait_for_tracker + pub fn reopen_tracker(&self) -> bool { + self.tracker.reopen_tracker() + } + + /// Returns `true` if the underlying [TaskTracker](tokio_util::task::TaskTracker) is [closed](Self::close_tracker). + pub fn is_tracker_closed(&self) -> bool { + self.tracker.is_tracker_closed() + } + + /// Returns the number of tasks tracked by the underlying [TaskTracker](tokio_util::task::TaskTracker). + pub fn tracked_tasks(&self) -> usize { + self.tracker.tracked_tasks() + } + + /// Returns `true` if there are no tasks in the underlying [TaskTracker](tokio_util::task::TaskTracker). + pub fn is_tracker_empty(&self) -> bool { + self.tracker.is_tracker_empty() + } + + /// Obtain a [ShutdownToken](crate::cancellation::ShutdownToken) that is a child of the root token + pub fn child_shutdown_token(&self) -> ShutdownToken { + self.tracker.root_cancellation_token.child_token() + } + + /// Obtain a [ShutdownToken](crate::cancellation::ShutdownToken) on the same hierarchical structure as the root token + pub fn clone_shutdown_token(&self) -> ShutdownToken { + self.tracker.root_cancellation_token.clone() + } + + /// Attempt to create a handle to a legacy [TaskClient] to support tasks that hasn't migrated + /// from the legacy [TaskManager]. + /// Note. To use this method [ShutdownManager] must be built with `.with_legacy_task_manager()` + #[must_use] + #[deprecated] + #[allow(deprecated)] + pub fn subscribe_legacy>(&self, child_suffix: S) -> crate::TaskClient { + // alternatively we could have set self.legacy_task_manager = Some(TaskManager::default()); + // on demand if it wasn't unavailable, but then we'd have to use mutable reference + #[allow(clippy::expect_used)] + self.legacy_task_manager + .as_ref() + .expect("did not enable legacy shutdown support") + .subscribe_named(child_suffix) + } + + /// Finalise the shutdown procedure by waiting until either: + /// - all tracked tasks have terminated + /// - timeout has been reached + /// - shutdown has been forced (by sending SIGINT) + async fn finish_shutdown(&mut self) { + let mut wait_futures = FuturesUnordered:: + Send>>>::new(); + + // force shutdown via ctrl-c + wait_futures.push(Box::pin(async move { + #[cfg(not(target_arch = "wasm32"))] + let interrupt_future = tokio::signal::ctrl_c(); + + #[cfg(target_arch = "wasm32")] + let interrupt_future = futures::future::pending::<()>(); + + let _ = interrupt_future.await; + info!("received interrupt - forcing shutdown"); + })); + + // timeout + let max_shutdown = self.max_shutdown_duration; + wait_futures.push(Box::pin(async move { + sleep(max_shutdown).await; + info!("timeout reached - forcing shutdown"); + })); + + // graceful + let tracker = self.tracker.clone(); + wait_futures.push(Box::pin(async move { + tracker.wait_for_tracker().await; + info!("all tracked tasks successfully shutdown"); + if let Some(legacy) = self.legacy_task_manager.as_mut() { + legacy.wait_for_graceful_shutdown().await; + info!("all legacy tasks successfully shutdown"); + } + + info!("all registered tasks successfully shutdown") + })); + + wait_futures.next().await; + } + + /// Remove the current set of [ShutdownSignals] from this instance of + /// [ShutdownManager] replacing it with an empty set. + /// + /// This is potentially useful if one wishes to start listening for the signals + /// before the whole process has been fully set up. + pub fn detach_shutdown_signals(&mut self) -> ShutdownSignals { + mem::take(&mut self.shutdown_signals) + } + + /// Replace the current set of [ShutdownSignals] used for determining + /// whether the underlying process should be stopped. + pub fn replace_shutdown_signals(&mut self, signals: ShutdownSignals) { + self.shutdown_signals = signals; + } + + /// Send cancellation signal to all registered tasks by cancelling the root token + /// and sending shutdown signal, if applicable, on the legacy [TaskManager] + pub fn send_cancellation(&self) { + if let Some(legacy_manager) = self.legacy_task_manager.as_ref() { + info!("attempting to shutdown legacy tasks"); + let _ = legacy_manager.signal_shutdown(); + } + self.tracker.root_cancellation_token.cancel(); + } + + /// Wait until receiving one of the registered shutdown signals + /// this method is cancellation safe + pub async fn wait_for_shutdown_signal(&mut self) { + #[cfg(not(target_arch = "wasm32"))] + self.shutdown_signals.0.join_next().await; + + #[cfg(target_arch = "wasm32")] + self.tracker.root_cancellation_token.cancelled().await; + } + + /// Perform system shutdown by sending relevant signals and waiting until either: + /// - all tracked tasks have terminated + /// - timeout has been reached + /// - shutdown has been forced (by sending SIGINT) + pub async fn perform_shutdown(&mut self) { + self.send_cancellation(); + + info!("waiting for tasks to finish... (press ctrl-c to force)"); + self.finish_shutdown().await; + } + + /// Wait until a shutdown signal has been received and trigger system shutdown. + pub async fn run_until_shutdown(&mut self) { + self.close_tracker(); + self.wait_for_shutdown_signal().await; + + self.perform_shutdown().await; + } +} + +#[cfg(test)] +mod tests { + use super::*; + use nym_test_utils::traits::{ElapsedExt, Timeboxed}; + use std::sync::atomic::AtomicBool; + use std::sync::Arc; + + #[tokio::test] + async fn shutdown_with_no_tracked_tasks_and_signals() -> anyhow::Result<()> { + let mut manager = ShutdownManager::new_without_signals(); + let res = manager.run_until_shutdown().timeboxed().await; + assert!(res.has_elapsed()); + + let mut manager = ShutdownManager::new_without_signals(); + let shutdown = manager.clone_shutdown_token(); + shutdown.cancel(); + let res = manager.run_until_shutdown().timeboxed().await; + assert!(!res.has_elapsed()); + + Ok(()) + } + + #[tokio::test] + async fn shutdown_signal() -> anyhow::Result<()> { + let timeout_shutdown = sleep(Duration::from_millis(100)); + let mut manager = ShutdownManager::new_without_signals().with_shutdown(timeout_shutdown); + + // execution finishes after the sleep gets finishes + let res = manager + .run_until_shutdown() + .execute_with_deadline(Duration::from_millis(200)) + .await; + assert!(!res.has_elapsed()); + + Ok(()) + } + + #[tokio::test] + async fn panic_hook() -> anyhow::Result<()> { + let mut manager = ShutdownManager::new_without_signals().with_cancel_on_panic(); + manager.spawn_with_shutdown(async move { + sleep(Duration::from_millis(10000)).await; + }); + manager.spawn_with_shutdown(async move { + sleep(Duration::from_millis(10)).await; + panic!("panicking"); + }); + + // execution finishes after the panic gets triggered + let res = manager + .run_until_shutdown() + .execute_with_deadline(Duration::from_millis(200)) + .await; + assert!(!res.has_elapsed()); + + Ok(()) + } + + #[tokio::test] + async fn task_cancellation() -> anyhow::Result<()> { + let timeout_shutdown = sleep(Duration::from_millis(100)); + let mut manager = ShutdownManager::new_without_signals().with_shutdown(timeout_shutdown); + + let cancelled1 = Arc::new(AtomicBool::new(false)); + let cancelled1_clone = cancelled1.clone(); + let cancelled2 = Arc::new(AtomicBool::new(false)); + let cancelled2_clone = cancelled2.clone(); + + let shutdown = manager.clone_shutdown_token(); + manager.spawn(async move { + shutdown.cancelled().await; + cancelled1_clone.store(true, std::sync::atomic::Ordering::Relaxed); + }); + + let shutdown = manager.clone_shutdown_token(); + manager.spawn(async move { + shutdown.cancelled().await; + cancelled2_clone.store(true, std::sync::atomic::Ordering::Relaxed); + }); + + let res = manager + .run_until_shutdown() + .execute_with_deadline(Duration::from_millis(200)) + .await; + + assert!(!res.has_elapsed()); + assert!(cancelled1.load(std::sync::atomic::Ordering::Relaxed)); + assert!(cancelled2.load(std::sync::atomic::Ordering::Relaxed)); + Ok(()) + } + + #[tokio::test] + async fn cancellation_within_task() -> anyhow::Result<()> { + let mut manager = ShutdownManager::new_without_signals(); + + let cancelled1 = Arc::new(AtomicBool::new(false)); + let cancelled1_clone = cancelled1.clone(); + + let shutdown = manager.clone_shutdown_token(); + manager.spawn(async move { + shutdown.cancelled().await; + cancelled1_clone.store(true, std::sync::atomic::Ordering::Relaxed); + }); + + let shutdown = manager.clone_shutdown_token(); + manager.spawn(async move { + sleep(Duration::from_millis(10)).await; + shutdown.cancel(); + }); + + let res = manager + .run_until_shutdown() + .execute_with_deadline(Duration::from_millis(200)) + .await; + + assert!(!res.has_elapsed()); + assert!(cancelled1.load(std::sync::atomic::Ordering::Relaxed)); + Ok(()) + } + + #[tokio::test] + async fn shutdown_timeout() -> anyhow::Result<()> { + let timeout_shutdown = sleep(Duration::from_millis(50)); + let mut manager = ShutdownManager::new_without_signals() + .with_shutdown(timeout_shutdown) + .with_shutdown_duration(Duration::from_millis(1000)); + + // ignore shutdown signals + manager.spawn(async move { + sleep(Duration::from_millis(1000)).await; + }); + + let res = manager + .run_until_shutdown() + .execute_with_deadline(Duration::from_millis(200)) + .await; + + assert!(res.has_elapsed()); + + let timeout_shutdown = sleep(Duration::from_millis(50)); + let mut manager = ShutdownManager::new_without_signals() + .with_shutdown(timeout_shutdown) + .with_shutdown_duration(Duration::from_millis(100)); + + // ignore shutdown signals + manager.spawn(async move { + sleep(Duration::from_millis(1000)).await; + }); + + let res = manager + .run_until_shutdown() + .execute_with_deadline(Duration::from_millis(200)) + .await; + + assert!(!res.has_elapsed()); + Ok(()) + } +} diff --git a/common/task/src/cancellation/mod.rs b/common/task/src/cancellation/mod.rs new file mode 100644 index 0000000000..fc9c5a648b --- /dev/null +++ b/common/task/src/cancellation/mod.rs @@ -0,0 +1,54 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +//! A [CancellationToken](tokio_util::sync::CancellationToken)-backed shutdown mechanism for Nym binaries. +//! +//! It allows creation of a centralised manager for keeping track of all signals that are meant +//! to trigger exit of all associated tasks and sending cancellation to the aforementioned futures. +//! +//! # Default usage +//! +//! ```no_run +//! use std::time::Duration; +//! use tokio::time::sleep; +//! use nym_task::{ShutdownManager, ShutdownToken}; +//! +//! async fn my_task() { +//! loop { +//! sleep(Duration::from_secs(5)).await +//! // do some periodic work that can be easily interrupted +//! } +//! } +//! +//! async fn important_work_that_cant_be_interrupted() {} +//! +//! async fn my_managed_task(shutdown_token: ShutdownToken) { +//! tokio::select! { +//! _ = shutdown_token.cancelled() => {} +//! _ = important_work_that_cant_be_interrupted() => {} +//! } +//! } +//! #[tokio::main] +//! async fn main() { +//! let mut shutdown_manager = ShutdownManager::build_new_default().expect("failed to register default shutdown signals"); +//! +//! let shutdown_token = shutdown_manager.child_shutdown_token(); +//! shutdown_manager.try_spawn_named(async move { my_managed_task(shutdown_token).await }, "important-managed-task"); +//! shutdown_manager.try_spawn_named_with_shutdown(my_task(), "another-task"); +//! +//! // wait for shutdown signal +//! shutdown_manager.run_until_shutdown().await; +//! } +//! ``` + +use std::time::Duration; + +pub mod manager; +pub mod token; +pub mod tracker; + +pub use manager::ShutdownManager; +pub use token::{ShutdownDropGuard, ShutdownToken}; +pub use tracker::ShutdownTracker; + +pub const DEFAULT_MAX_SHUTDOWN_DURATION: Duration = Duration::from_secs(5); diff --git a/common/task/src/cancellation/token.rs b/common/task/src/cancellation/token.rs new file mode 100644 index 0000000000..18d6648219 --- /dev/null +++ b/common/task/src/cancellation/token.rs @@ -0,0 +1,157 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::event::SentStatus; +use std::future::Future; +use tokio_util::sync::{ + CancellationToken, DropGuard, WaitForCancellationFuture, WaitForCancellationFutureOwned, +}; +use tracing::warn; + +/// A wrapped [CancellationToken](tokio_util::sync::CancellationToken) that is used for +/// signalling and listening for cancellation requests. +// We don't use CancellationToken in case we wanted to include additional fields/methods +// down the line. +#[derive(Debug, Clone, Default)] +pub struct ShutdownToken { + inner: CancellationToken, +} + +impl ShutdownToken { + /// A drop in no-op replacement for `send_status_msg` for easier migration from [TaskClient](crate::TaskClient). + #[deprecated] + #[track_caller] + pub fn send_status_msg(&self, status: SentStatus) { + let caller = std::panic::Location::caller(); + warn!("{caller} attempted to send {status} - there are no more listeners of those"); + } + + /// Creates a new ShutdownToken in the non-cancelled state. + pub fn new() -> Self { + ShutdownToken { + inner: CancellationToken::new(), + } + } + + /// Creates a new ShutdownToken given a tokio `CancellationToken`. + pub fn new_from_tokio_token(cancellation_token: CancellationToken) -> Self { + ShutdownToken { + inner: cancellation_token, + } + } + + /// Gets reference to the underlying [CancellationToken](tokio_util::sync::CancellationToken). + pub fn inner(&self) -> &CancellationToken { + &self.inner + } + + /// Creates a `ShutdownToken` which will get cancelled whenever the + /// current token gets cancelled. Unlike a cloned `ShutdownToken`, + /// cancelling a child token does not cancel the parent token. + /// + /// If the current token is already cancelled, the child token will get + /// returned in cancelled state. + pub fn child_token(&self) -> ShutdownToken { + ShutdownToken { + inner: self.inner.child_token(), + } + } + + /// Cancel the underlying [CancellationToken](tokio_util::sync::CancellationToken) and all child tokens which had been + /// derived from it. + /// + /// This will wake up all tasks which are waiting for cancellation. + pub fn cancel(&self) { + self.inner.cancel(); + } + + /// Returns `true` if the underlying [CancellationToken](tokio_util::sync::CancellationToken) is cancelled. + pub fn is_cancelled(&self) -> bool { + self.inner.is_cancelled() + } + + /// Returns a `Future` that gets fulfilled when cancellation is requested. + /// + /// The future will complete immediately if the token is already cancelled + /// when this method is called. + /// + /// # Cancel safety + /// + /// This method is cancel safe. + pub fn cancelled(&self) -> WaitForCancellationFuture<'_> { + self.inner.cancelled() + } + + /// Returns a `Future` that gets fulfilled when cancellation is requested. + /// + /// The future will complete immediately if the token is already cancelled + /// when this method is called. + /// + /// The function takes self by value and returns a future that owns the + /// token. + /// + /// # Cancel safety + /// + /// This method is cancel safe. + pub fn cancelled_owned(self) -> WaitForCancellationFutureOwned { + self.inner.cancelled_owned() + } + + /// Creates a `ShutdownDropGuard` for this token. + /// + /// Returned guard will cancel this token (and all its children) on drop + /// unless disarmed. + pub fn drop_guard(self) -> ShutdownDropGuard { + ShutdownDropGuard { + inner: self.inner.drop_guard(), + } + } + + /// Runs a future to completion and returns its result wrapped inside an `Option` + /// unless the `ShutdownToken` is cancelled. In that case the function returns + /// `None` and the future gets dropped. + /// + /// # Cancel safety + /// + /// This method is only cancel safe if `fut` is cancel safe. + pub async fn run_until_cancelled(&self, fut: F) -> Option + where + F: Future, + { + self.inner.run_until_cancelled(fut).await + } + + /// Runs a future to completion and returns its result wrapped inside an `Option` + /// unless the `ShutdownToken` is cancelled. In that case the function returns + /// `None` and the future gets dropped. + /// + /// The function takes self by value and returns a future that owns the token. + /// + /// # Cancel safety + /// + /// This method is only cancel safe if `fut` is cancel safe. + pub async fn run_until_cancelled_owned(self, fut: F) -> Option + where + F: Future, + { + self.inner.run_until_cancelled_owned(fut).await + } +} + +/// A wrapper for [DropGuard](tokio_util::sync::DropGuard) that wraps around a cancellation token +/// which automatically cancels it on drop. +/// It is created using `drop_guard` method on the `ShutdownToken`. +pub struct ShutdownDropGuard { + inner: DropGuard, +} + +impl ShutdownDropGuard { + /// Returns stored [ShutdownToken](ShutdownToken) and removes this drop guard instance + /// (i.e. it will no longer cancel token). Other guards for this token + /// are not affected. + pub fn disarm(self) -> ShutdownToken { + ShutdownToken { + inner: self.inner.disarm(), + } + } +} diff --git a/common/task/src/cancellation/tracker.rs b/common/task/src/cancellation/tracker.rs new file mode 100644 index 0000000000..c48b9c71ff --- /dev/null +++ b/common/task/src/cancellation/tracker.rs @@ -0,0 +1,317 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::cancellation::token::ShutdownToken; +use crate::spawn::{spawn_named_future, JoinHandle}; +use crate::spawn_future; +use std::future::Future; +use thiserror::Error; +use tokio_util::task::TaskTracker; +use tracing::{debug, trace}; + +#[derive(Debug, Error)] +#[error("task got cancelled")] +pub struct Cancelled; + +/// Extracted [TaskTracker](tokio_util::task::TaskTracker) and [ShutdownToken](ShutdownToken) to more easily allow tracking nested tasks +/// without having to pass whole [ShutdownManager](ShutdownManager) around. +#[derive(Clone, Default, Debug)] +pub struct ShutdownTracker { + /// The root [ShutdownToken](ShutdownToken) that will trigger all derived tasks + /// to receive cancellation signal. + pub(crate) root_cancellation_token: ShutdownToken, + + // Note: the reason we're not using a `JoinSet` is + // because it forces us to use futures with the same `::Output` type, + // which is not really a desirable property in this instance. + /// Tracker used for keeping track of all registered tasks + /// so that they could be stopped gracefully before ending the process. + pub(crate) tracker: TaskTracker, +} + +#[cfg(not(target_arch = "wasm32"))] +impl ShutdownTracker { + /// Spawn the provided future on the current Tokio runtime, and track it in the underlying [TaskTracker](tokio_util::task::TaskTracker). + #[track_caller] + pub fn spawn(&self, task: F) -> JoinHandle + where + F: Future + Send + 'static, + F::Output: Send + 'static, + { + let tracked = self.tracker.track_future(task); + spawn_future(tracked) + } + + /// Spawn the provided future on the current Tokio runtime, + /// and track it in the underlying [TaskTracker](tokio_util::task::TaskTracker). + /// Furthermore, attach a name to the spawned task to more easily track it within a [tokio console](https://github.com/tokio-rs/console) + /// + /// Note that is no different from [spawn](Self::spawn) if the underlying binary + /// has not been built with `RUSTFLAGS="--cfg tokio_unstable"` and `--features="tokio-tracing"` + #[track_caller] + pub fn try_spawn_named(&self, task: F, name: &str) -> JoinHandle + where + F: Future + Send + 'static, + F::Output: Send + 'static, + { + trace!("attempting to spawn task {name}"); + let tracked = self.tracker.track_future(task); + spawn_named_future(tracked, name) + } + + /// Spawn the provided future on the provided Tokio runtime, + /// and track it in the underlying [TaskTracker](tokio_util::task::TaskTracker). + #[track_caller] + pub fn spawn_on(&self, task: F, handle: &tokio::runtime::Handle) -> JoinHandle + where + F: Future + Send + 'static, + F::Output: Send + 'static, + { + self.tracker.spawn_on(task, handle) + } + + /// Spawn the provided future on the current [LocalSet](tokio::task::LocalSet), + /// and track it in the underlying [TaskTracker](tokio_util::task::TaskTracker). + #[track_caller] + pub fn spawn_local(&self, task: F) -> JoinHandle + where + F: Future + 'static, + F::Output: 'static, + { + self.tracker.spawn_local(task) + } + + /// Spawn the provided blocking task on the current Tokio runtime, + /// and track it in the underlying [TaskTracker](tokio_util::task::TaskTracker). + #[track_caller] + pub fn spawn_blocking(&self, task: F) -> JoinHandle + where + F: FnOnce() -> T, + F: Send + 'static, + T: Send + 'static, + { + self.tracker.spawn_blocking(task) + } + + /// Spawn the provided blocking task on the provided Tokio runtime, + /// and track it in the underlying [TaskTracker](tokio_util::task::TaskTracker). + #[track_caller] + pub fn spawn_blocking_on(&self, task: F, handle: &tokio::runtime::Handle) -> JoinHandle + where + F: FnOnce() -> T, + F: Send + 'static, + T: Send + 'static, + { + self.tracker.spawn_blocking_on(task, handle) + } + + /// Spawn the provided future on the current Tokio runtime + /// that will get cancelled once a global shutdown signal is detected, + /// and track it in the underlying [TaskTracker](tokio_util::task::TaskTracker). + /// + /// Note that to fully use the naming feature, such as tracking within a [tokio console](https://github.com/tokio-rs/console), + /// the underlying binary has to be built with `RUSTFLAGS="--cfg tokio_unstable"` and `--features="tokio-tracing"` + #[track_caller] + pub fn try_spawn_named_with_shutdown( + &self, + task: F, + name: &str, + ) -> JoinHandle> + where + F: Future + Send + 'static, + F::Output: Send + 'static, + { + trace!("attempting to spawn task {name} (with top-level cancellation)"); + + let caller = std::panic::Location::caller(); + let shutdown_token = self.clone_shutdown_token(); + let name_owned = name.to_string(); + let tracked = self.tracker.track_future(async move { + match shutdown_token.run_until_cancelled_owned(task).await { + Some(result) => { + debug!("{name_owned} @ {caller}: task has finished execution"); + Ok(result) + } + None => { + trace!("{name_owned} @ {caller}: shutdown signal received, shutting down"); + Err(Cancelled) + } + } + }); + spawn_named_future(tracked, name) + } + + /// Spawn the provided future on the current Tokio runtime + /// that will get cancelled once a global shutdown signal is detected, + /// and track it in the underlying [TaskTracker](tokio_util::task::TaskTracker). + #[track_caller] + pub fn spawn_with_shutdown(&self, task: F) -> JoinHandle> + where + F: Future + Send + 'static, + F::Output: Send + 'static, + { + let caller = std::panic::Location::caller(); + let shutdown_token = self.clone_shutdown_token(); + self.tracker.spawn(async move { + match shutdown_token.run_until_cancelled_owned(task).await { + Some(result) => { + debug!("{caller}: task has finished execution"); + Ok(result) + } + None => { + trace!("{caller}: shutdown signal received, shutting down"); + Err(Cancelled) + } + } + }) + } +} + +#[cfg(target_arch = "wasm32")] +impl ShutdownTracker { + /// Run the provided future on the current thread, and track it in the underlying [TaskTracker](tokio_util::task::TaskTracker). + #[track_caller] + pub fn spawn(&self, task: F) -> JoinHandle + where + F: Future + 'static, + { + let tracked = self.tracker.track_future(task); + spawn_future(tracked) + } + + /// Run the provided future on the current thread, and track it in the underlying [TaskTracker](tokio_util::task::TaskTracker). + /// It has exactly the same behaviour as [spawn](Self::spawn) and it only exists to provide + /// the same interface as non-wasm32 targets. + #[track_caller] + pub fn try_spawn_named(&self, task: F, name: &str) -> JoinHandle + where + F: Future + 'static, + { + let tracked = self.tracker.track_future(task); + spawn_named_future(tracked, name) + } + + /// Run the provided future on the current thread + /// that will get cancelled once a global shutdown signal is detected, + /// and track it in the underlying [TaskTracker](tokio_util::task::TaskTracker). + /// It has exactly the same behaviour as [spawn_with_shutdown](Self::spawn_with_shutdown) and it only exists to provide + /// the same interface as non-wasm32 targets. + #[track_caller] + pub fn try_spawn_named_with_shutdown( + &self, + task: F, + name: &str, + ) -> JoinHandle> + where + F: Future + 'static, + { + let caller = std::panic::Location::caller(); + let shutdown_token = self.clone_shutdown_token(); + let tracked = self.tracker.track_future(async move { + match shutdown_token.run_until_cancelled_owned(task).await { + Some(result) => { + debug!("{caller}: task has finished execution"); + Ok(result) + } + None => { + trace!("{caller}: shutdown signal received, shutting down"); + Err(Cancelled) + } + } + }); + spawn_named_future(tracked, name) + } + + /// Run the provided future on the current thread + /// that will get cancelled once a global shutdown signal is detected, + /// and track it in the underlying [TaskTracker](tokio_util::task::TaskTracker). + #[track_caller] + pub fn spawn_with_shutdown(&self, task: F) -> JoinHandle> + where + F: Future + 'static, + { + let caller = std::panic::Location::caller(); + let shutdown_token = self.clone_shutdown_token(); + let tracked = self.tracker.track_future(async move { + match shutdown_token.run_until_cancelled_owned(task).await { + Some(result) => { + debug!("{caller}: task has finished execution"); + Ok(result) + } + None => { + trace!("{caller}: shutdown signal received, shutting down"); + Err(Cancelled) + } + } + }); + spawn_future(tracked) + } +} + +impl ShutdownTracker { + /// Create new instance of the ShutdownTracker using an external shutdown token. + /// This could be useful in situations where shutdown is being managed by an external entity + /// that is not [ShutdownManager](ShutdownManager), but interface requires providing a ShutdownTracker, + /// such as client-core tasks + pub fn new_from_external_shutdown_token(shutdown_token: ShutdownToken) -> Self { + ShutdownTracker { + root_cancellation_token: shutdown_token, + tracker: Default::default(), + } + } + + /// Waits until the underlying [TaskTracker](tokio_util::task::TaskTracker) is both closed and empty. + /// + /// If the underlying [TaskTracker](tokio_util::task::TaskTracker) is already closed and empty when this method is called, then it + /// returns immediately. + pub async fn wait_for_tracker(&self) { + self.tracker.wait().await; + } + + /// Close the underlying [TaskTracker](tokio_util::task::TaskTracker). + /// + /// This allows [`wait_for_tracker`] futures to complete. It does not prevent you from spawning new tasks. + /// + /// Returns `true` if this closed the underlying [TaskTracker](tokio_util::task::TaskTracker), or `false` if it was already closed. + /// + /// [`wait_for_tracker`]: Self::wait_for_tracker + pub fn close_tracker(&self) -> bool { + self.tracker.close() + } + + /// Reopen the underlying [TaskTracker](tokio_util::task::TaskTracker). + /// + /// This prevents [`wait_for_tracker`] futures from completing even if the underlying [TaskTracker](tokio_util::task::TaskTracker) is empty. + /// + /// Returns `true` if this reopened the underlying [TaskTracker](tokio_util::task::TaskTracker), or `false` if it was already open. + /// + /// [`wait_for_tracker`]: Self::wait_for_tracker + pub fn reopen_tracker(&self) -> bool { + self.tracker.reopen() + } + + /// Returns `true` if the underlying [TaskTracker](tokio_util::task::TaskTracker) is [closed](Self::close_tracker). + pub fn is_tracker_closed(&self) -> bool { + self.tracker.is_closed() + } + + /// Returns the number of tasks tracked by the underlying [TaskTracker](tokio_util::task::TaskTracker). + pub fn tracked_tasks(&self) -> usize { + self.tracker.len() + } + + /// Returns `true` if there are no tasks in the underlying [TaskTracker](tokio_util::task::TaskTracker). + pub fn is_tracker_empty(&self) -> bool { + self.tracker.is_empty() + } + + /// Obtain a [ShutdownToken](crate::cancellation::ShutdownToken) that is a child of the root token + pub fn child_shutdown_token(&self) -> ShutdownToken { + self.root_cancellation_token.child_token() + } + + /// Obtain a [ShutdownToken](crate::cancellation::ShutdownToken) on the same hierarchical structure as the root token + pub fn clone_shutdown_token(&self) -> ShutdownToken { + self.root_cancellation_token.clone() + } +} diff --git a/common/task/src/connections.rs b/common/task/src/connections.rs index 35f448c622..b0e8fc6ecc 100644 --- a/common/task/src/connections.rs +++ b/common/task/src/connections.rs @@ -2,12 +2,9 @@ // SPDX-License-Identifier: Apache-2.0 use futures::channel::mpsc; -use std::{ - collections::HashMap, - time::{Duration, Instant}, -}; +use std::collections::HashMap; -const LANE_CONSIDERED_CLEAR: usize = 10; +// const LANE_CONSIDERED_CLEAR: usize = 10; pub type ConnectionId = u64; @@ -83,21 +80,21 @@ impl LaneQueueLengths { } } - pub async fn wait_until_clear(&self, lane: &TransmissionLane, timeout: Option) { - let total_time_waited = Instant::now(); - loop { - let lane_length = self.get(lane).unwrap_or_default(); - if lane_length < LANE_CONSIDERED_CLEAR { - break; - } - if timeout.is_some_and(|timeout| total_time_waited.elapsed() > timeout) { - log::warn!("Timeout reached while waiting for queue to clear"); - break; - } - log::trace!("Waiting for queue to clear ({lane_length} items left)"); - tokio::time::sleep(Duration::from_millis(100)).await; - } - } + // pub async fn wait_until_clear(&self, lane: &TransmissionLane, timeout: Option) { + // let total_time_waited = Instant::now(); + // loop { + // let lane_length = self.get(lane).unwrap_or_default(); + // if lane_length < LANE_CONSIDERED_CLEAR { + // break; + // } + // if timeout.is_some_and(|timeout| total_time_waited.elapsed() > timeout) { + // log::warn!("Timeout reached while waiting for queue to clear"); + // break; + // } + // log::trace!("Waiting for queue to clear ({lane_length} items left)"); + // tokio::time::sleep(Duration::from_millis(100)).await; + // } + // } } impl Default for LaneQueueLengths { diff --git a/common/task/src/lib.rs b/common/task/src/lib.rs index edbf0afef4..7be94f6c46 100644 --- a/common/task/src/lib.rs +++ b/common/task/src/lib.rs @@ -9,10 +9,11 @@ pub mod manager; pub mod signal; pub mod spawn; -pub use cancellation::{ShutdownDropGuard, ShutdownManager, ShutdownToken}; +pub use cancellation::{ShutdownDropGuard, ShutdownManager, ShutdownToken, ShutdownTracker}; pub use event::{StatusReceiver, StatusSender, TaskStatus, TaskStatusEvent}; -pub use manager::{TaskClient, TaskHandle, TaskManager}; -pub use spawn::{spawn, spawn_with_report_error}; +#[allow(deprecated)] +pub use manager::{TaskClient, TaskManager}; +pub use spawn::spawn_future; pub use tokio_util::task::TaskTracker; #[cfg(not(target_arch = "wasm32"))] diff --git a/common/task/src/manager.rs b/common/task/src/manager.rs index d5af78fcc0..5a6d22bfb6 100644 --- a/common/task/src/manager.rs +++ b/common/task/src/manager.rs @@ -44,6 +44,7 @@ enum TaskError { /// Listens to status and error messages from tasks, as well as notifying them to gracefully /// shutdown. Keeps track of if task stop unexpectedly, such as in a panic. +#[deprecated(note = "use ShutdownManager instead")] #[derive(Debug)] pub struct TaskManager { // optional name assigned to the task manager that all subscribed task clients will inherit @@ -72,6 +73,7 @@ pub struct TaskManager { task_status_rx: Option, } +#[allow(deprecated)] impl Default for TaskManager { fn default() -> Self { let (notify_tx, notify_rx) = watch::channel(()); @@ -95,6 +97,8 @@ impl Default for TaskManager { } } +#[allow(deprecated)] +#[allow(clippy::expect_used)] impl TaskManager { pub fn new(shutdown_timer_secs: u64) -> Self { Self { @@ -168,7 +172,7 @@ impl TaskManager { if let Some(mut task_status_rx) = self.task_status_rx.take() { log::info!("Starting status message listener"); - crate::spawn::spawn(async move { + crate::spawn::spawn_future(async move { loop { if let Some(msg) = task_status_rx.next().await { log::trace!("Got msg: {msg}"); @@ -186,12 +190,14 @@ impl TaskManager { } // used for compatibility with the ShutdownManager + #[cfg(not(target_arch = "wasm32"))] pub(crate) fn task_return_error_rx(&mut self) -> ErrorReceiver { self.task_return_error_rx .take() .expect("unable to get error channel: attempt to wait twice?") } + #[cfg(not(target_arch = "wasm32"))] pub(crate) fn task_drop_rx(&mut self) -> ErrorReceiver { self.task_drop_rx .take() @@ -259,6 +265,7 @@ impl TaskManager { /// Listen for shutdown notifications, and can send error and status messages back to the /// `TaskManager` #[derive(Debug)] +#[deprecated(note = "use ShutdownToken instead")] pub struct TaskClient { // optional name assigned to the shutdown handle name: Option, @@ -286,6 +293,7 @@ pub struct TaskClient { mode: ClientOperatingMode, } +#[allow(deprecated)] impl Clone for TaskClient { fn clone(&self) -> Self { // make sure to not accidentally overflow the stack if we keep cloning the handle @@ -313,6 +321,7 @@ impl Clone for TaskClient { } } +#[allow(deprecated)] impl TaskClient { const MAX_NAME_LENGTH: usize = 128; const OVERFLOW_NAME: &'static str = "reached maximum TaskClient children name depth"; @@ -433,6 +442,8 @@ impl TaskClient { .await } + // legacy code + #[allow(clippy::panic)] pub async fn recv_timeout(&mut self) { if self.mode.is_dummy() { return pending().await; @@ -505,6 +516,7 @@ impl TaskClient { } } +#[allow(deprecated)] impl Drop for TaskClient { fn drop(&mut self) { if !self.mode.should_signal_on_drop() { @@ -572,6 +584,8 @@ impl ClientOperatingMode { } } +#[deprecated] +#[allow(deprecated)] #[derive(Debug)] pub enum TaskHandle { /// Full [`TaskManager`] that was created by the underlying task. @@ -581,24 +595,28 @@ pub enum TaskHandle { External(TaskClient), } +#[allow(deprecated)] impl From for TaskHandle { fn from(value: TaskManager) -> Self { TaskHandle::Internal(value) } } +#[allow(deprecated)] impl From for TaskHandle { fn from(value: TaskClient) -> Self { TaskHandle::External(value) } } +#[allow(deprecated)] impl Default for TaskHandle { fn default() -> Self { TaskHandle::Internal(TaskManager::default()) } } +#[allow(deprecated)] impl TaskHandle { #[must_use] pub fn name_if_unnamed>(self, name: S) -> Self { @@ -666,6 +684,7 @@ mod tests { use super::*; #[tokio::test] + #[allow(deprecated)] async fn signal_shutdown() { let shutdown = TaskManager::default(); let mut listener = shutdown.subscribe(); diff --git a/common/task/src/signal.rs b/common/task/src/signal.rs index ebaab7b20f..a8d7f6865d 100644 --- a/common/task/src/signal.rs +++ b/common/task/src/signal.rs @@ -1,6 +1,7 @@ -use crate::{manager::SentError, TaskManager}; +use crate::manager::SentError; #[cfg(unix)] +#[allow(clippy::expect_used)] pub async fn wait_for_signal() { use tokio::signal::unix::{signal, SignalKind}; let mut sigterm = signal(SignalKind::terminate()).expect("Failed to setup SIGTERM channel"); @@ -28,8 +29,10 @@ pub async fn wait_for_signal() { } } +#[allow(deprecated)] #[cfg(unix)] -pub async fn wait_for_signal_and_error(shutdown: &mut TaskManager) -> Result<(), SentError> { +#[allow(clippy::expect_used)] +pub async fn wait_for_signal_and_error(shutdown: &mut crate::TaskManager) -> Result<(), SentError> { use tokio::signal::unix::{signal, SignalKind}; let mut sigterm = signal(SignalKind::terminate()).expect("Failed to setup SIGTERM channel"); @@ -55,8 +58,9 @@ pub async fn wait_for_signal_and_error(shutdown: &mut TaskManager) -> Result<(), } } +#[allow(deprecated)] #[cfg(not(unix))] -pub async fn wait_for_signal_and_error(shutdown: &mut TaskManager) -> Result<(), SentError> { +pub async fn wait_for_signal_and_error(shutdown: &mut crate::TaskManager) -> Result<(), SentError> { tokio::select! { _ = tokio::signal::ctrl_c() => { log::info!("Received SIGINT"); diff --git a/common/task/src/spawn.rs b/common/task/src/spawn.rs index e0fe98c5de..8929bf2a2b 100644 --- a/common/task/src/spawn.rs +++ b/common/task/src/spawn.rs @@ -1,35 +1,79 @@ -use crate::TaskClient; use std::future::Future; +#[cfg(not(target_arch = "wasm32"))] +pub type JoinHandle = tokio::task::JoinHandle; + +// no JoinHandle equivalent in wasm + #[cfg(target_arch = "wasm32")] -pub fn spawn(future: F) +#[derive(Clone, Copy)] +pub struct FakeJoinHandle { + _p: std::marker::PhantomData, +} +#[cfg(target_arch = "wasm32")] +pub type JoinHandle = FakeJoinHandle; + +#[cfg(target_arch = "wasm32")] +#[track_caller] +pub fn spawn_future(future: F) -> JoinHandle where - F: Future + 'static, + F: Future + 'static, { - wasm_bindgen_futures::spawn_local(future); + wasm_bindgen_futures::spawn_local(async move { + // make sure the future outputs `()` + future.await; + }); + FakeJoinHandle { + _p: std::marker::PhantomData, + } } +// Note: prefer spawning tasks directly on the ShutdownManager #[cfg(not(target_arch = "wasm32"))] #[track_caller] -pub fn spawn(future: F) +pub fn spawn_future(future: F) -> JoinHandle where F: Future + Send + 'static, F::Output: Send + 'static, { - tokio::spawn(future); + tokio::spawn(future) } +// Note: prefer spawning tasks directly on the ShutdownManager +#[cfg(not(target_arch = "wasm32"))] #[track_caller] -pub fn spawn_with_report_error(future: F, mut shutdown: TaskClient) +pub fn spawn_named_future(future: F, name: &str) -> JoinHandle where - F: Future> + Send + 'static, - T: 'static, - E: std::error::Error + Send + Sync + 'static, + F: Future + Send + 'static, + F::Output: Send + 'static, { - let future_that_sends = async move { - if let Err(err) = future.await { - shutdown.send_we_stopped(Box::new(err)); - } - }; - spawn(future_that_sends); + cfg_if::cfg_if! {if #[cfg(all(tokio_unstable, feature="tokio-tracing"))] { + #[allow(clippy::expect_used)] + tokio::task::Builder::new().name(name).spawn(future).expect("failed to spawn future") + } else { + let _ = name; + tracing::debug!(r#"the underlying binary hasn't been built with `RUSTFLAGS="--cfg tokio_unstable"` - the future naming won't do anything"#); + spawn_future(future) + }} +} + +#[cfg(target_arch = "wasm32")] +#[track_caller] +pub fn spawn_named_future(future: F, name: &str) -> JoinHandle +where + F: Future + 'static, +{ + // not supported in wasm + let _ = name; + spawn_future(future) +} + +#[macro_export] +macro_rules! spawn_future { + ($future:expr) => {{ + $crate::spawn_future($future) + }}; + ($future:expr, $name:expr) => {{ + $crate::spawn_named_future($future, $name) + }}; } diff --git a/common/test-utils/src/traits.rs b/common/test-utils/src/traits.rs index 9b95dbd6bf..e989be6089 100644 --- a/common/test-utils/src/traits.rs +++ b/common/test-utils/src/traits.rs @@ -32,6 +32,16 @@ pub trait Timeboxed: IntoFuture + Sized { impl Timeboxed for T where T: IntoFuture + Sized {} +pub trait ElapsedExt { + fn has_elapsed(&self) -> bool; +} + +impl ElapsedExt for Result { + fn has_elapsed(&self) -> bool { + self.is_err() + } +} + // those are internal testing traits so we're not concerned about auto traits #[allow(async_fn_in_trait)] pub trait Spawnable: Future + Sized + Send + 'static { diff --git a/common/types/src/error.rs b/common/types/src/error.rs index 823ec8b375..b72a4a18d6 100644 --- a/common/types/src/error.rs +++ b/common/types/src/error.rs @@ -33,10 +33,7 @@ pub enum TypesError { source: eyre::Report, }, #[error("{source}")] - NymApiError { - #[from] - source: NymAPIError, - }, + NymApiError { source: Box }, #[error("{source}")] IOError { #[from] @@ -97,10 +94,18 @@ impl Serialize for TypesError { } } +impl From for TypesError { + fn from(value: NymAPIError) -> Self { + TypesError::NymApiError { + source: Box::new(value), + } + } +} + impl From for TypesError { fn from(e: ValidatorClientError) -> Self { match e { - ValidatorClientError::NymAPIError { source } => source.into(), + ValidatorClientError::NymAPIError { source } => (*source).into(), ValidatorClientError::MalformedUrlProvided(e) => e.into(), ValidatorClientError::NyxdError(e) => e.into(), ValidatorClientError::NoAPIUrlAvailable => TypesError::NoNymApiUrlConfigured, diff --git a/common/upgrade-mode-check/Cargo.toml b/common/upgrade-mode-check/Cargo.toml new file mode 100644 index 0000000000..50e0cb3f7c --- /dev/null +++ b/common/upgrade-mode-check/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "nym-upgrade-mode-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] +jwt-simple = { workspace = true } +reqwest = { workspace = true, features = ["rustls-tls"] } +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } +time = { workspace = true, features = ["serde"] } +thiserror = { workspace = true } +tracing = { workspace = true } + +nym-http-api-client = { path = "../http-api-client", default-features = false } +nym-crypto = { path = "../crypto", features = ["asymmetric", "serde", "naive_jwt"] } + +[dev-dependencies] +anyhow = { workspace = true } +time = { workspace = true, features = ["macros"] } + +[lints] +workspace = true diff --git a/common/upgrade-mode-check/src/attestation.rs b/common/upgrade-mode-check/src/attestation.rs new file mode 100644 index 0000000000..42c115f3cd --- /dev/null +++ b/common/upgrade-mode-check/src/attestation.rs @@ -0,0 +1,123 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::UpgradeModeCheckError; +use nym_crypto::asymmetric::ed25519; +use nym_http_api_client::generate_user_agent; +use serde::{Deserialize, Serialize}; +use std::time::Duration; +use time::OffsetDateTime; + +#[derive(Serialize, Deserialize, Debug, PartialEq, Clone, Copy)] +pub struct UpgradeModeAttestation { + #[serde(flatten)] + pub content: UpgradeModeAttestationContent, + + #[serde(with = "ed25519::bs58_ed25519_signature")] + pub signature: ed25519::Signature, +} + +#[derive(Serialize, Deserialize, Debug, PartialEq, Clone, Copy)] +#[serde(tag = "type")] +#[serde(rename = "upgrade_mode")] +pub struct UpgradeModeAttestationContent { + #[serde(with = "time::serde::timestamp")] + pub starting_time: OffsetDateTime, + + #[serde(with = "ed25519::bs58_ed25519_pubkey")] + pub attester_public_key: ed25519::PublicKey, +} + +impl UpgradeModeAttestation { + pub fn verify(&self) -> bool { + self.content + .attester_public_key + .verify(self.content.as_json(), &self.signature) + .is_ok() + } +} + +impl UpgradeModeAttestationContent { + pub fn as_json(&self) -> String { + // SAFETY: Serialize impl is valid and we have no non-string map keys + #[allow(clippy::unwrap_used)] + serde_json::to_string(&self).unwrap() + } +} + +pub fn generate_new_attestation(key: &ed25519::PrivateKey) -> UpgradeModeAttestation { + generate_new_attestation_with_starting_time(key, OffsetDateTime::now_utc()) +} + +pub fn generate_new_attestation_with_starting_time( + key: &ed25519::PrivateKey, + starting_time: OffsetDateTime, +) -> UpgradeModeAttestation { + let content = UpgradeModeAttestationContent { + starting_time, + attester_public_key: key.into(), + }; + UpgradeModeAttestation { + signature: key.sign(content.as_json()), + content, + } +} + +pub async fn attempt_retrieve( + url: &str, +) -> Result, UpgradeModeCheckError> { + let retrieval_failure = |source| UpgradeModeCheckError::AttestationRetrievalFailure { + url: url.to_string(), + source, + }; + + let attestation = reqwest::ClientBuilder::new() + .user_agent(generate_user_agent!()) + .timeout(Duration::from_secs(5)) + .build() + .map_err(retrieval_failure)? + .get(url) + .send() + .await + .map_err(retrieval_failure)? + .json::>() + .await + .map_err(retrieval_failure)?; + + Ok(attestation) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn upgrade_mode_attestation_serde_json() -> anyhow::Result<()> { + // unix timestamp: 1629720000 + let starting_time = time::macros::datetime!(2021-08-23 12:00 UTC); + + let key = ed25519::PrivateKey::from_bytes(&[ + 108, 49, 193, 21, 126, 161, 249, 85, 242, 207, 74, 195, 238, 6, 64, 149, 201, 140, 248, + 163, 122, 170, 79, 198, 87, 85, 36, 29, 243, 92, 64, 161, + ])?; + + let attestation = generate_new_attestation_with_starting_time(&key, starting_time); + + let attestation_json = serde_json::to_string(&attestation)?; + let attestation_content_json = attestation.content.as_json(); + + let expected_attestation = r#"{"type":"upgrade_mode","starting_time":1629720000,"attester_public_key":"3pkFcBXCEmbmXBT2G8CkFMuKisJcH54mbBGvncHaDibt","signature":"5rWUr2ypaDTtrMKegMP3tQkkZGFAuhNTnEVCVe5Azv6QqvLzoGdQiMkFmeyhDd1XSfoXpL9fFM58rsdA1kf4GYMM"}"#; + let expected_content = r#"{"type":"upgrade_mode","starting_time":1629720000,"attester_public_key":"3pkFcBXCEmbmXBT2G8CkFMuKisJcH54mbBGvncHaDibt"}"#; + + assert_eq!(attestation_content_json, expected_content); + assert_eq!(attestation_json, expected_attestation); + + let recovered_attestation = serde_json::from_str(&attestation_json)?; + assert_eq!(attestation, recovered_attestation); + + let recovered_content = serde_json::from_str(&attestation_content_json)?; + assert_eq!(attestation.content, recovered_content); + + Ok(()) + } +} diff --git a/common/upgrade-mode-check/src/error.rs b/common/upgrade-mode-check/src/error.rs new file mode 100644 index 0000000000..d1f26b7c5f --- /dev/null +++ b/common/upgrade-mode-check/src/error.rs @@ -0,0 +1,23 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use nym_crypto::asymmetric::ed25519::Ed25519RecoveryError; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum UpgradeModeCheckError { + #[error("failed to decode jwt metadata")] + TokenMetadataDecodeFailure { source: jwt_simple::Error }, + + #[error("the jwt metadata didn't contain explicit public key")] + MissingTokenPublicKey, + + #[error("the attached public key was not valid ed25519 public key")] + MalformedEd25519PublicKey { source: Ed25519RecoveryError }, + + #[error("failed to verify the jwt: {source}")] + JwtVerificationFailure { source: jwt_simple::Error }, + + #[error("failed to retrieve attestation from {url}:{source}")] + AttestationRetrievalFailure { url: String, source: reqwest::Error }, +} diff --git a/common/upgrade-mode-check/src/jwt.rs b/common/upgrade-mode-check/src/jwt.rs new file mode 100644 index 0000000000..93be406ab2 --- /dev/null +++ b/common/upgrade-mode-check/src/jwt.rs @@ -0,0 +1,119 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::{UpgradeModeAttestation, UpgradeModeCheckError}; +use jwt_simple::claims::Claims; +use jwt_simple::common::{KeyMetadata, VerificationOptions}; +use jwt_simple::prelude::{EdDSAKeyPairLike, EdDSAPublicKeyLike}; +use jwt_simple::token::Token; +use nym_crypto::asymmetric::ed25519; +use std::collections::HashSet; +use std::time::Duration; + +// for now use static issuer such as "nym-credential-proxy" +pub fn generate_jwt_for_upgrade_mode_attestation( + attestation: UpgradeModeAttestation, + validity: Duration, + keys: &ed25519::KeyPair, + issuer: Option<&'static str>, +) -> String { + let claim = Claims::with_custom_claims(attestation, validity.into()); + let mut claim = if let Some(issuer) = issuer { + claim.with_issuer(issuer) + } else { + claim + }; + claim.create_nonce(); + + let md = KeyMetadata::default().with_public_key(keys.public_key().to_base58_string()); + + let mut jwt_keys = keys.to_jwt_compatible_keys(); + // SAFETY: trait impl for EdDSA is infallible + #[allow(clippy::unwrap_used)] + jwt_keys.attach_metadata(md).unwrap(); + + // SAFETY: our construction of the jwt is valid + #[allow(clippy::unwrap_used)] + jwt_keys.sign(claim).unwrap() +} + +pub fn validate_upgrade_mode_jwt( + token: &str, + expected_issuer: Option<&'static str>, +) -> Result { + // for now, we completely ignore the validity of the pubkey (I know, I know). + // that will be changed later on + // so as a bypass we have to extract the claimed issuer from the jwt to verify against it + let metadata = Token::decode_metadata(token) + .map_err(|source| UpgradeModeCheckError::TokenMetadataDecodeFailure { source })?; + + let pub_key = metadata + .public_key() + .ok_or(UpgradeModeCheckError::MissingTokenPublicKey)?; + + let ed25519_pub_key = ed25519::PublicKey::from_base58_string(pub_key) + .map_err(|source| UpgradeModeCheckError::MalformedEd25519PublicKey { source })?; + + let mut opts = VerificationOptions::default(); + if let Some(issuer) = expected_issuer { + opts.allowed_issuers = Some(HashSet::from_iter(vec![issuer.to_string()])); + } + + let attestation = ed25519_pub_key + .to_jwt_compatible_key() + .verify_token::(token, Some(opts)) + .map_err(|source| UpgradeModeCheckError::JwtVerificationFailure { source })? + .custom; + + Ok(attestation) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::generate_new_attestation; + use nym_crypto::asymmetric::ed25519; + + #[test] + fn generate_and_validate_jwt() { + let attestation_key = ed25519::PrivateKey::from_bytes(&[ + 108, 49, 193, 21, 126, 161, 249, 85, 242, 207, 74, 195, 238, 6, 64, 149, 201, 140, 248, + 163, 122, 170, 79, 198, 87, 85, 36, 29, 243, 92, 64, 161, + ]) + .unwrap(); + let jwt_key = ed25519::PrivateKey::from_bytes(&[ + 152, 17, 144, 255, 213, 219, 246, 208, 109, 33, 100, 73, 1, 141, 32, 63, 141, 89, 167, + 2, 52, 215, 241, 219, 200, 18, 159, 241, 76, 111, 42, 32, + ]) + .unwrap(); + let keys = ed25519::KeyPair::from(jwt_key); + + let attestation = generate_new_attestation(&attestation_key); + let jwt_issuer = generate_jwt_for_upgrade_mode_attestation( + attestation, + Duration::from_secs(60 * 60), + &keys, + Some("nym-credential-proxy"), + ); + // we expect 'nym-credential-proxy' issuer + assert!(validate_upgrade_mode_jwt(&jwt_issuer, Some("nym-credential-proxy")).is_ok()); + + // we don't care about issuer + assert!(validate_upgrade_mode_jwt(&jwt_issuer, None).is_ok()); + + // we expect another-issuer + assert!(validate_upgrade_mode_jwt(&jwt_issuer, Some("another-issuer")).is_err()); + + let jwt_no_issuer = generate_jwt_for_upgrade_mode_attestation( + attestation, + Duration::from_secs(60 * 60), + &keys, + None, + ); + // we expect 'nym-credential-proxy' issuer + assert!(validate_upgrade_mode_jwt(&jwt_no_issuer, Some("nym-credential-proxy")).is_err()); + + // we don't care about issuer + assert!(validate_upgrade_mode_jwt(&jwt_no_issuer, None).is_ok()); + } +} diff --git a/common/upgrade-mode-check/src/lib.rs b/common/upgrade-mode-check/src/lib.rs new file mode 100644 index 0000000000..21bc5402ec --- /dev/null +++ b/common/upgrade-mode-check/src/lib.rs @@ -0,0 +1,13 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub(crate) mod attestation; +pub(crate) mod error; +pub(crate) mod jwt; + +pub use attestation::{ + attempt_retrieve, generate_new_attestation, generate_new_attestation_with_starting_time, + UpgradeModeAttestation, +}; +pub use error::UpgradeModeCheckError; +pub use jwt::{generate_jwt_for_upgrade_mode_attestation, validate_upgrade_mode_jwt}; diff --git a/common/verloc/Cargo.toml b/common/verloc/Cargo.toml index e906f72dde..32b81ed1d6 100644 --- a/common/verloc/Cargo.toml +++ b/common/verloc/Cargo.toml @@ -25,3 +25,5 @@ url = { workspace = true } nym-crypto = { path = "../crypto", features = ["asymmetric"] } nym-task = { path = "../task" } nym-validator-client = { path = "../client-libs/validator-client" } +nym-http-api-client = { path = "../http-api-client" } +nym-api-requests = { path = "../../nym-api/nym-api-requests" } diff --git a/common/verloc/src/measurements/listener.rs b/common/verloc/src/measurements/listener.rs index cc74cf8984..8c817ce229 100644 --- a/common/verloc/src/measurements/listener.rs +++ b/common/verloc/src/measurements/listener.rs @@ -51,24 +51,26 @@ impl PacketListener { info!("Started listening for echo packets on {}", self.address); - while !self.shutdown_token.is_cancelled() { + loop { // cloning the arc as each accepted socket is handled in separate task let connection_handler = Arc::clone(&self.connection_handler); tokio::select! { + biased; + _ = self.shutdown_token.cancelled() => { + trace!("PacketListener: Received shutdown"); + break; + } socket = listener.accept() => { match socket { Ok((socket, remote_addr)) => { debug!("New verloc connection from {remote_addr}"); - let cancel = self.shutdown_token.child_token(format!("handler_{remote_addr}")); - tokio::spawn(async move { cancel.run_until_cancelled(connection_handler.handle_connection(socket, remote_addr)).await }); + let cancel = self.shutdown_token.child_token(); + tokio::spawn(cancel.run_until_cancelled_owned(connection_handler.handle_connection(socket, remote_addr))); } Err(err) => warn!("Failed to accept incoming connection - {err}"), } }, - _ = self.shutdown_token.cancelled() => { - trace!("PacketListener: Received shutdown"); - } } } } diff --git a/common/verloc/src/measurements/measurer.rs b/common/verloc/src/measurements/measurer.rs index 5831551363..8451207300 100644 --- a/common/verloc/src/measurements/measurer.rs +++ b/common/verloc/src/measurements/measurer.rs @@ -10,7 +10,7 @@ use futures::StreamExt; use nym_crypto::asymmetric::ed25519; use nym_task::ShutdownToken; use nym_validator_client::models::NymNodeDescription; -use nym_validator_client::NymApiClient; +use nym_validator_client::nym_api::NymApiClientExt; use rand::prelude::SliceRandom; use rand::thread_rng; use std::net::SocketAddr; @@ -40,12 +40,12 @@ impl VerlocMeasurer { config.packet_timeout, config.connection_timeout, config.delay_between_packets, - shutdown_token.clone_with_suffix("packet_sender"), + shutdown_token.clone(), )), packet_listener: Arc::new(PacketListener::new( config.listening_address, Arc::clone(&identity), - shutdown_token.clone_with_suffix("packet_listener"), + shutdown_token.clone(), )), shutdown_token, config, @@ -92,8 +92,13 @@ impl VerlocMeasurer { .collect::>(); // exhaust the results - while !self.shutdown_token.is_cancelled() { + loop { tokio::select! { + biased; + _ = self.shutdown_token.cancelled() => { + trace!("Shutdown received while measuring"); + return MeasurementOutcome::Shutdown; + } 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 @@ -117,10 +122,6 @@ impl VerlocMeasurer { }; chunk_results.push(VerlocNodeResult::new(identity, measurement_result)); }, - _ = self.shutdown_token.cancelled() => { - trace!("Shutdown received while measuring"); - return MeasurementOutcome::Shutdown; - } } } @@ -135,10 +136,15 @@ impl VerlocMeasurer { 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(), - ); + let client = match nym_http_api_client::Client::builder(api_endpoint.clone()) + .and_then(|b| b.with_user_agent(self.config.user_agent.clone()).build()) + { + Ok(c) => c, + Err(err) => { + warn!("failed to create client for {api_endpoint}: {err}"); + continue; + } + }; match client.get_all_described_nodes().await { Ok(res) => return Some(res), Err(err) => { @@ -208,6 +214,7 @@ impl VerlocMeasurer { _ = sleep(self.config.testing_interval) => {}, _ = self.shutdown_token.cancelled() => { trace!("Shutdown received while sleeping"); + break; } } } diff --git a/common/verloc/src/models.rs b/common/verloc/src/models.rs index 0bedab812c..970fc507b5 100644 --- a/common/verloc/src/models.rs +++ b/common/verloc/src/models.rs @@ -130,12 +130,7 @@ impl VerlocMeasurement { let variance_micros = data .iter() .map(|&value| { - // make sure we don't underflow - let diff = if mean > value { - mean - value - } else { - value - mean - }; + let diff = mean.abs_diff(value); // we don't need nanos precision let diff_micros = diff.as_micros(); diff_micros * diff_micros diff --git a/common/wasm/client-core/Cargo.toml b/common/wasm/client-core/Cargo.toml index d63e31e50a..85668972f9 100644 --- a/common/wasm/client-core/Cargo.toml +++ b/common/wasm/client-core/Cargo.toml @@ -34,6 +34,7 @@ nym-statistics-common = { path = "../../statistics" } nym-task = { path = "../../task" } nym-topology = { path = "../../topology", features = ["wasm-serde-types"] } nym-validator-client = { path = "../../client-libs/validator-client", default-features = false } +nym-http-api-client = { path = "../../http-api-client" } wasm-utils = { path = "../utils" } wasm-storage = { path = "../storage" } diff --git a/common/wasm/client-core/src/error.rs b/common/wasm/client-core/src/error.rs index 8066a9f2bb..ab41b190dd 100644 --- a/common/wasm/client-core/src/error.rs +++ b/common/wasm/client-core/src/error.rs @@ -37,6 +37,12 @@ pub enum WasmCoreError { source: ValidatorClientError, }, + #[error("failed to query nym api: {source}")] + NymApiQueryError { + #[from] + source: nym_validator_client::nym_api::error::NymAPIError, + }, + #[error("The provided wasm topology was invalid: {source}")] WasmTopologyError { #[from] diff --git a/common/wasm/client-core/src/helpers.rs b/common/wasm/client-core/src/helpers.rs index 4cb5f06c55..23aaee4cad 100644 --- a/common/wasm/client-core/src/helpers.rs +++ b/common/wasm/client-core/src/helpers.rs @@ -19,9 +19,9 @@ use nym_client_core::init::{ use nym_sphinx::addressing::clients::Recipient; use nym_sphinx::anonymous_replies::requests::AnonymousSenderTag; use nym_topology::wasm_helpers::WasmFriendlyNymTopology; -use nym_topology::{NymTopology, RoutingNode}; +use nym_topology::{EpochRewardedSet, NymTopology, RoutingNode}; use nym_validator_client::client::IdentityKey; -use nym_validator_client::{NymApiClient, UserAgent}; +use nym_validator_client::{nym_api::NymApiClientExt, UserAgent}; use rand::thread_rng; use url::Url; use wasm_bindgen::prelude::wasm_bindgen; @@ -72,7 +72,16 @@ pub async fn current_network_topology_async( } }; - let api_client = NymApiClient::new(url); + let api_client = nym_http_api_client::Client::builder(url.clone()) + .map_err(|_err| WasmCoreError::MalformedUrl { + raw: nym_api_url.to_string(), + source: url::ParseError::EmptyHost, + })? + .build() + .map_err(|_err| WasmCoreError::MalformedUrl { + raw: nym_api_url.to_string(), + source: url::ParseError::EmptyHost, + })?; let rewarded_set = api_client.get_current_rewarded_set().await?; let mixnodes_res = api_client .get_all_basic_active_mixing_assigned_nodes_with_metadata() @@ -90,9 +99,14 @@ pub async fn current_network_topology_async( let gateways = gateways_res.nodes; - let topology = NymTopology::new(metadata.to_topology_metadata(), rewarded_set, Vec::new()) - .with_skimmed_nodes(&mixnodes) - .with_skimmed_nodes(&gateways); + let epoch_rewarded_set: EpochRewardedSet = rewarded_set.into(); + let topology = NymTopology::new( + metadata.to_topology_metadata(), + epoch_rewarded_set, + Vec::new(), + ) + .with_skimmed_nodes(&mixnodes) + .with_skimmed_nodes(&gateways); Ok(topology.into()) } diff --git a/common/wasm/client-core/src/lib.rs b/common/wasm/client-core/src/lib.rs index 8569dd7ad9..761df16ae4 100644 --- a/common/wasm/client-core/src/lib.rs +++ b/common/wasm/client-core/src/lib.rs @@ -21,6 +21,7 @@ pub use nym_client_core::{ pub use nym_gateway_client::{ error::GatewayClientError, GatewayClient, GatewayClientConfig, GatewayConfig, }; +pub use nym_http_api_client::Client as ApiClient; pub use nym_sphinx::{ addressing::{clients::Recipient, nodes::NodeIdentity}, params::PacketType, @@ -29,7 +30,6 @@ pub use nym_sphinx::{ pub use nym_statistics_common::clients::ClientStatsSender; pub use nym_task; pub use nym_topology::{HardcodedTopologyProvider, MixLayer, NymTopology, TopologyProvider}; -pub use nym_validator_client::nym_api::Client as ApiClient; pub use nym_validator_client::{DirectSigningReqwestRpcNyxdClient, QueryReqwestRpcNyxdClient}; // TODO: that's a very nasty import path. it should come from contracts instead! pub use nym_validator_client::client::IdentityKey; diff --git a/common/wireguard-private-metadata/client/Cargo.toml b/common/wireguard-private-metadata/client/Cargo.toml new file mode 100644 index 0000000000..d7d23ce445 --- /dev/null +++ b/common/wireguard-private-metadata/client/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "nym-wireguard-private-metadata-client" +version = "1.0.0" +authors.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +async-trait = { workspace = true } +tracing = { workspace = true } +nym-http-api-client = { path = "../../http-api-client" } +nym-wireguard-private-metadata-shared = { path = "../shared" } + +[lints] +workspace = true diff --git a/common/wireguard-private-metadata/client/src/lib.rs b/common/wireguard-private-metadata/client/src/lib.rs new file mode 100644 index 0000000000..fd02fb3847 --- /dev/null +++ b/common/wireguard-private-metadata/client/src/lib.rs @@ -0,0 +1,53 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use async_trait::async_trait; +use tracing::instrument; + +use nym_http_api_client::{ApiClient, Client, HttpClientError, NO_PARAMS}; + +use nym_wireguard_private_metadata_shared::{ + routes, Version, {Request, Response}, +}; + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +pub trait WireguardMetadataApiClient: ApiClient { + #[instrument(level = "debug", skip(self))] + async fn version(&self) -> Result { + let version: u64 = self + .get_json( + &[routes::V1_API_VERSION, routes::BANDWIDTH, routes::VERSION], + NO_PARAMS, + ) + .await?; + Ok(version.into()) + } + + #[instrument(level = "debug", skip(self))] + async fn available_bandwidth( + &self, + request_body: &Request, + ) -> Result { + self.post_json( + &[routes::V1_API_VERSION, routes::BANDWIDTH, routes::AVAILABLE], + NO_PARAMS, + request_body, + ) + .await + } + + #[instrument(level = "debug", skip(self, request_body))] + async fn topup_bandwidth(&self, request_body: &Request) -> Result { + self.post_json( + &[routes::V1_API_VERSION, routes::BANDWIDTH, routes::TOPUP], + NO_PARAMS, + request_body, + ) + .await + } +} + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +impl WireguardMetadataApiClient for Client {} diff --git a/common/wireguard-private-metadata/server/Cargo.toml b/common/wireguard-private-metadata/server/Cargo.toml new file mode 100644 index 0000000000..fa850181f1 --- /dev/null +++ b/common/wireguard-private-metadata/server/Cargo.toml @@ -0,0 +1,43 @@ +[package] +name = "nym-wireguard-private-metadata-server" +version = "1.0.0" +authors.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +anyhow = { workspace = true } +axum = { workspace = true, features = ["tokio", "macros"] } +futures = { workspace = true } +tokio = { workspace = true, features = ["rt-multi-thread", "net", "io-util"] } +tokio-util = { workspace = true } +tower-http = { workspace = true, features = [ + "cors", + "trace", + "compression-br", + "compression-deflate", + "compression-gzip", + "compression-zstd", +] } +utoipa = { workspace = true, features = ["axum_extras", "time"] } +utoipa-swagger-ui = { workspace = true, features = ["axum"] } + +nym-credentials-interface = { path = "../../credentials-interface" } +nym-credential-verification = { path = "../../credential-verification" } +nym-http-api-common = { path = "../../http-api-common", features = [ + "middleware", + "utoipa", + "output", +] } +nym-wireguard = { path = "../../wireguard" } +nym-wireguard-private-metadata-shared = { path = "../shared" } + +[dev-dependencies] +async-trait = { workspace = true } + + +[lints] +workspace = true diff --git a/common/wireguard-private-metadata/server/src/http/mod.rs b/common/wireguard-private-metadata/server/src/http/mod.rs new file mode 100644 index 0000000000..4093dece8b --- /dev/null +++ b/common/wireguard-private-metadata/server/src/http/mod.rs @@ -0,0 +1,37 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use nym_wireguard::WgApiWrapper; +use std::sync::Arc; +use tokio::task::JoinHandle; + +pub(crate) mod openapi; +pub(crate) mod router; +pub(crate) mod state; + +/// Shutdown goes 2 directions: +/// 1. signal background tasks to gracefully finish +/// 2. signal server itself +/// +/// These are done through separate shutdown handles. Of course, shut down server +/// AFTER you have shut down BG tasks (or past their grace period). +#[allow(unused)] +pub struct ShutdownHandles { + /// Tokio JoinHandle for axum server's task + axum_join_handle: AxumJoinHandle, + /// Wireguard API for kernel interactions + wg_api: Arc, +} + +impl ShutdownHandles { + /// Cancellation token is given to Axum server constructor. When the token + /// receives a shutdown signal, Axum server will shut down gracefully. + pub fn new(axum_join_handle: AxumJoinHandle, wg_api: Arc) -> Self { + Self { + axum_join_handle, + wg_api, + } + } +} + +type AxumJoinHandle = JoinHandle>; diff --git a/common/wireguard-private-metadata/server/src/http/openapi.rs b/common/wireguard-private-metadata/server/src/http/openapi.rs new file mode 100644 index 0000000000..5e91266582 --- /dev/null +++ b/common/wireguard-private-metadata/server/src/http/openapi.rs @@ -0,0 +1,14 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use utoipa::OpenApi; + +use nym_wireguard_private_metadata_shared::{Request, Response}; + +#[derive(OpenApi)] +#[openapi( + info(title = "Nym Wireguard Private Metadata"), + tags(), + components(schemas(Request, Response)) +)] +pub(crate) struct ApiDoc; diff --git a/common/wireguard-private-metadata/server/src/http/router.rs b/common/wireguard-private-metadata/server/src/http/router.rs new file mode 100644 index 0000000000..ac4250e431 --- /dev/null +++ b/common/wireguard-private-metadata/server/src/http/router.rs @@ -0,0 +1,104 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use anyhow::anyhow; +use axum::response::Redirect; +use axum::routing::get; +use axum::Router; +use core::net::SocketAddr; +use nym_http_api_common::middleware::logging::log_request_info; +use std::future::Future; +use tokio::net::TcpListener; +use tower_http::cors::CorsLayer; +use utoipa::OpenApi; +use utoipa_swagger_ui::SwaggerUi; + +use crate::http::openapi::ApiDoc; +use crate::http::state::AppState; +use crate::network::bandwidth_routes; + +/// Wrapper around `axum::Router` which ensures correct [order of layers][order]. +/// Add new routes as if you were working directly with `axum`. +/// +/// Why? Middleware like logger, CORS, TLS which need to handle request before other +/// layers should be added last. Using this builder pattern ensures that. +/// +/// [order]: https://docs.rs/axum/latest/axum/middleware/index.html#ordering +pub struct RouterBuilder { + unfinished_router: Router, +} + +impl RouterBuilder { + /// All routes should be, if possible, added here. Exceptions are e.g. + /// routes which are added conditionally in other places based on some `if`. + pub fn with_default_routes() -> Self { + let default_routes = Router::new() + .merge(SwaggerUi::new("/swagger").url("/api-docs/openapi.json", ApiDoc::openapi())) + .route("/", get(|| async { Redirect::to("/swagger") })) + .nest("/v1", Router::new().nest("/bandwidth", bandwidth_routes())); + Self { + unfinished_router: default_routes, + } + } + + /// Invoke this as late as possible before constructing HTTP server + /// (after all routes were added). + pub fn with_state(self, state: AppState) -> RouterWithState { + RouterWithState { + router: self.finalize_routes().with_state(state), + } + } + + /// Middleware added here intercepts the request before it gets to other routes. + fn finalize_routes(self) -> Router { + self.unfinished_router + .layer(setup_cors()) + .layer(axum::middleware::from_fn(log_request_info)) + } +} + +fn setup_cors() -> CorsLayer { + CorsLayer::new() + .allow_origin(tower_http::cors::Any) + .allow_methods([axum::http::Method::GET, axum::http::Method::POST]) + .allow_headers(tower_http::cors::Any) + .allow_credentials(false) +} + +pub struct RouterWithState { + pub router: Router, +} + +impl RouterWithState { + pub async fn build_server(self, bind_address: &SocketAddr) -> anyhow::Result { + let listener = tokio::net::TcpListener::bind(bind_address) + .await + .map_err(|err| anyhow!("Couldn't bind to address {} due to {}", bind_address, err))?; + + Ok(ApiHttpServer { + router: self.router, + listener, + }) + } +} + +pub struct ApiHttpServer { + router: Router, + listener: TcpListener, +} + +impl ApiHttpServer { + pub async fn run(self, signal: F) -> Result<(), std::io::Error> + where + F: Future + Send + 'static, + { + // into_make_service_with_connect_info allows us to see client ip address + axum::serve( + self.listener, + self.router + .into_make_service_with_connect_info::(), + ) + .with_graceful_shutdown(signal) + .await + } +} diff --git a/common/wireguard-private-metadata/server/src/http/state.rs b/common/wireguard-private-metadata/server/src/http/state.rs new file mode 100644 index 0000000000..06916bb35f --- /dev/null +++ b/common/wireguard-private-metadata/server/src/http/state.rs @@ -0,0 +1,35 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use std::net::IpAddr; + +use nym_credentials_interface::CredentialSpendingData; + +use crate::transceiver::PeerControllerTransceiver; +use nym_wireguard_private_metadata_shared::error::MetadataError; + +#[derive(Clone, axum::extract::FromRef)] +pub struct AppState { + transceiver: PeerControllerTransceiver, +} + +impl AppState { + pub fn new(transceiver: PeerControllerTransceiver) -> Self { + Self { transceiver } + } + + pub async fn available_bandwidth(&self, ip: IpAddr) -> Result { + self.transceiver.query_bandwidth(ip).await + } + + // Top up with a credential and return the afterwards available bandwidth + pub async fn topup_bandwidth( + &self, + ip: IpAddr, + credential: CredentialSpendingData, + ) -> Result { + self.transceiver + .topup_bandwidth(ip, Box::new(credential)) + .await + } +} diff --git a/common/wireguard-private-metadata/server/src/lib.rs b/common/wireguard-private-metadata/server/src/lib.rs new file mode 100644 index 0000000000..3d772ac1e0 --- /dev/null +++ b/common/wireguard-private-metadata/server/src/lib.rs @@ -0,0 +1,13 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +mod http; +mod network; +mod transceiver; + +pub use http::{ + router::{ApiHttpServer, RouterBuilder, RouterWithState}, + state::AppState, + ShutdownHandles, +}; +pub use transceiver::PeerControllerTransceiver; diff --git a/common/wireguard-private-metadata/server/src/network.rs b/common/wireguard-private-metadata/server/src/network.rs new file mode 100644 index 0000000000..36b22ba32b --- /dev/null +++ b/common/wireguard-private-metadata/server/src/network.rs @@ -0,0 +1,111 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use std::net::SocketAddr; + +use axum::{ + extract::{ConnectInfo, Query, State}, + Json, Router, +}; +use nym_http_api_common::{FormattedResponse, OutputParams}; +use nym_wireguard_private_metadata_shared::{ + interface::{RequestData, ResponseData}, + latest, AxumErrorResponse, AxumResult, Construct, Extract, Request, Response, +}; +use tower_http::compression::CompressionLayer; + +use crate::http::state::AppState; + +pub(crate) fn bandwidth_routes() -> Router { + Router::new() + .route("/version", axum::routing::get(version)) + .route("/available", axum::routing::post(available_bandwidth)) + .route("/topup", axum::routing::post(topup_bandwidth)) + .layer(CompressionLayer::new()) +} + +#[utoipa::path( + tag = "bandwidth", + get, + path = "/v1/bandwidth/version", + responses( + (status = 200, content( + (Response = "application/bincode") + )) + ), +)] +async fn version(Query(output): Query) -> AxumResult> { + let output = output.output.unwrap_or_default(); + Ok(output.to_response(latest::VERSION.into())) +} + +#[utoipa::path( + tag = "bandwidth", + post, + request_body = Request, + path = "/v1/bandwidth/available", + responses( + (status = 200, content( + (Response = "application/bincode") + )) + ), +)] +async fn available_bandwidth( + ConnectInfo(addr): ConnectInfo, + Query(output): Query, + State(state): State, + Json(request): Json, +) -> AxumResult> { + let output = output.output.unwrap_or_default(); + + let (RequestData::AvailableBandwidth(_), version) = + request.extract().map_err(AxumErrorResponse::bad_request)? + else { + return Err(AxumErrorResponse::bad_request("incorrect request type")); + }; + let available_bandwidth = state + .available_bandwidth(addr.ip()) + .await + .map_err(AxumErrorResponse::bad_request)?; + let response = Response::construct( + ResponseData::AvailableBandwidth(available_bandwidth), + version, + ) + .map_err(AxumErrorResponse::bad_request)?; + + Ok(output.to_response(response)) +} + +#[utoipa::path( + tag = "bandwidth", + post, + request_body = Request, + path = "/v1/bandwidth/topup", + responses( + (status = 200, content( + (Response = "application/bincode") + )) + ), +)] +async fn topup_bandwidth( + ConnectInfo(addr): ConnectInfo, + Query(output): Query, + State(state): State, + Json(request): Json, +) -> AxumResult> { + let output = output.output.unwrap_or_default(); + + let (RequestData::TopUpBandwidth(credential), version) = + request.extract().map_err(AxumErrorResponse::bad_request)? + else { + return Err(AxumErrorResponse::bad_request("incorrect request type")); + }; + let available_bandwidth = state + .topup_bandwidth(addr.ip(), *credential) + .await + .map_err(AxumErrorResponse::bad_request)?; + let response = Response::construct(ResponseData::TopUpBandwidth(available_bandwidth), version) + .map_err(AxumErrorResponse::bad_request)?; + + Ok(output.to_response(response)) +} diff --git a/common/wireguard-private-metadata/server/src/transceiver.rs b/common/wireguard-private-metadata/server/src/transceiver.rs new file mode 100644 index 0000000000..cbe77126cf --- /dev/null +++ b/common/wireguard-private-metadata/server/src/transceiver.rs @@ -0,0 +1,307 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use std::net::IpAddr; + +use futures::channel::oneshot; +use tokio::sync::mpsc; + +use nym_credential_verification::ClientBandwidth; +use nym_credentials_interface::CredentialSpendingData; +use nym_wireguard::peer_controller::PeerControlRequest; +use nym_wireguard_private_metadata_shared::error::MetadataError; + +#[derive(Clone)] +pub struct PeerControllerTransceiver { + request_tx: mpsc::Sender, +} + +impl PeerControllerTransceiver { + pub fn new(request_tx: mpsc::Sender) -> Self { + Self { request_tx } + } + + async fn get_client_bandwidth(&self, ip: IpAddr) -> Result { + let (response_tx, response_rx) = oneshot::channel(); + let msg = PeerControlRequest::GetClientBandwidthByIp { ip, response_tx }; + self.request_tx + .send(msg) + .await + .map_err(|_| MetadataError::PeerInteractionStopped)?; + + response_rx + .await + .map_err(|_| MetadataError::NoResponse)? + .map_err(|err| MetadataError::Unsuccessful { + reason: err.to_string(), + }) + } + + pub(crate) async fn query_bandwidth(&self, ip: IpAddr) -> Result { + Ok(self.get_client_bandwidth(ip).await?.available().await) + } + + // Top up with a credential and return the afterwards available bandwidth + pub(crate) async fn topup_bandwidth( + &self, + ip: IpAddr, + credential: Box, + ) -> Result { + let (response_tx, response_rx) = oneshot::channel(); + let msg = PeerControlRequest::GetVerifierByIp { + ip, + credential, + response_tx, + }; + self.request_tx + .send(msg) + .await + .map_err(|_| MetadataError::PeerInteractionStopped)?; + + let mut verifier = response_rx + .await + .map_err(|_| MetadataError::NoResponse)? + .map_err(|err| MetadataError::Unsuccessful { + reason: err.to_string(), + })?; + let available_bandwidth = + verifier + .verify() + .await + .map_err(|err| MetadataError::CredentialVerification { + message: err.to_string(), + })?; + + Ok(available_bandwidth) + } +} + +#[cfg(test)] +pub(crate) mod tests { + use nym_credential_verification::TicketVerifier; + use nym_wireguard::CONTROL_CHANNEL_SIZE; + + use super::*; + + pub(crate) const CREDENTIAL_BYTES: [u8; 1245] = [ + 0, 0, 4, 133, 96, 179, 223, 185, 136, 23, 213, 166, 59, 203, 66, 69, 209, 181, 227, 254, + 16, 102, 98, 237, 59, 119, 170, 111, 31, 194, 51, 59, 120, 17, 115, 229, 79, 91, 11, 139, + 154, 2, 212, 23, 68, 70, 167, 3, 240, 54, 224, 171, 221, 1, 69, 48, 60, 118, 119, 249, 123, + 35, 172, 227, 131, 96, 232, 209, 187, 123, 4, 197, 102, 90, 96, 45, 125, 135, 140, 99, 1, + 151, 17, 131, 143, 157, 97, 107, 139, 232, 212, 87, 14, 115, 253, 255, 166, 167, 186, 43, + 90, 96, 173, 105, 120, 40, 10, 163, 250, 224, 214, 200, 178, 4, 160, 16, 130, 59, 76, 193, + 39, 240, 3, 101, 141, 209, 183, 226, 186, 207, 56, 210, 187, 7, 164, 240, 164, 205, 37, 81, + 184, 214, 193, 195, 90, 205, 238, 225, 195, 104, 12, 123, 203, 57, 233, 243, 215, 145, 195, + 196, 57, 38, 125, 172, 18, 47, 63, 165, 110, 219, 180, 40, 58, 116, 92, 254, 160, 98, 48, + 92, 254, 232, 107, 184, 80, 234, 60, 160, 235, 249, 76, 41, 38, 165, 28, 40, 136, 74, 48, + 166, 50, 245, 23, 201, 140, 101, 79, 93, 235, 128, 186, 146, 126, 180, 134, 43, 13, 186, + 19, 195, 48, 168, 201, 29, 216, 95, 176, 198, 132, 188, 64, 39, 212, 150, 32, 52, 53, 38, + 228, 199, 122, 226, 217, 75, 40, 191, 151, 48, 164, 242, 177, 79, 14, 122, 105, 151, 85, + 88, 199, 162, 17, 96, 103, 83, 178, 128, 9, 24, 30, 74, 108, 241, 85, 240, 166, 97, 241, + 85, 199, 11, 198, 226, 234, 70, 107, 145, 28, 208, 114, 51, 12, 234, 108, 101, 202, 112, + 48, 185, 22, 159, 67, 109, 49, 27, 149, 90, 109, 32, 226, 112, 7, 201, 208, 209, 104, 31, + 97, 134, 204, 145, 27, 181, 206, 181, 106, 32, 110, 136, 115, 249, 201, 111, 5, 245, 203, + 71, 121, 169, 126, 151, 178, 236, 59, 221, 195, 48, 135, 115, 6, 50, 227, 74, 97, 107, 107, + 213, 90, 2, 203, 154, 138, 47, 128, 52, 134, 128, 224, 51, 65, 240, 90, 8, 55, 175, 180, + 178, 204, 206, 168, 110, 51, 57, 189, 169, 48, 169, 136, 121, 99, 51, 170, 178, 214, 74, 1, + 96, 151, 167, 25, 173, 180, 171, 155, 10, 55, 142, 234, 190, 113, 90, 79, 80, 244, 71, 166, + 30, 235, 113, 150, 133, 1, 218, 17, 109, 111, 223, 24, 216, 177, 41, 2, 204, 65, 221, 212, + 207, 236, 144, 6, 65, 224, 55, 42, 1, 1, 161, 134, 118, 127, 111, 220, 110, 127, 240, 71, + 223, 129, 12, 93, 20, 220, 60, 56, 71, 146, 184, 95, 132, 69, 28, 56, 53, 192, 213, 22, + 119, 230, 152, 225, 182, 188, 163, 219, 37, 175, 247, 73, 14, 247, 38, 72, 243, 1, 48, 131, + 59, 8, 13, 96, 143, 185, 127, 241, 161, 217, 24, 149, 193, 40, 16, 30, 202, 151, 28, 119, + 240, 153, 101, 156, 61, 193, 72, 245, 199, 181, 12, 231, 65, 166, 67, 142, 121, 207, 202, + 58, 197, 113, 188, 248, 42, 124, 105, 48, 161, 241, 55, 209, 36, 194, 27, 63, 233, 144, + 189, 85, 117, 234, 9, 139, 46, 31, 206, 114, 95, 131, 29, 240, 13, 81, 142, 140, 133, 33, + 30, 41, 141, 37, 80, 217, 95, 221, 76, 115, 86, 201, 165, 51, 252, 9, 28, 209, 1, 48, 150, + 74, 248, 212, 187, 222, 66, 210, 3, 200, 19, 217, 171, 184, 42, 148, 53, 150, 57, 50, 6, + 227, 227, 62, 49, 42, 148, 148, 157, 82, 191, 58, 24, 34, 56, 98, 120, 89, 105, 176, 85, + 15, 253, 241, 41, 153, 195, 136, 1, 48, 142, 126, 213, 101, 223, 79, 133, 230, 105, 38, + 161, 149, 2, 21, 136, 150, 42, 72, 218, 85, 146, 63, 223, 58, 108, 186, 183, 248, 62, 20, + 47, 34, 113, 160, 177, 204, 181, 16, 24, 212, 224, 35, 84, 51, 168, 56, 136, 11, 1, 48, + 135, 242, 62, 149, 230, 178, 32, 224, 119, 26, 234, 163, 237, 224, 114, 95, 112, 140, 170, + 150, 96, 125, 136, 221, 180, 78, 18, 11, 12, 184, 2, 198, 217, 119, 43, 69, 4, 172, 109, + 55, 183, 40, 131, 172, 161, 88, 183, 101, 1, 48, 173, 216, 22, 73, 42, 255, 211, 93, 249, + 87, 159, 115, 61, 91, 55, 130, 17, 216, 60, 34, 122, 55, 8, 244, 244, 153, 151, 57, 5, 144, + 178, 55, 249, 64, 211, 168, 34, 148, 56, 89, 92, 203, 70, 124, 219, 152, 253, 165, 0, 32, + 203, 116, 63, 7, 240, 222, 82, 86, 11, 149, 167, 72, 224, 55, 190, 66, 201, 65, 168, 184, + 96, 47, 194, 241, 168, 124, 7, 74, 214, 250, 37, 76, 32, 218, 69, 122, 103, 215, 145, 169, + 24, 212, 229, 168, 106, 10, 144, 31, 13, 25, 178, 242, 250, 106, 159, 40, 48, 163, 165, 61, + 130, 57, 146, 4, 73, 32, 254, 233, 125, 135, 212, 29, 111, 4, 177, 114, 15, 210, 170, 82, + 108, 110, 62, 166, 81, 209, 106, 176, 156, 14, 133, 242, 60, 127, 120, 242, 28, 97, 0, 1, + 32, 103, 93, 109, 89, 240, 91, 1, 84, 150, 50, 206, 157, 203, 49, 220, 120, 234, 175, 234, + 150, 126, 225, 94, 163, 164, 199, 138, 114, 62, 99, 106, 112, 1, 32, 171, 40, 220, 82, 241, + 203, 76, 146, 111, 139, 182, 179, 237, 182, 115, 75, 128, 201, 107, 43, 214, 0, 135, 217, + 160, 68, 150, 232, 144, 114, 237, 98, 32, 30, 134, 232, 59, 93, 163, 253, 244, 13, 202, 52, + 147, 168, 83, 121, 123, 95, 21, 210, 209, 225, 223, 143, 49, 10, 205, 238, 1, 22, 83, 81, + 70, 1, 32, 26, 76, 6, 234, 160, 50, 139, 102, 161, 232, 155, 106, 130, 171, 226, 210, 233, + 178, 85, 247, 71, 123, 55, 53, 46, 67, 148, 137, 156, 207, 208, 107, 1, 32, 102, 31, 4, 98, + 110, 156, 144, 61, 229, 140, 198, 84, 196, 238, 128, 35, 131, 182, 137, 125, 241, 95, 69, + 131, 170, 27, 2, 144, 75, 72, 242, 102, 3, 32, 121, 80, 45, 173, 56, 65, 218, 27, 40, 251, + 197, 32, 169, 104, 123, 110, 90, 78, 153, 166, 38, 9, 129, 228, 99, 8, 1, 116, 142, 233, + 162, 69, 32, 216, 169, 159, 116, 95, 12, 63, 176, 195, 6, 183, 123, 135, 75, 61, 112, 106, + 83, 235, 176, 41, 27, 248, 48, 71, 165, 170, 12, 92, 103, 103, 81, 32, 58, 74, 75, 145, + 192, 94, 153, 69, 80, 128, 241, 3, 16, 117, 192, 86, 161, 103, 44, 174, 211, 196, 182, 124, + 55, 11, 107, 142, 49, 88, 6, 41, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 0, 37, 139, 240, 0, 0, + 0, 0, 0, 0, 0, 1, + ]; + + pub(crate) struct MockVerifier { + ret: i64, + } + + impl MockVerifier { + pub(crate) fn new(ret: i64) -> MockVerifier { + Self { ret } + } + } + + #[async_trait::async_trait] + impl TicketVerifier for MockVerifier { + async fn verify(&mut self) -> nym_credential_verification::Result { + Ok(self.ret) + } + } + + #[tokio::test] + async fn get_bandwidth() { + let (request_tx, mut request_rx) = mpsc::channel(CONTROL_CHANNEL_SIZE); + let transceiver = PeerControllerTransceiver::new(request_tx); + + tokio::spawn(async move { + match request_rx.recv().await.unwrap() { + PeerControlRequest::GetClientBandwidthByIp { ip: _, response_tx } => { + response_tx + .send(Ok(ClientBandwidth::new(Default::default()))) + .ok(); + } + _ => panic!("Not expected"), + } + }); + + let bw = transceiver + .query_bandwidth("10.0.0.42".parse().unwrap()) + .await + .unwrap(); + assert_eq!(bw, 0); + } + + #[tokio::test] + async fn stop_peer() { + let (request_tx, request_rx) = mpsc::channel(CONTROL_CHANNEL_SIZE); + let transceiver = PeerControllerTransceiver::new(request_tx); + + drop(request_rx); + let err = transceiver + .query_bandwidth("10.0.0.42".parse().unwrap()) + .await + .unwrap_err(); + assert_eq!(err, MetadataError::PeerInteractionStopped); + } + + #[tokio::test] + async fn unresponsive_peer() { + let (request_tx, mut request_rx) = mpsc::channel(CONTROL_CHANNEL_SIZE); + let transceiver = PeerControllerTransceiver::new(request_tx); + + tokio::spawn(async move { + match request_rx.recv().await.unwrap() { + PeerControlRequest::GetClientBandwidthByIp { + ip: _, + response_tx: _, + } => {} + _ => panic!("Not expected"), + } + }); + + let err = transceiver + .query_bandwidth("10.0.0.42".parse().unwrap()) + .await + .unwrap_err(); + assert_eq!(err, MetadataError::NoResponse); + } + + #[tokio::test] + async fn unsuccessful_query_bandwidth() { + let (request_tx, mut request_rx) = mpsc::channel(CONTROL_CHANNEL_SIZE); + let transceiver = PeerControllerTransceiver::new(request_tx); + + tokio::spawn(async move { + match request_rx.recv().await.unwrap() { + PeerControlRequest::GetClientBandwidthByIp { ip: _, response_tx } => { + response_tx + .send(Err(nym_wireguard::error::Error::Internal( + "testing".to_owned(), + ))) + .ok(); + } + _ => panic!("Not expected"), + } + }); + + let ret = transceiver + .query_bandwidth("10.0.0.42".parse().unwrap()) + .await; + assert!(ret.is_err()); + } + + #[tokio::test] + async fn topup() { + let (request_tx, mut request_rx) = mpsc::channel(CONTROL_CHANNEL_SIZE); + let transceiver = PeerControllerTransceiver::new(request_tx); + let credential = CredentialSpendingData::try_from_bytes(&CREDENTIAL_BYTES).unwrap(); + let verifier_bw = 42; + + tokio::spawn(async move { + match request_rx.recv().await.unwrap() { + PeerControlRequest::GetVerifierByIp { + ip: _, + credential: _, + response_tx, + } => { + response_tx + .send(Ok(Box::new(MockVerifier::new(verifier_bw)))) + .ok(); + } + _ => panic!("Not expected"), + } + }); + + let bw = transceiver + .topup_bandwidth("10.0.0.42".parse().unwrap(), Box::new(credential)) + .await + .unwrap(); + assert_eq!(bw, verifier_bw); + } + + #[tokio::test] + async fn unsuccessful_topup() { + let (request_tx, mut request_rx) = mpsc::channel(CONTROL_CHANNEL_SIZE); + let transceiver = PeerControllerTransceiver::new(request_tx); + let credential = CredentialSpendingData::try_from_bytes(&CREDENTIAL_BYTES).unwrap(); + + tokio::spawn(async move { + match request_rx.recv().await.unwrap() { + PeerControlRequest::GetVerifierByIp { + ip: _, + credential: _, + response_tx, + } => { + response_tx + .send(Err(nym_wireguard::error::Error::Internal( + "testing".to_owned(), + ))) + .ok(); + } + _ => panic!("Not expected"), + } + }); + + let ret = transceiver + .topup_bandwidth("10.0.0.42".parse().unwrap(), Box::new(credential)) + .await; + assert!(ret.is_err()); + } +} diff --git a/common/wireguard-private-metadata/shared/Cargo.toml b/common/wireguard-private-metadata/shared/Cargo.toml new file mode 100644 index 0000000000..26b3cc6cd6 --- /dev/null +++ b/common/wireguard-private-metadata/shared/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "nym-wireguard-private-metadata-shared" +version = "1.0.0" +authors.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +axum = { workspace = true } +bincode = { workspace = true } +schemars = { workspace = true, features = ["preserve_order"] } +serde = { workspace = true } +thiserror = { workspace = true } +utoipa = { workspace = true } + +nym-credentials-interface = { path = "../../credentials-interface" } + +[features] +testing = [] + +[lints] +workspace = true diff --git a/common/wireguard-private-metadata/shared/src/error.rs b/common/wireguard-private-metadata/shared/src/error.rs new file mode 100644 index 0000000000..3783462a4d --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/error.rs @@ -0,0 +1,28 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +#[derive(Debug, PartialEq, Eq, thiserror::Error)] +pub enum MetadataError { + #[error("peers can't be interacted with anymore")] + PeerInteractionStopped, + + #[error("no response received")] + NoResponse, + + #[error("query was not successful: {reason}")] + Unsuccessful { reason: String }, + + #[error("Models error: {message}")] + Models { message: String }, + + #[error("Credential verification error: {message}")] + CredentialVerification { message: String }, +} + +impl From for MetadataError { + fn from(value: crate::models::error::Error) -> Self { + Self::Models { + message: value.to_string(), + } + } +} diff --git a/common/wireguard-private-metadata/shared/src/lib.rs b/common/wireguard-private-metadata/shared/src/lib.rs new file mode 100644 index 0000000000..de79157874 --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/lib.rs @@ -0,0 +1,20 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod error; +mod models; +pub mod routes; + +#[cfg(feature = "testing")] +pub use models::v0; +pub use models::{ + error::Error as ModelError, interface, latest, v1, AxumErrorResponse, AxumResult, Construct, + ErrorResponse, Extract, Request, Response, Version, +}; + +fn make_bincode_serializer() -> impl bincode::Options { + use bincode::Options; + bincode::DefaultOptions::new() + .with_big_endian() + .with_varint_encoding() +} diff --git a/common/wireguard-private-metadata/shared/src/models/error.rs b/common/wireguard-private-metadata/shared/src/models/error.rs new file mode 100644 index 0000000000..45dc88617d --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/models/error.rs @@ -0,0 +1,30 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::models::Version; + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error(transparent)] + Bincode(#[from] bincode::Error), + + #[error("trying to deserialize from version {source_version:?} into {target_version:?}")] + InvalidVersion { + source_version: Version, + target_version: Version, + }, + + #[error( + "trying to deserialize from query type {source_query_type} query type {target_query_type}" + )] + InvalidQueryType { + source_query_type: String, + target_query_type: String, + }, + + #[error("update not possible from {from:?} to {to:?}")] + UpdateNotPossible { from: Version, to: Version }, + + #[error("downgrade not possible from {from:?} to {to:?}")] + DowngradeNotPossible { from: Version, to: Version }, +} diff --git a/common/wireguard-private-metadata/shared/src/models/interface.rs b/common/wireguard-private-metadata/shared/src/models/interface.rs new file mode 100644 index 0000000000..3f4537f170 --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/models/interface.rs @@ -0,0 +1,145 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use nym_credentials_interface::CredentialSpendingData; + +#[cfg(feature = "testing")] +use crate::models::v0; +use crate::models::{v1, Construct, Extract, Request, Response, Version}; + +pub enum RequestData { + AvailableBandwidth(()), + TopUpBandwidth(Box), +} + +impl From for RequestData { + fn from(value: super::latest::interface::RequestData) -> Self { + match value { + super::latest::interface::RequestData::AvailableBandwidth(inner) => { + Self::AvailableBandwidth(inner) + } + super::latest::interface::RequestData::TopUpBandwidth(credential_spending_data) => { + Self::TopUpBandwidth(credential_spending_data) + } + } + } +} + +impl From for super::latest::interface::RequestData { + fn from(value: RequestData) -> Self { + match value { + RequestData::AvailableBandwidth(inner) => Self::AvailableBandwidth(inner), + RequestData::TopUpBandwidth(credential_spending_data) => { + Self::TopUpBandwidth(credential_spending_data) + } + } + } +} + +impl From for ResponseData { + fn from(value: super::latest::interface::ResponseData) -> Self { + match value { + super::latest::interface::ResponseData::AvailableBandwidth(inner) => { + Self::AvailableBandwidth(inner) + } + super::latest::interface::ResponseData::TopUpBandwidth(credential_spending_data) => { + Self::TopUpBandwidth(credential_spending_data) + } + } + } +} + +impl From for super::latest::interface::ResponseData { + fn from(value: ResponseData) -> Self { + match value { + ResponseData::AvailableBandwidth(inner) => Self::AvailableBandwidth(inner), + ResponseData::TopUpBandwidth(credential_spending_data) => { + Self::TopUpBandwidth(credential_spending_data) + } + } + } +} + +impl Construct for Request { + fn construct(info: RequestData, version: Version) -> Result { + match version { + #[cfg(feature = "testing")] + Version::V0 => { + let translate_info = super::latest::interface::RequestData::from(info); + let downgrade_info = v0::interface::RequestData::try_from(translate_info)?; + let versioned_request = v0::VersionedRequest::construct(downgrade_info, version)?; + Ok(versioned_request.try_into()?) + } + Version::V1 => { + let versioned_request = v1::VersionedRequest::construct(info.into(), version)?; + Ok(versioned_request.try_into()?) + } + } + } +} + +impl Extract for Request { + fn extract(&self) -> Result<(RequestData, Version), crate::models::Error> { + match self.version { + #[cfg(feature = "testing")] + super::Version::V0 => { + let versioned_request = v0::VersionedRequest::try_from(self.clone())?; + let (request, version) = versioned_request.extract()?; + + let upgrade_request = super::latest::interface::RequestData::try_from(request)?; + + Ok((upgrade_request.into(), version)) + } + super::Version::V1 => { + let versioned_request = v1::VersionedRequest::try_from(self.clone())?; + let (extracted, version) = versioned_request.extract()?; + Ok((extracted.into(), version)) + } + } + } +} + +pub enum ResponseData { + AvailableBandwidth(i64), + TopUpBandwidth(i64), +} + +impl Construct for Response { + fn construct(info: ResponseData, version: Version) -> Result { + match version { + #[cfg(feature = "testing")] + super::Version::V0 => { + let translate_response = super::latest::interface::ResponseData::from(info); + let downgrade_response = v0::interface::ResponseData::try_from(translate_response)?; + let versioned_response = + v0::VersionedResponse::construct(downgrade_response, version)?; + Ok(versioned_response.try_into()?) + } + Version::V1 => { + let versioned_response = v1::VersionedResponse::construct(info.into(), version)?; + Ok(versioned_response.try_into()?) + } + } + } +} + +impl Extract for Response { + fn extract(&self) -> Result<(ResponseData, Version), super::error::Error> { + match self.version { + #[cfg(feature = "testing")] + super::Version::V0 => { + let versioned_response = v0::VersionedResponse::try_from(self.clone())?; + let (response, version) = versioned_response.extract()?; + + let upgrade_response = super::latest::interface::ResponseData::try_from(response)?; + + Ok((upgrade_response.into(), version)) + } + super::Version::V1 => { + let versioned_response = v1::VersionedResponse::try_from(self.clone())?; + let (extracted, version) = versioned_response.extract()?; + Ok((extracted.into(), version)) + } + } + } +} diff --git a/common/wireguard-private-metadata/shared/src/models/mod.rs b/common/wireguard-private-metadata/shared/src/models/mod.rs new file mode 100644 index 0000000000..e408d7c5df --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/models/mod.rs @@ -0,0 +1,175 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use std::fmt::{Display, Formatter}; + +use axum::http::StatusCode; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; + +pub(crate) mod error; +pub mod interface; +#[cfg(feature = "testing")] +pub mod v0; // dummy version, only for filling boilerplate code for update/downgrade and testing +pub mod v1; + +pub use v1 as latest; + +use crate::models::error::Error; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)] +pub enum Version { + #[cfg(feature = "testing")] + /// only used for testing purposes, don't include it in your matching arms + V0, + V1, +} + +impl From for Version { + fn from(value: u64) -> Self { + #[cfg(feature = "testing")] + let zero_version = Version::V0; + #[cfg(not(feature = "testing"))] + let zero_version = latest::VERSION; + match value { + 0 => zero_version, + 1 => Version::V1, + _ => latest::VERSION, // if unknown, it means we're behind, so we can use the latest we know about + } + } +} + +impl From for u64 { + fn from(value: Version) -> Self { + // remember to modify the above match if you're bumping the version + match value { + #[cfg(feature = "testing")] + Version::V0 => 0, + Version::V1 => 1, + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, ToSchema)] +pub struct Request { + pub version: Version, + pub(crate) inner: Vec, +} + +#[derive(Clone, Serialize, Deserialize, ToSchema)] +pub struct Response { + pub version: Version, + pub(crate) inner: Vec, +} + +pub trait Extract { + fn extract(&self) -> Result<(T, Version), Error>; +} + +pub trait Construct: Sized { + fn construct(info: T, version: Version) -> Result; +} + +pub type AxumResult = Result; + +#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)] +pub struct ErrorResponse { + pub message: String, +} + +impl Display for ErrorResponse { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + self.message.fmt(f) + } +} + +pub struct AxumErrorResponse { + message: ErrorResponse, + status: StatusCode, +} + +impl AxumErrorResponse { + pub fn bad_request(msg: impl Display) -> Self { + Self { + message: ErrorResponse { + message: msg.to_string(), + }, + status: StatusCode::BAD_REQUEST, + } + } +} + +impl axum::response::IntoResponse for AxumErrorResponse { + fn into_response(self) -> axum::response::Response { + (self.status, self.message.message.to_string()).into_response() + } +} + +mod tests { + #[allow(dead_code)] + pub(crate) const CREDENTIAL_BYTES: [u8; 1245] = [ + 0, 0, 4, 133, 96, 179, 223, 185, 136, 23, 213, 166, 59, 203, 66, 69, 209, 181, 227, 254, + 16, 102, 98, 237, 59, 119, 170, 111, 31, 194, 51, 59, 120, 17, 115, 229, 79, 91, 11, 139, + 154, 2, 212, 23, 68, 70, 167, 3, 240, 54, 224, 171, 221, 1, 69, 48, 60, 118, 119, 249, 123, + 35, 172, 227, 131, 96, 232, 209, 187, 123, 4, 197, 102, 90, 96, 45, 125, 135, 140, 99, 1, + 151, 17, 131, 143, 157, 97, 107, 139, 232, 212, 87, 14, 115, 253, 255, 166, 167, 186, 43, + 90, 96, 173, 105, 120, 40, 10, 163, 250, 224, 214, 200, 178, 4, 160, 16, 130, 59, 76, 193, + 39, 240, 3, 101, 141, 209, 183, 226, 186, 207, 56, 210, 187, 7, 164, 240, 164, 205, 37, 81, + 184, 214, 193, 195, 90, 205, 238, 225, 195, 104, 12, 123, 203, 57, 233, 243, 215, 145, 195, + 196, 57, 38, 125, 172, 18, 47, 63, 165, 110, 219, 180, 40, 58, 116, 92, 254, 160, 98, 48, + 92, 254, 232, 107, 184, 80, 234, 60, 160, 235, 249, 76, 41, 38, 165, 28, 40, 136, 74, 48, + 166, 50, 245, 23, 201, 140, 101, 79, 93, 235, 128, 186, 146, 126, 180, 134, 43, 13, 186, + 19, 195, 48, 168, 201, 29, 216, 95, 176, 198, 132, 188, 64, 39, 212, 150, 32, 52, 53, 38, + 228, 199, 122, 226, 217, 75, 40, 191, 151, 48, 164, 242, 177, 79, 14, 122, 105, 151, 85, + 88, 199, 162, 17, 96, 103, 83, 178, 128, 9, 24, 30, 74, 108, 241, 85, 240, 166, 97, 241, + 85, 199, 11, 198, 226, 234, 70, 107, 145, 28, 208, 114, 51, 12, 234, 108, 101, 202, 112, + 48, 185, 22, 159, 67, 109, 49, 27, 149, 90, 109, 32, 226, 112, 7, 201, 208, 209, 104, 31, + 97, 134, 204, 145, 27, 181, 206, 181, 106, 32, 110, 136, 115, 249, 201, 111, 5, 245, 203, + 71, 121, 169, 126, 151, 178, 236, 59, 221, 195, 48, 135, 115, 6, 50, 227, 74, 97, 107, 107, + 213, 90, 2, 203, 154, 138, 47, 128, 52, 134, 128, 224, 51, 65, 240, 90, 8, 55, 175, 180, + 178, 204, 206, 168, 110, 51, 57, 189, 169, 48, 169, 136, 121, 99, 51, 170, 178, 214, 74, 1, + 96, 151, 167, 25, 173, 180, 171, 155, 10, 55, 142, 234, 190, 113, 90, 79, 80, 244, 71, 166, + 30, 235, 113, 150, 133, 1, 218, 17, 109, 111, 223, 24, 216, 177, 41, 2, 204, 65, 221, 212, + 207, 236, 144, 6, 65, 224, 55, 42, 1, 1, 161, 134, 118, 127, 111, 220, 110, 127, 240, 71, + 223, 129, 12, 93, 20, 220, 60, 56, 71, 146, 184, 95, 132, 69, 28, 56, 53, 192, 213, 22, + 119, 230, 152, 225, 182, 188, 163, 219, 37, 175, 247, 73, 14, 247, 38, 72, 243, 1, 48, 131, + 59, 8, 13, 96, 143, 185, 127, 241, 161, 217, 24, 149, 193, 40, 16, 30, 202, 151, 28, 119, + 240, 153, 101, 156, 61, 193, 72, 245, 199, 181, 12, 231, 65, 166, 67, 142, 121, 207, 202, + 58, 197, 113, 188, 248, 42, 124, 105, 48, 161, 241, 55, 209, 36, 194, 27, 63, 233, 144, + 189, 85, 117, 234, 9, 139, 46, 31, 206, 114, 95, 131, 29, 240, 13, 81, 142, 140, 133, 33, + 30, 41, 141, 37, 80, 217, 95, 221, 76, 115, 86, 201, 165, 51, 252, 9, 28, 209, 1, 48, 150, + 74, 248, 212, 187, 222, 66, 210, 3, 200, 19, 217, 171, 184, 42, 148, 53, 150, 57, 50, 6, + 227, 227, 62, 49, 42, 148, 148, 157, 82, 191, 58, 24, 34, 56, 98, 120, 89, 105, 176, 85, + 15, 253, 241, 41, 153, 195, 136, 1, 48, 142, 126, 213, 101, 223, 79, 133, 230, 105, 38, + 161, 149, 2, 21, 136, 150, 42, 72, 218, 85, 146, 63, 223, 58, 108, 186, 183, 248, 62, 20, + 47, 34, 113, 160, 177, 204, 181, 16, 24, 212, 224, 35, 84, 51, 168, 56, 136, 11, 1, 48, + 135, 242, 62, 149, 230, 178, 32, 224, 119, 26, 234, 163, 237, 224, 114, 95, 112, 140, 170, + 150, 96, 125, 136, 221, 180, 78, 18, 11, 12, 184, 2, 198, 217, 119, 43, 69, 4, 172, 109, + 55, 183, 40, 131, 172, 161, 88, 183, 101, 1, 48, 173, 216, 22, 73, 42, 255, 211, 93, 249, + 87, 159, 115, 61, 91, 55, 130, 17, 216, 60, 34, 122, 55, 8, 244, 244, 153, 151, 57, 5, 144, + 178, 55, 249, 64, 211, 168, 34, 148, 56, 89, 92, 203, 70, 124, 219, 152, 253, 165, 0, 32, + 203, 116, 63, 7, 240, 222, 82, 86, 11, 149, 167, 72, 224, 55, 190, 66, 201, 65, 168, 184, + 96, 47, 194, 241, 168, 124, 7, 74, 214, 250, 37, 76, 32, 218, 69, 122, 103, 215, 145, 169, + 24, 212, 229, 168, 106, 10, 144, 31, 13, 25, 178, 242, 250, 106, 159, 40, 48, 163, 165, 61, + 130, 57, 146, 4, 73, 32, 254, 233, 125, 135, 212, 29, 111, 4, 177, 114, 15, 210, 170, 82, + 108, 110, 62, 166, 81, 209, 106, 176, 156, 14, 133, 242, 60, 127, 120, 242, 28, 97, 0, 1, + 32, 103, 93, 109, 89, 240, 91, 1, 84, 150, 50, 206, 157, 203, 49, 220, 120, 234, 175, 234, + 150, 126, 225, 94, 163, 164, 199, 138, 114, 62, 99, 106, 112, 1, 32, 171, 40, 220, 82, 241, + 203, 76, 146, 111, 139, 182, 179, 237, 182, 115, 75, 128, 201, 107, 43, 214, 0, 135, 217, + 160, 68, 150, 232, 144, 114, 237, 98, 32, 30, 134, 232, 59, 93, 163, 253, 244, 13, 202, 52, + 147, 168, 83, 121, 123, 95, 21, 210, 209, 225, 223, 143, 49, 10, 205, 238, 1, 22, 83, 81, + 70, 1, 32, 26, 76, 6, 234, 160, 50, 139, 102, 161, 232, 155, 106, 130, 171, 226, 210, 233, + 178, 85, 247, 71, 123, 55, 53, 46, 67, 148, 137, 156, 207, 208, 107, 1, 32, 102, 31, 4, 98, + 110, 156, 144, 61, 229, 140, 198, 84, 196, 238, 128, 35, 131, 182, 137, 125, 241, 95, 69, + 131, 170, 27, 2, 144, 75, 72, 242, 102, 3, 32, 121, 80, 45, 173, 56, 65, 218, 27, 40, 251, + 197, 32, 169, 104, 123, 110, 90, 78, 153, 166, 38, 9, 129, 228, 99, 8, 1, 116, 142, 233, + 162, 69, 32, 216, 169, 159, 116, 95, 12, 63, 176, 195, 6, 183, 123, 135, 75, 61, 112, 106, + 83, 235, 176, 41, 27, 248, 48, 71, 165, 170, 12, 92, 103, 103, 81, 32, 58, 74, 75, 145, + 192, 94, 153, 69, 80, 128, 241, 3, 16, 117, 192, 86, 161, 103, 44, 174, 211, 196, 182, 124, + 55, 11, 107, 142, 49, 88, 6, 41, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 0, 37, 139, 240, 0, 0, + 0, 0, 0, 0, 0, 1, + ]; +} diff --git a/common/wireguard-private-metadata/shared/src/models/v0/available_bandwidth/mod.rs b/common/wireguard-private-metadata/shared/src/models/v0/available_bandwidth/mod.rs new file mode 100644 index 0000000000..10698cca2d --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/models/v0/available_bandwidth/mod.rs @@ -0,0 +1,5 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod request; +pub mod response; diff --git a/common/wireguard-private-metadata/shared/src/models/v0/available_bandwidth/request.rs b/common/wireguard-private-metadata/shared/src/models/v0/available_bandwidth/request.rs new file mode 100644 index 0000000000..78dfdec7b5 --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/models/v0/available_bandwidth/request.rs @@ -0,0 +1,86 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use bincode::Options; +use serde::{Deserialize, Serialize}; + +use crate::{make_bincode_serializer, models::Request}; + +use super::super::{Error, QueryType, VersionedRequest}; + +#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct InnerAvailableBandwidthRequest {} + +impl TryFrom for InnerAvailableBandwidthRequest { + type Error = Error; + + fn try_from(value: VersionedRequest) -> Result { + match value.query_type { + QueryType::AvailableBandwidth => { + Ok(make_bincode_serializer().deserialize(&value.inner)?) + } + QueryType::TopupBandwidth => Err(Error::InvalidQueryType { + source_query_type: value.query_type.to_string(), + target_query_type: QueryType::AvailableBandwidth.to_string(), + }), + } + } +} + +impl TryFrom for VersionedRequest { + type Error = Error; + + fn try_from(value: InnerAvailableBandwidthRequest) -> Result { + Ok(Self { + query_type: QueryType::AvailableBandwidth, + inner: make_bincode_serializer().serialize(&value)?, + }) + } +} + +impl TryFrom for InnerAvailableBandwidthRequest { + type Error = crate::error::MetadataError; + + fn try_from(value: Request) -> Result { + VersionedRequest::try_from(value)? + .try_into() + .map_err(|err: Error| crate::error::MetadataError::Models { + message: err.to_string(), + }) + } +} + +impl TryFrom for Request { + type Error = crate::error::MetadataError; + + fn try_from(value: InnerAvailableBandwidthRequest) -> Result { + VersionedRequest::try_from(value)? + .try_into() + .map_err(|err: Error| crate::error::MetadataError::Models { + message: err.to_string(), + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn serde() { + let req = InnerAvailableBandwidthRequest {}; + let ser = VersionedRequest::try_from(req).unwrap(); + assert_eq!(QueryType::AvailableBandwidth, ser.query_type); + let de = InnerAvailableBandwidthRequest::try_from(ser).unwrap(); + assert_eq!(req, de); + } + + #[test] + fn empty_content() { + let future_req = VersionedRequest { + query_type: QueryType::AvailableBandwidth, + inner: vec![], + }; + assert!(InnerAvailableBandwidthRequest::try_from(future_req).is_ok()); + } +} diff --git a/common/wireguard-private-metadata/shared/src/models/v0/available_bandwidth/response.rs b/common/wireguard-private-metadata/shared/src/models/v0/available_bandwidth/response.rs new file mode 100644 index 0000000000..5243e312ee --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/models/v0/available_bandwidth/response.rs @@ -0,0 +1,86 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use bincode::Options; +use serde::{Deserialize, Serialize}; + +use crate::{make_bincode_serializer, models::Response}; + +use super::super::{Error, QueryType, VersionedResponse}; + +#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct InnerAvailableBandwidthResponse {} + +impl TryFrom for InnerAvailableBandwidthResponse { + type Error = Error; + + fn try_from(value: VersionedResponse) -> Result { + match value.query_type { + QueryType::AvailableBandwidth => { + Ok(make_bincode_serializer().deserialize(&value.inner)?) + } + QueryType::TopupBandwidth => Err(Error::InvalidQueryType { + source_query_type: value.query_type.to_string(), + target_query_type: QueryType::AvailableBandwidth.to_string(), + }), + } + } +} + +impl TryFrom for VersionedResponse { + type Error = Error; + + fn try_from(value: InnerAvailableBandwidthResponse) -> Result { + Ok(Self { + query_type: QueryType::AvailableBandwidth, + inner: make_bincode_serializer().serialize(&value)?, + }) + } +} + +impl TryFrom for InnerAvailableBandwidthResponse { + type Error = crate::error::MetadataError; + + fn try_from(value: Response) -> Result { + VersionedResponse::try_from(value)? + .try_into() + .map_err(|err: Error| crate::error::MetadataError::Models { + message: err.to_string(), + }) + } +} + +impl TryFrom for Response { + type Error = crate::error::MetadataError; + + fn try_from(value: InnerAvailableBandwidthResponse) -> Result { + VersionedResponse::try_from(value)? + .try_into() + .map_err(|err: Error| crate::error::MetadataError::Models { + message: err.to_string(), + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn serde() { + let resp = InnerAvailableBandwidthResponse {}; + let ser = VersionedResponse::try_from(resp).unwrap(); + assert_eq!(QueryType::AvailableBandwidth, ser.query_type); + let de = InnerAvailableBandwidthResponse::try_from(ser).unwrap(); + assert_eq!(resp, de); + } + + #[test] + fn empty_content() { + let future_resp = VersionedResponse { + query_type: QueryType::AvailableBandwidth, + inner: vec![], + }; + assert!(InnerAvailableBandwidthResponse::try_from(future_resp).is_ok()); + } +} diff --git a/common/wireguard-private-metadata/shared/src/models/v0/interface.rs b/common/wireguard-private-metadata/shared/src/models/v0/interface.rs new file mode 100644 index 0000000000..2f8fd2805c --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/models/v0/interface.rs @@ -0,0 +1,73 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use super::{ + available_bandwidth::{ + request::InnerAvailableBandwidthRequest, response::InnerAvailableBandwidthResponse, + }, + topup_bandwidth::{request::InnerTopUpRequest, response::InnerTopUpResponse}, + QueryType, VersionedRequest, VersionedResponse, VERSION, +}; +use crate::models::{error::Error, Construct, Extract, Version}; + +#[derive(Debug, Clone, PartialEq)] +pub enum RequestData { + AvailableBandwidth(()), + TopUpBandwidth(()), +} + +#[derive(Debug, Clone, PartialEq)] +pub enum ResponseData { + AvailableBandwidth(()), + TopUpBandwidth(()), +} + +impl Construct for VersionedRequest { + fn construct(info: RequestData, _version: Version) -> Result { + match info { + RequestData::AvailableBandwidth(_) => Ok(InnerAvailableBandwidthRequest {}.try_into()?), + RequestData::TopUpBandwidth(_) => Ok(InnerTopUpRequest {}.try_into()?), + } + } +} + +impl Extract for VersionedRequest { + fn extract(&self) -> Result<(RequestData, Version), Error> { + match self.query_type { + QueryType::AvailableBandwidth => { + let _req = InnerAvailableBandwidthRequest::try_from(self.clone())?; + Ok((RequestData::AvailableBandwidth(()), VERSION)) + } + QueryType::TopupBandwidth => { + let _req = InnerTopUpRequest::try_from(self.clone())?; + Ok((RequestData::TopUpBandwidth(()), VERSION)) + } + } + } +} + +impl Construct for VersionedResponse { + fn construct(info: ResponseData, _version: Version) -> Result { + match info { + ResponseData::AvailableBandwidth(()) => { + Ok(InnerAvailableBandwidthResponse {}.try_into()?) + } + ResponseData::TopUpBandwidth(()) => Ok(InnerTopUpResponse {}.try_into()?), + } + } +} + +impl Extract for VersionedResponse { + fn extract(&self) -> Result<(ResponseData, Version), Error> { + match self.query_type { + QueryType::AvailableBandwidth => { + let _resp = InnerAvailableBandwidthResponse::try_from(self.clone())?; + Ok((ResponseData::AvailableBandwidth(()), VERSION)) + } + QueryType::TopupBandwidth => { + let _resp = InnerTopUpResponse::try_from(self.clone())?; + Ok((ResponseData::TopUpBandwidth(()), VERSION)) + } + } + } +} diff --git a/common/wireguard-private-metadata/shared/src/models/v0/mod.rs b/common/wireguard-private-metadata/shared/src/models/v0/mod.rs new file mode 100644 index 0000000000..997fbf6f57 --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/models/v0/mod.rs @@ -0,0 +1,175 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use std::fmt::Display; + +use bincode::Options; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; + +use super::error::Error; +use crate::{ + make_bincode_serializer, + models::{Request, Response, Version}, +}; + +pub(crate) mod available_bandwidth; +pub mod interface; +pub(crate) mod topup_bandwidth; + +pub const VERSION: Version = Version::V0; + +pub use available_bandwidth::{ + request::InnerAvailableBandwidthRequest as AvailableBandwidthRequest, + response::InnerAvailableBandwidthResponse as AvailableBandwidthResponse, +}; +pub use topup_bandwidth::{ + request::InnerTopUpRequest as TopUpRequest, response::InnerTopUpResponse as TopUpResponse, +}; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)] +pub enum QueryType { + AvailableBandwidth, + TopupBandwidth, +} + +impl Display for QueryType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{self:?}") + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)] +pub struct VersionedRequest { + query_type: QueryType, + inner: Vec, +} + +impl TryFrom for Request { + type Error = Error; + + fn try_from(value: VersionedRequest) -> Result { + Ok(Request { + version: VERSION, + inner: make_bincode_serializer().serialize(&value)?, + }) + } +} + +impl TryFrom for VersionedRequest { + type Error = Error; + + fn try_from(value: Request) -> Result { + if value.version != VERSION { + return Err(Error::InvalidVersion { + source_version: value.version, + target_version: VERSION, + }); + } + Ok(make_bincode_serializer().deserialize(&value.inner)?) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)] +pub struct VersionedResponse { + query_type: QueryType, + inner: Vec, +} + +impl TryFrom for Response { + type Error = Error; + + fn try_from(value: VersionedResponse) -> Result { + Ok(Response { + version: VERSION, + inner: make_bincode_serializer().serialize(&value)?, + }) + } +} + +impl TryFrom for VersionedResponse { + type Error = Error; + + fn try_from(value: Response) -> Result { + if value.version != VERSION { + return Err(Error::InvalidVersion { + source_version: value.version, + target_version: VERSION, + }); + } + Ok(make_bincode_serializer().deserialize(&value.inner)?) + } +} + +#[cfg(test)] +mod tests { + use self::{ + available_bandwidth::{ + request::InnerAvailableBandwidthRequest, response::InnerAvailableBandwidthResponse, + }, + topup_bandwidth::{request::InnerTopUpRequest, response::InnerTopUpResponse}, + }; + + use super::*; + + #[test] + fn serde_request_av_bw() { + let req = VersionedRequest { + query_type: QueryType::AvailableBandwidth, + inner: make_bincode_serializer() + .serialize(&InnerAvailableBandwidthRequest {}) + .unwrap(), + }; + + let ser = Request::try_from(req.clone()).unwrap(); + assert_eq!(VERSION, ser.version); + let de = VersionedRequest::try_from(ser).unwrap(); + assert_eq!(req, de); + } + + #[test] + fn serde_response_av_bw() { + let resp = VersionedResponse { + query_type: QueryType::AvailableBandwidth, + inner: make_bincode_serializer() + .serialize(&InnerAvailableBandwidthResponse {}) + .unwrap(), + }; + + let ser = Response::try_from(resp.clone()).unwrap(); + assert_eq!(VERSION, ser.version); + let de = VersionedResponse::try_from(ser).unwrap(); + assert_eq!(resp, de); + } + + #[test] + fn serde_request_topup() { + let req = VersionedRequest { + query_type: QueryType::TopupBandwidth, + inner: make_bincode_serializer() + .serialize(&InnerTopUpRequest {}) + .unwrap(), + }; + + let ser = Request::try_from(req.clone()).unwrap(); + assert_eq!(VERSION, ser.version); + let de = VersionedRequest::try_from(ser).unwrap(); + assert_eq!(req, de); + } + + #[test] + fn serde_response_topup() { + let resp = VersionedResponse { + query_type: QueryType::TopupBandwidth, + inner: make_bincode_serializer() + .serialize(&InnerTopUpResponse {}) + .unwrap(), + }; + + let ser = Response::try_from(resp.clone()).unwrap(); + assert_eq!(VERSION, ser.version); + let de = VersionedResponse::try_from(ser).unwrap(); + assert_eq!(resp, de); + } +} diff --git a/common/wireguard-private-metadata/shared/src/models/v0/topup_bandwidth/mod.rs b/common/wireguard-private-metadata/shared/src/models/v0/topup_bandwidth/mod.rs new file mode 100644 index 0000000000..10698cca2d --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/models/v0/topup_bandwidth/mod.rs @@ -0,0 +1,5 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod request; +pub mod response; diff --git a/common/wireguard-private-metadata/shared/src/models/v0/topup_bandwidth/request.rs b/common/wireguard-private-metadata/shared/src/models/v0/topup_bandwidth/request.rs new file mode 100644 index 0000000000..9c333478d2 --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/models/v0/topup_bandwidth/request.rs @@ -0,0 +1,84 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use bincode::Options; +use serde::{Deserialize, Serialize}; + +use crate::{make_bincode_serializer, models::Request}; + +use super::super::{Error, QueryType, VersionedRequest}; + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct InnerTopUpRequest {} + +impl TryFrom for InnerTopUpRequest { + type Error = Error; + + fn try_from(value: VersionedRequest) -> Result { + match value.query_type { + QueryType::TopupBandwidth => Ok(make_bincode_serializer().deserialize(&value.inner)?), + QueryType::AvailableBandwidth => Err(Error::InvalidQueryType { + source_query_type: value.query_type.to_string(), + target_query_type: QueryType::TopupBandwidth.to_string(), + }), + } + } +} + +impl TryFrom for VersionedRequest { + type Error = Error; + + fn try_from(value: InnerTopUpRequest) -> Result { + Ok(Self { + query_type: QueryType::TopupBandwidth, + inner: make_bincode_serializer().serialize(&value)?, + }) + } +} + +impl TryFrom for InnerTopUpRequest { + type Error = crate::error::MetadataError; + + fn try_from(value: Request) -> Result { + VersionedRequest::try_from(value)? + .try_into() + .map_err(|err: Error| crate::error::MetadataError::Models { + message: err.to_string(), + }) + } +} + +impl TryFrom for Request { + type Error = crate::error::MetadataError; + + fn try_from(value: InnerTopUpRequest) -> Result { + VersionedRequest::try_from(value)? + .try_into() + .map_err(|err: Error| crate::error::MetadataError::Models { + message: err.to_string(), + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn serde() { + let req = InnerTopUpRequest {}; + let ser = VersionedRequest::try_from(req.clone()).unwrap(); + assert_eq!(QueryType::TopupBandwidth, ser.query_type); + let de = InnerTopUpRequest::try_from(ser).unwrap(); + assert_eq!(req, de); + } + + #[test] + fn empty_content() { + let future_req = VersionedRequest { + query_type: QueryType::TopupBandwidth, + inner: vec![], + }; + assert!(InnerTopUpRequest::try_from(future_req).is_ok()); + } +} diff --git a/common/wireguard-private-metadata/shared/src/models/v0/topup_bandwidth/response.rs b/common/wireguard-private-metadata/shared/src/models/v0/topup_bandwidth/response.rs new file mode 100644 index 0000000000..cd934b6e7e --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/models/v0/topup_bandwidth/response.rs @@ -0,0 +1,84 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use bincode::Options; +use serde::{Deserialize, Serialize}; + +use crate::{make_bincode_serializer, models::Response}; + +use super::super::{Error, QueryType, VersionedResponse}; + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct InnerTopUpResponse {} + +impl TryFrom for InnerTopUpResponse { + type Error = Error; + + fn try_from(value: VersionedResponse) -> Result { + match value.query_type { + QueryType::TopupBandwidth => Ok(make_bincode_serializer().deserialize(&value.inner)?), + QueryType::AvailableBandwidth => Err(Error::InvalidQueryType { + source_query_type: value.query_type.to_string(), + target_query_type: QueryType::TopupBandwidth.to_string(), + }), + } + } +} + +impl TryFrom for VersionedResponse { + type Error = Error; + + fn try_from(value: InnerTopUpResponse) -> Result { + Ok(Self { + query_type: QueryType::TopupBandwidth, + inner: make_bincode_serializer().serialize(&value)?, + }) + } +} + +impl TryFrom for InnerTopUpResponse { + type Error = crate::error::MetadataError; + + fn try_from(value: Response) -> Result { + VersionedResponse::try_from(value)? + .try_into() + .map_err(|err: Error| crate::error::MetadataError::Models { + message: err.to_string(), + }) + } +} + +impl TryFrom for Response { + type Error = crate::error::MetadataError; + + fn try_from(value: InnerTopUpResponse) -> Result { + VersionedResponse::try_from(value)? + .try_into() + .map_err(|err: Error| crate::error::MetadataError::Models { + message: err.to_string(), + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn serde() { + let resp = InnerTopUpResponse {}; + let ser = VersionedResponse::try_from(resp.clone()).unwrap(); + assert_eq!(QueryType::TopupBandwidth, ser.query_type); + let de = InnerTopUpResponse::try_from(ser).unwrap(); + assert_eq!(resp, de); + } + + #[test] + fn empty_content() { + let future_resp = VersionedResponse { + query_type: QueryType::TopupBandwidth, + inner: vec![], + }; + assert!(InnerTopUpResponse::try_from(future_resp).is_ok()); + } +} diff --git a/common/wireguard-private-metadata/shared/src/models/v1/available_bandwidth/mod.rs b/common/wireguard-private-metadata/shared/src/models/v1/available_bandwidth/mod.rs new file mode 100644 index 0000000000..10698cca2d --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/models/v1/available_bandwidth/mod.rs @@ -0,0 +1,5 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod request; +pub mod response; diff --git a/common/wireguard-private-metadata/shared/src/models/v1/available_bandwidth/request.rs b/common/wireguard-private-metadata/shared/src/models/v1/available_bandwidth/request.rs new file mode 100644 index 0000000000..78dfdec7b5 --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/models/v1/available_bandwidth/request.rs @@ -0,0 +1,86 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use bincode::Options; +use serde::{Deserialize, Serialize}; + +use crate::{make_bincode_serializer, models::Request}; + +use super::super::{Error, QueryType, VersionedRequest}; + +#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct InnerAvailableBandwidthRequest {} + +impl TryFrom for InnerAvailableBandwidthRequest { + type Error = Error; + + fn try_from(value: VersionedRequest) -> Result { + match value.query_type { + QueryType::AvailableBandwidth => { + Ok(make_bincode_serializer().deserialize(&value.inner)?) + } + QueryType::TopupBandwidth => Err(Error::InvalidQueryType { + source_query_type: value.query_type.to_string(), + target_query_type: QueryType::AvailableBandwidth.to_string(), + }), + } + } +} + +impl TryFrom for VersionedRequest { + type Error = Error; + + fn try_from(value: InnerAvailableBandwidthRequest) -> Result { + Ok(Self { + query_type: QueryType::AvailableBandwidth, + inner: make_bincode_serializer().serialize(&value)?, + }) + } +} + +impl TryFrom for InnerAvailableBandwidthRequest { + type Error = crate::error::MetadataError; + + fn try_from(value: Request) -> Result { + VersionedRequest::try_from(value)? + .try_into() + .map_err(|err: Error| crate::error::MetadataError::Models { + message: err.to_string(), + }) + } +} + +impl TryFrom for Request { + type Error = crate::error::MetadataError; + + fn try_from(value: InnerAvailableBandwidthRequest) -> Result { + VersionedRequest::try_from(value)? + .try_into() + .map_err(|err: Error| crate::error::MetadataError::Models { + message: err.to_string(), + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn serde() { + let req = InnerAvailableBandwidthRequest {}; + let ser = VersionedRequest::try_from(req).unwrap(); + assert_eq!(QueryType::AvailableBandwidth, ser.query_type); + let de = InnerAvailableBandwidthRequest::try_from(ser).unwrap(); + assert_eq!(req, de); + } + + #[test] + fn empty_content() { + let future_req = VersionedRequest { + query_type: QueryType::AvailableBandwidth, + inner: vec![], + }; + assert!(InnerAvailableBandwidthRequest::try_from(future_req).is_ok()); + } +} diff --git a/common/wireguard-private-metadata/shared/src/models/v1/available_bandwidth/response.rs b/common/wireguard-private-metadata/shared/src/models/v1/available_bandwidth/response.rs new file mode 100644 index 0000000000..f5addd609f --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/models/v1/available_bandwidth/response.rs @@ -0,0 +1,90 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use bincode::Options; +use serde::{Deserialize, Serialize}; + +use crate::{make_bincode_serializer, models::Response}; + +use super::super::{Error, QueryType, VersionedResponse}; + +#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct InnerAvailableBandwidthResponse { + pub available_bandwidth: i64, +} + +impl TryFrom for InnerAvailableBandwidthResponse { + type Error = Error; + + fn try_from(value: VersionedResponse) -> Result { + match value.query_type { + QueryType::AvailableBandwidth => { + Ok(make_bincode_serializer().deserialize(&value.inner)?) + } + QueryType::TopupBandwidth => Err(Error::InvalidQueryType { + source_query_type: value.query_type.to_string(), + target_query_type: QueryType::AvailableBandwidth.to_string(), + }), + } + } +} + +impl TryFrom for VersionedResponse { + type Error = Error; + + fn try_from(value: InnerAvailableBandwidthResponse) -> Result { + Ok(Self { + query_type: QueryType::AvailableBandwidth, + inner: make_bincode_serializer().serialize(&value)?, + }) + } +} + +impl TryFrom for InnerAvailableBandwidthResponse { + type Error = crate::error::MetadataError; + + fn try_from(value: Response) -> Result { + VersionedResponse::try_from(value)? + .try_into() + .map_err(|err: Error| crate::error::MetadataError::Models { + message: err.to_string(), + }) + } +} + +impl TryFrom for Response { + type Error = crate::error::MetadataError; + + fn try_from(value: InnerAvailableBandwidthResponse) -> Result { + VersionedResponse::try_from(value)? + .try_into() + .map_err(|err: Error| crate::error::MetadataError::Models { + message: err.to_string(), + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn serde() { + let resp = InnerAvailableBandwidthResponse { + available_bandwidth: 42, + }; + let ser = VersionedResponse::try_from(resp).unwrap(); + assert_eq!(QueryType::AvailableBandwidth, ser.query_type); + let de = InnerAvailableBandwidthResponse::try_from(ser).unwrap(); + assert_eq!(resp, de); + } + + #[test] + fn invalid_content() { + let future_resp = VersionedResponse { + query_type: QueryType::AvailableBandwidth, + inner: vec![], + }; + assert!(InnerAvailableBandwidthResponse::try_from(future_resp).is_err()); + } +} diff --git a/common/wireguard-private-metadata/shared/src/models/v1/interface.rs b/common/wireguard-private-metadata/shared/src/models/v1/interface.rs new file mode 100644 index 0000000000..46cafddc7d --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/models/v1/interface.rs @@ -0,0 +1,224 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use nym_credentials_interface::CredentialSpendingData; + +#[cfg(feature = "testing")] +use super::super::v0 as previous; + +use super::{ + available_bandwidth::{ + request::InnerAvailableBandwidthRequest, response::InnerAvailableBandwidthResponse, + }, + topup_bandwidth::{request::InnerTopUpRequest, response::InnerTopUpResponse}, + QueryType, VersionedRequest, VersionedResponse, VERSION, +}; +use crate::models::{error::Error, Construct, Extract, Version}; + +#[derive(Debug, Clone, PartialEq)] +pub enum RequestData { + AvailableBandwidth(()), + TopUpBandwidth(Box), +} + +#[derive(Debug, Clone, PartialEq)] +pub enum ResponseData { + AvailableBandwidth(i64), + TopUpBandwidth(i64), +} + +impl Construct for VersionedRequest { + fn construct(info: RequestData, _version: Version) -> Result { + match info { + RequestData::AvailableBandwidth(_) => Ok(InnerAvailableBandwidthRequest {}.try_into()?), + RequestData::TopUpBandwidth(credential) => Ok(InnerTopUpRequest { + credential: *credential, + } + .try_into()?), + } + } +} + +impl Extract for VersionedRequest { + fn extract(&self) -> Result<(RequestData, Version), Error> { + match self.query_type { + QueryType::AvailableBandwidth => { + let _req = InnerAvailableBandwidthRequest::try_from(self.clone())?; + Ok((RequestData::AvailableBandwidth(()), VERSION)) + } + QueryType::TopupBandwidth => { + let req = InnerTopUpRequest::try_from(self.clone())?; + Ok(( + RequestData::TopUpBandwidth(Box::new(req.credential)), + VERSION, + )) + } + } + } +} + +impl Construct for VersionedResponse { + fn construct(info: ResponseData, _version: Version) -> Result { + match info { + ResponseData::AvailableBandwidth(available_bandwidth) => { + Ok(InnerAvailableBandwidthResponse { + available_bandwidth, + } + .try_into()?) + } + ResponseData::TopUpBandwidth(available_bandwidth) => Ok(InnerTopUpResponse { + available_bandwidth, + } + .try_into()?), + } + } +} + +impl Extract for VersionedResponse { + fn extract(&self) -> Result<(ResponseData, Version), Error> { + match self.query_type { + QueryType::AvailableBandwidth => { + let resp = InnerAvailableBandwidthResponse::try_from(self.clone())?; + Ok(( + ResponseData::AvailableBandwidth(resp.available_bandwidth), + VERSION, + )) + } + QueryType::TopupBandwidth => { + let resp = InnerTopUpResponse::try_from(self.clone())?; + Ok(( + ResponseData::TopUpBandwidth(resp.available_bandwidth), + VERSION, + )) + } + } + } +} + +// this should be with #[cfg(feature = "testing")] only coming from v0, don't copy this for future versions +#[cfg(feature = "testing")] +impl TryFrom for RequestData { + type Error = super::Error; + + fn try_from(value: previous::interface::RequestData) -> Result { + match value { + previous::interface::RequestData::AvailableBandwidth(inner) => { + Ok(Self::AvailableBandwidth(inner)) + } + previous::interface::RequestData::TopUpBandwidth(_) => { + Err(super::Error::UpdateNotPossible { + from: previous::VERSION, + to: VERSION, + }) + } + } + } +} + +// this should be with #[cfg(feature = "testing")] only coming from v0, don't copy this for future versions +#[cfg(feature = "testing")] +impl TryFrom for previous::interface::RequestData { + type Error = super::Error; + + fn try_from(value: RequestData) -> Result { + match value { + RequestData::AvailableBandwidth(inner) => Ok(Self::AvailableBandwidth(inner)), + RequestData::TopUpBandwidth(_) => Ok(Self::TopUpBandwidth(())), + } + } +} + +// this should be with #[cfg(feature = "testing")] only coming from v0, don't copy this for future versions +#[cfg(feature = "testing")] +impl TryFrom for ResponseData { + type Error = super::Error; + + fn try_from(value: previous::interface::ResponseData) -> Result { + match value { + previous::interface::ResponseData::AvailableBandwidth(_) => { + Err(super::Error::UpdateNotPossible { + from: previous::VERSION, + to: VERSION, + }) + } + previous::interface::ResponseData::TopUpBandwidth(_) => { + Err(super::Error::UpdateNotPossible { + from: previous::VERSION, + to: VERSION, + }) + } + } + } +} + +// this should be with #[cfg(feature = "testing")] only coming from v0, don't copy this for future versions +#[cfg(feature = "testing")] +impl TryFrom for previous::interface::ResponseData { + type Error = super::Error; + + fn try_from(value: ResponseData) -> Result { + match value { + ResponseData::AvailableBandwidth(_) => Ok(Self::AvailableBandwidth(())), + ResponseData::TopUpBandwidth(_) => Ok(Self::TopUpBandwidth(())), + } + } +} + +#[cfg(test)] +mod test { + use crate::models::tests::CREDENTIAL_BYTES; + + use super::*; + + #[test] + fn request_upgrade() { + assert_eq!( + RequestData::try_from(previous::interface::RequestData::AvailableBandwidth(())) + .unwrap(), + RequestData::AvailableBandwidth(()) + ); + assert!( + RequestData::try_from(previous::interface::RequestData::TopUpBandwidth(())).is_err(), + ); + } + + #[test] + fn response_upgrade() { + assert!( + ResponseData::try_from(previous::interface::ResponseData::AvailableBandwidth(())) + .is_err() + ); + assert!( + ResponseData::try_from(previous::interface::ResponseData::TopUpBandwidth(())).is_err() + ); + } + + #[test] + fn request_downgrade() { + assert_eq!( + previous::interface::RequestData::try_from(RequestData::AvailableBandwidth(())) + .unwrap(), + previous::interface::RequestData::AvailableBandwidth(()) + ); + assert_eq!( + previous::interface::RequestData::try_from(RequestData::TopUpBandwidth(Box::new( + CredentialSpendingData::try_from_bytes(&CREDENTIAL_BYTES).unwrap() + ))) + .unwrap(), + previous::interface::RequestData::TopUpBandwidth(()) + ); + } + + #[test] + fn response_downgrade() { + assert_eq!( + previous::interface::ResponseData::try_from(ResponseData::AvailableBandwidth(42)) + .unwrap(), + previous::interface::ResponseData::AvailableBandwidth(()) + ); + assert_eq!( + previous::interface::ResponseData::try_from(ResponseData::TopUpBandwidth(42)).unwrap(), + previous::interface::ResponseData::TopUpBandwidth(()) + ); + } +} diff --git a/common/wireguard-private-metadata/shared/src/models/v1/mod.rs b/common/wireguard-private-metadata/shared/src/models/v1/mod.rs new file mode 100644 index 0000000000..787a74f671 --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/models/v1/mod.rs @@ -0,0 +1,224 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use std::fmt::Display; + +use bincode::Options; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; + +use super::error::Error; +use crate::{ + make_bincode_serializer, + models::{Request, Response, Version}, +}; + +pub use available_bandwidth::{ + request::InnerAvailableBandwidthRequest as AvailableBandwidthRequest, + response::InnerAvailableBandwidthResponse as AvailableBandwidthResponse, +}; +pub use topup_bandwidth::{ + request::InnerTopUpRequest as TopUpRequest, response::InnerTopUpResponse as TopUpResponse, +}; + +pub(crate) mod available_bandwidth; +pub mod interface; +pub(crate) mod topup_bandwidth; + +pub const VERSION: Version = Version::V1; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)] +pub enum QueryType { + AvailableBandwidth, + TopupBandwidth, +} + +impl Display for QueryType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{self:?}") + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)] +pub struct VersionedRequest { + query_type: QueryType, + inner: Vec, +} + +impl TryFrom for Request { + type Error = Error; + + fn try_from(value: VersionedRequest) -> Result { + Ok(Request { + version: VERSION, + inner: make_bincode_serializer().serialize(&value)?, + }) + } +} + +impl TryFrom for VersionedRequest { + type Error = Error; + + fn try_from(value: Request) -> Result { + if value.version != VERSION { + return Err(Error::InvalidVersion { + source_version: value.version, + target_version: VERSION, + }); + } + Ok(make_bincode_serializer().deserialize(&value.inner)?) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)] +pub struct VersionedResponse { + query_type: QueryType, + inner: Vec, +} + +impl TryFrom for Response { + type Error = Error; + + fn try_from(value: VersionedResponse) -> Result { + Ok(Response { + version: VERSION, + inner: make_bincode_serializer().serialize(&value)?, + }) + } +} + +impl TryFrom for VersionedResponse { + type Error = Error; + + fn try_from(value: Response) -> Result { + if value.version != VERSION { + return Err(Error::InvalidVersion { + source_version: value.version, + target_version: VERSION, + }); + } + Ok(make_bincode_serializer().deserialize(&value.inner)?) + } +} + +#[cfg(test)] +mod tests { + + use nym_credentials_interface::CredentialSpendingData; + + use crate::models::tests::CREDENTIAL_BYTES; + + use self::{ + available_bandwidth::{ + request::InnerAvailableBandwidthRequest, response::InnerAvailableBandwidthResponse, + }, + topup_bandwidth::{request::InnerTopUpRequest, response::InnerTopUpResponse}, + }; + + use super::*; + + #[test] + fn mismatched_request_version() { + let version = Version::V0; + let future_bw = Request { + version, + inner: vec![], + }; + if let Err(Error::InvalidVersion { + source_version, + target_version, + }) = VersionedRequest::try_from(future_bw) + { + assert_eq!(source_version, version); + assert_eq!(target_version, VERSION); + } else { + panic!("failed"); + }; + } + + #[test] + fn mismatched_response_version() { + let version = Version::V0; + let future_bw = Response { + version, + inner: vec![], + }; + if let Err(Error::InvalidVersion { + source_version, + target_version, + }) = VersionedResponse::try_from(future_bw) + { + assert_eq!(source_version, version); + assert_eq!(target_version, VERSION); + } else { + panic!("failed"); + }; + } + + #[test] + fn serde_request_av_bw() { + let req = VersionedRequest { + query_type: QueryType::AvailableBandwidth, + inner: make_bincode_serializer() + .serialize(&InnerAvailableBandwidthResponse { + available_bandwidth: 42, + }) + .unwrap(), + }; + + let ser = Request::try_from(req.clone()).unwrap(); + assert_eq!(VERSION, ser.version); + let de = VersionedRequest::try_from(ser).unwrap(); + assert_eq!(req, de); + } + + #[test] + fn serde_response_av_bw() { + let resp = VersionedResponse { + query_type: QueryType::AvailableBandwidth, + inner: make_bincode_serializer() + .serialize(&InnerAvailableBandwidthRequest {}) + .unwrap(), + }; + + let ser = Response::try_from(resp.clone()).unwrap(); + assert_eq!(VERSION, ser.version); + let de = VersionedResponse::try_from(ser).unwrap(); + assert_eq!(resp, de); + } + + #[test] + fn serde_request_topup() { + let req = VersionedRequest { + query_type: QueryType::TopupBandwidth, + inner: make_bincode_serializer() + .serialize(&InnerTopUpRequest { + credential: CredentialSpendingData::try_from_bytes(&CREDENTIAL_BYTES).unwrap(), + }) + .unwrap(), + }; + + let ser = Request::try_from(req.clone()).unwrap(); + assert_eq!(VERSION, ser.version); + let de = VersionedRequest::try_from(ser).unwrap(); + assert_eq!(req, de); + } + + #[test] + fn serde_response_topup() { + let resp = VersionedResponse { + query_type: QueryType::TopupBandwidth, + inner: make_bincode_serializer() + .serialize(&InnerTopUpResponse { + available_bandwidth: 42, + }) + .unwrap(), + }; + + let ser = Response::try_from(resp.clone()).unwrap(); + assert_eq!(VERSION, ser.version); + let de = VersionedResponse::try_from(ser).unwrap(); + assert_eq!(resp, de); + } +} diff --git a/common/wireguard-private-metadata/shared/src/models/v1/topup_bandwidth/mod.rs b/common/wireguard-private-metadata/shared/src/models/v1/topup_bandwidth/mod.rs new file mode 100644 index 0000000000..10698cca2d --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/models/v1/topup_bandwidth/mod.rs @@ -0,0 +1,5 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod request; +pub mod response; diff --git a/common/wireguard-private-metadata/shared/src/models/v1/topup_bandwidth/request.rs b/common/wireguard-private-metadata/shared/src/models/v1/topup_bandwidth/request.rs new file mode 100644 index 0000000000..871cc127ef --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/models/v1/topup_bandwidth/request.rs @@ -0,0 +1,92 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use bincode::Options; +use nym_credentials_interface::CredentialSpendingData; +use serde::{Deserialize, Serialize}; + +use crate::{make_bincode_serializer, models::Request}; + +use super::super::{Error, QueryType, VersionedRequest}; + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct InnerTopUpRequest { + /// Ecash credential + pub credential: CredentialSpendingData, +} + +impl TryFrom for InnerTopUpRequest { + type Error = Error; + + fn try_from(value: VersionedRequest) -> Result { + match value.query_type { + QueryType::TopupBandwidth => Ok(make_bincode_serializer().deserialize(&value.inner)?), + QueryType::AvailableBandwidth => Err(Error::InvalidQueryType { + source_query_type: value.query_type.to_string(), + target_query_type: QueryType::TopupBandwidth.to_string(), + }), + } + } +} + +impl TryFrom for VersionedRequest { + type Error = Error; + + fn try_from(value: InnerTopUpRequest) -> Result { + Ok(Self { + query_type: QueryType::TopupBandwidth, + inner: make_bincode_serializer().serialize(&value)?, + }) + } +} + +impl TryFrom for InnerTopUpRequest { + type Error = crate::error::MetadataError; + + fn try_from(value: Request) -> Result { + VersionedRequest::try_from(value)? + .try_into() + .map_err(|err: Error| crate::error::MetadataError::Models { + message: err.to_string(), + }) + } +} + +impl TryFrom for Request { + type Error = crate::error::MetadataError; + + fn try_from(value: InnerTopUpRequest) -> Result { + VersionedRequest::try_from(value)? + .try_into() + .map_err(|err: Error| crate::error::MetadataError::Models { + message: err.to_string(), + }) + } +} + +#[cfg(test)] +mod tests { + use crate::models::tests::CREDENTIAL_BYTES; + + use super::*; + + #[test] + fn serde() { + let req = InnerTopUpRequest { + credential: CredentialSpendingData::try_from_bytes(&CREDENTIAL_BYTES).unwrap(), + }; + let ser = VersionedRequest::try_from(req.clone()).unwrap(); + assert_eq!(QueryType::TopupBandwidth, ser.query_type); + let de = InnerTopUpRequest::try_from(ser).unwrap(); + assert_eq!(req, de); + } + + #[test] + fn invalid_content() { + let future_req = VersionedRequest { + query_type: QueryType::TopupBandwidth, + inner: vec![], + }; + assert!(InnerTopUpRequest::try_from(future_req).is_err()); + } +} diff --git a/common/wireguard-private-metadata/shared/src/models/v1/topup_bandwidth/response.rs b/common/wireguard-private-metadata/shared/src/models/v1/topup_bandwidth/response.rs new file mode 100644 index 0000000000..08e0ef111f --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/models/v1/topup_bandwidth/response.rs @@ -0,0 +1,88 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use bincode::Options; +use serde::{Deserialize, Serialize}; + +use crate::{make_bincode_serializer, models::Response}; + +use super::super::{Error, QueryType, VersionedResponse}; + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct InnerTopUpResponse { + pub available_bandwidth: i64, +} + +impl TryFrom for InnerTopUpResponse { + type Error = Error; + + fn try_from(value: VersionedResponse) -> Result { + match value.query_type { + QueryType::TopupBandwidth => Ok(make_bincode_serializer().deserialize(&value.inner)?), + QueryType::AvailableBandwidth => Err(Error::InvalidQueryType { + source_query_type: value.query_type.to_string(), + target_query_type: QueryType::TopupBandwidth.to_string(), + }), + } + } +} + +impl TryFrom for VersionedResponse { + type Error = Error; + + fn try_from(value: InnerTopUpResponse) -> Result { + Ok(Self { + query_type: QueryType::TopupBandwidth, + inner: make_bincode_serializer().serialize(&value)?, + }) + } +} + +impl TryFrom for InnerTopUpResponse { + type Error = crate::error::MetadataError; + + fn try_from(value: Response) -> Result { + VersionedResponse::try_from(value)? + .try_into() + .map_err(|err: Error| crate::error::MetadataError::Models { + message: err.to_string(), + }) + } +} + +impl TryFrom for Response { + type Error = crate::error::MetadataError; + + fn try_from(value: InnerTopUpResponse) -> Result { + VersionedResponse::try_from(value)? + .try_into() + .map_err(|err: Error| crate::error::MetadataError::Models { + message: err.to_string(), + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn serde() { + let resp = InnerTopUpResponse { + available_bandwidth: 42, + }; + let ser = VersionedResponse::try_from(resp.clone()).unwrap(); + assert_eq!(QueryType::TopupBandwidth, ser.query_type); + let de = InnerTopUpResponse::try_from(ser).unwrap(); + assert_eq!(resp, de); + } + + #[test] + fn invalid_content() { + let future_resp = VersionedResponse { + query_type: QueryType::TopupBandwidth, + inner: vec![], + }; + assert!(InnerTopUpResponse::try_from(future_resp).is_err()); + } +} diff --git a/common/wireguard-private-metadata/shared/src/routes.rs b/common/wireguard-private-metadata/shared/src/routes.rs new file mode 100644 index 0000000000..bda615fe1c --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/routes.rs @@ -0,0 +1,10 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub const V1_API_VERSION: &str = "v1"; + +pub const BANDWIDTH: &str = "bandwidth"; + +pub const VERSION: &str = "version"; +pub const AVAILABLE: &str = "available"; +pub const TOPUP: &str = "topup"; diff --git a/common/wireguard-private-metadata/tests/Cargo.toml b/common/wireguard-private-metadata/tests/Cargo.toml new file mode 100644 index 0000000000..6827c16f60 --- /dev/null +++ b/common/wireguard-private-metadata/tests/Cargo.toml @@ -0,0 +1,41 @@ +[package] +name = "nym-wireguard-private-metadata-tests" +version = "1.0.0" +authors.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +async-trait = { workspace = true } +axum = { workspace = true, features = ["tokio", "macros"] } +nym-credential-verification = { path = "../../credential-verification" } +nym-credentials-interface = { path = "../../credentials-interface" } +nym-http-api-client = { path = "../../http-api-client" } +nym-http-api-common = { path = "../../http-api-common", features = [ + "middleware", + "utoipa", + "output", +] } +nym-wireguard = { path = "../../wireguard" } +tokio = { workspace = true, features = ["rt-multi-thread", "net", "io-util"] } +tower-http = { workspace = true, features = [ + "cors", + "trace", + "compression-br", + "compression-deflate", + "compression-gzip", + "compression-zstd", +] } +utoipa = { workspace = true, features = ["axum_extras", "time"] } + +nym-wireguard-private-metadata-client = { path = "../client" } +nym-wireguard-private-metadata-shared = { path = "../shared", features = [ + "testing", +] } +nym-wireguard-private-metadata-server = { path = "../server" } + +[lints] +workspace = true diff --git a/common/wireguard-private-metadata/tests/src/lib.rs b/common/wireguard-private-metadata/tests/src/lib.rs new file mode 100644 index 0000000000..1ae9ccdd93 --- /dev/null +++ b/common/wireguard-private-metadata/tests/src/lib.rs @@ -0,0 +1,217 @@ +#[cfg(test)] +mod v0; + +#[cfg(test)] +mod tests { + use std::net::SocketAddr; + + use nym_credential_verification::{ClientBandwidth, TicketVerifier}; + use nym_credentials_interface::CredentialSpendingData; + use nym_http_api_client::Client; + use nym_wireguard::{peer_controller::PeerControlRequest, CONTROL_CHANNEL_SIZE}; + use nym_wireguard_private_metadata_client::WireguardMetadataApiClient; + use nym_wireguard_private_metadata_server::{ + AppState, PeerControllerTransceiver, RouterBuilder, + }; + use nym_wireguard_private_metadata_shared::{latest, v0, v1}; + use tokio::{net::TcpListener, sync::mpsc}; + + pub(crate) const VERIFIER_AVAILABLE_BANDWIDTH: i64 = 42; + pub(crate) const CREDENTIAL_BYTES: [u8; 1245] = [ + 0, 0, 4, 133, 96, 179, 223, 185, 136, 23, 213, 166, 59, 203, 66, 69, 209, 181, 227, 254, + 16, 102, 98, 237, 59, 119, 170, 111, 31, 194, 51, 59, 120, 17, 115, 229, 79, 91, 11, 139, + 154, 2, 212, 23, 68, 70, 167, 3, 240, 54, 224, 171, 221, 1, 69, 48, 60, 118, 119, 249, 123, + 35, 172, 227, 131, 96, 232, 209, 187, 123, 4, 197, 102, 90, 96, 45, 125, 135, 140, 99, 1, + 151, 17, 131, 143, 157, 97, 107, 139, 232, 212, 87, 14, 115, 253, 255, 166, 167, 186, 43, + 90, 96, 173, 105, 120, 40, 10, 163, 250, 224, 214, 200, 178, 4, 160, 16, 130, 59, 76, 193, + 39, 240, 3, 101, 141, 209, 183, 226, 186, 207, 56, 210, 187, 7, 164, 240, 164, 205, 37, 81, + 184, 214, 193, 195, 90, 205, 238, 225, 195, 104, 12, 123, 203, 57, 233, 243, 215, 145, 195, + 196, 57, 38, 125, 172, 18, 47, 63, 165, 110, 219, 180, 40, 58, 116, 92, 254, 160, 98, 48, + 92, 254, 232, 107, 184, 80, 234, 60, 160, 235, 249, 76, 41, 38, 165, 28, 40, 136, 74, 48, + 166, 50, 245, 23, 201, 140, 101, 79, 93, 235, 128, 186, 146, 126, 180, 134, 43, 13, 186, + 19, 195, 48, 168, 201, 29, 216, 95, 176, 198, 132, 188, 64, 39, 212, 150, 32, 52, 53, 38, + 228, 199, 122, 226, 217, 75, 40, 191, 151, 48, 164, 242, 177, 79, 14, 122, 105, 151, 85, + 88, 199, 162, 17, 96, 103, 83, 178, 128, 9, 24, 30, 74, 108, 241, 85, 240, 166, 97, 241, + 85, 199, 11, 198, 226, 234, 70, 107, 145, 28, 208, 114, 51, 12, 234, 108, 101, 202, 112, + 48, 185, 22, 159, 67, 109, 49, 27, 149, 90, 109, 32, 226, 112, 7, 201, 208, 209, 104, 31, + 97, 134, 204, 145, 27, 181, 206, 181, 106, 32, 110, 136, 115, 249, 201, 111, 5, 245, 203, + 71, 121, 169, 126, 151, 178, 236, 59, 221, 195, 48, 135, 115, 6, 50, 227, 74, 97, 107, 107, + 213, 90, 2, 203, 154, 138, 47, 128, 52, 134, 128, 224, 51, 65, 240, 90, 8, 55, 175, 180, + 178, 204, 206, 168, 110, 51, 57, 189, 169, 48, 169, 136, 121, 99, 51, 170, 178, 214, 74, 1, + 96, 151, 167, 25, 173, 180, 171, 155, 10, 55, 142, 234, 190, 113, 90, 79, 80, 244, 71, 166, + 30, 235, 113, 150, 133, 1, 218, 17, 109, 111, 223, 24, 216, 177, 41, 2, 204, 65, 221, 212, + 207, 236, 144, 6, 65, 224, 55, 42, 1, 1, 161, 134, 118, 127, 111, 220, 110, 127, 240, 71, + 223, 129, 12, 93, 20, 220, 60, 56, 71, 146, 184, 95, 132, 69, 28, 56, 53, 192, 213, 22, + 119, 230, 152, 225, 182, 188, 163, 219, 37, 175, 247, 73, 14, 247, 38, 72, 243, 1, 48, 131, + 59, 8, 13, 96, 143, 185, 127, 241, 161, 217, 24, 149, 193, 40, 16, 30, 202, 151, 28, 119, + 240, 153, 101, 156, 61, 193, 72, 245, 199, 181, 12, 231, 65, 166, 67, 142, 121, 207, 202, + 58, 197, 113, 188, 248, 42, 124, 105, 48, 161, 241, 55, 209, 36, 194, 27, 63, 233, 144, + 189, 85, 117, 234, 9, 139, 46, 31, 206, 114, 95, 131, 29, 240, 13, 81, 142, 140, 133, 33, + 30, 41, 141, 37, 80, 217, 95, 221, 76, 115, 86, 201, 165, 51, 252, 9, 28, 209, 1, 48, 150, + 74, 248, 212, 187, 222, 66, 210, 3, 200, 19, 217, 171, 184, 42, 148, 53, 150, 57, 50, 6, + 227, 227, 62, 49, 42, 148, 148, 157, 82, 191, 58, 24, 34, 56, 98, 120, 89, 105, 176, 85, + 15, 253, 241, 41, 153, 195, 136, 1, 48, 142, 126, 213, 101, 223, 79, 133, 230, 105, 38, + 161, 149, 2, 21, 136, 150, 42, 72, 218, 85, 146, 63, 223, 58, 108, 186, 183, 248, 62, 20, + 47, 34, 113, 160, 177, 204, 181, 16, 24, 212, 224, 35, 84, 51, 168, 56, 136, 11, 1, 48, + 135, 242, 62, 149, 230, 178, 32, 224, 119, 26, 234, 163, 237, 224, 114, 95, 112, 140, 170, + 150, 96, 125, 136, 221, 180, 78, 18, 11, 12, 184, 2, 198, 217, 119, 43, 69, 4, 172, 109, + 55, 183, 40, 131, 172, 161, 88, 183, 101, 1, 48, 173, 216, 22, 73, 42, 255, 211, 93, 249, + 87, 159, 115, 61, 91, 55, 130, 17, 216, 60, 34, 122, 55, 8, 244, 244, 153, 151, 57, 5, 144, + 178, 55, 249, 64, 211, 168, 34, 148, 56, 89, 92, 203, 70, 124, 219, 152, 253, 165, 0, 32, + 203, 116, 63, 7, 240, 222, 82, 86, 11, 149, 167, 72, 224, 55, 190, 66, 201, 65, 168, 184, + 96, 47, 194, 241, 168, 124, 7, 74, 214, 250, 37, 76, 32, 218, 69, 122, 103, 215, 145, 169, + 24, 212, 229, 168, 106, 10, 144, 31, 13, 25, 178, 242, 250, 106, 159, 40, 48, 163, 165, 61, + 130, 57, 146, 4, 73, 32, 254, 233, 125, 135, 212, 29, 111, 4, 177, 114, 15, 210, 170, 82, + 108, 110, 62, 166, 81, 209, 106, 176, 156, 14, 133, 242, 60, 127, 120, 242, 28, 97, 0, 1, + 32, 103, 93, 109, 89, 240, 91, 1, 84, 150, 50, 206, 157, 203, 49, 220, 120, 234, 175, 234, + 150, 126, 225, 94, 163, 164, 199, 138, 114, 62, 99, 106, 112, 1, 32, 171, 40, 220, 82, 241, + 203, 76, 146, 111, 139, 182, 179, 237, 182, 115, 75, 128, 201, 107, 43, 214, 0, 135, 217, + 160, 68, 150, 232, 144, 114, 237, 98, 32, 30, 134, 232, 59, 93, 163, 253, 244, 13, 202, 52, + 147, 168, 83, 121, 123, 95, 21, 210, 209, 225, 223, 143, 49, 10, 205, 238, 1, 22, 83, 81, + 70, 1, 32, 26, 76, 6, 234, 160, 50, 139, 102, 161, 232, 155, 106, 130, 171, 226, 210, 233, + 178, 85, 247, 71, 123, 55, 53, 46, 67, 148, 137, 156, 207, 208, 107, 1, 32, 102, 31, 4, 98, + 110, 156, 144, 61, 229, 140, 198, 84, 196, 238, 128, 35, 131, 182, 137, 125, 241, 95, 69, + 131, 170, 27, 2, 144, 75, 72, 242, 102, 3, 32, 121, 80, 45, 173, 56, 65, 218, 27, 40, 251, + 197, 32, 169, 104, 123, 110, 90, 78, 153, 166, 38, 9, 129, 228, 99, 8, 1, 116, 142, 233, + 162, 69, 32, 216, 169, 159, 116, 95, 12, 63, 176, 195, 6, 183, 123, 135, 75, 61, 112, 106, + 83, 235, 176, 41, 27, 248, 48, 71, 165, 170, 12, 92, 103, 103, 81, 32, 58, 74, 75, 145, + 192, 94, 153, 69, 80, 128, 241, 3, 16, 117, 192, 86, 161, 103, 44, 174, 211, 196, 182, 124, + 55, 11, 107, 142, 49, 88, 6, 41, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 0, 37, 139, 240, 0, 0, + 0, 0, 0, 0, 0, 1, + ]; + + pub(crate) struct MockVerifier { + ret: i64, + } + + impl MockVerifier { + pub(crate) fn new(ret: i64) -> MockVerifier { + Self { ret } + } + } + + #[async_trait::async_trait] + impl TicketVerifier for MockVerifier { + async fn verify(&mut self) -> nym_credential_verification::Result { + Ok(self.ret) + } + } + + pub(crate) async fn spawn_server_and_create_client() -> Client { + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let addr = listener.local_addr().unwrap(); + let (request_tx, mut request_rx) = mpsc::channel(CONTROL_CHANNEL_SIZE); + let router = RouterBuilder::with_default_routes() + .with_state(AppState::new(PeerControllerTransceiver::new(request_tx))) + .router; + + tokio::spawn(async move { + loop { + match request_rx.recv().await { + Some(PeerControlRequest::GetClientBandwidthByIp { ip: _, response_tx }) => { + response_tx + .send(Ok(ClientBandwidth::new(Default::default()))) + .ok(); + } + Some(PeerControlRequest::GetVerifierByIp { + ip: _, + credential: _, + response_tx, + }) => { + response_tx + .send(Ok(Box::new(MockVerifier::new( + VERIFIER_AVAILABLE_BANDWIDTH, + )))) + .ok(); + } + None => break, + _ => panic!("Not expected"), + } + } + }); + + tokio::spawn(async move { + axum::serve( + listener, + router.into_make_service_with_connect_info::(), + ) + .await + .unwrap(); + }); + Client::new_url(addr.to_string(), None).unwrap() + } + + #[tokio::test] + async fn query_latest_version() { + let client = spawn_server_and_create_client().await; + let version = client.version().await.unwrap(); + assert_eq!(version, latest::VERSION); + } + + #[tokio::test] + async fn query_against_server_v0() { + let client = super::v0::network::test::spawn_server_and_create_client().await; + + // version check + let version = client.version().await.unwrap(); + assert_eq!(version, v0::VERSION); + + // v0 reqwests + let request = v0::AvailableBandwidthRequest {}.try_into().unwrap(); + let response = client.available_bandwidth(&request).await.unwrap(); + v0::AvailableBandwidthResponse::try_from(response).unwrap(); + + let request = v0::TopUpRequest {}.try_into().unwrap(); + let response = client.topup_bandwidth(&request).await.unwrap(); + v0::TopUpResponse::try_from(response).unwrap(); + + // v1 reqwests + let request = v1::AvailableBandwidthRequest {}.try_into().unwrap(); + assert!(client.available_bandwidth(&request).await.is_err()); + + let request = v1::TopUpRequest { + credential: CredentialSpendingData::try_from_bytes(&CREDENTIAL_BYTES).unwrap(), + } + .try_into() + .unwrap(); + assert!(client.topup_bandwidth(&request).await.is_err()); + } + + #[tokio::test] + async fn query_against_server_v1() { + let client = spawn_server_and_create_client().await; + + // version check + let version = client.version().await.unwrap(); + assert_eq!(version, v1::VERSION); + + // v0 reqwests + let request = v0::AvailableBandwidthRequest {}.try_into().unwrap(); + let response = client.available_bandwidth(&request).await.unwrap(); + v0::AvailableBandwidthResponse::try_from(response).unwrap(); + + let request = v0::TopUpRequest {}.try_into().unwrap(); + assert!(client.topup_bandwidth(&request).await.is_err()); + + // v1 reqwests + let request = v1::AvailableBandwidthRequest {}.try_into().unwrap(); + let response = client.available_bandwidth(&request).await.unwrap(); + let available_bandwidth = v1::AvailableBandwidthResponse::try_from(response) + .unwrap() + .available_bandwidth; + assert_eq!(available_bandwidth, 0); + + let request = v1::TopUpRequest { + credential: CredentialSpendingData::try_from_bytes(&CREDENTIAL_BYTES).unwrap(), + } + .try_into() + .unwrap(); + let response = client.topup_bandwidth(&request).await.unwrap(); + let available_bandwidth = v1::TopUpResponse::try_from(response) + .unwrap() + .available_bandwidth; + assert_eq!(available_bandwidth, VERIFIER_AVAILABLE_BANDWIDTH); + } +} diff --git a/common/wireguard-private-metadata/tests/src/v0/interface.rs b/common/wireguard-private-metadata/tests/src/v0/interface.rs new file mode 100644 index 0000000000..77cd2d12fe --- /dev/null +++ b/common/wireguard-private-metadata/tests/src/v0/interface.rs @@ -0,0 +1,150 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use nym_wireguard_private_metadata_shared::{ + v0 as latest, Construct, Extract, Request, Response, Version, +}; + +pub enum RequestData { + AvailableBandwidth(()), + TopUpBandwidth(()), +} + +impl From for RequestData { + fn from(value: latest::interface::RequestData) -> Self { + match value { + latest::interface::RequestData::AvailableBandwidth(inner) => { + Self::AvailableBandwidth(inner) + } + latest::interface::RequestData::TopUpBandwidth(credential_spending_data) => { + Self::TopUpBandwidth(credential_spending_data) + } + } + } +} + +impl From for latest::interface::RequestData { + fn from(value: RequestData) -> Self { + match value { + RequestData::AvailableBandwidth(inner) => Self::AvailableBandwidth(inner), + RequestData::TopUpBandwidth(credential_spending_data) => { + Self::TopUpBandwidth(credential_spending_data) + } + } + } +} + +impl From for ResponseData { + fn from(value: latest::interface::ResponseData) -> Self { + match value { + latest::interface::ResponseData::AvailableBandwidth(inner) => { + Self::AvailableBandwidth(inner) + } + latest::interface::ResponseData::TopUpBandwidth(credential_spending_data) => { + Self::TopUpBandwidth(credential_spending_data) + } + } + } +} + +impl From for latest::interface::ResponseData { + fn from(value: ResponseData) -> Self { + match value { + ResponseData::AvailableBandwidth(inner) => Self::AvailableBandwidth(inner), + ResponseData::TopUpBandwidth(credential_spending_data) => { + Self::TopUpBandwidth(credential_spending_data) + } + } + } +} + +impl Construct for Request { + fn construct( + info: RequestData, + version: Version, + ) -> Result { + match version { + Version::V0 => { + let translate_info = latest::interface::RequestData::from(info); + let versioned_request = + latest::VersionedRequest::construct(translate_info, latest::VERSION)?; + Ok(versioned_request.try_into()?) + } + _ => Err( + nym_wireguard_private_metadata_shared::ModelError::DowngradeNotPossible { + from: version, + to: Version::V0, + }, + ), + } + } +} + +impl Extract for Request { + fn extract( + &self, + ) -> Result<(RequestData, Version), nym_wireguard_private_metadata_shared::ModelError> { + match self.version { + Version::V0 => { + let versioned_request = latest::VersionedRequest::try_from(self.clone())?; + let (request, version) = versioned_request.extract()?; + + Ok((request.into(), version)) + } + _ => Err( + nym_wireguard_private_metadata_shared::ModelError::UpdateNotPossible { + from: self.version, + to: Version::V0, + }, + ), + } + } +} + +pub enum ResponseData { + AvailableBandwidth(()), + TopUpBandwidth(()), +} + +impl Construct for Response { + fn construct( + info: ResponseData, + version: Version, + ) -> Result { + match version { + Version::V0 => { + let translate_response = latest::interface::ResponseData::from(info); + let versioned_response = + latest::VersionedResponse::construct(translate_response, version)?; + Ok(versioned_response.try_into()?) + } + _ => Err( + nym_wireguard_private_metadata_shared::ModelError::DowngradeNotPossible { + from: version, + to: Version::V0, + }, + ), + } + } +} + +impl Extract for Response { + fn extract( + &self, + ) -> Result<(ResponseData, Version), nym_wireguard_private_metadata_shared::ModelError> { + match self.version { + Version::V0 => { + let versioned_response = latest::VersionedResponse::try_from(self.clone())?; + let (response, version) = versioned_response.extract()?; + + Ok((response.into(), version)) + } + _ => Err( + nym_wireguard_private_metadata_shared::ModelError::UpdateNotPossible { + from: self.version, + to: Version::V0, + }, + ), + } + } +} diff --git a/common/wireguard-private-metadata/tests/src/v0/mod.rs b/common/wireguard-private-metadata/tests/src/v0/mod.rs new file mode 100644 index 0000000000..7338cc5ae2 --- /dev/null +++ b/common/wireguard-private-metadata/tests/src/v0/mod.rs @@ -0,0 +1,2 @@ +pub(crate) mod interface; +pub(crate) mod network; diff --git a/common/wireguard-private-metadata/tests/src/v0/network.rs b/common/wireguard-private-metadata/tests/src/v0/network.rs new file mode 100644 index 0000000000..0be32d66dc --- /dev/null +++ b/common/wireguard-private-metadata/tests/src/v0/network.rs @@ -0,0 +1,145 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +#[cfg(test)] +pub(crate) mod test { + use std::net::SocketAddr; + + use crate::{ + tests::{MockVerifier, VERIFIER_AVAILABLE_BANDWIDTH}, + v0::interface::{RequestData, ResponseData}, + }; + use axum::{extract::Query, Json, Router}; + use nym_credential_verification::ClientBandwidth; + use nym_http_api_client::Client; + use nym_http_api_common::{FormattedResponse, OutputParams}; + use nym_wireguard::{peer_controller::PeerControlRequest, CONTROL_CHANNEL_SIZE}; + use nym_wireguard_private_metadata_server::PeerControllerTransceiver; + use nym_wireguard_private_metadata_shared::{ + v0 as latest, AxumErrorResponse, AxumResult, Construct, Extract, Request, Response, + }; + use tokio::{net::TcpListener, sync::mpsc}; + use tower_http::compression::CompressionLayer; + + use nym_wireguard_private_metadata_server::AppState; + + fn bandwidth_routes() -> Router { + Router::new() + .route("/version", axum::routing::get(version)) + .route("/available", axum::routing::post(available_bandwidth)) + .route("/topup", axum::routing::post(topup_bandwidth)) + .layer(CompressionLayer::new()) + } + + #[utoipa::path( + tag = "bandwidth", + get, + path = "/v1/bandwidth/version", + responses( + (status = 200, content( + (Response = "application/bincode") + )) + ), +)] + async fn version(Query(output): Query) -> AxumResult> { + let output = output.output.unwrap_or_default(); + Ok(output.to_response(latest::VERSION.into())) + } + + #[utoipa::path( + tag = "bandwidth", + post, + request_body = Request, + path = "/v1/bandwidth/available", + responses( + (status = 200, content( + (Response = "application/bincode") + )) + ), +)] + async fn available_bandwidth( + Query(output): Query, + Json(request): Json, + ) -> AxumResult> { + let output = output.output.unwrap_or_default(); + + let (RequestData::AvailableBandwidth(_), version) = + request.extract().map_err(AxumErrorResponse::bad_request)? + else { + return Err(AxumErrorResponse::bad_request("incorrect request type")); + }; + let response = Response::construct(ResponseData::AvailableBandwidth(()), version) + .map_err(AxumErrorResponse::bad_request)?; + + Ok(output.to_response(response)) + } + + #[utoipa::path( + tag = "bandwidth", + post, + request_body = Request, + path = "/v1/bandwidth/topup", + responses( + (status = 200, content( + (Response = "application/bincode") + )) + ), +)] + async fn topup_bandwidth( + Query(output): Query, + Json(request): Json, + ) -> AxumResult> { + let output = output.output.unwrap_or_default(); + + let (RequestData::TopUpBandwidth(_), version) = + request.extract().map_err(AxumErrorResponse::bad_request)? + else { + return Err(AxumErrorResponse::bad_request("incorrect request type")); + }; + let response = Response::construct(ResponseData::TopUpBandwidth(()), version) + .map_err(AxumErrorResponse::bad_request)?; + + Ok(output.to_response(response)) + } + + pub(crate) async fn spawn_server_and_create_client() -> Client { + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let addr = listener.local_addr().unwrap(); + let (request_tx, mut request_rx) = mpsc::channel(CONTROL_CHANNEL_SIZE); + let router = Router::new() + .nest("/v1", Router::new().nest("/bandwidth", bandwidth_routes())) + .with_state(AppState::new(PeerControllerTransceiver::new(request_tx))); + + tokio::spawn(async move { + match request_rx.recv().await.unwrap() { + PeerControlRequest::GetClientBandwidthByIp { ip: _, response_tx } => { + response_tx + .send(Ok(ClientBandwidth::new(Default::default()))) + .ok(); + } + PeerControlRequest::GetVerifierByIp { + ip: _, + credential: _, + response_tx, + } => { + response_tx + .send(Ok(Box::new(MockVerifier::new( + VERIFIER_AVAILABLE_BANDWIDTH, + )))) + .ok(); + } + _ => panic!("Not expected"), + } + }); + + tokio::spawn(async move { + axum::serve( + listener, + router.into_make_service_with_connect_info::(), + ) + .await + .unwrap(); + }); + Client::new_url(addr.to_string(), None).unwrap() + } +} diff --git a/common/wireguard-types/src/config.rs b/common/wireguard-types/src/config.rs index fda6fc70f3..e322904d2e 100644 --- a/common/wireguard-types/src/config.rs +++ b/common/wireguard-types/src/config.rs @@ -17,9 +17,13 @@ pub struct Config { /// default: `fc01::1` pub private_ipv6: Ipv6Addr, - /// Port announced to external clients wishing to connect to the wireguard interface. + /// Tunnel 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, + pub announced_tunnel_port: u16, + + /// Metadata port announced to external clients wishing to connect to the endpoint. + /// Useful in the instances where the node is behind a proxy. + pub announced_metadata_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 diff --git a/common/wireguard/src/lib.rs b/common/wireguard/src/lib.rs index 7716880fdd..5bbce1b4b8 100644 --- a/common/wireguard/src/lib.rs +++ b/common/wireguard/src/lib.rs @@ -18,11 +18,13 @@ use tokio::sync::mpsc::{self, Receiver, Sender}; #[cfg(target_os = "linux")] use nym_network_defaults::constants::WG_TUN_BASE_NAME; -pub(crate) mod error; +pub mod error; pub mod peer_controller; pub mod peer_handle; pub mod peer_storage_manager; +pub const CONTROL_CHANNEL_SIZE: usize = 256; + pub struct WgApiWrapper { inner: WGApi, } @@ -126,7 +128,7 @@ pub struct WireguardGatewayData { impl WireguardGatewayData { pub fn new(config: Config, keypair: Arc) -> (Self, Receiver) { - let (peer_tx, peer_rx) = mpsc::channel(1); + let (peer_tx, peer_rx) = mpsc::channel(CONTROL_CHANNEL_SIZE); ( WireguardGatewayData { config, @@ -161,7 +163,7 @@ pub async fn start_wireguard( ecash_manager: Arc, metrics: nym_node_metrics::NymNodeMetrics, peers: Vec, - task_client: nym_task::TaskClient, + shutdown_token: nym_task::ShutdownToken, wireguard_data: WireguardData, ) -> Result, Box> { use base64::{prelude::BASE64_STANDARD, Engine}; @@ -178,10 +180,16 @@ pub async fn start_wireguard( let mut peer_bandwidth_managers = HashMap::with_capacity(peers.len()); for peer in peers.iter() { - let bandwidth_manager = Arc::new(RwLock::new( - PeerController::generate_bandwidth_manager(ecash_manager.storage(), &peer.public_key) + let bandwidth_manager = peer_handle::SharedBandwidthStorageManager::new( + Arc::new(RwLock::new( + PeerController::generate_bandwidth_manager( + ecash_manager.storage(), + &peer.public_key, + ) .await?, - )); + )), + peer.allowed_ips.clone(), + ); peer_bandwidth_managers.insert(peer.public_key.clone(), (bandwidth_manager, peer.clone())); } @@ -190,7 +198,7 @@ pub async fn start_wireguard( name: ifname.clone(), prvkey: BASE64_STANDARD.encode(wireguard_data.inner.keypair().private_key().to_bytes()), address: wireguard_data.inner.config().private_ipv4.to_string(), - port: wireguard_data.inner.config().announced_port as u32, + port: wireguard_data.inner.config().announced_tunnel_port as u32, peers, mtu: None, }; @@ -233,6 +241,7 @@ pub async fn start_wireguard( let host = wg_api.read_interface_data()?; let wg_api = std::sync::Arc::new(WgApiWrapper::new(wg_api)); + let mut controller = PeerController::new( ecash_manager, metrics, @@ -241,7 +250,7 @@ pub async fn start_wireguard( peer_bandwidth_managers, wireguard_data.inner.peer_tx.clone(), wireguard_data.peer_rx, - task_client, + shutdown_token, ); tokio::spawn(async move { controller.run().await }); diff --git a/common/wireguard/src/peer_controller.rs b/common/wireguard/src/peer_controller.rs index 8708a9c6df..4b294703fb 100644 --- a/common/wireguard/src/peer_controller.rs +++ b/common/wireguard/src/peer_controller.rs @@ -10,15 +10,18 @@ use futures::channel::oneshot; use log::info; use nym_credential_verification::{ bandwidth_storage_manager::BandwidthStorageManager, ecash::traits::EcashManager, - BandwidthFlushingBehaviourConfig, ClientBandwidth, CredentialVerifier, + BandwidthFlushingBehaviourConfig, ClientBandwidth, CredentialVerifier, TicketVerifier, }; use nym_credentials_interface::CredentialSpendingData; use nym_gateway_requests::models::CredentialSpendingRequest; use nym_gateway_storage::traits::BandwidthGatewayStorage; use nym_node_metrics::NymNodeMetrics; use nym_wireguard_types::DEFAULT_PEER_TIMEOUT_CHECK; -use std::time::{Duration, SystemTime}; use std::{collections::HashMap, sync::Arc}; +use std::{ + net::IpAddr, + time::{Duration, SystemTime}, +}; use tokio::sync::{mpsc, RwLock}; use tokio_stream::{wrappers::IntervalStream, StreamExt}; @@ -41,22 +44,31 @@ pub enum PeerControlRequest { key: Key, response_tx: oneshot::Sender, }, - GetClientBandwidth { + GetClientBandwidthByKey { key: Key, response_tx: oneshot::Sender, }, - GetVerifier { + GetClientBandwidthByIp { + ip: IpAddr, + response_tx: oneshot::Sender, + }, + GetVerifierByKey { key: Key, credential: Box, response_tx: oneshot::Sender, }, + GetVerifierByIp { + ip: IpAddr, + credential: Box, + response_tx: oneshot::Sender, + }, } pub type AddPeerControlResponse = Result<()>; pub type RemovePeerControlResponse = Result<()>; pub type QueryPeerControlResponse = Result>; pub type GetClientBandwidthControlResponse = Result; -pub type QueryVerifierControlResponse = Result; +pub type QueryVerifierControlResponse = Result>; pub struct PeerController { ecash_verifier: Arc, @@ -72,12 +84,12 @@ pub struct PeerController { host_information: Arc>, bw_storage_managers: HashMap, timeout_check_interval: IntervalStream, - task_client: nym_task::TaskClient, + shutdown_token: nym_task::ShutdownToken, } impl PeerController { #[allow(clippy::too_many_arguments)] - pub fn new( + pub(crate) fn new( ecash_verifier: Arc, metrics: NymNodeMetrics, wg_api: Arc, @@ -85,11 +97,10 @@ impl PeerController { bw_storage_managers: HashMap, request_tx: mpsc::Sender, request_rx: mpsc::Receiver, - task_client: nym_task::TaskClient, + shutdown_token: nym_task::ShutdownToken, ) -> Self { - let timeout_check_interval = tokio_stream::wrappers::IntervalStream::new( - tokio::time::interval(DEFAULT_PEER_TIMEOUT_CHECK), - ); + let timeout_check_interval = + IntervalStream::new(tokio::time::interval(DEFAULT_PEER_TIMEOUT_CHECK)); let host_information = Arc::new(RwLock::new(initial_host_information)); for (public_key, (bandwidth_storage_manager, peer)) in bw_storage_managers.iter() { let cached_peer_manager = CachedPeerManager::new(peer); @@ -99,7 +110,7 @@ impl PeerController { cached_peer_manager, bandwidth_storage_manager.clone(), request_tx.clone(), - &task_client, + &shutdown_token, ); let public_key = public_key.clone(); tokio::spawn(async move { @@ -120,7 +131,7 @@ impl PeerController { request_tx, request_rx, timeout_check_interval, - task_client, + shutdown_token, metrics, } } @@ -165,10 +176,13 @@ impl PeerController { async fn handle_add_request(&mut self, peer: &Peer) -> Result<()> { self.wg_api.configure_peer(peer)?; - let bandwidth_storage_manager = Arc::new(RwLock::new( - Self::generate_bandwidth_manager(self.ecash_verifier.storage(), &peer.public_key) - .await?, - )); + let bandwidth_storage_manager = SharedBandwidthStorageManager::new( + Arc::new(RwLock::new( + Self::generate_bandwidth_manager(self.ecash_verifier.storage(), &peer.public_key) + .await?, + )), + peer.allowed_ips.clone(), + ); let cached_peer_manager = CachedPeerManager::new(peer); let mut handle = PeerHandle::new( peer.public_key.clone(), @@ -176,7 +190,7 @@ impl PeerController { cached_peer_manager, bandwidth_storage_manager.clone(), self.request_tx.clone(), - &self.task_client, + &self.shutdown_token, ); self.bw_storage_managers .insert(peer.public_key.clone(), bandwidth_storage_manager); @@ -192,7 +206,20 @@ impl PeerController { Ok(()) } - async fn handle_query_peer(&self, key: &Key) -> Result> { + async fn ip_to_key(&self, ip: IpAddr) -> Result> { + Ok(self + .bw_storage_managers + .iter() + .find_map(|(key, bw_manager)| { + bw_manager + .allowed_ips() + .iter() + .find(|ip_mask| ip_mask.ip == ip) + .and(Some(key.clone())) + })) + } + + async fn handle_query_peer_by_key(&self, key: &Key) -> Result> { Ok(self .ecash_verifier .storage() @@ -202,20 +229,32 @@ impl PeerController { .transpose()?) } - async fn handle_get_client_bandwidth(&self, key: &Key) -> Result { + async fn handle_get_client_bandwidth_by_key(&self, key: &Key) -> Result { let bandwidth_storage_manager = self .bw_storage_managers .get(key) .ok_or(Error::MissingClientBandwidthEntry)?; - Ok(bandwidth_storage_manager.read().await.client_bandwidth()) + Ok(bandwidth_storage_manager + .inner() + .read() + .await + .client_bandwidth()) } - async fn handle_query_verifier( + async fn handle_get_client_bandwidth_by_ip(&self, ip: IpAddr) -> Result { + let Some(key) = self.ip_to_key(ip).await? else { + return Err(Error::MissingClientKernelEntry(ip.to_string())); + }; + + self.handle_get_client_bandwidth_by_key(&key).await + } + + async fn handle_query_verifier_by_key( &self, key: &Key, credential: CredentialSpendingData, - ) -> Result { + ) -> Result> { let storage = self.ecash_verifier.storage(); let client_id = storage .get_wireguard_peer(&key.to_string()) @@ -225,7 +264,11 @@ impl PeerController { let Some(bandwidth_storage_manager) = self.bw_storage_managers.get(key) else { return Err(Error::MissingClientBandwidthEntry); }; - let client_bandwidth = bandwidth_storage_manager.read().await.client_bandwidth(); + let client_bandwidth = bandwidth_storage_manager + .inner() + .read() + .await + .client_bandwidth(); let verifier = CredentialVerifier::new( CredentialSpendingRequest::new(credential), self.ecash_verifier.clone(), @@ -237,7 +280,19 @@ impl PeerController { true, ), ); - Ok(verifier) + Ok(Box::new(verifier)) + } + + async fn handle_query_verifier_by_ip( + &self, + ip: IpAddr, + credential: CredentialSpendingData, + ) -> Result> { + let Some(key) = self.ip_to_key(ip).await? else { + return Err(Error::MissingClientKernelEntry(ip.to_string())); + }; + + self.handle_query_verifier_by_key(&key, credential).await } async fn update_metrics(&self, new_host: &Host) { @@ -327,7 +382,7 @@ impl PeerController { *self.host_information.write().await = host; } - _ = self.task_client.recv() => { + _ = self.shutdown_token.cancelled() => { log::trace!("PeerController handler: Received shutdown"); break; } @@ -340,18 +395,23 @@ impl PeerController { response_tx.send(self.remove_peer(&key).await).ok(); } Some(PeerControlRequest::QueryPeer { key, response_tx }) => { - response_tx.send(self.handle_query_peer(&key).await).ok(); + response_tx.send(self.handle_query_peer_by_key(&key).await).ok(); } - Some(PeerControlRequest::GetClientBandwidth { key, response_tx }) => { - response_tx.send(self.handle_get_client_bandwidth(&key).await).ok(); + Some(PeerControlRequest::GetClientBandwidthByKey { key, response_tx }) => { + response_tx.send(self.handle_get_client_bandwidth_by_key(&key).await).ok(); } - Some(PeerControlRequest::GetVerifier { key, credential, response_tx }) => { - response_tx.send(self.handle_query_verifier(&key, *credential).await).ok(); + Some(PeerControlRequest::GetClientBandwidthByIp { ip, response_tx }) => { + response_tx.send(self.handle_get_client_bandwidth_by_ip(ip).await).ok(); + } + Some(PeerControlRequest::GetVerifierByKey { key, credential, response_tx }) => { + response_tx.send(self.handle_query_verifier_by_key(&key, *credential).await).ok(); + } + Some(PeerControlRequest::GetVerifierByIp { ip, credential, response_tx }) => { + response_tx.send(self.handle_query_verifier_by_ip(ip, *credential).await).ok(); } None => { log::trace!("PeerController [main loop]: stopping since channel closed"); break; - } } } @@ -452,7 +512,7 @@ pub fn start_controller( request_rx: mpsc::Receiver, ) -> ( Arc>, - nym_task::TaskManager, + nym_task::ShutdownManager, ) { use std::sync::Arc; @@ -463,7 +523,7 @@ pub fn start_controller( Box::new(storage.clone()), )); let wg_api = Arc::new(MockWgApi::default()); - let task_manager = nym_task::TaskManager::default(); + let shutdown_manager = nym_task::ShutdownManager::empty_mock(); let mut peer_controller = PeerController::new( ecash_manager, Default::default(), @@ -472,17 +532,17 @@ pub fn start_controller( Default::default(), request_tx, request_rx, - task_manager.subscribe(), + shutdown_manager.child_shutdown_token(), ); tokio::spawn(async move { peer_controller.run().await }); - (storage, task_manager) + (storage, shutdown_manager) } #[cfg(feature = "mock")] -pub async fn stop_controller(mut task_manager: nym_task::TaskManager) { - task_manager.signal_shutdown().unwrap(); - task_manager.wait_for_shutdown().await; +pub async fn stop_controller(mut shutdown_manager: nym_task::ShutdownManager) { + shutdown_manager.send_cancellation(); + shutdown_manager.run_until_shutdown().await; } #[cfg(test)] @@ -492,7 +552,7 @@ mod tests { #[tokio::test] async fn start_and_stop() { let (request_tx, request_rx) = mpsc::channel(1); - let (_, task_manager) = start_controller(request_tx.clone(), request_rx); - stop_controller(task_manager).await; + let (_, shutdown_manager) = start_controller(request_tx.clone(), request_rx); + stop_controller(shutdown_manager).await; } } diff --git a/common/wireguard/src/peer_handle.rs b/common/wireguard/src/peer_handle.rs index 014ec00fc5..80ec587a19 100644 --- a/common/wireguard/src/peer_handle.rs +++ b/common/wireguard/src/peer_handle.rs @@ -4,16 +4,37 @@ use crate::error::Error; use crate::peer_controller::PeerControlRequest; use crate::peer_storage_manager::{CachedPeerManager, PeerInformation}; -use defguard_wireguard_rs::{host::Host, key::Key}; +use defguard_wireguard_rs::{host::Host, key::Key, net::IpAddrMask}; use futures::channel::oneshot; use nym_credential_verification::bandwidth_storage_manager::BandwidthStorageManager; -use nym_task::TaskClient; +use nym_task::ShutdownToken; use nym_wireguard_types::DEFAULT_PEER_TIMEOUT_CHECK; use std::sync::Arc; use tokio::sync::{mpsc, RwLock}; use tokio_stream::{wrappers::IntervalStream, StreamExt}; -pub(crate) type SharedBandwidthStorageManager = Arc>; +#[derive(Clone)] +pub(crate) struct SharedBandwidthStorageManager { + inner: Arc>, + allowed_ips: Vec, +} + +impl SharedBandwidthStorageManager { + pub(crate) fn new( + inner: Arc>, + allowed_ips: Vec, + ) -> Self { + Self { inner, allowed_ips } + } + + pub(crate) fn inner(&self) -> &RwLock { + &self.inner + } + + pub(crate) fn allowed_ips(&self) -> &[IpAddrMask] { + &self.allowed_ips + } +} pub struct PeerHandle { public_key: Key, @@ -22,23 +43,22 @@ pub struct PeerHandle { bandwidth_storage_manager: SharedBandwidthStorageManager, request_tx: mpsc::Sender, timeout_check_interval: IntervalStream, - task_client: TaskClient, + shutdown_token: ShutdownToken, } impl PeerHandle { - pub fn new( + pub(crate) fn new( public_key: Key, host_information: Arc>, cached_peer: CachedPeerManager, bandwidth_storage_manager: SharedBandwidthStorageManager, request_tx: mpsc::Sender, - task_client: &TaskClient, + shutdown_token: &ShutdownToken, ) -> Self { let timeout_check_interval = tokio_stream::wrappers::IntervalStream::new( tokio::time::interval(DEFAULT_PEER_TIMEOUT_CHECK), ); - let mut task_client = task_client.fork(format!("peer_{public_key}")); - task_client.disarm(); + let shutdown_token = shutdown_token.clone(); PeerHandle { public_key, host_information, @@ -46,7 +66,7 @@ impl PeerHandle { bandwidth_storage_manager, request_tx, timeout_check_interval, - task_client, + shutdown_token, } } @@ -120,6 +140,7 @@ impl PeerHandle { if spent_bandwidth > 0 && self .bandwidth_storage_manager + .inner() .write() .await .try_use_bandwidth(spent_bandwidth) @@ -159,8 +180,18 @@ impl PeerHandle { } pub async fn run(&mut self) { - while !self.task_client.is_shutdown() { + loop { tokio::select! { + biased; + _ = self.shutdown_token.cancelled() => { + log::trace!("PeerHandle: Received shutdown"); + if let Err(e) = self.bandwidth_storage_manager.inner().write().await.sync_storage_bandwidth().await { + log::error!("Storage sync failed - {e}, unaccounted bandwidth might have been consumed"); + } + + log::trace!("PeerHandle: Finished shutdown"); + break; + } _ = self.timeout_check_interval.next() => { match self.continue_checking().await { Ok(true) => continue, @@ -179,15 +210,6 @@ impl PeerHandle { }, } } - - _ = self.task_client.recv() => { - log::trace!("PeerHandle: Received shutdown"); - if let Err(e) = self.bandwidth_storage_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/zulip-client/src/client.rs b/common/zulip-client/src/client.rs index 5de0a18b5c..60554e5c25 100644 --- a/common/zulip-client/src/client.rs +++ b/common/zulip-client/src/client.rs @@ -25,7 +25,7 @@ //! ``` use crate::error::ZulipClientError; -use crate::message::{SendMessageResponse, SendableMessage}; +use crate::message::{DirectMessage, SendMessageResponse, SendableMessage, StreamMessage}; use nym_bin_common::bin_info; use nym_http_api_client::UserAgent; use reqwest::{header, Method, RequestBuilder}; @@ -92,6 +92,20 @@ impl Client { .map_err(|source| ZulipClientError::RequestDecodeFailure { source }) } + pub async fn send_direct_message( + &self, + msg: impl Into, + ) -> Result { + self.send_message(msg.into()).await + } + + pub async fn send_channel_message( + &self, + msg: impl Into, + ) -> Result { + self.send_message(msg.into()).await + } + fn build_request(&self, method: Method, endpoint: &'static str) -> RequestBuilder { let url = format!("{}{endpoint}", self.server_url); trace!("posting to {url}"); diff --git a/common/zulip-client/src/message/mod.rs b/common/zulip-client/src/message/mod.rs index 307ef0222c..2877c724ee 100644 --- a/common/zulip-client/src/message/mod.rs +++ b/common/zulip-client/src/message/mod.rs @@ -22,7 +22,7 @@ pub enum SendMessageResponse { }, } -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "snake_case")] #[serde(tag = "type")] pub enum SendableMessageContent { @@ -40,7 +40,7 @@ pub enum SendableMessageContent { }, } -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "snake_case")] pub struct SendableMessage { #[serde(flatten)] @@ -117,17 +117,17 @@ impl StreamMessage { pub fn new( to: impl Into, content: impl Into, - topic: Option, + topic: impl IntoMaybeTopic, ) -> Self { StreamMessage { to: to.into().to_string(), - topic, + topic: topic.into_maybe_topic(), content: content.into(), } } pub fn no_topic(to: impl Into, content: impl Into) -> Self { - Self::new(to, content, None) + Self::new(to, content, None::) } #[must_use] @@ -194,22 +194,74 @@ impl From for SendableMessageContent { } } -impl From<(T, S, Option)> for StreamMessage +impl From<(T, S, U)> for StreamMessage where T: Into, S: Into, + U: IntoMaybeTopic, { - fn from((to, content, topic): (T, S, Option)) -> Self { - StreamMessage::new(to, content, topic.map(Into::into)) + fn from((to, content, topic): (T, S, U)) -> Self { + StreamMessage::new(to, content, topic) } } -impl From<(T, S, Option)> for SendableMessage +impl From<(T, S)> for StreamMessage where T: Into, S: Into, { - fn from(inner: (T, S, Option)) -> Self { + fn from((to, content): (T, S)) -> Self { + StreamMessage::no_topic(to, content) + } +} + +impl From<(T, S, U)> for SendableMessage +where + T: Into, + S: Into, + U: IntoMaybeTopic, +{ + fn from(inner: (T, S, U)) -> Self { StreamMessage::from(inner).into() } } + +pub trait IntoMaybeTopic { + fn into_maybe_topic(self) -> Option; +} + +impl IntoMaybeTopic for &Option +where + S: Into + Clone, +{ + fn into_maybe_topic(self) -> Option { + self.clone().map(|s| s.into()) + } +} + +impl IntoMaybeTopic for Option +where + S: Into, +{ + fn into_maybe_topic(self) -> Option { + self.map(Into::into) + } +} + +impl IntoMaybeTopic for String { + fn into_maybe_topic(self) -> Option { + Some(self) + } +} + +impl IntoMaybeTopic for &String { + fn into_maybe_topic(self) -> Option { + Some(self.clone()) + } +} + +impl IntoMaybeTopic for &str { + fn into_maybe_topic(self) -> Option { + Some(self.to_string()) + } +} diff --git a/contracts/Cargo.lock b/contracts/Cargo.lock index 11389591a5..86fd21a403 100644 --- a/contracts/Cargo.lock +++ b/contracts/Cargo.lock @@ -744,7 +744,7 @@ version = "0.1.0" dependencies = [ "cosmwasm-std", "quote", - "syn 1.0.109", + "syn 2.0.98", ] [[package]] @@ -1151,6 +1151,7 @@ dependencies = [ name = "nym-crypto" version = "0.4.0" dependencies = [ + "base64 0.22.1", "bs58", "ed25519-dalek", "nym-pemstore", diff --git a/contracts/ecash/src/contract/test.rs b/contracts/ecash/src/contract/test.rs index 09ece6dd4b..77f4dade4c 100644 --- a/contracts/ecash/src/contract/test.rs +++ b/contracts/ecash/src/contract/test.rs @@ -52,7 +52,7 @@ impl TestSetup { } } - pub fn query_ctx(&self) -> QueryCtx { + pub fn query_ctx(&self) -> QueryCtx<'_> { QueryCtx::from((self.deps.as_ref(), self.env.clone())) } } diff --git a/contracts/ecash/src/support/tests.rs b/contracts/ecash/src/support/tests.rs index fb3f3aa579..de31ea4e6d 100644 --- a/contracts/ecash/src/support/tests.rs +++ b/contracts/ecash/src/support/tests.rs @@ -73,13 +73,13 @@ impl TestSetupSimple { message_info(&admin, &[]) } - pub fn execute_ctx(&mut self, sender: MessageInfo) -> ExecCtx { + pub fn execute_ctx(&mut self, sender: MessageInfo) -> ExecCtx<'_> { let env = self.env.clone(); ExecCtx::from((self.deps.as_mut(), env, sender)) } #[allow(dead_code)] - pub fn query_ctx(&self) -> QueryCtx { + pub fn query_ctx(&self) -> QueryCtx<'_> { QueryCtx::from((self.deps.as_ref(), self.env.clone())) } diff --git a/contracts/mixnet/src/delegations/queries.rs b/contracts/mixnet/src/delegations/queries.rs index df2c9befa7..cf970e653e 100644 --- a/contracts/mixnet/src/delegations/queries.rs +++ b/contracts/mixnet/src/delegations/queries.rs @@ -139,9 +139,9 @@ mod tests { fn add_dummy_mixes_with_delegations(test: &mut TestSetup, delegators: usize, mixes: usize) { for i in 0..mixes { - let mix_id = test.add_legacy_mixnode(&test.make_addr(format!("mix-owner{}", i)), None); + let mix_id = test.add_legacy_mixnode(&test.make_addr(format!("mix-owner{i}")), None); for delegator in 0..delegators { - let name = &test.make_addr(format!("delegator{}", delegator)); + let name = &test.make_addr(format!("delegator{delegator}")); test.add_immediate_delegation(name, 100_000_000u32, mix_id) } } diff --git a/contracts/mixnet/src/delegations/transactions.rs b/contracts/mixnet/src/delegations/transactions.rs index 4611f83c20..dff4d46b39 100644 --- a/contracts/mixnet/src/delegations/transactions.rs +++ b/contracts/mixnet/src/delegations/transactions.rs @@ -300,7 +300,7 @@ mod tests { let amount1 = coin(100_000_000, TEST_COIN_DENOM); - let sender1 = message_info(owner, &[amount1.clone()]); + let sender1 = message_info(owner, std::slice::from_ref(&amount1)); try_delegate_to_node(test.deps_mut(), env.clone(), sender1, mix_id).unwrap(); diff --git a/contracts/mixnet/src/mixnodes/queries.rs b/contracts/mixnet/src/mixnodes/queries.rs index bac59ab591..7591b6cba4 100644 --- a/contracts/mixnet/src/mixnodes/queries.rs +++ b/contracts/mixnet/src/mixnodes/queries.rs @@ -503,8 +503,8 @@ pub(crate) mod tests { storage, id, &UnbondedMixnode { - identity_key: format!("dummy{}", id), - owner: Addr::unchecked(format!("dummy{}", id)), + identity_key: format!("dummy{id}"), + owner: Addr::unchecked(format!("dummy{id}")), proxy: None, unbonding_height: 123, }, @@ -570,7 +570,7 @@ pub(crate) mod tests { storage, id, &UnbondedMixnode { - identity_key: format!("dummy{}", id), + identity_key: format!("dummy{id}"), owner: owner.clone(), proxy: None, unbonding_height: 123, @@ -817,7 +817,7 @@ pub(crate) mod tests { id, &UnbondedMixnode { identity_key: identity.to_string(), - owner: Addr::unchecked(format!("dummy{}", id)), + owner: Addr::unchecked(format!("dummy{id}")), proxy: None, unbonding_height: 123, }, diff --git a/contracts/mixnet/src/support/tests/mod.rs b/contracts/mixnet/src/support/tests/mod.rs index 29aafaa92e..8ba4a4d3db 100644 --- a/contracts/mixnet/src/support/tests/mod.rs +++ b/contracts/mixnet/src/support/tests/mod.rs @@ -165,9 +165,9 @@ pub mod test_helpers { #[track_caller] pub fn assert_eq_with_leeway(a: Uint128, b: Uint128, leeway: Uint128) { if a > b { - assert!(a - b <= leeway, "{} != {}", a, b) + assert!(a - b <= leeway, "{a} != {b}") } else { - assert!(b - a <= leeway, "{} != {}", a, b) + assert!(b - a <= leeway, "{a} != {b}") } } @@ -175,9 +175,9 @@ pub mod test_helpers { pub fn assert_decimals(a: Decimal, b: Decimal) { let epsilon = Decimal::from_ratio(1u128, 100_000_000u128); if a > b { - assert!(a - b < epsilon, "{} != {}", a, b) + assert!(a - b < epsilon, "{a} != {b}") } else { - assert!(b - a < epsilon, "{} != {}", a, b) + assert!(b - a < epsilon, "{a} != {b}") } } @@ -1699,7 +1699,7 @@ pub mod test_helpers { deps.branch(), &env, env.block.height, - Addr::unchecked(format!("owner{}", i)), + Addr::unchecked(format!("owner{i}")), mix_id, tests::fixtures::good_mixnode_pledge().pop().unwrap(), ) @@ -1713,7 +1713,7 @@ pub mod test_helpers { n: usize, ) { for i in 0..n { - add_unbonded_mixnode(&mut rng, deps.branch(), None, &addr(format!("owner{}", i))); + add_unbonded_mixnode(&mut rng, deps.branch(), None, &addr(format!("owner{i}"))); } } @@ -1765,7 +1765,7 @@ pub mod test_helpers { id, &UnbondedMixnode { identity_key: identity_key - .unwrap_or(&*format!("identity{}", id)) + .unwrap_or(&*format!("identity{id}")) .to_string(), owner: Addr::unchecked(owner), proxy: None, diff --git a/contracts/performance/src/testing/mod.rs b/contracts/performance/src/testing/mod.rs index 4f8e7cf4bd..614982acc3 100644 --- a/contracts/performance/src/testing/mod.rs +++ b/contracts/performance/src/testing/mod.rs @@ -131,7 +131,7 @@ impl PreInitContract { } } - pub(crate) fn deps(&self) -> Deps { + pub(crate) fn deps(&self) -> Deps<'_> { Deps { storage: &self.storage, api: &self.api, @@ -139,7 +139,7 @@ impl PreInitContract { } } - pub(crate) fn deps_mut(&mut self) -> DepsMut { + pub(crate) fn deps_mut(&mut self) -> DepsMut<'_> { DepsMut { storage: &mut self.storage, api: &self.api, @@ -147,7 +147,7 @@ impl PreInitContract { } } - pub(crate) fn querier(&self) -> QuerierWrapper { + pub(crate) fn querier(&self) -> QuerierWrapper<'_> { self.tester_builder.querier() } diff --git a/contracts/vesting/src/vesting/mod.rs b/contracts/vesting/src/vesting/mod.rs index 48e0f48dc3..8c37ed5eac 100644 --- a/contracts/vesting/src/vesting/mod.rs +++ b/contracts/vesting/src/vesting/mod.rs @@ -137,7 +137,7 @@ mod tests { let response = execute( deps.as_mut(), env.clone(), - message_info(&admin, &[amount.clone()]), + message_info(&admin, std::slice::from_ref(&amount)), msg, ); assert_eq!( diff --git a/deny.toml b/deny.toml index 7a3323ce1b..4809f4e766 100644 --- a/deny.toml +++ b/deny.toml @@ -119,7 +119,6 @@ exceptions = [ #{ allow = ["Zlib"], crate = "adler32" }, { allow = ["GPL-3.0"], crate = "nym-api" }, { allow = ["GPL-3.0"], crate = "nym-gateway" }, - { allow = ["GPL-3.0"], crate = "nym-mixnode" }, { allow = ["GPL-3.0"], crate = "nym-network-requester" }, { allow = ["GPL-3.0"], crate = "nym-node" }, { allow = ["GPL-3.0"], crate = "nym-validator-rewarder" }, diff --git a/documentation/docs/components/outputs/api-scraping-outputs/circulating-supply.json b/documentation/docs/components/outputs/api-scraping-outputs/circulating-supply.json index 8f961e9082..83ee4980db 100644 --- a/documentation/docs/components/outputs/api-scraping-outputs/circulating-supply.json +++ b/documentation/docs/components/outputs/api-scraping-outputs/circulating-supply.json @@ -5,7 +5,7 @@ }, "mixmining_reserve": { "denom": "unym", - "amount": "184339094131786" + "amount": "182883243257647" }, "vesting_tokens": { "denom": "unym", @@ -13,6 +13,6 @@ }, "circulating_supply": { "denom": "unym", - "amount": "815660905868214" + "amount": "817116756742353" } } diff --git a/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/circulating-supply.md b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/circulating-supply.md index 758aa1bb47..3762743930 100644 --- a/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/circulating-supply.md +++ b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/circulating-supply.md @@ -1 +1 @@ -815_660_905 +817_116_756 diff --git a/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/epoch-reward-budget.md b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/epoch-reward-budget.md index 3982030435..2a89edffe7 100644 --- a/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/epoch-reward-budget.md +++ b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/epoch-reward-budget.md @@ -1 +1 @@ -5_120 +5_080 diff --git a/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/nyx-percent-stake.md b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/nyx-percent-stake.md index 0eec7ac32c..069e6eb141 100644 --- a/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/nyx-percent-stake.md +++ b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/nyx-percent-stake.md @@ -1 +1 @@ -0.74% +0.77% diff --git a/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/nyx-total-stake.md b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/nyx-total-stake.md index 037b73c86b..486924e832 100644 --- a/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/nyx-total-stake.md +++ b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/nyx-total-stake.md @@ -1 +1 @@ -38.479 +37.305 diff --git a/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/stake-saturation.md b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/stake-saturation.md index 3a671a25d2..7dce4fcd05 100644 --- a/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/stake-saturation.md +++ b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/stake-saturation.md @@ -1 +1 @@ -1_040_817 +250_000 diff --git a/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/staking-scale-factor.md b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/staking-scale-factor.md index efc97dcecc..1fc1b2163f 100644 --- a/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/staking-scale-factor.md +++ b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/staking-scale-factor.md @@ -1 +1 @@ -30.63% +7.34% diff --git a/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/staking-target.md b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/staking-target.md index 26e9e10529..a4417fced3 100644 --- a/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/staking-target.md +++ b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/staking-target.md @@ -1 +1 @@ -249_796_152 +60_000_000 diff --git a/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/staking_supply.md b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/staking_supply.md index 26e9e10529..a4417fced3 100644 --- a/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/staking_supply.md +++ b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/staking_supply.md @@ -1 +1 @@ -249_796_152 +60_000_000 diff --git a/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/token-table.md b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/token-table.md index 4b497bab6f..ea63891258 100644 --- a/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/token-table.md +++ b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/token-table.md @@ -1,7 +1,7 @@ | **Item** | **Description** | **Amount in NYM** | |:-------------------|:------------------------------------------------------|--------------------:| | Total Supply | Maximum amount of NYM token in existence | 1_000_000_000 | -| Mixmining Reserve | Tokens releasing for operators rewards | 184_339_094 | +| Mixmining Reserve | Tokens releasing for operators rewards | 182_883_243 | | Vesting Tokens | Tokens locked outside of cicrulation for future claim | 0 | -| Circulating Supply | Amount of unlocked tokens | 815_660_905 | -| Stake Saturation | Optimal size of node self-bond + delegation | 1_040_817 | +| Circulating Supply | Amount of unlocked tokens | 817_116_756 | +| Stake Saturation | Optimal size of node self-bond + delegation | 250_000 | diff --git a/documentation/docs/components/outputs/api-scraping-outputs/reward-params.json b/documentation/docs/components/outputs/api-scraping-outputs/reward-params.json index ae2d31a730..cd175a8719 100644 --- a/documentation/docs/components/outputs/api-scraping-outputs/reward-params.json +++ b/documentation/docs/components/outputs/api-scraping-outputs/reward-params.json @@ -1,18 +1,18 @@ { "interval": { - "reward_pool": "184339094131786.145886263466367605", - "staking_supply": "249796152422140.492822331813424919", - "staking_supply_scale_factor": "0.30625", - "epoch_reward_budget": "5120530392.54961516350731851", - "stake_saturation_point": "1040817301758.918720093049222603", + "reward_pool": "182883243257647.891553460395608456", + "staking_supply": "60000000000000", + "staking_supply_scale_factor": "0.07342892", + "epoch_reward_budget": "5080090090.490219209818344322", + "stake_saturation_point": "250000000000", "sybil_resistance": "0.3", "active_set_work_factor": "10", "interval_pool_emission": "0.02" }, "rewarded_set": { - "entry_gateways": 50, - "exit_gateways": 70, - "mixnodes": 120, + "entry_gateways": 80, + "exit_gateways": 100, + "mixnodes": 60, "standby": 0 } } diff --git a/documentation/docs/components/outputs/api-scraping-outputs/time-now.md b/documentation/docs/components/outputs/api-scraping-outputs/time-now.md index 00f2e2d18e..2467bafbb8 100644 --- a/documentation/docs/components/outputs/api-scraping-outputs/time-now.md +++ b/documentation/docs/components/outputs/api-scraping-outputs/time-now.md @@ -1 +1 @@ -Friday, August 22nd 2025, 10:15:08 UTC \ No newline at end of file +Tuesday, September 16th 2025, 11:07:26 UTC diff --git a/documentation/docs/components/outputs/csv2md-outputs/isp-sheet.md b/documentation/docs/components/outputs/csv2md-outputs/isp-sheet.md index e5b10cc72b..bbc71cfd4b 100644 --- a/documentation/docs/components/outputs/csv2md-outputs/isp-sheet.md +++ b/documentation/docs/components/outputs/csv2md-outputs/isp-sheet.md @@ -4,7 +4,7 @@ | [AmeriNoc](https://www.amerinoc.com) | USA | Yes | nan | nan | 07/2025 | | [BitLaunch](https://bitlaunch.io) | Canada, USA, UK | No | Yes | Expensive. Digial Ocean through BitLanch has IPv6 | 05/2024 | | [Cherry Servers](https://www.cherryservers.com) | Lithuania, Netherlands, USA, Singapore | No | Yes | Issued IP doesn’t match the location offered by the provider. | 05/2024 | -| [Colocall](https://www.colocall.net/) | Ukraine | Yes | nan | 07/2025 | nan | +| [Colocall](https://www.colocall.net/) | Ukraine | Yes | nan | nan | 07/2025 | | [DataPacket](https://www.datapacket.com/pricing) | NL, GR, SK, BE, RO, HU, DK, IE, DE, UA, PT, GB, ES, FR, IT, NO, CZ, BG, SE, AT, PL, HR, CH, USA, CO, AR, PE, MX, CL, TR, ZA, NG, IL, HK, AU, SG, JP | Yes | nan | nan | 07/2025 | | [Dataclub](https://www.dataclub.eu/) | Latvia, Sweden, Netherlands | Yes | nan | nan | 07/2027 | | [Flokinet](https://flokinet.is) | Netherlands, Iceland, Romania,France | Yes, needs a ticket and custom setup | yes, including XMR | Very slow customer support | 05/2024 | @@ -13,6 +13,7 @@ | [HostSailor](https://hostsailor.com) | USA | Yes, based on ticket | Yes | The IPv6 setup needs custom research and is not documented | 05/2024 | | [Hostiko](https://hostiko.com.ua) | Ukraine, Germany | Yes, on by default | Yes | Ukrainian provider. They allow Exit nodes on Germany boxes but limit the bandwidth, you also have to restrict certain ports like 25 and 587. Make sure you open a ticket. | 07/2024 | | [Hostinger](https://hostinger.com) | France, Lithuania, India, USA, Brazil | Yes, out of the box | Yes | Not fast enough, Crypto payments must be done per each server monthly or annually. | 07/2025 | +| [Hostraha](https://hostraha.com) | Kenya and other African countries | No, but advertised otherwise | Yes, USDT TRC20 | Don't recommend. Unresponsive technical and billing support, never provided IPv6 even though advertised and paid for. When VPS cancelled, company still tried to bill the credit card on file multiple times. | 08/2025 | | [Hostroyale](https://hostroyale.com/hosting/dedicated-server/) | Various countries with different pricing | nan | Yes | nan | 07/2025 | | [Hostslick](https://hostslick.com) | Netherlands, Germany | Yes, on by default | Yes | Good amount of bandwidth for the price. Make sure you open the ticket if you want to run Exit node | 07/2024 | | [Incognet](https://incognet.io) | Netherlands and USA | Yes, on by default | Yes | They allow Tor exit nodes but you must adhere to their rules https://incognet.io/tor-exits | 07/2024 | diff --git a/documentation/docs/data/csv/isp-sheet.csv b/documentation/docs/data/csv/isp-sheet.csv index 31ea1cb40c..aba01a1c8d 100644 --- a/documentation/docs/data/csv/isp-sheet.csv +++ b/documentation/docs/data/csv/isp-sheet.csv @@ -32,7 +32,7 @@ [Zenlayer](https://www.zenlayer.com/bare-metal/), [advertised over 50 locations](50+ https://www.zenlayer.com/global-network),,,,07/2025 [PrivateLayer](https://privatelayer.com),Swiss,Yes,Yes,Slow customer response,07/2025 [AmeriNoc](https://www.amerinoc.com),USA,Yes,,,07/2025 -[Colocall](https://www.colocall.net/),Ukraine,Yes,,07/2025, +[Colocall](https://www.colocall.net/),Ukraine,Yes,,,07/2025 [Incognet](https://incognet.io/kansas-city-dedicated-servers),"USA, Netherlands",Yes,,,07/2025 [FranTech](https://my.frantech.ca),USA,Yes,,,07/2025 [Psychz](https://www.psychz.net),"US, UK, Brazil, Japan, Russia, South Africa and many more",Yes,,,07/2025 @@ -41,4 +41,4 @@ [Dataclub](https://www.dataclub.eu/),"Latvia, Sweden, Netherlands",Yes,,,07/2027 [Privex](https://www.privex.io/tor-exit-policy/),"USA, Germany, Sweden",Yes,Yes,,07/2025 [Svea](https://svea.net/vps),Sweden,Yes,,,07/2025 -[Hostraha](https://hostraha.com),"Kenya and other African countries", "No, but advertised otherwise", "Yes, USDT TRC20", "Don't recommend. Unresponsive technical and billing support, never provided IPv6 even though advertised and paid for. When VPS cancelled, company still tried to bill the credit card on file multiple times.", 08/2025 +[Hostraha](https://hostraha.com),Kenya and other African countries,"No, but advertised otherwise","Yes, USDT TRC20","Don't recommend. Unresponsive technical and billing support, never provided IPv6 even though advertised and paid for. When VPS cancelled, company still tried to bill the credit card on file multiple times.",08/2025 diff --git a/documentation/docs/pages/developers/rust/mixnet/examples/custom-topology/custom-provider.mdx b/documentation/docs/pages/developers/rust/mixnet/examples/custom-topology/custom-provider.mdx index 3f833616be..be32aecb2d 100644 --- a/documentation/docs/pages/developers/rust/mixnet/examples/custom-topology/custom-provider.mdx +++ b/documentation/docs/pages/developers/rust/mixnet/examples/custom-topology/custom-provider.mdx @@ -20,16 +20,22 @@ use nym_sdk::mixnet; use nym_sdk::mixnet::MixnetMessageSender; use nym_topology::provider_trait::{async_trait, TopologyProvider}; use nym_topology::{nym_topology_from_detailed, NymTopology}; +use nym_validator_client::nym_api::NymApiClientExt; use url::Url; struct MyTopologyProvider { - validator_client: nym_validator_client::client::NymApiClient, + validator_client: nym_http_api_client::Client, } impl MyTopologyProvider { fn new(nym_api_url: Url) -> MyTopologyProvider { + let validator_client = nym_http_api_client::Client::builder::<_, nym_validator_client::models::RequestError>(nym_api_url) + .expect("Failed to create API client builder") + .build::() + .expect("Failed to build API client"); + MyTopologyProvider { - validator_client: nym_validator_client::client::NymApiClient::new(nym_api_url), + validator_client, } } diff --git a/documentation/docs/pages/operators/changelog.mdx b/documentation/docs/pages/operators/changelog.mdx index b0dcb2ba61..5f8b267014 100644 --- a/documentation/docs/pages/operators/changelog.mdx +++ b/documentation/docs/pages/operators/changelog.mdx @@ -39,7 +39,6 @@ export const LoadEndpointInfo = () => ( ); -{/* # Changelog @@ -49,23 +48,81 @@ This page displays a full list of all the changes during our release cycle from +## `v2025.16-halloumi` + +- **[`nym-node`](nodes/nym-node.mdx) is not part of the release, the latest stays on version `1.16.0`** +- [Release Binaries](https://github.com/nymtech/nym/releases/tag/nym-binaries-v2025.16-halloumi) + +### Operators Updates & Tools + + +Nodes receiving stake as a part of [**Nym Delegation Program**](https://nym.com/network/DP) are updated weekly based on the [rules](https://forms.nym.com/form/#/2/form/view/BRh8QroXFinjOF4D3FHgYiX76zbiRvUV2Sy+czaoKFQ) without prior notification given to the operators. + +[**Nym Delegation account**](https://explorer.nym.spectredao.net/account/n1rnxpdpx3kldygsklfft0gech7fhfcux4zst5lw) `n1rnxpdpx3kldygsklfft0gech7fhfcux4zst5lw` is a single source of truth. If you expect your node to have Nym team stake and it doesn't, please reach out in in the [**Node Operators Matrix channel**](https://matrix.to/#/#operators:nymtech.chat). + + +- New [Delegation Program page is out](https://nym.com/network/DP) +- Complete rules and application form are [here](https://forms.nym.com/form/#/2/form/view/BRh8QroXFinjOF4D3FHgYiX76zbiRvUV2Sy+czaoKFQ) + +### Features + +- [Wireguard private metadata](https://github.com/nymtech/nym/pull/5915) + +- [Change PK/FK on expiration date signatures tables](https://github.com/nymtech/nym/pull/5934) + +- [Introduce additional checks when attempting to send to bounded channels](https://github.com/nymtech/nym/pull/5941) + +- [Wireguard metadata client library](https://github.com/nymtech/nym/pull/5943): Tested end to end using [\#3316](https://github.com/nymtech/nym-vpn-client/pull/3316) in canary, with a pair of gateways that has the changes deployed: +``` +nym-vpnc connect --enable-two-hop --entry-gateway-id 7CWjY3QFoA9dgE535u9bQiXCfzgMZvSpJu842GA1Wn42 --exit-gateway-id 7fp3cmzCvgeRgbB1ycTnK6RokjH +``` + +- [Feature/testing utils](https://github.com/nymtech/nym/pull/5963): This PR introduces a couple of general helpers, in particular some mocks for sending across values using Stream/Sink and AsyncRead/AsyncWrite without actual underlying networking. Example implementation are with NymNoise (which was the original inspiration) and gateway handshake. + +- [Backport metadata endpoint](https://github.com/nymtech/nym/pull/6010) + +### Bugfix + +- [Fix rust `1.89` `clippy` issues](https://github.com/nymtech/nym/pull/5944) + +- [`http api` client adjustment](https://github.com/nymtech/nym/pull/5953): It fixes missing `feature-lock` when cloning the client and adds helper macro for user agent creation + +- [Fix `ci-build` for linux (and use updated runner)](https://github.com/nymtech/nym/pull/5958): This PR fixes our build pipeline by using correct (updated) linux runner and updates all the conditional steps that were behind `ubuntu` runners (which no longer exist) + +- [Fixing the ci for ns agent](https://github.com/nymtech/nym/pull/5965) + +- [Manually calculate per node work on rewarded set changes](https://github.com/nymtech/nym/pull/5972): This PR fixes: + 1. Nym rewarded set was set to X, for argument sake say 200 + 2. We sent transaction to update it to Y, say 100 + 3. This internally updated the interval rewarding parameters inside the mixnet contract including the default active and standby node work factors. Note that the rewarded set itself stayed the same, as it only changes after epoch rolls over and new one is assigned (by the `nym-api`) + 4. Epoch has finished and `nym-api` wanted to do the rewarding. It grabbed the **current** rewarded set (of X, 200) and started calculating the total work in the system. But since the contract already had new parameters (adjusted for size of Y, 100), the result was greater than 1 thus `nym-api` was preventably blowing up. + To fix it we introduce additional checks, so that if the current rewarded set does not match the specification defined in the contract rewarding parameters, `nym-api` will attempt to do its best to manually calculate work factors for this epoch. + +- [Fix the ns api ci workflow](https://github.com/nymtech/nym/pull/5981) + +- [Make sure tables are removed in correct order to not trigger FK constraint issue](https://github.com/nymtech/nym/pull/5987) + +### Refactors & Maintenance + +- [Move credential verifier in peer controller](https://github.com/nymtech/nym/pull/5938): This PR is to not duplicate the verifier code (minus the actual verification operation, which is harder to unit test because of expiration checks) + +- [Remove unused import](https://github.com/nymtech/nym/pull/5942) + +- [Updated refs to cheddar rev of nym repo](https://github.com/nymtech/nym/pull/5955) + +- [Update `sysinfo` to the latest](https://github.com/nymtech/nym/pull/5976): Shakes out `windows@0.57` from the tree + +- [Remove freshness check on testrun submit](https://github.com/nymtech/nym/pull/5977): + - Freshness is enforced by a background task that marks test runs as stale after a configured amount of time + - Make existing freshness period configurable to avoid code changes in the future + - Added `humantime` for parsing + +- [Move authenticator into gateway crate](https://github.com/nymtech/nym/pull/5982) + ## `v2025.15-gruyere` +- **[`nym-node`](nodes/nym-node.mdx) is not part of the release, the latest stays on version `1.16.0`** - [Release Binaries](https://github.com/nymtech/nym/releases/tag/nym-binaries-v2025.15-gruyere) -- [`nym-node`](nodes/nym-node.mdx) version `1.17.0` - -```sh -nym-node -Binary Name: nym-node -Build Timestamp: 2025-08-20T10:37:05.965300480Z -Build Version: 1.17.0 -Commit SHA: 40e1cbc7a9f518eafbb5649c383626b096dd167d -Commit Date: 2025-08-20T12:34:04.000000000+02:00 -Commit Branch: HEAD -rustc Version: 1.86.0 -rustc Channel: stable -cargo Profile: release -``` ### Operators Updates & Tools @@ -95,7 +152,6 @@ cargo Profile: release - [Migrate strum to `0.27.2`](https://github.com/nymtech/nym/pull/5960): This PR migrates strum to the latest. Notably all macros' were moved into `strum_macros`. The rest stays the same. -*/} ## `v2025.14-feta` diff --git a/documentation/docs/pages/operators/nodes/nym-node/setup.mdx b/documentation/docs/pages/operators/nodes/nym-node/setup.mdx index 451fcac578..3db66217b7 100644 --- a/documentation/docs/pages/operators/nodes/nym-node/setup.mdx +++ b/documentation/docs/pages/operators/nodes/nym-node/setup.mdx @@ -20,16 +20,18 @@ This documentation page provides a guide on how to set up and run a [NYM NODE](. ```sh nym-node Binary Name: nym-node -Build Timestamp: 2025-08-20T10:37:05.965300480Z -Build Version: 1.17.0 -Commit SHA: 40e1cbc7a9f518eafbb5649c383626b096dd167d -Commit Date: 2025-08-20T12:34:04.000000000+02:00 +Build Timestamp: 2025-08-05T09:14:30.322593213Z +Build Version: 1.16.0 +Commit SHA: 7f97f13799342f864e1b106e8cafc9f6d6c24c0f +Commit Date: 2025-07-24T11:00:58.000000000+01:00 Commit Branch: HEAD rustc Version: 1.86.0 rustc Channel: stable cargo Profile: release ``` +Detailed version archive and release notes is documented [here](../../changelog.mdx). + {/* COMMENTING THIS OUT ASS WE HAVE TO FIGURE OUT HOW TO SHOW THE LATEST VERSION FROM MASTER BRANCH */} diff --git a/gateway/Cargo.toml b/gateway/Cargo.toml index 445bc7a2be..13d80f7a14 100644 --- a/gateway/Cargo.toml +++ b/gateway/Cargo.toml @@ -72,6 +72,7 @@ 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-private-metadata-server = { path = "../common/wireguard-private-metadata/server" } nym-wireguard-types = { path = "../common/wireguard-types", default-features = false } nym-authenticator-requests = { path = "../common/authenticator-requests" } @@ -85,5 +86,5 @@ defguard_wireguard_rs = { workspace = true } [dev-dependencies] nym-gateway-storage = { path = "../common/gateway-storage", features = ["mock"] } nym-wireguard = { path = "../common/wireguard", features = ["mock"] } -mock_instant = "0.5.3" +mock_instant = "0.6.0" time = { workspace = true } diff --git a/gateway/src/node/client_handling/embedded_clients/mod.rs b/gateway/src/node/client_handling/embedded_clients/mod.rs index 95af759ed9..9823a9494b 100644 --- a/gateway/src/node/client_handling/embedded_clients/mod.rs +++ b/gateway/src/node/client_handling/embedded_clients/mod.rs @@ -8,8 +8,7 @@ use futures::StreamExt; 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 nym_task::ShutdownToken; use tracing::{debug, error, trace}; #[derive(Debug)] @@ -53,10 +52,6 @@ impl MessageRouter { } } - pub(crate) fn start_with_shutdown(self, shutdown: TaskClient) -> JoinHandle<()> { - tokio::spawn(self.run_with_shutdown(shutdown)) - } - fn handle_received_messages(&self, messages: Vec>) { if let Err(err) = self.packet_router.route_received(messages) { // TODO: what should we do here? I don't think this could/should ever fail. @@ -65,10 +60,15 @@ impl MessageRouter { } } - pub(crate) async fn run_with_shutdown(mut self, mut shutdown: TaskClient) { + pub(crate) async fn run_with_shutdown(mut self, shutdown: ShutdownToken) { debug!("Started embedded client message router with graceful shutdown support"); - while !shutdown.is_shutdown() { + loop { tokio::select! { + biased; + _ = shutdown.cancelled() => { + trace!("embedded_clients::MessageRouter: Received shutdown"); + break; + } messages = self.mix_receiver.next() => match messages { Some(messages) => self.handle_received_messages(messages), None => { @@ -76,11 +76,6 @@ impl MessageRouter { break; } }, - _ = shutdown.recv_with_delay() => { - trace!("embedded_clients::MessageRouter: Received shutdown"); - debug_assert!(shutdown.is_shutdown()); - break - } } } 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 a152092ef7..36b8029332 100644 --- a/gateway/src/node/client_handling/websocket/connection_handler/authenticated.rs +++ b/gateway/src/node/client_handling/websocket/connection_handler/authenticated.rs @@ -29,7 +29,6 @@ use nym_gateway_storage::traits::SharedKeyGatewayStorage; use nym_node_metrics::events::MetricsEvent; use nym_sphinx::forwarding::packet::MixPacket; use nym_statistics_common::{gateways::GatewaySessionEvent, types::SessionType}; -use nym_task::TaskClient; use nym_validator_client::coconut::EcashApiError; use rand::{random, CryptoRng, Rng}; use std::{process, time::Duration}; @@ -583,7 +582,7 @@ impl AuthenticatedHandler { /// Simultaneously listens for incoming client requests, which realistically should only be /// binary requests to forward sphinx packets or increase bandwidth /// 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) + pub(crate) async fn listen_for_requests(mut self) where R: Rng + CryptoRng, S: AsyncRead + AsyncWrite + Unpin, @@ -593,11 +592,8 @@ impl AuthenticatedHandler { // Ping timeout future used to check if the client responded to our ping request let mut ping_timeout: OptionFuture<_> = None.into(); - while !shutdown.is_shutdown() { + loop { tokio::select! { - _ = shutdown.recv() => { - trace!("client_handling::AuthenticatedHandler: received shutdown"); - }, // Received a request to ping the client to check if it's still active tx = self.is_active_request_receiver.next() => { match tx { 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 2edbf16b4c..ab308e7f8e 100644 --- a/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs +++ b/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs @@ -32,7 +32,7 @@ use nym_gateway_storage::traits::InboxGatewayStorage; use nym_gateway_storage::traits::SharedKeyGatewayStorage; use nym_node_metrics::events::MetricsEvent; use nym_sphinx::DestinationAddressBytes; -use nym_task::TaskClient; +use nym_task::ShutdownToken; use rand::CryptoRng; use std::net::SocketAddr; use std::time::Duration; @@ -127,7 +127,7 @@ pub(crate) struct FreshHandler { pub(crate) shared_state: CommonHandlerState, pub(crate) socket_connection: SocketStream, pub(crate) peer_address: SocketAddr, - pub(crate) shutdown: TaskClient, + pub(crate) shutdown: ShutdownToken, // currently unused (but populated) pub(crate) negotiated_protocol: Option, @@ -145,7 +145,7 @@ impl FreshHandler { conn: S, shared_state: CommonHandlerState, peer_address: SocketAddr, - shutdown: TaskClient, + shutdown: ShutdownToken, ) -> Self { FreshHandler { rng, @@ -917,60 +917,49 @@ impl FreshHandler { pub(crate) async fn handle_until_authenticated_or_failure( mut self, - shutdown: &mut TaskClient, ) -> Option> where S: AsyncRead + AsyncWrite + Unpin + Send, R: CryptoRng + RngCore + Send, { - while !shutdown.is_shutdown() { - let req = tokio::select! { - biased; - _ = shutdown.recv() => { - return None - }, - req = self.wait_for_initial_message() => req, - }; - - let initial_request = match req { - Ok(req) => req, - Err(err) => { - self.send_and_forget_error_response(err).await; - return None; - } - }; - - // see if we managed to register the client through this request - let maybe_auth_res = match self.handle_initial_client_request(initial_request).await { - Ok(maybe_auth_res) => maybe_auth_res, - Err(err) => { - debug!("initial client request handling error: {err}"); - self.send_and_forget_error_response(err).await; - return None; - } - }; - - if let Some(registration_details) = maybe_auth_res { - 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.shared_state.active_clients_store.insert_remote( - registration_details.address, - mix_sender, - is_active_request_sender, - registration_details.session_request_timestamp, - ); - - return AuthenticatedHandler::upgrade( - self, - registration_details, - mix_receiver, - is_active_request_receiver, - ) - .await - .inspect_err(|err| error!("failed to upgrade client handler: {err}")) - .ok(); + let initial_request = match self.wait_for_initial_message().await { + Ok(req) => req, + Err(err) => { + self.send_and_forget_error_response(err).await; + return None; } + }; + + // see if we managed to register the client through this request + let maybe_auth_res = match self.handle_initial_client_request(initial_request).await { + Ok(maybe_auth_res) => maybe_auth_res, + Err(err) => { + debug!("initial client request handling error: {err}"); + self.send_and_forget_error_response(err).await; + return None; + } + }; + + if let Some(registration_details) = maybe_auth_res { + 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.shared_state.active_clients_store.insert_remote( + registration_details.address, + mix_sender, + is_active_request_sender, + registration_details.session_request_timestamp, + ); + + AuthenticatedHandler::upgrade( + self, + registration_details, + mix_receiver, + is_active_request_receiver, + ) + .await + .inspect_err(|err| error!("failed to upgrade client handler: {err}")) + .ok(); } None @@ -1031,6 +1020,15 @@ impl FreshHandler { S: AsyncRead + AsyncWrite + Unpin + Send, R: CryptoRng + RngCore + Send, { - super::handle_connection(self).await + let remote = self.peer_address; + let shutdown = self.shutdown.clone(); + tokio::select! { + _ = shutdown.cancelled() => { + trace!("received cancellation") + } + _ = super::handle_connection(self) => { + debug!("finished connection handler for {remote}") + } + } } } 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 0ce52eec00..2fd97600a7 100644 --- a/gateway/src/node/client_handling/websocket/connection_handler/mod.rs +++ b/gateway/src/node/client_handling/websocket/connection_handler/mod.rs @@ -98,15 +98,6 @@ where R: Rng + CryptoRng + Send, S: AsyncRead + AsyncWrite + Unpin + Send, { - // don't accept any new requests if we have already received shutdown - if handle.shutdown.is_shutdown_poll() { - debug!("stopping the handle as we have received a shutdown"); - return; - } - - // If the connection handler abruptly stops, we shouldn't signal global shutdown - handle.shutdown.disarm(); - match tokio::time::timeout( WEBSOCKET_HANDSHAKE_TIMEOUT, handle.perform_websocket_handshake(), @@ -126,13 +117,8 @@ where trace!("managed to perform websocket handshake!"); - let mut shutdown = handle.shutdown.clone(); - - if let Some(auth_handle) = handle - .handle_until_authenticated_or_failure(&mut shutdown) - .await - { - auth_handle.listen_for_requests(shutdown).await + if let Some(auth_handle) = handle.handle_until_authenticated_or_failure().await { + auth_handle.listen_for_requests().await } trace!("the handler is done!"); diff --git a/gateway/src/node/client_handling/websocket/listener.rs b/gateway/src/node/client_handling/websocket/listener.rs index 94e11122ef..b2d234d4a7 100644 --- a/gateway/src/node/client_handling/websocket/listener.rs +++ b/gateway/src/node/client_handling/websocket/listener.rs @@ -3,19 +3,18 @@ use crate::node::client_handling::websocket::common_state::CommonHandlerState; use crate::node::client_handling::websocket::connection_handler::FreshHandler; -use nym_task::TaskClient; +use nym_task::ShutdownTracker; use rand::rngs::OsRng; use std::net::SocketAddr; use std::{io, process}; use tokio::net::TcpStream; -use tokio::task::JoinHandle; use tracing::*; pub struct Listener { address: SocketAddr, maximum_open_connections: usize, shared_state: CommonHandlerState, - shutdown: TaskClient, + shutdown: ShutdownTracker, } impl Listener { @@ -23,7 +22,7 @@ impl Listener { address: SocketAddr, maximum_open_connections: usize, shared_state: CommonHandlerState, - shutdown: TaskClient, + shutdown: ShutdownTracker, ) -> Self { Listener { address, @@ -45,15 +44,12 @@ impl Listener { socket: TcpStream, remote_address: SocketAddr, ) -> FreshHandler { - let shutdown = self - .shutdown - .fork(format!("websocket_handler_{remote_address}")); FreshHandler::new( OsRng, socket, self.shared_state.clone(), remote_address, - shutdown, + self.shutdown.clone_shutdown_token(), ) } @@ -88,16 +84,19 @@ impl Listener { .new_ingress_websocket_client(); // 4. spawn the task handling the client connection - tokio::spawn(async move { - // TODO: refactor it similarly to the mixnet listener on the nym-node - let metrics_ref = handle.shared_state.metrics.clone(); + self.shutdown.try_spawn_named( + async move { + // TODO: refactor it similarly to the mixnet listener on the nym-node + let metrics_ref = handle.shared_state.metrics.clone(); - // 4.1. handle all client requests until connection gets terminated - handle.start_handling().await; + // 4.1. handle all client requests until connection gets terminated + handle.start_handling().await; - // 4.2. decrement the connection counter - metrics_ref.network.disconnected_ingress_websocket_client(); - }); + // 4.2. decrement the connection counter + metrics_ref.network.disconnected_ingress_websocket_client(); + }, + &format!("Websocket::{remote_address}"), + ); } Err(err) => warn!("failed to accept client connection: {err}"), } @@ -105,7 +104,7 @@ impl Listener { // TODO: change the signature to pub(crate) async fn run(&self, handler: Handler) - pub(crate) async fn run(&mut self) { + pub 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, @@ -115,21 +114,18 @@ impl Listener { } }; - while !self.shutdown.is_shutdown() { + let shutdown_token = self.shutdown.clone_shutdown_token(); + loop { tokio::select! { biased; - _ = self.shutdown.recv() => { + _ = shutdown_token.cancelled() => { trace!("client_handling::Listener: received shutdown"); + break } connection = tcp_listener.accept() => { self.try_handle_accepted_connection(connection) } - } } } - - pub fn start(mut self) -> JoinHandle<()> { - tokio::spawn(async move { self.run().await }) - } } diff --git a/gateway/src/node/internal_service_providers/authenticator/config/mod.rs b/gateway/src/node/internal_service_providers/authenticator/config/mod.rs index 6a11d8bf67..1a1e542368 100644 --- a/gateway/src/node/internal_service_providers/authenticator/config/mod.rs +++ b/gateway/src/node/internal_service_providers/authenticator/config/mod.rs @@ -2,8 +2,8 @@ // SPDX-License-Identifier: Apache-2.0 use nym_network_defaults::{ - WG_PORT, WG_TUN_DEVICE_IP_ADDRESS_V4, WG_TUN_DEVICE_IP_ADDRESS_V6, WG_TUN_DEVICE_NETMASK_V4, - WG_TUN_DEVICE_NETMASK_V6, + WG_METADATA_PORT, WG_TUNNEL_PORT, WG_TUN_DEVICE_IP_ADDRESS_V4, WG_TUN_DEVICE_IP_ADDRESS_V6, + WG_TUN_DEVICE_NETMASK_V4, WG_TUN_DEVICE_NETMASK_V6, }; use serde::{Deserialize, Serialize}; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; @@ -46,9 +46,9 @@ pub struct Authenticator { /// default: `fc01::1` pub private_ipv6: Ipv6Addr, - /// Port announced to external clients wishing to connect to the wireguard interface. + /// Tunnel 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, + pub tunnel_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 @@ -62,10 +62,10 @@ pub struct Authenticator { impl Default for Authenticator { fn default() -> Self { Self { - bind_address: SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), WG_PORT), + bind_address: SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), WG_TUNNEL_PORT), private_ipv4: WG_TUN_DEVICE_IP_ADDRESS_V4, private_ipv6: WG_TUN_DEVICE_IP_ADDRESS_V6, - announced_port: WG_PORT, + tunnel_announced_port: WG_TUNNEL_PORT, private_network_prefix_v4: WG_TUN_DEVICE_NETMASK_V4, private_network_prefix_v6: WG_TUN_DEVICE_NETMASK_V6, } @@ -78,7 +78,8 @@ impl From for nym_wireguard_types::Config { bind_address: value.bind_address, private_ipv4: value.private_ipv4, private_ipv6: value.private_ipv6, - announced_port: value.announced_port, + announced_tunnel_port: value.tunnel_announced_port, + announced_metadata_port: WG_METADATA_PORT, private_network_prefix_v4: value.private_network_prefix_v4, private_network_prefix_v6: value.private_network_prefix_v6, } diff --git a/gateway/src/node/internal_service_providers/authenticator/mixnet_client.rs b/gateway/src/node/internal_service_providers/authenticator/mixnet_client.rs index 65d9b58e7a..136e1d2708 100644 --- a/gateway/src/node/internal_service_providers/authenticator/mixnet_client.rs +++ b/gateway/src/node/internal_service_providers/authenticator/mixnet_client.rs @@ -3,7 +3,7 @@ use nym_client_core::{config::disk_persistence::CommonClientPaths, TopologyProvider}; use nym_sdk::{GatewayTransceiver, NymNetworkDetails}; -use nym_task::TaskClient; +use nym_task::ShutdownTracker; use crate::node::internal_service_providers::authenticator::{ config::BaseClientConfig, error::AuthenticatorError, @@ -15,7 +15,7 @@ use crate::node::internal_service_providers::authenticator::{ // TODO: refactor this function and its arguments pub async fn create_mixnet_client( config: &BaseClientConfig, - shutdown: TaskClient, + shutdown: ShutdownTracker, custom_transceiver: Option>, custom_topology_provider: Option>, wait_for_gateway: bool, diff --git a/gateway/src/node/internal_service_providers/authenticator/mixnet_listener.rs b/gateway/src/node/internal_service_providers/authenticator/mixnet_listener.rs index 3d343f4fbf..9a6c9d3a50 100644 --- a/gateway/src/node/internal_service_providers/authenticator/mixnet_listener.rs +++ b/gateway/src/node/internal_service_providers/authenticator/mixnet_listener.rs @@ -39,7 +39,7 @@ use nym_sdk::mixnet::{ }; use nym_service_provider_requests_common::{Protocol, ServiceProviderType}; use nym_sphinx::receiver::ReconstructedMessage; -use nym_task::TaskHandle; +use nym_task::ShutdownToken; use nym_wireguard::WireguardGatewayData; use nym_wireguard_types::PeerPublicKey; use rand::{prelude::IteratorRandom, thread_rng}; @@ -70,9 +70,6 @@ pub(crate) struct MixnetListener { // The mixnet client that we use to send and receive packets from the mixnet pub(crate) mixnet_client: nym_sdk::mixnet::MixnetClient, - // The task handle for the main loop - pub(crate) task_handle: TaskHandle, - // Registrations awaiting confirmation pub(crate) registred_and_free: RwLock, @@ -91,7 +88,6 @@ impl MixnetListener { free_private_network_ips: PrivateIPs, wireguard_gateway_data: WireguardGatewayData, mixnet_client: nym_sdk::mixnet::MixnetClient, - task_handle: TaskHandle, ecash_verifier: Arc, ) -> Self { let timeout_check_interval = @@ -99,7 +95,6 @@ impl MixnetListener { MixnetListener { config, mixnet_client, - task_handle, registred_and_free: RwLock::new(RegistredAndFree::new(free_private_network_ips)), peer_manager: PeerManager::new(wireguard_gateway_data), ecash_verifier, @@ -291,7 +286,7 @@ impl MixnetListener { v1::registration::RegistredData { pub_key: PeerPublicKey::new(self.keypair().public_key().to_bytes().into()), private_ip: allowed_ipv4.into(), - wg_port: self.config.authenticator.announced_port, + wg_port: self.config.authenticator.tunnel_announced_port, }, reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?, request_id, @@ -304,7 +299,7 @@ impl MixnetListener { v2::registration::RegistredData { pub_key: PeerPublicKey::new(self.keypair().public_key().to_bytes().into()), private_ip: allowed_ipv4.into(), - wg_port: self.config.authenticator.announced_port, + wg_port: self.config.authenticator.tunnel_announced_port, }, reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?, request_id, @@ -317,7 +312,7 @@ impl MixnetListener { v3::registration::RegistredData { pub_key: PeerPublicKey::new(self.keypair().public_key().to_bytes().into()), private_ip: allowed_ipv4.into(), - wg_port: self.config.authenticator.announced_port, + wg_port: self.config.authenticator.tunnel_announced_port, }, reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?, request_id, @@ -330,7 +325,7 @@ impl MixnetListener { v4::registration::RegistredData { pub_key: PeerPublicKey::new(self.keypair().public_key().to_bytes().into()), private_ips: (allowed_ipv4, allowed_ipv6).into(), - wg_port: self.config.authenticator.announced_port, + wg_port: self.config.authenticator.tunnel_announced_port, }, reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?, request_id, @@ -343,7 +338,7 @@ impl MixnetListener { v5::registration::RegistredData { pub_key: PeerPublicKey::new(self.keypair().public_key().to_bytes().into()), private_ips: (allowed_ipv4, allowed_ipv6).into(), - wg_port: self.config.authenticator.announced_port, + wg_port: self.config.authenticator.tunnel_announced_port, }, request_id, ) @@ -374,7 +369,7 @@ impl MixnetListener { let registration_data = RegistrationData { nonce, gateway_data: gateway_data.clone(), - wg_port: self.config.authenticator.announced_port, + wg_port: self.config.authenticator.tunnel_announced_port, }; registred_and_free .registration_in_progres @@ -691,7 +686,7 @@ impl MixnetListener { } else { let mut verifier = self .peer_manager - .query_verifier(msg.pub_key(), msg.credential()) + .query_verifier_by_key(msg.pub_key(), msg.credential()) .await?; let available_bandwidth = verifier.verify().await?; self.seen_credential_cache @@ -812,14 +807,18 @@ impl MixnetListener { }) } - pub(crate) async fn run(mut self) -> Result<(), AuthenticatorError> { + pub(crate) async fn run( + mut self, + shutdown_token: ShutdownToken, + ) -> Result<(), AuthenticatorError> { tracing::info!("Using authenticator version {CURRENT_VERSION}"); - let mut task_client = self.task_handle.fork("main_loop"); - while !task_client.is_shutdown() { + loop { tokio::select! { - _ = task_client.recv() => { + biased; + _ = shutdown_token.cancelled() => { tracing::debug!("Authenticator [main loop]: received shutdown"); + break; }, _ = self.timeout_check_interval.next() => { if let Err(e) = self.remove_stale_registrations().await { diff --git a/gateway/src/node/internal_service_providers/authenticator/mod.rs b/gateway/src/node/internal_service_providers/authenticator/mod.rs index 12772b6a30..358bc5cb30 100644 --- a/gateway/src/node/internal_service_providers/authenticator/mod.rs +++ b/gateway/src/node/internal_service_providers/authenticator/mod.rs @@ -7,7 +7,7 @@ use ipnetwork::IpNetwork; use nym_client_core::{HardcodedTopologyProvider, TopologyProvider}; use nym_credential_verification::ecash::EcashManager; use nym_sdk::{mixnet::Recipient, GatewayTransceiver}; -use nym_task::{TaskClient, TaskHandle}; +use nym_task::ShutdownTracker; use nym_wireguard::WireguardGatewayData; use std::{net::IpAddr, path::Path, sync::Arc, time::SystemTime}; @@ -40,7 +40,7 @@ pub struct Authenticator { wireguard_gateway_data: WireguardGatewayData, ecash_verifier: Arc, used_private_network_ips: Vec, - shutdown: Option, + shutdown: ShutdownTracker, on_start: Option>, } @@ -50,6 +50,7 @@ impl Authenticator { wireguard_gateway_data: WireguardGatewayData, used_private_network_ips: Vec, ecash_verifier: Arc, + shutdown: ShutdownTracker, ) -> Self { Self { config, @@ -59,18 +60,11 @@ impl Authenticator { ecash_verifier, wireguard_gateway_data, used_private_network_ips, - shutdown: None, + shutdown, on_start: None, } } - #[must_use] - #[allow(unused)] - pub fn with_shutdown(mut self, shutdown: TaskClient) -> Self { - self.shutdown = Some(shutdown); - self - } - #[must_use] #[allow(unused)] pub fn with_wait_for_gateway(mut self, wait_for_gateway: bool) -> Self { @@ -123,14 +117,10 @@ impl Authenticator { pub async fn run_service_provider(self) -> Result<(), AuthenticatorError> { // Used to notify tasks to shutdown. Not all tasks fully supports this (yet). - let task_handle: TaskHandle = self.shutdown.map(Into::into).unwrap_or_default(); - // Connect to the mixnet let mixnet_client = crate::node::internal_service_providers::authenticator::mixnet_client::create_mixnet_client( &self.config.base, - task_handle - .get_handle() - .named("nym_sdk::MixnetClient[AUTH]"), + self.shutdown.clone(), self.custom_gateway_transceiver, self.custom_topology_provider, self.wait_for_gateway, @@ -162,7 +152,6 @@ impl Authenticator { free_private_network_ips, self.wireguard_gateway_data, mixnet_client, - task_handle, self.ecash_verifier, ); @@ -176,6 +165,8 @@ impl Authenticator { } } - mixnet_listener.run().await + mixnet_listener + .run(self.shutdown.clone_shutdown_token()) + .await } } diff --git a/gateway/src/node/internal_service_providers/authenticator/peer_manager.rs b/gateway/src/node/internal_service_providers/authenticator/peer_manager.rs index c13fb57b6b..fce8cf7174 100644 --- a/gateway/src/node/internal_service_providers/authenticator/peer_manager.rs +++ b/gateway/src/node/internal_service_providers/authenticator/peer_manager.rs @@ -4,7 +4,7 @@ use crate::node::internal_service_providers::authenticator::error::AuthenticatorError; use defguard_wireguard_rs::{host::Peer, key::Key}; use futures::channel::oneshot; -use nym_credential_verification::{ClientBandwidth, CredentialVerifier}; +use nym_credential_verification::{ClientBandwidth, TicketVerifier}; use nym_credentials_interface::CredentialSpendingData; use nym_wireguard::{peer_controller::PeerControlRequest, WireguardGatewayData}; use nym_wireguard_types::PeerPublicKey; @@ -99,7 +99,7 @@ impl PeerManager { ) -> Result { let key = Key::new(key.to_bytes()); let (response_tx, response_rx) = oneshot::channel(); - let msg = PeerControlRequest::GetClientBandwidth { key, response_tx }; + let msg = PeerControlRequest::GetClientBandwidthByKey { key, response_tx }; self.wireguard_gateway_data .peer_tx() .send(msg) @@ -120,14 +120,14 @@ impl PeerManager { }) } - pub async fn query_verifier( + pub async fn query_verifier_by_key( &mut self, key: PeerPublicKey, credential: CredentialSpendingData, - ) -> Result { + ) -> Result, AuthenticatorError> { let key = Key::new(key.to_bytes()); let (response_tx, response_rx) = oneshot::channel(); - let msg = PeerControlRequest::GetVerifier { + let msg = PeerControlRequest::GetVerifierByKey { key, credential: Box::new(credential), response_tx, @@ -398,13 +398,13 @@ mod tests { let credential = CredentialSpendingData::try_from_bytes(&CREDENTIAL_BYTES).unwrap(); assert!(peer_manager - .query_verifier(public_key, credential.clone()) + .query_verifier_by_key(public_key, credential.clone()) .await .is_err()); helper_add_peer(&storage, &mut peer_manager).await; peer_manager - .query_verifier(public_key, credential) + .query_verifier_by_key(public_key, credential) .await .unwrap(); diff --git a/gateway/src/node/internal_service_providers/ip_packet_router/config/mod.rs b/gateway/src/node/internal_service_providers/ip_packet_router/config/mod.rs new file mode 100644 index 0000000000..6064f90266 --- /dev/null +++ b/gateway/src/node/internal_service_providers/ip_packet_router/config/mod.rs @@ -0,0 +1,52 @@ +use nym_bin_common::logging::LoggingSettings; +use nym_network_defaults::mainnet; +use url::Url; + +mod persistence; + +pub use crate::service_providers::ip_packet_router::config::persistence::IpPacketRouterPaths; +pub use nym_client_core::config::Config as BaseClientConfig; + +#[derive(Debug, Clone, PartialEq)] +pub struct Config { + pub base: BaseClientConfig, + + pub ip_packet_router: IpPacketRouter, + + pub storage_paths: IpPacketRouterPaths, +} + +impl Config { + pub fn validate(&self) -> bool { + // no other sections have explicit requirements (yet) + self.base.validate() + } + + #[doc(hidden)] + pub fn set_no_poisson_process(&mut self) { + self.base.set_no_poisson_process() + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct IpPacketRouter { + /// Disable Poisson sending rate. + pub disable_poisson_rate: bool, + + /// Specifies the url for an upstream source of the exit policy used by this node. + pub upstream_exit_policy_url: Option, +} + +impl Default for IpPacketRouter { + fn default() -> Self { + IpPacketRouter { + disable_poisson_rate: true, + #[allow(clippy::expect_used)] + upstream_exit_policy_url: Some( + mainnet::EXIT_POLICY_URL + .parse() + .expect("invalid default exit policy URL"), + ), + } + } +} diff --git a/gateway/src/node/internal_service_providers/mod.rs b/gateway/src/node/internal_service_providers/mod.rs index cd4cd051b9..fa5a08ac19 100644 --- a/gateway/src/node/internal_service_providers/mod.rs +++ b/gateway/src/node/internal_service_providers/mod.rs @@ -17,9 +17,9 @@ 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 nym_task::ShutdownTracker; use std::fmt::Display; -use tokio::task::JoinHandle; +use std::marker::PhantomData; use tracing::error; pub mod authenticator; @@ -91,12 +91,11 @@ impl RunnableServiceProvider for Authenticator { pub struct ServiceProviderBeingBuilt { on_start_rx: oneshot::Receiver, sp_builder: T, - sp_message_router_builder: SpMessageRouterBuilder, + sp_message_router_builder: SpMessageRouterBuilder, + shutdown_tracker: ShutdownTracker, } pub struct StartedServiceProvider { - pub sp_join_handle: JoinHandle<()>, - pub message_router_join_handle: JoinHandle<()>, pub on_start_data: T::OnStartData, pub handle: LocalEmbeddedClientHandle, } @@ -109,26 +108,31 @@ where pub(crate) fn new( on_start_rx: oneshot::Receiver, sp_builder: T, - sp_message_router_builder: SpMessageRouterBuilder, + sp_message_router_builder: SpMessageRouterBuilder, + shutdown_tracker: ShutdownTracker, ) -> Self { ServiceProviderBeingBuilt { on_start_rx, sp_builder, sp_message_router_builder, + shutdown_tracker, } } pub async fn start_service_provider( mut self, ) -> Result, 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 - ) - } - }); + self.shutdown_tracker.try_spawn_named( + async move { + if let Err(err) = self.sp_builder.run_service_provider().await { + error!( + "the {} service provider encountered an error: {err}", + T::NAME + ) + } + }, + &format!("{}::Provider", T::NAME), + ); // TODO: if something is blocking during SP startup, the below will wait forever // we need to introduce additional timeouts here. @@ -145,13 +149,10 @@ where }; 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); + self.sp_message_router_builder + .start_message_router(packet_router, &self.shutdown_tracker); Ok(StartedServiceProvider { - sp_join_handle, - message_router_join_handle, handle: LocalEmbeddedClientHandle::new(on_start_data.address(), mix_sender), on_start_data, }) @@ -180,19 +181,19 @@ impl ExitServiceProviders { } } -pub struct SpMessageRouterBuilder { +pub struct SpMessageRouterBuilder { mix_sender: Option, mix_receiver: MixMessageReceiver, router_receiver: oneshot::Receiver, gateway_transceiver: Option, - shutdown: TaskClient, + + _typ: PhantomData, } -impl SpMessageRouterBuilder { +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(); @@ -204,7 +205,7 @@ impl SpMessageRouterBuilder { mix_receiver, router_receiver: router_rx, gateway_transceiver: Some(transceiver), - shutdown, + _typ: Default::default(), } } @@ -224,7 +225,17 @@ impl SpMessageRouterBuilder { .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) + fn start_message_router(self, packet_router: PacketRouter, shutdown_tracker: &ShutdownTracker) + where + T: RunnableServiceProvider, + { + let shutdown_token = shutdown_tracker.clone_shutdown_token(); + let message_router = MessageRouter::new(self.mix_receiver, packet_router); + shutdown_tracker.try_spawn_named( + async move { + message_router.run_with_shutdown(shutdown_token).await; + }, + &format!("{}::MessageRouter", T::NAME), + ); } } diff --git a/gateway/src/node/mod.rs b/gateway/src/node/mod.rs index 14d7571811..e4542b0dd6 100644 --- a/gateway/src/node/mod.rs +++ b/gateway/src/node/mod.rs @@ -1,7 +1,6 @@ // Copyright 2020-2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::config::Config; use crate::error::GatewayError; use crate::node::client_handling::websocket; use crate::node::internal_service_providers::{ @@ -19,7 +18,7 @@ use nym_network_defaults::NymNetworkDetails; use nym_network_requester::NRServiceProviderBuilder; use nym_node_metrics::events::MetricEventsSender; use nym_node_metrics::NymNodeMetrics; -use nym_task::TaskClient; +use nym_task::ShutdownTracker; use nym_topology::TopologyProvider; use nym_validator_client::nyxd::{Coin, CosmWasmClient}; use nym_validator_client::{nyxd, DirectSigningHttpRpcNyxdClient}; @@ -35,6 +34,7 @@ pub(crate) mod client_handling; pub(crate) mod internal_service_providers; mod stale_data_cleaner; +use crate::config::Config; use crate::node::internal_service_providers::authenticator::Authenticator; pub use client_handling::active_clients::ActiveClientsStore; pub use nym_gateway_stats_storage::PersistentStatsStorage; @@ -91,7 +91,7 @@ pub struct GatewayTasksBuilder { mnemonic: Arc>, - shutdown: TaskClient, + shutdown_tracker: ShutdownTracker, // populated and cached as necessary ecash_manager: Option>, @@ -101,14 +101,6 @@ pub struct GatewayTasksBuilder { wireguard_networks: Option>, } -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 GatewayTasksBuilder { #[allow(clippy::too_many_arguments)] pub fn new( @@ -119,7 +111,7 @@ impl GatewayTasksBuilder { metrics_sender: MetricEventsSender, metrics: NymNodeMetrics, mnemonic: Arc>, - shutdown: TaskClient, + shutdown_tracker: ShutdownTracker, ) -> GatewayTasksBuilder { GatewayTasksBuilder { config, @@ -133,7 +125,7 @@ impl GatewayTasksBuilder { metrics_sender, metrics, mnemonic, - shutdown, + shutdown_tracker, ecash_manager: None, wireguard_peers: None, wireguard_networks: None, @@ -223,17 +215,22 @@ impl GatewayTasksBuilder { }; 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?, + + let (ecash_manager, credential_handler) = EcashManager::new( + handler_config, + nyxd_client, + self.identity_keypair.public_key().to_bytes(), + self.storage.clone(), + ) + .await?; + + let shutdown_token = self.shutdown_tracker.clone_shutdown_token(); + self.shutdown_tracker.try_spawn_named( + async move { credential_handler.run(shutdown_token).await }, + "EcashCredentialHandler", ); - Ok(ecash_manager) + + Ok(Arc::new(ecash_manager)) } async fn ecash_manager(&mut self) -> Result, GatewayError> { @@ -270,7 +267,7 @@ impl GatewayTasksBuilder { self.config.gateway.websocket_bind_address, self.config.debug.maximum_open_connections, shared_state, - self.shutdown.fork("websocket"), + self.shutdown_tracker.clone(), )) } @@ -286,18 +283,17 @@ impl GatewayTasksBuilder { let mut message_router_builder = SpMessageRouterBuilder::new( *self.identity_keypair.public_key(), self.mix_packet_sender.clone(), - self.shutdown.fork("network_requester_message_router"), ); let transceiver = message_router_builder.gateway_transceiver(); let (on_start_tx, on_start_rx) = oneshot::channel(); - let mut nr_builder = NRServiceProviderBuilder::new(nr_opts.config.clone()) - .with_shutdown(self.shutdown.fork("network_requester_sp")) - .with_custom_gateway_transceiver(transceiver) - .with_wait_for_gateway(true) - .with_minimum_gateway_performance(0) - .with_custom_topology_provider(topology_provider) - .with_on_start(on_start_tx); + let mut nr_builder = + NRServiceProviderBuilder::new(nr_opts.config.clone(), self.shutdown_tracker.clone()) + .with_custom_gateway_transceiver(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) = &nr_opts.custom_mixnet_path { nr_builder = nr_builder.with_stored_topology(custom_mixnet)? @@ -307,6 +303,7 @@ impl GatewayTasksBuilder { on_start_rx, nr_builder, message_router_builder, + self.shutdown_tracker.clone(), )) } @@ -321,18 +318,17 @@ impl GatewayTasksBuilder { let mut message_router_builder = SpMessageRouterBuilder::new( *self.identity_keypair.public_key(), self.mix_packet_sender.clone(), - self.shutdown.fork("ipr_message_router"), ); let transceiver = message_router_builder.gateway_transceiver(); let (on_start_tx, on_start_rx) = oneshot::channel(); - let mut ip_packet_router = IpPacketRouter::new(ip_opts.config.clone()) - .with_shutdown(self.shutdown.fork("ipr_sp")) - .with_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); + let mut ip_packet_router = + IpPacketRouter::new(ip_opts.config.clone(), self.shutdown_tracker.clone()) + .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)? @@ -342,6 +338,7 @@ impl GatewayTasksBuilder { on_start_rx, ip_packet_router, message_router_builder, + self.shutdown_tracker.clone(), )) } @@ -427,7 +424,6 @@ impl GatewayTasksBuilder { let mut message_router_builder = SpMessageRouterBuilder::new( *self.identity_keypair.public_key(), self.mix_packet_sender.clone(), - self.shutdown.fork("authenticator_message_router"), ); let transceiver = message_router_builder.gateway_transceiver(); @@ -438,9 +434,9 @@ impl GatewayTasksBuilder { wireguard_data.inner.clone(), used_private_network_ips, ecash_manager, + self.shutdown_tracker.clone(), ) .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) @@ -454,13 +450,13 @@ impl GatewayTasksBuilder { on_start_rx, authenticator_server, message_router_builder, + self.shutdown_tracker.clone(), )) } pub fn build_stale_messages_cleaner(&self) -> StaleMessagesCleaner { StaleMessagesCleaner::new( &self.storage, - self.shutdown.fork("stale_messages_cleaner"), self.config.debug.stale_messages_max_age, self.config.debug.stale_messages_cleaner_run_interval, ) @@ -471,13 +467,17 @@ impl GatewayTasksBuilder { &mut self, ) -> Result, Box> { let _ = self.metrics.clone(); + let _ = self.shutdown_tracker.clone(); unimplemented!("wireguard is not supported on this platform") } #[cfg(target_os = "linux")] pub async fn try_start_wireguard( &mut self, - ) -> Result, Box> { + ) -> Result< + nym_wireguard_private_metadata_server::ShutdownHandles, + Box, + > { let all_peers = self.get_wireguard_peers().await?; let Some(wireguard_data) = self.wireguard_data.take() else { @@ -492,14 +492,39 @@ impl GatewayTasksBuilder { ); }; + let router = nym_wireguard_private_metadata_server::RouterBuilder::with_default_routes(); + let router = router.with_state(nym_wireguard_private_metadata_server::AppState::new( + nym_wireguard_private_metadata_server::PeerControllerTransceiver::new( + wireguard_data.inner.peer_tx().clone(), + ), + )); + + let bind_address = std::net::SocketAddr::new( + wireguard_data.inner.config().private_ipv4.into(), + wireguard_data.inner.config().announced_metadata_port, + ); + let wg_handle = nym_wireguard::start_wireguard( ecash_manager, self.metrics.clone(), all_peers, - self.shutdown.fork("wireguard"), + self.shutdown_tracker.clone_shutdown_token(), wireguard_data, ) .await?; - Ok(wg_handle) + + let server = router.build_server(&bind_address).await?; + let cancel_token = self.shutdown_tracker.clone_shutdown_token(); + let server_handle = tokio::spawn(async move { + { + info!("Started Wireguard Axum HTTP V2 server on {bind_address}"); + server.run(cancel_token.cancelled_owned()).await + } + }); + + let shutdown_handles = + nym_wireguard_private_metadata_server::ShutdownHandles::new(server_handle, wg_handle); + + Ok(shutdown_handles) } } diff --git a/gateway/src/node/stale_data_cleaner.rs b/gateway/src/node/stale_data_cleaner.rs index 271acfe1b5..172b865da6 100644 --- a/gateway/src/node/stale_data_cleaner.rs +++ b/gateway/src/node/stale_data_cleaner.rs @@ -2,16 +2,14 @@ // SPDX-License-Identifier: GPL-3.0-only use nym_gateway_storage::{GatewayStorage, InboxManager}; -use nym_task::TaskClient; +use nym_task::ShutdownToken; use std::error::Error; use std::time::Duration; use time::OffsetDateTime; -use tokio::task::JoinHandle; use tracing::{debug, trace, warn}; pub struct StaleMessagesCleaner { inbox_manager: InboxManager, - task_client: TaskClient, max_message_age: Duration, run_interval: Duration, } @@ -19,13 +17,11 @@ pub struct StaleMessagesCleaner { impl StaleMessagesCleaner { pub(crate) fn new( storage: &GatewayStorage, - task_client: TaskClient, max_message_age: Duration, run_interval: Duration, ) -> Self { StaleMessagesCleaner { inbox_manager: storage.inbox_manager().clone(), - task_client, max_message_age, run_interval, } @@ -36,13 +32,14 @@ impl StaleMessagesCleaner { self.inbox_manager.remove_stale(cutoff).await } - async fn run(&mut self) { + pub async fn run(&mut self, shutdown_token: ShutdownToken) { let mut interval = tokio::time::interval(self.run_interval); - while !self.task_client.is_shutdown() { + loop { tokio::select! { biased; - _ = self.task_client.recv() => { + _ = shutdown_token.cancelled() => { trace!("StaleMessagesCleaner: received shutdown"); + break; } _ = interval.tick() => { if let Err(err) = self.clean_up_stale_messages().await { @@ -53,8 +50,4 @@ impl StaleMessagesCleaner { } debug!("StaleMessagesCleaner: Exiting"); } - - pub fn start(mut self) -> JoinHandle<()> { - tokio::spawn(async move { self.run().await }) - } } diff --git a/nym-api/Cargo.toml b/nym-api/Cargo.toml index cabca429ba..ae07fc671c 100644 --- a/nym-api/Cargo.toml +++ b/nym-api/Cargo.toml @@ -93,6 +93,7 @@ nym-task = { path = "../common/task" } nym-topology = { path = "../common/topology" } nym-api-requests = { path = "nym-api-requests" } nym-validator-client = { path = "../common/client-libs/validator-client" } +nym-http-api-client = { path = "../common/http-api-client" } nym-bin-common = { path = "../common/bin-common", features = ["output_format", "openapi", "basic_tracing"] } nym-node-tester-utils = { path = "../common/node-tester-utils" } nym-node-requests = { path = "../nym-node/nym-node-requests" } @@ -126,7 +127,8 @@ cw3 = { workspace = true } cw-utils = { workspace = true } rand_chacha = { workspace = true } sha2 = { workspace = true } -dotenv = "0.15" +dotenvy = { workspace = true } +test-with = { workspace = true, default-features = false } [lints] workspace = true diff --git a/nym-api/build.rs b/nym-api/build.rs index 6bc476b72f..09184d677d 100644 --- a/nym-api/build.rs +++ b/nym-api/build.rs @@ -1,16 +1,20 @@ +use anyhow::Context; use sqlx::{Connection, FromRow, SqliteConnection}; use std::env; const SQLITE_DB_FILENAME: &str = "nym-api-example.sqlite"; -// it's fine if compilation fails -#[allow(clippy::unwrap_used)] -#[allow(clippy::expect_used)] #[tokio::main] -async fn main() { - let out_dir = env::var("OUT_DIR").unwrap(); +async fn main() -> anyhow::Result<()> { + let out_dir = env::var("OUT_DIR")?; let database_path = format!("{out_dir}/{SQLITE_DB_FILENAME}"); + // remove the db file if it already existed from previous build + // in case it was from a different branch + if std::fs::exists(&database_path)? { + std::fs::remove_file(&database_path)?; + } + #[cfg(target_family = "unix")] write_db_path_to_file(&out_dir, SQLITE_DB_FILENAME) .await @@ -18,12 +22,12 @@ async fn main() { let mut conn = SqliteConnection::connect(&format!("sqlite://{database_path}?mode=rwc")) .await - .expect("Failed to create SQLx database connection"); + .context("Failed to create SQLx database connection")?; sqlx::migrate!("./migrations") .run(&mut conn) .await - .expect("Failed to perform SQLx migrations"); + .context("Failed to perform SQLx migrations")?; #[derive(FromRow)] struct Exists { @@ -33,8 +37,7 @@ async fn main() { // check if it was already run let res: Exists = sqlx::query_as("SELECT EXISTS (SELECT 1 FROM v3_migration_info) AS 'exists'") .fetch_one(&mut conn) - .await - .unwrap(); + .await?; let already_run = res.exists; @@ -58,7 +61,7 @@ async fn main() { ) .execute(&mut conn) .await - .expect("failed to update post v3 migration tables"); + .context("failed to update post v3 migration tables")?; } #[cfg(target_family = "unix")] @@ -68,6 +71,8 @@ async fn main() { // for some strange reason we need to add a leading `/` to the windows path even though it's // not a valid windows path... but hey, it works... println!("cargo:rustc-env=DATABASE_URL=sqlite:///{}", &database_path); + + Ok(()) } /// use `./enter_db.sh` to inspect DB diff --git a/nym-api/nym-api-requests/bindings/ts-packages/types/src/types/rust/GatewayCoreStatusResponse.ts b/nym-api/nym-api-requests/bindings/ts-packages/types/src/types/rust/GatewayCoreStatusResponse.ts index 7a7d1ed793..1b60aaeeaf 100644 --- a/nym-api/nym-api-requests/bindings/ts-packages/types/src/types/rust/GatewayCoreStatusResponse.ts +++ b/nym-api/nym-api-requests/bindings/ts-packages/types/src/types/rust/GatewayCoreStatusResponse.ts @@ -1,3 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type GatewayCoreStatusResponse = { identity: string, count: number, }; +export type GatewayCoreStatusResponse = { identity: string, count: bigint, }; diff --git a/nym-api/nym-api-requests/bindings/ts-packages/types/src/types/rust/MixnodeCoreStatusResponse.ts b/nym-api/nym-api-requests/bindings/ts-packages/types/src/types/rust/MixnodeCoreStatusResponse.ts index 8883216403..2ad20c4401 100644 --- a/nym-api/nym-api-requests/bindings/ts-packages/types/src/types/rust/MixnodeCoreStatusResponse.ts +++ b/nym-api/nym-api-requests/bindings/ts-packages/types/src/types/rust/MixnodeCoreStatusResponse.ts @@ -1,3 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type MixnodeCoreStatusResponse = { mix_id: number, count: number, }; +export type MixnodeCoreStatusResponse = { mix_id: number, count: bigint, }; diff --git a/nym-api/nym-api-requests/src/legacy.rs b/nym-api/nym-api-requests/src/legacy.rs deleted file mode 100644 index cf1250653f..0000000000 --- a/nym-api/nym-api-requests/src/legacy.rs +++ /dev/null @@ -1,80 +0,0 @@ -// Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only - -use cosmwasm_std::Decimal; -use nym_mixnet_contract_common::mixnode::LegacyPendingMixNodeChanges; -use nym_mixnet_contract_common::{GatewayBond, LegacyMixLayer, MixNodeBond, NodeId, NodeRewarding}; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use std::ops::Deref; -use utoipa::ToSchema; - -#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, ToSchema)] -pub struct LegacyGatewayBondWithId { - // we need to flatten it so that consumers of endpoints that returned `GatewayBond` wouldn't break - #[serde(flatten)] - pub bond: GatewayBond, - pub node_id: NodeId, -} - -impl Deref for LegacyGatewayBondWithId { - type Target = GatewayBond; - fn deref(&self) -> &Self::Target { - &self.bond - } -} - -impl From for GatewayBond { - fn from(value: LegacyGatewayBondWithId) -> Self { - value.bond - } -} - -#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, ToSchema)] -pub struct LegacyMixNodeBondWithLayer { - // we need to flatten it so that consumers of endpoints that returned `MixNodeBond` wouldn't break - #[serde(flatten)] - pub bond: MixNodeBond, - - pub layer: LegacyMixLayer, -} - -impl Deref for LegacyMixNodeBondWithLayer { - type Target = MixNodeBond; - fn deref(&self) -> &Self::Target { - &self.bond - } -} - -impl From for MixNodeBond { - fn from(value: LegacyMixNodeBondWithLayer) -> Self { - value.bond - } -} - -#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, ToSchema)] -pub struct LegacyMixNodeDetailsWithLayer { - /// Basic bond information of this mixnode, such as owner address, original pledge, etc. - pub bond_information: LegacyMixNodeBondWithLayer, - - /// Details used for computation of rewarding related data. - pub rewarding_details: NodeRewarding, - - /// Adjustments to the mixnode that are ought to happen during future epoch transitions. - #[serde(default)] - pub pending_changes: LegacyPendingMixNodeChanges, -} - -impl LegacyMixNodeDetailsWithLayer { - pub fn mix_id(&self) -> NodeId { - self.bond_information.mix_id - } - - pub fn total_stake(&self) -> Decimal { - self.rewarding_details.node_bond() - } - - pub fn is_unbonding(&self) -> bool { - self.bond_information.is_unbonding - } -} diff --git a/nym-api/nym-api-requests/src/lib.rs b/nym-api/nym-api-requests/src/lib.rs index 371ab9ba56..e7b5e3d5ef 100644 --- a/nym-api/nym-api-requests/src/lib.rs +++ b/nym-api/nym-api-requests/src/lib.rs @@ -7,7 +7,6 @@ use serde::{Deserialize, Serialize}; pub mod constants; pub mod ecash; mod helpers; -pub mod legacy; pub mod models; pub mod nym_nodes; pub mod pagination; diff --git a/nym-api/nym-api-requests/src/models/described.rs b/nym-api/nym-api-requests/src/models/described.rs index 666cca1996..33e82e1642 100644 --- a/nym-api/nym-api-requests/src/models/described.rs +++ b/nym-api/nym-api-requests/src/models/described.rs @@ -8,7 +8,9 @@ use nym_crypto::asymmetric::x25519::serde_helpers::bs58_x25519_pubkey; use nym_crypto::asymmetric::{ed25519, x25519}; use nym_mixnet_contract_common::reward_params::Performance; use nym_mixnet_contract_common::NodeId; -use nym_network_defaults::{DEFAULT_MIX_LISTENING_PORT, DEFAULT_VERLOC_LISTENING_PORT}; +use nym_network_defaults::{ + DEFAULT_MIX_LISTENING_PORT, DEFAULT_VERLOC_LISTENING_PORT, WG_METADATA_PORT, WG_TUNNEL_PORT, +}; use nym_node_requests::api::v1::authenticator::models::Authenticator; use nym_node_requests::api::v1::gateway::models::Wireguard; use nym_node_requests::api::v1::ip_packet_router::models::IpPacketRouter; @@ -311,15 +313,29 @@ impl From for AuthenticatorDetails { } #[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] pub struct WireguardDetails { + // NOTE: the port field is deprecated in favour of tunnel_port pub port: u16, + #[serde(default = "default_tunnel_port")] + pub tunnel_port: u16, + #[serde(default = "default_metadata_port")] + pub metadata_port: u16, pub public_key: String, } +fn default_tunnel_port() -> u16 { + WG_TUNNEL_PORT +} +fn default_metadata_port() -> u16 { + WG_METADATA_PORT +} + // works for current simple case. impl From for WireguardDetails { fn from(value: Wireguard) -> Self { WireguardDetails { port: value.port, + tunnel_port: value.tunnel_port, + metadata_port: value.metadata_port, public_key: value.public_key, } } diff --git a/nym-api/nym-api-requests/src/models/legacy.rs b/nym-api/nym-api-requests/src/models/legacy.rs index c6cb6b2a78..89c654a5f0 100644 --- a/nym-api/nym-api-requests/src/models/legacy.rs +++ b/nym-api/nym-api-requests/src/models/legacy.rs @@ -1,364 +1,26 @@ // Copyright 2025 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use super::{CoinSchema, DeclaredRoles}; -use crate::legacy::{ - LegacyGatewayBondWithId, LegacyMixNodeBondWithLayer, LegacyMixNodeDetailsWithLayer, -}; -use crate::models::{NodePerformance, NymNodeData, StakeSaturation}; -use crate::nym_nodes::{BasicEntryInformation, NodeRole, SemiSkimmedNode, SkimmedNode}; -use cosmwasm_std::{Addr, Coin, Decimal}; -use nym_contracts_common::Percent; -use nym_mixnet_contract_common::reward_params::Performance; -use nym_mixnet_contract_common::rewarding::RewardEstimate; -use nym_mixnet_contract_common::{GatewayBond, Interval, MixNode, NodeId, RewardingParams}; +use nym_mixnet_contract_common::{GatewayBond, MixNodeDetails, NodeId}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use std::fmt; -use std::net::IpAddr; -use std::time::Duration; -use thiserror::Error; -use utoipa::{IntoParams, ToSchema}; +use utoipa::ToSchema; -#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] -pub struct LegacyDescribedGateway { +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, ToSchema)] +pub struct LegacyGatewayBondWithId { + #[serde(flatten)] pub bond: GatewayBond, - pub self_described: Option, + pub node_id: NodeId, } -impl From for LegacyDescribedGateway { - fn from(bond: LegacyGatewayBondWithId) -> Self { - LegacyDescribedGateway { - bond: bond.bond, - self_described: None, - } - } +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, ToSchema)] +pub struct LegacyMixnodesResponse { + pub count: usize, + pub nodes: Vec, } -#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] -pub struct LegacyDescribedMixNode { - pub bond: LegacyMixNodeBondWithLayer, - pub self_described: Option, -} - -impl From for LegacyDescribedMixNode { - fn from(bond: LegacyMixNodeBondWithLayer) -> Self { - LegacyDescribedMixNode { - bond, - self_described: None, - } - } -} - -#[deprecated] -#[derive(Clone, Serialize, schemars::JsonSchema, ToSchema)] -pub struct InclusionProbability { - #[schema(value_type = u32)] - pub mix_id: NodeId, - pub in_active: f64, - pub in_reserve: f64, -} - -#[deprecated] -#[derive(Clone, Serialize, schemars::JsonSchema, ToSchema)] -pub struct AllInclusionProbabilitiesResponse { - pub inclusion_probabilities: Vec, - pub samples: u64, - pub elapsed: Duration, - pub delta_max: f64, - pub delta_l2: f64, - pub as_at: i64, -} - -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, ToSchema)] -#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] -#[cfg_attr( - feature = "generate-ts", - ts( - export, - export_to = "ts-packages/types/src/types/rust/SelectionChance.ts" - ) -)] -#[deprecated] -pub enum SelectionChance { - High, - Good, - Low, -} - -impl From for SelectionChance { - fn from(p: f64) -> SelectionChance { - match p { - p if p >= 0.7 => SelectionChance::High, - p if p >= 0.3 => SelectionChance::Good, - _ => SelectionChance::Low, - } - } -} - -impl From for SelectionChance { - fn from(p: Decimal) -> Self { - match p { - p if p >= Decimal::from_ratio(70u32, 100u32) => SelectionChance::High, - p if p >= Decimal::from_ratio(30u32, 100u32) => SelectionChance::Good, - _ => SelectionChance::Low, - } - } -} - -impl fmt::Display for SelectionChance { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - SelectionChance::High => write!(f, "High"), - SelectionChance::Good => write!(f, "Good"), - SelectionChance::Low => write!(f, "Low"), - } - } -} - -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, ToSchema)] -#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] -#[cfg_attr( - feature = "generate-ts", - ts( - export, - export_to = "ts-packages/types/src/types/rust/InclusionProbabilityResponse.ts" - ) -)] -#[deprecated] -pub struct InclusionProbabilityResponse { - pub in_active: SelectionChance, - pub in_reserve: SelectionChance, -} - -impl fmt::Display for InclusionProbabilityResponse { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "in_active: {}, in_reserve: {}", - self.in_active, self.in_reserve - ) - } -} - -#[derive(Debug, Serialize, Deserialize, JsonSchema, ToSchema, IntoParams)] -pub struct ComputeRewardEstParam { - #[schema(value_type = Option)] - #[param(value_type = Option)] - pub performance: Option, - pub active_in_rewarded_set: Option, - pub pledge_amount: Option, - pub total_delegation: Option, - #[schema(value_type = Option)] - #[param(value_type = Option)] - pub interval_operating_cost: Option, - #[schema(value_type = Option)] - #[param(value_type = Option)] - pub profit_margin_percent: Option, -} - -#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] -#[cfg_attr( - feature = "generate-ts", - ts( - export, - export_to = "ts-packages/types/src/types/rust/RewardEstimationResponse.ts" - ) -)] -#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, ToSchema)] -pub struct RewardEstimationResponse { - pub estimation: RewardEstimate, - pub reward_params: RewardingParams, - pub epoch: Interval, - #[cfg_attr(feature = "generate-ts", ts(type = "number"))] - pub as_at: i64, -} - -impl MixNodeBondAnnotated { - pub fn mix_node(&self) -> &MixNode { - &self.mixnode_details.bond_information.mix_node - } - - pub fn mix_id(&self) -> NodeId { - self.mixnode_details.mix_id() - } - - pub fn identity_key(&self) -> &str { - self.mixnode_details.bond_information.identity() - } - - pub fn owner(&self) -> &Addr { - self.mixnode_details.bond_information.owner() - } - - pub fn version(&self) -> &str { - &self.mixnode_details.bond_information.mix_node.version - } - - pub fn try_to_skimmed_node(&self, role: NodeRole) -> Result { - Ok(SkimmedNode { - node_id: self.mix_id(), - ed25519_identity_pubkey: self - .identity_key() - .parse() - .map_err(|_| MalformedNodeBond::InvalidEd25519Key)?, - ip_addresses: self.ip_addresses.clone(), - mix_port: self.mix_node().mix_port, - x25519_sphinx_pubkey: self - .mix_node() - .sphinx_key - .parse() - .map_err(|_| MalformedNodeBond::InvalidX25519Key)?, - role, - supported_roles: DeclaredRoles { - mixnode: true, - entry: false, - exit_nr: false, - exit_ipr: false, - }, - entry: None, - performance: self.node_performance.last_24h, - }) - } - - pub fn try_to_semi_skimmed_node( - &self, - role: NodeRole, - ) -> Result { - let skimmed_node = self.try_to_skimmed_node(role)?; - Ok(SemiSkimmedNode { - basic: skimmed_node, - x25519_noise_versioned_key: None, // legacy node won't ever support Noise - }) - } -} - -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, ToSchema)] -pub struct GatewayBondAnnotated { - pub gateway_bond: LegacyGatewayBondWithId, - - #[serde(default)] - pub self_described: Option, - - // NOTE: the performance field is deprecated in favour of node_performance - #[schema(value_type = String)] - pub performance: Performance, - pub node_performance: NodePerformance, - pub blacklisted: bool, - - #[serde(default)] - #[schema(value_type = Vec)] - pub ip_addresses: Vec, -} - -impl GatewayBondAnnotated { - pub fn version(&self) -> &str { - &self.gateway_bond.gateway.version - } - - pub fn identity(&self) -> &String { - self.gateway_bond.bond.identity() - } - - pub fn owner(&self) -> &Addr { - self.gateway_bond.bond.owner() - } - - pub fn try_to_skimmed_node(&self, role: NodeRole) -> Result { - Ok(SkimmedNode { - node_id: self.gateway_bond.node_id, - ip_addresses: self.ip_addresses.clone(), - ed25519_identity_pubkey: self - .gateway_bond - .gateway - .identity_key - .parse() - .map_err(|_| MalformedNodeBond::InvalidEd25519Key)?, - mix_port: self.gateway_bond.bond.gateway.mix_port, - x25519_sphinx_pubkey: self - .gateway_bond - .gateway - .sphinx_key - .parse() - .map_err(|_| MalformedNodeBond::InvalidX25519Key)?, - role, - supported_roles: DeclaredRoles { - mixnode: false, - entry: true, - exit_nr: false, - exit_ipr: false, - }, - entry: Some(BasicEntryInformation { - hostname: None, - ws_port: self.gateway_bond.bond.gateway.clients_port, - wss_port: None, - }), - performance: self.node_performance.last_24h, - }) - } - - pub fn try_to_semi_skimmed_node( - &self, - role: NodeRole, - ) -> Result { - let skimmed_node = self.try_to_skimmed_node(role)?; - Ok(SemiSkimmedNode { - basic: skimmed_node, - x25519_noise_versioned_key: None, // legacy node won't ever support Noise - }) - } -} - -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, ToSchema)] -pub struct GatewayDescription { - // for now only expose what we need. this struct will evolve in the future (or be incorporated into nym-node properly) -} - -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, ToSchema)] -#[schema(title = "LegacyMixNodeDetailsWithLayer")] -pub struct LegacyMixNodeDetailsWithLayerSchema { - /// Basic bond information of this mixnode, such as owner address, original pledge, etc. - #[schema(example = "unimplemented schema")] - pub bond_information: String, - - /// Details used for computation of rewarding related data. - #[schema(example = "unimplemented schema")] - pub rewarding_details: String, - - /// Adjustments to the mixnode that are ought to happen during future epoch transitions. - #[schema(example = "unimplemented schema")] - pub pending_changes: String, -} - -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, ToSchema)] -pub struct MixNodeBondAnnotated { - #[schema(value_type = LegacyMixNodeDetailsWithLayerSchema)] - pub mixnode_details: LegacyMixNodeDetailsWithLayer, - #[schema(value_type = String)] - pub stake_saturation: StakeSaturation, - #[schema(value_type = String)] - pub uncapped_stake_saturation: StakeSaturation, - // NOTE: the performance field is deprecated in favour of node_performance - #[schema(value_type = String)] - pub performance: Performance, - pub node_performance: NodePerformance, - #[schema(value_type = String)] - pub estimated_operator_apy: Decimal, - #[schema(value_type = String)] - pub estimated_delegators_apy: Decimal, - pub blacklisted: bool, - - // a rather temporary thing until we query self-described endpoints of mixnodes - #[serde(default)] - #[schema(value_type = Vec)] - pub ip_addresses: Vec, -} - -#[derive(Debug, Error)] -pub enum MalformedNodeBond { - #[error("the associated ed25519 identity key is malformed")] - InvalidEd25519Key, - - #[error("the associated x25519 sphinx key is malformed")] - InvalidX25519Key, +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, ToSchema)] +pub struct LegacyGatewaysResponse { + pub count: usize, + pub nodes: Vec, } diff --git a/nym-api/nym-api-requests/src/models/mod.rs b/nym-api/nym-api-requests/src/models/mod.rs index 76074be659..6d7ab23f96 100644 --- a/nym-api/nym-api-requests/src/models/mod.rs +++ b/nym-api/nym-api-requests/src/models/mod.rs @@ -31,6 +31,7 @@ pub use schema_helpers::*; pub use nym_mixnet_contract_common::{EpochId, KeyRotationId, KeyRotationState}; pub use nym_node_requests::api::v1::node::models::BinaryBuildInformationOwned; +pub use nym_noise_keys::VersionedNoiseKey; #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] pub struct RequestError { diff --git a/nym-api/nym-api-requests/src/models/node_status.rs b/nym-api/nym-api-requests/src/models/node_status.rs index 03bd0ce2d8..6e432c250e 100644 --- a/nym-api/nym-api-requests/src/models/node_status.rs +++ b/nym-api/nym-api-requests/src/models/node_status.rs @@ -340,14 +340,12 @@ pub struct OldHistoricalUptimeResponse { pub struct MixnodeUptimeHistoryResponse { pub mix_id: NodeId, pub identity: String, - pub owner: String, pub history: Vec, } #[derive(Clone, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] pub struct GatewayUptimeHistoryResponse { pub identity: String, - pub owner: String, pub history: Vec, } diff --git a/nym-api/src/ecash/dkg/controller/mod.rs b/nym-api/src/ecash/dkg/controller/mod.rs index 6182d2f175..d5ee8cd14c 100644 --- a/nym-api/src/ecash/dkg/controller/mod.rs +++ b/nym-api/src/ecash/dkg/controller/mod.rs @@ -11,7 +11,7 @@ use anyhow::{bail, Result}; use nym_coconut_dkg_common::types::{Epoch, EpochId, EpochState}; use nym_crypto::asymmetric::ed25519; use nym_dkg::bte::keys::KeyPair as DkgKeyPair; -use nym_task::{TaskClient, TaskManager}; +use nym_task::{ShutdownManager, ShutdownToken}; use rand::rngs::OsRng; use rand::{CryptoRng, Rng, RngCore}; use std::path::PathBuf; @@ -273,7 +273,7 @@ impl DkgController { tick_duration < min } - pub(crate) async fn run(mut self, mut shutdown: TaskClient) { + pub(crate) async fn run(mut self, shutdown: ShutdownToken) { let mut interval = interval(self.polling_rate); interval.set_missed_tick_behavior(MissedTickBehavior::Delay); @@ -282,8 +282,13 @@ impl DkgController { let mut last_polled = OffsetDateTime::now_utc(); let mut last_tick_duration = Default::default(); - while !shutdown.is_shutdown() { + loop { tokio::select! { + biased; + _ = shutdown.cancelled() => { + trace!("DkgController: Received shutdown"); + break; + } _ = interval.tick() => { let now = OffsetDateTime::now_utc(); let tick_duration = now - last_polled; @@ -300,9 +305,6 @@ impl DkgController { error!("failed to update the DKG state: {err}") } } - _ = shutdown.recv() => { - trace!("DkgController: Received shutdown"); - } } } } @@ -314,12 +316,12 @@ impl DkgController { dkg_bte_keypair: DkgKeyPair, identity_key: ed25519::PublicKey, rng: R, - shutdown: &TaskManager, + shutdown_manager: &ShutdownManager, ) -> Result<()> where R: Sync + Send + 'static, { - let shutdown_listener = shutdown.subscribe(); + let shutdown_listener = shutdown_manager.clone_shutdown_token(); let dkg_controller = DkgController::new( config, nyxd_client, diff --git a/nym-api/src/ecash/error.rs b/nym-api/src/ecash/error.rs index 5e4c0e6e89..cd7a205eae 100644 --- a/nym-api/src/ecash/error.rs +++ b/nym-api/src/ecash/error.rs @@ -13,6 +13,7 @@ use nym_dkg::Threshold; use nym_ecash_contract_common::deposit::DepositId; use nym_ecash_contract_common::redeem_credential::BATCH_REDEMPTION_PROPOSAL_TITLE; use nym_validator_client::coconut::EcashApiError; +use nym_validator_client::nym_api::error::NymAPIError; use nym_validator_client::nyxd::error::NyxdError; use nym_validator_client::nyxd::AccountId; use thiserror::Error; @@ -46,6 +47,9 @@ pub enum EcashError { #[error("coconut api query failure: {0}")] CoconutApiError(#[from] EcashApiError), + #[error("nym api query failure: {0}")] + NymApiError(Box), + #[error(transparent)] SerdeJsonError(#[from] serde_json::Error), @@ -161,6 +165,12 @@ pub enum EcashError { MerkleProofGenerationFailure, } +impl From for EcashError { + fn from(e: NymAPIError) -> Self { + EcashError::NymApiError(Box::new(e)) + } +} + // impl<'r, 'o: 'r> Responder<'r, 'o> for EcashError { // fn respond_to(self, _: &'r Request<'_>) -> response::Result<'o> { // let err_msg = self.to_string(); diff --git a/nym-api/src/ecash/helpers.rs b/nym-api/src/ecash/helpers.rs index c636d47157..7e28e86e96 100644 --- a/nym-api/src/ecash/helpers.rs +++ b/nym-api/src/ecash/helpers.rs @@ -50,7 +50,7 @@ impl CredentialRequest for BlindSignRequestBody { } fn ecash_pubkey(&self) -> PublicKeyUser { - self.ecash_pubkey.clone() + self.ecash_pubkey } } @@ -60,7 +60,7 @@ pub(crate) fn blind_sign( ) -> Result { Ok(nym_compact_ecash::scheme::withdrawal::issue( signing_key, - request.ecash_pubkey().clone(), + request.ecash_pubkey(), request.withdrawal_request(), request.expiration_date_timestamp(), request.ticketbook_type(), diff --git a/nym-api/src/ecash/state/cleaner.rs b/nym-api/src/ecash/state/cleaner.rs index 4b4c242e35..478443810b 100644 --- a/nym-api/src/ecash/state/cleaner.rs +++ b/nym-api/src/ecash/state/cleaner.rs @@ -6,7 +6,7 @@ use crate::node_status_api::models::NymApiStorageError; use crate::support::config::Config; use crate::support::storage::NymApiStorage; use nym_ecash_time::ecash_today_date; -use nym_task::TaskClient; +use nym_task::ShutdownToken; use std::time::Duration; use time::Date; use tokio::task::JoinHandle; @@ -20,11 +20,15 @@ pub struct EcashBackgroundStateCleaner { verified_tickets_retention_period_days: u32, storage: NymApiStorage, - task_client: TaskClient, + shutdown_token: ShutdownToken, } impl EcashBackgroundStateCleaner { - pub fn new(global_config: &Config, storage: NymApiStorage, task_client: TaskClient) -> Self { + pub fn new( + global_config: &Config, + storage: NymApiStorage, + shutdown_token: ShutdownToken, + ) -> Self { EcashBackgroundStateCleaner { run_interval: global_config.ecash_signer.debug.stale_data_cleaner_interval, issued_ticketbooks_retention_period_days: global_config @@ -36,7 +40,7 @@ impl EcashBackgroundStateCleaner { .debug .verified_tickets_retention_period_days, storage, - task_client, + shutdown_token, } } @@ -68,7 +72,7 @@ impl EcashBackgroundStateCleaner { let mut ticker = tokio::time::interval(self.run_interval); loop { tokio::select! { - _ = self.task_client.recv() => { + _ = self.shutdown_token.cancelled() => { trace!("EcashBackgroundStateCleaner: Received shutdown"); break; } diff --git a/nym-api/src/ecash/state/mod.rs b/nym-api/src/ecash/state/mod.rs index a4d8a39f46..d18661e70d 100644 --- a/nym-api/src/ecash/state/mod.rs +++ b/nym-api/src/ecash/state/mod.rs @@ -44,8 +44,9 @@ use nym_ecash_contract_common::deposit::{Deposit, DepositId}; use nym_ecash_contract_common::msg::ExecuteMsg; use nym_ecash_contract_common::redeem_credential::BATCH_REDEMPTION_PROPOSAL_TITLE; use nym_ecash_time::{ecash_default_expiration_date, ecash_today_date}; -use nym_task::TaskClient; +use nym_task::ShutdownManager; use nym_ticketbooks_merkle::{IssuedTicketbook, IssuedTicketbooksFullMerkleProof, MerkleLeaf}; +use nym_validator_client::nym_api::NymApiClientExt; use nym_validator_client::nyxd::AccountId; use nym_validator_client::EcashApiClient; use rand::{thread_rng, RngCore}; @@ -126,7 +127,7 @@ impl EcashState { key_pair: KeyPair, comm_channel: D, storage: NymApiStorage, - task_client: TaskClient, + shutdown_manager: &ShutdownManager, ) -> Self where C: LocalClient + Send + Sync + 'static, @@ -135,7 +136,11 @@ impl EcashState { Self { config: EcashStateConfig::new(global_config), background_cleaner_state: BackgroundCleanerState::WaitingStartup( - EcashBackgroundStateCleaner::new(global_config, storage.clone(), task_client), + EcashBackgroundStateCleaner::new( + global_config, + storage.clone(), + shutdown_manager.clone_shutdown_token(), + ), ), global: GlobalEcachState::new(contract_address), local: LocalEcashState::new( diff --git a/nym-api/src/ecash/tests/mod.rs b/nym-api/src/ecash/tests/mod.rs index 2152ae7274..9cd13fd06e 100644 --- a/nym-api/src/ecash/tests/mod.rs +++ b/nym-api/src/ecash/tests/mod.rs @@ -59,7 +59,7 @@ use nym_crypto::asymmetric::ed25519; use nym_dkg::{NodeIndex, Threshold}; use nym_ecash_contract_common::blacklist::{BlacklistedAccountResponse, Blacklisting}; use nym_ecash_contract_common::deposit::{Deposit, DepositId, DepositResponse}; -use nym_task::TaskClient; +use nym_task::ShutdownManager; use nym_validator_client::nym_api::routes::{ ECASH_BLIND_SIGN, ECASH_ISSUED_TICKETBOOKS_CHALLENGE_COMMITMENT, ECASH_ISSUED_TICKETBOOKS_FOR, ECASH_ROUTES, V1_API_VERSION, @@ -67,7 +67,7 @@ use nym_validator_client::nym_api::routes::{ use nym_validator_client::nyxd::cosmwasm_client::logs::Log; use nym_validator_client::nyxd::cosmwasm_client::types::ExecuteResult; use nym_validator_client::nyxd::{AccountId, ExecTxResult, Fee, Hash, TxResponse}; -use nym_validator_client::{EcashApiClient, NymApiClient}; +use nym_validator_client::EcashApiClient; use rand::rngs::OsRng; use rand::RngCore; use std::collections::{BTreeMap, HashMap}; @@ -1148,7 +1148,10 @@ impl DummyCommunicationChannel { cosmos_address: AccountId, ) -> Self { let client = EcashApiClient { - api_client: NymApiClient::new("http://localhost:1234".parse().unwrap()), + api_client: nym_http_api_client::Client::new( + "http://localhost:1234".parse().unwrap(), + None, + ), verification_key: aggregated_verification_key, node_id: 1, cosmos_address, @@ -1348,7 +1351,7 @@ impl TestFixture { staged_key_pair, comm_channel, storage.clone(), - TaskClient::dummy(), + &ShutdownManager::empty_mock(), ); // ideally this would have been generic, but that's way too much work @@ -1489,7 +1492,6 @@ mod credential_tests { use super::*; use crate::ecash::storage::EcashStorageExt; use axum::http::StatusCode; - use nym_task::TaskClient; use nym_ticketbooks_merkle::MerkleLeaf; #[tokio::test] @@ -1580,7 +1582,7 @@ mod credential_tests { staged_key_pair, comm_channel, storage.clone(), - TaskClient::dummy(), + &ShutdownManager::empty_mock(), ); let deposit_id = 42; diff --git a/nym-api/src/epoch_operations/mod.rs b/nym-api/src/epoch_operations/mod.rs index 572b941c99..e8deb036a6 100644 --- a/nym-api/src/epoch_operations/mod.rs +++ b/nym-api/src/epoch_operations/mod.rs @@ -16,12 +16,13 @@ use crate::mixnet_contract_cache::cache::MixnetContractCache; use crate::node_describe_cache::cache::DescribedNodes; use crate::node_status_api::{NodeStatusCache, ONE_DAY}; use crate::support::caching::cache::SharedCache; +use crate::support::caching::refresher::RefreshRequester; use crate::support::nyxd::Client; use crate::support::storage::NymApiStorage; use error::RewardingError; pub(crate) use helpers::RewardedNodeWithParams; use nym_mixnet_contract_common::{CurrentIntervalResponse, Interval}; -use nym_task::{TaskClient, TaskManager}; +use nym_task::{ShutdownManager, ShutdownToken}; use std::time::Duration; use tokio::time::sleep; use tracing::{error, info, trace, warn}; @@ -37,7 +38,8 @@ mod transition_beginning; // this is struct responsible for advancing an epoch pub struct EpochAdvancer { nyxd_client: Client, - nym_contract_cache: MixnetContractCache, + mixnet_contract_cache: MixnetContractCache, + mixnet_contract_cache_refresh_requester: RefreshRequester, described_cache: SharedCache, status_cache: NodeStatusCache, storage: NymApiStorage, @@ -52,14 +54,16 @@ impl EpochAdvancer { pub(crate) fn new( nyxd_client: Client, - nym_contract_cache: MixnetContractCache, + mixnet_contract_cache: MixnetContractCache, + mixnet_contract_cache_refresh_requester: RefreshRequester, status_cache: NodeStatusCache, described_cache: SharedCache, storage: NymApiStorage, ) -> Self { EpochAdvancer { nyxd_client, - nym_contract_cache, + mixnet_contract_cache, + mixnet_contract_cache_refresh_requester, described_cache, status_cache, storage, @@ -119,7 +123,7 @@ impl EpochAdvancer { let epoch_end = interval.current_epoch_end(); - let nym_nodes = self.nym_contract_cache.nym_nodes().await; + let nym_nodes = self.mixnet_contract_cache.nym_nodes().await; if nym_nodes.is_empty() { // that's a bit weird, but ok @@ -161,10 +165,15 @@ impl EpochAdvancer { let cutoff = (epoch_end - 2 * ONE_DAY).unix_timestamp(); self.storage.purge_old_statuses(cutoff).await?; + // after all epoch progression has finished - force refresh the mixnet contract cache, + // so we'd know about new rewarded set (that's easier than manually overwriting the data) + self.mixnet_contract_cache_refresh_requester + .request_cache_refresh(); + Ok(()) } - async fn wait_until_epoch_end(&mut self, shutdown: &mut TaskClient) -> Option { + async fn wait_until_epoch_end(&mut self, shutdown_token: &ShutdownToken) -> Option { const POLL_INTERVAL: Duration = Duration::from_secs(120); loop { @@ -175,7 +184,7 @@ impl EpochAdvancer { _ = sleep(POLL_INTERVAL) => { continue }, - _ = shutdown.recv() => { + _ = shutdown_token.cancelled() => { trace!("wait_until_epoch_end: Received shutdown"); break None } @@ -203,7 +212,7 @@ impl EpochAdvancer { _ = sleep(wait_time) => { }, - _ = shutdown.recv() => { + _ = shutdown_token.cancelled() => { trace!("wait_until_epoch_end: Received shutdown"); break None } @@ -212,17 +221,20 @@ impl EpochAdvancer { } } - pub(crate) async fn run(&mut self, mut shutdown: TaskClient) -> Result<(), RewardingError> { + pub(crate) async fn run( + &mut self, + shutdown_token: ShutdownToken, + ) -> Result<(), RewardingError> { info!("waiting for initial contract cache values before we can start rewarding"); - self.nym_contract_cache + self.mixnet_contract_cache .naive_wait_for_initial_values() .await; info!("waiting for initial self-described cache values before we can start rewarding"); self.described_cache.naive_wait_for_initial_values().await; - while !shutdown.is_shutdown() { - let interval_details = match self.wait_until_epoch_end(&mut shutdown).await { + while !shutdown_token.is_cancelled() { + let interval_details = match self.wait_until_epoch_end(&shutdown_token).await { // received a shutdown None => return Ok(()), Some(interval) => interval, @@ -240,20 +252,22 @@ impl EpochAdvancer { pub(crate) fn start( nyxd_client: Client, nym_contract_cache: &MixnetContractCache, + mixnet_contract_cache_refresh_requester: RefreshRequester, status_cache: &NodeStatusCache, described_cache: SharedCache, storage: &NymApiStorage, - shutdown: &TaskManager, + shutdown_manager: &ShutdownManager, ) { - let mut rewarded_set_updater = EpochAdvancer::new( + let mut epoch_advancer = EpochAdvancer::new( nyxd_client, nym_contract_cache.to_owned(), + mixnet_contract_cache_refresh_requester, status_cache.to_owned(), described_cache, storage.to_owned(), ); - let shutdown_listener = shutdown.subscribe(); - tokio::spawn(async move { rewarded_set_updater.run(shutdown_listener).await }); + let shutdown_listener = shutdown_manager.clone_shutdown_token(); + tokio::spawn(async move { epoch_advancer.run(shutdown_listener).await }); } } diff --git a/nym-api/src/epoch_operations/rewarding.rs b/nym-api/src/epoch_operations/rewarding.rs index e02dba3208..bf95c2f3a1 100644 --- a/nym-api/src/epoch_operations/rewarding.rs +++ b/nym-api/src/epoch_operations/rewarding.rs @@ -81,7 +81,7 @@ impl EpochAdvancer { Ok(rewarded_set) => rewarded_set, Err(err) => { warn!("failed to obtain the current rewarded set: {err}. falling back to the cached version"); - self.nym_contract_cache + self.mixnet_contract_cache .rewarded_set_owned() .await .unwrap() @@ -94,7 +94,7 @@ impl EpochAdvancer { // SAFETY: `EpochAdvancer` is not started until cache is properly initialised let reward_params = self - .nym_contract_cache + .mixnet_contract_cache .interval_reward_params() .await .unwrap(); diff --git a/nym-api/src/key_rotation/mod.rs b/nym-api/src/key_rotation/mod.rs index c6bc0cbe8b..7d7c5220cc 100644 --- a/nym-api/src/key_rotation/mod.rs +++ b/nym-api/src/key_rotation/mod.rs @@ -4,7 +4,7 @@ use crate::mixnet_contract_cache::cache::MixnetContractCache; use crate::support::caching::refresher::{CacheUpdateWatcher, RefreshRequester}; use nym_mixnet_contract_common::{Interval, KeyRotationState}; -use nym_task::TaskClient; +use nym_task::ShutdownToken; use time::OffsetDateTime; use tracing::{debug, error, info, trace}; @@ -110,15 +110,16 @@ impl KeyRotationController { } } - async fn run(&mut self, mut task_client: TaskClient) { + async fn run(&mut self, shutdown_token: ShutdownToken) { self.contract_cache.naive_wait_for_initial_values().await; self.handle_contract_cache_update().await; - while !task_client.is_shutdown() { + loop { tokio::select! { biased; - _ = task_client.recv() => { + _ = shutdown_token.cancelled() => { trace!("KeyRotationController: Received shutdown"); + break; } _ = self.contract_cache_watcher.changed() => { self.handle_contract_cache_update().await @@ -129,8 +130,8 @@ impl KeyRotationController { trace!("KeyRotationController: exiting") } - pub(crate) fn start(mut self, task_client: TaskClient) { - tokio::spawn(async move { self.run(task_client).await }); + pub(crate) fn start(mut self, shutdown_token: ShutdownToken) { + tokio::spawn(async move { self.run(shutdown_token).await }); } } diff --git a/nym-api/src/main.rs b/nym-api/src/main.rs index a36027bd3c..96c7cb22d7 100644 --- a/nym-api/src/main.rs +++ b/nym-api/src/main.rs @@ -6,8 +6,6 @@ use crate::support::cli; use crate::support::storage; use ::nym_config::defaults::setup_env; use clap::Parser; -use mixnet_contract_cache::cache::MixnetContractCache; -use node_status_api::NodeStatusCache; use support::nyxd; use tracing::{info, trace}; diff --git a/nym-api/src/mixnet_contract_cache/cache/data.rs b/nym-api/src/mixnet_contract_cache/cache/data.rs index d648c1037b..be411b5fe9 100644 --- a/nym-api/src/mixnet_contract_cache/cache/data.rs +++ b/nym-api/src/mixnet_contract_cache/cache/data.rs @@ -1,11 +1,10 @@ // Copyright 2022-2023 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use nym_api_requests::legacy::{LegacyGatewayBondWithId, LegacyMixNodeDetailsWithLayer}; -use nym_api_requests::models::ConfigScoreDataResponse; +use nym_api_requests::models::{ConfigScoreDataResponse, LegacyGatewayBondWithId}; use nym_mixnet_contract_common::{ - ConfigScoreParams, HistoricalNymNodeVersionEntry, Interval, KeyRotationState, NymNodeDetails, - RewardingParams, + ConfigScoreParams, HistoricalNymNodeVersionEntry, Interval, KeyRotationState, MixNodeDetails, + NymNodeDetails, RewardingParams, }; use nym_topology::CachedEpochRewardedSet; @@ -31,7 +30,7 @@ impl From for ConfigScoreDataResponse { pub(crate) struct MixnetContractCacheData { pub(crate) rewarding_denom: String, - pub(crate) legacy_mixnodes: Vec, + pub(crate) legacy_mixnodes: Vec, pub(crate) legacy_gateways: Vec, pub(crate) nym_nodes: Vec, pub(crate) rewarded_set: CachedEpochRewardedSet, diff --git a/nym-api/src/mixnet_contract_cache/cache/mod.rs b/nym-api/src/mixnet_contract_cache/cache/mod.rs index 9279c989ec..7168a77acf 100644 --- a/nym-api/src/mixnet_contract_cache/cache/mod.rs +++ b/nym-api/src/mixnet_contract_cache/cache/mod.rs @@ -6,14 +6,11 @@ use crate::node_describe_cache::refresh::RefreshData; use crate::support::caching::cache::{SharedCache, UninitialisedCache}; use crate::support::caching::Cache; use data::MixnetContractCacheData; -use nym_api_requests::legacy::{ - LegacyGatewayBondWithId, LegacyMixNodeBondWithLayer, LegacyMixNodeDetailsWithLayer, -}; -use nym_api_requests::models::{CirculatingSupplyResponse, MixnodeStatus}; +use nym_api_requests::models::{CirculatingSupplyResponse, LegacyGatewayBondWithId}; use nym_contracts_common::truncate_decimal; use nym_crypto::asymmetric::ed25519; use nym_mixnet_contract_common::{ - Interval, KeyRotationState, NodeId, NymNodeDetails, RewardingParams, + Interval, KeyRotationState, MixNodeDetails, NodeId, NymNodeDetails, RewardingParams, }; use nym_topology::CachedEpochRewardedSet; use nym_validator_client::nyxd::Coin; @@ -64,60 +61,34 @@ impl MixnetContractCache { cache.timestamp() } - pub async fn all_cached_legacy_mixnodes( - &self, - ) -> Option>> { - self.get(|c| &c.legacy_mixnodes).await.ok() + pub async fn legacy_mixnodes(&self) -> Result, UninitialisedCache> { + self.get_owned(|c| c.legacy_mixnodes.clone()).await } - pub async fn legacy_gateway_owner(&self, node_id: NodeId) -> Option { - let Ok(cache) = self.inner.get().await else { - return Default::default(); - }; - - cache - .legacy_gateways - .iter() - .find(|gateway| gateway.node_id == node_id) - .map(|gateway| gateway.owner.to_string()) - } - - pub async fn all_cached_legacy_gateways( + pub async fn legacy_gateways( &self, - ) -> Option>> { - self.get(|c| &c.legacy_gateways).await.ok() + ) -> Result, UninitialisedCache> { + self.get_owned(|c| c.legacy_gateways.clone()).await } pub async fn all_cached_nym_nodes(&self) -> Option>> { self.get(|c| &c.nym_nodes).await.ok() } - pub async fn legacy_mixnodes_all(&self) -> Vec { - self.get_owned(|c| c.legacy_mixnodes.clone()) - .await - .unwrap_or_default() - } - - pub async fn legacy_mixnodes_all_basic(&self) -> Vec { - self.legacy_mixnodes_all() - .await - .into_iter() - .map(|bond| bond.bond_information) - .collect() - } - - pub async fn legacy_gateways_all(&self) -> Vec { - self.get_owned(|c| c.legacy_gateways.clone()) - .await - .unwrap_or_default() - } - pub async fn nym_nodes(&self) -> Vec { self.get_owned(|c| c.nym_nodes.clone()) .await .unwrap_or_default() } + pub async fn nym_node( + &self, + node_id: NodeId, + ) -> Result, UninitialisedCache> { + self.get_owned(|c| c.nym_nodes.iter().find(|n| n.node_id() == node_id).cloned()) + .await + } + pub async fn cached_rewarded_set( &self, ) -> Result, UninitialisedCache> { @@ -125,10 +96,6 @@ impl MixnetContractCache { Ok(Cache::as_mapped(&cache, |c| c.rewarded_set.clone())) } - pub async fn rewarded_set(&self) -> Option> { - self.get(|c| &c.rewarded_set).await.ok() - } - pub async fn rewarded_set_owned(&self) -> Result { self.get_owned(|c| c.rewarded_set.clone()).await } @@ -161,18 +128,6 @@ impl MixnetContractCache { .key_rotation_id(current_absolute_epoch_id)) } - pub async fn mixnode_status(&self, mix_id: NodeId) -> MixnodeStatus { - let Ok(cache) = self.inner.get().await else { - return Default::default(); - }; - - if cache.legacy_mixnodes.iter().any(|n| n.mix_id() == mix_id) { - MixnodeStatus::Inactive - } else { - MixnodeStatus::NotFound - } - } - pub async fn get_node_refresh_data( &self, node_identity: ed25519::PublicKey, @@ -189,28 +144,10 @@ impl MixnetContractCache { .iter() .find(|n| n.bond_information.identity() == encoded_identity) { - return nym_node.try_into().ok(); + nym_node.try_into().ok() + } else { + None } - - // 2. check legacy mixnodes - if let Some(mixnode) = cache - .legacy_mixnodes - .iter() - .find(|n| n.bond_information.identity() == encoded_identity) - { - return mixnode.try_into().ok(); - } - - // 3. check legacy gateways - if let Some(gateway) = cache - .legacy_gateways - .iter() - .find(|n| n.identity() == &encoded_identity) - { - return gateway.try_into().ok(); - } - - None } pub(crate) async fn get_circulating_supply(&self) -> Option { diff --git a/nym-api/src/mixnet_contract_cache/cache/refresher.rs b/nym-api/src/mixnet_contract_cache/cache/refresher.rs index 7e0735ca21..3f2e49a1a5 100644 --- a/nym-api/src/mixnet_contract_cache/cache/refresher.rs +++ b/nym-api/src/mixnet_contract_cache/cache/refresher.rs @@ -6,15 +6,9 @@ use crate::nyxd::Client; use crate::support::caching::refresher::CacheItemProvider; use anyhow::Result; use async_trait::async_trait; -use nym_api_requests::legacy::{ - LegacyGatewayBondWithId, LegacyMixNodeBondWithLayer, LegacyMixNodeDetailsWithLayer, -}; -use nym_mixnet_contract_common::LegacyMixLayer; +use nym_api_requests::models::LegacyGatewayBondWithId; use nym_validator_client::nyxd::error::NyxdError; -use rand::prelude::SliceRandom; -use rand::rngs::OsRng; use std::collections::HashMap; -use std::collections::HashSet; use tracing::info; pub struct MixnetContractDataProvider { @@ -42,9 +36,9 @@ impl MixnetContractDataProvider { let contract_state = self.nyxd_client.get_mixnet_contract_state().await?; let nym_nodes = self.nyxd_client.get_nymnodes().await?; - let mixnode_details = self.nyxd_client.get_mixnodes().await?; - let gateway_bonds = self.nyxd_client.get_gateways().await?; - let gateway_ids: HashMap<_, _> = self + let legacy_mixnode_details = self.nyxd_client.get_mixnodes().await?; + let legacy_gateway_bonds = self.nyxd_client.get_gateways().await?; + let legacy_gateway_ids: HashMap<_, _> = self .nyxd_client .get_gateway_ids() .await? @@ -52,12 +46,12 @@ impl MixnetContractDataProvider { .map(|id| (id.identity, id.node_id)) .collect(); - let mut legacy_gateways = Vec::with_capacity(gateway_bonds.len()); + let mut legacy_gateways = Vec::with_capacity(legacy_gateway_bonds.len()); #[allow(clippy::panic)] - for bond in gateway_bonds { + for bond in legacy_gateway_bonds { // we explicitly panic here because that value MUST exist. // if it doesn't, we messed up the migration and we have big problems - let node_id = *gateway_ids.get(bond.identity()).unwrap_or_else(|| { + let node_id = *legacy_gateway_ids.get(bond.identity()).unwrap_or_else(|| { panic!( "CONTRACT DATA INCONSISTENCY: MISSING GATEWAY ID FOR: {}", bond.identity() @@ -67,68 +61,20 @@ impl MixnetContractDataProvider { } let rewarded_set = self.nyxd_client.get_rewarded_set_nodes().await?; - let layer1 = rewarded_set - .assignment - .layer1 - .iter() - .collect::>(); - let layer2 = rewarded_set - .assignment - .layer2 - .iter() - .collect::>(); - let layer3 = rewarded_set - .assignment - .layer3 - .iter() - .collect::>(); - - let layer_choices = [ - LegacyMixLayer::One, - LegacyMixLayer::Two, - LegacyMixLayer::Three, - ]; - let mut rng = OsRng; - let mut legacy_mixnodes = Vec::with_capacity(mixnode_details.len()); - for detail in mixnode_details { - // if node is not in the rewarded set, well. - // slap a random layer on it because legacy clients don't understand a concept of layerless mixnodes - let layer = if layer1.contains(&detail.mix_id()) { - LegacyMixLayer::One - } else if layer2.contains(&detail.mix_id()) { - LegacyMixLayer::Two - } else if layer3.contains(&detail.mix_id()) { - LegacyMixLayer::Three - } else { - // SAFETY: the slice is not empty so the unwrap is fine - #[allow(clippy::unwrap_used)] - layer_choices.choose(&mut rng).copied().unwrap() - }; - - legacy_mixnodes.push(LegacyMixNodeDetailsWithLayer { - bond_information: LegacyMixNodeBondWithLayer { - bond: detail.bond_information, - layer, - }, - rewarding_details: detail.rewarding_details, - pending_changes: detail.pending_changes.into(), - }) - } - let key_rotation_state = self.nyxd_client.get_key_rotation_state().await?; 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?; info!( "Updating validator cache. There are {} [legacy] mixnodes, {} [legacy] gateways and {} nym nodes", - legacy_mixnodes.len(), + legacy_mixnode_details.len(), legacy_gateways.len(), nym_nodes.len(), ); Ok(MixnetContractCacheData { rewarding_denom: contract_state.rewarding_denom, - legacy_mixnodes, + legacy_mixnodes: legacy_mixnode_details, legacy_gateways, nym_nodes, rewarded_set: rewarded_set.into(), diff --git a/nym-api/src/mixnet_contract_cache/handlers.rs b/nym-api/src/mixnet_contract_cache/handlers.rs index e72182701b..23e0b83b16 100644 --- a/nym-api/src/mixnet_contract_cache/handlers.rs +++ b/nym-api/src/mixnet_contract_cache/handlers.rs @@ -1,439 +1,30 @@ // Copyright 2021-2023 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::node_status_api::helpers::{ - _get_active_set_legacy_mixnodes_detailed, _get_legacy_mixnodes_detailed, - _get_rewarded_set_legacy_mixnodes_detailed, -}; -use crate::node_status_api::models::ApiResult; +use crate::node_status_api::models::{ApiResult, AxumResult}; use crate::support::http::state::AppState; -use crate::support::legacy_helpers::{to_legacy_gateway, to_legacy_mixnode}; use axum::extract::{Query, State}; +use axum::routing::get; use axum::Router; -use nym_api_requests::legacy::LegacyMixNodeDetailsWithLayer; -use nym_api_requests::models::{KeyRotationDetails, KeyRotationInfoResponse, MixNodeBondAnnotated}; +use nym_api_requests::models::{ + KeyRotationDetails, KeyRotationInfoResponse, LegacyGatewaysResponse, LegacyMixnodesResponse, +}; use nym_http_api_common::{FormattedResponse, OutputParams}; -use nym_mixnet_contract_common::reward_params::Performance; -use nym_mixnet_contract_common::{reward_params::RewardingParams, GatewayBond, Interval, NodeId}; -use std::collections::HashSet; +use nym_mixnet_contract_common::{reward_params::RewardingParams, Interval}; -// we want to mark the routes as deprecated in swagger, but still expose them -#[allow(deprecated)] -pub(crate) fn nym_contract_cache_routes() -> Router { +// /v1/epoch/ +pub(crate) fn epoch_routes() -> Router { Router::new() - .route("/mixnodes", axum::routing::get(get_mixnodes)) - .route( - "/mixnodes/detailed", - axum::routing::get(get_mixnodes_detailed), - ) - .route("/gateways", axum::routing::get(get_gateways)) - .route("/mixnodes/rewarded", axum::routing::get(get_rewarded_set)) - .route( - "/mixnodes/rewarded/detailed", - axum::routing::get(get_rewarded_set_detailed), - ) - .route("/mixnodes/active", axum::routing::get(get_active_set)) - .route( - "/mixnodes/active/detailed", - axum::routing::get(get_active_set_detailed), - ) - .route( - "/mixnodes/blacklisted", - axum::routing::get(get_blacklisted_mixnodes), - ) - .route( - "/gateways/blacklisted", - axum::routing::get(get_blacklisted_gateways), - ) - .route( - "/epoch/reward_params", - axum::routing::get(get_interval_reward_params), - ) - .route("/epoch/current", axum::routing::get(get_current_epoch)) - .route( - "/epoch/key-rotation-info", - axum::routing::get(get_current_key_rotation_info), - ) + .route("/reward_params", get(get_interval_reward_params)) + .route("/current", get(get_current_epoch)) + .route("/key-rotation-info", get(get_current_key_rotation_info)) } -#[utoipa::path( - tag = "contract-cache", - get, - path = "/v1/mixnodes", - responses( - (status = 200, content( - (Vec = "application/json"), - (Vec = "application/yaml"), - (Vec = "application/bincode") - )) - ), - params(OutputParams) -)] -#[deprecated] -async fn get_mixnodes( - Query(output): Query, - State(state): State, -) -> FormattedResponse> { - let output = output.output.unwrap_or_default(); - - let Ok(describe_cache) = state.described_nodes_cache.get().await else { - return output.to_response(Vec::new()); - }; - - let Some(migrated_nymnodes) = state.nym_contract_cache().all_cached_nym_nodes().await else { - return output.to_response(Vec::new()); - }; - - let Ok(annotations) = state.node_annotations().await else { - return output.to_response(Vec::new()); - }; - - // safety: valid percentage value - #[allow(clippy::unwrap_used)] - let p50 = Performance::from_percentage_value(50).unwrap(); - - let mut nodes = Vec::new(); - for nym_node in &**migrated_nymnodes { - // if we can't get it self-described data, ignore it - let Some(description) = describe_cache.get_description(&nym_node.node_id()) else { - continue; - }; - // if the node hasn't declared it can be a mixnode, ignore it - if !description.declared_role.mixnode { - continue; - } - // if we don't have annotation for this node, ignore it - let Some(annotation) = annotations.get(&nym_node.node_id()) else { - continue; - }; - // equivalent of legacy mixnode being blacklisted - if annotation.last_24h_performance < p50 { - continue; - } - - let node = to_legacy_mixnode(nym_node, description); - nodes.push(node); - } - - output.to_response(nodes) -} - -// DEPRECATED: this endpoint now lives in `node_status_api`. Once all consumers are updated, -// replace this with -// ``` -// pub fn get_mixnodes_detailed() -> Redirect { -// Redirect::to(uri!("/v1/status/mixnodes/detailed")) -// } -// ``` -#[utoipa::path( - tag = "contract-cache", - get, - path = "/v1/mixnodes/detailed", - responses( - (status = 200, content( - (Vec = "application/json"), - (Vec = "application/yaml"), - (Vec = "application/bincode") - )) - ), - params(OutputParams) -)] -#[deprecated] -async fn get_mixnodes_detailed( - Query(output): Query, - State(state): State, -) -> FormattedResponse> { - let output = output.output.unwrap_or_default(); - - output.to_response(_get_legacy_mixnodes_detailed(state.node_status_cache()).await) -} - -#[utoipa::path( - tag = "contract-cache", - get, - path = "/v1/gateways", - responses( - (status = 200, content( - (Vec = "application/json"), - (Vec = "application/yaml"), - (Vec = "application/bincode") - )) - ), - params(OutputParams) -)] -#[deprecated] -async fn get_gateways( - Query(output): Query, - State(state): State, -) -> FormattedResponse> { - let output = output.output.unwrap_or_default(); - - let mut nodes = Vec::new(); - - let Ok(describe_cache) = state.described_nodes_cache.get().await else { - return output.to_response(nodes); - }; - - let Some(migrated_nymnodes) = state.nym_contract_cache().all_cached_nym_nodes().await else { - return output.to_response(nodes); - }; - - let Ok(annotations) = state.node_annotations().await else { - return output.to_response(nodes); - }; - - // safety: valid percentage value - #[allow(clippy::unwrap_used)] - let p50 = Performance::from_percentage_value(50).unwrap(); - - for nym_node in &**migrated_nymnodes { - // if we can't get it self-described data, ignore it - let Some(description) = describe_cache.get_description(&nym_node.node_id()) else { - continue; - }; - // if the node hasn't declared it can be a gateway, ignore it - if !description.declared_role.entry { - continue; - } - // if we don't have annotation for this node, ignore it - let Some(annotation) = annotations.get(&nym_node.node_id()) else { - continue; - }; - // equivalent of legacy gateway being blacklisted - if annotation.last_24h_performance < p50 { - continue; - } - - let node = to_legacy_gateway(nym_node, description); - nodes.push(node); - } - - output.to_response(nodes) -} - -#[utoipa::path( - tag = "contract-cache", - get, - path = "/v1/mixnodes/rewarded", - responses( - (status = 200, content( - (Vec = "application/json"), - (Vec = "application/yaml"), - (Vec = "application/bincode") - )) - ), - params(OutputParams) -)] -#[deprecated] -async fn get_rewarded_set( - Query(output): Query, - State(_state): State, -) -> FormattedResponse> { - let output = output.output.unwrap_or_default(); - - output.to_response(Vec::new()) -} - -// DEPRECATED: this endpoint now lives in `node_status_api`. Once all consumers are updated, -// replace this with -// ``` -// pub fn get_mixnodes_set_detailed() -> Redirect { -// Redirect::to(uri!("/v1/status/mixnodes/rewarded/detailed")) -// } -// ``` -#[utoipa::path( - tag = "contract-cache", - get, - path = "/v1/mixnodes/rewarded/detailed", - responses( - (status = 200, content( - (Vec = "application/json"), - (Vec = "application/yaml"), - (Vec = "application/bincode") - )) - ), - params(OutputParams) -)] -#[deprecated] -async fn get_rewarded_set_detailed( - Query(output): Query, - State(state): State, -) -> FormattedResponse> { - let output = output.output.unwrap_or_default(); - - output.to_response( - _get_rewarded_set_legacy_mixnodes_detailed( - state.node_status_cache(), - state.nym_contract_cache(), - ) - .await, - ) -} - -#[utoipa::path( - tag = "contract-cache", - get, - path = "/v1/mixnodes/active", - responses( - (status = 200, content( - (Vec = "application/json"), - (Vec = "application/yaml"), - (Vec = "application/bincode") - )) - ), - params(OutputParams) -)] -#[deprecated] -async fn get_active_set( - Query(output): Query, - State(state): State, -) -> FormattedResponse> { - let output = output.output.unwrap_or_default(); - - let mut out = Vec::new(); - - let Some(rewarded_set) = state.nym_contract_cache().rewarded_set().await else { - return output.to_response(out); - }; - - let Ok(describe_cache) = state.described_nodes_cache.get().await else { - return output.to_response(out); - }; - - let Some(migrated_nymnodes) = state.nym_contract_cache().all_cached_nym_nodes().await else { - return output.to_response(out); - }; - - let Ok(annotations) = state.node_annotations().await else { - return output.to_response(out); - }; - - // safety: valid percentage value - #[allow(clippy::unwrap_used)] - let p50 = Performance::from_percentage_value(50).unwrap(); - - for nym_node in &**migrated_nymnodes { - // if we can't get it self-described data, ignore it - let Some(description) = describe_cache.get_description(&nym_node.node_id()) else { - continue; - }; - // if the node hasn't declared it can be a mixnode, ignore it - if !description.declared_role.mixnode { - continue; - } - // if we don't have annotation for this node, ignore it - let Some(annotation) = annotations.get(&nym_node.node_id()) else { - continue; - }; - // equivalent of legacy mixnode being blacklisted - if annotation.last_24h_performance < p50 { - continue; - } - // if the node is not in the active set, ignore it - if !rewarded_set.is_active_mixnode(&nym_node.node_id()) { - continue; - } - - let node = to_legacy_mixnode(nym_node, description); - out.push(node); - } - - output.to_response(out) -} - -// DEPRECATED: this endpoint now lives in `node_status_api`. Once all consumers are updated, -// replace this with -// ``` -// pub fn get_active_set_detailed() -> Redirect { -// Redirect::to(uri!("/status/mixnodes/active/detailed")) -// } -// ``` - -#[utoipa::path( - tag = "contract-cache", - get, - path = "/v1/mixnodes/active/detailed", - responses( - (status = 200, content( - (Vec = "application/json"), - (Vec = "application/yaml"), - (Vec = "application/bincode") - )) - ), - params(OutputParams) -)] -#[deprecated] -async fn get_active_set_detailed( - Query(output): Query, - State(state): State, -) -> FormattedResponse> { - let output = output.output.unwrap_or_default(); - - output.to_response( - _get_active_set_legacy_mixnodes_detailed( - state.node_status_cache(), - state.nym_contract_cache(), - ) - .await, - ) -} - -#[utoipa::path( - tag = "contract-cache", - get, - path = "/v1/mixnodes/blacklisted", - responses( - (status = 200, content( - (Option> = "application/json"), - (Option> = "application/yaml"), - (Option> = "application/bincode") - )) - ), - params(OutputParams) -)] -#[deprecated] -async fn get_blacklisted_mixnodes( - Query(output): Query, - State(state): State, -) -> FormattedResponse>> { - let output = output.output.unwrap_or_default(); - - let cache = state.nym_contract_cache(); - - // since blacklist has been removed, the equivalent of a blacklisted node is a legacy node - let mixnodes = cache.legacy_mixnodes_all().await; - output.to_response(Some(mixnodes.into_iter().map(|m| m.mix_id()).collect())) -} - -#[utoipa::path( - tag = "contract-cache", - get, - path = "/v1/gateways/blacklisted", - responses( - (status = 200, content( - (Option> = "application/json"), - (Option> = "application/yaml"), - (Option> = "application/bincode") - )) - ), - params(OutputParams) -)] -#[deprecated] -async fn get_blacklisted_gateways( - Query(output): Query, - State(state): State, -) -> FormattedResponse>> { - let output = output.output.unwrap_or_default(); - - let cache = state.nym_contract_cache(); - // since blacklist has been removed, the equivalent of a blacklisted node is a legacy node - let gateways = cache.legacy_gateways_all().await; - output.to_response(Some( - gateways - .into_iter() - .map(|g| g.gateway.identity_key.clone()) - .collect(), - )) +// /v1/legacy +pub(crate) fn legacy_nodes_routes() -> Router { + Router::new() + .route("/mixnodes", get(get_legacy_mixnodes)) + .route("/gateways", get(get_legacy_gateways)) } #[utoipa::path( @@ -520,3 +111,57 @@ async fn get_current_key_rotation_info( Ok(output.to_response(details.into())) } + +#[utoipa::path( + tag = "contract-cache", + get, + path = "/mixnodes", + context_path = "/v1/legacy", + responses( + (status = 200, content( + (LegacyMixnodesResponse = "application/json"), + (LegacyMixnodesResponse = "application/yaml"), + (LegacyMixnodesResponse = "application/bincode") + )) + ), + params(OutputParams) +)] +async fn get_legacy_mixnodes( + Query(output): Query, + State(state): State, +) -> AxumResult> { + let output = output.get_output(); + let contract_cache = state.nym_contract_cache(); + let mixnodes = contract_cache.legacy_mixnodes().await?; + Ok(output.to_response(LegacyMixnodesResponse { + count: mixnodes.len(), + nodes: mixnodes, + })) +} + +#[utoipa::path( + tag = "contract-cache", + get, + path = "/gateways", + context_path = "/v1/legacy", + responses( + (status = 200, content( + (LegacyGatewaysResponse = "application/json"), + (LegacyGatewaysResponse = "application/yaml"), + (LegacyGatewaysResponse = "application/bincode") + )) + ), + params(OutputParams) +)] +async fn get_legacy_gateways( + Query(output): Query, + State(state): State, +) -> AxumResult> { + let output = output.get_output(); + let contract_cache = state.nym_contract_cache(); + let gateways = contract_cache.legacy_gateways().await?; + Ok(output.to_response(LegacyGatewaysResponse { + count: gateways.len(), + nodes: gateways, + })) +} diff --git a/nym-api/src/network_monitor/mod.rs b/nym-api/src/network_monitor/mod.rs index d0134cafa6..b5e8309a75 100644 --- a/nym-api/src/network_monitor/mod.rs +++ b/nym-api/src/network_monitor/mod.rs @@ -25,7 +25,7 @@ use nym_crypto::asymmetric::{ed25519, x25519}; use nym_sphinx::acknowledgements::AckKey; use nym_sphinx::params::PacketType; use nym_sphinx::receiver::MessageReceiver; -use nym_task::TaskManager; +use nym_task::{ShutdownManager, ShutdownToken}; use std::sync::Arc; use tracing::info; @@ -84,6 +84,7 @@ impl<'a> NetworkMonitorBuilder<'a> { pub(crate) async fn build( self, + shutdown_token: ShutdownToken, ) -> NetworkMonitorRunnables { // TODO: those keys change constant throughout the whole execution of the monitor. // and on top of that, they are used with ALL the gateways -> presumably this should change @@ -127,6 +128,7 @@ impl<'a> NetworkMonitorBuilder<'a> { gateway_status_update_sender, Arc::clone(&identity_keypair), bandwidth_controller, + shutdown_token, ); let received_processor = new_received_processor( @@ -167,12 +169,12 @@ impl NetworkMonitorRunnables { // 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! - pub(crate) fn spawn_tasks(self, shutdown: &TaskManager) { + pub(crate) fn spawn_tasks(self, shutdown: &ShutdownManager) { let mut packet_receiver = self.packet_receiver; let mut monitor = self.monitor; - let shutdown_listener = shutdown.subscribe(); + let shutdown_listener = shutdown.clone_shutdown_token(); tokio::spawn(async move { packet_receiver.run(shutdown_listener).await }); - let shutdown_listener = shutdown.subscribe(); + let shutdown_listener = shutdown.clone_shutdown_token(); tokio::spawn(async move { monitor.run(shutdown_listener).await }); } } @@ -202,12 +204,14 @@ fn new_packet_sender( gateways_status_updater: GatewayClientUpdateSender, local_identity: Arc, bandwidth_controller: BandwidthController, + shutdown_token: ShutdownToken, ) -> PacketSender { PacketSender::new( config, gateways_status_updater, local_identity, bandwidth_controller, + shutdown_token, ) } @@ -241,7 +245,7 @@ pub(crate) async fn start( node_status_cache: NodeStatusCache, storage: &NymApiStorage, nyxd_client: nyxd::Client, - shutdown: &TaskManager, + shutdown: &ShutdownManager, ) { let monitor_builder = setup( config, @@ -252,6 +256,7 @@ pub(crate) async fn start( nyxd_client, ); info!("Starting network monitor..."); - let runnables: NetworkMonitorRunnables = monitor_builder.build().await; + let runnables: NetworkMonitorRunnables = + monitor_builder.build(shutdown.clone_shutdown_token()).await; runnables.spawn_tasks(shutdown); } diff --git a/nym-api/src/network_monitor/monitor/mod.rs b/nym-api/src/network_monitor/monitor/mod.rs index 360a946638..ddd60f7461 100644 --- a/nym-api/src/network_monitor/monitor/mod.rs +++ b/nym-api/src/network_monitor/monitor/mod.rs @@ -12,7 +12,7 @@ use crate::support::config; use nym_mixnet_contract_common::NodeId; use nym_sphinx::params::PacketType; use nym_sphinx::receiver::MessageReceiver; -use nym_task::TaskClient; +use nym_task::ShutdownToken; use std::collections::{HashMap, HashSet}; use tokio::time::{sleep, Duration, Instant}; use tracing::{debug, error, info, trace}; @@ -291,8 +291,6 @@ impl Monitor { prepared_packets.mixnodes_under_test, prepared_packets.gateways_under_test, received, - prepared_packets.invalid_mixnodes, - prepared_packets.invalid_gateways, routes, ); @@ -325,7 +323,7 @@ impl Monitor { self.test_nonce += 1; } - pub(crate) async fn run(&mut self, mut shutdown: TaskClient) { + pub(crate) async fn run(&mut self, shutdown_token: ShutdownToken) { self.received_processor.start_receiving(); // wait for validator cache to be ready @@ -334,20 +332,24 @@ impl Monitor { .await; let mut run_interval = tokio::time::interval(self.run_interval); - while !shutdown.is_shutdown() { + loop { tokio::select! { + biased; + _ = shutdown_token.cancelled() => { + trace!("UpdateHandler: Received shutdown"); + break; + } _ = run_interval.tick() => { tokio::select! { biased; - _ = shutdown.recv() => { + _ = shutdown_token.cancelled() => { trace!("UpdateHandler: Received shutdown"); + break; } _ = self.test_run() => (), } } - _ = shutdown.recv() => { - trace!("UpdateHandler: Received shutdown"); - } + } } } diff --git a/nym-api/src/network_monitor/monitor/preparer.rs b/nym-api/src/network_monitor/monitor/preparer.rs index 49d48ab783..35c870c53c 100644 --- a/nym-api/src/network_monitor/monitor/preparer.rs +++ b/nym-api/src/network_monitor/monitor/preparer.rs @@ -8,8 +8,6 @@ use crate::node_describe_cache::cache::DescribedNodes; use crate::node_describe_cache::NodeDescriptionTopologyExt; use crate::node_status_api::NodeStatusCache; use crate::support::caching::cache::SharedCache; -use crate::support::legacy_helpers::legacy_host_to_ips_and_hostname; -use nym_api_requests::legacy::{LegacyGatewayBondWithId, LegacyMixNodeBondWithLayer}; use nym_api_requests::models::{NodeAnnotation, NymNodeDescription}; use nym_contracts_common::NaiveFloat; use nym_crypto::asymmetric::{ed25519, x25519}; @@ -20,12 +18,10 @@ use nym_sphinx::acknowledgements::AckKey; use nym_sphinx::addressing::clients::Recipient; use nym_sphinx::forwarding::packet::MixPacket; use nym_sphinx::params::{PacketSize, PacketType}; -use nym_topology::node::{EntryDetails, RoutingNode, SupportedRoles}; +use nym_topology::node::RoutingNode; use rand::prelude::SliceRandom; use rand::{rngs::ThreadRng, thread_rng, Rng}; use std::collections::HashMap; -use std::fmt::{self, Display, Formatter}; -use std::net::SocketAddr; use std::sync::Arc; use std::time::Duration; use tracing::{debug, error, info, trace}; @@ -33,29 +29,6 @@ use tracing::{debug, error, info, trace}; const DEFAULT_AVERAGE_PACKET_DELAY: Duration = Duration::from_millis(200); const DEFAULT_AVERAGE_ACK_DELAY: Duration = Duration::from_millis(200); -#[derive(Clone)] -pub(crate) enum InvalidNode { - Malformed { node: TestableNode }, -} - -impl Display for InvalidNode { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - match self { - InvalidNode::Malformed { node } => { - write!(f, "{node} is malformed") - } - } - } -} - -impl From for TestableNode { - fn from(value: InvalidNode) -> Self { - match value { - InvalidNode::Malformed { node } => node, - } - } -} - pub(crate) struct PreparedPackets { /// All packets that are going to get sent during the test as well as the gateways through /// which they ought to be sent. @@ -66,14 +39,6 @@ pub(crate) struct PreparedPackets { /// Vector containing list of public keys and owners of all gateways being tested. pub(super) gateways_under_test: Vec, - - /// All mixnodes that failed to get parsed correctly or were not version compatible. - /// They will be marked to the validator as being down for the test. - pub(super) invalid_mixnodes: Vec, - - /// All gateways that failed to get parsed correctly or were not version compatible. - /// They will be marked to the validator as being down for the test. - pub(super) invalid_gateways: Vec, } #[derive(Clone)] @@ -163,8 +128,6 @@ impl PacketPreparer { info!("Waiting for minimal topology to be online"); let initialisation_backoff = Duration::from_secs(30); loop { - let gateways = self.contract_cache.legacy_gateways_all().await; - let mixnodes = self.contract_cache.legacy_mixnodes_all_basic().await; let nym_nodes = self.contract_cache.nym_nodes().await; #[allow(clippy::expect_used)] @@ -174,8 +137,8 @@ impl PacketPreparer { .await .expect("the self-describe cache should have been initialised!"); - let mut gateways_count = gateways.len(); - let mut mixnodes_count = mixnodes.len(); + let mut gateways_count = 0; + let mut mixnodes_count = 0; for nym_node in nym_nodes { if let Some(described) = described_nodes.get_description(&nym_node.node_id()) { @@ -199,78 +162,6 @@ impl PacketPreparer { } } - async fn all_legacy_mixnodes_and_gateways( - &self, - ) -> ( - Vec, - Vec, - ) { - info!("Obtaining network topology..."); - - let mixnodes = self.contract_cache.legacy_mixnodes_all_basic().await; - let gateways = self.contract_cache.legacy_gateways_all().await; - - (mixnodes, gateways) - } - - pub(crate) fn try_parse_legacy_mix_bond( - &self, - bond: &LegacyMixNodeBondWithLayer, - ) -> Result { - fn parse_bond(bond: &LegacyMixNodeBondWithLayer) -> Option { - let (ips, _) = legacy_host_to_ips_and_hostname(&bond.mix_node.host)?; - - Some(RoutingNode { - node_id: bond.mix_id, - mix_host: SocketAddr::new(*ips.first()?, bond.mix_node.mix_port), - entry: None, - identity_key: ed25519::PublicKey::from_base58_string(&bond.mix_node.identity_key) - .ok()?, - sphinx_key: x25519::PublicKey::from_base58_string(&bond.mix_node.sphinx_key) - .ok()?, - supported_roles: SupportedRoles { - mixnode: true, - mixnet_entry: false, - mixnet_exit: false, - }, - }) - } - - let identity = bond.mix_node.identity_key.clone(); - parse_bond(bond).ok_or(identity) - } - - pub(crate) fn try_parse_legacy_gateway_bond( - &self, - gateway: &LegacyGatewayBondWithId, - ) -> Result { - fn parse_bond(bond: &LegacyGatewayBondWithId) -> Option { - let (ips, hostname) = legacy_host_to_ips_and_hostname(&bond.gateway.host)?; - - Some(RoutingNode { - node_id: bond.node_id, - mix_host: SocketAddr::new(*ips.first()?, bond.gateway.mix_port), - entry: Some(EntryDetails { - ip_addresses: ips, - clients_ws_port: bond.gateway.clients_port, - hostname, - clients_wss_port: None, - }), - identity_key: ed25519::PublicKey::from_base58_string(&bond.gateway.identity_key) - .ok()?, - sphinx_key: x25519::PublicKey::from_base58_string(&bond.gateway.sphinx_key).ok()?, - supported_roles: SupportedRoles { - mixnode: false, - mixnet_entry: true, - mixnet_exit: false, - }, - }) - } - - let identity = gateway.gateway.identity_key.clone(); - parse_bond(gateway).ok_or(identity) - } - fn random_legacy_layer(&self, rng: &mut R) -> LegacyMixLayer { let layer_choices = [ LegacyMixLayer::One, @@ -467,45 +358,6 @@ impl PacketPreparer { ) } - fn filter_outdated_and_malformed_mixnodes( - &self, - nodes: Vec, - ) -> (Vec, Vec) { - let mut parsed_nodes = Vec::new(); - let mut invalid_nodes = Vec::new(); - for mixnode in nodes { - if let Ok(parsed_node) = self.try_parse_legacy_mix_bond(&mixnode) { - parsed_nodes.push(parsed_node) - } else { - invalid_nodes.push(InvalidNode::Malformed { - node: TestableNode::new_mixnode(mixnode.identity().to_owned(), mixnode.mix_id), - }); - } - } - (parsed_nodes, invalid_nodes) - } - - fn filter_outdated_and_malformed_gateways( - &self, - nodes: Vec, - ) -> (Vec, Vec) { - let mut parsed_nodes = Vec::new(); - let mut invalid_nodes = Vec::new(); - for gateway in nodes { - if let Ok(parsed_node) = self.try_parse_legacy_gateway_bond(&gateway) { - parsed_nodes.push(parsed_node) - } else { - invalid_nodes.push(InvalidNode::Malformed { - node: TestableNode::new_gateway( - gateway.bond.identity().to_owned(), - gateway.node_id, - ), - }); - } - } - (parsed_nodes, invalid_nodes) - } - fn nym_node_to_routing_node( &self, current_rotation_id: u32, @@ -521,8 +373,6 @@ impl PacketPreparer { // TODO: Maybe do this _packet_type: PacketType, ) -> PreparedPackets { - let (mixnodes, gateways) = self.all_legacy_mixnodes_and_gateways().await; - // SAFETY: cache has already been initialised #[allow(clippy::unwrap_used)] let current_rotation_id = self.contract_cache.current_key_rotation_id().await.unwrap(); @@ -536,20 +386,10 @@ impl PacketPreparer { let mixing_nym_nodes = descriptions.mixing_nym_nodes(); let gateway_capable_nym_nodes = descriptions.entry_capable_nym_nodes(); - let (mut mixnodes_to_test_details, invalid_mixnodes) = - self.filter_outdated_and_malformed_mixnodes(mixnodes); - let (mut gateways_to_test_details, invalid_gateways) = - self.filter_outdated_and_malformed_gateways(gateways); - - // summary of nodes that got tested - let mut mixnodes_under_test = mixnodes_to_test_details - .iter() - .map(|node| TestableNode::new_routing(node, NodeType::Mixnode)) - .collect::>(); - let mut gateways_under_test = gateways_to_test_details - .iter() - .map(|node| TestableNode::new_routing(node, NodeType::Gateway)) - .collect::>(); + let mut mixnodes_to_test_details = Vec::new(); + let mut gateways_to_test_details = Vec::new(); + let mut mixnodes_under_test = Vec::new(); + let mut gateways_under_test = Vec::new(); // try to add nym-nodes into the fold for mix in mixing_nym_nodes { @@ -647,8 +487,6 @@ impl PacketPreparer { packets, mixnodes_under_test, gateways_under_test, - invalid_mixnodes, - invalid_gateways, } } } diff --git a/nym-api/src/network_monitor/monitor/receiver.rs b/nym-api/src/network_monitor/monitor/receiver.rs index e84398b9b4..2428713c58 100644 --- a/nym-api/src/network_monitor/monitor/receiver.rs +++ b/nym-api/src/network_monitor/monitor/receiver.rs @@ -7,7 +7,7 @@ use futures::channel::mpsc; use futures::StreamExt; use nym_crypto::asymmetric::ed25519; use nym_gateway_client::{AcknowledgementReceiver, MixnetMessageReceiver}; -use nym_task::TaskClient; +use nym_task::ShutdownToken; use tracing::{error, trace}; pub(crate) type GatewayClientUpdateSender = mpsc::UnboundedSender; @@ -57,12 +57,13 @@ impl PacketReceiver { } } - pub(crate) async fn run(&mut self, mut shutdown: TaskClient) { - while !shutdown.is_shutdown() { + pub(crate) async fn run(&mut self, shutdown_token: ShutdownToken) { + loop { tokio::select! { biased; - _ = shutdown.recv() => { + _ = shutdown_token.cancelled() => { trace!("UpdateHandler: Received shutdown"); + break; } // unwrap here is fine as it can only return a `None` if the PacketSender has died // and if that was the case, then the entire monitor is already in an undefined state diff --git a/nym-api/src/network_monitor/monitor/sender.rs b/nym-api/src/network_monitor/monitor/sender.rs index fa6ba454c9..358fb8e316 100644 --- a/nym-api/src/network_monitor/monitor/sender.rs +++ b/nym-api/src/network_monitor/monitor/sender.rs @@ -20,6 +20,7 @@ use nym_gateway_client::{ AcknowledgementReceiver, GatewayClient, MixnetMessageReceiver, PacketRouter, SharedGatewayKey, }; use nym_sphinx::forwarding::packet::MixPacket; +use nym_task::ShutdownToken; use pin_project::pin_project; use sqlx::__rt::timeout; use std::mem; @@ -91,6 +92,7 @@ impl GatewayPackets { struct FreshGatewayClientData { gateways_status_updater: GatewayClientUpdateSender, local_identity: Arc, + shutdown_token: ShutdownToken, gateway_response_timeout: Duration, bandwidth_controller: BandwidthController, disabled_credentials_mode: bool, @@ -127,11 +129,13 @@ impl PacketSender { gateways_status_updater: GatewayClientUpdateSender, local_identity: Arc, bandwidth_controller: BandwidthController, + shutdown_token: ShutdownToken, ) -> Self { PacketSender { fresh_gateway_client_data: Arc::new(FreshGatewayClientData { gateways_status_updater, local_identity, + shutdown_token, gateway_response_timeout: config.network_monitor.debug.gateway_response_timeout, bandwidth_controller, disabled_credentials_mode: config.network_monitor.debug.disabled_credentials_mode, @@ -154,10 +158,6 @@ impl PacketSender { GatewayClientHandle, (MixnetMessageReceiver, AcknowledgementReceiver), ) { - // I think the proper one should be passed around instead... - let task_client = - nym_task::TaskClient::dummy().named(format!("gateway-{}", config.gateway_identity)); - let (message_sender, message_receiver) = mpsc::unbounded(); // currently we do not care about acks at all, but we must keep the channel alive @@ -167,7 +167,7 @@ impl PacketSender { let gateway_packet_router = PacketRouter::new( ack_sender, message_sender, - task_client.fork("packet_router"), + fresh_gateway_client_data.shutdown_token.clone(), ); let shared_keys = fresh_gateway_client_data @@ -186,11 +186,11 @@ impl PacketSender { Some(fresh_gateway_client_data.bandwidth_controller.clone()), nym_statistics_common::clients::ClientStatsSender::new( None, - task_client.fork("client_stats_sender"), + fresh_gateway_client_data.shutdown_token.clone(), ), #[cfg(unix)] None, - task_client, + fresh_gateway_client_data.shutdown_token.clone(), ); ( diff --git a/nym-api/src/network_monitor/monitor/summary_producer.rs b/nym-api/src/network_monitor/monitor/summary_producer.rs index a55212c89c..ebd7686105 100644 --- a/nym-api/src/network_monitor/monitor/summary_producer.rs +++ b/nym-api/src/network_monitor/monitor/summary_producer.rs @@ -1,7 +1,6 @@ // Copyright 2021-2023 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::network_monitor::monitor::preparer::InvalidNode; use crate::network_monitor::test_packet::NodeTestMessage; use crate::network_monitor::test_route::TestRoute; use nym_node_tester_utils::node::{NodeType, TestableNode}; @@ -283,8 +282,6 @@ impl SummaryProducer { tested_mixnodes: Vec, tested_gateways: Vec, received_packets: Vec, - invalid_mixnodes: Vec, - invalid_gateways: Vec, test_routes: &[TestRoute], ) -> TestSummary { // we expect each route to receive this many packets in the ideal world @@ -307,14 +304,6 @@ impl SummaryProducer { raw_results.insert(tested_gateway, 0); } - for invalid_mixnode in invalid_mixnodes { - raw_results.insert(invalid_mixnode.into(), 0); - } - - for invalid_gateway in invalid_gateways { - raw_results.insert(invalid_gateway.into(), 0); - } - for received in received_packets { *raw_results.entry(received.tested_node).or_default() += 1usize; *raw_route_results.entry(received.ext.route_id).or_default() += 1usize; diff --git a/nym-api/src/node_describe_cache/mod.rs b/nym-api/src/node_describe_cache/mod.rs index 096555aa26..b72c1a9103 100644 --- a/nym-api/src/node_describe_cache/mod.rs +++ b/nym-api/src/node_describe_cache/mod.rs @@ -30,7 +30,7 @@ pub enum NodeDescribeCacheError { node_id: NodeId, #[source] - source: NymNodeApiClientError, + source: Box, }, #[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)] @@ -41,7 +41,7 @@ pub enum NodeDescribeCacheError { node_id: NodeId, #[source] - source: NymNodeApiClientError, + source: Box, }, // TODO: perhaps include more details here like whether key/signature/payload was malformed diff --git a/nym-api/src/node_describe_cache/provider.rs b/nym-api/src/node_describe_cache/provider.rs index 259b14ba1e..b0ed9713d9 100644 --- a/nym-api/src/node_describe_cache/provider.rs +++ b/nym-api/src/node_describe_cache/provider.rs @@ -50,35 +50,8 @@ impl CacheItemProvider for NodeDescriptionProvider { } async fn try_refresh(&mut self) -> Result, Self::Error> { - // we need to query: - // - legacy mixnodes (because they might already be running nym-nodes, but haven't updated contract info) - // - legacy gateways (because they might already be running nym-nodes, but haven't updated contract info) - // - nym-nodes - let mut nodes_to_query: Vec = Vec::new(); - match self.contract_cache.all_cached_legacy_mixnodes().await { - None => error!("failed to obtain mixnodes information from the cache"), - Some(legacy_mixnodes) => { - for node in &**legacy_mixnodes { - if let Ok(data) = node.try_into() { - nodes_to_query.push(data); - } - } - } - } - - match self.contract_cache.all_cached_legacy_gateways().await { - None => error!("failed to obtain gateways information from the cache"), - Some(legacy_gateways) => { - for node in &**legacy_gateways { - if let Ok(data) = node.try_into() { - nodes_to_query.push(data); - } - } - } - } - match self.contract_cache.all_cached_nym_nodes().await { None => error!("failed to obtain nym-nodes information from the cache"), Some(nym_nodes) => { diff --git a/nym-api/src/node_describe_cache/query_helpers.rs b/nym-api/src/node_describe_cache/query_helpers.rs index 78611ca624..182257bcc0 100644 --- a/nym-api/src/node_describe_cache/query_helpers.rs +++ b/nym-api/src/node_describe_cache/query_helpers.rs @@ -41,7 +41,10 @@ pub(crate) async fn query_for_described_data( client: &Client, node_id: NodeId, ) -> Result { - let map_query_err = |source| NodeDescribeCacheError::ApiFailure { node_id, source }; + let map_query_err = |source| NodeDescribeCacheError::ApiFailure { + node_id, + source: Box::new(source), + }; // all of those should be happening concurrently. NodeDescribedInfoMegaFuture::new( diff --git a/nym-api/src/node_describe_cache/refresh.rs b/nym-api/src/node_describe_cache/refresh.rs index 6f3949c6c6..24fea96c44 100644 --- a/nym-api/src/node_describe_cache/refresh.rs +++ b/nym-api/src/node_describe_cache/refresh.rs @@ -3,7 +3,6 @@ use crate::node_describe_cache::query_helpers::query_for_described_data; use crate::node_describe_cache::NodeDescribeCacheError; -use nym_api_requests::legacy::{LegacyGatewayBondWithId, LegacyMixNodeDetailsWithLayer}; use nym_api_requests::models::{DescribedNodeType, NymNodeDescription}; use nym_bin_common::bin_info; use nym_config::defaults::DEFAULT_NYM_NODE_HTTP_PORT; @@ -24,34 +23,6 @@ pub(crate) struct RefreshData { port: Option, } -impl<'a> TryFrom<&'a LegacyMixNodeDetailsWithLayer> for RefreshData { - type Error = ed25519::Ed25519RecoveryError; - - fn try_from(node: &'a LegacyMixNodeDetailsWithLayer) -> Result { - Ok(RefreshData::new( - &node.bond_information.mix_node.host, - node.bond_information.identity().parse()?, - DescribedNodeType::LegacyMixnode, - node.mix_id(), - Some(node.bond_information.mix_node.http_api_port), - )) - } -} - -impl<'a> TryFrom<&'a LegacyGatewayBondWithId> for RefreshData { - type Error = ed25519::Ed25519RecoveryError; - - fn try_from(node: &'a LegacyGatewayBondWithId) -> Result { - Ok(RefreshData::new( - &node.bond.gateway.host, - node.bond.identity().parse()?, - DescribedNodeType::LegacyGateway, - node.node_id, - None, - )) - } -} - impl<'a> TryFrom<&'a NymNodeDetails> for RefreshData { type Error = ed25519::Ed25519RecoveryError; @@ -132,7 +103,7 @@ async fn try_get_client( return Err(NodeDescribeCacheError::MalformedHost { host: host.to_string(), node_id, - source: err, + source: Box::new(err), }); } }; @@ -158,7 +129,7 @@ async fn try_get_description( let map_query_err = |err| NodeDescribeCacheError::ApiFailure { node_id: data.node_id, - source: err, + source: Box::new(err), }; let host_info = client.get_host_information().await.map_err(map_query_err)?; diff --git a/nym-api/src/node_performance/contract_cache/mod.rs b/nym-api/src/node_performance/contract_cache/mod.rs index 412b2f00b1..518f00a789 100644 --- a/nym-api/src/node_performance/contract_cache/mod.rs +++ b/nym-api/src/node_performance/contract_cache/mod.rs @@ -10,7 +10,7 @@ use crate::support::caching::cache::SharedCache; use crate::support::caching::refresher::CacheRefresher; use crate::support::{config, nyxd}; use anyhow::bail; -use nym_task::TaskManager; +use nym_task::ShutdownManager; pub(crate) mod data; pub(crate) mod refresher; @@ -19,7 +19,7 @@ pub(crate) async fn start_cache_refresher( config: &config::PerformanceProvider, nyxd_client: nyxd::Client, mixnet_contract_cache: MixnetContractCache, - task_manager: &TaskManager, + shutdown_manager: &ShutdownManager, ) -> anyhow::Result> { let values_to_retain = config.debug.max_epoch_entries_to_retain; @@ -45,7 +45,7 @@ pub(crate) async fn start_cache_refresher( .with_update_fn(move |main_cache, update| { refresher_update_fn(main_cache, update, values_to_retain) }) - .start(task_manager.subscribe_named("performance-contract-cache-refresher")); + .start(shutdown_manager.clone_shutdown_token()); Ok(warmed_up_cache) } diff --git a/nym-api/src/node_status_api/cache/data.rs b/nym-api/src/node_status_api/cache/data.rs index b66e25de15..1a174eae6b 100644 --- a/nym-api/src/node_status_api/cache/data.rs +++ b/nym-api/src/node_status_api/cache/data.rs @@ -2,26 +2,15 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::support::caching::Cache; -use nym_api_requests::models::{GatewayBondAnnotated, MixNodeBondAnnotated, NodeAnnotation}; -use nym_contracts_common::IdentityKey; +use nym_api_requests::models::NodeAnnotation; use nym_mixnet_contract_common::NodeId; use std::collections::HashMap; #[derive(Default)] #[allow(deprecated)] pub(crate) struct NodeStatusCacheData { - pub(crate) legacy_gateway_mapping: Cache>, - - /// Basic annotation for **all** nodes, i.e. legacy + nym-nodes + /// Basic annotation for nym-nodes pub(crate) node_annotations: Cache>, - - /// Annotations as before, just for legacy things - pub(crate) mixnodes_annotated: Cache>, - pub(crate) gateways_annotated: Cache>, - - // Estimated active set inclusion probabilities from Monte Carlo simulation - pub(crate) inclusion_probabilities: - Cache, } impl NodeStatusCacheData { diff --git a/nym-api/src/node_status_api/cache/inclusion_probabilities.rs b/nym-api/src/node_status_api/cache/inclusion_probabilities.rs deleted file mode 100644 index 3cc9abc807..0000000000 --- a/nym-api/src/node_status_api/cache/inclusion_probabilities.rs +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright 2023 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only - -#![allow(deprecated)] - -use nym_api_requests::legacy::LegacyMixNodeDetailsWithLayer; -use nym_api_requests::models::InclusionProbability; -use nym_mixnet_contract_common::NodeId; -use serde::Serialize; -use std::time::Duration; - -#[deprecated] -#[derive(Clone, Default, Serialize, schemars::JsonSchema)] -pub(crate) struct InclusionProbabilities { - pub inclusion_probabilities: Vec, - pub samples: u64, - pub elapsed: Duration, - pub delta_max: f64, - pub delta_l2: f64, -} - -impl InclusionProbabilities { - pub(crate) fn legacy_zero( - mixnodes: &[LegacyMixNodeDetailsWithLayer], - ) -> InclusionProbabilities { - // (all legacy mixnodes have 0% chance of being selected) - InclusionProbabilities { - inclusion_probabilities: mixnodes - .iter() - .map(|m| InclusionProbability { - mix_id: m.mix_id(), - in_active: 0.0, - in_reserve: 0.0, - }) - .collect(), - samples: 0, - elapsed: Default::default(), - delta_max: 0.0, - delta_l2: 0.0, - } - } - - pub(crate) fn node(&self, mix_id: NodeId) -> Option<&InclusionProbability> { - self.inclusion_probabilities - .iter() - .find(|x| x.mix_id == mix_id) - } -} diff --git a/nym-api/src/node_status_api/cache/mod.rs b/nym-api/src/node_status_api/cache/mod.rs index 8791341b7a..32fd552f18 100644 --- a/nym-api/src/node_status_api/cache/mod.rs +++ b/nym-api/src/node_status_api/cache/mod.rs @@ -5,8 +5,7 @@ use self::data::NodeStatusCacheData; use crate::node_performance::provider::PerformanceRetrievalFailure; use crate::support::caching::cache::UninitialisedCache; use crate::support::caching::Cache; -use nym_api_requests::models::{GatewayBondAnnotated, MixNodeBondAnnotated, NodeAnnotation}; -use nym_contracts_common::IdentityKey; +use nym_api_requests::models::NodeAnnotation; use nym_mixnet_contract_common::NodeId; use std::collections::HashMap; use std::{sync::Arc, time::Duration}; @@ -19,7 +18,6 @@ const CACHE_TIMEOUT_MS: u64 = 100; mod config_score; pub mod data; -mod inclusion_probabilities; pub mod refresher; #[derive(Debug, Error)] @@ -58,45 +56,15 @@ impl NodeStatusCache { } /// Updates the cache with the latest data. - #[allow(deprecated)] - async fn update( - &self, - legacy_gateway_mapping: HashMap, - node_annotations: HashMap, - mixnodes: HashMap, - gateways: HashMap, - inclusion_probabilities: inclusion_probabilities::InclusionProbabilities, - ) { + async fn update(&self, node_annotations: HashMap) { match time::timeout(Duration::from_millis(CACHE_TIMEOUT_MS), self.inner.write()).await { Ok(mut cache) => { - cache.mixnodes_annotated.unchecked_update(mixnodes); - cache - .legacy_gateway_mapping - .unchecked_update(legacy_gateway_mapping); cache.node_annotations.unchecked_update(node_annotations); - cache.gateways_annotated.unchecked_update(gateways); - cache - .inclusion_probabilities - .unchecked_update(inclusion_probabilities); } Err(e) => error!("{e}"), } } - /// Returns a copy of the current cache data. - async fn get_owned( - &self, - fn_arg: impl FnOnce(RwLockReadGuard<'_, NodeStatusCacheData>) -> Cache, - ) -> Option> { - match time::timeout(Duration::from_millis(CACHE_TIMEOUT_MS), self.inner.read()).await { - Ok(cache) => Some(fn_arg(cache)), - Err(e) => { - error!("{e}"); - None - } - } - } - async fn get<'a, T: 'a>( &'a self, fn_arg: impl FnOnce(&NodeStatusCacheData) -> &Cache, @@ -115,67 +83,4 @@ impl NodeStatusCache { ) -> Option>>> { self.get(|c| &c.node_annotations).await } - - pub(crate) async fn map_identity_to_node_id(&self, identity: &str) -> Option { - self.inner - .read() - .await - .legacy_gateway_mapping - .get(identity) - .copied() - } - - pub(crate) async fn annotated_legacy_mixnodes( - &self, - ) -> Option>>> { - self.get(|c| &c.mixnodes_annotated).await - } - - pub(crate) async fn mixnodes_annotated_full(&self) -> Option> { - let mixnodes = self.get(|c| &c.mixnodes_annotated).await?; - - // just clone everything and return the vec to work with the existing code - Some(mixnodes.values().cloned().collect()) - } - - pub(crate) async fn mixnodes_annotated_filtered(&self) -> Option> { - let full = self.mixnodes_annotated_full().await?; - Some(full.iter().filter(|m| !m.blacklisted).cloned().collect()) - } - - pub(crate) async fn mixnode_annotated(&self, mix_id: NodeId) -> Option { - let mixnodes = self.get(|c| &c.mixnodes_annotated).await?; - mixnodes.get(&mix_id).cloned() - } - - pub(crate) async fn annotated_legacy_gateways( - &self, - ) -> Option>>> { - self.get(|c| &c.gateways_annotated).await - } - - pub(crate) async fn gateways_annotated_full(&self) -> Option> { - let gateways = self.get(|c| &c.gateways_annotated).await?; - - // just clone everything and return the vec to work with the existing code - Some(gateways.values().cloned().collect()) - } - - pub(crate) async fn gateways_annotated_filtered(&self) -> Option> { - let full = self.gateways_annotated_full().await?; - Some(full.iter().filter(|m| !m.blacklisted).cloned().collect()) - } - - pub(crate) async fn gateway_annotated(&self, node_id: NodeId) -> Option { - let gateways = self.get(|c| &c.gateways_annotated).await?; - gateways.get(&node_id).cloned() - } - - #[allow(deprecated)] - pub(crate) async fn inclusion_probabilities( - &self, - ) -> Option> { - self.get_owned(|c| c.inclusion_probabilities.clone_cache()) - .await - } } diff --git a/nym-api/src/node_status_api/cache/refresher.rs b/nym-api/src/node_status_api/cache/refresher.rs index 5232155625..0da2748846 100644 --- a/nym-api/src/node_status_api/cache/refresher.rs +++ b/nym-api/src/node_status_api/cache/refresher.rs @@ -8,20 +8,14 @@ use crate::node_performance::provider::{NodePerformanceProvider, NodesRoutingSco use crate::node_status_api::cache::config_score::calculate_config_score; use crate::node_status_api::models::Uptime; use crate::support::caching::cache::SharedCache; -use crate::support::legacy_helpers::legacy_host_to_ips_and_hostname; use crate::{ mixnet_contract_cache::cache::MixnetContractCache, node_status_api::cache::NodeStatusCacheError, support::caching::CacheNotification, }; use ::time::OffsetDateTime; -use cosmwasm_std::Decimal; -use nym_api_requests::legacy::{LegacyGatewayBondWithId, LegacyMixNodeDetailsWithLayer}; -use nym_api_requests::models::{ - DetailedNodePerformance, GatewayBondAnnotated, MixNodeBondAnnotated, NodeAnnotation, - NodePerformance, -}; -use nym_mixnet_contract_common::{NodeId, NymNodeDetails, RewardingParams}; -use nym_task::TaskClient; +use nym_api_requests::models::{DetailedNodePerformance, NodeAnnotation}; +use nym_mixnet_contract_common::{NodeId, NymNodeDetails}; +use nym_task::ShutdownToken; use nym_topology::CachedEpochRewardedSet; use std::collections::HashMap; use std::time::Duration; @@ -66,29 +60,32 @@ impl NodeStatusCacheRefresher { } /// Runs the node status cache refresher task. - pub async fn run(&mut self, mut shutdown: TaskClient) { + pub async fn run(&mut self, shutdown_token: ShutdownToken) { let mut last_update = OffsetDateTime::now_utc(); let mut fallback_interval = time::interval(self.fallback_caching_interval); - while !shutdown.is_shutdown() { + loop { tokio::select! { biased; - _ = shutdown.recv() => { + _ = shutdown_token.cancelled() => { trace!("NodeStatusCacheRefresher: Received shutdown"); + break; } // Update node status cache when the contract cache / describe cache is updated Ok(_) = self.mixnet_contract_cache_listener.changed() => { tokio::select! { _ = self.maybe_refresh(&mut fallback_interval, &mut last_update) => (), - _ = shutdown.recv() => { + _ = shutdown_token.cancelled() => { trace!("NodeStatusCacheRefresher: Received shutdown"); + break; } } } Ok(_) = self.describe_cache_listener.changed() => { tokio::select! { _ = self.maybe_refresh(&mut fallback_interval, &mut last_update) => (), - _ = shutdown.recv() => { + _ = shutdown_token.cancelled() => { trace!("NodeStatusCacheRefresher: Received shutdown"); + break; } } } @@ -97,8 +94,9 @@ impl NodeStatusCacheRefresher { _ = fallback_interval.tick() => { tokio::select! { _ = self.maybe_refresh(&mut fallback_interval, &mut last_update) => (), - _ = shutdown.recv() => { + _ = shutdown_token.cancelled() => { trace!("NodeStatusCacheRefresher: Received shutdown"); + break; } } } @@ -144,70 +142,16 @@ impl NodeStatusCacheRefresher { fallback_interval.reset(); } - #[allow(clippy::too_many_arguments)] pub(crate) async fn produce_node_annotations( &self, config_score_data: &ConfigScoreData, routing_scores: &NodesRoutingScores, - legacy_mixnodes: &[LegacyMixNodeDetailsWithLayer], - legacy_gateways: &[LegacyGatewayBondWithId], nym_nodes: &[NymNodeDetails], rewarded_set: &CachedEpochRewardedSet, described_nodes: &DescribedNodes, ) -> HashMap { let mut annotations = HashMap::new(); - for legacy_mix in legacy_mixnodes { - let node_id = legacy_mix.mix_id(); - let routing_score = routing_scores.get_or_log(node_id); - - let config_score = - 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 - let scaled_performance = performance * 100.; - let legacy_performance = Uptime::new(scaled_performance as f32).into(); - - annotations.insert( - legacy_mix.mix_id(), - NodeAnnotation { - last_24h_performance: legacy_performance, - current_role: rewarded_set.role(legacy_mix.mix_id()).map(|r| r.into()), - detailed_performance: DetailedNodePerformance::new( - performance, - routing_score, - config_score, - ), - }, - ); - } - - for legacy_gateway in legacy_gateways { - let node_id = legacy_gateway.node_id; - let routing_score = routing_scores.get_or_log(node_id); - let config_score = - 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 - let scaled_performance = performance * 100.; - let legacy_performance = Uptime::new(scaled_performance as f32).into(); - - annotations.insert( - legacy_gateway.node_id, - NodeAnnotation { - last_24h_performance: legacy_performance, - current_role: rewarded_set.role(legacy_gateway.node_id).map(|r| r.into()), - detailed_performance: DetailedNodePerformance::new( - performance, - routing_score, - config_score, - ), - }, - ); - } - for nym_node in nym_nodes { let node_id = nym_node.node_id(); let routing_score = routing_scores.get_or_log(node_id); @@ -236,133 +180,25 @@ impl NodeStatusCacheRefresher { annotations } - #[deprecated] - pub(super) async fn annotate_legacy_mixnodes_nodes_with_details( - &self, - mixnodes: Vec, - routing_scores: &NodesRoutingScores, - interval_reward_params: RewardingParams, - ) -> HashMap { - let mut annotated = HashMap::new(); - for mixnode in mixnodes { - let stake_saturation = mixnode - .rewarding_details - .bond_saturation(&interval_reward_params); - - let uncapped_stake_saturation = mixnode - .rewarding_details - .uncapped_bond_saturation(&interval_reward_params); - - let score = routing_scores.get_or_log(mixnode.mix_id()); - let legacy_report = NodePerformance { - most_recent: score.legacy_performance(), - last_hour: score.legacy_performance(), - last_24h: score.legacy_performance(), - }; - - let Some((ip_addresses, _)) = - legacy_host_to_ips_and_hostname(&mixnode.bond_information.mix_node.host) - else { - continue; - }; - - // legacy node will never get rewarded - let estimated_operator_apy = Decimal::zero(); - let estimated_delegators_apy = Decimal::zero(); - - annotated.insert( - mixnode.mix_id(), - MixNodeBondAnnotated { - // all legacy nodes are always blacklisted - blacklisted: true, - mixnode_details: mixnode, - stake_saturation, - uncapped_stake_saturation, - performance: score.legacy_performance(), - node_performance: legacy_report, - estimated_operator_apy, - estimated_delegators_apy, - ip_addresses, - }, - ); - } - annotated - } - - #[deprecated] - pub(crate) async fn annotate_legacy_gateways_with_details( - &self, - gateway_bonds: Vec, - routing_scores: &NodesRoutingScores, - ) -> HashMap { - let mut annotated = HashMap::new(); - for gateway_bond in gateway_bonds { - let score = routing_scores.get_or_log(gateway_bond.node_id); - let legacy_report = NodePerformance { - most_recent: score.legacy_performance(), - last_hour: score.legacy_performance(), - last_24h: score.legacy_performance(), - }; - - let Some((ip_addresses, _)) = - legacy_host_to_ips_and_hostname(&gateway_bond.bond.gateway.host) - else { - continue; - }; - - annotated.insert( - gateway_bond.node_id, - GatewayBondAnnotated { - // all legacy nodes are always blacklisted - blacklisted: true, - gateway_bond, - self_described: None, - performance: score.legacy_performance(), - node_performance: legacy_report, - ip_addresses, - }, - ); - } - annotated - } - /// Refreshes the node status cache by fetching the latest data from the contract cache #[allow(deprecated)] async fn refresh(&self) -> Result<(), NodeStatusCacheError> { info!("Updating node status cache"); // Fetch contract cache data to work with - let mixnode_details = self.mixnet_contract_cache.legacy_mixnodes_all().await; - let interval_reward_params = self.mixnet_contract_cache.interval_reward_params().await?; let current_interval = self.mixnet_contract_cache.current_interval().await?; let rewarded_set = self.mixnet_contract_cache.rewarded_set_owned().await?; - let gateway_bonds = self.mixnet_contract_cache.legacy_gateways_all().await; let nym_nodes = self.mixnet_contract_cache.nym_nodes().await; let config_score_data = self.mixnet_contract_cache.maybe_config_score_data().await?; - // Compute inclusion probabilities - // (all legacy mixnodes have 0% chance of being selected) - let inclusion_probabilities = crate::node_status_api::cache::inclusion_probabilities::InclusionProbabilities::legacy_zero(&mixnode_details); - let Ok(described) = self.described_cache.get().await else { return Err(NodeStatusCacheError::UnavailableDescribedCache); }; - let mut legacy_gateway_mapping = HashMap::new(); - for gateway in &gateway_bonds { - legacy_gateway_mapping.insert(gateway.identity().clone(), gateway.node_id); - } - - let all_ids = mixnode_details + let all_ids = nym_nodes .iter() - .map(|m| m.bond_information.mix_id) - .chain( - gateway_bonds - .iter() - .map(|g| g.node_id) - .chain(nym_nodes.iter().map(|n| n.bond_information.node_id)), - ) - .collect::>(); + .map(|n| n.bond_information.node_id) + .collect(); // note: any internal errors imply failures for that node in particular let routing_scores = self @@ -375,36 +211,14 @@ impl NodeStatusCacheRefresher { .produce_node_annotations( &config_score_data, &routing_scores, - &mixnode_details, - &gateway_bonds, &nym_nodes, &rewarded_set, &described, ) .await; - let mixnodes_annotated = self - .annotate_legacy_mixnodes_nodes_with_details( - mixnode_details, - &routing_scores, - interval_reward_params, - ) - .await; - - let gateways_annotated = self - .annotate_legacy_gateways_with_details(gateway_bonds, &routing_scores) - .await; - // Update the cache - self.cache - .update( - legacy_gateway_mapping, - node_annotations, - mixnodes_annotated, - gateways_annotated, - inclusion_probabilities, - ) - .await; + self.cache.update(node_annotations).await; Ok(()) } } 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 559ca11182..5a29f09b30 100644 --- a/nym-api/src/node_status_api/handlers/network_monitor.rs +++ b/nym-api/src/node_status_api/handlers/network_monitor.rs @@ -5,22 +5,16 @@ 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, - _gateway_uptime_history, _get_gateway_avg_uptime, _get_legacy_gateways_detailed, - _get_legacy_gateways_detailed_unfiltered, _get_mixnode_avg_uptime, - _get_mixnode_reward_estimation, _get_mixnodes_detailed_unfiltered, _mixnode_core_status_count, - _mixnode_report, _mixnode_uptime_history, + _gateway_core_status_count, _gateway_uptime_history, _mixnode_core_status_count, + _mixnode_uptime_history, }; use crate::node_status_api::models::AxumResult; use crate::support::http::state::AppState; use axum::extract::{Path, Query, State}; -use axum::Json; use axum::Router; use nym_api_requests::models::{ - ComputeRewardEstParam, GatewayBondAnnotated, GatewayCoreStatusResponse, - GatewayStatusReportResponse, GatewayUptimeHistoryResponse, GatewayUptimeResponse, - MixNodeBondAnnotated, MixnodeCoreStatusResponse, MixnodeStatusReportResponse, - MixnodeUptimeHistoryResponse, RewardEstimationResponse, UptimeResponse, + GatewayCoreStatusResponse, GatewayUptimeHistoryResponse, MixnodeCoreStatusResponse, + MixnodeUptimeHistoryResponse, }; use nym_http_api_common::{FormattedResponse, Output, OutputParams}; use serde::Deserialize; @@ -33,57 +27,34 @@ pub(super) fn network_monitor_routes() -> Router { .nest( "/gateway/:identity", Router::new() - .route("/report", axum::routing::get(gateway_report)) .route("/history", axum::routing::get(gateway_uptime_history)) .route( "/core-status-count", axum::routing::get(gateway_core_status_count), - ) - .route("/avg_uptime", axum::routing::get(get_gateway_avg_uptime)), + ), ) .nest( "/mixnode/:mix_id", Router::new() - .route("/report", axum::routing::get(mixnode_report)) .route("/history", axum::routing::get(mixnode_uptime_history)) .route( "/core-status-count", axum::routing::get(mixnode_core_status_count), - ) - .route( - "/reward-estimation", - axum::routing::get(get_mixnode_reward_estimation), - ) - .route( - "/compute-reward-estimation", - axum::routing::post(compute_mixnode_reward_estimation), - ) - .route("/avg_uptime", axum::routing::get(get_mixnode_avg_uptime)), + ), ) .nest( "/mixnodes", - Router::new() - .route( - "/detailed-unfiltered", - axum::routing::get(get_mixnodes_detailed_unfiltered), - ) - .route( - "/unstable/:mix_id/test-results", - axum::routing::get(unstable::mixnode_test_results), - ), + Router::new().route( + "/unstable/:mix_id/test-results", + axum::routing::get(unstable::mixnode_test_results), + ), ) .nest( "/gateways", - Router::new() - .route("/detailed", axum::routing::get(get_gateways_detailed)) - .route( - "/detailed-unfiltered", - axum::routing::get(get_gateways_detailed_unfiltered), - ) - .route( - "/unstable/:gateway_identity/test-results", - axum::routing::get(unstable::gateway_test_results), - ), + Router::new().route( + "/unstable/:gateway_identity/test-results", + axum::routing::get(unstable::gateway_test_results), + ), ) .nest( "/network-monitor/unstable", @@ -99,30 +70,6 @@ pub(super) fn network_monitor_routes() -> Router { ) } -#[utoipa::path( - tag = "network-monitor-status", - get, - path = "/v1/status/gateway/{identity}/report", - responses( - (status = 200, content( - (GatewayStatusReportResponse = "application/json"), - (GatewayStatusReportResponse = "application/yaml"), - (GatewayStatusReportResponse = "application/bincode") - )) - ), - params(OutputParams) -)] -#[deprecated] -async fn gateway_report( - Path(identity): Path, - Query(output): Query, - State(state): State, -) -> AxumResult> { - let output = output.output.unwrap_or_default(); - - Ok(output.to_response(_gateway_report(state.node_status_cache(), &identity).await?)) -} - #[utoipa::path( tag = "network-monitor-status", get, @@ -144,9 +91,7 @@ async fn gateway_uptime_history( ) -> AxumResult> { let output = output.output.unwrap_or_default(); - Ok(output.to_response( - _gateway_uptime_history(state.storage(), state.nym_contract_cache(), &identity).await?, - )) + Ok(output.to_response(_gateway_uptime_history(state.storage(), &identity).await?)) } #[derive(Deserialize, IntoParams)] @@ -182,54 +127,6 @@ async fn gateway_core_status_count( .to_response(_gateway_core_status_count(state.storage(), &identity, since).await?)) } -#[utoipa::path( - tag = "network-monitor-status", - get, - path = "/v1/status/gateway/{identity}/avg_uptime", - responses( - (status = 200, content( - (GatewayUptimeResponse = "application/json"), - (GatewayUptimeResponse = "application/yaml"), - (GatewayUptimeResponse = "application/bincode") - )) - ), - params(OutputParams) -)] -#[deprecated] -async fn get_gateway_avg_uptime( - Path(identity): Path, - Query(output): Query, - State(state): State, -) -> AxumResult> { - let output = output.output.unwrap_or_default(); - - Ok(output.to_response(_get_gateway_avg_uptime(state.node_status_cache(), &identity).await?)) -} - -#[utoipa::path( - tag = "network-monitor-status", - get, - path = "/v1/status/mixnode/{mix_id}/report", - responses( - (status = 200, content( - (MixnodeStatusReportResponse = "application/json"), - (MixnodeStatusReportResponse = "application/yaml"), - (MixnodeStatusReportResponse = "application/bincode") - )) - ), - params(OutputParams, MixIdParam) -)] -#[deprecated] -async fn mixnode_report( - Path(MixIdParam { mix_id }): Path, - Query(output): Query, - State(state): State, -) -> AxumResult> { - let output = output.output.unwrap_or_default(); - - Ok(output.to_response(_mixnode_report(state.node_status_cache(), mix_id).await?)) -} - #[utoipa::path( tag = "network-monitor-status", get, @@ -251,9 +148,7 @@ async fn mixnode_uptime_history( ) -> AxumResult> { let output = output.output.unwrap_or_default(); - Ok(output.to_response( - _mixnode_uptime_history(state.storage(), state.nym_contract_cache(), mix_id).await?, - )) + Ok(output.to_response(_mixnode_uptime_history(state.storage(), mix_id).await?)) } #[utoipa::path( @@ -281,167 +176,3 @@ async fn mixnode_core_status_count( Ok(output.to_response(_mixnode_core_status_count(state.storage(), mix_id, since).await?)) } - -#[utoipa::path( - tag = "network-monitor-status", - get, - params( - MixIdParam, OutputParams - ), - path = "/v1/status/mixnode/{mix_id}/reward-estimation", - responses( - (status = 200, content( - (RewardEstimationResponse = "application/json"), - (RewardEstimationResponse = "application/yaml"), - (RewardEstimationResponse = "application/bincode") - )) - ), -)] -#[deprecated] -async fn get_mixnode_reward_estimation( - Path(MixIdParam { mix_id }): Path, - Query(output): Query, - State(state): State, -) -> AxumResult> { - let output = output.output.unwrap_or_default(); - - Ok(output.to_response( - _get_mixnode_reward_estimation( - state.node_status_cache(), - state.nym_contract_cache(), - mix_id, - ) - .await?, - )) -} - -#[utoipa::path( - tag = "network-monitor-status", - post, - params( - OutputParams, MixIdParam - ), - path = "/v1/status/mixnode/{mix_id}/compute-reward-estimation", - request_body = ComputeRewardEstParam, - responses( - (status = 200, content( - (RewardEstimationResponse = "application/json"), - (RewardEstimationResponse = "application/yaml"), - (RewardEstimationResponse = "application/bincode") - )) - ), -)] -#[deprecated] -async fn compute_mixnode_reward_estimation( - Path(MixIdParam { mix_id }): Path, - Query(output): Query, - State(state): State, - Json(user_reward_param): Json, -) -> AxumResult> { - let output = output.output.unwrap_or_default(); - - Ok(output.to_response( - _compute_mixnode_reward_estimation( - &user_reward_param, - state.node_status_cache(), - state.nym_contract_cache(), - mix_id, - ) - .await?, - )) -} - -#[utoipa::path( - tag = "network-monitor-status", - get, - params( - MixIdParam, OutputParams - ), - path = "/v1/status/mixnode/{mix_id}/avg_uptime", - responses( - (status = 200, content( - (UptimeResponse = "application/json"), - (UptimeResponse = "application/yaml"), - (UptimeResponse = "application/bincode") - )) - ), -)] -#[deprecated] -async fn get_mixnode_avg_uptime( - Path(MixIdParam { mix_id }): Path, - Query(output): Query, - State(state): State, -) -> AxumResult> { - let output = output.output.unwrap_or_default(); - - Ok(output.to_response(_get_mixnode_avg_uptime(state.node_status_cache(), mix_id).await?)) -} - -#[utoipa::path( - tag = "network-monitor-status", - get, - path = "/v1/status/mixnodes/detailed-unfiltered", - responses( - (status = 200, content( - (MixNodeBondAnnotated = "application/json"), - (MixNodeBondAnnotated = "application/yaml"), - (MixNodeBondAnnotated = "application/bincode") - )) - ), - params(OutputParams) -)] -#[deprecated] -pub async fn get_mixnodes_detailed_unfiltered( - Query(output): Query, - State(state): State, -) -> FormattedResponse> { - let output = output.output.unwrap_or_default(); - - output.to_response(_get_mixnodes_detailed_unfiltered(state.node_status_cache()).await) -} - -#[utoipa::path( - tag = "network-monitor-status", - get, - path = "/v1/status/gateways/detailed", - responses( - (status = 200, content( - (GatewayBondAnnotated = "application/json"), - (GatewayBondAnnotated = "application/yaml"), - (GatewayBondAnnotated = "application/bincode") - )) - ), - params(OutputParams) -)] -#[deprecated] -pub async fn get_gateways_detailed( - Query(output): Query, - State(state): State, -) -> FormattedResponse> { - let output = output.output.unwrap_or_default(); - - output.to_response(_get_legacy_gateways_detailed(state.node_status_cache()).await) -} - -#[utoipa::path( - tag = "network-monitor-status", - get, - path = "/v1/status/gateways/detailed-unfiltered", - responses( - (status = 200, content( - (GatewayBondAnnotated = "application/json"), - (GatewayBondAnnotated = "application/yaml"), - (GatewayBondAnnotated = "application/bincode") - )) - ), - params(OutputParams) -)] -#[deprecated] -pub async fn get_gateways_detailed_unfiltered( - Query(output): Query, - State(state): State, -) -> FormattedResponse> { - let output = output.output.unwrap_or_default(); - - output.to_response(_get_legacy_gateways_detailed_unfiltered(state.node_status_cache()).await) -} diff --git a/nym-api/src/node_status_api/handlers/without_monitor.rs b/nym-api/src/node_status_api/handlers/without_monitor.rs index d66addae61..08d8b7c9da 100644 --- a/nym-api/src/node_status_api/handlers/without_monitor.rs +++ b/nym-api/src/node_status_api/handlers/without_monitor.rs @@ -1,25 +1,12 @@ // Copyright 2021-2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -// we want to mark the routes as deprecated in swagger, but still expose them -#![allow(deprecated)] -use crate::node_status_api::handlers::MixIdParam; -use crate::node_status_api::helpers::{ - _get_active_set_legacy_mixnodes_detailed, _get_legacy_mixnodes_detailed, - _get_mixnode_inclusion_probabilities, _get_mixnode_inclusion_probability, - _get_mixnode_stake_saturation, _get_mixnode_status, _get_rewarded_set_legacy_mixnodes_detailed, -}; use crate::node_status_api::models::{AxumErrorResponse, AxumResult}; use crate::support::http::state::AppState; -use axum::extract::{Path, Query, State}; -use axum::routing::{get, post}; +use axum::extract::State; +use axum::routing::post; use axum::Json; use axum::Router; -use nym_api_requests::models::{ - MixNodeBondAnnotated, MixnodeStatusResponse, StakeSaturationResponse, -}; -use nym_http_api_common::{FormattedResponse, OutputParams}; -use nym_mixnet_contract_common::NodeId; use nym_types::monitoring::MonitorMessage; use tracing::error; @@ -33,29 +20,6 @@ pub(super) fn mandatory_routes() -> Router { "/submit-node-monitoring-results", post(submit_node_monitoring_results), ) - .nest( - "/mixnode/:mix_id", - Router::new() - .route("/status", get(get_mixnode_status)) - .route("/stake-saturation", get(get_mixnode_stake_saturation)) - .route( - "/inclusion-probability", - get(get_mixnode_inclusion_probability), - ), - ) - .merge( - Router::new().nest( - "/mixnodes", - Router::new() - .route( - "/inclusion-probability", - get(get_mixnode_inclusion_probabilities), - ) - .route("/detailed", get(get_mixnodes_detailed)) - .route("/rewarded/detailed", get(get_rewarded_set_detailed)) - .route("/active/detailed", get(get_active_set_detailed)), - ), - ) } #[utoipa::path( @@ -145,196 +109,3 @@ pub(crate) async fn submit_node_monitoring_results( } } } - -#[utoipa::path( - tag = "status", - get, - params( - MixIdParam - ), - path = "/v1/status/mixnode/{mix_id}/status", - responses( - (status = 200, content( - (MixnodeStatusResponse = "application/json"), - (MixnodeStatusResponse = "application/yaml"), - (MixnodeStatusResponse = "application/bincode") - )) - ), - params(OutputParams) -)] -#[deprecated] -async fn get_mixnode_status( - Path(MixIdParam { mix_id }): Path, - Query(output): Query, - State(state): State, -) -> FormattedResponse { - let output = output.output.unwrap_or_default(); - output.to_response(_get_mixnode_status(state.nym_contract_cache(), mix_id).await) -} - -#[utoipa::path( - tag = "status", - get, - params( - MixIdParam - ), - path = "/v1/status/mixnode/{mix_id}/stake-saturation", - responses( - (status = 200, content( - (StakeSaturationResponse = "application/json"), - (StakeSaturationResponse = "application/yaml"), - (StakeSaturationResponse = "application/bincode") - )) - ), - params(OutputParams) -)] -#[deprecated] -async fn get_mixnode_stake_saturation( - Path(mix_id): Path, - Query(output): Query, - State(state): State, -) -> AxumResult> { - let output = output.output.unwrap_or_default(); - - Ok(output.to_response( - _get_mixnode_stake_saturation( - state.node_status_cache(), - state.nym_contract_cache(), - mix_id, - ) - .await?, - )) -} - -#[utoipa::path( - tag = "status", - get, - params( - MixIdParam, OutputParams - ), - path = "/v1/status/mixnode/{mix_id}/inclusion-probability", - responses( - (status = 200, content( - (nym_api_requests::models::InclusionProbabilityResponse = "application/json"), - (nym_api_requests::models::InclusionProbabilityResponse = "application/yaml"), - (nym_api_requests::models::InclusionProbabilityResponse = "application/bincode") - )) - ), -)] -#[deprecated] -#[allow(deprecated)] -async fn get_mixnode_inclusion_probability( - Path(mix_id): Path, - Query(output): Query, - State(state): State, -) -> AxumResult> { - let output = output.output.unwrap_or_default(); - - Ok(output - .to_response(_get_mixnode_inclusion_probability(state.node_status_cache(), mix_id).await?)) -} - -#[utoipa::path( - tag = "status", - get, - path = "/v1/status/mixnodes/inclusion-probability", - responses( - (status = 200, content( - (nym_api_requests::models::AllInclusionProbabilitiesResponse = "application/json"), - (nym_api_requests::models::AllInclusionProbabilitiesResponse = "application/yaml"), - (nym_api_requests::models::AllInclusionProbabilitiesResponse = "application/bincode") - )) - ), - params(OutputParams) -)] -#[deprecated] -#[allow(deprecated)] -async fn get_mixnode_inclusion_probabilities( - Query(output): Query, - State(state): State, -) -> AxumResult> { - let output = output.output.unwrap_or_default(); - - Ok(output.to_response(_get_mixnode_inclusion_probabilities(state.node_status_cache()).await?)) -} - -#[utoipa::path( - tag = "status", - get, - path = "/v1/status/mixnodes/detailed", - responses( - (status = 200, content( - (MixNodeBondAnnotated = "application/json"), - (MixNodeBondAnnotated = "application/yaml"), - (MixNodeBondAnnotated = "application/bincode") - )) - ), - params(OutputParams) -)] -#[deprecated] -pub async fn get_mixnodes_detailed( - Query(output): Query, - State(state): State, -) -> FormattedResponse> { - let output = output.output.unwrap_or_default(); - - output.to_response(_get_legacy_mixnodes_detailed(state.node_status_cache()).await) -} - -#[utoipa::path( - tag = "status", - get, - path = "/v1/status/mixnodes/rewarded/detailed", - responses( - (status = 200, content( - (MixNodeBondAnnotated = "application/json"), - (MixNodeBondAnnotated = "application/yaml"), - (MixNodeBondAnnotated = "application/bincode") - )) - ), - params(OutputParams) -)] -#[deprecated] -pub async fn get_rewarded_set_detailed( - Query(output): Query, - State(state): State, -) -> FormattedResponse> { - let output = output.output.unwrap_or_default(); - - output.to_response( - _get_rewarded_set_legacy_mixnodes_detailed( - state.node_status_cache(), - state.nym_contract_cache(), - ) - .await, - ) -} - -#[utoipa::path( - tag = "status", - get, - path = "/v1/status/mixnodes/active/detailed", - responses( - (status = 200, content( - (MixNodeBondAnnotated = "application/json"), - (MixNodeBondAnnotated = "application/yaml"), - (MixNodeBondAnnotated = "application/bincode") - )) - ), - params(OutputParams) -)] -#[deprecated] -pub async fn get_active_set_detailed( - Query(output): Query, - State(state): State, -) -> FormattedResponse> { - let output = output.output.unwrap_or_default(); - - output.to_response( - _get_active_set_legacy_mixnodes_detailed( - state.node_status_cache(), - state.nym_contract_cache(), - ) - .await, - ) -} diff --git a/nym-api/src/node_status_api/helpers.rs b/nym-api/src/node_status_api/helpers.rs index e4d6257ace..73756023da 100644 --- a/nym-api/src/node_status_api/helpers.rs +++ b/nym-api/src/node_status_api/helpers.rs @@ -3,75 +3,14 @@ use crate::node_status_api::models::{AxumErrorResponse, AxumResult}; use crate::storage::NymApiStorage; -use crate::support::caching::Cache; -use crate::{MixnetContractCache, NodeStatusCache}; use nym_api_requests::models::{ - ComputeRewardEstParam, GatewayBondAnnotated, GatewayCoreStatusResponse, - GatewayStatusReportResponse, GatewayUptimeHistoryResponse, GatewayUptimeResponse, - MixNodeBondAnnotated, MixnodeCoreStatusResponse, MixnodeStatusReportResponse, - MixnodeStatusResponse, MixnodeUptimeHistoryResponse, RewardEstimationResponse, - StakeSaturationResponse, UptimeResponse, + GatewayCoreStatusResponse, GatewayUptimeHistoryResponse, MixnodeCoreStatusResponse, + MixnodeUptimeHistoryResponse, }; -use nym_mixnet_contract_common::rewarding::RewardEstimate; use nym_mixnet_contract_common::NodeId; -async fn gateway_identity_to_node_id( - cache: &NodeStatusCache, - identity: &str, -) -> AxumResult { - let node_id = cache - .map_identity_to_node_id(identity) - .await - .ok_or(AxumErrorResponse::not_found("gateway bond not found"))?; - Ok(node_id) -} - -async fn get_gateway_bond_annotated( - cache: &NodeStatusCache, - node_id: NodeId, -) -> AxumResult { - cache - .gateway_annotated(node_id) - .await - .ok_or(AxumErrorResponse::not_found("gateway bond not found")) -} - -async fn get_gateway_bond_annotated_by_identity( - cache: &NodeStatusCache, - identity: &str, -) -> AxumResult { - let node_id = gateway_identity_to_node_id(cache, identity).await?; - get_gateway_bond_annotated(cache, node_id).await -} - -async fn get_mixnode_bond_annotated( - cache: &NodeStatusCache, - mix_id: NodeId, -) -> AxumResult { - cache - .mixnode_annotated(mix_id) - .await - .ok_or(AxumErrorResponse::not_found("mixnode bond not found")) -} - -pub(crate) async fn _gateway_report( - cache: &NodeStatusCache, - identity: &str, -) -> AxumResult { - let gateway = get_gateway_bond_annotated_by_identity(cache, identity).await?; - - Ok(GatewayStatusReportResponse { - identity: gateway.identity().to_owned(), - owner: gateway.owner().to_string(), - most_recent: gateway.node_performance.most_recent.round_to_integer(), - last_hour: gateway.node_performance.last_hour.round_to_integer(), - last_day: gateway.node_performance.last_24h.round_to_integer(), - }) -} - pub(crate) async fn _gateway_uptime_history( storage: &NymApiStorage, - nym_contract_cache: &MixnetContractCache, identity: &str, ) -> AxumResult { let history = storage @@ -79,14 +18,8 @@ pub(crate) async fn _gateway_uptime_history( .await .map_err(AxumErrorResponse::not_found)?; - let owner = nym_contract_cache - .legacy_gateway_owner(history.node_id) - .await - .ok_or_else(|| AxumErrorResponse::not_found("could not determine gateway owner"))?; - Ok(GatewayUptimeHistoryResponse { identity: history.identity, - owner, history: history.history.into_iter().map(Into::into).collect(), }) } @@ -107,25 +40,8 @@ pub(crate) async fn _gateway_core_status_count( }) } -pub(crate) async fn _mixnode_report( - cache: &NodeStatusCache, - mix_id: NodeId, -) -> AxumResult { - let mixnode = get_mixnode_bond_annotated(cache, mix_id).await?; - - Ok(MixnodeStatusReportResponse { - mix_id, - identity: mixnode.identity_key().to_owned(), - owner: mixnode.owner().to_string(), - most_recent: mixnode.node_performance.most_recent.round_to_integer(), - last_hour: mixnode.node_performance.last_hour.round_to_integer(), - last_day: mixnode.node_performance.last_24h.round_to_integer(), - }) -} - pub(crate) async fn _mixnode_uptime_history( storage: &NymApiStorage, - nym_contract_cache: &MixnetContractCache, mix_id: NodeId, ) -> AxumResult { let history = storage @@ -133,15 +49,9 @@ pub(crate) async fn _mixnode_uptime_history( .await .map_err(AxumErrorResponse::not_found)?; - let owner = nym_contract_cache - .legacy_gateway_owner(mix_id) - .await - .ok_or_else(|| AxumErrorResponse::not_found("could not determine mixnode owner"))?; - Ok(MixnodeUptimeHistoryResponse { mix_id, identity: history.identity, - owner, history: history.history.into_iter().map(Into::into).collect(), }) } @@ -158,224 +68,3 @@ pub(crate) async fn _mixnode_core_status_count( Ok(MixnodeCoreStatusResponse { mix_id, count }) } - -pub(crate) async fn _get_mixnode_status( - cache: &MixnetContractCache, - mix_id: NodeId, -) -> MixnodeStatusResponse { - MixnodeStatusResponse { - status: cache.mixnode_status(mix_id).await, - } -} - -pub(crate) async fn _get_mixnode_reward_estimation( - status_cache: &NodeStatusCache, - contract_cache: &MixnetContractCache, - mix_id: NodeId, -) -> AxumResult { - let _ = status_cache - .mixnode_annotated(mix_id) - .await - .ok_or_else(|| AxumErrorResponse::not_found("mixnode bond not found"))?; - // legacy mixnode will never get any rewards - let reward_estimation = RewardEstimate::zero(); - - let reward_params = contract_cache.interval_reward_params().await?; - let current_interval = contract_cache.current_interval().await?; - - // in some very rare edge cases this value might be off (as internals might have got updated between - // queries for `reward_params` and `current_interval`, but timestamp is only informative to begin with) - let as_at = contract_cache.cache_timestamp().await; - - Ok(RewardEstimationResponse { - estimation: reward_estimation, - reward_params, - epoch: current_interval, - as_at: as_at.unix_timestamp(), - }) -} - -pub(crate) async fn _compute_mixnode_reward_estimation( - _: &ComputeRewardEstParam, - status_cache: &NodeStatusCache, - contract_cache: &MixnetContractCache, - mix_id: NodeId, -) -> AxumResult { - let _ = status_cache - .mixnode_annotated(mix_id) - .await - .ok_or_else(|| AxumErrorResponse::not_found("mixnode bond not found"))?; - - let reward_estimation = RewardEstimate::zero(); - - let reward_params = contract_cache.interval_reward_params().await?; - let current_interval = contract_cache.current_interval().await?; - - // in some very rare edge cases this value might be off (as internals might have got updated between - // queries for `reward_params` and `current_interval`, but timestamp is only informative to begin with) - let as_at = contract_cache.cache_timestamp().await; - - Ok(RewardEstimationResponse { - estimation: reward_estimation, - reward_params, - epoch: current_interval, - as_at: as_at.unix_timestamp(), - }) -} - -pub(crate) async fn _get_mixnode_stake_saturation( - status_cache: &NodeStatusCache, - contract_cache: &MixnetContractCache, - mix_id: NodeId, -) -> AxumResult { - let mixnode = status_cache - .mixnode_annotated(mix_id) - .await - .ok_or_else(|| AxumErrorResponse::not_found("mixnode bond not found"))?; - - // Recompute the stake saturation just so that we can confidently state that the `as_at` - // field is consistent and correct. Luckily this is very cheap. - let rewarding_params = contract_cache.interval_reward_params().await?; - let as_at = contract_cache.cache_timestamp().await; - - Ok(StakeSaturationResponse { - saturation: mixnode - .mixnode_details - .rewarding_details - .bond_saturation(&rewarding_params), - uncapped_saturation: mixnode - .mixnode_details - .rewarding_details - .uncapped_bond_saturation(&rewarding_params), - as_at: as_at.unix_timestamp(), - }) -} - -#[allow(deprecated)] -pub(crate) async fn _get_mixnode_inclusion_probability( - cache: &NodeStatusCache, - mix_id: NodeId, -) -> AxumResult { - cache - .inclusion_probabilities() - .await - .map(Cache::into_inner) - .and_then(|p| p.node(mix_id).cloned()) - .map(|p| nym_api_requests::models::InclusionProbabilityResponse { - in_active: p.in_active.into(), - in_reserve: p.in_reserve.into(), - }) - .ok_or_else(|| AxumErrorResponse::not_found("mixnode bond not found")) -} - -pub(crate) async fn _get_mixnode_avg_uptime( - cache: &NodeStatusCache, - mix_id: NodeId, -) -> AxumResult { - let mixnode = get_mixnode_bond_annotated(cache, mix_id).await?; - - Ok(UptimeResponse { - mix_id, - avg_uptime: mixnode.node_performance.last_24h.round_to_integer(), - node_performance: mixnode.node_performance, - }) -} - -pub(crate) async fn _get_gateway_avg_uptime( - cache: &NodeStatusCache, - identity: &str, -) -> AxumResult { - let gateway = get_gateway_bond_annotated_by_identity(cache, identity).await?; - - Ok(GatewayUptimeResponse { - identity: identity.to_string(), - avg_uptime: gateway.node_performance.last_24h.round_to_integer(), - node_performance: gateway.node_performance, - }) -} - -#[allow(deprecated)] -pub(crate) async fn _get_mixnode_inclusion_probabilities( - cache: &NodeStatusCache, -) -> AxumResult { - if let Some(prob) = cache.inclusion_probabilities().await { - let as_at = prob.timestamp(); - let prob = prob.into_inner(); - Ok( - nym_api_requests::models::AllInclusionProbabilitiesResponse { - inclusion_probabilities: prob.inclusion_probabilities, - samples: prob.samples, - elapsed: prob.elapsed, - delta_max: prob.delta_max, - delta_l2: prob.delta_l2, - as_at: as_at.unix_timestamp(), - }, - ) - } else { - Err(AxumErrorResponse::service_unavailable()) - } -} - -pub(crate) async fn _get_legacy_mixnodes_detailed( - cache: &NodeStatusCache, -) -> Vec { - cache - .mixnodes_annotated_filtered() - .await - .unwrap_or_default() -} - -pub(crate) async fn _get_mixnodes_detailed_unfiltered( - cache: &NodeStatusCache, -) -> Vec { - cache.mixnodes_annotated_full().await.unwrap_or_default() -} - -pub(crate) async fn _get_rewarded_set_legacy_mixnodes_detailed( - status_cache: &NodeStatusCache, - contract_cache: &MixnetContractCache, -) -> Vec { - let Some(rewarded_set) = contract_cache.rewarded_set().await else { - return Vec::new(); - }; - let Some(mixnodes) = status_cache.mixnodes_annotated_full().await else { - return Vec::new(); - }; - mixnodes - .into_iter() - .filter(|m| { - rewarded_set.is_active_mixnode(&m.mix_id()) || rewarded_set.is_standby(&m.mix_id()) - }) - .collect() -} - -pub(crate) async fn _get_active_set_legacy_mixnodes_detailed( - status_cache: &NodeStatusCache, - contract_cache: &MixnetContractCache, -) -> Vec { - let Some(rewarded_set) = contract_cache.rewarded_set().await else { - return Vec::new(); - }; - let Some(mixnodes) = status_cache.mixnodes_annotated_full().await else { - return Vec::new(); - }; - mixnodes - .into_iter() - .filter(|m| rewarded_set.is_active_mixnode(&m.mix_id())) - .collect() -} - -pub(crate) async fn _get_legacy_gateways_detailed( - cache: &NodeStatusCache, -) -> Vec { - cache - .gateways_annotated_filtered() - .await - .unwrap_or_default() -} - -pub(crate) async fn _get_legacy_gateways_detailed_unfiltered( - cache: &NodeStatusCache, -) -> Vec { - cache.gateways_annotated_full().await.unwrap_or_default() -} diff --git a/nym-api/src/node_status_api/mod.rs b/nym-api/src/node_status_api/mod.rs index 1b3d97f839..7820d9a953 100644 --- a/nym-api/src/node_status_api/mod.rs +++ b/nym-api/src/node_status_api/mod.rs @@ -11,7 +11,7 @@ use crate::{ support::{self}, }; pub(crate) use cache::NodeStatusCache; -use nym_task::TaskManager; +use nym_task::ShutdownManager; use std::time::Duration; use tokio::sync::watch; @@ -39,7 +39,7 @@ pub(crate) fn start_cache_refresh( performance_provider: Box, nym_contract_cache_listener: watch::Receiver, described_cache_cache_listener: watch::Receiver, - shutdown: &TaskManager, + shutdown_manager: &ShutdownManager, ) { let mut nym_api_cache_refresher = NodeStatusCacheRefresher::new( node_status_cache_state.to_owned(), @@ -50,6 +50,6 @@ pub(crate) fn start_cache_refresh( described_cache_cache_listener, performance_provider, ); - let shutdown_listener = shutdown.subscribe(); + let shutdown_listener = shutdown_manager.clone_shutdown_token(); tokio::spawn(async move { nym_api_cache_refresher.run(shutdown_listener).await }); } diff --git a/nym-api/src/node_status_api/uptime_updater.rs b/nym-api/src/node_status_api/uptime_updater.rs index a68979ced1..bfa1d66101 100644 --- a/nym-api/src/node_status_api/uptime_updater.rs +++ b/nym-api/src/node_status_api/uptime_updater.rs @@ -6,7 +6,7 @@ use crate::node_status_api::models::{ }; use crate::node_status_api::ONE_DAY; use crate::storage::NymApiStorage; -use nym_task::{TaskClient, TaskManager}; +use nym_task::{ShutdownManager, ShutdownToken}; use std::time::Duration; use time::macros::time; use time::{OffsetDateTime, PrimitiveDateTime}; @@ -70,7 +70,7 @@ impl HistoricalUptimeUpdater { Ok(()) } - pub(crate) async fn run(&self, mut shutdown: TaskClient) { + pub(crate) async fn run(&self, shutdown_token: ShutdownToken) { // update uptimes at 23:00 UTC each day so that we'd have data from the actual [almost] whole day // and so that we would avoid the edge case of starting validator API at 23:59 and having some // nodes update for different days @@ -98,11 +98,12 @@ impl HistoricalUptimeUpdater { let start = Instant::now() + time_left; let mut interval = interval_at(start, ONE_DAY); - while !shutdown.is_shutdown() { + loop { tokio::select! { biased; - _ = shutdown.recv() => { + _ = shutdown_token.cancelled() => { trace!("UpdateHandler: Received shutdown"); + break; } _ = interval.tick() => { info!("updating historical uptimes of nodes"); @@ -116,9 +117,9 @@ impl HistoricalUptimeUpdater { } } - pub(crate) fn start(storage: NymApiStorage, shutdown: &TaskManager) { + pub(crate) fn start(storage: NymApiStorage, shutdown: &ShutdownManager) { let uptime_updater = HistoricalUptimeUpdater::new(storage); - let shutdown_listener = shutdown.subscribe(); + let shutdown_listener = shutdown.child_shutdown_token(); tokio::spawn(async move { uptime_updater.run(shutdown_listener).await }); } } diff --git a/nym-api/src/nym_nodes/handlers/legacy.rs b/nym-api/src/nym_nodes/handlers/legacy.rs deleted file mode 100644 index bc36227c93..0000000000 --- a/nym-api/src/nym_nodes/handlers/legacy.rs +++ /dev/null @@ -1,119 +0,0 @@ -// Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only - -use crate::support::http::state::AppState; -use crate::support::legacy_helpers::{to_legacy_gateway, to_legacy_mixnode}; -use axum::extract::{Query, State}; -use axum::Router; -use nym_api_requests::models::{LegacyDescribedGateway, LegacyDescribedMixNode}; -use nym_http_api_common::{FormattedResponse, OutputParams}; -use tower_http::compression::CompressionLayer; - -// we want to mark the routes as deprecated in swagger, but still expose them -#[allow(deprecated)] -pub(crate) fn legacy_nym_node_routes() -> Router { - Router::new() - .route( - "/gateways/described", - axum::routing::get(get_gateways_described), - ) - .route( - "/mixnodes/described", - axum::routing::get(get_mixnodes_described), - ) - .layer(CompressionLayer::new()) -} - -#[utoipa::path( - tag = "Legacy gateways", - get, - path = "/v1/gateways/described", - responses( - (status = 200, content( - (Vec = "application/json"), - (Vec = "application/yaml"), - (Vec = "application/bincode") - )) - ), - params(OutputParams) -)] -#[deprecated] -async fn get_gateways_described( - Query(output): Query, - State(state): State, -) -> FormattedResponse> { - let describe_cache = state.described_nodes_cache(); - let output = output.output.unwrap_or_default(); - - let Ok(describe_cache) = describe_cache.get().await else { - return output.to_response(Vec::new()); - }; - - let migrated_nymnodes = state.nym_contract_cache().nym_nodes().await; - let mut described = Vec::new(); - - for nym_node in migrated_nymnodes { - // we ALWAYS need description to set legacy fields - let Some(description) = describe_cache.get_description(&nym_node.node_id()) else { - continue; - }; - // if the node hasn't declared it can be a gateway, ignore it - if !description.declared_role.entry { - continue; - } - - described.push(LegacyDescribedGateway { - bond: to_legacy_gateway(&nym_node, description), - self_described: Some(description.clone()), - }) - } - - output.to_response(described) -} - -#[utoipa::path( - tag = "Legacy Mixnodes", - get, - path = "/v1/mixnodes/described", - responses( - (status = 200, content( - (Vec = "application/json"), - (Vec = "application/yaml"), - (Vec = "application/bincode") - )) - ), - params(OutputParams) -)] -#[deprecated] -async fn get_mixnodes_described( - Query(output): Query, - State(state): State, -) -> FormattedResponse> { - let describe_cache = state.described_nodes_cache(); - let output = output.output.unwrap_or_default(); - - let Ok(describe_cache) = describe_cache.get().await else { - return output.to_response(Vec::new()); - }; - - let migrated_nymnodes = state.nym_contract_cache().nym_nodes().await; - let mut described = Vec::new(); - - for nym_node in migrated_nymnodes { - // we ALWAYS need description to set legacy fields - let Some(description) = describe_cache.get_description(&nym_node.node_id()) else { - continue; - }; - // if the node hasn't declared it can be a gateway, ignore it - if !description.declared_role.mixnode { - continue; - } - - described.push(LegacyDescribedMixNode { - bond: to_legacy_mixnode(&nym_node, description).bond_information, - self_described: Some(description.clone()), - }) - } - - output.to_response(described) -} diff --git a/nym-api/src/nym_nodes/handlers/mod.rs b/nym-api/src/nym_nodes/handlers/mod.rs index 6fed23d30a..954349c03a 100644 --- a/nym-api/src/nym_nodes/handlers/mod.rs +++ b/nym-api/src/nym_nodes/handlers/mod.rs @@ -10,7 +10,7 @@ use axum::{Json, Router}; use nym_api_requests::models::{ AnnotationResponse, NodeDatePerformanceResponse, NodePerformanceResponse, NodeRefreshBody, NoiseDetails, NymNodeDescription, PerformanceHistoryResponse, RewardedSetResponse, - UptimeHistoryResponse, + StakeSaturationResponse, UptimeHistoryResponse, }; use nym_api_requests::pagination::{PaginatedResponse, Pagination}; use nym_contracts_common::NaiveFloat; @@ -23,8 +23,6 @@ use time::{Date, OffsetDateTime}; use tower_http::compression::CompressionLayer; use utoipa::{IntoParams, ToSchema}; -pub(crate) mod legacy; - pub(crate) fn nym_node_routes() -> Router { Router::new() .route("/refresh-described", post(refresh_described)) @@ -33,6 +31,7 @@ pub(crate) fn nym_node_routes() -> Router { .route("/described", get(get_described_nodes)) .route("/annotation/:node_id", get(get_node_annotation)) .route("/performance/:node_id", get(get_current_node_performance)) + .route("/stake-saturation/:node_id", get(get_node_stake_saturation)) .route( "/historical-performance/:node_id", get(get_historical_performance), @@ -320,6 +319,44 @@ async fn get_current_node_performance( })) } +#[utoipa::path( + tag = "Nym Nodes", + get, + path = "/stake-saturation/{node_id}", + context_path = "/v1/nym-nodes", + responses( + (status = 200, content( + (StakeSaturationResponse = "application/json"), + (StakeSaturationResponse = "application/yaml"), + (StakeSaturationResponse = "application/bincode") + )) + ), + params(NodeIdParam, OutputParams), +)] +async fn get_node_stake_saturation( + Path(NodeIdParam { node_id }): Path, + Query(output): Query, + State(state): State, +) -> AxumResult> { + let output = output.get_output(); + + let contract_cache = state.nym_contract_cache(); + let Some(node) = contract_cache.nym_node(node_id).await? else { + return Err(AxumErrorResponse::not_found("nym node bond not found")); + }; + + let rewarding_params = contract_cache.interval_reward_params().await?; + let as_at = contract_cache.cache_timestamp().await; + + Ok(output.to_response(StakeSaturationResponse { + saturation: node.rewarding_details.bond_saturation(&rewarding_params), + uncapped_saturation: node + .rewarding_details + .uncapped_bond_saturation(&rewarding_params), + as_at: as_at.unix_timestamp(), + })) +} + // todo; probably extract it to requests crate #[derive(Debug, Serialize, Deserialize, Copy, Clone, IntoParams, ToSchema)] #[into_params(parameter_in = Query)] diff --git a/nym-api/src/signers_cache/mod.rs b/nym-api/src/signers_cache/mod.rs index f79e1a1f1d..5b647f8699 100644 --- a/nym-api/src/signers_cache/mod.rs +++ b/nym-api/src/signers_cache/mod.rs @@ -6,7 +6,7 @@ use crate::signers_cache::cache::SignersCacheData; use crate::support::caching::cache::SharedCache; use crate::support::caching::refresher::CacheRefresher; use crate::support::{config, nyxd}; -use nym_task::TaskManager; +use nym_task::ShutdownManager; pub(crate) mod cache; pub(crate) mod handlers; @@ -14,7 +14,7 @@ pub(crate) mod handlers; pub(crate) fn start_refresher( config: &config::SignersCache, nyxd_client: nyxd::Client, - task_manager: &TaskManager, + shutdown_manager: &ShutdownManager, ) -> SharedCache { let refresher = CacheRefresher::new( SignersCacheDataProvider::new(nyxd_client), @@ -23,7 +23,7 @@ pub(crate) fn start_refresher( .named("signers-cache-refresher"); let shared_cache = refresher.get_shared_cache(); refresher.start_with_delay( - task_manager.subscribe_named("signers-cache-refresher"), + shutdown_manager.clone_shutdown_token(), config.debug.refresher_start_delay, ); shared_cache diff --git a/nym-api/src/support/caching/cache.rs b/nym-api/src/support/caching/cache.rs index 4923b70a9f..40d7a59ef1 100644 --- a/nym-api/src/support/caching/cache.rs +++ b/nym-api/src/support/caching/cache.rs @@ -175,8 +175,7 @@ impl Cache> { } impl Cache { - // ugh. I hate to expose it, but it'd have broken pre-existing code - pub(crate) fn new(value: T) -> Self { + fn new(value: T) -> Self { Cache { value, as_at: OffsetDateTime::now_utc(), @@ -205,17 +204,6 @@ impl Cache { } } - // ugh. I hate to expose it, but it'd have broken pre-existing code - pub(crate) fn clone_cache(&self) -> Self - where - T: Clone, - { - Cache { - value: self.value.clone(), - as_at: self.as_at, - } - } - pub(crate) fn update(&mut self, update: S, update_fn: impl Fn(&mut T, S)) { update_fn(&mut self.value, update); self.as_at = OffsetDateTime::now_utc(); @@ -242,10 +230,6 @@ impl Cache { pub fn timestamp(&self) -> OffsetDateTime { self.as_at } - - pub fn into_inner(self) -> T { - self.value - } } impl Deref for Cache { diff --git a/nym-api/src/support/caching/refresher.rs b/nym-api/src/support/caching/refresher.rs index cfb412e449..2b21397c89 100644 --- a/nym-api/src/support/caching/refresher.rs +++ b/nym-api/src/support/caching/refresher.rs @@ -4,7 +4,7 @@ use crate::support::caching::cache::SharedCache; use crate::support::caching::CacheNotification; use async_trait::async_trait; -use nym_task::TaskClient; +use nym_task::ShutdownToken; use std::sync::Arc; use std::time::Duration; use tokio::sync::{watch, Notify}; @@ -223,33 +223,34 @@ where } } - pub async fn refresh(&mut self, task_client: &mut TaskClient) { + pub async fn refresh(&mut self, shutdown_token: &ShutdownToken) { info!("{}: refreshing cache state", self.name); tokio::select! { biased; - _ = task_client.recv() => { + _ = shutdown_token.cancelled() => { trace!("{}: Received shutdown while refreshing cache", self.name) } _ = self.do_refresh_cache() => (), } } - pub async fn run(&mut self, mut task_client: TaskClient) { + pub async fn run(&mut self, shutdown_token: ShutdownToken) { self.provider.wait_until_ready().await; let mut refresh_interval = interval(self.refreshing_interval); - while !task_client.is_shutdown() { + loop { tokio::select! { biased; - _ = task_client.recv() => { - trace!("{}: Received shutdown", self.name) + _ = shutdown_token.cancelled() => { + trace!("{}: Received shutdown", self.name); + break } - _ = refresh_interval.tick() => self.refresh(&mut task_client).await, + _ = refresh_interval.tick() => self.refresh(&shutdown_token).await, // note: `Notify` is not cancellation safe, HOWEVER, there's only one listener, // so it doesn't matter if we lose our queue position _ = self.refresh_requester.0.notified() => { - self.refresh(&mut task_client).await; + self.refresh(&shutdown_token).await; // since we just performed the full request, we can reset our existing interval refresh_interval.reset(); } @@ -257,16 +258,16 @@ where } } - pub fn start(mut self, task_client: TaskClient) + pub fn start(mut self, shutdown_token: ShutdownToken) where T: Send + Sync + 'static, E: Send + Sync + 'static, S: Send + Sync + 'static, { - tokio::spawn(async move { self.run(task_client).await }); + tokio::spawn(async move { self.run(shutdown_token).await }); } - pub fn start_with_delay(mut self, mut task_client: TaskClient, delay: Duration) + pub fn start_with_delay(mut self, shutdown_token: ShutdownToken, delay: Duration) where T: Send + Sync + 'static, E: Send + Sync + 'static, @@ -276,24 +277,24 @@ where let sleep = tokio::time::sleep(delay); tokio::select! { biased; - _ = task_client.recv() => { + _ = shutdown_token.cancelled() => { trace!("{}: Received shutdown", self.name); return } _ = sleep => {}, } - self.run(task_client).await + self.run(shutdown_token).await }); } - pub fn start_with_watcher(self, task_client: TaskClient) -> CacheUpdateWatcher + pub fn start_with_watcher(self, shutdown_token: ShutdownToken) -> CacheUpdateWatcher where T: Send + Sync + 'static, E: Send + Sync + 'static, S: Send + Sync + 'static, { let receiver = self.update_watcher(); - self.start(task_client); + self.start(shutdown_token); receiver } } diff --git a/nym-api/src/support/cli/run.rs b/nym-api/src/support/cli/run.rs index eb8f609ee4..6c8750149a 100644 --- a/nym-api/src/support/cli/run.rs +++ b/nym-api/src/support/cli/run.rs @@ -27,7 +27,7 @@ use crate::support::http::state::chain_status::ChainStatusCache; use crate::support::http::state::contract_details::ContractDetailsCache; use crate::support::http::state::force_refresh::ForcedRefresh; use crate::support::http::state::AppState; -use crate::support::http::{RouterBuilder, ShutdownHandles, TASK_MANAGER_TIMEOUT_S}; +use crate::support::http::{RouterBuilder, TASK_MANAGER_TIMEOUT_S}; use crate::support::nyxd; use crate::support::storage::runtime_migrations::m001_directory_services_v2_1::migrate_to_directory_services_v2_1; use crate::support::storage::NymApiStorage; @@ -39,12 +39,12 @@ use crate::{ use anyhow::{bail, Context}; use nym_config::defaults::NymNetworkDetails; use nym_sphinx::receiver::SphinxMessageReceiver; -use nym_task::TaskManager; +use nym_task::ShutdownManager; use nym_validator_client::nyxd::Coin; use std::net::SocketAddr; use std::sync::Arc; -use tokio_util::sync::CancellationToken; -use tracing::{error, info}; +use std::time::Duration; +use tracing::info; #[derive(clap::Args, Debug)] pub(crate) struct Args { @@ -119,8 +119,9 @@ pub(crate) struct Args { pub(crate) allow_illegal_ips: bool, } -async fn start_nym_api_tasks(config: &Config) -> anyhow::Result { - let task_manager = TaskManager::new(TASK_MANAGER_TIMEOUT_S); +async fn start_nym_api_tasks(config: &Config) -> anyhow::Result { + let shutdown_manager = ShutdownManager::build_new_default()? + .with_shutdown_duration(Duration::from_secs(TASK_MANAGER_TIMEOUT_S)); let nyxd_client = nyxd::Client::new(config)?; let connected_nyxd = config.get_nyxd_url(); @@ -155,6 +156,14 @@ async fn start_nym_api_tasks(config: &Config) -> anyhow::Result let described_nodes_cache = SharedCache::::new(); let node_info_cache = unstable::NodeInfoCache::default(); + let mixnet_contract_cache_refresher = mixnet_contract_cache::build_refresher( + &config.mixnet_contract_cache, + &mixnet_contract_cache_state.clone(), + nyxd_client.clone(), + ); + let mixnet_contract_cache_refresh_requester = + mixnet_contract_cache_refresher.refresh_requester(); + let ecash_contract = nyxd_client .get_ecash_contract_address() .await @@ -171,7 +180,7 @@ async fn start_nym_api_tasks(config: &Config) -> anyhow::Result ecash_keypair_wrapper.clone(), comm_channel, storage.clone(), - task_manager.subscribe_named("ecash-state-data-cleaner"), + &shutdown_manager, ); // if ecash signer is enabled, there are additional constraints on the nym-api, @@ -204,7 +213,11 @@ async fn start_nym_api_tasks(config: &Config) -> anyhow::Result // check if signers cache is enabled, and if so, start the refresher let ecash_signers_cache = if config.signers_cache.enabled { - signers_cache::start_refresher(&config.signers_cache, nyxd_client.clone(), &task_manager) + signers_cache::start_refresher( + &config.signers_cache, + nyxd_client.clone(), + &shutdown_manager, + ) } else { SharedCache::new() }; @@ -243,8 +256,8 @@ async fn start_nym_api_tasks(config: &Config) -> anyhow::Result let describe_cache_refresh_requester = describe_cache_refresher.refresh_requester(); - let describe_cache_watcher = describe_cache_refresher - .start_with_watcher(task_manager.subscribe_named("node-self-described-data-refresher")); + let describe_cache_watcher = + describe_cache_refresher.start_with_watcher(shutdown_manager.clone_shutdown_token()); let performance_provider = if config.performance_provider.use_performance_contract_data { if network_details @@ -260,7 +273,7 @@ async fn start_nym_api_tasks(config: &Config) -> anyhow::Result &config.performance_provider, nyxd_client.clone(), mixnet_contract_cache_state.clone(), - &task_manager, + &shutdown_manager, ) .await?; let provider = ContractPerformanceProvider::new( @@ -276,13 +289,8 @@ async fn start_nym_api_tasks(config: &Config) -> anyhow::Result }; // start all the caches first - let mixnet_contract_cache_refresher = mixnet_contract_cache::build_refresher( - &config.mixnet_contract_cache, - &mixnet_contract_cache_state.clone(), - nyxd_client.clone(), - ); let contract_cache_watcher = - mixnet_contract_cache_refresher.start_with_watcher(task_manager.subscribe()); + mixnet_contract_cache_refresher.start_with_watcher(shutdown_manager.clone_shutdown_token()); node_status_api::start_cache_refresh( &config.node_status_api, @@ -292,7 +300,7 @@ async fn start_nym_api_tasks(config: &Config) -> anyhow::Result performance_provider, contract_cache_watcher.clone(), describe_cache_watcher, - &task_manager, + &shutdown_manager, ); // start dkg task @@ -306,7 +314,7 @@ async fn start_nym_api_tasks(config: &Config) -> anyhow::Result dkg_bte_keypair, identity_public_key, rand::rngs::OsRng, - &task_manager, + &shutdown_manager, )?; } @@ -323,11 +331,11 @@ async fn start_nym_api_tasks(config: &Config) -> anyhow::Result node_status_cache_state.clone(), &storage, nyxd_client.clone(), - &task_manager, + &shutdown_manager, ) .await; - HistoricalUptimeUpdater::start(storage.to_owned(), &task_manager); + HistoricalUptimeUpdater::start(storage.to_owned(), &shutdown_manager); } // start 'rewarding' if its enabled and there exists source for performance data @@ -336,10 +344,11 @@ async fn start_nym_api_tasks(config: &Config) -> anyhow::Result EpochAdvancer::start( nyxd_client, &mixnet_contract_cache_state, + mixnet_contract_cache_refresh_requester, &node_status_cache_state, described_nodes_cache.clone(), &storage, - &task_manager, + &shutdown_manager, ); } @@ -350,24 +359,22 @@ async fn start_nym_api_tasks(config: &Config) -> anyhow::Result contract_cache_watcher, mixnet_contract_cache_state, ) - .start(task_manager.subscribe_named("KeyRotationController")); + .start(shutdown_manager.clone_shutdown_token()); let bind_address = config.base.bind_address.to_owned(); let server = router.build_server(&bind_address).await?; - let cancellation_token = CancellationToken::new(); - let shutdown_button = cancellation_token.clone(); - let axum_shutdown_receiver = cancellation_token.cancelled_owned(); - let server_handle = tokio::spawn(async move { + let http_shutdown = shutdown_manager.clone_shutdown_token(); + tokio::spawn(async move { { info!("Started Axum HTTP V2 server on {bind_address}"); - server.run(axum_shutdown_receiver).await + server.run(http_shutdown).await } }); - let shutdown = ShutdownHandles::new(task_manager, server_handle, shutdown_button); + shutdown_manager.close_tracker(); - Ok(shutdown) + Ok(shutdown_manager) } pub(crate) async fn execute(args: Args) -> anyhow::Result<()> { @@ -378,32 +385,8 @@ pub(crate) async fn execute(args: Args) -> anyhow::Result<()> { config.validate()?; - let mut axum_shutdown = start_nym_api_tasks(&config).await?; - - // it doesn't matter which server catches the interrupt: it needs only be caught once - if let Err(err) = axum_shutdown.task_manager_mut().catch_interrupt().await { - error!("Error stopping axum tasks: {err}"); - } - - info!("Stopping nym API"); - - axum_shutdown.task_manager_mut().signal_shutdown().ok(); - axum_shutdown.task_manager_mut().wait_for_shutdown().await; - - let running_server = axum_shutdown.shutdown_axum(); - - match running_server.await { - Ok(Ok(_)) => { - info!("Axum HTTP server shut down without errors"); - } - Ok(Err(err)) => { - error!("Axum HTTP server terminated with: {err}"); - anyhow::bail!(err) - } - Err(err) => { - error!("Server task panicked: {err}"); - } - }; + let mut shutdown_manager = start_nym_api_tasks(&config).await?; + shutdown_manager.run_until_shutdown().await; Ok(()) } diff --git a/nym-api/src/support/http/mod.rs b/nym-api/src/support/http/mod.rs index 39049f38d3..db73c64a3c 100644 --- a/nym-api/src/support/http/mod.rs +++ b/nym-api/src/support/http/mod.rs @@ -1,10 +1,6 @@ // Copyright 2022-2023 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use nym_task::TaskManager; -use tokio::task::JoinHandle; -use tokio_util::sync::CancellationToken; - pub(crate) mod helpers; pub(crate) mod openapi; pub(crate) mod router; @@ -13,50 +9,3 @@ pub(crate) mod state; pub(crate) use router::RouterBuilder; pub(crate) const TASK_MANAGER_TIMEOUT_S: u64 = 10; - -/// Shutdown goes 2 directions: -/// 1. signal background tasks to gracefully finish -/// 2. signal server itself -/// -/// These are done through separate shutdown handles. Of course, shut down server -/// AFTER you have shut down BG tasks (or past their grace period). -pub(crate) struct ShutdownHandles { - task_manager: TaskManager, - axum_shutdown_button: ShutdownAxum, - /// Tokio JoinHandle for axum server's task - axum_join_handle: AxumJoinHandle, -} - -impl ShutdownHandles { - /// Cancellation token is given to Axum server constructor. When the token - /// receives a shutdown signal, Axum server will shut down gracefully. - pub(crate) fn new( - task_manager: TaskManager, - axum_server_handle: AxumJoinHandle, - shutdown_button: CancellationToken, - ) -> Self { - Self { - task_manager, - axum_shutdown_button: ShutdownAxum(shutdown_button.clone()), - axum_join_handle: axum_server_handle, - } - } - - pub(crate) fn task_manager_mut(&mut self) -> &mut TaskManager { - &mut self.task_manager - } - - /// Signal server to shut down, then return join handle to its - /// `tokio` task - /// - /// https://tikv.github.io/doc/tokio/task/struct.JoinHandle.html - #[must_use] - pub(crate) fn shutdown_axum(self) -> AxumJoinHandle { - self.axum_shutdown_button.0.cancel(); - self.axum_join_handle - } -} - -struct ShutdownAxum(CancellationToken); - -type AxumJoinHandle = JoinHandle>; diff --git a/nym-api/src/support/http/router.rs b/nym-api/src/support/http/router.rs index b04956eed8..56a8950968 100644 --- a/nym-api/src/support/http/router.rs +++ b/nym-api/src/support/http/router.rs @@ -3,10 +3,9 @@ use crate::circulating_supply_api::handlers::circulating_supply_routes; use crate::ecash::api_routes::handlers::ecash_routes; -use crate::mixnet_contract_cache::handlers::nym_contract_cache_routes; +use crate::mixnet_contract_cache::handlers::{epoch_routes, legacy_nodes_routes}; use crate::network::handlers::nym_network_routes; use crate::node_status_api::handlers::status_routes; -use crate::nym_nodes::handlers::legacy::legacy_nym_node_routes; use crate::nym_nodes::handlers::nym_node_routes; use crate::status; use crate::support::http::openapi::ApiDoc; @@ -19,8 +18,8 @@ use axum::routing::get; use axum::Router; use core::net::SocketAddr; use nym_http_api_common::middleware::logging::log_request_info; +use nym_task::ShutdownToken; use tokio::net::TcpListener; -use tokio_util::sync::WaitForCancellationFutureOwned; use tower_http::cors::CorsLayer; use utoipa::OpenApi; use utoipa_swagger_ui::SwaggerUi; @@ -57,15 +56,15 @@ impl RouterBuilder { "/v1", Router::new() // unfortunately some routes didn't use correct prefix and were attached to the root - .merge(nym_contract_cache_routes()) - .merge(legacy_nym_node_routes()) + .nest("/epoch", epoch_routes()) .nest("/circulating-supply", circulating_supply_routes()) .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_v1()), // CORS layer needs to be "outside" of routes + .nest("/unstable", unstable_routes_v1()) + .nest("/legacy", legacy_nodes_routes()), // CORS layer needs to be "outside" of routes ) .nest("/v2", Router::new().nest("/unstable", unstable_routes_v2())); @@ -131,14 +130,13 @@ pub(crate) struct ApiHttpServer { } impl ApiHttpServer { - pub async fn run(self, receiver: WaitForCancellationFutureOwned) -> Result<(), std::io::Error> { - // into_make_service_with_connect_info allows us to see client ip address + pub async fn run(self, shutdown_token: ShutdownToken) -> Result<(), std::io::Error> { axum::serve( self.listener, self.router .into_make_service_with_connect_info::(), ) - .with_graceful_shutdown(receiver) + .with_graceful_shutdown(async move { shutdown_token.cancelled().await }) .await } } diff --git a/nym-api/src/support/http/state/mod.rs b/nym-api/src/support/http/state/mod.rs index bad1b7f4ed..f0ac2d121b 100644 --- a/nym-api/src/support/http/state/mod.rs +++ b/nym-api/src/support/http/state/mod.rs @@ -20,7 +20,7 @@ use crate::support::storage; use crate::unstable_routes::v1::account::cache::AddressInfoCache; use crate::unstable_routes::v1::account::models::NyxAccountDetails; use axum::extract::FromRef; -use nym_api_requests::models::{GatewayBondAnnotated, MixNodeBondAnnotated, NodeAnnotation}; +use nym_api_requests::models::NodeAnnotation; use nym_crypto::asymmetric::ed25519; use nym_mixnet_contract_common::NodeId; use nym_topology::CachedEpochRewardedSet; @@ -168,26 +168,6 @@ impl AppState { .ok_or_else(AxumErrorResponse::internal) } - pub(crate) async fn legacy_mixnode_annotations( - &self, - ) -> Result>>, AxumErrorResponse> - { - self.node_status_cache() - .annotated_legacy_mixnodes() - .await - .ok_or_else(AxumErrorResponse::internal) - } - - pub(crate) async fn legacy_gateways_annotations( - &self, - ) -> Result>>, AxumErrorResponse> - { - self.node_status_cache() - .annotated_legacy_gateways() - .await - .ok_or_else(AxumErrorResponse::internal) - } - pub(crate) async fn get_address_info( self, account_id: nym_validator_client::nyxd::AccountId, diff --git a/nym-api/src/support/legacy_helpers.rs b/nym-api/src/support/legacy_helpers.rs deleted file mode 100644 index 430a73aad6..0000000000 --- a/nym-api/src/support/legacy_helpers.rs +++ /dev/null @@ -1,114 +0,0 @@ -// Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only - -use nym_api_requests::legacy::{LegacyMixNodeBondWithLayer, LegacyMixNodeDetailsWithLayer}; -use nym_api_requests::models::NymNodeData; -use nym_config::defaults::DEFAULT_NYM_NODE_HTTP_PORT; -use nym_mixnet_contract_common::mixnode::LegacyPendingMixNodeChanges; -use nym_mixnet_contract_common::{ - Gateway, GatewayBond, LegacyMixLayer, MixNode, MixNodeBond, NymNodeDetails, -}; -use rand::prelude::SliceRandom; -use rand::rngs::OsRng; -use std::net::{IpAddr, ToSocketAddrs}; -use std::str::FromStr; - -pub(crate) fn legacy_host_to_ips_and_hostname( - legacy: &str, -) -> Option<(Vec, Option)> { - if let Ok(ip) = IpAddr::from_str(legacy) { - return Some((vec![ip], None)); - } - - let resolved = (legacy, 1789u16) - .to_socket_addrs() - .ok()? - .collect::>(); - Some(( - resolved.into_iter().map(|s| s.ip()).collect(), - Some(legacy.to_string()), - )) -} - -pub(crate) fn to_legacy_mixnode( - nym_node: &NymNodeDetails, - description: &NymNodeData, -) -> LegacyMixNodeDetailsWithLayer { - let layer_choices = [ - LegacyMixLayer::One, - LegacyMixLayer::Two, - LegacyMixLayer::Three, - ]; - let mut rng = OsRng; - - // slap a random layer on it because legacy clients don't understand a concept of layerless mixnodes - // SAFETY: the slice is not empty so the unwrap is fine - #[allow(clippy::unwrap_used)] - let layer = layer_choices.choose(&mut rng).copied().unwrap(); - - LegacyMixNodeDetailsWithLayer { - bond_information: LegacyMixNodeBondWithLayer { - bond: MixNodeBond { - mix_id: nym_node.node_id(), - owner: nym_node.bond_information.owner.clone(), - original_pledge: nym_node.bond_information.original_pledge.clone(), - mix_node: MixNode { - host: nym_node.bond_information.node.host.clone(), - mix_port: description.mix_port(), - verloc_port: description.verloc_port(), - http_api_port: nym_node - .bond_information - .node - .custom_http_port - .unwrap_or(DEFAULT_NYM_NODE_HTTP_PORT), - sphinx_key: description - .host_information - .keys - .current_x25519_sphinx_key - .public_key - .to_base58_string(), - identity_key: nym_node.bond_information.node.identity_key.clone(), - version: description.build_information.build_version.clone(), - }, - proxy: None, - bonding_height: nym_node.bond_information.bonding_height, - is_unbonding: nym_node.bond_information.is_unbonding, - }, - layer, - }, - rewarding_details: nym_node.rewarding_details.clone(), - pending_changes: LegacyPendingMixNodeChanges { - pledge_change: nym_node.pending_changes.pledge_change, - }, - } -} - -pub(crate) fn to_legacy_gateway( - nym_node: &NymNodeDetails, - description: &NymNodeData, -) -> GatewayBond { - GatewayBond { - pledge_amount: nym_node.bond_information.original_pledge.clone(), - owner: nym_node.bond_information.owner.clone(), - block_height: nym_node.bond_information.bonding_height, - gateway: Gateway { - host: nym_node.bond_information.node.host.clone(), - mix_port: description.mix_port(), - clients_port: description.mixnet_websockets.ws_port, - location: description - .auxiliary_details - .location - .map(|c| c.to_string()) - .unwrap_or_default(), - sphinx_key: description - .host_information - .keys - .current_x25519_sphinx_key - .public_key - .to_base58_string(), - identity_key: nym_node.bond_information.node.identity_key.clone(), - version: description.build_information.build_version.clone(), - }, - proxy: None, - } -} diff --git a/nym-api/src/support/mod.rs b/nym-api/src/support/mod.rs index a1773f9804..28abaf3b94 100644 --- a/nym-api/src/support/mod.rs +++ b/nym-api/src/support/mod.rs @@ -7,5 +7,3 @@ pub(crate) mod config; pub(crate) mod http; pub(crate) mod nyxd; pub(crate) mod storage; - -pub(crate) mod legacy_helpers; diff --git a/nym-api/src/unstable_routes/helpers.rs b/nym-api/src/unstable_routes/helpers.rs index 216ffacf4b..8d860dcb44 100644 --- a/nym-api/src/unstable_routes/helpers.rs +++ b/nym-api/src/unstable_routes/helpers.rs @@ -1,68 +1,9 @@ // Copyright 2025 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use nym_api_requests::models::{ - GatewayBondAnnotated, MalformedNodeBond, MixNodeBondAnnotated, OffsetDateTimeJsonSchemaWrapper, -}; -use nym_api_requests::nym_nodes::{NodeRole, SemiSkimmedNode, SkimmedNode}; -use nym_mixnet_contract_common::reward_params::Performance; +use nym_api_requests::models::OffsetDateTimeJsonSchemaWrapper; use time::OffsetDateTime; -pub(crate) trait LegacyAnnotation { - fn performance(&self) -> Performance; - - fn identity(&self) -> &str; - - fn try_to_skimmed_node(&self, role: NodeRole) -> Result; - - fn try_to_semi_skimmed_node( - &self, - role: NodeRole, - ) -> Result; -} - -impl LegacyAnnotation for MixNodeBondAnnotated { - fn performance(&self) -> Performance { - self.node_performance.last_24h - } - - fn identity(&self) -> &str { - self.identity_key() - } - - fn try_to_skimmed_node(&self, role: NodeRole) -> Result { - self.try_to_skimmed_node(role) - } - - fn try_to_semi_skimmed_node( - &self, - role: NodeRole, - ) -> Result { - self.try_to_semi_skimmed_node(role) - } -} - -impl LegacyAnnotation for GatewayBondAnnotated { - fn performance(&self) -> Performance { - self.node_performance.last_24h - } - - fn identity(&self) -> &str { - self.identity() - } - - fn try_to_skimmed_node(&self, role: NodeRole) -> Result { - self.try_to_skimmed_node(role) - } - - fn try_to_semi_skimmed_node( - &self, - role: NodeRole, - ) -> Result { - self.try_to_semi_skimmed_node(role) - } -} - pub(crate) fn refreshed_at( iter: impl IntoIterator, ) -> OffsetDateTimeJsonSchemaWrapper { diff --git a/nym-api/src/unstable_routes/v2/nym_nodes/semi_skimmed/mod.rs b/nym-api/src/unstable_routes/v2/nym_nodes/semi_skimmed/mod.rs index 69cc2dea45..e8ceaf911b 100644 --- a/nym-api/src/unstable_routes/v2/nym_nodes/semi_skimmed/mod.rs +++ b/nym-api/src/unstable_routes/v2/nym_nodes/semi_skimmed/mod.rs @@ -1,10 +1,9 @@ // Copyright 2025 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::node_describe_cache::cache::DescribedNodes; use crate::node_status_api::models::AxumResult; use crate::support::http::state::AppState; -use crate::unstable_routes::helpers::{refreshed_at, LegacyAnnotation}; +use crate::unstable_routes::helpers::refreshed_at; use crate::unstable_routes::v2::nym_nodes::helpers::NodesParamsWithRole; use axum::extract::{Query, State}; use nym_api_requests::models::{ @@ -16,7 +15,6 @@ use nym_http_api_common::FormattedResponse; use nym_mixnet_contract_common::NodeId; use nym_topology::CachedEpochRewardedSet; use std::collections::HashMap; -use tracing::trace; use utoipa::ToSchema; pub type PaginatedSemiSkimmedNodes = @@ -57,39 +55,6 @@ where nodes } -//SW TODO : this is copied from skimmed nodes, surely we can do better than that -/// Given all relevant caches, add appropriate legacy nodes to the part of the response -fn add_legacy( - nodes: &mut Vec, - rewarded_set: &CachedEpochRewardedSet, - describe_cache: &DescribedNodes, - annotated_legacy_nodes: &HashMap, - current_key_rotation: u32, -) where - LN: LegacyAnnotation, -{ - for (node_id, legacy) in annotated_legacy_nodes.iter() { - let role: NodeRole = rewarded_set.role(*node_id).into(); - - // if we have self-described info, prefer it over contract data - if let Some(described) = describe_cache.get_node(node_id) { - nodes.push(described.to_semi_skimmed_node( - current_key_rotation, - role, - legacy.performance(), - )) - } else { - match legacy.try_to_semi_skimmed_node(role) { - Ok(node) => nodes.push(node), - Err(err) => { - let id = legacy.identity(); - trace!("node {id} is malformed: {err}") - } - } - } - } -} - #[allow(dead_code)] // not dead, used in OpenAPI docs #[derive(ToSchema)] #[schema(title = "PaginatedCachedNodesExpandedResponseSchema")] @@ -125,14 +90,12 @@ pub(super) async fn nodes_expanded( let describe_cache = state.describe_nodes_cache_data().await?; let all_nym_nodes = describe_cache.all_nym_nodes(); let annotations = state.node_annotations().await?; - let legacy_mixnodes = state.legacy_mixnode_annotations().await?; - let legacy_gateways = state.legacy_gateways_annotations().await?; let contract_cache = state.nym_contract_cache(); let current_key_rotation = contract_cache.current_key_rotation_id().await?; let interval = contract_cache.current_interval().await?; - let mut nodes = build_nym_nodes_response( + let nodes = build_nym_nodes_response( &rewarded_set, all_nym_nodes, &annotations, @@ -140,31 +103,11 @@ pub(super) async fn nodes_expanded( false, ); - // add legacy gateways to the response - add_legacy( - &mut nodes, - &rewarded_set, - &describe_cache, - &legacy_gateways, - current_key_rotation, - ); - - // add legacy mixnodes to the response - add_legacy( - &mut nodes, - &rewarded_set, - &describe_cache, - &legacy_mixnodes, - current_key_rotation, - ); - // min of all caches let refreshed_at = refreshed_at([ rewarded_set.timestamp(), annotations.timestamp(), describe_cache.timestamp(), - legacy_mixnodes.timestamp(), - legacy_gateways.timestamp(), ]); let output = query_params.output.unwrap_or_default(); diff --git a/nym-api/src/unstable_routes/v2/nym_nodes/skimmed/helpers.rs b/nym-api/src/unstable_routes/v2/nym_nodes/skimmed/helpers.rs index 1f1fb39203..56f9619803 100644 --- a/nym-api/src/unstable_routes/v2/nym_nodes/skimmed/helpers.rs +++ b/nym-api/src/unstable_routes/v2/nym_nodes/skimmed/helpers.rs @@ -1,11 +1,8 @@ // Copyright 2025 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::node_describe_cache::cache::DescribedNodes; -use crate::node_status_api::models::AxumErrorResponse; -use crate::support::caching::Cache; use crate::support::http::state::AppState; -use crate::unstable_routes::helpers::{refreshed_at, LegacyAnnotation}; +use crate::unstable_routes::helpers::refreshed_at; use crate::unstable_routes::v2::nym_nodes::helpers::NodesParams; use crate::unstable_routes::v2::nym_nodes::skimmed::PaginatedSkimmedNodes; use axum::extract::{Query, State}; @@ -17,10 +14,7 @@ use nym_http_api_common::Output; use nym_mixnet_contract_common::{Interval, NodeId}; use nym_topology::CachedEpochRewardedSet; use std::collections::HashMap; -use std::future::Future; use std::time::Duration; -use tokio::sync::RwLockReadGuard; -use tracing::trace; /// Given all relevant caches, build part of response for JUST Nym Nodes fn build_nym_nodes_response<'a, NI>( @@ -57,41 +51,6 @@ where nodes } -/// Given all relevant caches, add appropriate legacy nodes to the part of the response -fn add_legacy( - nodes: &mut Vec, - rewarded_set: &CachedEpochRewardedSet, - describe_cache: &DescribedNodes, - annotated_legacy_nodes: &HashMap, - current_key_rotation: u32, - active_only: bool, -) where - LN: LegacyAnnotation, -{ - for (node_id, legacy) in annotated_legacy_nodes.iter() { - let role: NodeRole = rewarded_set.role(*node_id).into(); - - // if the role is inactive, see if our filter allows it - if active_only && role.is_inactive() { - continue; - } - - // if we have self-described info, prefer it over contract data - if let Some(described) = describe_cache.get_node(node_id) { - // legacy nodes don't support key rotation - nodes.push(described.to_skimmed_node(current_key_rotation, role, legacy.performance())) - } else { - match legacy.try_to_skimmed_node(role) { - Ok(node) => nodes.push(node), - Err(err) => { - let id = legacy.identity(); - trace!("node {id} is malformed: {err}") - } - } - } - } -} - fn maybe_add_expires_header( output: Output, interval: Interval, @@ -123,25 +82,16 @@ fn maybe_add_expires_header( // hehe, what an abomination, but it's used in multiple different places and I hate copy-pasting code, // especially if it has multiple loops, etc -pub(crate) async fn build_skimmed_nodes_response<'a, NI, LG, Fut, LN>( +pub(crate) async fn build_skimmed_nodes_response<'a, NI>( state: &'a AppState, Query(query_params): Query, nym_nodes_subset: NI, - annotated_legacy_nodes_getter: LG, active_only: bool, output: Output, ) -> PaginatedSkimmedNodes where // iterator returning relevant subset of nym-nodes (like mixing nym-nodes, entries, etc.) NI: Iterator + 'a, - - // async function that returns cache of appropriate legacy nodes (mixnodes or gateways) - LG: Fn(&'a AppState) -> Fut, - Fut: - Future>>, AxumErrorResponse>>, - - // the legacy node (MixNodeBondAnnotated or GatewayBondAnnotated) - LN: LegacyAnnotation + 'a, { // TODO: implement it let _ = query_params.per_page; @@ -175,7 +125,7 @@ where } // 4. start building the response - let mut nodes = build_nym_nodes_response( + let nodes = build_nym_nodes_response( &rewarded_set, nym_nodes_subset, &annotations, @@ -202,24 +152,11 @@ where ); } - // 6. grab relevant legacy nodes - // (due to the existence of the legacy endpoints, we already have fully annotated data on them) - let annotated_legacy_nodes = annotated_legacy_nodes_getter(state).await?; - add_legacy( - &mut nodes, - &rewarded_set, - &describe_cache, - &annotated_legacy_nodes, - current_key_rotation, - active_only, - ); - // min of all caches let refreshed_at = refreshed_at([ rewarded_set.timestamp(), annotations.timestamp(), describe_cache.timestamp(), - annotated_legacy_nodes.timestamp(), ]); maybe_add_expires_header( @@ -247,13 +184,11 @@ pub(crate) async fn nodes_basic( let describe_cache = state.describe_nodes_cache_data().await?; let all_nym_nodes = describe_cache.all_nym_nodes(); let annotations = state.node_annotations().await?; - let legacy_mixnodes = state.legacy_mixnode_annotations().await?; - let legacy_gateways = state.legacy_gateways_annotations().await?; let interval = state.nym_contract_cache().current_interval().await?; let current_key_rotation = state.nym_contract_cache().current_key_rotation_id().await?; - let mut nodes = build_nym_nodes_response( + let nodes = build_nym_nodes_response( &rewarded_set, all_nym_nodes, &annotations, @@ -261,33 +196,11 @@ pub(crate) async fn nodes_basic( active_only, ); - // add legacy gateways to the response - add_legacy( - &mut nodes, - &rewarded_set, - &describe_cache, - &legacy_gateways, - current_key_rotation, - active_only, - ); - - // add legacy mixnodes to the response - add_legacy( - &mut nodes, - &rewarded_set, - &describe_cache, - &legacy_mixnodes, - current_key_rotation, - active_only, - ); - // min of all caches let refreshed_at = refreshed_at([ rewarded_set.timestamp(), annotations.timestamp(), describe_cache.timestamp(), - legacy_mixnodes.timestamp(), - legacy_gateways.timestamp(), ]); Ok(output.to_response(PaginatedCachedNodesResponseV2::new_full( @@ -313,7 +226,6 @@ pub(crate) async fn mixnodes_basic( &state.0, query_params, mixing_nym_nodes, - |state| state.legacy_mixnode_annotations(), active_only, output, ) @@ -335,7 +247,6 @@ pub(crate) async fn entry_gateways_basic( &state.0, query_params, mixing_nym_nodes, - |state| state.legacy_gateways_annotations(), active_only, output, ) @@ -357,7 +268,6 @@ pub(crate) async fn exit_gateways_basic( &state.0, query_params, mixing_nym_nodes, - |state| state.legacy_gateways_annotations(), active_only, output, ) diff --git a/nym-api/tests/public-api/api_status.rs b/nym-api/tests/public-api/api_status.rs index f6e6230ff2..e06f42ea5c 100644 --- a/nym-api/tests/public-api/api_status.rs +++ b/nym-api/tests/public-api/api_status.rs @@ -1,6 +1,7 @@ use crate::utils::{base_url, make_request, validate_json_response}; #[tokio::test] +#[test_with::env(NYM_API)] async fn test_health() -> Result<(), String> { let url = format!("{}/v1/api-status/health", base_url()?); let res = make_request(&url).await?; @@ -11,6 +12,7 @@ async fn test_health() -> Result<(), String> { } #[tokio::test] +#[test_with::env(NYM_API)] async fn test_build_information() -> Result<(), String> { let url = format!("{}/v1/api-status/build-information", base_url()?); let res = make_request(&url).await?; diff --git a/nym-api/tests/public-api/circulating_supply.rs b/nym-api/tests/public-api/circulating_supply.rs index 105c48dd07..0f112f0595 100644 --- a/nym-api/tests/public-api/circulating_supply.rs +++ b/nym-api/tests/public-api/circulating_supply.rs @@ -1,6 +1,7 @@ use crate::utils::{base_url, make_request, validate_json_response}; #[tokio::test] +#[test_with::env(NYM_API)] async fn test_get_circulating_supply() -> Result<(), String> { let url = format!("{}/v1/circulating-supply", base_url()?); let res = make_request(&url).await?; @@ -14,6 +15,7 @@ async fn test_get_circulating_supply() -> Result<(), String> { } #[tokio::test] +#[test_with::env(NYM_API)] async fn test_get_circulating_supply_value() -> Result<(), String> { let url = format!( "{}/v1/circulating-supply/circulating-supply-value", @@ -32,6 +34,7 @@ async fn test_get_circulating_supply_value() -> Result<(), String> { } #[tokio::test] +#[test_with::env(NYM_API)] async fn test_get_total_supply_value() -> Result<(), String> { let url = format!("{}/v1/circulating-supply/total-supply-value", base_url()?); let res = make_request(&url).await?; diff --git a/nym-api/tests/public-api/contract_cache.rs b/nym-api/tests/public-api/contract_cache.rs index 8510f30d95..0d93bda1f8 100644 --- a/nym-api/tests/public-api/contract_cache.rs +++ b/nym-api/tests/public-api/contract_cache.rs @@ -1,6 +1,7 @@ use crate::utils::{base_url, make_request, validate_json_response}; #[tokio::test] +#[test_with::env(NYM_API)] async fn test_get_current_epoch() -> Result<(), String> { let url = format!("{}/v1/epoch/current", base_url()?); let res = make_request(&url).await?; @@ -19,6 +20,7 @@ async fn test_get_current_epoch() -> Result<(), String> { } #[tokio::test] +#[test_with::env(NYM_API)] async fn test_get_reward_params() -> Result<(), String> { let url = format!("{}/v1/epoch/reward_params", base_url()?); let res = make_request(&url).await?; diff --git a/nym-api/tests/public-api/network.rs b/nym-api/tests/public-api/network.rs index b6e1e3bc4c..6b23f8df7d 100644 --- a/nym-api/tests/public-api/network.rs +++ b/nym-api/tests/public-api/network.rs @@ -1,6 +1,7 @@ use crate::utils::{base_url, test_client, validate_json_response}; #[tokio::test] +#[test_with::env(NYM_API)] async fn test_get_chain_status() -> Result<(), String> { let url = format!("{}/v1/network/chain-status", base_url()?); let res = test_client() @@ -29,6 +30,7 @@ async fn test_get_chain_status() -> Result<(), String> { } #[tokio::test] +#[test_with::env(NYM_API)] async fn test_get_network_details() -> Result<(), String> { let url = format!("{}/v1/network/details", base_url()?); let res = test_client() @@ -54,6 +56,7 @@ async fn test_get_network_details() -> Result<(), String> { } #[tokio::test] +#[test_with::env(NYM_API)] async fn test_get_nym_contracts() -> Result<(), String> { let url = format!("{}/v1/network/nym-contracts", base_url()?); let res = test_client() @@ -75,6 +78,7 @@ async fn test_get_nym_contracts() -> Result<(), String> { } #[tokio::test] +#[test_with::env(NYM_API)] async fn test_get_nym_contracts_detailed() -> Result<(), String> { let url = format!("{}/v1/network/nym-contracts-detailed", base_url()?); let res = test_client() diff --git a/nym-api/tests/public-api/nym_nodes.rs b/nym-api/tests/public-api/nym_nodes.rs index 425a115620..35d2eaf0ea 100644 --- a/nym-api/tests/public-api/nym_nodes.rs +++ b/nym-api/tests/public-api/nym_nodes.rs @@ -2,6 +2,7 @@ use crate::utils::{base_url, get_any_node_id, make_request, test_client, validat use time::OffsetDateTime; #[tokio::test] +#[test_with::env(NYM_API)] async fn test_get_bonded_nodes() -> Result<(), String> { let url = format!("{}/v1/nym-nodes/bonded", base_url()?); let res = make_request(&url).await?; @@ -19,6 +20,7 @@ async fn test_get_bonded_nodes() -> Result<(), String> { } #[tokio::test] +#[test_with::env(NYM_API)] async fn test_get_described_nodes() -> Result<(), String> { let url = format!("{}/v1/nym-nodes/described", base_url()?); let res = make_request(&url).await?; @@ -37,13 +39,14 @@ async fn test_get_described_nodes() -> Result<(), String> { // TODO enable this once noise is properly integrated // #[tokio::test] +#[test_with::env(NYM_API)] // async fn test_get_noise() -> Result<(), String> { // let url = format!("{}/v1/nym-nodes/noise", base_url()?); // let res = test_client().get(&url).send().await.map_err(|err| panic!("Failed to send request to {}: {}", url, err))?; // let json = validate_json_response(res).await; // } - #[tokio::test] +#[test_with::env(NYM_API)] async fn test_get_rewarded_set() -> Result<(), String> { let url = format!("{}/v1/nym-nodes/rewarded-set", base_url()?); let res = make_request(&url).await?; @@ -64,6 +67,7 @@ async fn test_get_rewarded_set() -> Result<(), String> { } #[tokio::test] +#[test_with::env(NYM_API)] async fn test_get_annotation_for_node() -> Result<(), String> { let id = get_any_node_id().await?; let url = format!("{}/v1/nym-nodes/annotation/{}", base_url()?, id); @@ -81,6 +85,7 @@ async fn test_get_annotation_for_node() -> Result<(), String> { } #[tokio::test] +#[test_with::env(NYM_API)] async fn test_get_historical_performance() -> Result<(), String> { let id = get_any_node_id().await?; let date = OffsetDateTime::now_utc().date().to_string(); @@ -101,6 +106,7 @@ async fn test_get_historical_performance() -> Result<(), String> { } #[tokio::test] +#[test_with::env(NYM_API)] async fn test_get_performance_history() -> Result<(), String> { let id = get_any_node_id().await?; let url = format!("{}/v1/nym-nodes/performance-history/{}", base_url()?, id); @@ -120,6 +126,7 @@ async fn test_get_performance_history() -> Result<(), String> { } #[tokio::test] +#[test_with::env(NYM_API)] async fn test_get_performance() -> Result<(), String> { let id = get_any_node_id().await?; let url = format!("{}/v1/nym-nodes/performance/{}", base_url()?, id); @@ -138,6 +145,7 @@ async fn test_get_performance() -> Result<(), String> { } #[tokio::test] +#[test_with::env(NYM_API)] async fn test_get_uptime_history() -> Result<(), String> { let id = get_any_node_id().await?; let url = format!("{}/v1/nym-nodes/uptime-history/{}", base_url()?, id); diff --git a/nym-api/tests/public-api/status.rs b/nym-api/tests/public-api/status.rs index d0631fb68f..36510f3360 100644 --- a/nym-api/tests/public-api/status.rs +++ b/nym-api/tests/public-api/status.rs @@ -1,6 +1,7 @@ use crate::utils::{base_url, make_request, validate_json_response}; #[tokio::test] +#[test_with::env(NYM_API)] async fn test_get_config_score_details() -> Result<(), String> { let url = format!("{}/v1/status/config-score-details", base_url()?); let res = make_request(&url).await?; diff --git a/nym-api/tests/public-api/unstable_nym_nodes.rs b/nym-api/tests/public-api/unstable_nym_nodes.rs index e83aea6297..542ddbb35e 100644 --- a/nym-api/tests/public-api/unstable_nym_nodes.rs +++ b/nym-api/tests/public-api/unstable_nym_nodes.rs @@ -1,6 +1,7 @@ use crate::utils::{base_url, make_request, validate_json_response}; #[tokio::test] +#[test_with::env(NYM_API)] async fn test_get_skimmed_nodes_active() -> Result<(), String> { let url = format!("{}/v1/unstable/nym-nodes/skimmed/active", base_url()?); let res = make_request(&url).await?; @@ -19,6 +20,7 @@ async fn test_get_skimmed_nodes_active() -> Result<(), String> { } #[tokio::test] +#[test_with::env(NYM_API)] async fn test_get_skimmed_active_mixnodes() -> Result<(), String> { let url = format!( "{}/v1/unstable/nym-nodes/skimmed/mixnodes/active", @@ -44,6 +46,7 @@ async fn test_get_skimmed_active_mixnodes() -> Result<(), String> { } #[tokio::test] +#[test_with::env(NYM_API)] async fn test_get_skimmed_all_mixnodes() -> Result<(), String> { let url = format!("{}/v1/unstable/nym-nodes/skimmed/mixnodes/all", base_url()?); let res = make_request(&url).await?; @@ -66,6 +69,7 @@ async fn test_get_skimmed_all_mixnodes() -> Result<(), String> { } #[tokio::test] +#[test_with::env(NYM_API)] async fn test_get_skimmed_active_exit_gateways() -> Result<(), String> { let url = format!( "{}/v1/unstable/nym-nodes/skimmed/exit-gateways/active", @@ -91,6 +95,7 @@ async fn test_get_skimmed_active_exit_gateways() -> Result<(), String> { } #[tokio::test] +#[test_with::env(NYM_API)] async fn test_get_skimmed_all_exit_gateways() -> Result<(), String> { let url = format!( "{}/v1/unstable/nym-nodes/skimmed/exit-gateways/all", @@ -116,6 +121,7 @@ async fn test_get_skimmed_all_exit_gateways() -> Result<(), String> { } #[tokio::test] +#[test_with::env(NYM_API)] async fn test_get_skimmed_active_entry_gateways() -> Result<(), String> { let url = format!( "{}/v1/unstable/nym-nodes/skimmed/entry-gateways/active", @@ -141,6 +147,7 @@ async fn test_get_skimmed_active_entry_gateways() -> Result<(), String> { } #[tokio::test] +#[test_with::env(NYM_API)] async fn test_get_skimmed_all_entry_gateways() -> Result<(), String> { let url = format!( "{}/v1/unstable/nym-nodes/skimmed/entry-gateways/all", diff --git a/nym-api/tests/public-api/unstable_status.rs b/nym-api/tests/public-api/unstable_status.rs index 52a0084480..6ebbb476ca 100644 --- a/nym-api/tests/public-api/unstable_status.rs +++ b/nym-api/tests/public-api/unstable_status.rs @@ -4,6 +4,7 @@ use crate::utils::{ }; #[tokio::test] +#[test_with::env(NYM_API)] async fn test_get_gateway_unstable_test_results() -> Result<(), String> { let identity = get_gateway_identity_key().await?; let url = format!( @@ -35,6 +36,7 @@ async fn test_get_gateway_unstable_test_results() -> Result<(), String> { } #[tokio::test] +#[test_with::env(NYM_API)] async fn test_get_mixnode_unstable_test_results() -> Result<(), String> { let mix_id = get_mixnode_node_id().await?; let url = format!( @@ -66,6 +68,7 @@ async fn test_get_mixnode_unstable_test_results() -> Result<(), String> { } #[tokio::test] +#[test_with::env(NYM_API)] async fn test_get_latest_network_monitor_run_details() -> Result<(), String> { let url = format!( "{}/v1/status/network-monitor/unstable/run/latest/details", diff --git a/nym-api/tests/public-api/utils.rs b/nym-api/tests/public-api/utils.rs index ad0f61f653..fb697945ff 100644 --- a/nym-api/tests/public-api/utils.rs +++ b/nym-api/tests/public-api/utils.rs @@ -1,4 +1,4 @@ -use dotenv::dotenv; +use dotenvy::dotenv; use reqwest::{Client, Response}; use serde_json::Value; diff --git a/nym-authenticator-client/Cargo.toml b/nym-authenticator-client/Cargo.toml new file mode 100644 index 0000000000..35a72ae019 --- /dev/null +++ b/nym-authenticator-client/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "nym-authenticator-client" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true +edition.workspace = true +license.workspace = true + +[lints] +workspace = true + +[dependencies] +bincode.workspace = true +futures.workspace = true +semver.workspace = true +thiserror.workspace = true +tokio-util.workspace = true +tokio.workspace = true +tracing.workspace = true + +nym-authenticator-requests = { path = "../common/authenticator-requests" } +nym-credentials-interface = { path = "../common/credentials-interface" } +nym-crypto = { path = "../common/crypto" } +nym-sdk = { path = "../sdk/rust/nym-sdk" } +nym-service-provider-requests-common = { path = "../common/service-provider-requests-common" } +nym-wireguard-types = { path = "../common/wireguard-types" } diff --git a/nym-authenticator-client/src/error.rs b/nym-authenticator-client/src/error.rs new file mode 100644 index 0000000000..7bb0cd4dc4 --- /dev/null +++ b/nym-authenticator-client/src/error.rs @@ -0,0 +1,42 @@ +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("mixnet client stopped returning responses")] + NoMixnetMessagesReceived, + + #[error("failed to get version from message")] + NoVersionInMessage, + + #[error( + "received response with version v{received}, the client is too new and can only understand v{expected}" + )] + ReceivedResponseWithOldVersion { expected: u8, received: u8 }, + + #[error( + "received response with version v{received}, the client is too old and can only understand v{expected}" + )] + ReceivedResponseWithNewVersion { expected: u8, received: u8 }, + + #[error("failed to send mixnet message")] + SendMixnetMessage(#[source] Box), + + #[error("timeout waiting for connect response from exit gateway (authenticator)")] + TimeoutWaitingForConnectResponse, + + #[error("unable to get mixnet handle when sending authenticator message")] + UnableToGetMixnetHandle, + + #[error("unknown version number")] + UnknownVersion, + + #[error(transparent)] + Bincode(#[from] bincode::Error), + + #[error("gateway doesn't support this type of message")] + UnsupportedMessage, + + #[error(transparent)] + AuthenticatorRequests(#[from] nym_authenticator_requests::Error), +} + +// Result type based on our error type +pub type Result = std::result::Result; diff --git a/nym-authenticator-client/src/lib.rs b/nym-authenticator-client/src/lib.rs new file mode 100644 index 0000000000..03112e1651 --- /dev/null +++ b/nym-authenticator-client/src/lib.rs @@ -0,0 +1,1232 @@ +use std::{ + fmt, + net::{Ipv4Addr, Ipv6Addr}, + time::Duration, +}; + +use nym_authenticator_requests::{ + v2, v3, v4, + v5::{self, registration::IpPair}, +}; + +use nym_credentials_interface::CredentialSpendingData; +use nym_crypto::asymmetric::x25519::PrivateKey; +use nym_sdk::mixnet::{ + IncludedSurbs, MixnetClientSender, MixnetMessageSender, Recipient, ReconstructedMessage, + TransmissionLane, +}; +use nym_service_provider_requests_common::ServiceProviderType; +use nym_wireguard_types::PeerPublicKey; +use tracing::{debug, error}; + +mod error; +mod mixnet_listener; + +pub use crate::{ + error::{Error, Result}, + mixnet_listener::{ + AuthClientMixnetListener, AuthClientMixnetListenerHandle, MixnetMessageBroadcastReceiver, + }, +}; + +pub trait Versionable { + fn version(&self) -> AuthenticatorVersion; +} + +impl Versionable for v2::registration::InitMessage { + fn version(&self) -> AuthenticatorVersion { + AuthenticatorVersion::V2 + } +} + +impl Versionable for v3::registration::InitMessage { + fn version(&self) -> AuthenticatorVersion { + AuthenticatorVersion::V3 + } +} + +impl Versionable for v4::registration::InitMessage { + fn version(&self) -> AuthenticatorVersion { + AuthenticatorVersion::V4 + } +} + +impl Versionable for v5::registration::InitMessage { + fn version(&self) -> AuthenticatorVersion { + AuthenticatorVersion::V5 + } +} + +impl Versionable for v2::registration::FinalMessage { + fn version(&self) -> AuthenticatorVersion { + AuthenticatorVersion::V2 + } +} + +impl Versionable for v3::registration::FinalMessage { + fn version(&self) -> AuthenticatorVersion { + AuthenticatorVersion::V3 + } +} + +impl Versionable for v4::registration::FinalMessage { + fn version(&self) -> AuthenticatorVersion { + AuthenticatorVersion::V4 + } +} + +impl Versionable for v5::registration::FinalMessage { + fn version(&self) -> AuthenticatorVersion { + AuthenticatorVersion::V5 + } +} + +impl Versionable for PeerPublicKey { + fn version(&self) -> AuthenticatorVersion { + AuthenticatorVersion::V3 + } +} + +impl Versionable for v3::topup::TopUpMessage { + fn version(&self) -> AuthenticatorVersion { + AuthenticatorVersion::V3 + } +} + +impl Versionable for v4::topup::TopUpMessage { + fn version(&self) -> AuthenticatorVersion { + AuthenticatorVersion::V4 + } +} + +impl Versionable for v5::topup::TopUpMessage { + fn version(&self) -> AuthenticatorVersion { + AuthenticatorVersion::V5 + } +} + +pub trait InitMessage: Versionable + fmt::Debug { + fn pub_key(&self) -> PeerPublicKey; +} + +impl InitMessage for v2::registration::InitMessage { + fn pub_key(&self) -> PeerPublicKey { + self.pub_key + } +} + +impl InitMessage for v3::registration::InitMessage { + fn pub_key(&self) -> PeerPublicKey { + self.pub_key + } +} + +impl InitMessage for v4::registration::InitMessage { + fn pub_key(&self) -> PeerPublicKey { + self.pub_key + } +} + +impl InitMessage for v5::registration::InitMessage { + fn pub_key(&self) -> PeerPublicKey { + self.pub_key + } +} + +pub trait FinalMessage: Versionable + fmt::Debug { + fn gateway_client_pub_key(&self) -> PeerPublicKey; + fn gateway_client_ipv4(&self) -> Option; + fn gateway_client_ipv6(&self) -> Option; + fn gateway_client_mac(&self) -> Vec; + fn credential(&self) -> Option; +} + +impl FinalMessage for v2::registration::FinalMessage { + fn gateway_client_pub_key(&self) -> PeerPublicKey { + self.gateway_client.pub_key + } + + fn gateway_client_ipv4(&self) -> Option { + match self.gateway_client.private_ip { + std::net::IpAddr::V4(ipv4_addr) => Some(ipv4_addr), + std::net::IpAddr::V6(_) => None, + } + } + + fn gateway_client_ipv6(&self) -> Option { + None + } + + fn gateway_client_mac(&self) -> Vec { + self.gateway_client.mac.to_vec() + } + + fn credential(&self) -> Option { + self.credential.clone() + } +} + +impl FinalMessage for v3::registration::FinalMessage { + fn gateway_client_pub_key(&self) -> PeerPublicKey { + self.gateway_client.pub_key + } + + fn gateway_client_ipv4(&self) -> Option { + match self.gateway_client.private_ip { + std::net::IpAddr::V4(ipv4_addr) => Some(ipv4_addr), + std::net::IpAddr::V6(_) => None, + } + } + + fn gateway_client_ipv6(&self) -> Option { + None + } + + fn gateway_client_mac(&self) -> Vec { + self.gateway_client.mac.to_vec() + } + + fn credential(&self) -> Option { + self.credential.clone() + } +} + +impl FinalMessage for v4::registration::FinalMessage { + fn gateway_client_pub_key(&self) -> PeerPublicKey { + self.gateway_client.pub_key + } + + fn gateway_client_ipv4(&self) -> Option { + Some(self.gateway_client.private_ips.ipv4) + } + + fn gateway_client_ipv6(&self) -> Option { + Some(self.gateway_client.private_ips.ipv6) + } + + fn gateway_client_mac(&self) -> Vec { + self.gateway_client.mac.to_vec() + } + + fn credential(&self) -> Option { + self.credential.clone() + } +} + +impl FinalMessage for v5::registration::FinalMessage { + fn gateway_client_pub_key(&self) -> PeerPublicKey { + self.gateway_client.pub_key + } + + fn gateway_client_ipv4(&self) -> Option { + Some(self.gateway_client.private_ips.ipv4) + } + + fn gateway_client_ipv6(&self) -> Option { + Some(self.gateway_client.private_ips.ipv6) + } + + fn gateway_client_mac(&self) -> Vec { + self.gateway_client.mac.to_vec() + } + + fn credential(&self) -> Option { + self.credential.clone() + } +} + +// Temporary solution for lacking a query message wrapper in monorepo +#[derive(Debug)] +pub struct QueryMessageImpl { + pub pub_key: PeerPublicKey, + pub version: AuthenticatorVersion, +} + +impl Versionable for QueryMessageImpl { + fn version(&self) -> AuthenticatorVersion { + self.version + } +} + +pub trait QueryMessage: Versionable + fmt::Debug { + fn pub_key(&self) -> PeerPublicKey; +} + +impl QueryMessage for QueryMessageImpl { + fn pub_key(&self) -> PeerPublicKey { + self.pub_key + } +} + +pub trait TopUpMessage: Versionable + fmt::Debug { + fn pub_key(&self) -> PeerPublicKey; + fn credential(&self) -> CredentialSpendingData; +} + +impl TopUpMessage for v3::topup::TopUpMessage { + fn pub_key(&self) -> PeerPublicKey { + self.pub_key + } + + fn credential(&self) -> CredentialSpendingData { + self.credential.clone() + } +} + +impl TopUpMessage for v4::topup::TopUpMessage { + fn pub_key(&self) -> PeerPublicKey { + self.pub_key + } + + fn credential(&self) -> CredentialSpendingData { + self.credential.clone() + } +} + +impl TopUpMessage for v5::topup::TopUpMessage { + fn pub_key(&self) -> PeerPublicKey { + self.pub_key + } + + fn credential(&self) -> CredentialSpendingData { + self.credential.clone() + } +} + +#[derive(Debug)] +pub enum ClientMessage { + Initial(Box), + Final(Box), + Query(Box), + TopUp(Box), +} + +impl ClientMessage { + // check if message is wasteful e.g. contains a credential + pub fn is_wasteful(&self) -> bool { + match self { + Self::Final(msg) => msg.credential().is_some(), + Self::TopUp(_) => true, + Self::Initial(_) | Self::Query(_) => false, + } + } + + fn version(&self) -> AuthenticatorVersion { + match self { + ClientMessage::Initial(msg) => msg.version(), + ClientMessage::Final(msg) => msg.version(), + ClientMessage::Query(msg) => msg.version(), + ClientMessage::TopUp(msg) => msg.version(), + } + } + + pub fn bytes(&self, reply_to: Recipient) -> Result<(Vec, u64)> { + match self.version() { + AuthenticatorVersion::V2 => { + use v2::{ + registration::{ClientMac, FinalMessage, GatewayClient, InitMessage}, + request::AuthenticatorRequest, + }; + match self { + ClientMessage::Initial(init_message) => { + let (req, id) = AuthenticatorRequest::new_initial_request( + InitMessage { + pub_key: init_message.pub_key(), + }, + reply_to, + ); + Ok((req.to_bytes()?, id)) + } + ClientMessage::Final(final_message) => { + let (req, id) = AuthenticatorRequest::new_final_request( + FinalMessage { + gateway_client: GatewayClient { + pub_key: final_message.gateway_client_pub_key(), + private_ip: final_message + .gateway_client_ipv4() + .ok_or(Error::UnsupportedMessage)? + .into(), + mac: ClientMac::new(final_message.gateway_client_mac()), + }, + credential: final_message.credential(), + }, + reply_to, + ); + Ok((req.to_bytes()?, id)) + } + ClientMessage::Query(query_message) => { + let (req, id) = AuthenticatorRequest::new_query_request( + query_message.pub_key(), + reply_to, + ); + Ok((req.to_bytes()?, id)) + } + _ => Err(Error::UnsupportedMessage), + } + } + AuthenticatorVersion::V3 => { + use v3::{ + registration::{ClientMac, FinalMessage, GatewayClient, InitMessage}, + request::AuthenticatorRequest, + topup::TopUpMessage, + }; + match self { + ClientMessage::Initial(init_message) => { + let (req, id) = AuthenticatorRequest::new_initial_request( + InitMessage { + pub_key: init_message.pub_key(), + }, + reply_to, + ); + Ok((req.to_bytes()?, id)) + } + ClientMessage::Final(final_message) => { + let (req, id) = AuthenticatorRequest::new_final_request( + FinalMessage { + gateway_client: GatewayClient { + pub_key: final_message.gateway_client_pub_key(), + private_ip: final_message + .gateway_client_ipv4() + .ok_or(Error::UnsupportedMessage)? + .into(), + mac: ClientMac::new(final_message.gateway_client_mac()), + }, + credential: final_message.credential(), + }, + reply_to, + ); + Ok((req.to_bytes()?, id)) + } + ClientMessage::Query(query_message) => { + let (req, id) = AuthenticatorRequest::new_query_request( + query_message.pub_key(), + reply_to, + ); + Ok((req.to_bytes()?, id)) + } + ClientMessage::TopUp(top_up_message) => { + let (req, id) = AuthenticatorRequest::new_topup_request( + TopUpMessage { + pub_key: top_up_message.pub_key(), + credential: top_up_message.credential(), + }, + reply_to, + ); + Ok((req.to_bytes()?, id)) + } + } + } + AuthenticatorVersion::V4 => { + use v4::{ + registration::{ClientMac, FinalMessage, GatewayClient, InitMessage}, + request::AuthenticatorRequest, + topup::TopUpMessage, + }; + match self { + ClientMessage::Initial(init_message) => { + let (req, id) = AuthenticatorRequest::new_initial_request( + InitMessage { + pub_key: init_message.pub_key(), + }, + reply_to, + ); + Ok((req.to_bytes()?, id)) + } + ClientMessage::Final(final_message) => { + let (req, id) = AuthenticatorRequest::new_final_request( + FinalMessage { + gateway_client: GatewayClient { + pub_key: final_message.gateway_client_pub_key(), + private_ips: IpPair { + ipv4: final_message + .gateway_client_ipv4() + .ok_or(Error::UnsupportedMessage)?, + ipv6: final_message + .gateway_client_ipv6() + .ok_or(Error::UnsupportedMessage)?, + } + .into(), + mac: ClientMac::new(final_message.gateway_client_mac()), + }, + credential: final_message.credential(), + }, + reply_to, + ); + Ok((req.to_bytes()?, id)) + } + ClientMessage::Query(query_message) => { + let (req, id) = AuthenticatorRequest::new_query_request( + query_message.pub_key(), + reply_to, + ); + Ok((req.to_bytes()?, id)) + } + ClientMessage::TopUp(top_up_message) => { + let (req, id) = AuthenticatorRequest::new_topup_request( + TopUpMessage { + pub_key: top_up_message.pub_key(), + credential: top_up_message.credential(), + }, + reply_to, + ); + Ok((req.to_bytes()?, id)) + } + } + } + AuthenticatorVersion::V5 => { + use v5::{ + registration::{ClientMac, FinalMessage, GatewayClient, InitMessage}, + request::AuthenticatorRequest, + topup::TopUpMessage, + }; + match self { + ClientMessage::Initial(init_message) => { + let (req, id) = AuthenticatorRequest::new_initial_request(InitMessage { + pub_key: init_message.pub_key(), + }); + Ok((req.to_bytes()?, id)) + } + ClientMessage::Final(final_message) => { + let (req, id) = AuthenticatorRequest::new_final_request(FinalMessage { + gateway_client: GatewayClient { + pub_key: final_message.gateway_client_pub_key(), + private_ips: IpPair { + ipv4: final_message + .gateway_client_ipv4() + .ok_or(Error::UnsupportedMessage)?, + ipv6: final_message + .gateway_client_ipv6() + .ok_or(Error::UnsupportedMessage)?, + }, + mac: ClientMac::new(final_message.gateway_client_mac()), + }, + credential: final_message.credential(), + }); + Ok((req.to_bytes()?, id)) + } + ClientMessage::Query(query_message) => { + let (req, id) = + AuthenticatorRequest::new_query_request(query_message.pub_key()); + Ok((req.to_bytes()?, id)) + } + ClientMessage::TopUp(top_up_message) => { + let (req, id) = AuthenticatorRequest::new_topup_request(TopUpMessage { + pub_key: top_up_message.pub_key(), + credential: top_up_message.credential(), + }); + Ok((req.to_bytes()?, id)) + } + } + } + AuthenticatorVersion::UNKNOWN => Err(Error::UnknownVersion), + } + } + + pub fn use_surbs(&self) -> bool { + match self.version() { + AuthenticatorVersion::V2 | AuthenticatorVersion::V3 | AuthenticatorVersion::V4 => false, + AuthenticatorVersion::V5 => true, + AuthenticatorVersion::UNKNOWN => true, + } + } +} + +pub trait Id { + fn id(&self) -> u64; +} + +impl Id for v2::response::PendingRegistrationResponse { + fn id(&self) -> u64 { + self.request_id + } +} + +impl Id for v3::response::PendingRegistrationResponse { + fn id(&self) -> u64 { + self.request_id + } +} + +impl Id for v4::response::PendingRegistrationResponse { + fn id(&self) -> u64 { + self.request_id + } +} + +impl Id for v5::response::PendingRegistrationResponse { + fn id(&self) -> u64 { + self.request_id + } +} + +impl Id for v2::response::RegisteredResponse { + fn id(&self) -> u64 { + self.request_id + } +} + +impl Id for v3::response::RegisteredResponse { + fn id(&self) -> u64 { + self.request_id + } +} + +impl Id for v4::response::RegisteredResponse { + fn id(&self) -> u64 { + self.request_id + } +} + +impl Id for v5::response::RegisteredResponse { + fn id(&self) -> u64 { + self.request_id + } +} + +impl Id for v2::response::RemainingBandwidthResponse { + fn id(&self) -> u64 { + self.request_id + } +} + +impl Id for v3::response::RemainingBandwidthResponse { + fn id(&self) -> u64 { + self.request_id + } +} + +impl Id for v4::response::RemainingBandwidthResponse { + fn id(&self) -> u64 { + self.request_id + } +} + +impl Id for v5::response::RemainingBandwidthResponse { + fn id(&self) -> u64 { + self.request_id + } +} + +impl Id for v3::response::TopUpBandwidthResponse { + fn id(&self) -> u64 { + self.request_id + } +} + +impl Id for v4::response::TopUpBandwidthResponse { + fn id(&self) -> u64 { + self.request_id + } +} + +impl Id for v5::response::TopUpBandwidthResponse { + fn id(&self) -> u64 { + self.request_id + } +} + +pub trait PendingRegistrationResponse: Id + fmt::Debug { + fn nonce(&self) -> u64; + fn verify( + &self, + gateway_key: &PrivateKey, + ) -> std::result::Result<(), nym_authenticator_requests::Error>; + fn pub_key(&self) -> PeerPublicKey; + fn private_ips(&self) -> IpPair; +} + +impl PendingRegistrationResponse for v2::response::PendingRegistrationResponse { + fn nonce(&self) -> u64 { + self.reply.nonce + } + + fn verify( + &self, + gateway_key: &PrivateKey, + ) -> std::result::Result<(), nym_authenticator_requests::Error> { + self.reply.gateway_data.verify(gateway_key, self.nonce()) + } + + fn pub_key(&self) -> PeerPublicKey { + self.reply.gateway_data.pub_key + } + + fn private_ips(&self) -> IpPair { + self.reply.gateway_data.private_ip.into() + } +} + +impl PendingRegistrationResponse for v3::response::PendingRegistrationResponse { + fn nonce(&self) -> u64 { + self.reply.nonce + } + + fn verify( + &self, + gateway_key: &PrivateKey, + ) -> std::result::Result<(), nym_authenticator_requests::Error> { + self.reply.gateway_data.verify(gateway_key, self.nonce()) + } + + fn pub_key(&self) -> PeerPublicKey { + self.reply.gateway_data.pub_key + } + + fn private_ips(&self) -> IpPair { + self.reply.gateway_data.private_ip.into() + } +} + +impl PendingRegistrationResponse for v4::response::PendingRegistrationResponse { + fn nonce(&self) -> u64 { + self.reply.nonce + } + + fn verify( + &self, + gateway_key: &PrivateKey, + ) -> std::result::Result<(), nym_authenticator_requests::Error> { + self.reply.gateway_data.verify(gateway_key, self.nonce()) + } + + fn pub_key(&self) -> PeerPublicKey { + self.reply.gateway_data.pub_key + } + + fn private_ips(&self) -> IpPair { + self.reply.gateway_data.private_ips.into() + } +} + +impl PendingRegistrationResponse for v5::response::PendingRegistrationResponse { + fn nonce(&self) -> u64 { + self.reply.nonce + } + + fn verify( + &self, + gateway_key: &PrivateKey, + ) -> std::result::Result<(), nym_authenticator_requests::Error> { + self.reply.gateway_data.verify(gateway_key, self.nonce()) + } + + fn pub_key(&self) -> PeerPublicKey { + self.reply.gateway_data.pub_key + } + + fn private_ips(&self) -> IpPair { + self.reply.gateway_data.private_ips + } +} + +pub trait RegisteredResponse: Id + fmt::Debug { + fn private_ips(&self) -> IpPair; + fn pub_key(&self) -> PeerPublicKey; + fn wg_port(&self) -> u16; +} + +impl RegisteredResponse for v2::response::RegisteredResponse { + fn private_ips(&self) -> IpPair { + self.reply.private_ip.into() + } + + fn pub_key(&self) -> PeerPublicKey { + self.reply.pub_key + } + + fn wg_port(&self) -> u16 { + self.reply.wg_port + } +} + +impl RegisteredResponse for v3::response::RegisteredResponse { + fn private_ips(&self) -> IpPair { + self.reply.private_ip.into() + } + + fn pub_key(&self) -> PeerPublicKey { + self.reply.pub_key + } + + fn wg_port(&self) -> u16 { + self.reply.wg_port + } +} +impl RegisteredResponse for v4::response::RegisteredResponse { + fn private_ips(&self) -> IpPair { + self.reply.private_ips.into() + } + + fn pub_key(&self) -> PeerPublicKey { + self.reply.pub_key + } + + fn wg_port(&self) -> u16 { + self.reply.wg_port + } +} + +impl RegisteredResponse for v5::response::RegisteredResponse { + fn private_ips(&self) -> IpPair { + self.reply.private_ips + } + + fn pub_key(&self) -> PeerPublicKey { + self.reply.pub_key + } + + fn wg_port(&self) -> u16 { + self.reply.wg_port + } +} + +pub trait RemainingBandwidthResponse: Id + fmt::Debug { + fn available_bandwidth(&self) -> Option; +} + +impl RemainingBandwidthResponse for v2::response::RemainingBandwidthResponse { + fn available_bandwidth(&self) -> Option { + self.reply.as_ref().map(|r| r.available_bandwidth) + } +} + +impl RemainingBandwidthResponse for v3::response::RemainingBandwidthResponse { + fn available_bandwidth(&self) -> Option { + self.reply.as_ref().map(|r| r.available_bandwidth) + } +} + +impl RemainingBandwidthResponse for v4::response::RemainingBandwidthResponse { + fn available_bandwidth(&self) -> Option { + self.reply.as_ref().map(|r| r.available_bandwidth) + } +} + +impl RemainingBandwidthResponse for v5::response::RemainingBandwidthResponse { + fn available_bandwidth(&self) -> Option { + self.reply.as_ref().map(|r| r.available_bandwidth) + } +} + +pub trait TopUpBandwidthResponse: Id + fmt::Debug { + fn available_bandwidth(&self) -> i64; +} + +impl TopUpBandwidthResponse for v3::response::TopUpBandwidthResponse { + fn available_bandwidth(&self) -> i64 { + self.reply.available_bandwidth + } +} + +impl TopUpBandwidthResponse for v4::response::TopUpBandwidthResponse { + fn available_bandwidth(&self) -> i64 { + self.reply.available_bandwidth + } +} + +impl TopUpBandwidthResponse for v5::response::TopUpBandwidthResponse { + fn available_bandwidth(&self) -> i64 { + self.reply.available_bandwidth + } +} + +#[derive(Debug)] +pub enum AuthenticatorResponse { + PendingRegistration(Box), + Registered(Box), + RemainingBandwidth(Box), + TopUpBandwidth(Box), +} + +impl Id for AuthenticatorResponse { + fn id(&self) -> u64 { + match self { + AuthenticatorResponse::PendingRegistration(pending_registration_response) => { + pending_registration_response.id() + } + AuthenticatorResponse::Registered(registered_response) => registered_response.id(), + AuthenticatorResponse::RemainingBandwidth(remaining_bandwidth_response) => { + remaining_bandwidth_response.id() + } + AuthenticatorResponse::TopUpBandwidth(top_up_bandwidth_response) => { + top_up_bandwidth_response.id() + } + } + } +} + +impl From for AuthenticatorResponse { + fn from(value: v2::response::AuthenticatorResponse) -> Self { + match value.data { + v2::response::AuthenticatorResponseData::PendingRegistration( + pending_registration_response, + ) => Self::PendingRegistration(Box::new(pending_registration_response)), + v2::response::AuthenticatorResponseData::Registered(registered_response) => { + Self::Registered(Box::new(registered_response)) + } + v2::response::AuthenticatorResponseData::RemainingBandwidth( + remaining_bandwidth_response, + ) => Self::RemainingBandwidth(Box::new(remaining_bandwidth_response)), + } + } +} + +impl From for AuthenticatorResponse { + fn from(value: v3::response::AuthenticatorResponse) -> Self { + match value.data { + v3::response::AuthenticatorResponseData::PendingRegistration( + pending_registration_response, + ) => Self::PendingRegistration(Box::new(pending_registration_response)), + v3::response::AuthenticatorResponseData::Registered(registered_response) => { + Self::Registered(Box::new(registered_response)) + } + v3::response::AuthenticatorResponseData::RemainingBandwidth( + remaining_bandwidth_response, + ) => Self::RemainingBandwidth(Box::new(remaining_bandwidth_response)), + v3::response::AuthenticatorResponseData::TopUpBandwidth(top_up_bandwidth_response) => { + Self::TopUpBandwidth(Box::new(top_up_bandwidth_response)) + } + } + } +} + +impl From for AuthenticatorResponse { + fn from(value: v4::response::AuthenticatorResponse) -> Self { + match value.data { + v4::response::AuthenticatorResponseData::PendingRegistration( + pending_registration_response, + ) => Self::PendingRegistration(Box::new(pending_registration_response)), + v4::response::AuthenticatorResponseData::Registered(registered_response) => { + Self::Registered(Box::new(registered_response)) + } + v4::response::AuthenticatorResponseData::RemainingBandwidth( + remaining_bandwidth_response, + ) => Self::RemainingBandwidth(Box::new(remaining_bandwidth_response)), + v4::response::AuthenticatorResponseData::TopUpBandwidth(top_up_bandwidth_response) => { + Self::TopUpBandwidth(Box::new(top_up_bandwidth_response)) + } + } + } +} + +impl From for AuthenticatorResponse { + fn from(value: v5::response::AuthenticatorResponse) -> Self { + match value.data { + v5::response::AuthenticatorResponseData::PendingRegistration( + pending_registration_response, + ) => Self::PendingRegistration(Box::new(pending_registration_response)), + v5::response::AuthenticatorResponseData::Registered(registered_response) => { + Self::Registered(Box::new(registered_response)) + } + v5::response::AuthenticatorResponseData::RemainingBandwidth( + remaining_bandwidth_response, + ) => Self::RemainingBandwidth(Box::new(remaining_bandwidth_response)), + v5::response::AuthenticatorResponseData::TopUpBandwidth(top_up_bandwidth_response) => { + Self::TopUpBandwidth(Box::new(top_up_bandwidth_response)) + } + } + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum AuthenticatorVersion { + V2, + V3, + V4, + V5, + UNKNOWN, +} + +impl AuthenticatorVersion { + pub const LATEST: AuthenticatorVersion = AuthenticatorVersion::V5; +} + +impl fmt::Display for AuthenticatorVersion { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::V2 => write!(f, "v2"), + Self::V3 => write!(f, "v3"), + Self::V4 => write!(f, "v4"), + Self::V5 => write!(f, "v5"), + Self::UNKNOWN => write!(f, "unknown"), + } + } +} + +impl From for AuthenticatorVersion { + fn from(value: u8) -> Self { + if value == 2 { + Self::V2 + } else if value == 3 { + Self::V3 + } else if value == 4 { + Self::V4 + } else if value == 5 { + Self::V5 + } else { + Self::UNKNOWN + } + } +} + +impl From<&str> for AuthenticatorVersion { + fn from(value: &str) -> Self { + let Ok(semver) = semver::Version::parse(value) else { + return Self::UNKNOWN; + }; + + semver.into() + } +} + +impl From> for AuthenticatorVersion { + fn from(value: Option<&String>) -> Self { + match value { + None => Self::UNKNOWN, + Some(value) => value.as_str().into(), + } + } +} + +impl From for AuthenticatorVersion { + fn from(value: String) -> Self { + Self::from(value.as_str()) + } +} + +impl From> for AuthenticatorVersion { + fn from(value: Option) -> Self { + value.as_ref().into() + } +} + +impl From for AuthenticatorVersion { + fn from(semver: semver::Version) -> Self { + if semver.major < 1 { + return Self::UNKNOWN; + } + if semver.minor < 1 { + return Self::UNKNOWN; + } + if semver.minor == 1 && semver.patch == 9 { + return Self::V2; + } + if semver.minor == 1 && semver.patch >= 10 { + return Self::V3; + } + if semver.minor < 6 { + return Self::V4; + } + if semver.minor == 6 && semver.patch == 0 { + return Self::V4; + } + if semver.minor == 6 && semver.patch >= 1 { + return Self::V5; + } + Self::LATEST + } +} + +#[derive(Clone)] +pub struct AuthenticatorClient { + auth_mix_client: AuthenticatorMixnetClient, + auth_recipient: Recipient, + auth_version: AuthenticatorVersion, +} + +impl AuthenticatorClient { + pub fn new( + auth_mix_client: AuthenticatorMixnetClient, + auth_recipient: Recipient, + auth_version: AuthenticatorVersion, + ) -> Self { + Self { + auth_mix_client, + auth_recipient, + auth_version, + } + } + + pub fn auth_recipient(&self) -> Recipient { + self.auth_recipient + } + + pub fn auth_version(&self) -> AuthenticatorVersion { + self.auth_version + } + + pub async fn send(&mut self, message: &ClientMessage) -> Result { + self.auth_mix_client + .send(message, self.auth_recipient) + .await + } +} + +pub struct AuthenticatorMixnetClient { + mixnet_listener: MixnetMessageBroadcastReceiver, + mixnet_sender: MixnetClientSender, + our_nym_address: Recipient, +} + +impl Clone for AuthenticatorMixnetClient { + fn clone(&self) -> Self { + Self { + mixnet_listener: self.mixnet_listener.resubscribe(), + mixnet_sender: self.mixnet_sender.clone(), + our_nym_address: self.our_nym_address, + } + } +} + +impl AuthenticatorMixnetClient { + pub async fn new( + mixnet_sender: MixnetClientSender, + mixnet_listener: MixnetMessageBroadcastReceiver, + our_nym_address: Recipient, + ) -> Self { + Self { + mixnet_listener, + mixnet_sender, + our_nym_address, + } + } + + pub async fn send( + &mut self, + message: &ClientMessage, + authenticator_address: Recipient, + ) -> Result { + self.send_inner(message, authenticator_address).await + } + + async fn send_inner( + &mut self, + message: &ClientMessage, + authenticator_address: Recipient, + ) -> Result { + let request_id = self + .send_connect_request(message, authenticator_address) + .await?; + + debug!("Waiting for reply..."); + self.listen_for_connect_response(request_id).await + } + + async fn send_connect_request( + &self, + message: &ClientMessage, + authenticator_address: Recipient, + ) -> Result { + let (data, request_id) = message.bytes(self.our_nym_address)?; + + // We use 20 surbs for the connect request because typically the + // authenticator mixnet client on the nym-node is configured to have a min + // threshold of 10 surbs that it reserves for itself to request additional + // surbs. + let surbs = if message.use_surbs() { + match &message { + ClientMessage::Initial(_) => IncludedSurbs::new(20), + _ => IncludedSurbs::new(1), + } + } else { + IncludedSurbs::ExposeSelfAddress + }; + let input_message = create_input_message(authenticator_address, data, surbs); + + self.mixnet_sender + .send(input_message) + .await + .map_err(|e| Error::SendMixnetMessage(Box::new(e)))?; + + Ok(request_id) + } + + async fn listen_for_connect_response( + &mut self, + request_id: u64, + ) -> Result { + let timeout = tokio::time::sleep(Duration::from_secs(10)); + tokio::pin!(timeout); + + loop { + tokio::select! { + _ = &mut timeout => { + error!("Timed out waiting for reply to connect request"); + return Err(Error::TimeoutWaitingForConnectResponse); + } + msg = self.mixnet_listener.recv() => match msg { + Err(_) => { + return Err(Error::NoMixnetMessagesReceived); + } + Ok(msg) => { + if !check_if_authenticator_message(&msg) { + debug!("Received non-authenticator message while waiting for connect response"); + continue; + } + // Confirm that the version is correct + let version = check_auth_message_version(&msg)?; + + // Then we deserialize the message + debug!("AuthClient: got message while waiting for connect response with version {version:?}"); + let ret: Result = match version { + AuthenticatorVersion::V2 => v2::response::AuthenticatorResponse::from_reconstructed_message(&msg).map(Into::into).map_err(Into::into), + AuthenticatorVersion::V3 => v3::response::AuthenticatorResponse::from_reconstructed_message(&msg).map(Into::into).map_err(Into::into), + AuthenticatorVersion::V4 => v4::response::AuthenticatorResponse::from_reconstructed_message(&msg).map(Into::into).map_err(Into::into), + AuthenticatorVersion::V5 => v5::response::AuthenticatorResponse::from_reconstructed_message(&msg).map(Into::into).map_err(Into::into), + AuthenticatorVersion::UNKNOWN => Err(Error::UnknownVersion), + }; + let Ok(response) = ret else { + // This is ok, it's likely just one of our self-pings + debug!("Failed to deserialize reconstructed message"); + continue; + }; + + if response.id() == request_id { + debug!("Got response with matching id"); + return Ok(response); + } + } + } + } + } + } +} + +fn check_if_authenticator_message(message: &ReconstructedMessage) -> bool { + if let Some(msg_type) = message.message.get(1) { + ServiceProviderType::Authenticator as u8 == *msg_type + } else { + false + } +} + +fn check_auth_message_version(message: &ReconstructedMessage) -> Result { + // Assuing it's an Authenticator message, it will have a version as its first byte + if let Some(&version) = message.message.first() { + Ok(version.into()) + } else { + Err(Error::NoVersionInMessage) + } +} + +fn create_input_message( + recipient: Recipient, + data: Vec, + surbs: IncludedSurbs, +) -> nym_sdk::mixnet::InputMessage { + match surbs { + IncludedSurbs::Amount(surbs) => nym_sdk::mixnet::InputMessage::new_anonymous( + recipient, + data, + surbs, + TransmissionLane::General, + None, + ), + IncludedSurbs::ExposeSelfAddress => nym_sdk::mixnet::InputMessage::new_regular( + recipient, + data, + TransmissionLane::General, + None, + ), + } +} diff --git a/nym-authenticator-client/src/mixnet_listener.rs b/nym-authenticator-client/src/mixnet_listener.rs new file mode 100644 index 0000000000..113148b8ce --- /dev/null +++ b/nym-authenticator-client/src/mixnet_listener.rs @@ -0,0 +1,113 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +// To remove with the Registration Client PR +#![allow(clippy::unwrap_used)] + +use std::sync::Arc; + +use futures::StreamExt; +use nym_sdk::mixnet::{MixnetClient, ReconstructedMessage}; +use tokio::{sync::broadcast, task::JoinHandle}; +use tokio_util::sync::CancellationToken; + +use crate::AuthenticatorMixnetClient; + +pub type SharedMixnetClient = Arc>>; +pub type MixnetMessageBroadcastSender = broadcast::Sender>; +pub type MixnetMessageBroadcastReceiver = broadcast::Receiver>; + +// The AuthClientsMixnetListener listens to mixnet messages and rebroadcasts them to the +// AuthClients, or whoever else is interested. +// While it is running, it has a lock on the shared mixnet client. This is the reason it's +// designed to be able to start and stop, so that the lock can be released when it's not needed. +// +// NOTE: this is potentially bit wasteful. Ideally we should have proper channels where the +// recipient only gets messages they're interested in. +pub struct AuthClientMixnetListener { + // The shared mixnet client that we're listening to + mixnet_client: SharedMixnetClient, + + // Broadcast channel for the messages that we re-broadcast to the AuthClients + message_broadcast: MixnetMessageBroadcastSender, + + // Listen to cancel from the outside world + shutdown_token: CancellationToken, +} + +impl AuthClientMixnetListener { + pub fn new(mixnet_client: SharedMixnetClient, shutdown_token: CancellationToken) -> Self { + let (message_broadcast, _) = broadcast::channel(100); + Self { + mixnet_client, + message_broadcast, + shutdown_token, + } + } + + pub fn subscribe(&self) -> MixnetMessageBroadcastReceiver { + self.message_broadcast.subscribe() + } + + async fn run(self) { + let mut mixnet_client = self.mixnet_client.lock().await.take().unwrap(); + self.shutdown_token + .run_until_cancelled(async { + while let Some(event) = mixnet_client.next().await { + if let Err(err) = self.message_broadcast.send(Arc::new(event)) { + tracing::error!("Failed to broadcast mixnet message: {err}"); + } + } + tracing::error!("Mixnet client stream ended unexpectedly"); + }) + .await; + self.mixnet_client.lock().await.replace(mixnet_client); + } + + pub fn start(self) -> AuthClientMixnetListenerHandle { + let mixnet_client = self.mixnet_client.clone(); + let message_broadcast = self.message_broadcast.clone(); + let handle = tokio::spawn(self.run()); + + AuthClientMixnetListenerHandle { + mixnet_client, + message_broadcast, + handle, + } + } +} + +pub struct AuthClientMixnetListenerHandle { + mixnet_client: SharedMixnetClient, + message_broadcast: MixnetMessageBroadcastSender, + handle: JoinHandle<()>, +} + +impl AuthClientMixnetListenerHandle { + /// Returns new `AuthClient` or `None` if `MixnetClient` is already moved from shared reference. + pub async fn new_auth_client(&self) -> Option { + let mixnet_client_guard = self.mixnet_client.lock().await; + let mixnet_client_ref = mixnet_client_guard.as_ref()?; + let mixnet_sender = mixnet_client_ref.split_sender(); + let nym_address = *mixnet_client_ref.nym_address(); + + Some( + AuthenticatorMixnetClient::new( + mixnet_sender, + self.message_broadcast.subscribe(), + nym_address, + ) + .await, + ) + } + + pub fn subscribe(&self) -> MixnetMessageBroadcastReceiver { + self.message_broadcast.subscribe() + } + + pub async fn wait(self) { + if let Err(err) = self.handle.await { + tracing::error!("Error waiting for auth clients mixnet listener to stop: {err}"); + } + } +} diff --git a/nym-credential-proxy/nym-credential-proxy-requests/Cargo.toml b/nym-credential-proxy/nym-credential-proxy-requests/Cargo.toml index 4dd51fc5c8..9aadebbe50 100644 --- a/nym-credential-proxy/nym-credential-proxy-requests/Cargo.toml +++ b/nym-credential-proxy/nym-credential-proxy-requests/Cargo.toml @@ -37,5 +37,5 @@ features = ["tokio"] [features] default = ["query-types"] query-types = ["nym-http-api-common", "nym-http-api-common/output"] -openapi = ["utoipa"] +openapi = ["utoipa", "nym-http-api-common/utoipa"] tsify = ["dep:tsify", "wasm-bindgen"] 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 22f04a7483..061b40a37d 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,12 +268,20 @@ pub struct WebhookTicketbookWalletSharesRequest { #[cfg_attr(feature = "openapi", derive(utoipa::ToSchema, utoipa::IntoParams))] #[cfg(feature = "query-types")] #[serde(default, rename_all = "kebab-case")] -pub struct TicketbookObtainQueryParams { - pub output: Option, - +pub struct TicketbookObtainParams { #[serde(default)] pub skip_webhook: bool, + #[serde(default)] + #[serde(flatten)] + pub global: GlobalDataParams, +} + +#[derive(Default, Debug, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema, utoipa::IntoParams))] +#[cfg(feature = "query-types")] +#[serde(default, rename_all = "kebab-case")] +pub struct GlobalDataParams { pub include_master_verification_key: bool, pub include_coin_index_signatures: bool, @@ -281,6 +289,18 @@ pub struct TicketbookObtainQueryParams { pub include_expiration_date_signatures: bool, } +#[derive(Default, Debug, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema, utoipa::IntoParams))] +#[cfg(feature = "query-types")] +#[serde(default, rename_all = "kebab-case")] +pub struct TicketbookObtainQueryParams { + pub output: Option, + + #[serde(default)] + #[serde(flatten)] + pub obtain_params: TicketbookObtainParams, +} + #[derive(Default, Debug, Serialize, Deserialize, Clone)] #[cfg_attr(feature = "openapi", derive(utoipa::ToSchema, utoipa::IntoParams))] #[cfg(feature = "query-types")] @@ -288,9 +308,7 @@ pub struct TicketbookObtainQueryParams { pub struct SharesQueryParams { pub output: Option, - pub include_master_verification_key: bool, - - pub include_coin_index_signatures: bool, - - pub include_expiration_date_signatures: bool, + #[serde(default)] + #[serde(flatten)] + pub global: GlobalDataParams, } diff --git a/nym-credential-proxy/nym-credential-proxy-requests/src/client.rs b/nym-credential-proxy/nym-credential-proxy-requests/src/client.rs index d9247f15b8..e9b16514f9 100644 --- a/nym-credential-proxy/nym-credential-proxy-requests/src/client.rs +++ b/nym-credential-proxy/nym-credential-proxy-requests/src/client.rs @@ -22,11 +22,15 @@ pub struct VpnApiClient { bearer_token: String, } +#[allow(clippy::result_large_err)] pub fn new_client( base_url: impl IntoUrl, bearer_token: impl Into, ) -> Result { - let url = base_url.into_url()?; + let raw = base_url.as_str().to_string(); + let url = base_url + .into_url() + .map_err(|source| VpnApiClientError::MalformedUrl { raw, source })?; Ok(VpnApiClient { inner: Client::builder(url)? .with_user_agent(format!( @@ -97,9 +101,14 @@ impl NymVpnApiClient for VpnApiClient { { let req = self .inner - .create_get_request(path, NO_PARAMS) + .create_get_request(path, NO_PARAMS)? .bearer_auth(&self.bearer_token) - .send(); + .build() + .map_err(VpnApiClientError::reqwest_client_build_error)?; + + let url = req.url().clone(); + + let req = reqwest::Client::new().execute(req); // the only reason for that target lock is so that I could call this method from an ephemeral test // running in non-wasm mode (since I wanted to use tokio) @@ -110,8 +119,9 @@ impl NymVpnApiClient for VpnApiClient { .map_err(|_timeout| HttpClientError::RequestTimeout)??; #[cfg(not(target_arch = "wasm32"))] - let res = req.await?; - + let res = req + .await + .map_err(|source| VpnApiClientError::request_send_error(url, source))?; parse_response(res, false).await } @@ -129,9 +139,14 @@ impl NymVpnApiClient for VpnApiClient { { let req = self .inner - .create_post_request(path, params, json_body) + .create_post_request(path, params, json_body)? .bearer_auth(&self.bearer_token) - .send(); + .build() + .map_err(VpnApiClientError::reqwest_client_build_error)?; + + let url = req.url().clone(); + + let req = reqwest::Client::new().execute(req); // the only reason for that target lock is so that I could call this method from an ephemeral test // running in non-wasm mode (since I wanted to use tokio) @@ -142,7 +157,9 @@ impl NymVpnApiClient for VpnApiClient { .map_err(|_timeout| HttpClientError::RequestTimeout)??; #[cfg(not(target_arch = "wasm32"))] - let res = req.await?; + let res = req + .await + .map_err(|source| VpnApiClientError::request_send_error(url, source))?; parse_response(res, false).await } diff --git a/nym-credential-proxy/nym-credential-proxy/Cargo.toml b/nym-credential-proxy/nym-credential-proxy/Cargo.toml index 25fafef798..d5b8252b96 100644 --- a/nym-credential-proxy/nym-credential-proxy/Cargo.toml +++ b/nym-credential-proxy/nym-credential-proxy/Cargo.toml @@ -7,6 +7,7 @@ homepage.workspace = true documentation.workspace = true edition.workspace = true license.workspace = true +rust-version.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -23,7 +24,13 @@ rand.workspace = true reqwest = { workspace = true, features = ["rustls-tls"] } serde = { workspace = true, features = ["derive"] } serde_json.workspace = true -sqlx = { workspace = true, features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate", "time"] } +sqlx = { workspace = true, features = [ + "runtime-tokio-rustls", + "sqlite", + "macros", + "migrate", + "time", +] } strum = { workspace = true, features = ["derive"] } strum_macros.workspace = true time.workspace = true @@ -38,26 +45,49 @@ utoipa = { workspace = true, features = ["axum_extras", "time"] } utoipa-swagger-ui = { workspace = true, features = ["axum"] } zeroize.workspace = true -nym-bin-common = { path = "../../common/bin-common", features = ["basic_tracing"] } +nym-bin-common = { path = "../../common/bin-common", features = [ + "basic_tracing", +] } nym-compact-ecash = { path = "../../common/nym_offline_compact_ecash" } nym-config = { path = "../../common/config" } -nym-crypto = { path = "../../common/crypto", features = ["asymmetric", "rand", "serde"] } +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", "middleware"] } +nym-http-api-common = { path = "../../common/http-api-common", features = [ + "utoipa", + "middleware", +] } nym-validator-client = { path = "../../common/client-libs/validator-client" } nym-network-defaults = { path = "../../common/network-defaults" } -nym-credential-proxy-requests = { path = "../nym-credential-proxy-requests", features = ["openapi"] } +nym-credential-proxy-requests = { path = "../nym-credential-proxy-requests", features = [ + "openapi", +] } +nym-ecash-signer-check = { path = "../../common/ecash-signer-check" } + +nym-credential-proxy-lib = { path = "../../common/credential-proxy" } [dev-dependencies] tempfile = { workspace = true } [build-dependencies] +anyhow = { workspace = true } tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } -sqlx = { workspace = true, features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"] } +sqlx = { workspace = true, features = [ + "runtime-tokio-rustls", + "sqlite", + "macros", + "migrate", +] } [features] default = ["cors"] cors = ["tower-http"] + +[lints] +workspace = true diff --git a/nym-credential-proxy/nym-credential-proxy/src/cli.rs b/nym-credential-proxy/nym-credential-proxy/src/cli.rs index cf5b25cf23..f500bd064f 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/cli.rs +++ b/nym-credential-proxy/nym-credential-proxy/src/cli.rs @@ -2,15 +2,18 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::config::default_database_filepath; -use crate::webhook::ZkNymWebHookConfig; use clap::builder::ArgPredicate; -use clap::Parser; +use clap::{Args, Parser}; use nym_bin_common::bin_info; +use nym_credential_proxy_lib::error::CredentialProxyError; +use nym_credential_proxy_lib::webhook::ZkNymWebhook; use std::fs::create_dir_all; use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use std::path::PathBuf; use std::sync::OnceLock; +use std::time::Duration; use tracing::info; +use url::Url; fn pretty_build_info_static() -> &'static str { static PRETTY_BUILD_INFORMATION: OnceLock = OnceLock::new(); @@ -64,10 +67,53 @@ pub struct Cli { )] pub(crate) max_concurrent_deposits: usize, + /// Specify the size of the deposits buffer the credential proxy should have available at any time + /// (default: 256) + #[clap( + long, + env = "NYM_CREDENTIAL_PROXY_DEPOSITS_BUFFER", + default_value_t = 256 + )] + pub(crate) deposits_buffer_size: usize, + + #[clap( + long, + env = "NYM_CREDENTIAL_PROXY_QUORUM_CHECK_INTERVAL", + default_value = "5m", + value_parser = humantime::parse_duration + )] + pub(crate) quorum_check_interval: Duration, + #[clap(long, env = "NYM_CREDENTIAL_PROXY_PERSISTENT_STORAGE_STORAGE")] pub(crate) persistent_storage_path: Option, } +#[derive(Args, Debug, Clone)] +pub struct ZkNymWebHookConfig { + #[clap(long, env = "WEBHOOK_ZK_NYMS_URL")] + pub webhook_url: Url, + + #[clap(long, env = "WEBHOOK_ZK_NYMS_CLIENT_ID")] + pub webhook_client_id: String, + + #[clap(long, env = "WEBHOOK_ZK_NYMS_CLIENT_SECRET")] + pub webhook_client_secret: String, +} + +impl TryFrom for ZkNymWebhook { + type Error = CredentialProxyError; + + fn try_from(cfg: ZkNymWebHookConfig) -> Result { + Ok(ZkNymWebhook { + webhook_client_url: cfg + .webhook_url + .join(&cfg.webhook_client_id) + .map_err(|_| CredentialProxyError::InvalidWebhookUrl)?, + webhook_client_secret: cfg.webhook_client_secret, + }) + } +} + impl Cli { pub fn bind_address(&self) -> SocketAddr { // SAFETY: @@ -90,10 +136,7 @@ impl Cli { create_dir_all(parent).unwrap(); } - info!( - "setting the storage path path to {}", - default_path.display() - ); + info!("setting the storage path to {}", default_path.display()); default_path }) diff --git a/nym-credential-proxy/nym-credential-proxy/src/credentials/mod.rs b/nym-credential-proxy/nym-credential-proxy/src/credentials/mod.rs deleted file mode 100644 index a6ee988de5..0000000000 --- a/nym-credential-proxy/nym-credential-proxy/src/credentials/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -// Copyright 2024 Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only - -pub mod ticketbook; 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 33ad66391e..e69de29bb2 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,392 +0,0 @@ -// Copyright 2024 Nym Technologies SA -// 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; -use futures::{stream, StreamExt}; -use nym_credential_proxy_requests::api::v1::ticketbook::models::{ - TicketbookAsyncRequest, TicketbookObtainQueryParams, TicketbookRequest, - TicketbookWalletSharesResponse, WalletShare, WebhookTicketbookWalletShares, - WebhookTicketbookWalletSharesRequest, -}; -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::Coin; -use rand::rngs::OsRng; -use std::collections::HashMap; -use std::sync::Arc; -use std::time::Duration; -use time::OffsetDateTime; -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 { - 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( - expiration_date = %request_data.expiration_date, - ticketbook_type = %request_data.ticketbook_type - ) -)] -pub(crate) async fn try_obtain_wallet_shares( - state: &ApiState, - request: Uuid, - requested_on: OffsetDateTime, - request_data: TicketbookRequest, -) -> Result, VpnApiError> { - let mut rng = OsRng; - - let ed25519_keypair = ed25519::KeyPair::new(&mut rng); - - let epoch = state.current_epoch_id().await?; - let deposit_amount = state.deposit_amount().await?; - let threshold = state.ecash_threshold(epoch).await?; - let expiration_date = request_data.expiration_date; - - // before we commit to making the deposit, ensure we have required signatures cached and stored - let _ = state.master_verification_key(Some(epoch)).await?; - let _ = state.master_coin_index_signatures(Some(epoch)).await?; - let _ = state - .master_expiration_date_signatures(epoch, expiration_date) - .await?; - let ecash_api_clients = state.ecash_clients(epoch).await?.clone(); - - let DepositResponse { - deposit_id, - tx_hash, - } = make_deposit(state, *ed25519_keypair.public_key(), &deposit_amount).await?; - - 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 - .storage() - .insert_deposit_data( - deposit_id, - tx_hash, - requested_on, - request, - deposit_amount, - &request_data.ecash_pubkey, - &ed25519_keypair, - ) - .await?; - - let plaintext = - IssuanceTicketBook::request_plaintext(&request_data.withdrawal_request, deposit_id); - let signature = ed25519_keypair.private_key().sign(plaintext); - - let credential_request = BlindSignRequestBody::new( - request_data.withdrawal_request.into(), - deposit_id, - signature, - request_data.ecash_pubkey, - request_data.expiration_date, - request_data.ticketbook_type, - ); - - let wallet_shares = Arc::new(Mutex::new(HashMap::new())); - - info!("attempting to contract all nym-apis for the partial wallets..."); - stream::iter(ecash_api_clients) - .for_each_concurrent(None, |client| async { - // move the client into the block - let client = client; - - debug!("contacting {client} for blinded partial wallet"); - let res = timeout( - Duration::from_secs(5), - client.api_client.blind_sign(&credential_request), - ) - .await - .map_err(|_| VpnApiError::EcashApiRequestTimeout { - client_repr: client.to_string(), - }) - .and_then(|res| res.map_err(Into::into)); - - // 1. try to store it - if let Err(err) = state - .storage() - .insert_partial_wallet_share( - deposit_id, - epoch, - expiration_date, - client.node_id, - &res, - ) - .await - { - error!("failed to persist issued partial share: {err}") - } - - // 2. add it to the map - match res { - Ok(share) => { - wallet_shares - .lock() - .await - .insert(client.node_id, share.blinded_signature); - } - Err(err) => { - error!("failed to obtain partial blinded wallet share from {client}: {err}") - } - } - }) - .await; - - // SAFETY: the futures have completed, so we MUST have the only arc reference - #[allow(clippy::unwrap_used)] - let wallet_shares = Arc::into_inner(wallet_shares).unwrap().into_inner(); - let shares = wallet_shares.len(); - - if shares < threshold as usize { - return Err(VpnApiError::InsufficientNumberOfCredentials { - available: shares, - threshold, - }); - } - - Ok(wallet_shares - .into_iter() - .map(|(node_index, share)| WalletShare { - node_index, - bs58_encoded_share: share.to_bs58(), - }) - .collect()) -} - -// same as try_obtain_wallet_shares, but writes failures into the db -async fn try_obtain_wallet_shares_async( - state: &ApiState, - request: Uuid, - requested_on: OffsetDateTime, - request_data: TicketbookRequest, - device_id: &str, - credential_id: &str, -) -> Result, VpnApiError> { - let shares = match try_obtain_wallet_shares(state, request, requested_on, request_data).await { - Ok(shares) => shares, - Err(err) => { - let obtained = match err { - VpnApiError::InsufficientNumberOfCredentials { available, .. } => available, - _ => 0, - }; - - // currently there's no retry mechanisms, but, who knows, that might change - if let Err(err) = state - .storage() - .update_pending_async_blinded_shares_error( - obtained, - device_id, - credential_id, - &err.to_string(), - ) - .await - { - error!("failed to update database with the error information: {err}") - } - return Err(err); - } - }; - - Ok(shares) -} - -async fn try_obtain_blinded_ticketbook_async_inner( - state: &ApiState, - request: Uuid, - requested_on: OffsetDateTime, - request_data: TicketbookAsyncRequest, - params: TicketbookObtainQueryParams, - pending: &BlindedShares, -) -> Result<(), VpnApiError> { - let epoch_id = state.current_epoch_id().await?; - - let device_id = &request_data.device_id; - let credential_id = &request_data.credential_id; - let secret = request_data.secret.clone(); - - // 1. try to obtain global data - let ( - master_verification_key, - aggregated_expiration_date_signatures, - aggregated_coin_index_signatures, - ) = state - .global_data( - params.include_master_verification_key, - params.include_coin_index_signatures, - params.include_expiration_date_signatures, - epoch_id, - request_data.inner.expiration_date, - ) - .await?; - - // 2. try to obtain shares (failures are written to the DB) - let shares = try_obtain_wallet_shares_async( - state, - request, - requested_on, - request_data.inner, - device_id, - credential_id, - ) - .await?; - - // 3. update the storage, if possible - // (as long as we can trigger webhook, we should still be good) - if let Err(err) = state - .storage() - .update_pending_async_blinded_shares_issued(shares.len(), device_id, credential_id) - .await - { - error!(uuid = %request, "failed to update db with issued information: {err}") - } - - // 4. build the webhook request body - let data = Some(TicketbookWalletSharesResponse { - epoch_id, - shares, - master_verification_key, - aggregated_coin_index_signatures, - aggregated_expiration_date_signatures, - }); - - let ticketbook_wallet_shares = WebhookTicketbookWalletShares { - id: pending.id, - status: pending.status.to_string(), - device_id: device_id.clone(), - credential_id: credential_id.clone(), - data, - error_message: None, - created: pending.created, - updated: pending.updated, - }; - - let webhook_request = WebhookTicketbookWalletSharesRequest { - ticketbook_wallet_shares, - secret, - }; - - // 5. call the webhook - state - .zk_nym_web_hook() - .try_trigger(request, &webhook_request) - .await; - - Ok(()) -} - -async fn try_trigger_webhook_request_for_error( - state: &ApiState, - request: Uuid, - request_data: TicketbookAsyncRequest, - pending: &BlindedShares, - error_message: String, -) -> Result<(), VpnApiError> { - let device_id = &request_data.device_id; - let credential_id = &request_data.credential_id; - let secret = request_data.secret.clone(); - - let ticketbook_wallet_shares = WebhookTicketbookWalletShares { - id: pending.id, - status: "error".to_string(), - device_id: device_id.clone(), - credential_id: credential_id.clone(), - data: None, - error_message: Some(error_message), - created: pending.created, - updated: pending.updated, - }; - - let webhook_request = WebhookTicketbookWalletSharesRequest { - ticketbook_wallet_shares, - secret, - }; - - state - .zk_nym_web_hook() - .try_trigger(request, &webhook_request) - .await; - - Ok(()) -} - -#[instrument(skip_all, fields(credential_id = %request_data.credential_id, device_id = %request_data.device_id))] -#[allow(clippy::too_many_arguments)] -pub(crate) async fn try_obtain_blinded_ticketbook_async( - state: ApiState, - request: Uuid, - requested_on: OffsetDateTime, - request_data: TicketbookAsyncRequest, - params: TicketbookObtainQueryParams, - pending: BlindedShares, -) { - let skip_webhook = params.skip_webhook; - if let Err(err) = try_obtain_blinded_ticketbook_async_inner( - &state, - request, - requested_on, - request_data.clone(), - params, - &pending, - ) - .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, - request, - request_data, - &pending, - format!("Failed to get ticketbook: {err}"), - ) - .await - { - error!(uuid = %request, "failed to make webhook request to report error: {webhook_err}") - } - error!(uuid = %request, "failed to resolve the blinded ticketbook issuance: {err}") - } else { - info!(uuid = %request, "managed to resolve the blinded ticketbook issuance") - } -} diff --git a/nym-credential-proxy/nym-credential-proxy/src/deposit_maker.rs b/nym-credential-proxy/nym-credential-proxy/src/deposit_maker.rs deleted file mode 100644 index 2ec685d656..0000000000 --- a/nym-credential-proxy/nym-credential-proxy/src/deposit_maker.rs +++ /dev/null @@ -1,205 +0,0 @@ -// Copyright 2024 - Nym Technologies SA -// 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>, -} - -impl DepositRequest { - pub(crate) fn new( - pubkey: ed25519::PublicKey, - deposit_amount: &Coin, - on_done: oneshot::Sender>, - ) -> Self { - DepositRequest { - pubkey, - deposit_amount: deposit_amount.clone(), - on_done, - } - } -} - -pub(crate) type DepositRequestReceiver = mpsc::Receiver; - -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); - -impl From> for DepositRequestSender { - fn from(inner: mpsc::Sender) -> 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, - ) -> 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/helpers.rs b/nym-credential-proxy/nym-credential-proxy/src/helpers.rs index 8a528ff575..d4d3742939 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/helpers.rs +++ b/nym-credential-proxy/nym-credential-proxy/src/helpers.rs @@ -1,59 +1,12 @@ // Copyright 2024 Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only +use crate::{cli::Cli, http::HttpServer}; use nym_bin_common::bin_info; -use time::OffsetDateTime; -use tokio_util::sync::CancellationToken; -use tracing::{debug, error, info, warn}; - -use crate::{ - cli::Cli, - deposit_maker::DepositMaker, - error::VpnApiError, - http::{ - state::{ApiState, ChainClient}, - HttpServer, - }, - storage::VpnApiStorage, - tasks::StoragePruner, -}; - -pub struct LockTimer { - created: OffsetDateTime, - message: String, -} - -impl LockTimer { - pub fn new>(message: S) -> Self { - LockTimer { - message: message.into(), - ..Default::default() - } - } -} - -impl Drop for LockTimer { - fn drop(&mut self) { - let time_taken = OffsetDateTime::now_utc() - self.created; - let time_taken_formatted = humantime::format_duration(time_taken.unsigned_abs()); - if time_taken > time::Duration::SECOND * 10 { - warn!(time_taken = %time_taken_formatted, "{}", self.message) - } else if time_taken > time::Duration::SECOND * 5 { - info!(time_taken = %time_taken_formatted, "{}", self.message) - } else { - debug!(time_taken = %time_taken_formatted, "{}", self.message) - }; - } -} - -impl Default for LockTimer { - fn default() -> Self { - LockTimer { - created: OffsetDateTime::now_utc(), - message: "released the lock".to_string(), - } - } -} +use nym_credential_proxy_lib::error::CredentialProxyError; +use nym_credential_proxy_lib::storage::CredentialProxyStorage; +use nym_credential_proxy_lib::ticketbook_manager::TicketbookManager; +use tracing::{error, info}; pub async fn wait_for_signal() { use tokio::signal::unix::{signal, SignalKind}; @@ -77,6 +30,7 @@ pub async fn wait_for_signal() { } } +#[allow(clippy::panic)] fn build_sha_short() -> &'static str { let bin_info = bin_info!(); if bin_info.commit_sha.len() < 7 { @@ -91,51 +45,34 @@ fn build_sha_short() -> &'static str { &bin_info.commit_sha[..7] } -pub(crate) async fn run_api(cli: Cli) -> Result<(), VpnApiError> { - // create the tasks +pub(crate) async fn run_api(cli: Cli) -> Result<(), CredentialProxyError> { let bind_address = cli.bind_address(); - - let storage = VpnApiStorage::init(cli.persistent_storage_path()).await?; + let storage = CredentialProxyStorage::init(cli.persistent_storage_path()).await?; let mnemonic = cli.mnemonic; let auth_token = cli.http_auth_token; let webhook_cfg = cli.webhook; - let chain_client = ChainClient::new(mnemonic)?; - let cancellation_token = CancellationToken::new(); - let deposit_maker = DepositMaker::new( + let ticketbook_manager = TicketbookManager::new( build_sha_short(), - chain_client.clone(), + cli.quorum_check_interval, + cli.deposits_buffer_size, cli.max_concurrent_deposits, - cancellation_token.clone(), - ); - - 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(), + storage, + mnemonic, + webhook_cfg.try_into()?, ) .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()); + let http_server = HttpServer::new(bind_address, ticketbook_manager.clone(), auth_token); + + // spawn the http server as a separate task / thread(-ish) + http_server.spawn_as_task(); // wait for cancel signal (SIGINT, SIGTERM or SIGQUIT) wait_for_signal().await; // cancel all the tasks and wait for all task to terminate - api_state.cancel_and_wait().await; + ticketbook_manager.cancel_and_wait().await; Ok(()) } diff --git a/nym-credential-proxy/nym-credential-proxy/src/http/helpers.rs b/nym-credential-proxy/nym-credential-proxy/src/http/helpers.rs deleted file mode 100644 index 844e1510c0..0000000000 --- a/nym-credential-proxy/nym-credential-proxy/src/http/helpers.rs +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright 2024 Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only - -use crate::error::VpnApiError; -use crate::http::types::RequestError; -use axum::http::StatusCode; -use rand::rngs::OsRng; -use rand::RngCore; -use tracing::warn; -use uuid::Uuid; - -pub fn random_uuid() -> Uuid { - let mut bytes = [0u8; 16]; - let mut rng = OsRng; - rng.fill_bytes(&mut bytes); - Uuid::from_bytes(bytes) -} - -pub fn db_failure(err: VpnApiError, uuid: Uuid) -> Result { - warn!("db failure: {err}"); - Err(RequestError::new_with_uuid( - format!("oh no, something went wrong {err}"), - uuid, - StatusCode::INTERNAL_SERVER_ERROR, - )) -} 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 e9359ac16a..de37dbba58 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/http/mod.rs +++ b/nym-credential-proxy/nym-credential-proxy/src/http/mod.rs @@ -1,56 +1,54 @@ // Copyright 2024 Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::error::VpnApiError; use crate::http::router::build_router; -use crate::http::state::ApiState; -use axum::Router; +use nym_credential_proxy_lib::error::CredentialProxyError; +use nym_credential_proxy_lib::ticketbook_manager::TicketbookManager; use std::net::SocketAddr; -use tokio_util::sync::CancellationToken; use tracing::info; -pub mod helpers; pub mod router; pub mod state; -pub mod types; pub struct HttpServer { bind_address: SocketAddr, - cancellation: CancellationToken, - router: Router, + ticketbook_manager: TicketbookManager, + auth_token: String, } impl HttpServer { pub fn new( bind_address: SocketAddr, - state: ApiState, + ticketbook_manager: TicketbookManager, auth_token: String, - cancellation: CancellationToken, ) -> Self { HttpServer { bind_address, - cancellation, - router: build_router(state, auth_token), + ticketbook_manager, + auth_token, } } - pub async fn run_forever(self) -> Result<(), VpnApiError> { - let address = self.bind_address; - info!("starting the http server on http://{address}"); + pub fn spawn_as_task(self) { + let cancellation = self.ticketbook_manager.shutdown_token(); - let listener = tokio::net::TcpListener::bind(address) + let ticketbook_manager = self.ticketbook_manager.clone(); + ticketbook_manager.try_spawn_in_background(async move { + let address = self.bind_address; + let router = build_router(self.ticketbook_manager, self.auth_token); + info!("starting the http server on http://{address}"); + + let listener = tokio::net::TcpListener::bind(address) + .await + .map_err(|source| CredentialProxyError::SocketBindFailure { address, source })?; + + axum::serve( + listener, + router.into_make_service_with_connect_info::(), + ) + .with_graceful_shutdown(async move { cancellation.cancelled().await }) .await - .map_err(|source| VpnApiError::SocketBindFailure { address, source })?; - - let cancellation = self.cancellation; - - axum::serve( - listener, - self.router - .into_make_service_with_connect_info::(), - ) - .with_graceful_shutdown(async move { cancellation.cancelled().await }) - .await - .map_err(|source| VpnApiError::HttpServerFailure { source }) + .map_err(|source| CredentialProxyError::HttpServerFailure { source }) + }); } } 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 530d2bdad1..e09531af1e 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 @@ -1,27 +1,19 @@ // Copyright 2024 Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::credentials::ticketbook::{ - try_obtain_blinded_ticketbook_async, try_obtain_wallet_shares, -}; -use crate::http::helpers::random_uuid; use crate::http::state::ApiState; -use crate::http::types::RequestError; -use crate::nym_api_helpers::ensure_sane_expiration_date; use axum::extract::{Query, State}; -use axum::http::StatusCode; use axum::routing::{get, post}; use axum::{Json, Router}; -use nym_compact_ecash::Base58; +use nym_credential_proxy_lib::helpers::random_uuid; +use nym_credential_proxy_lib::http_helpers::RequestError; use nym_credential_proxy_requests::api::v1::ticketbook::models::{ - CurrentEpochResponse, DepositResponse, MasterVerificationKeyResponse, PartialVerificationKey, + CurrentEpochResponse, DepositResponse, MasterVerificationKeyResponse, PartialVerificationKeysResponse, TicketbookAsyncRequest, TicketbookObtainQueryParams, TicketbookRequest, TicketbookWalletSharesAsyncResponse, TicketbookWalletSharesResponse, }; 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, Instrument, Level}; pub(crate) mod shares; @@ -68,61 +60,15 @@ pub(crate) async fn obtain_ticketbook_shares( Json(payload): Json, ) -> Result { let uuid = random_uuid(); - let requested_on = OffsetDateTime::now_utc(); + let output = params.output.unwrap_or_default(); - let span = span!(Level::INFO, "obtain ticketboook", uuid = %uuid); - async move { - info!(""); + let response = state + .inner_state() + .obtain_ticketbook_shares(uuid, payload, params.obtain_params.global) + .await + .map_err(|err| RequestError::new_server_error(err, uuid))?; - 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, - )); - } - - // 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?; - - 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 + Ok(output.to_response(response)) } /// Attempt to obtain blinded shares of an ecash ticketbook wallet asynchronously @@ -159,72 +105,15 @@ pub(crate) async fn obtain_ticketbook_shares_async( Json(payload): Json, ) -> Result { let uuid = random_uuid(); - let requested_on = OffsetDateTime::now_utc(); + let output = params.output.unwrap_or_default(); - let span = span!(Level::INFO, "[async] obtain ticketboook", uuid = %uuid); - async move { - info!(""); - let output = params.output.unwrap_or_default(); + let response = state + .inner_state() + .obtain_ticketbook_shares_async(uuid, payload, params.obtain_params) + .await + .map_err(|err| RequestError::new_server_error(err, uuid))?; - // 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}"); - 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 + Ok(output.to_response(response)) } /// Obtain the current value of the bandwidth voucher deposit @@ -251,15 +140,14 @@ pub(crate) async fn current_deposit( State(state): State, ) -> Result { let output = output.output.unwrap_or_default(); - let current_deposit = state - .deposit_amount() - .await - .map_err(|err| RequestError::new(err.to_string(), StatusCode::INTERNAL_SERVER_ERROR))?; - Ok(output.to_response(DepositResponse { - current_deposit_amount: current_deposit.amount, - current_deposit_denom: current_deposit.denom, - })) + let response = state + .inner_state() + .current_deposit() + .await + .map_err(RequestError::new_plain_error)?; + + Ok(output.to_response(response)) } /// Obtain partial verification keys of all signers for the current epoch. @@ -288,28 +176,13 @@ pub(crate) async fn partial_verification_keys( ) -> Result { let output = output.output.unwrap_or_default(); - state.ensure_not_in_epoch_transition(None).await?; - - let epoch_id = state - .current_epoch_id() + let response = state + .inner_state() + .partial_verification_keys() .await - .map_err(|err| RequestError::new(err.to_string(), StatusCode::INTERNAL_SERVER_ERROR))?; + .map_err(RequestError::new_plain_error)?; - let signers = state - .ecash_clients(epoch_id) - .await - .map_err(|err| RequestError::new(err.to_string(), StatusCode::INTERNAL_SERVER_ERROR))?; - - Ok(output.to_response(PartialVerificationKeysResponse { - epoch_id, - keys: signers - .iter() - .map(|signer| PartialVerificationKey { - node_index: signer.node_id, - bs58_encoded_key: signer.verification_key.to_bs58(), - }) - .collect(), - })) + Ok(output.to_response(response)) } /// Obtain the master verification key for the current epoch. @@ -338,22 +211,13 @@ pub(crate) async fn master_verification_key( ) -> Result { let output = output.output.unwrap_or_default(); - state.ensure_not_in_epoch_transition(None).await?; - - let epoch_id = state - .current_epoch_id() + let response = state + .inner_state() + .master_verification_key() .await - .map_err(|err| RequestError::new(err.to_string(), StatusCode::INTERNAL_SERVER_ERROR))?; + .map_err(RequestError::new_plain_error)?; - let key = state - .master_verification_key(Some(epoch_id)) - .await - .map_err(|err| RequestError::new(err.to_string(), StatusCode::INTERNAL_SERVER_ERROR))?; - - Ok(output.to_response(MasterVerificationKeyResponse { - epoch_id, - bs58_encoded_key: key.to_bs58(), - })) + Ok(output.to_response(response)) } /// Obtain the id of the current epoch. @@ -383,14 +247,13 @@ pub(crate) async fn current_epoch( ) -> Result { let output = output.output.unwrap_or_default(); - state.ensure_not_in_epoch_transition(None).await?; - - let epoch_id = state - .current_epoch_id() + let response = state + .inner_state() + .current_epoch() .await - .map_err(|err| RequestError::new(err.to_string(), StatusCode::INTERNAL_SERVER_ERROR))?; + .map_err(RequestError::new_plain_error)?; - Ok(output.to_response(CurrentEpochResponse { epoch_id })) + Ok(output.to_response(response)) } pub(crate) fn routes() -> Router { 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 8b3dacb82d..b41173e47e 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 @@ -1,76 +1,18 @@ // Copyright 2024 Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::error::VpnApiError; -use crate::http::helpers::{db_failure, random_uuid}; use crate::http::router::api::v1::ticketbook::FormattedTicketbookWalletSharesResponse; use crate::http::state::ApiState; -use crate::http::types::RequestError; -use crate::storage::models::MinimalWalletShare; use axum::extract::{Path, Query, State}; -use axum::http::StatusCode; use axum::routing::get; use axum::Router; +use nym_credential_proxy_lib::helpers::random_uuid; +use nym_credential_proxy_lib::http_helpers::RequestError; use nym_credential_proxy_requests::api::v1::ticketbook::models::{ SharesQueryParams, TicketbookWalletSharesResponse, }; 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, Instrument, Level}; -use uuid::Uuid; - -async fn shares_to_response( - state: ApiState, - uuid: Uuid, - shares: Vec, - params: SharesQueryParams, -) -> Result { - // in all calls we ensured the shares are non-empty - #[allow(clippy::unwrap_used)] - let first = shares.first().unwrap(); - let expiration_date = first.expiration_date; - let epoch_id = first.epoch_id as EpochId; - - let threshold = state.response_ecash_threshold(uuid, epoch_id).await?; - if shares.len() < threshold as usize { - return Err(RequestError::new_server_error( - VpnApiError::InsufficientNumberOfCredentials { - available: shares.len(), - threshold, - }, - uuid, - )); - } - - // grab any requested additional data - 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, - expiration_date, - uuid, - ) - .await?; - - // finally produce a response - Ok(params - .output - .unwrap_or_default() - .to_response(TicketbookWalletSharesResponse { - epoch_id, - shares: shares.into_iter().map(Into::into).collect(), - master_verification_key, - aggregated_coin_index_signatures, - aggregated_expiration_date_signatures, - })) -} /// Query by id for blinded shares of a bandwidth voucher #[utoipa::path( @@ -98,53 +40,15 @@ pub(crate) async fn query_for_shares_by_id( Path(share_id): Path, ) -> Result { let uuid = random_uuid(); + let output = params.output.unwrap_or_default(); - let span = span!(Level::INFO, "query shares by id", uuid = %uuid, share_id = %share_id); - async move { - debug!(""); + let response = state + .inner_state() + .query_for_shares_by_id(uuid, params.global, share_id) + .await + .map_err(|err| RequestError::new_server_error(err, uuid))?; - // 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), - } - - return Err(RequestError::new_with_uuid( - format!("not found - share_id = {share_id}"), - uuid, - StatusCode::NOT_FOUND, - )); - } - shares - } - Err(err) => return db_failure(err, uuid), - }; - - shares_to_response(state, uuid, shares, params).await - }.instrument(span).await + Ok(output.to_response(response)) } /// Query by id for blinded wallet shares of a ticketbook @@ -173,53 +77,20 @@ pub(crate) async fn query_for_shares_by_device_id_and_credential_id( Path((device_id, credential_id)): Path<(String, String)>, ) -> Result { let uuid = random_uuid(); + let output = params.output.unwrap_or_default(); - let span = span!(Level::INFO, "query shares by device and credential ids", uuid = %uuid, device_id = %device_id, credential_id = %credential_id); - async move { - debug!(""); + let response = state + .inner_state() + .query_for_shares_by_device_id_and_credential_id( + uuid, + params.global, + device_id, + credential_id, + ) + .await + .map_err(|err| RequestError::new_server_error(err, uuid))?; - // 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), - } - - return Err(RequestError::new_with_uuid( - format!("not found - device_id = {device_id}, credential_id = {credential_id}"), - uuid, - StatusCode::NOT_FOUND, - )); - } - shares - } - Err(err) => return db_failure(err, uuid), - }; - - shares_to_response(state, uuid, shares, params).await - }.instrument(span).await + Ok(output.to_response(response)) } pub(crate) fn routes() -> Router { 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 243d3494d5..292d27a486 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 @@ -18,7 +18,7 @@ fn swagger_redirect() -> MethodRouter { get(|| async { Redirect::to("/api/v1/swagger/") }) } -pub fn build_router(state: ApiState, auth_token: String) -> Router { +pub fn build_router(state: impl Into, auth_token: String) -> Router { // let auth_layer = from_extractor::(); let auth_middleware = AuthLayer::new(Arc::new(Zeroizing::new(auth_token))); @@ -32,7 +32,7 @@ pub fn build_router(state: ApiState, auth_token: String) -> Router { // we don't have to be using middleware, but we already had that code // we might want something like: https://github.com/tokio-rs/axum/blob/main/examples/tracing-aka-logging/src/main.rs#L44 instead .layer(axum::middleware::from_fn(logging::log_request_info)) - .with_state(state); + .with_state(state.into()); cfg_if::cfg_if! { if #[cfg(feature = "cors")] { 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 4579b677fa..a7c1b5b2b5 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,771 +1,21 @@ // Copyright 2024 Nym Technologies SA // 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; -use crate::nym_api_helpers::{ - ensure_sane_expiration_date, query_all_threshold_apis, CachedEpoch, CachedImmutableEpochItem, - CachedImmutableItems, -}; -use crate::storage::VpnApiStorage; -use crate::webhook::ZkNymWebHookConfig; -use axum::http::StatusCode; -use bip39::Mnemonic; -use nym_compact_ecash::scheme::coin_indices_signatures::{ - aggregate_annotated_indices_signatures, CoinIndexSignatureShare, -}; -use nym_compact_ecash::scheme::expiration_date_signatures::{ - aggregate_annotated_expiration_signatures, ExpirationDateSignatureShare, -}; -use nym_compact_ecash::Base58; -use nym_credential_proxy_requests::api::v1::ticketbook::models::{ - AggregatedCoinIndicesSignaturesResponse, AggregatedExpirationDateSignaturesResponse, - MasterVerificationKeyResponse, -}; -use nym_credentials::ecash::utils::{ecash_today, EcashTime}; -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::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}; -use uuid::Uuid; +use nym_credential_proxy_lib::ticketbook_manager::TicketbookManager; -// currently we need to hold our keypair so that we could request a freepass credential #[derive(Clone)] pub struct ApiState { - inner: Arc, + inner: TicketbookManager, +} + +impl From for ApiState { + fn from(inner: TicketbookManager) -> Self { + Self { inner } + } } -// a lot of functionalities, mostly to do with caching and storage is just copy-pasted from nym-api, -// since we have to do more or less the same work impl ApiState { - pub async fn new( - storage: VpnApiStorage, - zk_nym_web_hook_config: ZkNymWebHookConfig, - client: ChainClient, - deposit_requester: DepositRequestSender, - cancellation_token: CancellationToken, - ) -> Result { - let state = ApiState { - inner: Arc::new(ApiStateInner { - storage, - client, - ecash_state: EcashState::default(), - zk_nym_web_hook_config, - task_tracker: TaskTracker::new(), - deposit_requester, - cancellation_token, - }), - }; - - // since this is startup, - // might as well do all the needed network queries to establish needed global signatures - // if we don't already have them - state.build_initial_cache().await?; - - Ok(state) - } - - async fn build_initial_cache(&self) -> Result<(), VpnApiError> { - let today = ecash_today().date(); - - let epoch_id = self.current_epoch_id().await?; - let _ = self.deposit_amount().await?; - let _ = self.master_verification_key(Some(epoch_id)).await?; - let _ = self.ecash_threshold(epoch_id).await?; - let _ = self.ecash_clients(epoch_id).await?; - let _ = self.master_coin_index_signatures(Some(epoch_id)).await?; - let _ = self - .master_expiration_date_signatures(epoch_id, today) - .await?; - - Ok(()) - } - - pub(crate) fn try_spawn(&self, task: F) -> Option> - where - F: Future + Send + 'static, - F::Output: Send + 'static, - { - // don't spawn new task if we've received cancellation token - if self.inner.cancellation_token.is_cancelled() { - None - } else { - self.inner.task_tracker.reopen(); - // TODO: later use a task queue since most requests will be blocked waiting on chain permit anyway - let join_handle = self.inner.task_tracker.spawn(task); - self.inner.task_tracker.close(); - Some(join_handle) - } - } - - pub(crate) async fn cancel_and_wait(&self) { - self.inner.cancellation_token.cancel(); - self.inner.task_tracker.wait().await - } - - pub(crate) fn zk_nym_web_hook(&self) -> &ZkNymWebHookConfig { - &self.inner.zk_nym_web_hook_config - } - - async fn ensure_credentials_issuable(&self) -> Result<(), VpnApiError> { - let epoch = self.current_epoch().await?; - - if epoch.state.is_final() { - Ok(()) - } else if let Some(final_timestamp) = epoch.final_timestamp_secs() { - // SAFETY: the timestamp values in our DKG contract should be valid timestamps, - // otherwise it means the chain is seriously misbehaving - #[allow(clippy::unwrap_used)] - let finish_dt = OffsetDateTime::from_unix_timestamp(final_timestamp as i64).unwrap(); - - Err(VpnApiError::CredentialsNotYetIssuable { - availability: finish_dt, - }) - } else if epoch.state.is_waiting_initialisation() { - Err(VpnApiError::UninitialisedDkg) - } else { - Err(VpnApiError::UnknownEcashFailure) - } - } - - pub(crate) fn storage(&self) -> &VpnApiStorage { - &self.inner.storage - } - - pub async fn deposit_amount(&self) -> Result { - let read_guard = self.inner.ecash_state.required_deposit_cache.read().await; - if read_guard.is_valid() { - return Ok(read_guard.required_amount.clone()); - } - - // update cache - drop(read_guard); - let mut write_guard = self.inner.ecash_state.required_deposit_cache.write().await; - let deposit_amount = self - .query_chain() - .await - .get_required_deposit_amount() - .await?; - - write_guard.update(deposit_amount.clone().into()); - - Ok(deposit_amount.into()) - } - - async fn current_epoch(&self) -> Result { - let read_guard = self.inner.ecash_state.cached_epoch.read().await; - if read_guard.is_valid() { - return Ok(read_guard.current_epoch); - } - - // update cache - drop(read_guard); - let mut write_guard = self.inner.ecash_state.cached_epoch.write().await; - let epoch = self.query_chain().await.get_current_epoch().await?; - - write_guard.update(epoch); - Ok(epoch) - } - - pub async fn current_epoch_id(&self) -> Result { - let read_guard = self.inner.ecash_state.cached_epoch.read().await; - if read_guard.is_valid() { - return Ok(read_guard.current_epoch.epoch_id); - } - - // update cache - drop(read_guard); - let mut write_guard = self.inner.ecash_state.cached_epoch.write().await; - let epoch = self.query_chain().await.get_current_epoch().await?; - - write_guard.update(epoch); - Ok(epoch.epoch_id) - } - - pub(crate) async fn query_chain(&self) -> RwLockReadGuard<'_, DirectSigningHttpRpcNyxdClient> { - self.inner.client.query_chain().await - } - - pub(crate) async fn request_deposit(&self, request: DepositRequest) { - let start = Instant::now(); - self.inner.deposit_requester.request_deposit(request).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}") - } - } - - pub(crate) async fn global_data( - &self, - include_master_verification_key: bool, - include_expiration_date_signatures: bool, - include_coin_index_signatures: bool, - epoch_id: EpochId, - expiration_date: Date, - ) -> Result< - ( - Option, - Option, - Option, - ), - VpnApiError, - > { - let master_verification_key = if include_master_verification_key { - debug!("including master verification key in the response"); - Some( - self.master_verification_key(Some(epoch_id)) - .await - .map(|key| MasterVerificationKeyResponse { - epoch_id, - bs58_encoded_key: key.to_bs58(), - }) - .inspect_err(|err| warn!("request failure: {err}"))?, - ) - } else { - None - }; - - let aggregated_expiration_date_signatures = if include_expiration_date_signatures { - debug!("including expiration date signatures in the response"); - Some( - self.master_expiration_date_signatures(epoch_id, expiration_date) - .await - .map(|signatures| AggregatedExpirationDateSignaturesResponse { - signatures: signatures.clone(), - }) - .inspect_err(|err| warn!("request failure: {err}"))?, - ) - } else { - None - }; - - let aggregated_coin_index_signatures = if include_coin_index_signatures { - debug!("including coin index signatures in the response"); - Some( - self.master_coin_index_signatures(Some(epoch_id)) - .await - .map(|signatures| AggregatedCoinIndicesSignaturesResponse { - signatures: signatures.clone(), - }) - .inspect_err(|err| warn!("request failure: {err}"))?, - ) - } else { - None - }; - - Ok(( - master_verification_key, - aggregated_expiration_date_signatures, - aggregated_coin_index_signatures, - )) - } - - pub(crate) async fn response_global_data( - &self, - include_master_verification_key: bool, - include_expiration_date_signatures: bool, - include_coin_index_signatures: bool, - epoch_id: EpochId, - expiration_date: Date, - uuid: Uuid, - ) -> Result< - ( - Option, - Option, - Option, - ), - RequestError, - > { - self.global_data( - include_master_verification_key, - include_expiration_date_signatures, - include_coin_index_signatures, - epoch_id, - expiration_date, - ) - .await - .map_err(|err| RequestError::new_server_error(err, uuid)) - } - - pub async fn ensure_not_in_epoch_transition( - &self, - uuid: Option, - ) -> Result<(), RequestError> { - if let Err(err) = self.ensure_credentials_issuable().await { - return if let Some(uuid) = uuid { - Err(RequestError::new_with_uuid( - err.to_string(), - uuid, - StatusCode::SERVICE_UNAVAILABLE, - )) - } else { - Err(RequestError::new( - err.to_string(), - StatusCode::SERVICE_UNAVAILABLE, - )) - }; - } - Ok(()) - } - - pub(crate) async fn ecash_clients( - &self, - epoch_id: EpochId, - ) -> Result>, VpnApiError> { - self.inner - .ecash_state - .epoch_clients - .get_or_init(epoch_id, || async { - Ok(self - .query_chain() - .await - .get_all_verification_key_shares(epoch_id) - .await? - .into_iter() - .map(TryInto::try_into) - .collect::, EcashApiError>>()?) - }) - .await - } - - pub(crate) async fn ecash_threshold(&self, epoch_id: EpochId) -> Result { - self.inner - .ecash_state - .threshold_values - .get_or_init(epoch_id, || async { - if let Some(threshold) = self - .query_chain() - .await - .get_epoch_threshold(epoch_id) - .await? - { - Ok(threshold) - } else { - Err(VpnApiError::UnavailableThreshold { epoch_id }) - } - }) - .await - .map(|t| *t) - } - - pub(crate) async fn response_ecash_threshold( - &self, - uuid: Uuid, - epoch_id: EpochId, - ) -> Result { - self.ecash_threshold(epoch_id) - .await - .map_err(|err| RequestError::new_server_error(err, uuid)) - } - - pub(crate) async fn master_verification_key( - &self, - epoch_id: Option, - ) -> Result, VpnApiError> { - let epoch_id = match epoch_id { - Some(id) => id, - None => self.current_epoch_id().await?, - }; - - self.inner - .ecash_state - .master_verification_key - .get_or_init(epoch_id, || async { - // 1. check the storage - if let Some(stored) = self - .inner - .storage - .get_master_verification_key(epoch_id) - .await? - { - return Ok(stored.key); - } - - info!("attempting to establish master verification key for epoch {epoch_id}..."); - - // 2. perform actual aggregation - let all_apis = self.ecash_clients(epoch_id).await?; - let threshold = self.ecash_threshold(epoch_id).await?; - - if all_apis.len() < threshold as usize { - return Err(VpnApiError::InsufficientNumberOfSigners { - threshold, - available: all_apis.len(), - }); - } - - let master_key = nym_credentials::aggregate_verification_keys(&all_apis)?; - - let epoch = EpochVerificationKey { - epoch_id, - key: master_key, - }; - - // 3. save the key in the storage for when we reboot - self.inner - .storage - .insert_master_verification_key(&epoch) - .await?; - - Ok(epoch.key) - }) - .await - } - - pub(crate) async fn master_coin_index_signatures( - &self, - epoch_id: Option, - ) -> Result, VpnApiError> { - let epoch_id = match epoch_id { - Some(id) => id, - None => self.current_epoch_id().await?, - }; - - self.inner - .ecash_state - .coin_index_signatures - .get_or_init(epoch_id, || async { - // 1. check the storage - if let Some(master_sigs) = self - .inner - .storage - .get_master_coin_index_signatures(epoch_id) - .await? - { - return Ok(master_sigs); - } - - info!( - "attempting to establish master coin index signatures for epoch {epoch_id}..." - ); - - // 2. go around APIs and attempt to aggregate the data - let master_vk = self.master_verification_key(Some(epoch_id)).await?; - let all_apis = self.ecash_clients(epoch_id).await?; - let threshold = self.ecash_threshold(epoch_id).await?; - - let get_partial_signatures = |api: EcashApiClient| async { - // move the api into the closure - let api = api; - let node_index = api.node_id; - let partial_vk = api.verification_key; - - let partial = api - .api_client - .partial_coin_indices_signatures(Some(epoch_id)) - .await? - .signatures; - Ok(CoinIndexSignatureShare { - index: node_index, - key: partial_vk, - signatures: partial, - }) - }; - - let shares = - query_all_threshold_apis(all_apis.clone(), threshold, get_partial_signatures) - .await?; - - let aggregated = aggregate_annotated_indices_signatures( - nym_credentials_interface::ecash_parameters(), - &master_vk, - &shares, - )?; - - let sigs = AggregatedCoinIndicesSignatures { - epoch_id, - signatures: aggregated, - }; - - // 3. save the signatures in the storage for when we reboot - self.inner - .storage - .insert_master_coin_index_signatures(&sigs) - .await?; - - Ok(sigs) - }) - .await - } - - pub(crate) async fn master_expiration_date_signatures( - &self, - epoch_id: EpochId, - expiration_date: Date, - ) -> Result, VpnApiError> { - self.inner - .ecash_state - .expiration_date_signatures - .get_or_init((epoch_id, expiration_date), || async { - // 1. sanity check to see if the expiration_date is not nonsense - ensure_sane_expiration_date(expiration_date)?; - - // 2. check the storage - if let Some(master_sigs) = self - .inner - .storage - .get_master_expiration_date_signatures(expiration_date, epoch_id) - .await? - { - return Ok(master_sigs); - } - - - info!( - "attempting to establish master expiration date signatures for {expiration_date} and epoch {epoch_id}..." - ); - - // 3. go around APIs and attempt to aggregate the data - let epoch_id = self.current_epoch_id().await?; - let master_vk = self.master_verification_key(Some(epoch_id)).await?; - let all_apis = self.ecash_clients(epoch_id).await?; - let threshold = self.ecash_threshold(epoch_id).await?; - - let get_partial_signatures = |api: EcashApiClient| async { - // move the api into the closure - let api = api; - let node_index = api.node_id; - let partial_vk = api.verification_key; - - let partial = api - .api_client - .partial_expiration_date_signatures(Some(expiration_date), Some(epoch_id)) - .await? - .signatures; - Ok(ExpirationDateSignatureShare { - index: node_index, - key: partial_vk, - signatures: partial, - }) - }; - - let shares = - query_all_threshold_apis(all_apis.clone(), threshold, get_partial_signatures) - .await?; - - let aggregated = aggregate_annotated_expiration_signatures( - &master_vk, - expiration_date.ecash_unix_timestamp(), - &shares, - )?; - - let sigs = AggregatedExpirationDateSignatures { - epoch_id, - expiration_date, - signatures: aggregated, - }; - - // 4. save the signatures in the storage for when we reboot - self.inner - .storage - .insert_master_expiration_date_signatures(&sigs) - .await?; - - Ok(sigs) - }) - .await - } -} - -#[derive(Clone)] -pub struct ChainClient(Arc>); - -impl ChainClient { - pub fn new(mnemonic: Mnemonic) -> Result { - 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: ChainClient, - - deposit_requester: DepositRequestSender, - - zk_nym_web_hook_config: ZkNymWebHookConfig, - - ecash_state: EcashState, - - task_tracker: TaskTracker, - - cancellation_token: CancellationToken, -} - -pub(crate) struct CachedDeposit { - valid_until: OffsetDateTime, - required_amount: Coin, -} - -impl CachedDeposit { - const MAX_VALIDITY: time::Duration = time::Duration::MINUTE; - - fn is_valid(&self) -> bool { - self.valid_until > OffsetDateTime::now_utc() - } - - fn update(&mut self, required_amount: Coin) { - self.valid_until = OffsetDateTime::now_utc() + Self::MAX_VALIDITY; - self.required_amount = required_amount; - } -} - -impl Default for CachedDeposit { - fn default() -> Self { - CachedDeposit { - valid_until: OffsetDateTime::UNIX_EPOCH, - required_amount: Coin { - amount: u128::MAX, - denom: "unym".to_string(), - }, - } - } -} - -#[derive(Default)] -pub(crate) struct EcashState { - pub(crate) required_deposit_cache: RwLock, - - pub(crate) cached_epoch: RwLock, - - pub(crate) master_verification_key: CachedImmutableEpochItem, - - pub(crate) threshold_values: CachedImmutableEpochItem, - - pub(crate) epoch_clients: CachedImmutableEpochItem>, - - pub(crate) coin_index_signatures: CachedImmutableEpochItem, - - pub(crate) expiration_date_signatures: - CachedImmutableItems<(EpochId, 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 - #[allow(dead_code)] - lock_timer: LockTimer, - inner: RwLockWriteGuard<'a, DirectSigningHttpRpcNyxdClient>, -} - -impl ChainWritePermit<'_> { - pub(crate) async fn make_deposits( - self, - short_sha: &'static str, - info: Vec<(String, Coin)>, - ) -> Result { - 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::>(); - - 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 { - self.inner.deref() + pub(crate) fn inner_state(&self) -> &TicketbookManager { + &self.inner } } diff --git a/nym-credential-proxy/nym-credential-proxy/src/main.rs b/nym-credential-proxy/nym-credential-proxy/src/main.rs index 1ca9ccf316..8b85fef400 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/main.rs +++ b/nym-credential-proxy/nym-credential-proxy/src/main.rs @@ -1,11 +1,6 @@ // Copyright 2024 Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -#![warn(clippy::expect_used)] -#![warn(clippy::unwrap_used)] -#![warn(clippy::todo)] -#![warn(clippy::dbg_macro)] - cfg_if::cfg_if! { if #[cfg(unix)] { use crate::cli::Cli; @@ -17,15 +12,8 @@ cfg_if::cfg_if! { pub mod cli; pub mod config; - pub mod credentials; - mod deposit_maker; - pub mod error; pub mod helpers; pub mod http; - pub mod nym_api_helpers; - pub mod storage; - pub mod tasks; - mod webhook; } } @@ -38,7 +26,6 @@ async fn main() -> anyhow::Result<()> { // ); let cli = Cli::parse(); - cli.webhook.ensure_valid_client_url()?; trace!("args: {cli:#?}"); setup_env(cli.config_env_file.as_ref()); @@ -55,5 +42,6 @@ async fn main() -> anyhow::Result<()> { #[tokio::main] async fn main() -> anyhow::Result<()> { eprintln!("This tool is only supported on Unix systems"); + #[allow(clippy::exit)] std::process::exit(1) } diff --git a/nym-ip-packet-client/Cargo.toml b/nym-ip-packet-client/Cargo.toml new file mode 100644 index 0000000000..7a66b1358b --- /dev/null +++ b/nym-ip-packet-client/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "nym-ip-packet-client" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true +edition.workspace = true +license.workspace = true + +[lints] +workspace = true + +[dependencies] +bytes.workspace = true +futures.workspace = true +thiserror.workspace = true +tokio-util.workspace = true +tokio.workspace = true +tracing.workspace = true + +nym-sdk = { path = "../sdk/rust/nym-sdk" } +nym-ip-packet-requests = { path = "../common/ip-packet-requests" } diff --git a/nym-ip-packet-client/src/connect.rs b/nym-ip-packet-client/src/connect.rs new file mode 100644 index 0000000000..f499e2373f --- /dev/null +++ b/nym-ip-packet-client/src/connect.rs @@ -0,0 +1,199 @@ +// Copyright 2023-2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +// To remove with the Registration Client PR +#![allow(clippy::unwrap_used)] + +use std::{sync::Arc, time::Duration}; + +use nym_ip_packet_requests::IpPair; +use nym_sdk::mixnet::{ + InputMessage, MixnetClient, MixnetClientSender, MixnetMessageSender, Recipient, + TransmissionLane, +}; +use tokio::time::sleep; +use tokio_util::sync::CancellationToken; +use tracing::{debug, error}; + +use crate::{ + current::{ + request::IpPacketRequest, + response::{ + ConnectResponse, ConnectResponseReply, ControlResponse, IpPacketResponse, + IpPacketResponseData, + }, + }, + error::{Error, Result}, + helpers::check_ipr_message_version, +}; + +pub type SharedMixnetClient = Arc>>; + +const IPR_CONNECT_TIMEOUT: Duration = Duration::from_secs(10); + +#[derive(Clone, Debug, PartialEq, Eq)] +enum ConnectionState { + Disconnected, + Connecting, + Connected, + #[allow(unused)] + Disconnecting, +} + +pub struct IprClientConnect { + // During connection we need the mixnet client, but once connected we expect to setup a channel + // from the main mixnet listener at the top-level. + // As such, we drop the shared mixnet client once we're connected. + mixnet_client: SharedMixnetClient, + mixnet_sender: MixnetClientSender, + connected: ConnectionState, + cancel_token: CancellationToken, +} + +impl IprClientConnect { + pub async fn new(mixnet_client: SharedMixnetClient, cancel_token: CancellationToken) -> Self { + let mixnet_sender = mixnet_client.lock().await.as_ref().unwrap().split_sender(); + Self { + mixnet_client, + mixnet_sender, + connected: ConnectionState::Disconnected, + cancel_token, + } + } + + pub async fn connect(&mut self, ip_packet_router_address: Recipient) -> Result { + if self.connected != ConnectionState::Disconnected { + return Err(Error::AlreadyConnected); + } + + tracing::info!("Connecting to exit gateway"); + self.connected = ConnectionState::Connecting; + match self.connect_inner(ip_packet_router_address).await { + Ok(ips) => { + debug!("Successfully connected to the ip-packet-router"); + self.connected = ConnectionState::Connected; + Ok(ips) + } + Err(err) => { + error!("Failed to connect to the ip-packet-router: {:?}", err); + self.connected = ConnectionState::Disconnected; + Err(err) + } + } + } + + async fn connect_inner(&mut self, ip_packet_router_address: Recipient) -> Result { + let request_id = self.send_connect_request(ip_packet_router_address).await?; + + debug!("Waiting for reply..."); + self.listen_for_connect_response(request_id).await + } + + async fn send_connect_request(&self, ip_packet_router_address: Recipient) -> Result { + let (request, request_id) = IpPacketRequest::new_connect_request(None); + + // We use 20 surbs for the connect request because typically the IPR is configured to have + // a min threshold of 10 surbs that it reserves for itself to request additional surbs. + let surbs = 20; + self.mixnet_sender + .send(create_input_message( + ip_packet_router_address, + request, + surbs, + )) + .await + .map_err(|err| Error::SdkError(Box::new(err)))?; + + Ok(request_id) + } + + async fn handle_connect_response(&self, response: ConnectResponse) -> Result { + debug!("Handling dynamic connect response"); + match response.reply { + ConnectResponseReply::Success(r) => Ok(r.ips), + ConnectResponseReply::Failure(reason) => Err(Error::ConnectRequestDenied { reason }), + } + } + + async fn handle_ip_packet_router_response(&self, response: IpPacketResponse) -> Result { + let control_response = match response.data { + IpPacketResponseData::Control(control_response) => control_response, + _ => { + error!("Received non-control response while waiting for connect response"); + return Err(Error::UnexpectedConnectResponse); + } + }; + + match *control_response { + ControlResponse::Connect(resp) => self.handle_connect_response(resp).await, + response => { + error!("Unexpected response: {response:?}"); + Err(Error::UnexpectedConnectResponse) + } + } + } + + async fn listen_for_connect_response(&self, request_id: u64) -> Result { + // Connecting is basically synchronous from the perspective of the mixnet client, so it's safe + // to just grab ahold of the mutex and keep it until we get the response. + let mut mixnet_client_handle = self.mixnet_client.lock().await; + let mixnet_client = mixnet_client_handle.as_mut().unwrap(); + + let timeout = sleep(IPR_CONNECT_TIMEOUT); + tokio::pin!(timeout); + + loop { + tokio::select! { + _ = self.cancel_token.cancelled() => { + error!("Cancelled while waiting for reply to connect request"); + return Err(Error::Cancelled); + }, + _ = &mut timeout => { + error!("Timed out waiting for reply to connect request"); + return Err(Error::TimeoutWaitingForConnectResponse); + }, + msgs = mixnet_client.wait_for_messages() => match msgs { + None => { + return Err(Error::NoMixnetMessagesReceived); + } + Some(msgs) => { + for msg in msgs { + // Confirm that the version is correct + if let Err(err) = check_ipr_message_version(&msg) { + tracing::info!("Mixnet message version mismatch: {err}"); + continue; + } + + // Then we deserialize the message + tracing::debug!("IprClient: got message while waiting for connect response"); + let Ok(response) = IpPacketResponse::from_reconstructed_message(&msg) else { + // This is ok, it's likely just one of our self-pings + tracing::debug!("Failed to deserialize mixnet message"); + continue; + }; + + if response.id() == Some(request_id) { + tracing::debug!("Got response with matching id"); + return self.handle_ip_packet_router_response(response).await; + } + } + } + } + } + } + } +} + +fn create_input_message( + recipient: Recipient, + request: IpPacketRequest, + surbs: u32, +) -> InputMessage { + InputMessage::new_anonymous( + recipient, + request.to_bytes().unwrap(), + surbs, + TransmissionLane::General, + None, + ) +} diff --git a/nym-ip-packet-client/src/error.rs b/nym-ip-packet-client/src/error.rs new file mode 100644 index 0000000000..0979602dc2 --- /dev/null +++ b/nym-ip-packet-client/src/error.rs @@ -0,0 +1,52 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::current::response::ConnectFailureReason; + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("nym sdk")] + SdkError(#[source] Box), + + #[error( + "received response with version v{received}, the client is too new and can only understand v{expected}" + )] + ReceivedResponseWithOldVersion { expected: u8, received: u8 }, + + #[error( + "received response with version v{received}, the client is too old and can only understand v{expected}" + )] + ReceivedResponseWithNewVersion { expected: u8, received: u8 }, + + #[error("got reply for connect request, but it appears intended for the wrong address?")] + GotReplyIntendedForWrongAddress, + + #[error("unexpected connect response")] + UnexpectedConnectResponse, + + #[error("mixnet client stopped returning responses")] + NoMixnetMessagesReceived, + + #[error("timeout waiting for connect response from exit gateway (ipr)")] + TimeoutWaitingForConnectResponse, + + #[error("connection cancelled")] + Cancelled, + + #[error("connect request denied: {reason}")] + ConnectRequestDenied { reason: ConnectFailureReason }, + + #[error("failed to get version from message")] + NoVersionInMessage, + + #[error("already connected to the mixnet")] + AlreadyConnected, + + #[error("failed to create connect request")] + FailedToCreateConnectRequest { + source: nym_ip_packet_requests::sign::SignatureError, + }, +} + +// Result type based on our error type +pub type Result = std::result::Result; diff --git a/nym-ip-packet-client/src/helpers.rs b/nym-ip-packet-client/src/helpers.rs new file mode 100644 index 0000000000..2f1cf718cb --- /dev/null +++ b/nym-ip-packet-client/src/helpers.rs @@ -0,0 +1,30 @@ +// Copyright 2023-2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use std::cmp::Ordering; + +use nym_sdk::mixnet::ReconstructedMessage; + +use crate::{current::VERSION as CURRENT_VERSION, error::Result, Error}; + +pub(crate) fn check_ipr_message_version(message: &ReconstructedMessage) -> Result<()> { + // Assuming it's a IPR message, it will have a version as its first byte + if let Some(version) = message.message.first() { + match version.cmp(&CURRENT_VERSION) { + Ordering::Greater => Err(Error::ReceivedResponseWithNewVersion { + expected: CURRENT_VERSION, + received: *version, + }), + Ordering::Less => Err(Error::ReceivedResponseWithOldVersion { + expected: CURRENT_VERSION, + received: *version, + }), + Ordering::Equal => { + // We're good + Ok(()) + } + } + } else { + Err(Error::NoVersionInMessage) + } +} diff --git a/nym-ip-packet-client/src/lib.rs b/nym-ip-packet-client/src/lib.rs new file mode 100644 index 0000000000..8aa5ffdf42 --- /dev/null +++ b/nym-ip-packet-client/src/lib.rs @@ -0,0 +1,14 @@ +// Copyright 2023-2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +mod connect; +mod error; +mod helpers; +mod listener; + +pub use connect::IprClientConnect; +pub use error::Error; +pub use listener::{IprListener, MixnetMessageOutcome}; + +// Re-export the currently used version +pub use nym_ip_packet_requests::v8 as current; diff --git a/nym-ip-packet-client/src/listener.rs b/nym-ip-packet-client/src/listener.rs new file mode 100644 index 0000000000..7380300175 --- /dev/null +++ b/nym-ip-packet-client/src/listener.rs @@ -0,0 +1,121 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use bytes::Bytes; +use futures::StreamExt; +use nym_ip_packet_requests::{codec::MultiIpPacketCodec, v8::response::ControlResponse}; +use nym_sdk::mixnet::ReconstructedMessage; +use tokio_util::codec::FramedRead; +use tracing::{debug, error, info, warn}; + +use crate::{ + current::{ + request::{ControlRequest, IpPacketRequest, IpPacketRequestData}, + response::{InfoLevel, IpPacketResponse, IpPacketResponseData}, + }, + helpers::check_ipr_message_version, +}; + +pub enum MixnetMessageOutcome { + IpPackets(Vec), + MixnetSelfPing, + Disconnect, +} + +pub struct IprListener {} + +#[derive(Debug, thiserror::Error)] +pub enum IprListenerError { + #[error(transparent)] + IprClientError(#[from] crate::Error), +} + +impl IprListener { + pub fn new() -> Self { + Self {} + } + + fn is_mix_ping(&self, request: &IpPacketRequest) -> bool { + match request.data { + IpPacketRequestData::Control(ref control) => { + matches!(**control, ControlRequest::Ping(_)) + } + _ => { + debug!("Received unexpected request: {request:?}"); + false + } + } + } + + pub async fn handle_reconstructed_message( + &mut self, + message: ReconstructedMessage, + ) -> Result, IprListenerError> { + check_ipr_message_version(&message)?; + + match IpPacketResponse::from_reconstructed_message(&message) { + Ok(response) => { + match response.data { + IpPacketResponseData::Data(data_response) => { + // Un-bundle the mixnet message and send the individual IP packets + // to the tun device + let framed_reader = FramedRead::new( + data_response.ip_packet.as_ref(), + MultiIpPacketCodec::new(), + ); + let responses: Vec = framed_reader + .filter_map(|res| async { res.ok().map(|packet| packet.into_bytes()) }) + .collect() + .await; + return Ok(Some(MixnetMessageOutcome::IpPackets(responses))); + } + IpPacketResponseData::Control(control_response) => match *control_response { + ControlResponse::Connect(_) => { + info!("Received connect response when already connected - ignoring"); + } + ControlResponse::Disconnect(_) => { + info!("Received disconnect response"); + return Ok(Some(MixnetMessageOutcome::Disconnect)); + } + ControlResponse::UnrequestedDisconnect(_) => { + info!("Received unrequested disconnect response, ignoring for now"); + } + ControlResponse::Pong(_) => { + info!("Received pong response, ignoring for now"); + } + ControlResponse::Health(_) => { + info!("Received health response, ignoring for now"); + } + ControlResponse::Info(info) => { + let msg = + format!("Received info response from the mixnet: {}", info.reply); + match info.level { + InfoLevel::Info => info!("{msg}"), + InfoLevel::Warn => warn!("{msg}"), + InfoLevel::Error => error!("{msg}"), + } + } + }, + } + } + Err(err) => { + // The exception to when we are not expecting a response, is when we + // are sending a ping to ourselves. + if let Ok(request) = IpPacketRequest::from_reconstructed_message(&message) { + if self.is_mix_ping(&request) { + return Ok(Some(MixnetMessageOutcome::MixnetSelfPing)); + } + } else { + warn!("Failed to deserialize reconstructed message: {err}"); + } + } + } + Ok(None) + } +} + +impl Default for IprListener { + fn default() -> Self { + Self::new() + } +} diff --git a/nym-network-monitor/Cargo.toml b/nym-network-monitor/Cargo.toml index c693cb5b19..a84a5f3c02 100644 --- a/nym-network-monitor/Cargo.toml +++ b/nym-network-monitor/Cargo.toml @@ -40,3 +40,5 @@ nym-sphinx = { path = "../common/nymsphinx" } nym-topology = { path = "../common/topology" } nym-types = { path = "../common/types" } nym-validator-client = { path = "../common/client-libs/validator-client" } +nym-http-api-client = { path = "../common/http-api-client" } +nym-mixnet-contract-common = { path = "../common/cosmwasm-smart-contracts/mixnet-contract" } diff --git a/nym-network-monitor/src/main.rs b/nym-network-monitor/src/main.rs index d0d04d62c6..4dc9980f00 100644 --- a/nym-network-monitor/src/main.rs +++ b/nym-network-monitor/src/main.rs @@ -6,12 +6,15 @@ use log::{info, warn}; use nym_bin_common::bin_info; use nym_client_core::config::ForgetMe; use nym_crypto::asymmetric::ed25519::PrivateKey; +use nym_mixnet_contract_common::EpochRewardedSet; use nym_network_defaults::setup_env; use nym_network_defaults::var_names::NYM_API; use nym_sdk::mixnet::{self, MixnetClient}; use nym_sphinx::chunking::monitoring; use nym_topology::provider_trait::ToTopologyMetadata; use nym_topology::{HardcodedTopologyProvider, NymTopology}; +use nym_validator_client::nym_api::NymApiClientExt; +use nym_validator_client::UserAgent; use std::fs::File; use std::io::Write; use std::sync::LazyLock; @@ -160,10 +163,9 @@ async fn nym_topology_from_env() -> anyhow::Result { let api_url = std::env::var(NYM_API)?; info!("Generating topology from {api_url}"); - let client = nym_validator_client::client::NymApiClient::new_with_user_agent( - api_url.parse()?, - bin_info!(), - ); + let client = nym_http_api_client::Client::builder(api_url)? + .with_user_agent(UserAgent::from(bin_info!())) + .build()?; let rewarded_set = client.get_current_rewarded_set().await?; @@ -172,10 +174,15 @@ async fn nym_topology_from_env() -> anyhow::Result { let nodes = nodes_response.nodes; let metadata = nodes_response.metadata; - Ok( - NymTopology::new(metadata.to_topology_metadata(), rewarded_set, Vec::new()) - .with_skimmed_nodes(&nodes), + // Convert RewardedSetResponse to EpochRewardedSet which can then be converted to CachedEpochRewardedSet + let epoch_rewarded_set: EpochRewardedSet = rewarded_set.into(); + + Ok(NymTopology::new( + metadata.to_topology_metadata(), + epoch_rewarded_set, + Vec::new(), ) + .with_skimmed_nodes(&nodes)) } #[tokio::main] diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-124d45b9604439584650f401607c46bdbd162c7c689f74fe9ddfdfd48f5ddc07.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-32cf68dd34346fcac5cd0cd585845a9df296fdaebc5920dd1c12398e2967cffb.json similarity index 69% rename from nym-node-status-api/nym-node-status-api/.sqlx/query-124d45b9604439584650f401607c46bdbd162c7c689f74fe9ddfdfd48f5ddc07.json rename to nym-node-status-api/nym-node-status-api/.sqlx/query-32cf68dd34346fcac5cd0cd585845a9df296fdaebc5920dd1c12398e2967cffb.json index b32a31ab5a..4f61646962 100644 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-124d45b9604439584650f401607c46bdbd162c7c689f74fe9ddfdfd48f5ddc07.json +++ b/nym-node-status-api/nym-node-status-api/.sqlx/query-32cf68dd34346fcac5cd0cd585845a9df296fdaebc5920dd1c12398e2967cffb.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT\n date_utc as \"date_utc!\",\n SUM(total_stake) as \"total_stake!: i64\",\n SUM(packets_received) as \"total_packets_received!: i64\",\n SUM(packets_sent) as \"total_packets_sent!: i64\",\n SUM(packets_dropped) as \"total_packets_dropped!: i64\"\n FROM (\n SELECT\n date_utc,\n n.total_stake,\n n.packets_received,\n n.packets_sent,\n n.packets_dropped\n FROM nym_node_daily_mixing_stats n\n UNION ALL\n SELECT\n m.date_utc,\n m.total_stake,\n m.packets_received,\n m.packets_sent,\n m.packets_dropped\n FROM mixnode_daily_stats m\n LEFT JOIN nym_node_daily_mixing_stats ON m.mix_id = nym_node_daily_mixing_stats.node_id\n WHERE nym_node_daily_mixing_stats.node_id IS NULL\n )\n GROUP BY date_utc\n ORDER BY date_utc ASC\n ", + "query": "\n SELECT\n date_utc as \"date_utc!\",\n SUM(total_stake) as \"total_stake!: i64\",\n SUM(packets_received) as \"total_packets_received!: i64\",\n SUM(packets_sent) as \"total_packets_sent!: i64\",\n SUM(packets_dropped) as \"total_packets_dropped!: i64\"\n FROM (\n SELECT\n date_utc,\n n.total_stake,\n n.packets_received,\n n.packets_sent,\n n.packets_dropped\n FROM nym_node_daily_mixing_stats n\n )\n GROUP BY date_utc\n ORDER BY date_utc ASC\n ", "describe": { "columns": [ { @@ -33,12 +33,12 @@ "Left": [] }, "nullable": [ - null, + false, null, null, null, null ] }, - "hash": "124d45b9604439584650f401607c46bdbd162c7c689f74fe9ddfdfd48f5ddc07" + "hash": "32cf68dd34346fcac5cd0cd585845a9df296fdaebc5920dd1c12398e2967cffb" } diff --git a/nym-node-status-api/nym-node-status-api/.sqlx/query-74b76cd0d94c1afc51c21c14c12236a2b964b981c8cbcc282117fe9bc38338dd.json b/nym-node-status-api/nym-node-status-api/.sqlx/query-74b76cd0d94c1afc51c21c14c12236a2b964b981c8cbcc282117fe9bc38338dd.json deleted file mode 100644 index 7fa6221256..0000000000 --- a/nym-node-status-api/nym-node-status-api/.sqlx/query-74b76cd0d94c1afc51c21c14c12236a2b964b981c8cbcc282117fe9bc38338dd.json +++ /dev/null @@ -1,80 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT\n mn.mix_id as \"mix_id!\",\n mn.bonded as \"bonded: bool\",\n mn.is_dp_delegatee as \"is_dp_delegatee: bool\",\n mn.total_stake as \"total_stake!\",\n mn.full_details as \"full_details!\",\n mn.self_described as \"self_described\",\n mn.last_updated_utc as \"last_updated_utc!\",\n md.moniker as \"moniker!\",\n md.website as \"website!\",\n md.security_contact as \"security_contact!\",\n md.details as \"details!\"\n FROM mixnodes mn\n LEFT JOIN mixnode_description md ON mn.mix_id = md.mix_id\n ORDER BY mn.mix_id", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "mix_id!", - "type_info": "Int8" - }, - { - "ordinal": 1, - "name": "bonded: bool", - "type_info": "Bool" - }, - { - "ordinal": 2, - "name": "is_dp_delegatee: bool", - "type_info": "Bool" - }, - { - "ordinal": 3, - "name": "total_stake!", - "type_info": "Int8" - }, - { - "ordinal": 4, - "name": "full_details!", - "type_info": "Varchar" - }, - { - "ordinal": 5, - "name": "self_described", - "type_info": "Varchar" - }, - { - "ordinal": 6, - "name": "last_updated_utc!", - "type_info": "Int8" - }, - { - "ordinal": 7, - "name": "moniker!", - "type_info": "Varchar" - }, - { - "ordinal": 8, - "name": "website!", - "type_info": "Varchar" - }, - { - "ordinal": 9, - "name": "security_contact!", - "type_info": "Varchar" - }, - { - "ordinal": 10, - "name": "details!", - "type_info": "Varchar" - } - ], - "parameters": { - "Left": [] - }, - "nullable": [ - false, - false, - false, - false, - true, - true, - false, - true, - true, - true, - true - ] - }, - "hash": "74b76cd0d94c1afc51c21c14c12236a2b964b981c8cbcc282117fe9bc38338dd" -} 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 60c998d561..841ffd1e7d 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 = "3.3.0" +version = "3.3.2" authors.workspace = true repository.workspace = true homepage.workspace = true diff --git a/nym-node-status-api/nym-node-status-api/build.rs b/nym-node-status-api/nym-node-status-api/build.rs index a3b6635aef..f2fe512707 100644 --- a/nym-node-status-api/nym-node-status-api/build.rs +++ b/nym-node-status-api/nym-node-status-api/build.rs @@ -1,71 +1,13 @@ use anyhow::Result; -#[cfg(feature = "sqlite")] -use sqlx::{Connection, SqliteConnection}; -#[cfg(feature = "sqlite")] -#[cfg(target_family = "unix")] -use std::fs::Permissions; -#[cfg(feature = "sqlite")] -#[cfg(target_family = "unix")] -use std::os::unix::fs::PermissionsExt; -#[cfg(feature = "sqlite")] -use tokio::{fs::File, io::AsyncWriteExt}; - -#[cfg(feature = "sqlite")] -const SQLITE_DB_FILENAME: &str = "nym-node-status-api.sqlite"; - -#[cfg(feature = "sqlite")] -async fn init_db() -> Result<()> { - let out_dir = read_env_var("OUT_DIR")?; - let database_path = format!("{out_dir}/{SQLITE_DB_FILENAME}?mode=rwc"); - - write_db_path_to_file(&out_dir, SQLITE_DB_FILENAME).await?; - let mut conn = SqliteConnection::connect(&database_path).await?; - sqlx::migrate!("./migrations").run(&mut conn).await?; - - #[cfg(target_family = "unix")] - println!("cargo::rustc-env=DATABASE_URL=sqlite://{}", &database_path); - - #[cfg(target_family = "windows")] - // for some strange reason we need to add a leading `/` to the windows path even though it's - // not a valid windows path... but hey, it works... - println!("cargo::rustc-env=DATABASE_URL=sqlite:///{}", &database_path); - - Ok(()) -} /// If you need to re-run migrations or reset the db, just run /// cargo clean -p nym-node-status-api #[tokio::main(flavor = "current_thread")] async fn main() -> Result<()> { - #[cfg(feature = "sqlite")] - init_db().await?; - Ok(()) -} - -#[cfg(feature = "sqlite")] -fn read_env_var(var: &str) -> Result { - std::env::var(var).map_err(|_| anyhow::anyhow!("You need to set {} env var", var)) -} - -/// use `./enter_db.sh` to inspect DB -#[cfg(feature = "sqlite")] -async fn write_db_path_to_file(out_dir: &str, db_filename: &str) -> anyhow::Result<()> { - let mut file = File::create("settings.sql").await?; - let settings = ".mode columns -.headers on"; - file.write_all(settings.as_bytes()).await?; - - let mut file = File::create("enter_db.sh").await?; - let contents = format!( - "#!/bin/bash\n\ - sqlite3 -init settings.sql {out_dir}/{db_filename}", - ); - file.write_all(contents.as_bytes()).await?; - - #[cfg(target_family = "unix")] - file.set_permissions(Permissions::from_mode(0o755)) - .await - .map_err(anyhow::Error::from)?; + #[cfg(feature = "pg")] + if let Ok(database_url) = std::env::var("DATABASE_URL") { + println!("cargo::rustc-env=DATABASE_URL={database_url}"); + } Ok(()) } diff --git a/nym-node-status-api/nym-node-status-api/launch_node_status_api.sh b/nym-node-status-api/nym-node-status-api/launch_node_status_api.sh index 9f49c1e895..b5409280e5 100755 --- a/nym-node-status-api/nym-node-status-api/launch_node_status_api.sh +++ b/nym-node-status-api/nym-node-status-api/launch_node_status_api.sh @@ -23,7 +23,7 @@ function run_bare() { echo "RUST_LOG=${RUST_LOG}" # --conection-url is provided in build.rs - cargo run --package nym-node-status-api --no-default-features --features sqlite + cargo run --package nym-node-status-api --features pg --no-default-features } function run_docker() { 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 0fcd5adee4..e4138bf559 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,14 +4,17 @@ use crate::{ }; use anyhow::Context; use nym_contracts_common::Percent; +use nym_crypto::asymmetric::ed25519::serde_helpers::bs58_ed25519_pubkey; +use nym_crypto::asymmetric::x25519::serde_helpers::bs58_x25519_pubkey; use nym_crypto::asymmetric::{ed25519, x25519}; use nym_network_defaults::DEFAULT_NYM_NODE_HTTP_PORT; -use nym_node_requests::api::v1::node::models::NodeDescription; +use nym_node_requests::api::v1::node::models::{AuxiliaryDetails, NodeDescription}; use nym_validator_client::{ client::NymNodeDetails, models::NymNodeDescription, nym_api::SkimmedNode, }; use serde::{Deserialize, Serialize}; use sqlx::FromRow; +use std::net::IpAddr; use std::str::FromStr; use strum_macros::{EnumString, FromRepr}; use time::{Date, OffsetDateTime, UtcDateTime}; @@ -108,19 +111,6 @@ impl TryFrom for http::models::Gateway { } } -pub(crate) struct MixnodeRecord { - pub(crate) mix_id: u32, - pub(crate) identity_key: String, - pub(crate) bonded: bool, - pub(crate) total_stake: i64, - pub(crate) host: String, - pub(crate) http_port: u16, - pub(crate) full_details: String, - pub(crate) self_described: Option, - pub(crate) last_updated_utc: i64, - pub(crate) is_dp_delegatee: bool, -} - #[derive(Debug, Clone, FromRow)] pub(crate) struct MixnodeDto { pub(crate) mix_id: i64, @@ -212,7 +202,6 @@ impl TryFrom for SummaryHistory { } } -pub(crate) const MIXNODES_LEGACY_COUNT: &str = "mixnodes.legacy.count"; pub(crate) const NYMNODES_DESCRIBED_COUNT: &str = "nymnode.described.count"; pub(crate) const NYMNODE_COUNT: &str = "nymnode.total.count"; @@ -229,6 +218,13 @@ pub(crate) const GATEWAYS_HISTORICAL_COUNT: &str = "gateways.historical.count"; // have to import it use gateway::GatewaySummary; use mixnode::MixnodeSummary; +use nym_bin_common::build_information::BinaryBuildInformationOwned; +use nym_mixnet_contract_common::NodeId; +use nym_validator_client::models::{ + AuthenticatorDetails, DeclaredRoles, DescribedNodeType, HostInformation, HostKeys, + IpPacketRouterDetails, NetworkRequesterDetails, NymNodeData, OffsetDateTimeJsonSchemaWrapper, + SphinxKey, VersionedNoiseKey, WebSockets, WireguardDetails, +}; #[derive(Debug, Clone, Deserialize, Serialize, ToSchema)] pub(crate) struct NetworkSummary { @@ -250,7 +246,6 @@ pub(crate) mod mixnode { pub(crate) struct MixingNodesSummary { pub(crate) count: i32, pub(crate) self_described: i32, - pub(crate) legacy: i32, pub(crate) last_updated_utc: String, } @@ -364,7 +359,6 @@ impl TryFrom for http::models::SessionStats { #[derive(strum_macros::Display, Clone)] pub(crate) enum ScrapeNodeKind { - LegacyMixnode { mix_id: i64 }, MixingNymNode { node_id: i64 }, EntryExitNymNode { node_id: i64, identity_key: String }, } @@ -372,7 +366,6 @@ pub(crate) enum ScrapeNodeKind { impl ScrapeNodeKind { pub(crate) fn node_id(&self) -> &i64 { match self { - ScrapeNodeKind::LegacyMixnode { mix_id } => mix_id, ScrapeNodeKind::MixingNymNode { node_id } => node_id, ScrapeNodeKind::EntryExitNymNode { node_id, .. } => node_id, } @@ -528,3 +521,119 @@ pub struct InsertStatsRecord { pub unix_timestamp: i64, pub stats: NodeStats, } + +#[derive(Clone, Serialize, Deserialize, Debug)] +pub struct NymNodeDescriptionDeHelper { + pub node_id: NodeId, + pub contract_node_type: DescribedNodeType, + pub description: NymNodeDataDeHelper, +} + +#[allow(deprecated)] +impl From for NymNodeDescription { + fn from(helper: NymNodeDescriptionDeHelper) -> Self { + let current_x25519_sphinx_key = helper + .description + .host_information + .keys + .current_x25519_sphinx_key + .unwrap_or(SphinxKey { + // indicate the legacy case + rotation_id: u32::MAX, + public_key: helper.description.host_information.keys.x25519, + }); + + NymNodeDescription { + node_id: helper.node_id, + contract_node_type: helper.contract_node_type, + description: NymNodeData { + last_polled: helper.description.last_polled, + host_information: HostInformation { + ip_address: helper.description.host_information.ip_address, + hostname: helper.description.host_information.hostname, + keys: HostKeys { + ed25519: helper.description.host_information.keys.ed25519, + x25519: helper.description.host_information.keys.x25519, + current_x25519_sphinx_key, + pre_announced_x25519_sphinx_key: helper + .description + .host_information + .keys + .pre_announced_x25519_sphinx_key, + x25519_versioned_noise: helper + .description + .host_information + .keys + .x25519_versioned_noise, + }, + }, + declared_role: helper.description.declared_role, + auxiliary_details: helper.description.auxiliary_details, + build_information: helper.description.build_information, + network_requester: helper.description.network_requester, + ip_packet_router: helper.description.ip_packet_router, + authenticator: helper.description.authenticator, + wireguard: helper.description.wireguard, + mixnet_websockets: helper.description.mixnet_websockets, + }, + } + } +} + +#[derive(Clone, Serialize, Deserialize, Debug)] +pub struct NymNodeDataDeHelper { + #[serde(default)] + pub last_polled: OffsetDateTimeJsonSchemaWrapper, + + pub host_information: HostInformationDeHelper, + + #[serde(default)] + pub declared_role: DeclaredRoles, + + #[serde(default)] + pub auxiliary_details: AuxiliaryDetails, + + // TODO: do we really care about ALL build info or just the version? + pub build_information: BinaryBuildInformationOwned, + + #[serde(default)] + pub network_requester: Option, + + #[serde(default)] + pub ip_packet_router: Option, + + #[serde(default)] + pub authenticator: Option, + + #[serde(default)] + pub wireguard: Option, + + // for now we only care about their ws/wss situation, nothing more + pub mixnet_websockets: WebSockets, +} + +#[derive(Clone, Serialize, Deserialize, Debug)] +pub struct HostInformationDeHelper { + pub ip_address: Vec, + pub hostname: Option, + pub keys: HostKeysDeHelper, +} + +#[derive(Clone, Serialize, Deserialize, Debug)] +pub struct HostKeysDeHelper { + #[serde(with = "bs58_ed25519_pubkey")] + pub ed25519: ed25519::PublicKey, + + #[deprecated(note = "use the current_x25519_sphinx_key with explicit rotation information")] + #[serde(with = "bs58_x25519_pubkey")] + pub x25519: x25519::PublicKey, + + // legacy data will NOT have this information + pub current_x25519_sphinx_key: Option, + + #[serde(default)] + pub pre_announced_x25519_sphinx_key: Option, + + #[serde(default)] + pub x25519_versioned_noise: Option, +} diff --git a/nym-node-status-api/nym-node-status-api/src/db/queries/mixnodes.rs b/nym-node-status-api/nym-node-status-api/src/db/queries/mixnodes.rs deleted file mode 100644 index 2f53e52857..0000000000 --- a/nym-node-status-api/nym-node-status-api/src/db/queries/mixnodes.rs +++ /dev/null @@ -1,192 +0,0 @@ -use std::collections::HashSet; - -use futures_util::TryStreamExt; -use sqlx::Row; -use tracing::error; - -use crate::{ - db::{ - models::{MixnodeDto, MixnodeRecord}, - DbConnection, DbPool, - }, - http::models::{DailyStats, Mixnode}, - node_scraper::helpers::NodeDescriptionResponse, -}; - -pub(crate) async fn update_mixnodes( - pool: &DbPool, - mixnodes: Vec, -) -> anyhow::Result<()> { - let mut tx = pool.begin().await?; - - // mark all as unbonded - crate::db::query( - r#"UPDATE - mixnodes - SET - bonded = false - "#, - ) - .execute(&mut *tx) - .await?; - - // existing nodes get updated on insert - for record in mixnodes.into_iter() { - // https://www.sqlite.org/lang_upsert.html - crate::db::query( - "INSERT INTO mixnodes - (mix_id, identity_key, bonded, total_stake, - host, http_api_port, full_details, - self_described, last_updated_utc, is_dp_delegatee) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - ON CONFLICT(mix_id) DO UPDATE SET - bonded=excluded.bonded, - total_stake=excluded.total_stake, host=excluded.host, - http_api_port=excluded.http_api_port, - full_details=excluded.full_details,self_described=excluded.self_described, - last_updated_utc=excluded.last_updated_utc, - is_dp_delegatee = excluded.is_dp_delegatee;", - ) - .bind(record.mix_id as i64) - .bind(record.identity_key) - .bind(record.bonded) - .bind(record.total_stake) - .bind(record.host) - .bind(record.http_port as i32) - .bind(record.full_details) - .bind(record.self_described) - .bind(record.last_updated_utc) - .bind(record.is_dp_delegatee) - .execute(&mut *tx) - .await?; - } - - tx.commit().await?; - - Ok(()) -} - -pub(crate) async fn get_all_mixnodes(pool: &DbPool) -> anyhow::Result> { - let mut conn = pool.acquire().await?; - let items = sqlx::query_as!( - MixnodeDto, - r#"SELECT - mn.mix_id as "mix_id!", - mn.bonded as "bonded: bool", - mn.is_dp_delegatee as "is_dp_delegatee: bool", - mn.total_stake as "total_stake!", - mn.full_details as "full_details!", - mn.self_described as "self_described", - mn.last_updated_utc as "last_updated_utc!", - md.moniker as "moniker!", - md.website as "website!", - md.security_contact as "security_contact!", - md.details as "details!" - FROM mixnodes mn - LEFT JOIN mixnode_description md ON mn.mix_id = md.mix_id - ORDER BY mn.mix_id"# - ) - .fetch(&mut *conn) - .try_collect::>() - .await?; - - let items = items - .into_iter() - .map(|item| item.try_into()) - .collect::>>() - .map_err(|e| { - error!("Conversion from DTO failed: {e}. Invalidly stored data?"); - e - })?; - Ok(items) -} - -pub(crate) async fn get_daily_stats(pool: &DbPool) -> anyhow::Result> { - let mut conn = pool.acquire().await?; - let items = sqlx::query_as!( - DailyStats, - r#" - SELECT - date_utc as "date_utc!", - SUM(total_stake) as "total_stake!: i64", - SUM(packets_received) as "total_packets_received!: i64", - SUM(packets_sent) as "total_packets_sent!: i64", - SUM(packets_dropped) as "total_packets_dropped!: i64" - FROM ( - SELECT - date_utc, - n.total_stake, - n.packets_received, - n.packets_sent, - n.packets_dropped - FROM nym_node_daily_mixing_stats n - UNION ALL - SELECT - m.date_utc, - m.total_stake, - m.packets_received, - m.packets_sent, - m.packets_dropped - FROM mixnode_daily_stats m - LEFT JOIN nym_node_daily_mixing_stats ON m.mix_id = nym_node_daily_mixing_stats.node_id - WHERE nym_node_daily_mixing_stats.node_id IS NULL - ) - GROUP BY date_utc - ORDER BY date_utc ASC - "#, - ) - .fetch(&mut *conn) - .try_collect::>() - .await?; - - Ok(items) -} - -pub(crate) async fn get_bonded_mix_ids(pool: &DbPool) -> anyhow::Result> { - let mut conn = pool.acquire().await?; - let items = crate::db::query( - r#" - SELECT mix_id - FROM mixnodes - WHERE bonded = true - "#, - ) - .fetch_all(&mut *conn) - .await? - .into_iter() - .map(|record| record.try_get::("mix_id").unwrap()) - .collect::>(); - - Ok(items) -} - -pub(crate) async fn insert_mixnode_description( - conn: &mut DbConnection, - mix_id: i64, - description: NodeDescriptionResponse, - timestamp: i64, -) -> anyhow::Result<()> { - crate::db::query( - r#" - INSERT INTO mixnode_description ( - mix_id, moniker, website, security_contact, details, last_updated_utc - ) VALUES (?, ?, ?, ?, ?, ?) - ON CONFLICT (mix_id) DO UPDATE SET - moniker = excluded.moniker, - website = excluded.website, - security_contact = excluded.security_contact, - details = excluded.details, - last_updated_utc = excluded.last_updated_utc - "#, - ) - .bind(mix_id) - .bind(description.moniker) - .bind(description.website) - .bind(description.security_contact) - .bind(description.details) - .bind(timestamp) - .execute(conn.as_mut()) - .await - .map(drop) - .map_err(From::from) -} 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 012d32ae24..d097bba29f 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,7 +1,6 @@ mod gateways; mod gateways_stats; mod misc; -mod mixnodes; mod nym_nodes; mod packet_stats; pub(crate) mod scraper; @@ -14,10 +13,10 @@ pub(crate) use gateways::{ }; pub(crate) use gateways_stats::{delete_old_records, get_sessions_stats, insert_session_records}; pub(crate) use misc::insert_summaries; -pub(crate) use mixnodes::{get_all_mixnodes, get_bonded_mix_ids, get_daily_stats, update_mixnodes}; pub(crate) use nym_nodes::{ - get_all_nym_nodes, get_bonded_node_description, get_described_bonded_nym_nodes, - get_described_node_bond_info, get_node_self_description, update_nym_nodes, + get_all_nym_nodes, get_bonded_node_description, get_daily_stats, + get_described_bonded_nym_nodes, get_described_node_bond_info, get_node_self_description, + update_nym_nodes, }; pub(crate) use packet_stats::{ batch_store_packet_stats, get_raw_node_stats, insert_daily_node_stats_uncommitted, diff --git a/nym-node-status-api/nym-node-status-api/src/db/queries/nym_nodes.rs b/nym-node-status-api/nym-node-status-api/src/db/queries/nym_nodes.rs index 0a0afc1893..4144e791a7 100644 --- a/nym-node-status-api/nym-node-status-api/src/db/queries/nym_nodes.rs +++ b/nym-node-status-api/nym-node-status-api/src/db/queries/nym_nodes.rs @@ -6,8 +6,10 @@ use nym_validator_client::{ }; use sqlx::Row; use std::collections::HashMap; -use tracing::instrument; +use tracing::{instrument, warn}; +use crate::db::models::NymNodeDescriptionDeHelper; +use crate::http::models::DailyStats; use crate::{ db::{ models::{NymNodeDto, NymNodeInsertRecord}, @@ -214,9 +216,12 @@ pub(crate) async fn get_node_self_description( .filter_map(|record| { let node_id: i32 = record.try_get("node_id").ok()?; let self_described: serde_json::Value = record.try_get("self_described").ok()?; - serde_json::from_value::(self_described) - .ok() - .map(|res| (node_id as i64 as NodeId, res)) + + let val = serde_json::from_value::(self_described) + .inspect_err(|err| { + warn!("malformed description data for node {node_id}: {err}") + }); + val.ok().map(|res| (node_id as NodeId, res.into())) }) .collect::>() }) @@ -249,7 +254,7 @@ pub(crate) async fn get_bonded_node_description( records .into_iter() .map(|elem| { - let node_id: i64 = elem.try_get("node_id").unwrap_or(0); + let node_id: i32 = elem.try_get("node_id").unwrap_or(0); let node_id: NodeId = node_id.try_into().unwrap_or_default(); ( node_id, @@ -296,3 +301,34 @@ pub(crate) async fn insert_nym_node_description( .map(drop) .map_err(From::from) } + +pub(crate) async fn get_daily_stats(pool: &DbPool) -> anyhow::Result> { + let mut conn = pool.acquire().await?; + let items = sqlx::query_as!( + DailyStats, + r#" + SELECT + date_utc as "date_utc!", + SUM(total_stake) as "total_stake!: i64", + SUM(packets_received) as "total_packets_received!: i64", + SUM(packets_sent) as "total_packets_sent!: i64", + SUM(packets_dropped) as "total_packets_dropped!: i64" + FROM ( + SELECT + date_utc, + n.total_stake, + n.packets_received, + n.packets_sent, + n.packets_dropped + FROM nym_node_daily_mixing_stats n + ) + GROUP BY date_utc + ORDER BY date_utc ASC + "#, + ) + .fetch(&mut *conn) + .try_collect::>() + .await?; + + Ok(items) +} diff --git a/nym-node-status-api/nym-node-status-api/src/db/queries/packet_stats.rs b/nym-node-status-api/nym-node-status-api/src/db/queries/packet_stats.rs index fed7f8dc49..5862a723d4 100644 --- a/nym-node-status-api/nym-node-status-api/src/db/queries/packet_stats.rs +++ b/nym-node-status-api/nym-node-status-api/src/db/queries/packet_stats.rs @@ -67,22 +67,6 @@ pub(crate) async fn insert_node_packet_stats_uncommitted( timestamp_utc: i64, ) -> Result<()> { match node_kind { - ScrapeNodeKind::LegacyMixnode { mix_id } => { - sqlx::query( - r#" - INSERT INTO mixnode_packet_stats_raw ( - mix_id, timestamp_utc, packets_received, packets_sent, packets_dropped - ) VALUES (?, ?, ?, ?, ?) - "#, - ) - .bind(mix_id) - .bind(timestamp_utc) - .bind(stats.packets_received) - .bind(stats.packets_sent) - .bind(stats.packets_dropped) - .execute(tx.as_mut()) - .await?; - } ScrapeNodeKind::MixingNymNode { node_id } | ScrapeNodeKind::EntryExitNymNode { node_id, .. } => { sqlx::query( @@ -113,22 +97,6 @@ pub(crate) async fn insert_node_packet_stats_uncommitted( timestamp_utc: i64, ) -> Result<()> { match node_kind { - ScrapeNodeKind::LegacyMixnode { mix_id } => { - sqlx::query( - r#" - INSERT INTO mixnode_packet_stats_raw ( - mix_id, timestamp_utc, packets_received, packets_sent, packets_dropped - ) VALUES ($1, $2, $3, $4, $5) - "#, - ) - .bind(mix_id) - .bind(timestamp_utc) - .bind(stats.packets_received) - .bind(stats.packets_sent) - .bind(stats.packets_dropped) - .execute(tx.as_mut()) - .await?; - } ScrapeNodeKind::MixingNymNode { node_id } | ScrapeNodeKind::EntryExitNymNode { node_id, .. } => { sqlx::query( @@ -159,23 +127,6 @@ pub(crate) async fn get_raw_node_stats( let packets = match node_kind { // if no packets are found, it's fine to assume 0 because that's also // SQL default value if none provided - ScrapeNodeKind::LegacyMixnode { mix_id } => { - sqlx::query_as::<_, NodeStats>( - r#" - SELECT - COALESCE(packets_received, 0) as packets_received, - COALESCE(packets_sent, 0) as packets_sent, - COALESCE(packets_dropped, 0) as packets_dropped - FROM mixnode_packet_stats_raw - WHERE mix_id = ? - ORDER BY timestamp_utc DESC - LIMIT 1 OFFSET 1 - "#, - ) - .bind(mix_id) - .fetch_optional(tx.as_mut()) - .await? - } ScrapeNodeKind::MixingNymNode { node_id } | ScrapeNodeKind::EntryExitNymNode { node_id, .. } => { sqlx::query_as::<_, NodeStats>( @@ -207,23 +158,6 @@ pub(crate) async fn get_raw_node_stats( let packets = match node_kind { // if no packets are found, it's fine to assume 0 because that's also // SQL default value if none provided - ScrapeNodeKind::LegacyMixnode { mix_id } => { - sqlx::query_as::<_, NodeStats>( - r#" - SELECT - COALESCE(packets_received, 0) as packets_received, - COALESCE(packets_sent, 0) as packets_sent, - COALESCE(packets_dropped, 0) as packets_dropped - FROM mixnode_packet_stats_raw - WHERE mix_id = $1 - ORDER BY timestamp_utc DESC - LIMIT 1 OFFSET 1 - "#, - ) - .bind(mix_id) - .fetch_optional(tx.as_mut()) - .await? - } ScrapeNodeKind::MixingNymNode { node_id } | ScrapeNodeKind::EntryExitNymNode { node_id, .. } => { sqlx::query_as::<_, NodeStats>( @@ -255,42 +189,6 @@ pub(crate) async fn insert_daily_node_stats_uncommitted( packets: NodeStats, ) -> Result<()> { match node_kind { - ScrapeNodeKind::LegacyMixnode { mix_id } => { - let total_stake = sqlx::query_scalar::<_, i64>( - r#" - SELECT - total_stake - FROM mixnodes - WHERE mix_id = ? - "#, - ) - .bind(mix_id) - .fetch_one(tx.as_mut()) - .await?; - - sqlx::query( - r#" - INSERT INTO mixnode_daily_stats ( - mix_id, date_utc, - total_stake, packets_received, - packets_sent, packets_dropped - ) VALUES (?, ?, ?, ?, ?, ?) - ON CONFLICT(mix_id, date_utc) DO UPDATE SET - total_stake = excluded.total_stake, - packets_received = mixnode_daily_stats.packets_received + excluded.packets_received, - packets_sent = mixnode_daily_stats.packets_sent + excluded.packets_sent, - packets_dropped = mixnode_daily_stats.packets_dropped + excluded.packets_dropped - "#, - ) - .bind(mix_id) - .bind(date_utc) - .bind(total_stake) - .bind(packets.packets_received) - .bind(packets.packets_sent) - .bind(packets.packets_dropped) - .execute(tx.as_mut()) - .await?; - } ScrapeNodeKind::MixingNymNode { node_id } | ScrapeNodeKind::EntryExitNymNode { node_id, .. } => { let total_stake = sqlx::query_scalar::<_, i64>( @@ -341,42 +239,6 @@ pub(crate) async fn insert_daily_node_stats_uncommitted( packets: NodeStats, ) -> Result<()> { match node_kind { - ScrapeNodeKind::LegacyMixnode { mix_id } => { - let total_stake = sqlx::query_scalar::<_, i64>( - r#" - SELECT - total_stake - FROM mixnodes - WHERE mix_id = $1 - "#, - ) - .bind(mix_id) - .fetch_one(tx.as_mut()) - .await?; - - sqlx::query( - r#" - INSERT INTO mixnode_daily_stats ( - mix_id, date_utc, - total_stake, packets_received, - packets_sent, packets_dropped - ) VALUES ($1, $2, $3, $4, $5, $6) - ON CONFLICT(mix_id, date_utc) DO UPDATE SET - total_stake = excluded.total_stake, - packets_received = mixnode_daily_stats.packets_received + excluded.packets_received, - packets_sent = mixnode_daily_stats.packets_sent + excluded.packets_sent, - packets_dropped = mixnode_daily_stats.packets_dropped + excluded.packets_dropped - "#, - ) - .bind(mix_id) - .bind(date_utc) - .bind(total_stake) - .bind(packets.packets_received) - .bind(packets.packets_sent) - .bind(packets.packets_dropped) - .execute(tx.as_mut()) - .await?; - } ScrapeNodeKind::MixingNymNode { node_id } | ScrapeNodeKind::EntryExitNymNode { node_id, .. } => { let total_stake = sqlx::query_scalar::<_, i64>( diff --git a/nym-node-status-api/nym-node-status-api/src/db/queries/scraper.rs b/nym-node-status-api/nym-node-status-api/src/db/queries/scraper.rs index efecd3c83b..d72d0c870d 100644 --- a/nym-node-status-api/nym-node-status-api/src/db/queries/scraper.rs +++ b/nym-node-status-api/nym-node-status-api/src/db/queries/scraper.rs @@ -2,8 +2,7 @@ use crate::{ db::{ models::{ScrapeNodeKind, ScraperNodeInfo}, queries::{ - self, gateways::insert_gateway_description, mixnodes::insert_mixnode_description, - nym_nodes::insert_nym_node_description, + self, gateways::insert_gateway_description, nym_nodes::insert_nym_node_description, }, DbPool, }, @@ -12,12 +11,10 @@ use crate::{ }; use anyhow::Result; use nym_validator_client::nym_api::SkimmedNode; -use sqlx::Row; pub(crate) async fn get_nodes_for_scraping(pool: &DbPool) -> Result> { let mut nodes_to_scrape = Vec::new(); - let mixnode_ids = queries::get_bonded_mix_ids(pool).await?; let gateway_keys = queries::get_bonded_gateway_id_keys(pool).await?; let mut entry_exit_nodes = 0; @@ -39,11 +36,7 @@ pub(crate) async fn get_nodes_for_scraping(pool: &DbPool) -> Result Result { - insert_mixnode_description(&mut conn, mix_id, description, timestamp).await?; - } ScrapeNodeKind::MixingNymNode { node_id } => { insert_nym_node_description(&mut conn, node_id, description, timestamp).await?; } 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 3695c4481b..d0efb9a406 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 @@ -9,8 +9,8 @@ use crate::{ mixnode::{MixingNodesSummary, MixnodeSummary, MixnodeSummaryHistorical}, NetworkSummary, SummaryDto, SummaryHistoryDto, ASSIGNED_ENTRY_COUNT, ASSIGNED_EXIT_COUNT, ASSIGNED_MIXING_COUNT, GATEWAYS_BONDED_COUNT, - GATEWAYS_HISTORICAL_COUNT, MIXNODES_HISTORICAL_COUNT, MIXNODES_LEGACY_COUNT, - NYMNODES_DESCRIBED_COUNT, NYMNODE_COUNT, + GATEWAYS_HISTORICAL_COUNT, MIXNODES_HISTORICAL_COUNT, NYMNODES_DESCRIBED_COUNT, + NYMNODE_COUNT, }, DbPool, }, @@ -81,7 +81,6 @@ async fn from_summary_dto(items: Vec) -> HttpResult let keys = [ NYMNODE_COUNT, ASSIGNED_MIXING_COUNT, - MIXNODES_LEGACY_COUNT, NYMNODES_DESCRIBED_COUNT, GATEWAYS_BONDED_COUNT, ASSIGNED_ENTRY_COUNT, @@ -113,8 +112,7 @@ async fn from_summary_dto(items: Vec) -> HttpResult .get(NYMNODES_DESCRIBED_COUNT) .cloned() .unwrap_or_default(); - let legacy_mixnodes_count: SummaryDto = - map.get(MIXNODES_LEGACY_COUNT).cloned().unwrap_or_default(); + let gateways_bonded_count: SummaryDto = map.get(GATEWAYS_BONDED_COUNT).cloned().unwrap_or_default(); let mixnodes_historical_count: SummaryDto = map @@ -132,7 +130,6 @@ async fn from_summary_dto(items: Vec) -> HttpResult bonded: MixingNodesSummary { count: to_count_i32(&assigned_mixing_count), self_described: to_count_i32(&self_described), - legacy: to_count_i32(&legacy_mixnodes_count), last_updated_utc: to_timestamp(&assigned_mixing_count), }, historical: MixnodeSummaryHistorical { diff --git a/nym-node-status-api/nym-node-status-api/src/db/tests.rs b/nym-node-status-api/nym-node-status-api/src/db/tests.rs index a176e6d340..c6917733d1 100644 --- a/nym-node-status-api/nym-node-status-api/src/db/tests.rs +++ b/nym-node-status-api/nym-node-status-api/src/db/tests.rs @@ -380,9 +380,6 @@ fn test_scraper_node_info_contact_addresses() { fn test_scrape_node_kind_node_id() { use crate::db::models::ScrapeNodeKind; - let legacy = ScrapeNodeKind::LegacyMixnode { mix_id: 42 }; - assert_eq!(*legacy.node_id(), 42); - let mixing = ScrapeNodeKind::MixingNymNode { node_id: 123 }; assert_eq!(*mixing.node_id(), 123); diff --git a/nym-node-status-api/nym-node-status-api/src/http/api/mixnodes.rs b/nym-node-status-api/nym-node-status-api/src/http/api/mixnodes.rs index ed18bcdb6a..52f3f6cbf9 100644 --- a/nym-node-status-api/nym-node-status-api/src/http/api/mixnodes.rs +++ b/nym-node-status-api/nym-node-status-api/src/http/api/mixnodes.rs @@ -1,5 +1,5 @@ use axum::{ - extract::{Path, Query, State}, + extract::{Query, State}, Json, Router, }; use serde::Deserialize; @@ -8,67 +8,12 @@ use utoipa::IntoParams; use crate::http::{ error::{HttpError, HttpResult}, - models::{DailyStats, Mixnode}, + models::DailyStats, state::AppState, - PagedResult, Pagination, }; pub(crate) fn routes() -> Router { - Router::new() - .route("/", axum::routing::get(mixnodes)) - .route("/:mix_id", axum::routing::get(get_mixnodes)) - .route("/stats", axum::routing::get(get_stats)) -} - -#[utoipa::path( - tag = "Mixnodes", - get, - params( - Pagination - ), - path = "/v2/mixnodes", - responses( - (status = 200, body = PagedResult) - ) -)] -#[instrument(level = tracing::Level::DEBUG, skip_all, fields(page=pagination.page, size=pagination.size))] -async fn mixnodes( - Query(pagination): Query, - State(state): State, -) -> HttpResult>> { - let db = state.db_pool(); - let res = state.cache().get_mixnodes_list(db).await; - - Ok(Json(PagedResult::paginate(pagination, res))) -} - -#[derive(Deserialize, IntoParams)] -#[into_params(parameter_in = Path)] -struct MixIdParam { - mix_id: String, -} - -#[utoipa::path( - tag = "Mixnodes", - get, - params( - MixIdParam - ), - path = "/v2/mixnodes/{mix_id}", - responses( - (status = 200, body = Mixnode) - ) -)] -#[instrument(level = tracing::Level::DEBUG, skip_all, fields(mix_id = mix_id))] -async fn get_mixnodes( - Path(MixIdParam { mix_id }): Path, - State(state): State, -) -> HttpResult> { - find_mixnode_by_id( - &mix_id, - state.cache().get_mixnodes_list(state.db_pool()).await, - ) - .map(Json) + Router::new().route("/stats", axum::routing::get(get_stats)) } #[derive(Deserialize, IntoParams)] @@ -102,17 +47,6 @@ async fn get_stats( Ok(Json(last_30_days)) } -// Extract business logic for testing -fn find_mixnode_by_id(mix_id: &str, mixnodes: Vec) -> HttpResult { - match mix_id.parse::() { - Ok(parsed_mix_id) => mixnodes - .into_iter() - .find(|item| item.mix_id == parsed_mix_id) - .ok_or_else(|| HttpError::invalid_input(mix_id)), - Err(_e) => Err(HttpError::invalid_input(mix_id)), - } -} - fn validate_offset(offset: Option) -> HttpResult { offset .unwrap_or(0) @@ -123,26 +57,7 @@ fn validate_offset(offset: Option) -> HttpResult { #[cfg(test)] mod tests { use super::*; - use crate::http::models::{DailyStats, Mixnode}; - use nym_node_requests::api::v1::node::models::NodeDescription; - - fn create_test_mixnode(mix_id: u32, is_dp_delegatee: bool) -> Mixnode { - Mixnode { - mix_id, - bonded: true, - is_dp_delegatee, - total_stake: 100000, - full_details: Some(serde_json::json!({"test": "data"})), - self_described: Some(serde_json::json!({"version": "1.0"})), - description: NodeDescription { - moniker: format!("Mixnode {mix_id}"), - website: "".to_string(), - security_contact: "".to_string(), - details: "".to_string(), - }, - last_updated_utc: "2024-01-20T10:00:00Z".to_string(), - } - } + use crate::http::models::DailyStats; #[test] fn test_routes_construction() { @@ -152,49 +67,6 @@ mod tests { let _routes = router; } - #[test] - fn test_find_mixnode_by_id_success() { - let mixnodes = vec![ - create_test_mixnode(1, false), - create_test_mixnode(42, true), - create_test_mixnode(100, false), - ]; - - let result = find_mixnode_by_id("42", mixnodes).unwrap(); - assert_eq!(result.mix_id, 42); - assert!(result.is_dp_delegatee); - } - - #[test] - fn test_find_mixnode_by_id_not_found() { - let mixnodes = vec![create_test_mixnode(1, false), create_test_mixnode(2, false)]; - - let result = find_mixnode_by_id("99", mixnodes); - assert!(result.is_err()); - } - - #[test] - fn test_find_mixnode_by_id_invalid_format() { - let mixnodes = vec![create_test_mixnode(1, false)]; - - // Test various invalid formats - assert!(find_mixnode_by_id("abc", mixnodes.clone()).is_err()); - assert!(find_mixnode_by_id("", mixnodes.clone()).is_err()); - assert!(find_mixnode_by_id("12.34", mixnodes.clone()).is_err()); - assert!(find_mixnode_by_id("-1", mixnodes).is_err()); - } - - #[test] - fn test_find_mixnode_by_id_edge_cases() { - let mixnodes = vec![ - create_test_mixnode(0, false), - create_test_mixnode(u32::MAX, false), - ]; - - assert!(find_mixnode_by_id("0", mixnodes.clone()).is_ok()); - assert!(find_mixnode_by_id(&u32::MAX.to_string(), mixnodes).is_ok()); - } - #[test] fn test_validate_offset_valid() { assert_eq!(validate_offset(None).unwrap(), 0); @@ -210,13 +82,6 @@ mod tests { assert!(validate_offset(Some(i64::MIN)).is_err()); } - #[test] - fn test_mix_id_param_deserialization() { - let json = r#"{"mix_id": "123"}"#; - let param: MixIdParam = serde_json::from_str(json).unwrap(); - assert_eq!(param.mix_id, "123"); - } - #[test] fn test_mix_stats_query_params_deserialization() { let json = r#"{"offset": 50}"#; 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 f665067814..9ff469eee2 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 @@ -6,9 +6,7 @@ use nym_mixnet_contract_common::CoinSchema; use nym_node_requests::api::v1::node::models::NodeDescription; use nym_validator_client::{ client::NodeId, - models::{ - AuthenticatorDetails, BinaryBuildInformationOwned, IpPacketRouterDetails, NymNodeData, - }, + models::{AuthenticatorDetails, BinaryBuildInformationOwned, IpPacketRouterDetails}, nym_api::SkimmedNode, nym_nodes::{BasicEntryInformation, NodeRole}, }; @@ -16,6 +14,7 @@ use serde::{Deserialize, Serialize}; use tracing::{error, instrument}; use utoipa::ToSchema; +use crate::db::models::NymNodeDataDeHelper; pub(crate) use nym_node_status_client::models::TestrunAssignment; #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] @@ -188,10 +187,12 @@ impl DVpnGateway { }) .map(|bond| bond.location)?; - let self_described = gateway + let self_described: NymNodeDataDeHelper = gateway .self_described .ok_or_else(|| anyhow::anyhow!("Missing self_described")) - .and_then(|value| serde_json::from_value::(value).map_err(From::from))?; + .and_then(|value| { + serde_json::from_value::(value).map_err(From::from) + })?; let last_probe_result = match gateway.last_probe_result { Some(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 35a3799202..effaf517fe 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 @@ -20,9 +20,7 @@ use crate::{ db::{queries, DbPool}, http::{ error::{HttpError, HttpResult}, - models::{ - DVpnGateway, DailyStats, ExtendedNymNode, Gateway, Mixnode, NodeGeoData, SummaryHistory, - }, + models::{DVpnGateway, DailyStats, ExtendedNymNode, Gateway, NodeGeoData, SummaryHistory}, }, monitor::{DelegationsCache, NodeGeoCache}, }; @@ -143,7 +141,6 @@ impl AppState { static GATEWAYS_LIST_KEY: &str = "gateways"; static DVPN_GATEWAYS_LIST_KEY: &str = "dvpn_gateways"; -static MIXNODES_LIST_KEY: &str = "mixnodes"; static NYM_NODES_LIST_KEY: &str = "nym_nodes"; static MIXSTATS_LIST_KEY: &str = "mixstats"; static SUMMARY_HISTORY_LIST_KEY: &str = "summary-history"; @@ -155,7 +152,6 @@ const MIXNODE_STATS_HISTORY_DAYS: usize = 30; pub(crate) struct HttpCache { gateways: Cache>>>, dvpn_gateways: Cache>>>, - mixnodes: Cache>>>, nym_nodes: Cache>>>, mixstats: Cache>>>, history: Cache>>>, @@ -173,10 +169,6 @@ impl HttpCache { .max_capacity(6) .time_to_live(Duration::from_secs(ttl_seconds)) .build(), - mixnodes: Cache::builder() - .max_capacity(2) - .time_to_live(Duration::from_secs(ttl_seconds)) - .build(), nym_nodes: Cache::builder() .max_capacity(2) .time_to_live(Duration::from_secs(ttl_seconds)) @@ -426,50 +418,6 @@ impl HttpCache { .collect() } - pub async fn upsert_mixnode_list( - &self, - new_mixnode_list: Vec, - ) -> Entry>>> { - self.mixnodes - .entry_by_ref(MIXNODES_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 = new_mixnode_list; - v.clone() - } else { - Arc::new(RwLock::new(new_mixnode_list)) - } - }) - .await - } - - pub async fn get_mixnodes_list(&self, db: &DbPool) -> Vec { - match self.mixnodes.get(MIXNODES_LIST_KEY).await { - Some(guard) => { - tracing::trace!("Fetching from cache..."); - let read_lock = guard.read().await; - read_lock.clone() - } - None => { - tracing::trace!("No mixnodes in cache, refreshing cache from DB..."); - - let mixnodes = crate::db::queries::get_all_mixnodes(db) - .await - .unwrap_or_default(); - - if mixnodes.is_empty() { - tracing::warn!("Database contains 0 mixnodes"); - } else { - self.upsert_mixnode_list(mixnodes.clone()).await; - } - - mixnodes - } - } - } - pub async fn upsert_nym_node_list( &self, nym_node_list: Vec, diff --git a/nym-node-status-api/nym-node-status-api/src/metrics_scraper/error.rs b/nym-node-status-api/nym-node-status-api/src/metrics_scraper/error.rs index 99cff49a06..86d28d7d08 100644 --- a/nym-node-status-api/nym-node-status-api/src/metrics_scraper/error.rs +++ b/nym-node-status-api/nym-node-status-api/src/metrics_scraper/error.rs @@ -10,7 +10,7 @@ pub enum NodeScraperError { host: String, node_id: NodeId, #[source] - source: NymNodeApiClientError, + source: Box, }, #[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)] @@ -22,14 +22,19 @@ mod tests { use super::*; use std::error::Error; + fn dummy_url() -> reqwest::Url { + "http://nym.com".parse().unwrap() + } + #[test] + #[allow(deprecated)] fn test_malformed_host_error() { // Create a generic error to test with let source_error = NymNodeApiClientError::GenericRequestFailure("Invalid URL".to_string()); let error = NodeScraperError::MalformedHost { host: "invalid-host:abc".to_string(), node_id: 42, - source: source_error, + source: Box::new(source_error), }; // Test error message formatting @@ -43,12 +48,13 @@ mod tests { } #[test] + #[allow(deprecated)] fn test_malformed_host_error_edge_cases() { // Test with empty host let error = NodeScraperError::MalformedHost { host: "".to_string(), node_id: 0, - source: NymNodeApiClientError::NotFound, + source: Box::new(NymNodeApiClientError::NotFound { url: dummy_url() }), }; let error_msg = error.to_string(); @@ -59,7 +65,9 @@ mod tests { let error = NodeScraperError::MalformedHost { host: long_host.clone(), node_id: u32::MAX, - source: NymNodeApiClientError::GenericRequestFailure("Too long".to_string()), + source: Box::new(NymNodeApiClientError::GenericRequestFailure( + "Too long".to_string(), + )), }; let error_msg = error.to_string(); @@ -98,20 +106,21 @@ mod tests { } #[test] + #[allow(deprecated)] fn test_error_different_sources() { // Test with different NymNodeApiClientError variants - let not_found_error = NymNodeApiClientError::NotFound; + let not_found_error = NymNodeApiClientError::NotFound { url: dummy_url() }; let error1 = NodeScraperError::MalformedHost { host: "host1".to_string(), node_id: 1, - source: not_found_error, + source: Box::new(not_found_error), }; let generic_error = NymNodeApiClientError::GenericRequestFailure("404 error".to_string()); let error2 = NodeScraperError::MalformedHost { host: "host2".to_string(), node_id: 2, - source: generic_error, + source: Box::new(generic_error), }; // Both should format differently based on their source diff --git a/nym-node-status-api/nym-node-status-api/src/metrics_scraper/mod.rs b/nym-node-status-api/nym-node-status-api/src/metrics_scraper/mod.rs index bc3cd23fe7..8766141737 100644 --- a/nym-node-status-api/nym-node-status-api/src/metrics_scraper/mod.rs +++ b/nym-node-status-api/nym-node-status-api/src/metrics_scraper/mod.rs @@ -5,11 +5,11 @@ use nym_node_requests::api::{client::NymNodeApiClientExt, v1::metrics::models::S use nym_validator_client::{ client::{NodeId, NymNodeDetails}, models::{DescribedNodeType, NymNodeDescription}, - NymApiClient, }; use time::OffsetDateTime; use nym_statistics_common::types::SessionType; +use nym_validator_client::client::NymApiClientExt; use std::collections::HashMap; use tokio::time::Duration; use tracing::instrument; @@ -60,13 +60,11 @@ async fn run( let nym_api = nym_http_api_client::ClientBuilder::new_with_urls(vec![default_api_url.into()]) .no_hickory_dns() .with_timeout(nym_api_client_timeout) - .build::<&str>()?; - - let api_client = NymApiClient::from(nym_api); + .build()?; //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 + let bonded_nodes = nym_api.get_all_bonded_nym_nodes().await?; + let all_nodes = nym_api.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 = bonded_nodes @@ -183,7 +181,7 @@ impl MetricsScrapingData { return Err(NodeScraperError::MalformedHost { host: self.host.to_string(), node_id: self.node_id, - source: err, + source: Box::new(err), }); } }; 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 d41d0cedc9..8df1bb3e7f 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 @@ -1,31 +1,24 @@ #![allow(deprecated)] use crate::db::models::{ - gateway, mixnode, GatewayInsertRecord, MixnodeRecord, NetworkSummary, NymNodeInsertRecord, + gateway, mixnode, GatewayInsertRecord, NetworkSummary, NymNodeInsertRecord, ASSIGNED_ENTRY_COUNT, ASSIGNED_EXIT_COUNT, ASSIGNED_MIXING_COUNT, GATEWAYS_BONDED_COUNT, - GATEWAYS_HISTORICAL_COUNT, MIXNODES_HISTORICAL_COUNT, MIXNODES_LEGACY_COUNT, - NYMNODES_DESCRIBED_COUNT, NYMNODE_COUNT, + GATEWAYS_HISTORICAL_COUNT, MIXNODES_HISTORICAL_COUNT, NYMNODES_DESCRIBED_COUNT, NYMNODE_COUNT, }; use crate::db::{queries, DbPool}; use crate::utils::now_utc; -use crate::utils::{decimal_to_i64, LogError, NumericalCheckedCast}; -use anyhow::anyhow; +use crate::utils::{LogError, NumericalCheckedCast}; use moka::future::Cache; use nym_network_defaults::NymNetworkDetails; use nym_validator_client::{ client::{NodeId, NymApiClientExt, NymNodeDetails}, - models::{LegacyDescribedMixNode, MixNodeBondAnnotated, NymNodeDescription}, + models::NymNodeDescription, }; use nym_validator_client::{ nym_nodes::{NodeRole, SkimmedNode}, - nyxd::{contract_traits::PagedMixnetQueryClient, AccountId}, - NymApiClient, QueryHttpRpcNyxdClient, -}; -use std::{ - collections::{HashMap, HashSet}, - str::FromStr, - sync::Arc, + QueryHttpRpcNyxdClient, }; +use std::{collections::HashMap, sync::Arc}; use tokio::{sync::RwLock, time::Duration}; use tracing::instrument; @@ -36,7 +29,6 @@ mod geodata; mod node_delegations; const MONITOR_FAILURE_RETRY_DELAY: Duration = Duration::from_secs(60); -static DELEGATION_PROGRAM_WALLET: &str = "n1rnxpdpx3kldygsklfft0gech7fhfcux4zst5lw"; pub(crate) type NodeGeoCache = Cache; struct Monitor { @@ -109,11 +101,9 @@ impl Monitor { nym_http_api_client::ClientBuilder::new_with_urls(vec![default_api_url.into()]) .no_hickory_dns() .with_timeout(self.nym_api_client_timeout) - .build::<&str>()?; + .build()?; - let api_client = NymApiClient::from(nym_api); - - let described_nodes = api_client + let described_nodes = nym_api .get_all_described_nodes() .await .log_error("get_all_described_nodes")? @@ -135,7 +125,7 @@ impl Monitor { tracing::info!("🟣 🚪 gateway nodes: {}", gateways.len()); - let bonded_nym_nodes = api_client + let bonded_nym_nodes = nym_api .get_all_bonded_nym_nodes() .await? .into_iter() @@ -146,10 +136,11 @@ impl Monitor { tracing::info!("🟣 bonded_nodes: {}", bonded_nym_nodes.len()); // returns only bonded nodes - let nym_nodes = api_client - .get_all_basic_nodes() + let nym_nodes = nym_api + .get_all_basic_nodes_with_metadata() .await - .log_error("get_all_basic_nodes")?; + .log_error("get_all_basic_nodes")? + .nodes; let nym_node_count = nym_nodes.len(); tracing::info!("🟣 get_all_basic_nodes: {}", nym_node_count); @@ -167,46 +158,13 @@ impl Monitor { self.location_cached(node_description).await; } - let mixnodes_detailed = api_client - .nym_api - .get_mixnodes_detailed_unfiltered() - .await - .log_error("get_mixnodes_detailed_unfiltered")?; - - tracing::info!( - "🟣 mixnodes_detailed_unfiltered: {}", - mixnodes_detailed.len() - ); - - let mixnodes_detailed_set = mixnodes_detailed - .iter() - .map(|elem| elem.identity_key().to_owned()) - .collect::>(); - - let mixnodes_legacy = nym_nodes - .iter() - .filter(|node| { - mixnodes_detailed_set.contains(&node.ed25519_identity_pubkey.to_base58_string()) - }) - .collect::>(); - - let mixnodes_described = api_client - .nym_api - .get_mixnodes_described() - .await - .log_error("get_mixnodes_described")?; - - tracing::info!("🟣 mixnodes_described: {}", mixnodes_described.len()); - let mixing_assigned_nodes = api_client - .nym_api + let mixing_assigned_nodes = nym_api .get_basic_active_mixing_assigned_nodes(false, None, None, false) .await .log_error("get_basic_active_mixing_assigned_nodes")? .nodes .data; - let delegation_program_members = self.get_delegation_program_details().await?; - // keep stats for later let assigned_entry_count = nym_nodes .iter() @@ -218,7 +176,6 @@ impl Monitor { .count(); let count_bonded_gateways = gateways.len(); let assigned_mixing_count = mixing_assigned_nodes.len(); - let count_legacy_mixnodes = mixnodes_legacy.len(); let gateway_records = self .prepare_gateway_data(&gateways, &nym_nodes, &bonded_nym_nodes) @@ -232,18 +189,6 @@ impl Monitor { tracing::debug!("{} gateway records written to DB!", gateways_count); })?; - let mixnode_records = self.prepare_mixnode_data( - &mixnodes_detailed, - mixnodes_described, - delegation_program_members, - )?; - let mixnodes_count = mixnode_records.len(); - queries::update_mixnodes(&pool, mixnode_records) - .await - .map(|_| { - tracing::debug!("{} mixnode info written to DB!", mixnodes_count); - })?; - self.refresh_node_delegations(&bonded_nym_nodes).await; let (all_historical_gateways, all_historical_mixnodes) = historical_count(&pool).await?; @@ -255,7 +200,6 @@ impl Monitor { let nodes_summary = vec![ (NYMNODE_COUNT.to_string(), nym_node_count), (ASSIGNED_MIXING_COUNT.to_string(), assigned_mixing_count), - (MIXNODES_LEGACY_COUNT.to_string(), count_legacy_mixnodes), (NYMNODES_DESCRIBED_COUNT.to_string(), described_nodes.len()), (GATEWAYS_BONDED_COUNT.to_string(), count_bonded_gateways), (ASSIGNED_ENTRY_COUNT.to_string(), assigned_entry_count), @@ -280,7 +224,6 @@ impl Monitor { bonded: mixnode::MixingNodesSummary { count: assigned_mixing_count.cast_checked()?, self_described: described_nodes.len().cast_checked()?, - legacy: count_legacy_mixnodes.cast_checked()?, last_updated_utc: last_updated_utc.clone(), }, historical: mixnode::MixnodeSummaryHistorical { @@ -420,49 +363,6 @@ impl Monitor { Ok(gateway_records) } - fn prepare_mixnode_data( - &self, - mixnodes: &[MixNodeBondAnnotated], - mixnodes_described: Vec, - delegation_program_members: Vec, - ) -> anyhow::Result> { - let mut mixnode_records = Vec::new(); - - for mixnode in mixnodes { - let mix_id = mixnode.mix_id(); - let identity_key = mixnode.identity_key(); - // only bonded nodes are given to this function - let bonded = true; - let total_stake = decimal_to_i64(mixnode.mixnode_details.total_stake()); - 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 = now_utc().unix_timestamp(); - - mixnode_records.push(MixnodeRecord { - mix_id, - identity_key: identity_key.to_owned(), - bonded, - total_stake, - host, - http_port, - full_details, - self_described, - last_updated_utc, - is_dp_delegatee, - }); - } - - Ok(mixnode_records) - } - async fn check_ipinfo_bandwidth(&self) { match self.ipinfo.check_remaining_bandwidth().await { Ok(bandwidth) => { @@ -481,23 +381,6 @@ impl Monitor { // update after refreshing all to avoid holding write lock for too long *self.node_delegations.write().await = delegations_per_node; } - - async fn get_delegation_program_details(&self) -> anyhow::Result> { - let account_id = AccountId::from_str(DELEGATION_PROGRAM_WALLET) - .map_err(|e| anyhow!("Invalid bech32 address: {}", e))?; - - let delegations = self - .nyxd_client - .get_all_delegator_delegations(&account_id) - .await?; - - let mix_ids: Vec = delegations - .iter() - .map(|delegation| delegation.node_id) - .collect(); - - Ok(mix_ids) - } } async fn historical_count(pool: &DbPool) -> anyhow::Result<(usize, usize)> { diff --git a/nym-node/Cargo.toml b/nym-node/Cargo.toml index fcb45a818d..45f494d408 100644 --- a/nym-node/Cargo.toml +++ b/nym-node/Cargo.toml @@ -40,6 +40,7 @@ tracing-indicatif = { workspace = true } tracing-subscriber.workspace = true tokio = { workspace = true, features = ["macros", "sync", "rt-multi-thread"] } tokio-util = { workspace = true, features = ["codec"] } +tokio-stream = { workspace = true } toml = { workspace = true } url = { workspace = true, features = ["serde"] } zeroize = { workspace = true, features = ["zeroize_derive"] } @@ -129,7 +130,7 @@ criterion = { workspace = true, features = ["async_tokio"] } rand_chacha = { workspace = true } [features] -tokio-console = ["console-subscriber"] +tokio-console = ["console-subscriber", "nym-task/tokio-tracing"] [lints] workspace = true diff --git a/nym-node/nym-node-requests/src/api/client.rs b/nym-node/nym-node-requests/src/api/client.rs index 2d21e5477c..e8a5d5205e 100644 --- a/nym-node/nym-node-requests/src/api/client.rs +++ b/nym-node/nym-node-requests/src/api/client.rs @@ -5,7 +5,6 @@ use crate::api::v1::gateway::models::WebSockets; use crate::api::v1::node::models::{ AuxiliaryDetails, NodeDescription, NodeRoles, SignedHostInformation, }; -use crate::api::ErrorResponse; use crate::routes; use async_trait::async_trait; use nym_bin_common::build_information::BinaryBuildInformationOwned; @@ -21,7 +20,7 @@ use crate::api::v1::network_requester::models::NetworkRequester; use crate::api::v1::node_load::models::NodeLoad; pub use nym_http_api_client::Client; -pub type NymNodeApiClientError = HttpClientError; +pub type NymNodeApiClientError = HttpClientError; #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] diff --git a/nym-node/nym-node-requests/src/api/v1/gateway/models.rs b/nym-node/nym-node-requests/src/api/v1/gateway/models.rs index 24597847ca..259bd35efa 100644 --- a/nym-node/nym-node-requests/src/api/v1/gateway/models.rs +++ b/nym-node/nym-node-requests/src/api/v1/gateway/models.rs @@ -16,9 +16,16 @@ pub struct Gateway { #[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)] #[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] pub struct Wireguard { + #[deprecated(note = "use specific port instead (tunnel or metadata service)")] #[cfg_attr(feature = "openapi", schema(example = 51822, default = 51822))] pub port: u16, + #[cfg_attr(feature = "openapi", schema(example = 51822, default = 51822))] + pub tunnel_port: u16, + + #[cfg_attr(feature = "openapi", schema(example = 51830, default = 51830))] + pub metadata_port: u16, + pub public_key: String, } diff --git a/nym-node/src/cli/helpers.rs b/nym-node/src/cli/helpers.rs index 3ff2388c45..cf6d18b683 100644 --- a/nym-node/src/cli/helpers.rs +++ b/nym-node/src/cli/helpers.rs @@ -276,13 +276,13 @@ pub(crate) struct WireguardArgs { )] pub(crate) wireguard_bind_address: Option, - /// Port announced to external clients wishing to connect to the wireguard interface. + /// Tunnel port announced to external clients wishing to connect to the wireguard interface. /// Useful in the instances where the node is behind a proxy. #[clap( long, env = NYMNODE_WG_ANNOUNCED_PORT_ARG )] - pub(crate) wireguard_announced_port: Option, + pub(crate) wireguard_tunnel_announced_port: Option, /// The prefix denoting the maximum number of the clients that can be connected via Wireguard. /// The maximum value for IPv4 is 32 and for IPv6 is 128 @@ -311,8 +311,8 @@ impl WireguardArgs { section.bind_address = bind_address } - if let Some(announced_port) = self.wireguard_announced_port { - section.announced_port = announced_port + if let Some(announced_tunnel_port) = self.wireguard_tunnel_announced_port { + section.announced_tunnel_port = announced_tunnel_port } if let Some(private_network_prefix) = self.wireguard_private_network_prefix { diff --git a/nym-node/src/config/helpers.rs b/nym-node/src/config/helpers.rs index 282f26af78..cdbffb0f25 100644 --- a/nym-node/src/config/helpers.rs +++ b/nym-node/src/config/helpers.rs @@ -159,7 +159,6 @@ pub fn gateway_tasks_config(config: &Config) -> GatewayTasksConfig { .to_common_client_paths(), ip_packet_router_description: Default::default(), }, - logging: config.logging, }, custom_mixnet_path: None, @@ -202,7 +201,8 @@ pub fn gateway_tasks_config(config: &Config) -> GatewayTasksConfig { bind_address: config.wireguard.bind_address, private_ipv4: config.wireguard.private_ipv4, private_ipv6: config.wireguard.private_ipv6, - announced_port: config.wireguard.announced_port, + announced_tunnel_port: config.wireguard.announced_tunnel_port, + announced_metadata_port: config.wireguard.announced_metadata_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(), diff --git a/nym-node/src/config/mod.rs b/nym-node/src/config/mod.rs index baf9c8376d..e401de4e20 100644 --- a/nym-node/src/config/mod.rs +++ b/nym-node/src/config/mod.rs @@ -10,7 +10,7 @@ use human_repr::HumanCount; use nym_bin_common::logging::LoggingSettings; use nym_config::defaults::{ mainnet, var_names, DEFAULT_MIX_LISTENING_PORT, DEFAULT_NYM_NODE_HTTP_PORT, - DEFAULT_VERLOC_LISTENING_PORT, WG_PORT, WG_TUN_DEVICE_IP_ADDRESS_V4, + DEFAULT_VERLOC_LISTENING_PORT, WG_METADATA_PORT, WG_TUNNEL_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}; @@ -931,9 +931,13 @@ pub struct Wireguard { /// default: `fc01::1` pub private_ipv6: Ipv6Addr, - /// Port announced to external clients wishing to connect to the wireguard interface. + /// Tunnel 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, + pub announced_tunnel_port: u16, + + /// Metadata port announced to external clients wishing to connect to the metadata endpoint. + /// Useful in the instances where the node is behind a proxy. + pub announced_metadata_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 @@ -951,10 +955,11 @@ impl Wireguard { pub fn new_default>(data_dir: P) -> Self { Wireguard { enabled: false, - bind_address: SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), WG_PORT), + bind_address: SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), WG_TUNNEL_PORT), private_ipv4: WG_TUN_DEVICE_IP_ADDRESS_V4, private_ipv6: WG_TUN_DEVICE_IP_ADDRESS_V6, - announced_port: WG_PORT, + announced_tunnel_port: WG_TUNNEL_PORT, + announced_metadata_port: WG_METADATA_PORT, private_network_prefix_v4: WG_TUN_DEVICE_NETMASK_V4, private_network_prefix_v6: WG_TUN_DEVICE_NETMASK_V6, storage_paths: persistence::WireguardPaths::new(data_dir), @@ -968,7 +973,8 @@ impl From for nym_wireguard_types::Config { bind_address: value.bind_address, private_ipv4: value.private_ipv4, private_ipv6: value.private_ipv6, - announced_port: value.announced_port, + announced_tunnel_port: value.announced_tunnel_port, + announced_metadata_port: value.announced_metadata_port, private_network_prefix_v4: value.private_network_prefix_v4, private_network_prefix_v6: value.private_network_prefix_v6, } @@ -981,7 +987,7 @@ impl From for nym_authenticator::config::Authenticator { bind_address: value.bind_address, private_ipv4: value.private_ipv4, private_ipv6: value.private_ipv6, - announced_port: value.announced_port, + tunnel_announced_port: value.announced_tunnel_port, private_network_prefix_v4: value.private_network_prefix_v4, private_network_prefix_v6: value.private_network_prefix_v6, } diff --git a/nym-node/src/config/old_configs/mod.rs b/nym-node/src/config/old_configs/mod.rs index 48c1ede17b..e67f2a83e9 100644 --- a/nym-node/src/config/old_configs/mod.rs +++ b/nym-node/src/config/old_configs/mod.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: GPL-3.0-only mod old_config_v1; +mod old_config_v10; mod old_config_v2; mod old_config_v3; mod old_config_v4; @@ -12,6 +13,7 @@ mod old_config_v8; mod old_config_v9; pub use old_config_v1::try_upgrade_config_v1; +pub use old_config_v10::try_upgrade_config_v10; 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; diff --git a/nym-node/src/config/old_configs/old_config_v10.rs b/nym-node/src/config/old_configs/old_config_v10.rs new file mode 100644 index 0000000000..a55b5b1113 --- /dev/null +++ b/nym-node/src/config/old_configs/old_config_v10.rs @@ -0,0 +1,1695 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::config::authenticator::{Authenticator, AuthenticatorDebug}; +use crate::config::gateway_tasks::{ + ClientBandwidthDebug, StaleMessageDebug, ZkNymTicketHandlerDebug, +}; +use crate::config::persistence::{ + AuthenticatorPaths, GatewayTasksPaths, IpPacketRouterPaths, KeysPaths, NetworkRequesterPaths, + NymNodePaths, ReplayProtectionPaths, ServiceProvidersPaths, WireguardPaths, +}; +use crate::config::service_providers::{ + IpPacketRouter, IpPacketRouterDebug, NetworkRequester, NetworkRequesterDebug, +}; +use crate::config::{ + gateway_tasks, service_providers, Config, GatewayTasksConfig, Host, Http, KeyRotation, + KeyRotationDebug, Mixnet, MixnetDebug, NodeModes, ReplayProtection, ReplayProtectionDebug, + ServiceProvidersConfig, Verloc, VerlocDebug, Wireguard, DEFAULT_HTTP_PORT, +}; +use crate::error::NymNodeError; +use celes::Country; +use clap::ValueEnum; +use nym_bin_common::logging::LoggingSettings; +use nym_client_core_config_types::DebugConfig as ClientDebugConfig; +use nym_config::defaults::{DEFAULT_VERLOC_LISTENING_PORT, WG_METADATA_PORT}; +use nym_config::helpers::{in6addr_any_init, inaddr_any}; +use nym_config::{ + defaults::TICKETBOOK_VALIDITY_DAYS, + read_config_from_toml_file, + serde_helpers::{de_maybe_port, de_maybe_stringified}, +}; +use serde::{Deserialize, Serialize}; +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 WireguardPathsV10 { + 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 WireguardV10 { + /// 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: `[::]: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: WireguardPathsV10, +} + +// 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 NodeModeV10 { + #[default] + #[clap(alias = "mix")] + Mixnode, + + #[clap(alias = "entry", alias = "gateway")] + EntryGateway, + + // to not break existing behaviour, this means exit capabilities AND entry capabilities + #[clap(alias = "exit")] + ExitGateway, + + // will start only SP needed for exit capabilities WITHOUT entry routing + ExitProvidersOnly, +} + +impl From for NodeModes { + fn from(config: NodeModeV10) -> Self { + match config { + NodeModeV10::Mixnode => *NodeModes::default().with_mixnode(), + NodeModeV10::EntryGateway => *NodeModes::default().with_entry(), + // in old version exit implied entry + NodeModeV10::ExitGateway => *NodeModes::default().with_entry().with_exit(), + NodeModeV10::ExitProvidersOnly => *NodeModes::default().with_exit(), + } + } +} + +#[derive(Debug, Default, Serialize, Deserialize, Clone, Copy)] +pub struct NodeModesV10 { + /// 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? +} + +// 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 HostV10 { + /// 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, + + /// 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, + + /// Optional ISO 3166 alpha-2 two-letter country code of the node's **physical** location + #[serde(deserialize_with = "de_maybe_stringified")] + pub location: Option, +} + +#[derive(Debug, Copy, Clone, Deserialize, PartialEq, Serialize)] +#[serde(default)] +pub struct KeyRotationDebugV10 { + /// Specifies how often the node should poll for any changes in the key rotation global state. + #[serde(with = "humantime_serde")] + pub rotation_state_poling_interval: Duration, +} + +impl KeyRotationDebugV10 { + pub const DEFAULT_ROTATION_STATE_POLLING_INTERVAL: Duration = Duration::from_secs(4 * 60 * 60); +} + +impl Default for KeyRotationDebugV10 { + fn default() -> Self { + KeyRotationDebugV10 { + rotation_state_poling_interval: Self::DEFAULT_ROTATION_STATE_POLLING_INTERVAL, + } + } +} + +#[derive(Debug, Default, Copy, Clone, Deserialize, PartialEq, Serialize)] +#[serde(default)] +pub struct KeyRotationV10 { + pub debug: KeyRotationDebugV10, +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)] +#[serde(default)] +#[serde(deny_unknown_fields)] +pub struct MixnetDebugV10 { + /// 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")] + 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, + + /// Specify whether any framed packets between nodes should use the legacy format (v7) + /// as opposed to the current (v8) one. + /// The legacy format has to be used until sufficient number of nodes on the network has upgraded and understand the new variant. + /// This will allow for optimisations to indicate which [sphinx] key is meant to be used when + /// processing received packets. + pub use_legacy_packet_encoding: bool, + + /// Specifies whether this node should **NOT** use noise protocol in the connections (currently not implemented) + pub unsafe_disable_noise: bool, +} + +impl MixnetDebugV10 { + // 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 + pub(crate) const DEFAULT_MAXIMUM_FORWARD_PACKET_DELAY: Duration = Duration::from_secs(10); + pub(crate) const DEFAULT_PACKET_FORWARDING_INITIAL_BACKOFF: Duration = + Duration::from_millis(10_000); + pub(crate) const DEFAULT_PACKET_FORWARDING_MAXIMUM_BACKOFF: Duration = + Duration::from_millis(300_000); + pub(crate) const DEFAULT_INITIAL_CONNECTION_TIMEOUT: Duration = Duration::from_millis(1_500); + pub(crate) const DEFAULT_MAXIMUM_CONNECTION_BUFFER_SIZE: usize = 2000; +} + +impl Default for MixnetDebugV10 { + fn default() -> Self { + MixnetDebugV10 { + 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, + maximum_connection_buffer_size: Self::DEFAULT_MAXIMUM_CONNECTION_BUFFER_SIZE, + // TODO: update this in few releases... + use_legacy_packet_encoding: true, + unsafe_disable_noise: false, + } + } +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct MixnetV10 { + /// Address this node will bind to for listening for mixnet packets + /// default: `[::]: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")] + #[serde(default)] + pub announce_port: Option, + + /// Addresses to nym APIs from which the node gets the view of the network. + pub nym_api_urls: Vec, + + /// Addresses to nyxd which the node uses to interact with the nyx chain. + pub nyxd_urls: Vec, + + /// Settings for controlling replay detection + pub replay_protection: ReplayProtectionV10, + + #[serde(default)] + pub key_rotation: KeyRotationV10, + + #[serde(default)] + pub debug: MixnetDebugV10, +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)] +pub struct ReplayProtectionV10 { + /// Paths for current bloomfilters + pub storage_paths: ReplayProtectionPathsV10, + + #[serde(default)] + pub debug: ReplayProtectionDebugV10, +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct ReplayProtectionPathsV10 { + /// Path to the directory storing currently used bloomfilter(s). + pub current_bloomfilters_directory: PathBuf, +} + +#[derive(Debug, Copy, Clone, Deserialize, PartialEq, Serialize)] +#[serde(default)] +pub struct ReplayProtectionDebugV10 { + /// Specifies whether this node should **NOT** use replay protection + pub unsafe_disabled: bool, + + /// How long the processing task is willing to skip mutex acquisition before it will block the thread + /// until it actually obtains it + pub maximum_replay_detection_deferral: Duration, + + /// How many packets the processing task is willing to queue before it will block the thread + /// until it obtains the mutex + pub maximum_replay_detection_pending_packets: usize, + + /// Probability of false positives, fraction between 0 and 1 or a number indicating 1-in-p + pub false_positive_rate: f64, + + /// Defines initial expected number of packets this node will process a second, + /// so that an initial bloomfilter could be established. + /// As the node is running and BF are cleared, the value will be adjusted dynamically + pub initial_expected_packets_per_second: usize, + + /// Defines minimum expected number of packets this node will process a second + /// when used for calculating the BF size after reset. + /// This is to avoid degenerate cases where node receives 0 packets (because say it's misconfigured) + /// and it constructs an empty bloomfilter. + pub bloomfilter_minimum_packets_per_second_size: usize, + + /// Specifies the amount the bloomfilter size is going to get multiplied by after each reset. + /// It's performed in case the traffic rates increase before the next bloomfilter update. + pub bloomfilter_size_multiplier: f64, + + /// Specifies how often the bloomfilter is flushed to disk for recovery in case of a crash + #[serde(with = "humantime_serde")] + pub bloomfilter_disk_flushing_rate: Duration, +} + +impl ReplayProtectionDebugV10 { + pub const DEFAULT_MAXIMUM_REPLAY_DETECTION_DEFERRAL: Duration = Duration::from_millis(50); + + pub const DEFAULT_MAXIMUM_REPLAY_DETECTION_PENDING_PACKETS: usize = 100; + + // 12% (completely arbitrary) + pub const DEFAULT_BLOOMFILTER_SIZE_MULTIPLIER: f64 = 1.12; + + // 10^-5 + pub const DEFAULT_REPLAY_DETECTION_FALSE_POSITIVE_RATE: f64 = 1e-5; + + // we must have some reasonable balance between losing values and trashing the disk. + // since on average HDD it would take ~30s to save a 2GB bloomfilter + pub const DEFAULT_BF_DISK_FLUSHING_RATE: Duration = Duration::from_secs(10 * 60); + + // this value will have to be adjusted in the future + pub const DEFAULT_INITIAL_EXPECTED_PACKETS_PER_SECOND: usize = 2000; + + pub const DEFAULT_BLOOMFILTER_MINIMUM_PACKETS_PER_SECOND_SIZE: usize = 200; +} + +impl Default for ReplayProtectionDebugV10 { + fn default() -> Self { + ReplayProtectionDebugV10 { + unsafe_disabled: false, + maximum_replay_detection_deferral: Self::DEFAULT_MAXIMUM_REPLAY_DETECTION_DEFERRAL, + maximum_replay_detection_pending_packets: + Self::DEFAULT_MAXIMUM_REPLAY_DETECTION_PENDING_PACKETS, + false_positive_rate: Self::DEFAULT_REPLAY_DETECTION_FALSE_POSITIVE_RATE, + initial_expected_packets_per_second: Self::DEFAULT_INITIAL_EXPECTED_PACKETS_PER_SECOND, + bloomfilter_minimum_packets_per_second_size: + Self::DEFAULT_BLOOMFILTER_MINIMUM_PACKETS_PER_SECOND_SIZE, + bloomfilter_size_multiplier: Self::DEFAULT_BLOOMFILTER_SIZE_MULTIPLIER, + bloomfilter_disk_flushing_rate: Self::DEFAULT_BF_DISK_FLUSHING_RATE, + } + } +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct KeysPathsV10 { + /// 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 the primary x25519 sphinx private key. + pub primary_x25519_sphinx_key_file: PathBuf, + + /// Path to file containing the secondary x25519 sphinx private key. + pub secondary_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, +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct NymNodePathsV10 { + pub keys: KeysPathsV10, + + /// 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 HttpV10 { + /// Socket address this node will use for binding its http API. + /// default: `[::]: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, + + /// An optional bearer token for accessing certain http endpoints. + /// Currently only used for obtaining mixnode's stats. + #[serde(default)] + pub access_token: Option, + + /// 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, + + /// Specify the cache ttl of the node load. + /// default: 30s + #[serde(with = "humantime_serde")] + pub node_load_cache_ttl: Duration, +} + +impl HttpV10 { + pub const DEFAULT_NODE_LOAD_CACHE_TTL: Duration = Duration::from_secs(30); +} + +impl Default for HttpV10 { + fn default() -> Self { + HttpV10 { + 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, + node_load_cache_ttl: Self::DEFAULT_NODE_LOAD_CACHE_TTL, + } + } +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct MixnodePathsV10 {} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct DebugV10 { + /// 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 VerlocDebugV10 { + /// 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 VerlocV10 { + /// Socket address this node will use for binding its verloc API. + /// default: `[::]: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, + + #[serde(default)] + pub debug: VerlocDebugV10, +} + +impl VerlocV10 { + pub const DEFAULT_VERLOC_PORT: u16 = DEFAULT_VERLOC_LISTENING_PORT; +} + +impl Default for VerlocV10 { + fn default() -> Self { + VerlocV10 { + bind_address: SocketAddr::new(in6addr_any_init(), Self::DEFAULT_VERLOC_PORT), + announce_port: None, + debug: Default::default(), + } + } +} + +impl VerlocDebugV10 { + 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 VerlocDebugV10 { + fn default() -> Self { + VerlocDebugV10 { + 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 MixnodeConfigV10 { + pub storage_paths: MixnodePathsV10, + + pub verloc: VerlocV10, + + #[serde(default)] + pub debug: DebugV10, +} + +impl DebugV10 { + 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 DebugV10 { + fn default() -> Self { + DebugV10 { + 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 EntryGatewayPathsV10 { + /// 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: AuthenticatorPathsV10, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(default)] +pub struct ZkNymTicketHandlerDebugV10 { + /// 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 ZkNymTicketHandlerDebugV10 { + 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 ZkNymTicketHandlerDebugV10 { + fn default() -> Self { + ZkNymTicketHandlerDebugV10 { + 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 EntryGatewayConfigDebugV10 { + /// 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: ZkNymTicketHandlerDebugV10, +} + +impl EntryGatewayConfigDebugV10 { + const DEFAULT_MESSAGE_RETRIEVAL_LIMIT: i64 = 100; +} + +impl Default for EntryGatewayConfigDebugV10 { + fn default() -> Self { + EntryGatewayConfigDebugV10 { + message_retrieval_limit: Self::DEFAULT_MESSAGE_RETRIEVAL_LIMIT, + zk_nym_tickets: Default::default(), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct EntryGatewayConfigV10 { + pub storage_paths: EntryGatewayPathsV10, + + /// 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: `[::]: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, + + /// If applicable, announced port for listening for secure websocket client traffic. + /// (default: None) + #[serde(deserialize_with = "de_maybe_port")] + pub announce_wss_port: Option, + + #[serde(default)] + pub debug: EntryGatewayConfigDebugV10, +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct NetworkRequesterPathsV10 { + /// 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 IpPacketRouterPathsV10 { + /// 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 AuthenticatorPathsV10 { + /// 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 +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct ExitGatewayPathsV10 { + pub clients_storage: PathBuf, + + pub stats_storage: PathBuf, + + pub network_requester: NetworkRequesterPathsV10, + + pub ip_packet_router: IpPacketRouterPathsV10, + + pub authenticator: AuthenticatorPathsV10, +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)] +pub struct AuthenticatorV10 { + #[serde(default)] + pub debug: AuthenticatorDebugV10, +} + +#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Serialize)] +#[serde(default)] +pub struct AuthenticatorDebugV10 { + /// 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 AuthenticatorDebugV10 { + fn default() -> Self { + AuthenticatorDebugV10 { + enabled: true, + disable_poisson_rate: true, + client_debug: Default::default(), + } + } +} + +#[allow(clippy::derivable_impls)] +impl Default for AuthenticatorV10 { + fn default() -> Self { + AuthenticatorV10 { + debug: Default::default(), + } + } +} + +#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Serialize)] +#[serde(default)] +pub struct IpPacketRouterDebugV10 { + /// 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 IpPacketRouterDebugV10 { + fn default() -> Self { + IpPacketRouterDebugV10 { + enabled: true, + disable_poisson_rate: true, + client_debug: Default::default(), + } + } +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)] +pub struct IpPacketRouterV10 { + #[serde(default)] + pub debug: IpPacketRouterDebugV10, +} + +#[allow(clippy::derivable_impls)] +impl Default for IpPacketRouterV10 { + fn default() -> Self { + IpPacketRouterV10 { + debug: Default::default(), + } + } +} + +#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Serialize)] +pub struct NetworkRequesterDebugV10 { + /// 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 NetworkRequesterDebugV10 { + fn default() -> Self { + NetworkRequesterDebugV10 { + enabled: true, + disable_poisson_rate: true, + client_debug: Default::default(), + } + } +} + +#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Serialize)] +pub struct NetworkRequesterV10 { + #[serde(default)] + pub debug: NetworkRequesterDebugV10, +} + +#[allow(clippy::derivable_impls)] +impl Default for NetworkRequesterV10 { + fn default() -> Self { + NetworkRequesterV10 { + debug: Default::default(), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct ExitGatewayDebugV10 { + /// 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 ExitGatewayDebugV10 { + const DEFAULT_MESSAGE_RETRIEVAL_LIMIT: i64 = 100; +} + +impl Default for ExitGatewayDebugV10 { + fn default() -> Self { + ExitGatewayDebugV10 { + message_retrieval_limit: Self::DEFAULT_MESSAGE_RETRIEVAL_LIMIT, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct ExitGatewayConfigV10 { + pub storage_paths: ExitGatewayPathsV10, + + /// 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: NetworkRequesterV10, + + pub ip_packet_router: IpPacketRouterV10, + + #[serde(default)] + pub debug: ExitGatewayDebugV10, +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct GatewayTasksPathsV10 { + /// 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, + + /// Path to sqlite database containing all persistent stats data. + pub stats_storage: PathBuf, + + /// Path to file containing cosmos account mnemonic used for zk-nym redemption. + pub cosmos_mnemonic: PathBuf, +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +pub struct StaleMessageDebugV10 { + /// Specifies how often the clean-up task should check for stale data. + #[serde(with = "humantime_serde")] + pub cleaner_run_interval: Duration, + + /// Specifies maximum age of stored messages before they are removed from the storage + #[serde(with = "humantime_serde")] + pub max_age: Duration, +} + +impl StaleMessageDebugV10 { + const DEFAULT_STALE_MESSAGES_CLEANER_RUN_INTERVAL: Duration = Duration::from_secs(60 * 60); + const DEFAULT_STALE_MESSAGES_MAX_AGE: Duration = Duration::from_secs(24 * 60 * 60); +} + +impl Default for StaleMessageDebugV10 { + fn default() -> Self { + StaleMessageDebugV10 { + cleaner_run_interval: Self::DEFAULT_STALE_MESSAGES_CLEANER_RUN_INTERVAL, + max_age: Self::DEFAULT_STALE_MESSAGES_MAX_AGE, + } + } +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +pub struct ClientBandwidthDebugV10 { + /// Defines maximum delay between client bandwidth information being flushed to the persistent storage. + pub max_flushing_rate: Duration, + + /// Defines a maximum change in client bandwidth before it gets flushed to the persistent storage. + pub max_delta_flushing_amount: i64, +} + +impl ClientBandwidthDebugV10 { + const DEFAULT_CLIENT_BANDWIDTH_MAX_FLUSHING_RATE: Duration = Duration::from_millis(5); + const DEFAULT_CLIENT_BANDWIDTH_MAX_DELTA_FLUSHING_AMOUNT: i64 = 512 * 1024; // 512kB +} + +impl Default for ClientBandwidthDebugV10 { + fn default() -> Self { + ClientBandwidthDebugV10 { + max_flushing_rate: Self::DEFAULT_CLIENT_BANDWIDTH_MAX_FLUSHING_RATE, + max_delta_flushing_amount: Self::DEFAULT_CLIENT_BANDWIDTH_MAX_DELTA_FLUSHING_AMOUNT, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +#[serde(default)] +pub struct GatewayTasksConfigDebugV10 { + /// 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, + + /// The maximum number of client connections the gateway will keep open at once. + pub maximum_open_connections: usize, + + /// Specifies the minimum performance of mixnodes in the network that are to be used in internal topologies + /// of the services providers + pub minimum_mix_performance: u8, + + /// Defines the timestamp skew of a signed authentication request before it's deemed too excessive to process. + #[serde(alias = "maximum_auth_request_age")] + pub max_request_timestamp_skew: Duration, + + pub stale_messages: StaleMessageDebugV10, + + pub client_bandwidth: ClientBandwidthDebugV10, + + pub zk_nym_tickets: ZkNymTicketHandlerDebugV10, +} + +impl GatewayTasksConfigDebugV10 { + pub const DEFAULT_MESSAGE_RETRIEVAL_LIMIT: i64 = 100; + pub const DEFAULT_MINIMUM_MIX_PERFORMANCE: u8 = 50; + pub const DEFAULT_MAXIMUM_AUTH_REQUEST_TIMESTAMP_SKEW: Duration = Duration::from_secs(120); + pub const DEFAULT_MAXIMUM_OPEN_CONNECTIONS: usize = 8192; +} + +impl Default for GatewayTasksConfigDebugV10 { + fn default() -> Self { + GatewayTasksConfigDebugV10 { + message_retrieval_limit: Self::DEFAULT_MESSAGE_RETRIEVAL_LIMIT, + maximum_open_connections: Self::DEFAULT_MAXIMUM_OPEN_CONNECTIONS, + max_request_timestamp_skew: Self::DEFAULT_MAXIMUM_AUTH_REQUEST_TIMESTAMP_SKEW, + minimum_mix_performance: Self::DEFAULT_MINIMUM_MIX_PERFORMANCE, + stale_messages: Default::default(), + client_bandwidth: Default::default(), + zk_nym_tickets: Default::default(), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct GatewayTasksConfigV10 { + pub storage_paths: GatewayTasksPathsV10, + + /// 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, + + /// Socket address this node will use for binding its client websocket API. + /// default: `[::]:9000` + pub ws_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, + + /// If applicable, announced port for listening for secure websocket client traffic. + /// (default: None) + #[serde(deserialize_with = "de_maybe_port")] + pub announce_wss_port: Option, + + #[serde(default)] + pub debug: GatewayTasksConfigDebugV10, +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct ServiceProvidersPathsV10 { + /// 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, + + /// Path to sqlite database containing all persistent stats data. + pub stats_storage: PathBuf, + + pub network_requester: NetworkRequesterPathsV10, + + pub ip_packet_router: IpPacketRouterPathsV10, + + pub authenticator: AuthenticatorPathsV10, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct ServiceProvidersConfigDebugV10 { + /// 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 ServiceProvidersConfigDebugV10 { + const DEFAULT_MESSAGE_RETRIEVAL_LIMIT: i64 = 100; +} + +impl Default for ServiceProvidersConfigDebugV10 { + fn default() -> Self { + ServiceProvidersConfigDebugV10 { + message_retrieval_limit: Self::DEFAULT_MESSAGE_RETRIEVAL_LIMIT, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct ServiceProvidersConfigV10 { + pub storage_paths: ServiceProvidersPathsV10, + + /// 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: NetworkRequesterV10, + + pub ip_packet_router: IpPacketRouterV10, + + pub authenticator: AuthenticatorV10, + + #[serde(default)] + pub debug: ServiceProvidersConfigDebugV10, +} + +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct MetricsConfigV10 { + #[serde(default)] + pub debug: MetricsDebugV10, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct MetricsDebugV10 { + /// 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 target rate of updating global prometheus counters. + #[serde(with = "humantime_serde")] + pub global_prometheus_counters_update_rate: Duration, + + /// Specify the target rate of updating egress packets pending delivery counter. + #[serde(with = "humantime_serde")] + pub pending_egress_packets_update_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 MetricsDebugV10 { + 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_METRICS_UPDATE_RATE: Duration = Duration::from_secs(3600); + const DEFAULT_CLIENT_SESSIONS_UPDATE_RATE: Duration = Duration::from_secs(3600); + const GLOBAL_PROMETHEUS_COUNTERS_UPDATE_INTERVAL: Duration = Duration::from_secs(30); + const DEFAULT_PENDING_EGRESS_PACKETS_UPDATE_RATE: Duration = Duration::from_secs(30); +} + +impl Default for MetricsDebugV10 { + fn default() -> Self { + MetricsDebugV10 { + 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_METRICS_UPDATE_RATE, + global_prometheus_counters_update_rate: + Self::GLOBAL_PROMETHEUS_COUNTERS_UPDATE_INTERVAL, + pending_egress_packets_update_rate: Self::DEFAULT_PENDING_EGRESS_PACKETS_UPDATE_RATE, + clients_sessions_update_rate: Self::DEFAULT_CLIENT_SESSIONS_UPDATE_RATE, + } + } +} + +#[derive(Debug, Default, Copy, Clone, Deserialize, PartialEq, Eq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct LoggingSettingsV10 { + // well, we need to implement something here at some point... +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct ConfigV10 { + // additional metadata holding on-disk location of this config file + #[serde(skip)] + pub(crate) save_path: Option, + + /// Human-readable ID of this particular node. + pub id: String, + + /// Current modes of this nym-node. + pub modes: NodeModesV10, + + pub host: HostV10, + + pub mixnet: MixnetV10, + + /// Storage paths to persistent nym-node data, such as its long term keys. + pub storage_paths: NymNodePathsV10, + + #[serde(default)] + pub http: HttpV10, + + #[serde(default)] + pub verloc: VerlocV10, + + pub wireguard: WireguardV10, + + #[serde(alias = "entry_gateway")] + pub gateway_tasks: GatewayTasksConfigV10, + + #[serde(alias = "exit_gateway")] + pub service_providers: ServiceProvidersConfigV10, + + #[serde(default)] + pub metrics: MetricsConfigV10, + + #[serde(default)] + pub logging: LoggingSettingsV10, + + #[serde(default)] + pub debug: DebugV10, +} + +impl ConfigV10 { + // simple wrapper that reads config file and assigns path location + fn read_from_path>(path: P) -> Result { + let path = path.as_ref(); + let mut loaded: ConfigV10 = + 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_v10>( + path: P, + prev_config: Option, +) -> Result { + debug!("attempting to load v10 config..."); + + let old_cfg = if let Some(prev_config) = prev_config { + prev_config + } else { + ConfigV10::read_from_path(&path)? + }; + + let cfg = Config { + save_path: old_cfg.save_path, + id: old_cfg.id, + modes: NodeModes { + mixnode: old_cfg.modes.mixnode, + entry: old_cfg.modes.entry, + exit: old_cfg.modes.exit, + }, + 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, + replay_protection: ReplayProtection { + storage_paths: ReplayProtectionPaths { + current_bloomfilters_directory: old_cfg + .mixnet + .replay_protection + .storage_paths + .current_bloomfilters_directory, + }, + debug: ReplayProtectionDebug { + unsafe_disabled: old_cfg.mixnet.replay_protection.debug.unsafe_disabled, + maximum_replay_detection_deferral: old_cfg + .mixnet + .replay_protection + .debug + .maximum_replay_detection_deferral, + maximum_replay_detection_pending_packets: old_cfg + .mixnet + .replay_protection + .debug + .maximum_replay_detection_pending_packets, + false_positive_rate: old_cfg.mixnet.replay_protection.debug.false_positive_rate, + initial_expected_packets_per_second: old_cfg + .mixnet + .replay_protection + .debug + .initial_expected_packets_per_second, + bloomfilter_minimum_packets_per_second_size: old_cfg + .mixnet + .replay_protection + .debug + .bloomfilter_minimum_packets_per_second_size, + bloomfilter_size_multiplier: old_cfg + .mixnet + .replay_protection + .debug + .bloomfilter_size_multiplier, + bloomfilter_disk_flushing_rate: old_cfg + .mixnet + .replay_protection + .debug + .bloomfilter_disk_flushing_rate, + }, + }, + key_rotation: KeyRotation { + debug: KeyRotationDebug { + rotation_state_poling_interval: old_cfg + .mixnet + .key_rotation + .debug + .rotation_state_poling_interval, + }, + }, + debug: MixnetDebug { + maximum_forward_packet_delay: old_cfg.mixnet.debug.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, + use_legacy_packet_encoding: old_cfg.mixnet.debug.use_legacy_packet_encoding, + }, + }, + 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, + primary_x25519_sphinx_key_file: old_cfg + .storage_paths + .keys + .primary_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, + secondary_x25519_sphinx_key_file: old_cfg + .storage_paths + .keys + .secondary_x25519_sphinx_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, + node_load_cache_ttl: old_cfg.http.node_load_cache_ttl, + }, + verloc: Verloc { + bind_address: old_cfg.verloc.bind_address, + announce_port: old_cfg.verloc.announce_port, + debug: VerlocDebug { + packets_per_node: old_cfg.verloc.debug.packets_per_node, + connection_timeout: old_cfg.verloc.debug.connection_timeout, + packet_timeout: old_cfg.verloc.debug.packet_timeout, + delay_between_packets: old_cfg.verloc.debug.delay_between_packets, + tested_nodes_batch_size: old_cfg.verloc.debug.tested_nodes_batch_size, + testing_interval: old_cfg.verloc.debug.testing_interval, + retry_timeout: old_cfg.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_tunnel_port: old_cfg.wireguard.announced_port, + announced_metadata_port: WG_METADATA_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.gateway_tasks.storage_paths.clients_storage, + stats_storage: old_cfg.gateway_tasks.storage_paths.stats_storage, + cosmos_mnemonic: old_cfg.gateway_tasks.storage_paths.cosmos_mnemonic, + }, + enforce_zk_nyms: old_cfg.gateway_tasks.enforce_zk_nyms, + ws_bind_address: old_cfg.gateway_tasks.ws_bind_address, + announce_ws_port: old_cfg.gateway_tasks.announce_ws_port, + announce_wss_port: old_cfg.gateway_tasks.announce_wss_port, + debug: gateway_tasks::Debug { + message_retrieval_limit: old_cfg.gateway_tasks.debug.message_retrieval_limit, + maximum_open_connections: old_cfg.gateway_tasks.debug.maximum_open_connections, + minimum_mix_performance: old_cfg.gateway_tasks.debug.minimum_mix_performance, + max_request_timestamp_skew: old_cfg.gateway_tasks.debug.max_request_timestamp_skew, + stale_messages: StaleMessageDebug { + cleaner_run_interval: old_cfg + .gateway_tasks + .debug + .stale_messages + .cleaner_run_interval, + max_age: old_cfg.gateway_tasks.debug.stale_messages.max_age, + }, + client_bandwidth: ClientBandwidthDebug { + max_flushing_rate: old_cfg + .gateway_tasks + .debug + .client_bandwidth + .max_flushing_rate, + max_delta_flushing_amount: old_cfg + .gateway_tasks + .debug + .client_bandwidth + .max_delta_flushing_amount, + }, + zk_nym_tickets: ZkNymTicketHandlerDebug { + revocation_bandwidth_penalty: old_cfg + .gateway_tasks + .debug + .zk_nym_tickets + .revocation_bandwidth_penalty, + pending_poller: old_cfg.gateway_tasks.debug.zk_nym_tickets.pending_poller, + minimum_api_quorum: old_cfg + .gateway_tasks + .debug + .zk_nym_tickets + .minimum_api_quorum, + minimum_redemption_tickets: old_cfg + .gateway_tasks + .debug + .zk_nym_tickets + .minimum_redemption_tickets, + maximum_time_between_redemption: old_cfg + .gateway_tasks + .debug + .zk_nym_tickets + .maximum_time_between_redemption, + }, + }, + }, + service_providers: ServiceProvidersConfig { + storage_paths: ServiceProvidersPaths { + clients_storage: old_cfg.service_providers.storage_paths.clients_storage, + stats_storage: old_cfg.service_providers.storage_paths.stats_storage, + network_requester: NetworkRequesterPaths { + private_ed25519_identity_key_file: old_cfg + .service_providers + .storage_paths + .network_requester + .private_ed25519_identity_key_file, + public_ed25519_identity_key_file: old_cfg + .service_providers + .storage_paths + .network_requester + .public_ed25519_identity_key_file, + private_x25519_diffie_hellman_key_file: old_cfg + .service_providers + .storage_paths + .network_requester + .private_x25519_diffie_hellman_key_file, + public_x25519_diffie_hellman_key_file: old_cfg + .service_providers + .storage_paths + .network_requester + .public_x25519_diffie_hellman_key_file, + ack_key_file: old_cfg + .service_providers + .storage_paths + .network_requester + .ack_key_file, + reply_surb_database: old_cfg + .service_providers + .storage_paths + .network_requester + .reply_surb_database, + gateway_registrations: old_cfg + .service_providers + .storage_paths + .network_requester + .gateway_registrations, + }, + ip_packet_router: IpPacketRouterPaths { + private_ed25519_identity_key_file: old_cfg + .service_providers + .storage_paths + .ip_packet_router + .private_ed25519_identity_key_file, + public_ed25519_identity_key_file: old_cfg + .service_providers + .storage_paths + .ip_packet_router + .public_ed25519_identity_key_file, + private_x25519_diffie_hellman_key_file: old_cfg + .service_providers + .storage_paths + .ip_packet_router + .private_x25519_diffie_hellman_key_file, + public_x25519_diffie_hellman_key_file: old_cfg + .service_providers + .storage_paths + .ip_packet_router + .public_x25519_diffie_hellman_key_file, + ack_key_file: old_cfg + .service_providers + .storage_paths + .ip_packet_router + .ack_key_file, + reply_surb_database: old_cfg + .service_providers + .storage_paths + .ip_packet_router + .reply_surb_database, + gateway_registrations: old_cfg + .service_providers + .storage_paths + .ip_packet_router + .gateway_registrations, + }, + authenticator: AuthenticatorPaths { + private_ed25519_identity_key_file: old_cfg + .service_providers + .storage_paths + .authenticator + .private_ed25519_identity_key_file, + public_ed25519_identity_key_file: old_cfg + .service_providers + .storage_paths + .authenticator + .public_ed25519_identity_key_file, + private_x25519_diffie_hellman_key_file: old_cfg + .service_providers + .storage_paths + .authenticator + .private_x25519_diffie_hellman_key_file, + public_x25519_diffie_hellman_key_file: old_cfg + .service_providers + .storage_paths + .authenticator + .public_x25519_diffie_hellman_key_file, + ack_key_file: old_cfg + .service_providers + .storage_paths + .authenticator + .ack_key_file, + reply_surb_database: old_cfg + .service_providers + .storage_paths + .authenticator + .reply_surb_database, + gateway_registrations: old_cfg + .service_providers + .storage_paths + .authenticator + .gateway_registrations, + }, + }, + open_proxy: old_cfg.service_providers.open_proxy, + upstream_exit_policy_url: old_cfg.service_providers.upstream_exit_policy_url, + network_requester: NetworkRequester { + debug: NetworkRequesterDebug { + enabled: old_cfg.service_providers.network_requester.debug.enabled, + disable_poisson_rate: old_cfg + .service_providers + .network_requester + .debug + .disable_poisson_rate, + client_debug: old_cfg + .service_providers + .network_requester + .debug + .client_debug, + }, + }, + ip_packet_router: IpPacketRouter { + debug: IpPacketRouterDebug { + enabled: old_cfg.service_providers.ip_packet_router.debug.enabled, + disable_poisson_rate: old_cfg + .service_providers + .ip_packet_router + .debug + .disable_poisson_rate, + client_debug: old_cfg + .service_providers + .ip_packet_router + .debug + .client_debug, + }, + }, + authenticator: Authenticator { + debug: AuthenticatorDebug { + enabled: old_cfg.service_providers.authenticator.debug.enabled, + disable_poisson_rate: old_cfg + .service_providers + .authenticator + .debug + .disable_poisson_rate, + client_debug: old_cfg.service_providers.authenticator.debug.client_debug, + }, + }, + debug: service_providers::Debug { + message_retrieval_limit: old_cfg.service_providers.debug.message_retrieval_limit, + }, + }, + metrics: Default::default(), + logging: LoggingSettings {}, + debug: Default::default(), + }; + Ok(cfg) +} diff --git a/nym-node/src/config/old_configs/old_config_v9.rs b/nym-node/src/config/old_configs/old_config_v9.rs index 94b9559844..839a54b679 100644 --- a/nym-node/src/config/old_configs/old_config_v9.rs +++ b/nym-node/src/config/old_configs/old_config_v9.rs @@ -1,29 +1,26 @@ // Copyright 2025 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::config::authenticator::{Authenticator, AuthenticatorDebug}; -use crate::config::gateway_tasks::{ - ClientBandwidthDebug, StaleMessageDebug, ZkNymTicketHandlerDebug, +use crate::config::old_configs::old_config_v10::{ + AuthenticatorDebugV10, AuthenticatorPathsV10, AuthenticatorV10, ClientBandwidthDebugV10, + ConfigV10, GatewayTasksConfigDebugV10, GatewayTasksConfigV10, GatewayTasksPathsV10, HostV10, + HttpV10, IpPacketRouterDebugV10, IpPacketRouterPathsV10, IpPacketRouterV10, KeysPathsV10, + LoggingSettingsV10, MixnetDebugV10, MixnetV10, NetworkRequesterDebugV10, + NetworkRequesterPathsV10, NetworkRequesterV10, NodeModesV10, NymNodePathsV10, + ReplayProtectionDebugV10, ReplayProtectionPathsV10, ReplayProtectionV10, + ServiceProvidersConfigDebugV10, ServiceProvidersConfigV10, ServiceProvidersPathsV10, + StaleMessageDebugV10, VerlocDebugV10, VerlocV10, WireguardPathsV10, WireguardV10, + ZkNymTicketHandlerDebugV10, }; use crate::config::persistence::{ - AuthenticatorPaths, GatewayTasksPaths, IpPacketRouterPaths, KeysPaths, NetworkRequesterPaths, - NymNodePaths, ReplayProtectionPaths, ServiceProvidersPaths, WireguardPaths, DEFAULT_PRIMARY_X25519_SPHINX_KEY_FILENAME, DEFAULT_SECONDARY_X25519_SPHINX_KEY_FILENAME, }; -use crate::config::service_providers::{ - IpPacketRouter, IpPacketRouterDebug, NetworkRequester, NetworkRequesterDebug, -}; -use crate::config::{ - gateway_tasks, service_providers, Config, GatewayTasksConfig, Host, Http, Mixnet, MixnetDebug, - NodeModes, ReplayProtection, ReplayProtectionDebug, ServiceProvidersConfig, Verloc, - VerlocDebug, Wireguard, DEFAULT_HTTP_PORT, -}; +use crate::config::{NodeModes, DEFAULT_HTTP_PORT}; use crate::error::{KeyIOFailure, NymNodeError}; use crate::node::helpers::{get_current_rotation_id, load_key, store_key}; use crate::node::key_rotation::key::SphinxPrivateKey; use celes::Country; use clap::ValueEnum; -use nym_bin_common::logging::LoggingSettings; use nym_client_core_config_types::DebugConfig as ClientDebugConfig; use nym_config::defaults::DEFAULT_VERLOC_LISTENING_PORT; use nym_config::helpers::{in6addr_any_init, inaddr_any}; @@ -1330,7 +1327,7 @@ async fn upgrade_sphinx_key(old_cfg: &ConfigV9) -> Result<(PathBuf, PathBuf), Ny pub async fn try_upgrade_config_v9>( path: P, prev_config: Option, -) -> Result { +) -> Result { debug!("attempting to load v9 config..."); let old_cfg = if let Some(prev_config) = prev_config { @@ -1342,33 +1339,33 @@ pub async fn try_upgrade_config_v9>( let (primary_x25519_sphinx_key_file, secondary_x25519_sphinx_key_file) = upgrade_sphinx_key(&old_cfg).await?; - let cfg = Config { + let cfg = ConfigV10 { save_path: old_cfg.save_path, id: old_cfg.id, - modes: NodeModes { + modes: NodeModesV10 { mixnode: old_cfg.modes.mixnode, entry: old_cfg.modes.entry, exit: old_cfg.modes.exit, }, - host: Host { + host: HostV10 { public_ips: old_cfg.host.public_ips, hostname: old_cfg.host.hostname, location: old_cfg.host.location, }, - mixnet: Mixnet { + mixnet: MixnetV10 { 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, - replay_protection: ReplayProtection { - storage_paths: ReplayProtectionPaths { + replay_protection: ReplayProtectionV10 { + storage_paths: ReplayProtectionPathsV10 { current_bloomfilters_directory: old_cfg .mixnet .replay_protection .storage_paths .current_bloomfilters_directory, }, - debug: ReplayProtectionDebug { + debug: ReplayProtectionDebugV10 { unsafe_disabled: old_cfg.mixnet.replay_protection.debug.unsafe_disabled, maximum_replay_detection_deferral: old_cfg .mixnet @@ -1404,7 +1401,7 @@ pub async fn try_upgrade_config_v9>( }, }, key_rotation: Default::default(), - debug: MixnetDebug { + debug: MixnetDebugV10 { maximum_forward_packet_delay: old_cfg.mixnet.debug.maximum_forward_packet_delay, packet_forwarding_initial_backoff: old_cfg .mixnet @@ -1420,8 +1417,8 @@ pub async fn try_upgrade_config_v9>( ..Default::default() }, }, - storage_paths: NymNodePaths { - keys: KeysPaths { + storage_paths: NymNodePathsV10 { + keys: KeysPathsV10 { private_ed25519_identity_key_file: old_cfg .storage_paths .keys @@ -1443,7 +1440,7 @@ pub async fn try_upgrade_config_v9>( }, description: old_cfg.storage_paths.description, }, - http: Http { + http: HttpV10 { 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, @@ -1452,10 +1449,10 @@ pub async fn try_upgrade_config_v9>( expose_crypto_hardware: old_cfg.http.expose_crypto_hardware, node_load_cache_ttl: old_cfg.http.node_load_cache_ttl, }, - verloc: Verloc { + verloc: VerlocV10 { bind_address: old_cfg.verloc.bind_address, announce_port: old_cfg.verloc.announce_port, - debug: VerlocDebug { + debug: VerlocDebugV10 { packets_per_node: old_cfg.verloc.debug.packets_per_node, connection_timeout: old_cfg.verloc.debug.connection_timeout, packet_timeout: old_cfg.verloc.debug.packet_timeout, @@ -1465,7 +1462,7 @@ pub async fn try_upgrade_config_v9>( retry_timeout: old_cfg.verloc.debug.retry_timeout, }, }, - wireguard: Wireguard { + wireguard: WireguardV10 { enabled: old_cfg.wireguard.enabled, bind_address: old_cfg.wireguard.bind_address, private_ipv4: old_cfg.wireguard.private_ipv4, @@ -1473,7 +1470,7 @@ pub async fn try_upgrade_config_v9>( 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 { + storage_paths: WireguardPathsV10 { private_diffie_hellman_key_file: old_cfg .wireguard .storage_paths @@ -1484,8 +1481,8 @@ pub async fn try_upgrade_config_v9>( .public_diffie_hellman_key_file, }, }, - gateway_tasks: GatewayTasksConfig { - storage_paths: GatewayTasksPaths { + gateway_tasks: GatewayTasksConfigV10 { + storage_paths: GatewayTasksPathsV10 { clients_storage: old_cfg.gateway_tasks.storage_paths.clients_storage, stats_storage: old_cfg.gateway_tasks.storage_paths.stats_storage, cosmos_mnemonic: old_cfg.gateway_tasks.storage_paths.cosmos_mnemonic, @@ -1494,12 +1491,12 @@ pub async fn try_upgrade_config_v9>( ws_bind_address: old_cfg.gateway_tasks.ws_bind_address, announce_ws_port: old_cfg.gateway_tasks.announce_ws_port, announce_wss_port: old_cfg.gateway_tasks.announce_wss_port, - debug: gateway_tasks::Debug { + debug: GatewayTasksConfigDebugV10 { message_retrieval_limit: old_cfg.gateway_tasks.debug.message_retrieval_limit, maximum_open_connections: old_cfg.gateway_tasks.debug.maximum_open_connections, minimum_mix_performance: old_cfg.gateway_tasks.debug.minimum_mix_performance, max_request_timestamp_skew: old_cfg.gateway_tasks.debug.max_request_timestamp_skew, - stale_messages: StaleMessageDebug { + stale_messages: StaleMessageDebugV10 { cleaner_run_interval: old_cfg .gateway_tasks .debug @@ -1507,7 +1504,7 @@ pub async fn try_upgrade_config_v9>( .cleaner_run_interval, max_age: old_cfg.gateway_tasks.debug.stale_messages.max_age, }, - client_bandwidth: ClientBandwidthDebug { + client_bandwidth: ClientBandwidthDebugV10 { max_flushing_rate: old_cfg .gateway_tasks .debug @@ -1519,7 +1516,7 @@ pub async fn try_upgrade_config_v9>( .client_bandwidth .max_delta_flushing_amount, }, - zk_nym_tickets: ZkNymTicketHandlerDebug { + zk_nym_tickets: ZkNymTicketHandlerDebugV10 { revocation_bandwidth_penalty: old_cfg .gateway_tasks .debug @@ -1544,11 +1541,11 @@ pub async fn try_upgrade_config_v9>( }, }, }, - service_providers: ServiceProvidersConfig { - storage_paths: ServiceProvidersPaths { + service_providers: ServiceProvidersConfigV10 { + storage_paths: ServiceProvidersPathsV10 { clients_storage: old_cfg.service_providers.storage_paths.clients_storage, stats_storage: old_cfg.service_providers.storage_paths.stats_storage, - network_requester: NetworkRequesterPaths { + network_requester: NetworkRequesterPathsV10 { private_ed25519_identity_key_file: old_cfg .service_providers .storage_paths @@ -1585,7 +1582,7 @@ pub async fn try_upgrade_config_v9>( .network_requester .gateway_registrations, }, - ip_packet_router: IpPacketRouterPaths { + ip_packet_router: IpPacketRouterPathsV10 { private_ed25519_identity_key_file: old_cfg .service_providers .storage_paths @@ -1622,7 +1619,7 @@ pub async fn try_upgrade_config_v9>( .ip_packet_router .gateway_registrations, }, - authenticator: AuthenticatorPaths { + authenticator: AuthenticatorPathsV10 { private_ed25519_identity_key_file: old_cfg .service_providers .storage_paths @@ -1662,8 +1659,8 @@ pub async fn try_upgrade_config_v9>( }, open_proxy: old_cfg.service_providers.open_proxy, upstream_exit_policy_url: old_cfg.service_providers.upstream_exit_policy_url, - network_requester: NetworkRequester { - debug: NetworkRequesterDebug { + network_requester: NetworkRequesterV10 { + debug: NetworkRequesterDebugV10 { enabled: old_cfg.service_providers.network_requester.debug.enabled, disable_poisson_rate: old_cfg .service_providers @@ -1677,8 +1674,8 @@ pub async fn try_upgrade_config_v9>( .client_debug, }, }, - ip_packet_router: IpPacketRouter { - debug: IpPacketRouterDebug { + ip_packet_router: IpPacketRouterV10 { + debug: IpPacketRouterDebugV10 { enabled: old_cfg.service_providers.ip_packet_router.debug.enabled, disable_poisson_rate: old_cfg .service_providers @@ -1692,8 +1689,8 @@ pub async fn try_upgrade_config_v9>( .client_debug, }, }, - authenticator: Authenticator { - debug: AuthenticatorDebug { + authenticator: AuthenticatorV10 { + debug: AuthenticatorDebugV10 { enabled: old_cfg.service_providers.authenticator.debug.enabled, disable_poisson_rate: old_cfg .service_providers @@ -1703,12 +1700,12 @@ pub async fn try_upgrade_config_v9>( client_debug: old_cfg.service_providers.authenticator.debug.client_debug, }, }, - debug: service_providers::Debug { + debug: ServiceProvidersConfigDebugV10 { message_retrieval_limit: old_cfg.service_providers.debug.message_retrieval_limit, }, }, metrics: Default::default(), - logging: LoggingSettings {}, + logging: LoggingSettingsV10 {}, debug: Default::default(), }; Ok(cfg) diff --git a/nym-node/src/config/template.rs b/nym-node/src/config/template.rs index bfe413a62b..bf8ed72710 100644 --- a/nym-node/src/config/template.rs +++ b/nym-node/src/config/template.rs @@ -145,7 +145,11 @@ private_ipv6 = '{{ wireguard.private_ipv6 }}' # Port announced to external clients wishing to connect to the wireguard interface. # Useful in the instances where the node is behind a proxy. -announced_port = {{ wireguard.announced_port }} +announced_tunnel_port = {{ wireguard.announced_tunnel_port }} + +# Port announced to external clients wishing to connect to the metadata service. +# Useful in the instances where the node is behind a proxy. +announced_metadata_port = {{ wireguard.announced_metadata_port }} # The prefix denoting the maximum number of the clients that can be connected via Wireguard using IPv4. # The maximum value for IPv4 is 32 diff --git a/nym-node/src/config/upgrade_helpers.rs b/nym-node/src/config/upgrade_helpers.rs index 85e2cced9c..bdb10ec8b7 100644 --- a/nym-node/src/config/upgrade_helpers.rs +++ b/nym-node/src/config/upgrade_helpers.rs @@ -16,7 +16,8 @@ async fn try_upgrade_config(path: &Path) -> Result<(), NymNodeError> { let cfg = try_upgrade_config_v6(path, cfg).await.ok(); let cfg = try_upgrade_config_v7(path, cfg).await.ok(); let cfg = try_upgrade_config_v8(path, cfg).await.ok(); - match try_upgrade_config_v9(path, cfg).await { + let cfg = try_upgrade_config_v9(path, cfg).await.ok(); + match try_upgrade_config_v10(path, cfg).await { Ok(cfg) => cfg.save(), Err(e) => { tracing::error!("Failed to finish upgrade: {e}"); diff --git a/nym-node/src/error.rs b/nym-node/src/error.rs index 37bf7eb59a..e199e3aef1 100644 --- a/nym-node/src/error.rs +++ b/nym-node/src/error.rs @@ -302,6 +302,6 @@ pub enum ServiceProvidersError { impl From for NymNodeError { fn from(value: HttpClientError) -> Self { - Self::HttpFailure(NymNodeHttpError::ClientError { source: value }) + Self::HttpFailure(NymNodeHttpError::from(value)) } } diff --git a/nym-node/src/node/helpers.rs b/nym-node/src/node/helpers.rs index bdb00df7b1..dbd8b15086 100644 --- a/nym-node/src/node/helpers.rs +++ b/nym-node/src/node/helpers.rs @@ -187,7 +187,7 @@ pub(crate) async fn get_current_rotation_id( nym_apis: &[Url], fallback_nyxd: &[Url], ) -> Result { - let apis_client = NymApisClient::new(nym_apis, ShutdownToken::ephemeral())?; + let apis_client = NymApisClient::new(nym_apis, ShutdownToken::default())?; if let Ok(rotation_info) = apis_client.get_key_rotation_info().await.map(|r| r.details) { if rotation_info.is_epoch_stuck() { return Err(NymNodeError::StuckEpoch); diff --git a/nym-node/src/node/http/error.rs b/nym-node/src/node/http/error.rs index 178e26fdcd..de3d746b48 100644 --- a/nym-node/src/node/http/error.rs +++ b/nym-node/src/node/http/error.rs @@ -27,8 +27,13 @@ pub enum NymNodeHttpError { }, #[error("error building or using HTTP client: {source}")] - ClientError { - #[from] - source: HttpClientError, - }, + ClientError { source: Box }, +} + +impl From for NymNodeHttpError { + fn from(source: HttpClientError) -> Self { + NymNodeHttpError::ClientError { + source: Box::new(source), + } + } } diff --git a/nym-node/src/node/key_rotation/controller.rs b/nym-node/src/node/key_rotation/controller.rs index 0aa8aa5fa5..c7aee7d671 100644 --- a/nym-node/src/node/key_rotation/controller.rs +++ b/nym-node/src/node/key_rotation/controller.rs @@ -349,7 +349,7 @@ impl KeyRotationController { let state_update_future = sleep(next_action.until_deadline()); pin_mut!(state_update_future); - while !self.shutdown_token.is_cancelled() { + loop { tokio::select! { biased; _ = self.shutdown_token.cancelled() => { diff --git a/nym-node/src/node/metrics/aggregator.rs b/nym-node/src/node/metrics/aggregator.rs index 7d67e925c2..3be11cedcf 100644 --- a/nym-node/src/node/metrics/aggregator.rs +++ b/nym-node/src/node/metrics/aggregator.rs @@ -12,7 +12,6 @@ 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}; @@ -25,11 +24,10 @@ pub(crate) struct MetricsAggregator { // registered_handlers: HashMap>, event_sender: MetricEventsSender, event_receiver: MetricEventsReceiver, - shutdown: ShutdownToken, } impl MetricsAggregator { - pub fn new(handlers_update_interval: Duration, shutdown: ShutdownToken) -> Self { + pub fn new(handlers_update_interval: Duration) -> Self { let (event_sender, event_receiver) = events_channels(); MetricsAggregator { @@ -37,7 +35,6 @@ impl MetricsAggregator { registered_handlers: Default::default(), event_sender, event_receiver, - shutdown, } } @@ -106,7 +103,7 @@ impl MetricsAggregator { } } - pub async fn run(&mut self) { + pub async fn run(&mut self, shutdown_token: ShutdownToken) { self.on_start().await; let start = Instant::now() + self.handlers_update_interval; @@ -117,7 +114,7 @@ impl MetricsAggregator { loop { tokio::select! { biased; - _ = self.shutdown.cancelled() => { + _ = shutdown_token.cancelled() => { debug!("MetricsAggregator: Received shutdown"); break; } @@ -144,8 +141,4 @@ impl MetricsAggregator { } 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 index c7db72960b..35b25ff153 100644 --- a/nym-node/src/node/metrics/console_logger.rs +++ b/nym-node/src/node/metrics/console_logger.rs @@ -1,15 +1,15 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only +use futures::StreamExt; use human_repr::HumanCount; use human_repr::HumanThroughput; use nym_node_metrics::NymNodeMetrics; -use nym_task::ShutdownToken; use std::time::Duration; use time::OffsetDateTime; -use tokio::task::JoinHandle; use tokio::time::{interval_at, Instant}; -use tracing::{info, trace}; +use tokio_stream::wrappers::IntervalStream; +use tracing::{error, info, trace}; struct AtLastUpdate { time: OffsetDateTime, @@ -49,20 +49,14 @@ pub(crate) struct ConsoleLogger { logging_delay: Duration, at_last_update: AtLastUpdate, metrics: NymNodeMetrics, - shutdown: ShutdownToken, } impl ConsoleLogger { - pub(crate) fn new( - logging_delay: Duration, - metrics: NymNodeMetrics, - shutdown: ShutdownToken, - ) -> Self { + pub(crate) fn new(logging_delay: Duration, metrics: NymNodeMetrics) -> Self { ConsoleLogger { logging_delay, at_last_update: AtLastUpdate::new(), metrics, - shutdown, } } @@ -123,23 +117,19 @@ impl ConsoleLogger { // TODO: add websocket-client traffic } - async fn run(&mut self) { + pub(crate) async fn run(&mut self) { trace!("Starting ConsoleLogger"); - let mut interval = interval_at(Instant::now() + self.logging_delay, self.logging_delay); - loop { - tokio::select! { - biased; - _ = self.shutdown.cancelled() => { - trace!("ConsoleLogger: Received shutdown"); - break - } - _ = interval.tick() => self.log_running_stats().await, - }; - } - trace!("ConsoleLogger: Exiting"); - } - pub(crate) fn start(mut self) -> JoinHandle<()> { - tokio::spawn(async move { self.run().await }) + let mut stream = IntervalStream::new(interval_at( + Instant::now() + self.logging_delay, + self.logging_delay, + )); + + while stream.next().await.is_some() { + self.log_running_stats().await + } + + // this should never get triggered + error!("console logger interval has been exhausted!") } } diff --git a/nym-node/src/node/mixnet/handler.rs b/nym-node/src/node/mixnet/handler.rs index aca227455a..6a14277c1d 100644 --- a/nym-node/src/node/mixnet/handler.rs +++ b/nym-node/src/node/mixnet/handler.rs @@ -91,7 +91,6 @@ impl Drop for ConnectionHandler { impl ConnectionHandler { pub(crate) fn new(shared: &SharedData, remote_address: SocketAddr) -> Self { - let shutdown = shared.shutdown.child_token(remote_address.to_string()); shared.metrics.network.new_active_ingress_mixnet_client(); ConnectionHandler { @@ -103,7 +102,7 @@ impl ConnectionHandler { final_hop: shared.final_hop.clone(), noise_config: shared.noise_config.clone(), metrics: shared.metrics.clone(), - shutdown, + shutdown_token: shared.shutdown_token.child_token(), }, remote_address, pending_packets: PendingReplayCheckPackets::new(), @@ -369,7 +368,7 @@ impl ConnectionHandler { Some(Err(_)) => { // our mutex got poisoned - we have to shut down error!("CRITICAL FAILURE: replay bloomfilter mutex poisoning!"); - self.shared.shutdown.cancel(); + self.shared.shutdown_token.cancel(); return false; } }; @@ -394,7 +393,7 @@ impl ConnectionHandler { else { // our mutex got poisoned - we have to shut down error!("CRITICAL FAILURE: replay bloomfilter mutex poisoning!"); - self.shared.shutdown.cancel(); + self.shared.shutdown_token.cancel(); return; }; @@ -489,7 +488,7 @@ impl ConnectionHandler { loop { tokio::select! { biased; - _ = self.shared.shutdown.cancelled() => { + _ = self.shared.shutdown_token.cancelled() => { trace!("connection handler: received shutdown"); break } diff --git a/nym-node/src/node/mixnet/listener.rs b/nym-node/src/node/mixnet/listener.rs index f5900414b0..7fdca155a7 100644 --- a/nym-node/src/node/mixnet/listener.rs +++ b/nym-node/src/node/mixnet/listener.rs @@ -4,12 +4,10 @@ use crate::node::mixnet::SharedData; use nym_task::ShutdownToken; use std::net::SocketAddr; -use tokio::task::JoinHandle; use tracing::{debug, error, info, trace}; pub(crate) struct Listener { bind_address: SocketAddr, - shutdown: ShutdownToken, shared_data: SharedData, } @@ -17,19 +15,18 @@ impl Listener { pub(crate) fn new(bind_address: SocketAddr, shared_data: SharedData) -> Self { Listener { bind_address, - shutdown: shared_data.shutdown.clone_with_suffix("socket-listener"), shared_data, } } - pub(crate) async fn run(&mut self) { + pub(crate) async fn run(&mut self, shutdown: ShutdownToken) { 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); - self.shutdown.cancel(); + shutdown.cancel(); return; } }; @@ -37,7 +34,7 @@ impl Listener { loop { tokio::select! { biased; - _ = self.shutdown.cancelled() => { + _ = shutdown.cancelled() => { trace!("mixnet listener: received shutdown"); break } @@ -48,8 +45,4 @@ impl Listener { } debug!("mixnet socket listener: Exiting"); } - - pub(crate) fn start(mut self) -> JoinHandle<()> { - tokio::spawn(async move { self.run().await }) - } } diff --git a/nym-node/src/node/mixnet/packet_forwarding/mod.rs b/nym-node/src/node/mixnet/packet_forwarding/mod.rs index e397124ee0..d76e4cea55 100644 --- a/nym-node/src/node/mixnet/packet_forwarding/mod.rs +++ b/nym-node/src/node/mixnet/packet_forwarding/mod.rs @@ -26,16 +26,10 @@ pub struct PacketForwarder { packet_sender: MixForwardingSender, packet_receiver: MixForwardingReceiver, - shutdown: ShutdownToken, } impl PacketForwarder { - pub fn new( - client: C, - routing_filter: F, - metrics: NymNodeMetrics, - shutdown: ShutdownToken, - ) -> Self { + pub fn new(client: C, routing_filter: F, metrics: NymNodeMetrics) -> Self { let (packet_sender, packet_receiver) = mix_forwarding_channels(); PacketForwarder { @@ -45,7 +39,6 @@ impl PacketForwarder { routing_filter, packet_sender, packet_receiver, - shutdown, } } @@ -127,7 +120,7 @@ impl PacketForwarder { .update_packet_forwarder_queue_size(channel_size) } - pub async fn run(&mut self) + pub async fn run(&mut self, shutdown_token: ShutdownToken) where C: SendWithoutResponse, F: RoutingFilter, @@ -137,7 +130,7 @@ impl PacketForwarder { loop { tokio::select! { biased; - _ = self.shutdown.cancelled() => { + _ = shutdown_token.cancelled() => { debug!("PacketForwarder: Received shutdown"); break; } diff --git a/nym-node/src/node/mixnet/shared/mod.rs b/nym-node/src/node/mixnet/shared/mod.rs index 7dee952d44..67dea46a7a 100644 --- a/nym-node/src/node/mixnet/shared/mod.rs +++ b/nym-node/src/node/mixnet/shared/mod.rs @@ -63,7 +63,7 @@ impl ProcessingConfig { } } -// explicitly do NOT derive clone as we want to manually apply relevant suffixes to the task clients +// explicitly do NOT derive clone as we want the childs to use CHILD shutdown tokens pub(crate) struct SharedData { pub(super) processing_config: ProcessingConfig, pub(super) sphinx_keys: ActiveSphinxKeys, @@ -79,7 +79,8 @@ pub(crate) struct SharedData { pub(super) noise_config: NoiseConfig, pub(super) metrics: NymNodeMetrics, - pub(super) shutdown: ShutdownToken, + + pub(super) shutdown_token: ShutdownToken, } fn convert_to_metrics_version(processed: MixPacketVersion) -> PacketKind { @@ -99,7 +100,7 @@ impl SharedData { final_hop: SharedFinalHopData, noise_config: NoiseConfig, metrics: NymNodeMetrics, - shutdown: ShutdownToken, + shutdown_token: ShutdownToken, ) -> Self { SharedData { processing_config, @@ -109,7 +110,7 @@ impl SharedData { final_hop, noise_config, metrics, - shutdown, + shutdown_token, } } @@ -188,10 +189,10 @@ impl SharedData { .mixnet_forwarder .forward_packet(PacketToForward::new(packet, delay_until)) .is_err() - && !self.shutdown.is_cancelled() + && !self.shutdown_token.is_cancelled() { error!("failed to forward sphinx packet on the channel while the process is not going through the shutdown!"); - self.shutdown.cancel(); + self.shutdown_token.cancel(); } } diff --git a/nym-node/src/node/mod.rs b/nym-node/src/node/mod.rs index 6ec9105482..fc3ad47599 100644 --- a/nym-node/src/node/mod.rs +++ b/nym-node/src/node/mod.rs @@ -54,7 +54,7 @@ use nym_noise::config::{NoiseConfig, NoiseNetworkView}; use nym_noise_keys::VersionedNoiseKey; use nym_sphinx_acknowledgements::AckKey; use nym_sphinx_addressing::Recipient; -use nym_task::{ShutdownManager, ShutdownToken, TaskClient}; +use nym_task::{ShutdownManager, ShutdownToken, ShutdownTracker}; use nym_validator_client::UserAgent; use nym_verloc::measurements::SharedVerlocStats; use nym_verloc::{self, measurements::VerlocMeasurer}; @@ -465,19 +465,21 @@ impl NymNode { wireguard: Some(wireguard_data), config, accepted_operator_terms_and_conditions: false, - shutdown_manager: ShutdownManager::new("NymNode") - .with_legacy_task_manager() - .with_default_shutdown_signals() + shutdown_manager: ShutdownManager::build_new_default() .map_err(|source| NymNodeError::ShutdownSignalFailure { source })?, }) } - pub(crate) fn config(&self) -> &Config { - &self.config + pub(crate) fn shutdown_tracker(&self) -> &ShutdownTracker { + self.shutdown_manager.shutdown_tracker() } - pub(crate) fn shutdown_token>(&self, child_suffix: S) -> ShutdownToken { - self.shutdown_manager.clone_token(child_suffix) + pub(crate) fn shutdown_token(&self) -> ShutdownToken { + self.shutdown_manager.clone_shutdown_token() + } + + pub(crate) fn config(&self) -> &Config { + &self.config } pub(crate) fn with_accepted_operator_terms_and_conditions( @@ -561,7 +563,7 @@ impl NymNode { self.config.mixnet.nym_api_urls.clone(), self.config.debug.topology_cache_ttl, self.config.debug.routing_nodes_check_interval, - self.shutdown_manager.clone_token("network-refresher"), + self.shutdown_manager.clone_shutdown_token(), ) .await } @@ -605,7 +607,6 @@ impl NymNode { metrics_sender: MetricEventsSender, active_clients_store: ActiveClientsStore, mix_packet_sender: MixForwardingSender, - task_client: TaskClient, ) -> Result<(), NymNodeError> { let config = gateway_tasks_config(&self.config); @@ -623,7 +624,7 @@ impl NymNode { metrics_sender, self.metrics.clone(), self.entry_gateway.mnemonic.clone(), - task_client, + self.shutdown_tracker().clone(), ); // if we're running in entry mode, start the websocket @@ -632,10 +633,11 @@ impl NymNode { "starting the clients websocket... on {}", self.config.gateway_tasks.ws_bind_address ); - let websocket = gateway_tasks_builder + let mut websocket = gateway_tasks_builder .build_websocket_listener(active_clients_store.clone()) .await?; - websocket.start(); + self.shutdown_tracker() + .try_spawn_named(async move { websocket.run().await }, "EntryWebsocket"); } else { info!("node not running in entry mode: the websocket will remain closed"); } @@ -695,8 +697,12 @@ impl NymNode { } // start task for removing stale and un-retrieved client messages - let stale_messages_cleaner = gateway_tasks_builder.build_stale_messages_cleaner(); - stale_messages_cleaner.start(); + let mut stale_messages_cleaner = gateway_tasks_builder.build_stale_messages_cleaner(); + let shutdown_token = self.shutdown_token(); + self.shutdown_tracker().try_spawn_named( + async move { stale_messages_cleaner.run(shutdown_token).await }, + "StaleMessagesCleaner", + ); Ok(()) } @@ -716,8 +722,11 @@ impl NymNode { // entry gateway info let wireguard = if self.config.wireguard.enabled { + #[allow(deprecated)] Some(api_requests::v1::gateway::models::Wireguard { - port: self.config.wireguard.announced_port, + port: self.config.wireguard.announced_tunnel_port, + tunnel_port: self.config.wireguard.announced_tunnel_port, + metadata_port: self.config.wireguard.announced_metadata_port, public_key: self.x25519_wireguard_key()?.to_string(), }) } else { @@ -870,25 +879,23 @@ impl NymNode { let mut verloc_measurer = VerlocMeasurer::new( config, self.ed25519_identity_keys.clone(), - self.shutdown_manager.clone_token("verloc"), + self.shutdown_manager.clone_shutdown_token(), ); verloc_measurer.set_shared_state(self.verloc_stats.clone()); - tokio::spawn(async move { verloc_measurer.run().await }); + self.shutdown_manager + .try_spawn_named(async move { verloc_measurer.run().await }, "VerlocMeasurer"); } pub(crate) fn setup_metrics_backend( &self, active_clients_store: ActiveClientsStore, active_egress_mixnet_connections: ActiveConnections, - shutdown: ShutdownToken, ) -> 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.clone_with_suffix("aggregator"), - ); + let mut metrics_aggregator = + MetricsAggregator::new(self.config.metrics.debug.aggregator_update_rate); // >>>> START: register all relevant handlers for custom events @@ -945,18 +952,25 @@ impl NymNode { // console logger to preserve old mixnode functionalities if self.config.metrics.debug.log_stats_to_console { - ConsoleLogger::new( + let mut console_logger = ConsoleLogger::new( self.config.metrics.debug.console_logging_update_interval, self.metrics.clone(), - shutdown.clone_with_suffix("metrics-console-logger"), - ) - .start(); + ); + + self.shutdown_tracker().try_spawn_named_with_shutdown( + async move { console_logger.run().await }, + "ConsoleLogger", + ); } let events_sender = metrics_aggregator.sender(); // spawn the aggregator task - metrics_aggregator.start(); + let shutdown_token = self.shutdown_token(); + self.shutdown_tracker().try_spawn_named( + async move { metrics_aggregator.run(shutdown_token).await }, + "MetricsAggregator", + ); events_sender } @@ -978,14 +992,15 @@ impl NymNode { sphinx_keys.keys.primary_key_rotation_id(), sphinx_keys.keys.secondary_key_rotation_id(), self.metrics.clone(), - self.shutdown_manager - .clone_token("replay-detection-background-flush"), + self.shutdown_manager.clone_shutdown_token(), ) .await?; let bloomfilters_manager = replay_detection_background.bloomfilters_manager(); - self.shutdown_manager - .spawn(async move { replay_detection_background.run().await }); + self.shutdown_manager.try_spawn_named( + async move { replay_detection_background.run().await }, + "ReplayDetection", + ); Ok(bloomfilters_manager) } @@ -993,7 +1008,7 @@ impl NymNode { fn setup_nym_apis_client(&self) -> Result { NymApisClient::new( &self.config.mixnet.nym_api_urls, - self.shutdown_manager.clone_token("nym-apis-client"), + self.shutdown_manager.clone_shutdown_token(), ) } @@ -1024,7 +1039,7 @@ impl NymNode { nym_apis_client, replay_protection_manager, managed_keys, - self.shutdown_manager.clone_token("key-rotation-controller"), + self.shutdown_manager.clone_shutdown_token(), ); rotation_controller.start(); @@ -1037,7 +1052,6 @@ impl NymNode { replay_protection_bloomfilter: ReplayProtectionBloomfilters, routing_filter: F, noise_config: NoiseConfig, - shutdown: ShutdownToken, ) -> Result<(MixForwardingSender, ActiveConnections), NymNodeError> where F: RoutingFilter + Send + Sync + 'static, @@ -1068,14 +1082,16 @@ impl NymNode { ); let active_connections = mixnet_client.active_connections(); - let mut packet_forwarder = PacketForwarder::new( - mixnet_client, - routing_filter, - self.metrics.clone(), - shutdown.clone_with_suffix("mix-packet-forwarder"), - ); + let mut packet_forwarder = + PacketForwarder::new(mixnet_client, routing_filter, self.metrics.clone()); let mix_packet_sender = packet_forwarder.sender(); - tokio::spawn(async move { packet_forwarder.run().await }); + + let shutdown_token = self.shutdown_token(); + + self.shutdown_tracker().try_spawn_named( + async move { packet_forwarder.run(shutdown_token).await }, + "PacketForwarder", + ); let final_hop_data = SharedFinalHopData::new( active_clients_store.clone(), @@ -1090,14 +1106,21 @@ impl NymNode { final_hop_data, noise_config, self.metrics.clone(), - shutdown, + self.shutdown_token(), + ); + + let mut mixnet_listener = mixnet::Listener::new(self.config.mixnet.bind_address, shared); + + let shutdown_token = self.shutdown_token(); + self.shutdown_tracker().try_spawn_named( + async move { mixnet_listener.run(shutdown_token).await }, + "MixnetListener", ); - mixnet::Listener::new(self.config.mixnet.bind_address, shared).start(); Ok((mix_packet_sender, active_connections)) } - pub(crate) async fn run_minimal_mixnet_processing(self) -> Result<(), NymNodeError> { + pub(crate) async fn run_minimal_mixnet_processing(mut self) -> Result<(), NymNodeError> { let noise_config = nym_noise::config::NoiseConfig::new( self.x25519_noise_keys.clone(), NoiseNetworkView::new_empty(), @@ -1110,11 +1133,10 @@ impl NymNode { ReplayProtectionBloomfilters::new_disabled(), OpenFilter, noise_config, - self.shutdown_manager.clone_token("mixnet-traffic"), ) .await?; - self.shutdown_manager.close(); + self.shutdown_manager.close_tracker(); self.shutdown_manager.run_until_shutdown().await; Ok(()) @@ -1132,16 +1154,17 @@ impl NymNode { let http_server = self.build_http_server().await?; let bind_address = self.config.http.bind_address; - let server_shutdown = self.shutdown_manager.clone_token("http-server"); + let server_shutdown = self.shutdown_manager.clone_shutdown_token(); - self.shutdown_manager.spawn(async move { - { + self.shutdown_manager.try_spawn_named( + async move { info!("starting NymNodeHTTPServer on {bind_address}"); http_server .with_graceful_shutdown(async move { server_shutdown.cancelled().await }) .await - } - }); + }, + "HttpApi", + ); let nym_apis_client = self.setup_nym_apis_client()?; @@ -1167,14 +1190,12 @@ impl NymNode { bloomfilters_manager.bloomfilters(), network_refresher.routing_filter(), noise_config, - self.shutdown_manager.clone_token("mixnet-traffic"), ) .await?; let metrics_sender = self.setup_metrics_backend( active_clients_store.clone(), active_egress_mixnet_connections, - self.shutdown_manager.clone_token("metrics"), ); self.start_gateway_tasks( @@ -1182,7 +1203,6 @@ impl NymNode { metrics_sender, active_clients_store, mix_packet_sender, - self.shutdown_manager.subscribe_legacy("gateway-tasks"), ) .await?; @@ -1190,7 +1210,7 @@ impl NymNode { .await?; network_refresher.start(); - self.shutdown_manager.close(); + self.shutdown_manager.close_tracker(); Ok(self.shutdown_manager) } diff --git a/nym-node/src/node/nym_apis_client.rs b/nym-node/src/node/nym_apis_client.rs index 08ef68aff7..869270f163 100644 --- a/nym-node/src/node/nym_apis_client.rs +++ b/nym-node/src/node/nym_apis_client.rs @@ -10,7 +10,6 @@ use nym_task::ShutdownToken; use nym_validator_client::client::NymApiClientExt; use nym_validator_client::models::{KeyRotationInfoResponse, NodeRefreshBody}; use nym_validator_client::nym_api::error::NymAPIError; -use nym_validator_client::NymApiClient; use rand::prelude::SliceRandom; use rand::thread_rng; use std::sync::Arc; @@ -27,7 +26,7 @@ pub struct NymApisClient { struct InnerClient { // NOTE: this was implemented before the internal http client supported multiple URLs - active_client: NymApiClient, + active_client: Client, available_urls: Vec, shutdown_token: ShutdownToken, currently_used_api: usize, @@ -45,7 +44,7 @@ impl NymApisClient { let mut urls = nym_apis.to_vec(); urls.shuffle(&mut thread_rng()); - let active_client = nym_http_api_client::Client::builder(urls[0].clone())? + let active_client = Client::builder(urls[0].clone())? .no_hickory_dns() .with_user_agent(NymNode::user_agent()) .with_timeout(Duration::from_secs(5)) @@ -53,7 +52,7 @@ impl NymApisClient { Ok(NymApisClient { inner: Arc::new(RwLock::new(InnerClient { - active_client: NymApiClient::from(active_client), + active_client: active_client.clone(), available_urls: urls, shutdown_token, currently_used_api: 0, @@ -88,9 +87,19 @@ impl NymApisClient { if guard.currently_used_api != last_working_endpoint { drop(guard); let mut guard = self.inner.write().await; - let next_url = guard.available_urls[last_working_endpoint].clone(); guard.currently_used_api = last_working_endpoint; - guard.active_client.change_nym_api(next_url); + + // Provide all URLs starting from the working endpoint for automatic failover + let rotated_urls: Vec<_> = guard + .available_urls + .iter() + .cycle() + .skip(last_working_endpoint) + .take(guard.available_urls.len()) + .map(|u| u.clone().into()) + .collect(); + + guard.active_client.change_base_urls(rotated_urls); } Ok(res) @@ -123,10 +132,10 @@ impl InnerClient { { let broadcast_fut = stream::iter(self.available_urls.clone()).for_each_concurrent(None, |url| { - let nym_api = self - .active_client - .nym_api - .clone_with_new_url(url.clone().into()); + let mut nym_api = self.active_client.clone(); + // For broadcast, we intentionally set a single URL per client + // to ensure each endpoint receives the request + nym_api.change_base_urls(vec![url.clone().into()]); let req_fut = req(nym_api, request_body); async move { if let Err(err) = req_fut.await { @@ -172,10 +181,10 @@ impl InnerClient { .skip(last_working) .chain(self.available_urls.iter().enumerate().take(last_working)) { - let nym_api = self - .active_client - .nym_api - .clone_with_new_url(url.clone().into()); + let mut nym_api = self.active_client.clone(); + // For exhaustive query, we test each endpoint individually in sequence + // to find a working one - so single URL is correct here + nym_api.change_base_urls(vec![url.clone().into()]); let timeout_fut = sleep(timeout_duration); let query_fut = req(nym_api); @@ -216,8 +225,8 @@ impl InnerClient { } } -impl AsRef for InnerClient { - fn as_ref(&self) -> &NymApiClient { +impl AsRef for InnerClient { + fn as_ref(&self) -> &Client { &self.active_client } } diff --git a/nym-node/src/node/shared_network.rs b/nym-node/src/node/shared_network.rs index 7fb88b31b0..b87475254e 100644 --- a/nym-node/src/node/shared_network.rs +++ b/nym-node/src/node/shared_network.rs @@ -7,6 +7,7 @@ use crate::node::routing_filter::network_filter::NetworkRoutingFilter; use async_trait::async_trait; use nym_crypto::asymmetric::ed25519; use nym_gateway::node::UserAgent; +use nym_http_api_client::Client; use nym_node_metrics::prometheus_wrapper::{PrometheusMetric, PROMETHEUS_METRICS}; use nym_noise::config::NoiseNetworkView; use nym_task::ShutdownToken; @@ -20,7 +21,7 @@ use nym_validator_client::nym_api::NymApiClientExt; use nym_validator_client::nym_nodes::{ NodesByAddressesResponse, SemiSkimmedNode, SemiSkimmedNodesWithMetadata, }; -use nym_validator_client::{NymApiClient, ValidatorClientError}; +use nym_validator_client::ValidatorClientError; use std::collections::{HashMap, HashSet}; use std::net::{IpAddr, SocketAddr}; use std::ops::Deref; @@ -35,7 +36,7 @@ use url::Url; const LOCAL_NODE_ID: NodeId = 1234567890; struct NodesQuerier { - client: NymApiClient, + client: Client, nym_api_urls: Vec, currently_used_api: usize, } @@ -49,7 +50,7 @@ impl NodesQuerier { self.currently_used_api = (self.currently_used_api + 1) % self.nym_api_urls.len(); self.client - .change_nym_api(self.nym_api_urls[self.currently_used_api].clone()) + .change_base_urls(self.nym_api_urls.iter().map(|u| u.clone().into()).collect()) } async fn rewarded_set(&mut self) -> Result { @@ -62,7 +63,7 @@ impl NodesQuerier { if res.is_err() { self.use_next_nym_api() } - res + Ok(res?.into()) } async fn current_nymnodes( @@ -77,7 +78,7 @@ impl NodesQuerier { if res.is_err() { self.use_next_nym_api() } - res + Ok(res?) } async fn query_for_info( @@ -86,7 +87,6 @@ impl NodesQuerier { ) -> Result { let res = self .client - .nym_api .nodes_by_addresses(ips) .await .inspect_err(|err| error!("failed to obtain node information: {err}")); @@ -228,7 +228,7 @@ impl NetworkRefresher { let mut this = NetworkRefresher { querier: NodesQuerier { - client: NymApiClient::from(nym_api), + client: nym_api, nym_api_urls, currently_used_api: 0, }, @@ -378,11 +378,12 @@ impl NetworkRefresher { let mut pending_check_interval = interval(self.pending_check_interval); pending_check_interval.reset(); - while !self.shutdown_token.is_cancelled() { + loop { tokio::select! { biased; _ = self.shutdown_token.cancelled() => { - trace!("NetworkRefresher: Received shutdown"); + trace!("NetworkRefresher: Received shutdown"); + break; } _ = pending_check_interval.tick() => { self.inspect_pending().await; diff --git a/nym-node/src/throughput_tester/mod.rs b/nym-node/src/throughput_tester/mod.rs index aaa2360726..81b8f38c4d 100644 --- a/nym-node/src/throughput_tester/mod.rs +++ b/nym-node/src/throughput_tester/mod.rs @@ -132,7 +132,7 @@ pub(crate) fn test_mixing_throughput( let mut tasks_handles = Vec::new(); for (sender_id, stats) in stats.iter().enumerate() { - let token = nym_node.shutdown_token(format!("dummy-load-client-{sender_id}")); + let token = nym_node.shutdown_token(); let client_future = run_testing_client( sender_id, @@ -152,7 +152,7 @@ pub(crate) fn test_mixing_throughput( header_span, stats, output_directory, - nym_node.shutdown_token("global-stats"), + nym_node.shutdown_token(), ); let stats_handle = tester.clients_runtime.spawn(async move { diff --git a/nym-signers-monitor/Cargo.toml b/nym-signers-monitor/Cargo.toml new file mode 100644 index 0000000000..d043aa2871 --- /dev/null +++ b/nym-signers-monitor/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "nym-signers-monitor" +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", "env", "string"] } +humantime = { workspace = true } +itertools = { workspace = true } +time = { workspace = true } +tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } +tracing = { workspace = true } +url = { workspace = true } + +nym-bin-common = { path = "../common/bin-common", features = ["output_format", "basic_tracing"] } +nym-ecash-signer-check = { path = "../common/ecash-signer-check" } +nym-network-defaults = { path = "../common/network-defaults" } +nym-task = { path = "../common/task" } +nym-validator-client = { path = "../common/client-libs/validator-client" } +zulip-client = { path = "../common/zulip-client" } + +[lints] +workspace = true diff --git a/nym-signers-monitor/src/cli/build_info.rs b/nym-signers-monitor/src/cli/build_info.rs new file mode 100644 index 0000000000..077d989975 --- /dev/null +++ b/nym-signers-monitor/src/cli/build_info.rs @@ -0,0 +1,15 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use nym_bin_common::bin_info_owned; +use nym_bin_common::output_format::OutputFormat; + +#[derive(clap::Args, Debug)] +pub(crate) struct Args { + #[arg(short, long, default_value_t = OutputFormat::default())] + output: OutputFormat, +} + +pub(crate) fn execute(args: Args) { + println!("{}", args.output.format(&bin_info_owned!())) +} diff --git a/nym-signers-monitor/src/cli/env.rs b/nym-signers-monitor/src/cli/env.rs new file mode 100644 index 0000000000..2be0cbbb1e --- /dev/null +++ b/nym-signers-monitor/src/cli/env.rs @@ -0,0 +1,20 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +pub(crate) mod vars { + pub(crate) const ZULIP_BOT_EMAIL_ARG: &str = "ZULIP_BOT_EMAIL"; + pub(crate) const ZULIP_BOT_API_KEY_ARG: &str = "ZULIP_BOT_API_KEY"; + pub(crate) const ZULIP_SERVER_URL_ARG: &str = "ZULIP_SERVER_URL"; + pub(crate) const ZULIP_NOTIFICATION_CHANNEL_ID_ARG: &str = "ZULIP_NOTIFICATION_CHANNEL_ID"; + pub(crate) const ZULIP_NOTIFICATION_CHANNEL_TOPIC_ARG: &str = + "ZULIP_NOTIFICATION_CHANNEL_TOPIC"; + + pub(crate) const SIGNERS_MONITOR_CHECK_INTERVAL_ARG: &str = "SIGNERS_MONITOR_CHECK_INTERVAL"; + pub(crate) const SIGNERS_MONITOR_MIN_NOTIFICATION_DELAY_ARG: &str = + "SIGNERS_MONITOR_MIN_NOTIFICATION_DELAY"; + + pub(crate) const KNOWN_NETWORK_NAME_ARG: &str = "KNOWN_NETWORK_NAME"; + pub(crate) const NYXD_CLIENT_CONFIG_ENV_FILE_ARG: &str = "NYXD_CLIENT_CONFIG_ENV_FILE"; + pub(crate) const NYXD_RPC_ENDPOINT_ARG: &str = "NYXD_RPC_ENDPOINT"; + pub(crate) const NYXD_DKG_CONTRACT_ADDRESS_ARG: &str = "NYXD_DKG_CONTRACT_ADDRESS"; +} diff --git a/nym-signers-monitor/src/cli/mod.rs b/nym-signers-monitor/src/cli/mod.rs new file mode 100644 index 0000000000..7a59f459fd --- /dev/null +++ b/nym-signers-monitor/src/cli/mod.rs @@ -0,0 +1,43 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use clap::{Parser, Subcommand}; +use nym_bin_common::bin_info; +use std::sync::OnceLock; + +pub(crate) mod build_info; +pub(crate) mod env; +pub(crate) mod run; + +// Helper for passing LONG_VERSION to clap +fn pretty_build_info_static() -> &'static str { + static PRETTY_BUILD_INFORMATION: OnceLock = OnceLock::new(); + PRETTY_BUILD_INFORMATION.get_or_init(|| bin_info!().pretty_print()) +} + +#[derive(Parser, Debug)] +#[clap(author = "Nymtech", version, long_version = pretty_build_info_static(), about)] +pub(crate) struct Cli { + #[clap(subcommand)] + command: Commands, +} + +impl Cli { + pub async fn execute(self) -> anyhow::Result<()> { + match self.command { + Commands::BuildInfo(args) => build_info::execute(args), + Commands::Run(args) => run::execute(*args).await?, + } + + Ok(()) + } +} + +#[derive(Subcommand, Debug)] +pub(crate) enum Commands { + /// Show build information of this binary + BuildInfo(build_info::Args), + + /// Start signers monitor and send notifications on any failures + Run(Box), +} diff --git a/nym-signers-monitor/src/cli/run.rs b/nym-signers-monitor/src/cli/run.rs new file mode 100644 index 0000000000..b0b0855b49 --- /dev/null +++ b/nym-signers-monitor/src/cli/run.rs @@ -0,0 +1,167 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::cli::env::vars::*; +use crate::monitor::SignersMonitor; +use anyhow::{bail, Context}; +use clap::ArgGroup; +use nym_network_defaults::{setup_env, NymNetworkDetails}; +use nym_validator_client::nyxd::AccountId; +use nym_validator_client::QueryHttpRpcNyxdClient; +use std::time::Duration; +use url::Url; + +#[derive(Debug, clap::Args)] +pub(crate) struct NyxdConnectionArgs { + // for well-known networks, such mainnet, we can use hardcoded values + /// Name of a well known network (such as 'mainnet') that has well-known + /// pre-configured setup values + #[clap(long, env = KNOWN_NETWORK_NAME_ARG)] + pub(crate) known_network_name: Option, + + /// Path pointing to an env file that configures the nyxd client. + #[clap( + short, + long, + env = NYXD_CLIENT_CONFIG_ENV_FILE_ARG + )] + pub(crate) config_env_file: Option, + + /// For unknown networks (or if one wishes to override defaults), + /// specify the RPC endpoint of a node from which signer information should be retrieved + #[clap(long, env = NYXD_RPC_ENDPOINT_ARG)] + pub(crate) nyxd_rpc_endpoint: Option, + + /// For unknown networks, specify address of the DKG contract to pull signer information from. + #[clap( + long, + requires("nyxd_rpc_endpoint"), + env = NYXD_DKG_CONTRACT_ADDRESS_ARG + )] + pub(crate) dkg_contract_address: Option, + // if needed down the line (not sure why), we could define additional args + // for specifying denoms, etc. + // #[clap(long, requires("dkg_contract_address"))] + // pub(crate) mix_denom: Option, +} + +impl NyxdConnectionArgs { + fn get_minimal_nym_network_details(&self) -> anyhow::Result { + if let Some(known_network_name) = &self.known_network_name { + match known_network_name.as_str() { + "mainnet" => return Ok(NymNetworkDetails::new_mainnet()), + other => bail!("{other} is not a known network name - please use another method of setting up chain connection"), + } + } + + if let Some(config_env_file) = &self.config_env_file { + setup_env(Some(config_env_file)); + return Ok(NymNetworkDetails::new_from_env()); + } + + // SAFETY: clap ensures at least one of the fields is set + #[allow(clippy::unwrap_used)] + let dkg_contract = self.dkg_contract_address.as_ref().unwrap(); + + // use mainnet's chain details (i.e. prefixes, denoms, etc) + let mainnet_chain_details = NymNetworkDetails::new_mainnet().chain_details; + Ok(NymNetworkDetails::new_empty() + .with_chain_details(mainnet_chain_details) + .with_coconut_dkg_contract(Some(dkg_contract.to_string()))) + } + + pub(crate) fn try_create_nyxd_client(&self) -> anyhow::Result { + let network_details = self.get_minimal_nym_network_details()?; + + let nyxd_endpoint = match &self.nyxd_rpc_endpoint { + Some(nyxd_rpc_endpoint) => nyxd_rpc_endpoint.clone(), + None => network_details + .endpoints + .first() + .context("no nyxd endpoints provided")? + .nyxd_url + .parse()?, + }; + + Ok(QueryHttpRpcNyxdClient::connect_with_network_details( + nyxd_endpoint.as_str(), + network_details, + )?) + } +} + +#[derive(clap::Args, Debug)] +#[command(group( + ArgGroup::new("nyxd_connection") + .multiple(true) + .required(true) + .args([ + "known_network_name", + "config_env_file", + "nyxd_rpc_endpoint" + ]) +))] +pub(crate) struct Args { + /// Specify email address for the bot responsible for sending notifications to the zulip server + /// in case 'upgrade' mode is detected + #[clap( + long, + env = ZULIP_BOT_EMAIL_ARG + )] + pub(crate) zulip_bot_email: String, + + /// Specify the API key for the bot responsible for sending notifications to the zulip server + /// in case 'upgrade' mode is detected + #[clap( + long, + env = ZULIP_BOT_API_KEY_ARG + )] + pub(crate) zulip_bot_api_key: String, + + /// Specify the sever endpoint for the bot responsible for sending notifications + /// in case 'upgrade' mode is detected + #[clap( + long, + env = ZULIP_SERVER_URL_ARG + )] + pub(crate) zulip_server_url: Url, + + /// Specify the channel id for where the notification is going to be sent + #[clap( + long, + env = ZULIP_NOTIFICATION_CHANNEL_ID_ARG + )] + pub(crate) zulip_notification_channel_id: u32, + + /// Optionally specify the channel topic for where the notification is going to be sent + #[clap( + long, + env = ZULIP_NOTIFICATION_CHANNEL_TOPIC_ARG + )] + pub(crate) zulip_notification_topic: Option, + + /// Specify the delay between subsequent signers checks + #[clap( + long, + env = SIGNERS_MONITOR_CHECK_INTERVAL_ARG, + value_parser = humantime::parse_duration, + default_value = "15m" + )] + pub(crate) signers_check_interval: Duration, + + /// Specify the minimum delay between two subsequent notifications + #[clap( + long, + env = SIGNERS_MONITOR_MIN_NOTIFICATION_DELAY_ARG, + value_parser = humantime::parse_duration, + default_value = "1h" + )] + pub(crate) minimum_notification_delay: Duration, + + #[clap(flatten)] + pub(crate) nyxd_connection: NyxdConnectionArgs, +} + +pub(crate) async fn execute(args: Args) -> anyhow::Result<()> { + SignersMonitor::new(args)?.run().await +} diff --git a/nym-signers-monitor/src/main.rs b/nym-signers-monitor/src/main.rs new file mode 100644 index 0000000000..b2955277e0 --- /dev/null +++ b/nym-signers-monitor/src/main.rs @@ -0,0 +1,24 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::cli::Cli; +use clap::Parser; +use nym_bin_common::bin_info_owned; +use nym_bin_common::logging::setup_tracing_logger; +use tracing::{info, trace}; + +mod cli; +mod monitor; +pub(crate) mod test_result; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + setup_tracing_logger(); + let cli = Cli::parse(); + trace!("args: {cli:#?}"); + + let bin_info = bin_info_owned!(); + info!("using the following version: {bin_info}"); + + cli.execute().await +} diff --git a/nym-signers-monitor/src/monitor.rs b/nym-signers-monitor/src/monitor.rs new file mode 100644 index 0000000000..e1136f7718 --- /dev/null +++ b/nym-signers-monitor/src/monitor.rs @@ -0,0 +1,222 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::cli::run; +use crate::test_result::{DisplayableSignerResult, Summary, TestResult}; +use nym_ecash_signer_check::check_signers_with_client; +use nym_task::ShutdownManager; +use nym_validator_client::QueryHttpRpcNyxdClient; +use std::time::Duration; +use time::OffsetDateTime; +use tokio::time::interval; +use tracing::{error, info}; + +const LOST_QUORUM_MSG: &str = r#" +# 🔥🔥🔥 LOST SIGNING QUORUM 🔥🔥🔥 +We seem to have lost the signing quorum - check if we should enable the 'upgrade' mode! +"#; + +const UNKNOWN_QUORUM_MSG: &str = r#" +# ❓❓❓ UNKNOWN SIGNING QUORUM ❓❓❓ +We can't determine the signing quroum - if we're not undergoing DKG exchange check if we should enable the 'upgrade' mode! +"#; + +pub(crate) struct SignersMonitor { + zulip_client: zulip_client::Client, + nyxd_client: QueryHttpRpcNyxdClient, + + notification_channel_id: u32, + notification_topic: Option, + check_interval: Duration, + min_notification_delay: Duration, + last_notification_sent: Option, +} + +impl SignersMonitor { + pub(crate) fn new(args: run::Args) -> anyhow::Result { + let zulip_client = zulip_client::Client::builder( + args.zulip_bot_email, + args.zulip_bot_api_key, + args.zulip_server_url, + )? + .build()?; + let nyxd_client = args.nyxd_connection.try_create_nyxd_client()?; + + Ok(SignersMonitor { + zulip_client, + nyxd_client, + notification_channel_id: args.zulip_notification_channel_id, + notification_topic: args.zulip_notification_topic, + check_interval: args.signers_check_interval, + min_notification_delay: args.minimum_notification_delay, + last_notification_sent: None, + }) + } + + async fn check_signers(&mut self) -> anyhow::Result { + info!("starting signer check..."); + let check_result = check_signers_with_client(&self.nyxd_client).await?; + + let mut unreachable_signers = 0; + let mut unknown_local_chain_status = 0; + let mut stalled_local_chain = 0; + let mut working_local_chain = 0; + let mut unknown_credential_issuance_status = 0; + let mut working_credential_issuance = 0; + let mut unavailable_credential_issuance = 0; + + let mut fully_working = 0; + + let mut signers = Vec::new(); + for result in &check_result.results { + if result.signer_unreachable() { + unreachable_signers += 1; + } + + if result.unknown_chain_status() { + unknown_local_chain_status += 1; + } + if result.chain_available() { + working_local_chain += 1; + } + if result.chain_provably_stalled() || result.chain_unprovably_stalled() { + stalled_local_chain += 1; + } + + if result.unknown_signing_status() { + unknown_credential_issuance_status += 1; + } + if result.signing_available() { + working_credential_issuance += 1; + } + if result.signing_provably_unavailable() || result.signing_unprovably_unavailable() { + unavailable_credential_issuance += 1; + } + + let signing_available = if result.unknown_signing_status() { + None + } else { + Some(result.signing_available()) + }; + + let chain_not_stalled = if result.unknown_chain_status() { + None + } else { + Some(result.chain_available()) + }; + + if (result.chain_available()) && (result.signing_available()) { + fully_working += 1; + } + + signers.push(DisplayableSignerResult { + version: result + .try_get_test_result() + .map(|r| r.reported_version.clone()), + url: result.information.announce_address.clone(), + signing_available, + chain_not_stalled, + }) + } + + let signing_quorum_available = check_result.threshold.map(|threshold| { + (working_local_chain as u64) >= threshold + && (working_credential_issuance as u64) >= threshold + }); + signers.sort_by_key(|s| s.version.clone()); + + let summary = Summary { + signing_quorum_available, + fully_working, + unreachable_signers, + registered_signers: check_result.results.len(), + unknown_local_chain_status, + stalled_local_chain, + working_local_chain, + unknown_credential_issuance_status, + working_credential_issuance, + unavailable_credential_issuance, + threshold: check_result.threshold, + }; + + Ok(TestResult { summary, signers }) + } + + async fn perform_signer_check(&mut self) -> anyhow::Result<()> { + let result = self.check_signers().await?; + let result_md = result.results_to_markdown_message(); + + let msg = if result.quorum_unavailable() { + Some(format!("{LOST_QUORUM_MSG}\n\n{result_md}",)) + } else if result.quorum_unknown() { + Some(format!("{UNKNOWN_QUORUM_MSG}\n\n{result_md}",)) + } else { + None + }; + + if let Some(msg) = msg { + self.maybe_notify_about_failure(&msg).await? + } + + Ok(()) + } + + async fn maybe_notify_about_failure(&mut self, message: &String) -> anyhow::Result<()> { + if let Some(last_notification_sent) = self.last_notification_sent { + if last_notification_sent + self.min_notification_delay > OffsetDateTime::now_utc() { + info!("too soon to send another notification"); + return Ok(()); + } + } + self.send_zulip_notification(message).await?; + self.last_notification_sent = Some(OffsetDateTime::now_utc()); + Ok(()) + } + + async fn send_zulip_notification(&self, message: &String) -> anyhow::Result<()> { + self.zulip_client + .send_channel_message(( + self.notification_channel_id, + message, + &self.notification_topic, + )) + .await?; + Ok(()) + } + + async fn send_shutdown_notification(&self) -> anyhow::Result<()> { + println!("here be sending shutdown notification"); + Ok(()) + } + + pub(crate) async fn run(&mut self) -> anyhow::Result<()> { + let mut shutdown_manager = ShutdownManager::build_new_default()?; + let root_token = shutdown_manager.clone_shutdown_token(); + + let mut check_interval = interval(self.check_interval); + + while !root_token.is_cancelled() { + tokio::select! { + biased; + _ = root_token.cancelled() => { + info!("received shutdown"); + break; + } + _ = check_interval.tick() => { + if let Err(err) = self.perform_signer_check().await { + error!("failed to check signers: {err}"); + } + } + + } + } + + shutdown_manager.run_until_shutdown().await; + + if let Err(err) = self.send_shutdown_notification().await { + error!("failed to send shutdown notification: {err}"); + } + + Ok(()) + } +} diff --git a/nym-signers-monitor/src/test_result.rs b/nym-signers-monitor/src/test_result.rs new file mode 100644 index 0000000000..1fb091ec14 --- /dev/null +++ b/nym-signers-monitor/src/test_result.rs @@ -0,0 +1,115 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use itertools::Itertools; + +fn maybe_bool_to_emoji_string(maybe_bool: Option) -> String { + match maybe_bool { + None => "⚠️ unknown".into(), + Some(true) => "✅ yes".into(), + Some(false) => "❌ no".into(), + } +} + +pub(crate) struct DisplayableSignerResult { + pub(crate) url: String, + pub(crate) version: Option, + pub(crate) signing_available: Option, + pub(crate) chain_not_stalled: Option, +} + +impl DisplayableSignerResult { + fn to_markdown_table_row(&self) -> String { + format!( + "| {} | {} | {} | {} |", + self.url, + self.version.as_deref().unwrap_or("unknown"), + maybe_bool_to_emoji_string(self.signing_available), + maybe_bool_to_emoji_string(self.chain_not_stalled) + ) + } +} + +pub(crate) struct TestResult { + pub(crate) summary: Summary, + pub(crate) signers: Vec, +} + +impl TestResult { + pub(crate) fn quorum_unavailable(&self) -> bool { + self.summary.signing_quorum_available.unwrap_or(false) + } + + pub(crate) fn quorum_unknown(&self) -> bool { + self.summary.signing_quorum_available.is_none() + } + + pub(crate) fn results_to_markdown_message(&self) -> String { + let p_available = format!( + "{:.2}", + (self.summary.fully_working as f32 / self.summary.registered_signers as f32) * 100. + ); + + format!( + r#" +## Summary +- quorum available: {} ({p_available}% of signers fully available) +- signers fully working: {} +- signing threshold: {} +- registered signers: {} +- unreachable signers: {} + +### Chain Status +- unknown status: {} +- working chain: {} +- stalled chain: {} + +### Credential Issuance Status +(note: signers below 1.1.64 do not return fully reliable results) +- unknown status: {} +- working issuance: {} +- unavailable issuance: {} + +## Detailed Results +| address | version | chain working | issuance (maybe) available | +| - | - | - | - | +{} + "#, + maybe_bool_to_emoji_string(self.summary.signing_quorum_available), + self.summary.fully_working, + self.summary + .threshold + .map(|threshold| threshold.to_string()) + .unwrap_or("???".to_string()), + self.summary.registered_signers, + self.summary.unreachable_signers, + self.summary.unknown_local_chain_status, + self.summary.working_local_chain, + self.summary.stalled_local_chain, + self.summary.unknown_credential_issuance_status, + self.summary.working_credential_issuance, + self.summary.unavailable_credential_issuance, + self.signers + .iter() + .map(|r| r.to_markdown_table_row()) + .join("\n") + ) + } +} + +pub(crate) struct Summary { + pub(crate) signing_quorum_available: Option, + pub(crate) fully_working: usize, + pub(crate) threshold: Option, + + pub(crate) registered_signers: usize, + pub(crate) unreachable_signers: usize, + + pub(crate) unknown_local_chain_status: usize, + pub(crate) stalled_local_chain: usize, + pub(crate) working_local_chain: usize, + + pub(crate) unknown_credential_issuance_status: usize, + pub(crate) working_credential_issuance: usize, + pub(crate) unavailable_credential_issuance: usize, +} diff --git a/nym-statistics-api/src/main.rs b/nym-statistics-api/src/main.rs index 4af7c83a36..c3e8ec919b 100644 --- a/nym-statistics-api/src/main.rs +++ b/nym-statistics-api/src/main.rs @@ -27,19 +27,17 @@ async fn main() -> anyhow::Result<()> { .await?; tracing::info!("Connection to database successful"); - let shutdown_manager = ShutdownManager::new("nym-statistics-api"); + let mut shutdown_manager = ShutdownManager::build_new_default()?; - let network_refresher = NetworkRefresher::initialise_new( - args.nym_api_url, - shutdown_manager.child_token("network-refresher"), - ) - .await; + let network_refresher = + NetworkRefresher::initialise_new(args.nym_api_url, shutdown_manager.child_shutdown_token()) + .await; let http_server = http::server::build_http_api(storage, network_refresher.network_view(), args.http_port) .await .expect("Failed to build http server"); - let server_shutdown = shutdown_manager.clone_token("http-api-server"); + let server_shutdown = shutdown_manager.clone_shutdown_token(); // Starting tasks shutdown_manager.spawn(async move { http_server.run(server_shutdown).await }); @@ -47,7 +45,7 @@ async fn main() -> anyhow::Result<()> { tracing::info!("Started HTTP server on port {}", args.http_port); - shutdown_manager.close(); + shutdown_manager.close_tracker(); shutdown_manager.run_until_shutdown().await; Ok(()) diff --git a/nym-statistics-api/src/network_view.rs b/nym-statistics-api/src/network_view.rs index ca8352d6b0..db3a153de0 100644 --- a/nym-statistics-api/src/network_view.rs +++ b/nym-statistics-api/src/network_view.rs @@ -6,7 +6,6 @@ use nym_task::ShutdownToken; use celes::Country; use nym_validator_client::models::NymNodeDescription; -use nym_validator_client::NymApiClient; use std::collections::HashMap; use std::time::Duration; use std::{net::IpAddr, sync::Arc}; @@ -14,6 +13,8 @@ use tokio::sync::RwLock; use tokio::time::interval; use url::Url; +use nym_http_api_client::Client; +use nym_validator_client::client::NymApiClientExt; use tracing::{error, info, trace, warn}; const NETWORK_CACHE_TTL: Duration = Duration::from_secs(600); @@ -22,7 +23,7 @@ type IpToCountryMap = HashMap>; // SW this should use a proper NS API client once it exists struct NodesQuerier { - client: NymApiClient, + client: Client, } impl NodesQuerier { @@ -99,14 +100,11 @@ impl NetworkRefresher { this } - fn build_http_api_client(url: Url) -> Result { - Ok( - nym_http_api_client::Client::builder::<_, anyhow::Error>(url)? - .no_hickory_dns() - .with_user_agent("node-statistics-api") - .build::()? - .into(), - ) + fn build_http_api_client(url: Url) -> Result { + Ok(Client::builder(url)? + .no_hickory_dns() + .with_user_agent("node-statistics-api") + .build()?) } async fn refresh_network_nodes(&mut self) -> Result<()> { @@ -142,11 +140,12 @@ impl NetworkRefresher { let mut full_refresh_interval = interval(self.full_refresh_interval); full_refresh_interval.reset(); - while !self.shutdown_token.is_cancelled() { + loop { tokio::select! { biased; _ = self.shutdown_token.cancelled() => { trace!("NetworkRefresher: Received shutdown"); + break; } _ = full_refresh_interval.tick() => { if self.refresh_network_nodes().await.is_err() { diff --git a/nym-validator-rewarder/Cargo.toml b/nym-validator-rewarder/Cargo.toml index 3281e9f782..be1ae056dc 100644 --- a/nym-validator-rewarder/Cargo.toml +++ b/nym-validator-rewarder/Cargo.toml @@ -42,6 +42,7 @@ nym-credentials = { path = "../common/credentials" } nym-network-defaults = { path = "../common/network-defaults" } nym-task = { path = "../common/task" } nym-validator-client = { path = "../common/client-libs/validator-client" } +nym-http-api-client = { path = "../common/http-api-client" } nym-coconut-dkg-common = { path = "../common/cosmwasm-smart-contracts/coconut-dkg" } nyxd-scraper = { path = "../common/nyxd-scraper" } nym-ticketbooks-merkle = { path = "../common/ticketbooks-merkle" } @@ -49,6 +50,7 @@ nym-serde-helpers = { path = "../common/serde-helpers", features = ["base64"] } nym-pemstore = { path = "../common/pemstore" } [build-dependencies] +anyhow = { workspace = true } sqlx = { workspace = true, features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"] } tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } diff --git a/nym-validator-rewarder/build.rs b/nym-validator-rewarder/build.rs index 545f9c66b6..fbb510e095 100644 --- a/nym-validator-rewarder/build.rs +++ b/nym-validator-rewarder/build.rs @@ -1,25 +1,24 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -// it's fine if compilation fails -#[allow(clippy::unwrap_used)] -#[allow(clippy::expect_used)] +use anyhow::Context; + #[tokio::main] -async fn main() { +async fn main() -> anyhow::Result<()> { use sqlx::{Connection, SqliteConnection}; use std::env; - let out_dir = env::var("OUT_DIR").unwrap(); + let out_dir = env::var("OUT_DIR")?; let database_path = format!("{out_dir}/scraper-example.sqlite"); let mut conn = SqliteConnection::connect(&format!("sqlite://{database_path}?mode=rwc")) .await - .expect("Failed to create SQLx database connection"); + .context("Failed to create SQLx database connection")?; sqlx::migrate!("./migrations") .run(&mut conn) .await - .expect("Failed to perform SQLx migrations"); + .context("Failed to perform SQLx migrations")?; #[cfg(target_family = "unix")] println!("cargo:rustc-env=DATABASE_URL=sqlite://{}", &database_path); @@ -28,4 +27,6 @@ async fn main() { // for some strange reason we need to add a leading `/` to the windows path even though it's // not a valid windows path... but hey, it works... println!("cargo:rustc-env=DATABASE_URL=sqlite:///{}", &database_path); + + Ok(()) } diff --git a/nym-validator-rewarder/src/cli/dry_run_check_issuer.rs b/nym-validator-rewarder/src/cli/dry_run_check_issuer.rs index 49d0d250d6..f8974bd601 100644 --- a/nym-validator-rewarder/src/cli/dry_run_check_issuer.rs +++ b/nym-validator-rewarder/src/cli/dry_run_check_issuer.rs @@ -9,6 +9,7 @@ use crate::rewarder::ticketbook_issuance::verifier::TicketbookIssuanceVerifier; use crate::rewarder::Rewarder; use anyhow::bail; use nym_ecash_time::ecash_default_expiration_date; +use nym_validator_client::nym_api::NymApiClientExt; use std::collections::HashSet; use std::path::PathBuf; use time::macros::format_description; diff --git a/nym-validator-rewarder/src/error.rs b/nym-validator-rewarder/src/error.rs index 48150139df..31bf710efb 100644 --- a/nym-validator-rewarder/src/error.rs +++ b/nym-validator-rewarder/src/error.rs @@ -15,6 +15,9 @@ use thiserror::Error; #[derive(Debug, Error)] pub enum NymRewarderError { + #[error(transparent)] + IoFailure(#[from] io::Error), + #[error("experienced internal database error: {0}")] InternalDatabaseError(#[from] sqlx::Error), @@ -127,7 +130,7 @@ pub enum NymRewarderError { }, #[error("failed to resolve nym-api query: {0}")] - ApiQueryFailure(#[from] NymAPIError), + ApiQueryFailure(Box), #[error("operator {runner_account} didn't return all requested credentials! requested {requested} but got only {received}")] IncompleteRequest { @@ -210,6 +213,12 @@ pub enum NymRewarderError { NoBlocksProcessedInEpoch { epoch: Epoch }, } +impl From for NymRewarderError { + fn from(err: NymAPIError) -> Self { + NymRewarderError::ApiQueryFailure(Box::new(err)) + } +} + #[derive(Debug)] pub struct InsufficientBalance { pub daily_budget: Coin, diff --git a/nym-validator-rewarder/src/rewarder/mod.rs b/nym-validator-rewarder/src/rewarder/mod.rs index ed38ba1d3c..3674989fa2 100644 --- a/nym-validator-rewarder/src/rewarder/mod.rs +++ b/nym-validator-rewarder/src/rewarder/mod.rs @@ -14,12 +14,11 @@ use futures::future::{FusedFuture, OptionFuture}; use futures::FutureExt; use nym_crypto::asymmetric::ed25519; use nym_ecash_time::{ecash_today, ecash_today_date, EcashTime}; -use nym_task::TaskManager; +use nym_task::ShutdownManager; use nym_validator_client::nyxd::{AccountId, Coin, Hash}; use nyxd_scraper::NyxdScraper; use std::sync::Arc; use time::Date; -use tokio::pin; use tracing::{error, info, instrument, warn}; pub(crate) mod block_signing; @@ -540,7 +539,7 @@ impl Rewarder { async fn main_loop( mut self, - mut task_manager: TaskManager, + mut shutdown_manager: ShutdownManager, mut scraper_cancellation: impl FusedFuture + Unpin, ) { let mut block_signing_epoch_ticker = self @@ -550,17 +549,13 @@ impl Rewarder { // runs daily let mut ticketbook_issuance_ticker = end_of_day_ticker(); - let shutdown_future = task_manager.catch_interrupt(); - pin!(shutdown_future); + let mut shutdown_signals = shutdown_manager.detach_shutdown_signals(); loop { tokio::select! { biased; - interrupt_res = &mut shutdown_future => { + _ = shutdown_signals.wait_for_signal() => { info!("received interrupt"); - if let Err(err) = interrupt_res { - error!("runtime interrupt failure: {err}") - } break; } _ = &mut scraper_cancellation, if !scraper_cancellation.is_terminated() => { @@ -575,13 +570,19 @@ impl Rewarder { if let Some(epoch_signing) = self.epoch_signing { epoch_signing.nyxd_scraper.stop().await; } + + // in case we received cancellation from the scraper, kill other tasks (currently none) + if !shutdown_manager.is_cancelled() { + shutdown_manager.send_cancellation(); + } + shutdown_manager.run_until_shutdown().await } pub async fn run(mut self) -> Result<(), NymRewarderError> { info!("Starting nym validators rewarder"); // setup shutdowns - let task_manager = TaskManager::new(5); + let shutdown_manager = ShutdownManager::build_new_default()?; let scraper_cancellation = self.setup_tasks().await?; if let Err(err) = self.startup_resync().await { @@ -598,7 +599,7 @@ impl Rewarder { return Err(err); } - self.main_loop(task_manager, scraper_cancellation).await; + self.main_loop(shutdown_manager, scraper_cancellation).await; Ok(()) } diff --git a/nym-validator-rewarder/src/rewarder/nyxd_client.rs b/nym-validator-rewarder/src/rewarder/nyxd_client.rs index bf2bd9e43c..c5de2c37b1 100644 --- a/nym-validator-rewarder/src/rewarder/nyxd_client.rs +++ b/nym-validator-rewarder/src/rewarder/nyxd_client.rs @@ -15,7 +15,7 @@ use nym_validator_client::nyxd::module_traits::staking::{ use nym_validator_client::nyxd::{ AccountId, Coin, CosmWasmClient, Hash, PageRequest, StakingQueryClient, }; -use nym_validator_client::{nyxd, DirectSigningHttpRpcNyxdClient, NymApiClient}; +use nym_validator_client::{nyxd, DirectSigningHttpRpcNyxdClient}; use std::collections::HashMap; use std::ops::Deref; use std::sync::Arc; @@ -139,10 +139,23 @@ impl NyxdClient { continue; }; + let api_client = match nym_http_api_client::Client::builder(api_address) + .and_then(|b| b.build()) + { + Ok(client) => client, + Err(err) => { + error!( + "Failed to create API client for issuer {}: {}", + info.assigned_index, err + ); + continue; + } + }; + issuers.push(CredentialIssuer { public_key, operator_account: addr_to_account_id(share.owner), - api_client: NymApiClient::new(api_address), + api_client, verification_key, node_id: info.assigned_index, }) diff --git a/nym-validator-rewarder/src/rewarder/ticketbook_issuance/types.rs b/nym-validator-rewarder/src/rewarder/ticketbook_issuance/types.rs index dc396a7321..f677c0e9bc 100644 --- a/nym-validator-rewarder/src/rewarder/ticketbook_issuance/types.rs +++ b/nym-validator-rewarder/src/rewarder/ticketbook_issuance/types.rs @@ -6,8 +6,8 @@ use cosmwasm_std::{Addr, Decimal, Uint128}; use nym_coconut_dkg_common::types::NodeIndex; use nym_compact_ecash::VerificationKeyAuth; use nym_crypto::asymmetric::ed25519; +use nym_validator_client::nym_api::NymApiClientExt; use nym_validator_client::nyxd::{AccountId, Coin}; -use nym_validator_client::NymApiClient; use std::fmt::{Display, Formatter}; use tracing::info; @@ -68,7 +68,7 @@ impl TicketbookIssuanceResults { pub struct CredentialIssuer { pub public_key: ed25519::PublicKey, pub operator_account: AccountId, - pub api_client: NymApiClient, + pub api_client: nym_http_api_client::Client, pub verification_key: VerificationKeyAuth, pub node_id: NodeIndex, } diff --git a/nym-validator-rewarder/src/rewarder/ticketbook_issuance/verifier.rs b/nym-validator-rewarder/src/rewarder/ticketbook_issuance/verifier.rs index cb0964caf3..dcd7525476 100644 --- a/nym-validator-rewarder/src/rewarder/ticketbook_issuance/verifier.rs +++ b/nym-validator-rewarder/src/rewarder/ticketbook_issuance/verifier.rs @@ -16,6 +16,7 @@ use nym_validator_client::ecash::models::{ IssuedTicketbooksChallengeCommitmentResponse, IssuedTicketbooksDataRequestBody, IssuedTicketbooksDataResponse, IssuedTicketbooksDataResponseBody, IssuedTicketbooksForResponse, }; +use nym_validator_client::nym_api::NymApiClientExt; use nym_validator_client::nyxd::AccountId; use nym_validator_client::signable::{SignableMessageBody, SignedMessage}; use rand::distributions::{Distribution, WeightedIndex}; diff --git a/nym-wallet/Cargo.lock b/nym-wallet/Cargo.lock index f4bff02a23..4603c90e9b 100644 --- a/nym-wallet/Cargo.lock +++ b/nym-wallet/Cargo.lock @@ -3051,7 +3051,7 @@ dependencies = [ "httpdate", "itoa 1.0.15", "pin-project-lite", - "socket2", + "socket2 0.5.9", "tokio", "tower-service", "tracing", @@ -3140,7 +3140,7 @@ dependencies = [ "hyper 1.6.0", "libc", "pin-project-lite", - "socket2", + "socket2 0.5.9", "tokio", "tower-service", "tracing", @@ -3384,13 +3384,33 @@ dependencies = [ "generic-array", ] +[[package]] +name = "inventory" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc61209c082fbeb19919bee74b176221b27223e27b65d781eb91af24eb1fb46e" +dependencies = [ + "rustversion", +] + +[[package]] +name = "io-uring" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" +dependencies = [ + "bitflags 2.9.0", + "cfg-if", + "libc", +] + [[package]] name = "ipconfig" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f" dependencies = [ - "socket2", + "socket2 0.5.9", "widestring", "windows-sys 0.48.0", "winreg 0.50.0", @@ -3631,9 +3651,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.171" +version = "0.2.175" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" +checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" [[package]] name = "libloading" @@ -4125,6 +4145,7 @@ dependencies = [ name = "nym-crypto" version = "0.4.0" dependencies = [ + "base64 0.22.1", "bs58", "ed25519-dalek", "nym-pemstore", @@ -4202,23 +4223,39 @@ dependencies = [ "async-trait", "bincode", "bytes", + "cfg-if", "encoding_rs", "hickory-resolver", "http 1.3.1", + "inventory", "itertools 0.14.0", "mime", "nym-bin-common", + "nym-http-api-client-macro", "nym-http-api-common", "once_cell", "reqwest 0.12.15", "serde", "serde_json", + "serde_plain", + "serde_yaml", "thiserror 2.0.12", "tracing", "url", "wasmtimer", ] +[[package]] +name = "nym-http-api-client-macro" +version = "0.1.0" +dependencies = [ + "proc-macro-crate 3.3.0", + "proc-macro2", + "quote", + "syn 2.0.100", + "uuid", +] + [[package]] name = "nym-http-api-common" version = "0.1.0" @@ -5493,7 +5530,7 @@ dependencies = [ "quinn-udp", "rustc-hash", "rustls 0.23.25", - "socket2", + "socket2 0.5.9", "thiserror 2.0.12", "tokio", "tracing", @@ -5529,7 +5566,7 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2", + "socket2 0.5.9", "tracing", "windows-sys 0.59.0", ] @@ -6297,6 +6334,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_plain" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce1fc6db65a611022b23a0dec6975d63fb80a302cb3388835ff02c097258d50" +dependencies = [ + "serde", +] + [[package]] name = "serde_repr" version = "0.1.20" @@ -6359,6 +6405,19 @@ dependencies = [ "syn 2.0.100", ] +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap 2.8.0", + "itoa 1.0.15", + "ryu", + "serde", + "unsafe-libyaml", +] + [[package]] name = "serdect" version = "0.2.0" @@ -6522,6 +6581,16 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "socket2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + [[package]] name = "softbuffer" version = "0.4.6" @@ -7435,20 +7504,22 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.45.1" +version = "1.47.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779" +checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" dependencies = [ "backtrace", "bytes", + "io-uring", "libc", "mio", "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2", + "slab", + "socket2 0.6.0", "tokio-macros", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -7836,6 +7907,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + [[package]] name = "untrusted" version = "0.9.0" diff --git a/nym-wallet/src-tauri/src/error.rs b/nym-wallet/src-tauri/src/error.rs index 3e3123e088..6f491d4532 100644 --- a/nym-wallet/src-tauri/src/error.rs +++ b/nym-wallet/src-tauri/src/error.rs @@ -2,7 +2,6 @@ use nym_contracts_common::signing::SigningAlgorithm; use nym_crypto::asymmetric::ed25519::Ed25519RecoveryError; use nym_node_requests::api::client::NymNodeApiClientError; use nym_types::error::TypesError; -use nym_validator_client::nym_api::error::NymAPIError; use nym_validator_client::signing::direct_wallet::DirectSecp256k1HdWalletError; use nym_validator_client::{nyxd::error::NyxdError, ValidatorClientError}; use nym_wallet_types::network::Network; @@ -45,15 +44,7 @@ pub enum BackendError { source: eyre::Report, }, #[error(transparent)] - NymApiError { - #[from] - source: NymAPIError, - }, - #[error(transparent)] - NymNodeApiError { - #[from] - source: NymNodeApiClientError, - }, + NymNodeApiError { source: Box }, #[error(transparent)] IOError { #[from] @@ -214,3 +205,11 @@ impl From for BackendError { TypesError::from(e).into() } } + +impl From for BackendError { + fn from(e: NymNodeApiClientError) -> Self { + BackendError::NymNodeApiError { + source: Box::new(e), + } + } +} diff --git a/nym-wallet/src-tauri/src/operations/mixnet/bond.rs b/nym-wallet/src-tauri/src/operations/mixnet/bond.rs index 881b54aa3c..3fbf91bbe1 100644 --- a/nym-wallet/src-tauri/src/operations/mixnet/bond.rs +++ b/nym-wallet/src-tauri/src/operations/mixnet/bond.rs @@ -15,7 +15,6 @@ use nym_mixnet_contract_common::nym_node::{NodeConfigUpdate, StakeSaturationResp use nym_mixnet_contract_common::{MixNodeConfigUpdate, NodeId, NymNode}; use nym_node_requests::api::client::NymNodeApiClientExt; use nym_node_requests::api::v1::node::models::NodeDescription; -use nym_node_requests::api::ErrorResponse; use nym_types::currency::DecCoin; use nym_types::gateway::GatewayBond; use nym_types::mixnode::{MixNodeDetails, NodeCostParams}; @@ -586,14 +585,12 @@ pub async fn get_nym_node_description( port: u16, ) -> Result { Ok( - nym_node_requests::api::Client::builder::<_, ErrorResponse>(format!( - "http://{host}:{port}" - ))? - .with_timeout(Duration::from_millis(1000)) - .with_user_agent(format!("nym-wallet/{}", env!("CARGO_PKG_VERSION"))) - .build::()? - .get_description() - .await?, + nym_node_requests::api::Client::builder(format!("http://{host}:{port}"))? + .with_timeout(Duration::from_millis(1000)) + .with_user_agent(format!("nym-wallet/{}", env!("CARGO_PKG_VERSION"))) + .build()? + .get_description() + .await?, ) } diff --git a/nym-wallet/src-tauri/src/operations/nym_api/status.rs b/nym-wallet/src-tauri/src/operations/nym_api/status.rs index 2f3332b700..e17c828514 100644 --- a/nym-wallet/src-tauri/src/operations/nym_api/status.rs +++ b/nym-wallet/src-tauri/src/operations/nym_api/status.rs @@ -4,15 +4,57 @@ use crate::api_client; use crate::error::BackendError; use crate::state::WalletState; +use cosmwasm_std::testing::mock_env; +use log::error; +use nym_mixnet_contract_common::reward_params::RewardedSetParams; +use nym_mixnet_contract_common::rewarding::RewardEstimate; use nym_mixnet_contract_common::{ - reward_params::Performance, Coin, IdentityKeyRef, NodeId, Percent, + reward_params::Performance, Coin, IdentityKeyRef, Interval, IntervalRewardParams, NodeId, + Percent, RewardingParams, }; use nym_validator_client::client::NymApiClientExt; use nym_validator_client::models::{ - AnnotationResponse, ComputeRewardEstParam, DisplayRole, GatewayCoreStatusResponse, - GatewayStatusReportResponse, MixnodeCoreStatusResponse, MixnodeStatusResponse, - RewardEstimationResponse, StakeSaturationResponse, + AnnotationResponse, DisplayRole, GatewayCoreStatusResponse, GatewayStatusReportResponse, + MixnodeCoreStatusResponse, MixnodeStatusResponse, StakeSaturationResponse, }; +use serde::{Deserialize, Serialize}; +use std::time::Duration; + +#[derive(Serialize, Deserialize)] +pub struct LegacyRewardEstimationResponse { + estimation: RewardEstimate, + reward_params: RewardingParams, + epoch: Interval, + as_at: i64, +} + +impl LegacyRewardEstimationResponse { + fn empty() -> LegacyRewardEstimationResponse { + LegacyRewardEstimationResponse { + estimation: Default::default(), + reward_params: RewardingParams { + interval: IntervalRewardParams { + reward_pool: Default::default(), + staking_supply: Default::default(), + staking_supply_scale_factor: Default::default(), + epoch_reward_budget: Default::default(), + stake_saturation_point: Default::default(), + sybil_resistance: Default::default(), + active_set_work_factor: Default::default(), + interval_pool_emission: Default::default(), + }, + rewarded_set: RewardedSetParams { + entry_gateways: 0, + exit_gateways: 0, + mixnodes: 0, + standby: 0, + }, + }, + epoch: Interval::init_interval(720, Duration::from_secs(60), &mock_env()), + as_at: 0, + } + } +} // TODO: fix later (yeah...) #[allow(deprecated)] @@ -45,68 +87,71 @@ pub async fn gateway_core_node_status( #[tauri::command] pub async fn gateway_report( identity: IdentityKeyRef<'_>, - state: tauri::State<'_, WalletState>, + _: tauri::State<'_, WalletState>, ) -> Result { - Ok(api_client!(state).get_gateway_report(identity).await?) + error!("‼️‼️‼️ using legacy and no longer supported gateway report query! returning a default response"); + Ok(GatewayStatusReportResponse { + identity: identity.to_string(), + owner: "".to_string(), + most_recent: 0, + last_hour: 0, + last_day: 0, + }) } // TODO: fix later (yeah...) #[allow(deprecated)] #[tauri::command] pub async fn mixnode_status( - mix_id: NodeId, - state: tauri::State<'_, WalletState>, + _: NodeId, + _: tauri::State<'_, WalletState>, ) -> Result { - Ok(api_client!(state).get_mixnode_status(mix_id).await?) + error!("‼️‼️‼️ using legacy and no longer supported mixnode status query! returning a default response"); + Ok(MixnodeStatusResponse { + status: Default::default(), + }) } // TODO: fix later (yeah...) #[allow(deprecated)] #[tauri::command] pub async fn mixnode_reward_estimation( - mix_id: NodeId, - state: tauri::State<'_, WalletState>, -) -> Result { - Ok(api_client!(state) - .get_mixnode_reward_estimation(mix_id) - .await?) + _: NodeId, + _: tauri::State<'_, WalletState>, +) -> Result { + error!("‼️‼️‼️ using legacy and no longer supported mixnode reward estimation! returning a default response"); + Ok(LegacyRewardEstimationResponse::empty()) } // TODO: fix later (yeah...) #[allow(deprecated)] #[tauri::command] pub async fn compute_mixnode_reward_estimation( - mix_id: u32, - performance: Option, - pledge_amount: Option, - total_delegation: Option, - interval_operating_cost: Option, - profit_margin_percent: Option, - state: tauri::State<'_, WalletState>, -) -> Result { - let request_body = ComputeRewardEstParam { - performance, - active_in_rewarded_set: Some(true), - pledge_amount, - total_delegation, - interval_operating_cost, - profit_margin_percent, - }; - Ok(api_client!(state) - .compute_mixnode_reward_estimation(mix_id, &request_body) - .await?) + _: u32, + _: Option, + _: Option, + _: Option, + _: Option, + _: Option, + _: tauri::State<'_, WalletState>, +) -> Result { + error!("‼️‼️‼️ using legacy and no longer supported mixnode reward estimation! returning a default response"); + Ok(LegacyRewardEstimationResponse::empty()) } // TODO: fix later (yeah...) #[allow(deprecated)] #[tauri::command] pub async fn mixnode_stake_saturation( - mix_id: NodeId, - state: tauri::State<'_, WalletState>, + _: NodeId, + _: tauri::State<'_, WalletState>, ) -> Result { - Ok(api_client!(state) - .get_mixnode_stake_saturation(mix_id) - .await?) + error!("‼️‼️‼️ using legacy and no longer supported mixnode stake saturation! returning a default response"); + Ok(StakeSaturationResponse { + saturation: Default::default(), + uncapped_saturation: Default::default(), + as_at: 0, + }) } #[tauri::command] diff --git a/nym-wg-gateway-client/Cargo.toml b/nym-wg-gateway-client/Cargo.toml new file mode 100644 index 0000000000..7870fd96b7 --- /dev/null +++ b/nym-wg-gateway-client/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "nym-wg-gateway-client" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true +edition.workspace = true +license.workspace = true + +[lints] +workspace = true + +[dependencies] +nym-authenticator-client = { path = "../nym-authenticator-client" } +nym-authenticator-requests = { path = "../common/authenticator-requests" } +nym-bandwidth-controller = { path = "../common/bandwidth-controller" } +nym-credentials-interface = { path = "../common/credentials-interface" } +nym-crypto = { path = "../common/crypto" } +nym-node-requests = { path = "../nym-node/nym-node-requests" } +nym-pemstore = { path = "../common/pemstore" } +nym-sdk = { path = "../sdk/rust/nym-sdk" } +nym-statistics-common = { path = "../common/statistics" } +nym-validator-client = { path = "../common/client-libs/validator-client" } +rand.workspace = true +thiserror.workspace = true +tracing.workspace = true +url.workspace = true diff --git a/nym-wg-gateway-client/src/deprecated.rs b/nym-wg-gateway-client/src/deprecated.rs new file mode 100644 index 0000000000..65a9d47950 --- /dev/null +++ b/nym-wg-gateway-client/src/deprecated.rs @@ -0,0 +1,154 @@ +use std::time::Duration; + +use nym_authenticator_client::{ + AuthenticatorClient, AuthenticatorResponse, AuthenticatorVersion, ClientMessage, + QueryMessageImpl, +}; +use nym_authenticator_requests::{v3, v4, v5}; +use nym_credentials_interface::CredentialSpendingData; +use nym_crypto::asymmetric::encryption; +use nym_node_requests::api::v1::gateway::client_interfaces::wireguard::models::PeerPublicKey; +use nym_sdk::mixnet::Recipient; + +use crate::error::{Error, Result}; + +const RETRY_PERIOD: Duration = Duration::from_secs(30); + +impl crate::WgGatewayClient { + pub fn light_client(&self) -> WgGatewayLightClient { + WgGatewayLightClient { + public_key: *self.keypair.public_key(), + auth_client: self.auth_client.clone(), + } + } +} + +#[derive(Clone)] +pub struct WgGatewayLightClient { + public_key: encryption::PublicKey, + auth_client: AuthenticatorClient, +} +impl WgGatewayLightClient { + pub fn auth_recipient(&self) -> Recipient { + self.auth_client.auth_recipient() + } + + pub fn auth_client(&self) -> &AuthenticatorClient { + &self.auth_client + } + + pub fn set_auth_client(&mut self, auth_client: AuthenticatorClient) { + self.auth_client = auth_client; + } + pub async fn query_bandwidth(&mut self) -> Result> { + let query_message = match self.auth_client.auth_version() { + AuthenticatorVersion::V2 => ClientMessage::Query(Box::new(QueryMessageImpl { + pub_key: PeerPublicKey::new(self.public_key.to_bytes().into()), + version: AuthenticatorVersion::V2, + })), + AuthenticatorVersion::V3 => ClientMessage::Query(Box::new(QueryMessageImpl { + pub_key: PeerPublicKey::new(self.public_key.to_bytes().into()), + version: AuthenticatorVersion::V3, + })), + AuthenticatorVersion::V4 => ClientMessage::Query(Box::new(QueryMessageImpl { + pub_key: PeerPublicKey::new(self.public_key.to_bytes().into()), + version: AuthenticatorVersion::V4, + })), + AuthenticatorVersion::V5 => ClientMessage::Query(Box::new(QueryMessageImpl { + pub_key: PeerPublicKey::new(self.public_key.to_bytes().into()), + version: AuthenticatorVersion::V5, + })), + AuthenticatorVersion::UNKNOWN => return Err(Error::UnsupportedAuthenticatorVersion), + }; + let response = self.auth_client.send(&query_message).await?; + + let available_bandwidth = match response { + nym_authenticator_client::AuthenticatorResponse::RemainingBandwidth( + remaining_bandwidth_response, + ) => { + if let Some(available_bandwidth) = + remaining_bandwidth_response.available_bandwidth() + { + available_bandwidth + } else { + return Ok(None); + } + } + _ => return Err(Error::InvalidGatewayAuthResponse), + }; + + let remaining_pretty = if available_bandwidth > 1024 * 1024 { + format!("{:.2} MB", available_bandwidth as f64 / 1024.0 / 1024.0) + } else { + format!("{} KB", available_bandwidth / 1024) + }; + tracing::debug!( + "Remaining wireguard bandwidth with gateway {} for today: {}", + self.auth_client.auth_recipient().gateway(), + remaining_pretty + ); + if available_bandwidth < 1024 * 1024 { + tracing::warn!( + "Remaining bandwidth is under 1 MB. The wireguard mode will get suspended after that until tomorrow, UTC time. The client might shutdown with timeout soon" + ); + } + Ok(Some(available_bandwidth)) + } + async fn send(&mut self, msg: ClientMessage) -> Result { + let now = std::time::Instant::now(); + while now.elapsed() < RETRY_PERIOD { + match self.auth_client.send(&msg).await { + Ok(response) => return Ok(response), + Err(nym_authenticator_client::Error::TimeoutWaitingForConnectResponse) => continue, + Err(source) => { + if msg.is_wasteful() { + return Err(Error::NoRetry { source }); + } else { + return Err(Error::AuthenticatorClientError(source)); + } + } + } + } + if msg.is_wasteful() { + Err(Error::NoRetry { + source: nym_authenticator_client::Error::TimeoutWaitingForConnectResponse, + }) + } else { + Err(Error::AuthenticatorClientError( + nym_authenticator_client::Error::TimeoutWaitingForConnectResponse, + )) + } + } + + pub async fn top_up(&mut self, credential: CredentialSpendingData) -> Result { + let top_up_message = match self.auth_client.auth_version() { + AuthenticatorVersion::V3 => ClientMessage::TopUp(Box::new(v3::topup::TopUpMessage { + pub_key: PeerPublicKey::new(self.public_key.to_bytes().into()), + credential, + })), + // NOTE: looks like a bug here using v3. But we're leaving it as is since it's working + // and V4 is deprecated in favour of V5 + AuthenticatorVersion::V4 => ClientMessage::TopUp(Box::new(v4::topup::TopUpMessage { + pub_key: PeerPublicKey::new(self.public_key.to_bytes().into()), + credential, + })), + AuthenticatorVersion::V5 => ClientMessage::TopUp(Box::new(v5::topup::TopUpMessage { + pub_key: PeerPublicKey::new(self.public_key.to_bytes().into()), + credential, + })), + AuthenticatorVersion::V2 | AuthenticatorVersion::UNKNOWN => { + return Err(Error::UnsupportedAuthenticatorVersion); + } + }; + let response = self.send(top_up_message).await?; + + let remaining_bandwidth = match response { + AuthenticatorResponse::TopUpBandwidth(top_up_bandwidth_response) => { + top_up_bandwidth_response.available_bandwidth() + } + _ => return Err(Error::InvalidGatewayAuthResponse), + }; + + Ok(remaining_bandwidth) + } +} diff --git a/nym-wg-gateway-client/src/error.rs b/nym-wg-gateway-client/src/error.rs new file mode 100644 index 0000000000..9223c04a12 --- /dev/null +++ b/nym-wg-gateway-client/src/error.rs @@ -0,0 +1,48 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use nym_credentials_interface::TicketType; +use nym_sdk::mixnet::NodeIdentity; + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("received invalid response from gateway authenticator")] + InvalidGatewayAuthResponse, + + #[error("unknown authenticator version number")] + UnsupportedAuthenticatorVersion, + + #[error(transparent)] + AuthenticatorClientError(#[from] nym_authenticator_client::Error), + + #[error("error that should stop auto retrying")] + NoRetry { + #[source] + source: nym_authenticator_client::Error, + }, + + #[error("verification failure")] + VerificationFailed(#[source] nym_authenticator_requests::Error), + + #[error("failed to parse entry gateway socket addr")] + FailedToParseEntryGatewaySocketAddr(#[source] std::net::AddrParseError), + + #[error("failed to get {ticketbook_type} ticket")] + GetTicket { + ticketbook_type: TicketType, + #[source] + source: nym_bandwidth_controller::error::BandwidthControllerError, + }, +} + +#[derive(Debug, thiserror::Error)] +pub enum ErrorMessage { + #[error("out of bandwidth for gateway: {gateway_id}")] + OutOfBandwidth { gateway_id: Box }, + + #[error("gateway {gateway_id} is erroring out")] + ErrorsFromGateway { gateway_id: Box }, +} + +// Result type based on our error type +pub type Result = std::result::Result; diff --git a/nym-wg-gateway-client/src/lib.rs b/nym-wg-gateway-client/src/lib.rs new file mode 100644 index 0000000000..3d425a6e2f --- /dev/null +++ b/nym-wg-gateway-client/src/lib.rs @@ -0,0 +1,302 @@ +// Copyright 2023-2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +pub mod deprecated; +mod error; + +use std::{ + net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}, + path::PathBuf, + str::FromStr, +}; + +pub use error::{Error, ErrorMessage}; +use nym_authenticator_client::{ + AuthenticatorClient, AuthenticatorMixnetClient, AuthenticatorResponse, AuthenticatorVersion, + ClientMessage, +}; +use nym_authenticator_requests::{v2, v3, v4, v5}; +use nym_bandwidth_controller::PreparedCredential; +use nym_credentials_interface::TicketType; +use nym_crypto::asymmetric::{encryption, x25519::KeyPair, x25519::PublicKey}; +use nym_node_requests::api::v1::gateway::client_interfaces::wireguard::models::PeerPublicKey; +use nym_pemstore::KeyPairPath; +use nym_sdk::mixnet::{CredentialStorage, NodeIdentity, Recipient}; +use nym_validator_client::QueryHttpRpcNyxdClient; +use rand::{rngs::OsRng, CryptoRng, RngCore}; +use tracing::{debug, error, trace}; + +use crate::error::Result; + +pub const DEFAULT_PRIVATE_ENTRY_WIREGUARD_KEY_FILENAME: &str = "free_private_entry_wireguard.pem"; +pub const DEFAULT_PUBLIC_ENTRY_WIREGUARD_KEY_FILENAME: &str = "free_public_entry_wireguard.pem"; +pub const DEFAULT_PRIVATE_EXIT_WIREGUARD_KEY_FILENAME: &str = "free_private_exit_wireguard.pem"; +pub const DEFAULT_PUBLIC_EXIT_WIREGUARD_KEY_FILENAME: &str = "free_public_exit_wireguard.pem"; + +pub const TICKETS_TO_SPEND: u32 = 1; + +#[derive(Clone, Debug)] +pub struct GatewayData { + pub public_key: PublicKey, + pub endpoint: SocketAddr, + pub private_ipv4: Ipv4Addr, + pub private_ipv6: Ipv6Addr, +} + +pub struct WgGatewayClient { + keypair: encryption::KeyPair, + auth_client: AuthenticatorClient, +} + +impl WgGatewayClient { + fn new_type( + data_path: &Option, + auth_mix_client: AuthenticatorMixnetClient, + auth_recipient: Recipient, + auth_version: AuthenticatorVersion, + private_file_name: &str, + public_file_name: &str, + ) -> Self { + let mut rng = OsRng; + let auth_client = AuthenticatorClient::new(auth_mix_client, auth_recipient, auth_version); + if let Some(data_path) = data_path { + let paths = KeyPairPath::new( + data_path.join(private_file_name), + data_path.join(public_file_name), + ); + let keypair = load_or_generate_keypair(&mut rng, paths); + WgGatewayClient { + keypair, + auth_client, + } + } else { + WgGatewayClient { + keypair: KeyPair::new(&mut rng), + auth_client, + } + } + } + + pub fn new_entry( + data_path: &Option, + auth_mix_client: AuthenticatorMixnetClient, + auth_recipient: Recipient, + auth_version: AuthenticatorVersion, + ) -> Self { + Self::new_type( + data_path, + auth_mix_client, + auth_recipient, + auth_version, + DEFAULT_PRIVATE_ENTRY_WIREGUARD_KEY_FILENAME, + DEFAULT_PUBLIC_ENTRY_WIREGUARD_KEY_FILENAME, + ) + } + + pub fn new_exit( + data_path: &Option, + auth_mix_client: AuthenticatorMixnetClient, + auth_recipient: Recipient, + auth_version: AuthenticatorVersion, + ) -> Self { + Self::new_type( + data_path, + auth_mix_client, + auth_recipient, + auth_version, + DEFAULT_PRIVATE_EXIT_WIREGUARD_KEY_FILENAME, + DEFAULT_PUBLIC_EXIT_WIREGUARD_KEY_FILENAME, + ) + } + + pub fn keypair(&self) -> &encryption::KeyPair { + &self.keypair + } + + pub fn auth_recipient(&self) -> Recipient { + self.auth_client.auth_recipient() + } + + pub fn auth_version(&self) -> AuthenticatorVersion { + self.auth_client.auth_version() + } + + pub async fn request_bandwidth( + gateway_id: NodeIdentity, + controller: &nym_bandwidth_controller::BandwidthController, + ticketbook_type: TicketType, + ) -> Result + where + ::StorageError: Send + Sync + 'static, + { + let credential = controller + .prepare_ecash_ticket(ticketbook_type, gateway_id.to_bytes(), TICKETS_TO_SPEND) + .await + .map_err(|source| Error::GetTicket { + ticketbook_type, + source, + })?; + Ok(credential) + } + + pub async fn register_wireguard( + &mut self, + gateway_host: IpAddr, + controller: &nym_bandwidth_controller::BandwidthController, + ticketbook_type: TicketType, + ) -> Result + where + ::StorageError: Send + Sync + 'static, + { + debug!("Registering with the wg gateway..."); + let init_message = match self.auth_version() { + AuthenticatorVersion::V2 => { + ClientMessage::Initial(Box::new(v2::registration::InitMessage { + pub_key: PeerPublicKey::new(self.keypair.public_key().to_bytes().into()), + })) + } + AuthenticatorVersion::V3 => { + ClientMessage::Initial(Box::new(v3::registration::InitMessage { + pub_key: PeerPublicKey::new(self.keypair.public_key().to_bytes().into()), + })) + } + AuthenticatorVersion::V4 => { + ClientMessage::Initial(Box::new(v4::registration::InitMessage { + pub_key: PeerPublicKey::new(self.keypair.public_key().to_bytes().into()), + })) + } + AuthenticatorVersion::V5 => { + ClientMessage::Initial(Box::new(v5::registration::InitMessage { + pub_key: PeerPublicKey::new(self.keypair.public_key().to_bytes().into()), + })) + } + AuthenticatorVersion::UNKNOWN => return Err(Error::UnsupportedAuthenticatorVersion), + }; + trace!("sending init msg to {}: {:?}", &gateway_host, &init_message); + let response = self.auth_client.send(&init_message).await?; + let registered_data = match response { + AuthenticatorResponse::PendingRegistration(pending_registration_response) => { + // Unwrap since we have already checked that we have the keypair. + debug!("Verifying data"); + if let Err(e) = pending_registration_response.verify(self.keypair.private_key()) { + return Err(Error::VerificationFailed(e)); + } + + trace!( + "received \"pending-registration\" msg from {}: {:?}", + &gateway_host, + &pending_registration_response + ); + + let credential = Some( + Self::request_bandwidth( + self.auth_recipient().gateway(), + controller, + ticketbook_type, + ) + .await? + .data, + ); + + let finalized_message = match self.auth_version() { + AuthenticatorVersion::V2 => { + ClientMessage::Final(Box::new(v2::registration::FinalMessage { + gateway_client: v2::registration::GatewayClient::new( + self.keypair.private_key(), + pending_registration_response.pub_key().inner(), + pending_registration_response.private_ips().ipv4.into(), + pending_registration_response.nonce(), + ), + credential, + })) + } + AuthenticatorVersion::V3 => { + ClientMessage::Final(Box::new(v3::registration::FinalMessage { + gateway_client: v3::registration::GatewayClient::new( + self.keypair.private_key(), + pending_registration_response.pub_key().inner(), + pending_registration_response.private_ips().ipv4.into(), + pending_registration_response.nonce(), + ), + credential, + })) + } + AuthenticatorVersion::V4 => { + ClientMessage::Final(Box::new(v4::registration::FinalMessage { + gateway_client: v4::registration::GatewayClient::new( + self.keypair.private_key(), + pending_registration_response.pub_key().inner(), + pending_registration_response.private_ips().into(), + pending_registration_response.nonce(), + ), + credential, + })) + } + AuthenticatorVersion::V5 => { + ClientMessage::Final(Box::new(v5::registration::FinalMessage { + gateway_client: v5::registration::GatewayClient::new( + self.keypair.private_key(), + pending_registration_response.pub_key().inner(), + pending_registration_response.private_ips(), + pending_registration_response.nonce(), + ), + credential, + })) + } + AuthenticatorVersion::UNKNOWN => { + return Err(Error::UnsupportedAuthenticatorVersion); + } + }; + trace!( + "sending final msg to {}: {:?}", + &gateway_host, + &finalized_message + ); + + let response = self.auth_client.send(&finalized_message).await?; + let AuthenticatorResponse::Registered(registered_response) = response else { + return Err(Error::InvalidGatewayAuthResponse); + }; + registered_response + } + AuthenticatorResponse::Registered(registered_response) => registered_response, + _ => return Err(Error::InvalidGatewayAuthResponse), + }; + + trace!( + "received \"registered\" msg from {}: {:?}", + &gateway_host, + ®istered_data + ); + + let gateway_data = GatewayData { + public_key: registered_data.pub_key().inner().into(), + endpoint: SocketAddr::from_str(&format!( + "{}:{}", + gateway_host, + registered_data.wg_port() + )) + .map_err(Error::FailedToParseEntryGatewaySocketAddr)?, + private_ipv4: registered_data.private_ips().ipv4, + private_ipv6: registered_data.private_ips().ipv6, + }; + + Ok(gateway_data) + } +} + +fn load_or_generate_keypair(rng: &mut R, paths: KeyPairPath) -> KeyPair { + match nym_pemstore::load_keypair(&paths) { + Ok(keypair) => keypair, + Err(_) => { + let keypair = KeyPair::new(rng); + if let Err(e) = nym_pemstore::store_keypair(&keypair, &paths) { + error!( + "could not store generated keypair at {:?} - {:?}; will use ephemeral keys", + paths, e + ); + } + keypair + } + } +} diff --git a/scripts/delegation-program/README.md b/scripts/delegation-program/README.md new file mode 100644 index 0000000000..b39bc2468a --- /dev/null +++ b/scripts/delegation-program/README.md @@ -0,0 +1,60 @@ +## Stake Adjustment Program (`stake_adjustment.py`) + +### Overview + +This simple argument based program is designed primarily for Delegation program management. + +The main goal is to generate a csv of which first two columns without headers can be passed to nym-cli as input for a quick delegation adjustment, using this command: + +```sh +./nym-cli mixnet delegators delegate-multi --mnemonic "" --input /.csv +``` + +The default values are in sync with DP rules: + +`--wallet_address`: Nym Team DP wallet address +`--saturation`: 250k NYM +`--stake_cap`: 90% as per DP rules +`--adjustment_step`: 25k NYM as per DP rules +`--max_wallet_delegation`: 125k NYM as per DP rules +`--denom`: NYM not uNYM to make it smoother and aligned with delegate-multi command of nym-cli + +Additionaly the program scrapes [api.nym.spectredao.net/api/v1/nodes](https://api.nym.spectredao.net/api/v1/nodes) and [validator.nymtech.net/api/v1/nym-nodes/described](https://validator.nymtech.net/api/v1/nym-nodes/described) endpoints and returns a sheet with 20 values per eacvh node passed in the csv input. + +The outcome is a table with these values: +NODE ID, SUGGESTED WALLET DELEGATION, CURRENT WALLET DELEGATION, SUGGESTED TOTAL STAKE, CURRENT TOTAL STAKE, SUGGESTED SATURATION, CURRENT SATURATION, UPTIME, VERSION, T&C, BINARY, ROLE, WIREGUARD, IP ADDRESS, HOSTNAME, WSS PORT, MONIKER, IDENTITY KEY, BONDING WALLET, EXPLORER URL. + +### Install and Run + +1. Download from this branch and make executable +```sh +wget https://raw.githubusercontent.com/nymtech/nym/refs/heads/develop/scripts/delegation-program/stake_adjustment.py && chmod u+x stake_adjustment.py +``` +2. Make a simple column csv with `node_ids` (for DP, use all the DP nodes `node_id` column, and save it to the same dir, example can be `input.csv`: +```csv +# example +1398 +1365 +2464 +1423 +1269 +1870 +1824 +1707 +``` +3. Run the program with the `input.csv` positional arg (for DP use the default values without any optional args): +```sh +./stake_adjustment.py input.csv +``` +4. The output is a `csv` with bunch of useful data +5. For DP extract the first two columns with `NODE_ID` and `SUGGESTED_WALLET_DELEGATION` without the headers and save them as a `dp_update.csv` +6. Run this `dp_update.csv` as an input with `nym-cli` to adjust the DP delegations: +```sh +./nym-cli mixnet delegators delegate-multi --mnemonic "" --input dp_update.csv +``` +### Help + +To preview all commands and arguments, run it with `--help` flag: +```sh +./stake_adjustment.py --help +``` diff --git a/scripts/delegation-program/stake_adjustment.py b/scripts/delegation-program/stake_adjustment.py new file mode 100755 index 0000000000..527101651d --- /dev/null +++ b/scripts/delegation-program/stake_adjustment.py @@ -0,0 +1,569 @@ +#!/usr/bin/env python3 +import argparse +import csv +import sys +from pathlib import Path +import requests +import pandas as pd +import re + +API_SPECTRE_ROOT = "https://api.nym.spectredao.net/api/v1" +API_VALIDATOR = "https://validator.nymtech.net/api/v1" +API_BASE = f"{API_SPECTRE_ROOT}/nodes" +NYM_FACTOR = 1_000_000 + +""" +This simple argument based program is designed primarily for Delegation program management. +The main goal is to generate a csv of which first two columns without headers can also be reused for nym-cli as input: +./nym-cli mixnet delegators delegate-multi --mnemonic "" --input /.csv + +The default values therefore are: +--wallet_address: Nym Team DP wallet address +--saturation: 250k NYM +--stake_cap: 90% as per DP rules +--adjustment_Step: 25k NYM as per DP rules +--max_wallet_delegation: 125k NYM as per DP rules +--denom: NYM not uNYM to make it smoother and aligned with delegate-multi command of nym-cli + +Additionaly the program scrapes described endpoint and returns a sheet with 20 values per node. Those are: +NODE ID, SUGGESTED WALLET DELEGATION, CURRENT WALLET DELEGATION, SUGGESTED TOTAL STAKE, CURRENT TOTAL STAKE, +SUGGESTED SATURATION, CURRENT SATURATION, UPTIME, VERSION, T&C, BINARY, ROLE, WIREGUARD, IP ADDRESS, HOSTNAME, +WSS PORT, MONIKER, IDENTITY KEY, BONDING WALLET, EXPLORER URL. +""" + +def parse_args(): + p = argparse.ArgumentParser( + prog="stake_adjustment.py", + description="Suggest wallet delegation adjustments per node to hit a target saturation cap", + ) + p.add_argument("input", help="Path to CSV with a single column of NODE_ID values") + p.add_argument("--saturation", type=int, default=250_000, + help="Stake saturation in NYM (or uNYM if --denom uNYM). Default: 250000") + p.add_argument("--wallet_address", default="n1rnxpdpx3kldygsklfft0gech7fhfcux4zst5lw", + help="Delegation wallet address to track and adjust. Default: %(default)s") + p.add_argument("--stake_cap", type=int, default=90, + help="Target percentage of max saturation (e.g., 90 for 90%%). Default: 90") + p.add_argument("--adjustment_step", type=int, default=25_000, + help="Amount to undelegate per step (NYM or uNYM) until target delegation percentage is met. Default: 25000") + p.add_argument("--max_wallet_delegation", type=int, default=125_000, + help="Maximum delegation allowed by the wallet (NYM or uNYM). Default: 125000") + p.add_argument("--denom", type=str, default="NYM", choices=["NYM", "uNYM", "nym", "unym"], + help="Input/output denomination. Default: NYM") + return p.parse_args() + + +def to_unym(value: int, denom: str) -> int: + d = denom.lower() + if d == "nym": + return int(value) * NYM_FACTOR + if d == "unym": + return int(value) + raise ValueError("denom must be NYM or uNYM") + + +def from_unym(value_unym: int, denom: str) -> int: + d = denom.lower() + if d == "nym": + return int(value_unym // NYM_FACTOR) + if d == "unym": + return int(value_unym) + raise ValueError("denom must be NYM or uNYM") + + +def read_node_ids(csv_path: str) -> list[int]: + path = Path(csv_path) + if not path.exists(): + raise RuntimeError(f"Input file not found: {csv_path}") + + node_ids: list[int] = [] + with path.open(newline="") as f: + reader = csv.reader(f) + for row in reader: + if not row: + continue + if len(row) != 1: + raise RuntimeError("Input CSV must have exactly one column of NODE_ID values.") + try: + node_ids.append(int(row[0])) + except ValueError: + raise RuntimeError(f"Invalid NODE_ID (not an integer): {row[0]!r}") + if not node_ids: + raise RuntimeError("Input CSV contains no NODE_IDs.") + return node_ids + + +# pagination helpers: limit/offset -> then page/page_size -> single shot fallback +def _fetch_all_limit_offset(url: str, limit: int = 1000, timeout: int = 60) -> list: + items = [] + offset = 0 + seen_guard = None + loops = 0 + while True: + loops += 1 + r = requests.get(url, params={"limit": limit, "offset": offset}, timeout=timeout) + if r.status_code >= 400: + return None + try: + data = r.json() + except Exception: + return None + if isinstance(data, dict) and "data" in data and isinstance(data["data"], list): + batch = data["data"] + elif isinstance(data, list): + batch = data + else: + return None + if not batch: + break + items.extend(batch) + if len(batch) < limit: + break + # guard against APIs that ignore offset + first_sig = str(batch[0]) + if first_sig == seen_guard: + break + seen_guard = first_sig + offset += len(batch) + if loops > 1000 or offset > 1_000_000: + break + return items + + +def _fetch_all_page_pagesize(url: str, page_size: int = 1000, timeout: int = 60) -> list: + items = [] + page = 1 + seen_guard = None + loops = 0 + while True: + loops += 1 + r = requests.get(url, params={"page": page, "page_size": page_size}, timeout=timeout) + if r.status_code >= 400: + return None + try: + data = r.json() + except Exception: + return None + if isinstance(data, dict) and "data" in data and isinstance(data["data"], list): + batch = data["data"] + elif isinstance(data, list): + batch = data + else: + return None + if not batch: + break + items.extend(batch) + if len(batch) < page_size: + break + first_sig = str(batch[0]) + if first_sig == seen_guard: + break + seen_guard = first_sig + page += 1 + if loops > 1000 or page > 10000: + break + return items + + +def _fetch_all_any(url: str, timeout: int = 60) -> list: + # try limit/offset + got = _fetch_all_limit_offset(url, limit=1000, timeout=timeout) + if isinstance(got, list) and got: + return got + # try page/page_size + got = _fetch_all_page_pagesize(url, page_size=1000, timeout=timeout) + if isinstance(got, list) and got: + return got + # fallback: single shot + r = requests.get(url, timeout=timeout) + r.raise_for_status() + data = r.json() + if isinstance(data, dict) and "data" in data and isinstance(data["data"], list): + return data["data"] + if isinstance(data, list): + return data + raise RuntimeError(f"Unexpected response format from {url}") + + +# fetching functions using robust pagination +def fetch_wallet_delegations(wallet: str) -> list[dict]: + url = f"{API_SPECTRE_ROOT}/delegations/{wallet}" + return _fetch_all_any(url, timeout=45) + + +def fetch_nodes_spectre() -> list[dict]: + url = f"{API_SPECTRE_ROOT}/nodes" + return _fetch_all_any(url, timeout=60) + + +def fetch_nodes_validator() -> list[dict]: + url = f"{API_VALIDATOR}/nym-nodes/described" + return _fetch_all_any(url, timeout=60) + + +def fetch_node_delegations_sum_unym(node_id: int) -> int: + url = f"{API_SPECTRE_ROOT}/nodes/{node_id}/delegations" + try: + data = _fetch_all_any(url, timeout=45) + except Exception: + # last resort + r = requests.get(url, timeout=45) + r.raise_for_status() + data = r.json() + if not isinstance(data, list): + return 0 + total = 0 + for item in data: + try: + total += int(item.get("amount", {}).get("amount")) + except Exception: + pass + return total + + +def suggest_wallet_delegation( + node_id: int, + wallet: str, + saturation_unym: int, + cap_pct: int, + step_unym: int, + max_wallet_unym: int, + out_denom: str, + nodes_map: dict[int, dict], + val_map: dict[int, dict], + wallet_map: dict[int, int], +) -> dict: + # CURRENT TOTAL STAKE (in uNYM) + current_total_unym = None + meta_src = None + + if node_id in nodes_map and isinstance(nodes_map[node_id], dict): + # spectre nodes + current_total_unym = int(nodes_map[node_id].get("total_stake") or 0) + meta_src = nodes_map[node_id] + elif node_id in val_map and isinstance(val_map[node_id], dict): + # validator described + current_total_unym = int(val_map[node_id].get("total_stake") or 0) + meta_src = val_map[node_id] + + # if still unknown, sum delegations as a fallback + if current_total_unym is None or current_total_unym == 0: + current_total_unym = fetch_node_delegations_sum_unym(node_id) + + # CURRENT WALLET DELEGATION (in uNYM) from wallet_map + wallet_unym = int(wallet_map.get(node_id, 0)) + + # target cap in uNYM + target_unym = (saturation_unym * cap_pct) // 100 + + # start from min(current_wallet, max_wallet) and back off by step until under target + suggested_wallet_unym = min(wallet_unym, max_wallet_unym) + suggested_total_unym = current_total_unym - wallet_unym + suggested_wallet_unym + + if suggested_total_unym > target_unym and step_unym > 0: + while suggested_total_unym > target_unym and suggested_wallet_unym > 0: + dec = min(step_unym, suggested_wallet_unym) + suggested_wallet_unym -= dec + suggested_total_unym -= dec + + # convert to denom for output + suggested_wallet = from_unym(suggested_wallet_unym, out_denom) + current_wallet = from_unym(wallet_unym, out_denom) + suggested_total = from_unym(suggested_total_unym, out_denom) + current_total = from_unym(current_total_unym, out_denom) + saturation_val = from_unym(saturation_unym, out_denom) + + # saturation as integer percentages + suggested_sat = int((suggested_total * 100) // (saturation_val or 1)) + current_sat = int((current_total * 100) // (saturation_val or 1)) + + # extra fields + uptime = None + version = None + accepted_tnc= None + binary_name = _sanitize_text(binary_name) + role = _sanitize_text(role) + ip_address = _sanitize_text(ip_address) + hostname = _sanitize_text(hostname) + moniker = _sanitize_text(moniker) + identity_key = _sanitize_text(identity_key) + bonding_addr = _sanitize_text(bonding_addr) + explorer_url = _sanitize_text(explorer_url) + version = _sanitize_text(version) + + m = meta_src or {} + +def suggest_wallet_delegation( + node_id: int, + wallet: str, + saturation_unym: int, + cap_pct: int, + step_unym: int, + max_wallet_unym: int, + out_denom: str, + # extra maps for node + nodes_map: dict[int, dict], + val_map: dict[int, dict], + wallet_map: dict[int, int], +) -> dict: + # CURRENT TOTAL STAKE (in uNYM) + current_total_unym = None + meta_src = None + + if node_id in nodes_map and isinstance(nodes_map[node_id], dict): + # spectre nodes + current_total_unym = int(nodes_map[node_id].get("total_stake") or 0) + meta_src = nodes_map[node_id] + elif node_id in val_map and isinstance(val_map[node_id], dict): + # validator described + current_total_unym = int(val_map[node_id].get("total_stake") or 0) + meta_src = val_map[node_id] + + # if still unknown, sum delegations as a fallback + if current_total_unym is None or current_total_unym == 0: + current_total_unym = fetch_node_delegations_sum_unym(node_id) + + # CURRENT WALLET DELEGATION (in uNYM) from wallet_map + wallet_unym = int(wallet_map.get(node_id, 0)) + + # target cap in uNYM + target_unym = (saturation_unym * cap_pct) // 100 + + # start from min(current_wallet, max_wallet) and back off by step until under target + suggested_wallet_unym = min(wallet_unym, max_wallet_unym) + suggested_total_unym = current_total_unym - wallet_unym + suggested_wallet_unym + + if suggested_total_unym > target_unym and step_unym > 0: + while suggested_total_unym > target_unym and suggested_wallet_unym > 0: + dec = min(step_unym, suggested_wallet_unym) + suggested_wallet_unym -= dec + suggested_total_unym -= dec + + # convert to denom for output + suggested_wallet = from_unym(suggested_wallet_unym, out_denom) + current_wallet = from_unym(wallet_unym, out_denom) + suggested_total = from_unym(suggested_total_unym, out_denom) + current_total = from_unym(current_total_unym, out_denom) + saturation_val = from_unym(saturation_unym, out_denom) + + # saturation as integer percentages + suggested_sat = int((suggested_total * 100) // (saturation_val or 1)) + current_sat = int((current_total * 100) // (saturation_val or 1)) + + # extra fields + uptime = None + version = None + accepted_tnc = None + binary_name = None + role = None + wg_enabled = None + ip_address = None + hostname = None + wss_port = None + moniker = None + identity_key = None + bonding_addr = None + explorer_url = None + + m = meta_src or {} + # spectre and validator conventions translation + identity_key = m.get("identity_key") + bonding_addr = m.get("bonding_address") + uptime = m.get("uptime") + accepted_tnc = m.get("accepted_tnc") + desc = m.get("description") or {} + build = desc.get("build_information") or {} + binary_name = build.get("binary_name") + version = build.get("build_version") + + declared = desc.get("declared_role") or {} + if declared: + if declared.get("exit_ipr") or declared.get("exit_nr"): + role = "exit-gateway" + elif declared.get("entry"): + role = "entry-gateway" + elif declared.get("mixnode"): + role = "mixnode" + + host_info = desc.get("host_information") or {} + ip_list = host_info.get("ip_address") + if isinstance(ip_list, list) and ip_list: + ip_address = ip_list[0] + elif isinstance(ip_list, str): + ip_address = ip_list + hostname = host_info.get("hostname") + + # wss/ws ports under mixnet_websockets + webs = desc.get("mixnet_websockets") or {} + wss_port = webs.get("wss_port") + + # wireguard info + wg = m.get("wireguard") or desc.get("wireguard") + if isinstance(wg, dict) and wg.get("port") and wg.get("public_key"): + wg_enabled = True + else: + wg_enabled = False + + # self desc to get moniker + self_desc = m.get("self_description") or {} + moniker = self_desc.get("moniker") + + explorer_url = f"https://explorer.nym.spectredao.net/nodes/{identity_key}" if identity_key else "" + + # sanitize all string-ish fields to kill tabs/newlines and weird spacing + binary_name = _sanitize_text(binary_name) + role = _sanitize_text(role) + ip_address = _sanitize_text(ip_address) + hostname = _sanitize_text(hostname) + moniker = _sanitize_text(moniker) + identity_key = _sanitize_text(identity_key) + bonding_addr = _sanitize_text(bonding_addr) + explorer_url = _sanitize_text(explorer_url) + version = _sanitize_text(version) + + return { + "NODE ID": node_id, + "SUGGESTED WALLET DELEGATION": suggested_wallet, + "CURRENT WALLET DELEGATION": current_wallet, + "SUGGESTED TOTAL STAKE": suggested_total, + "CURRENT TOTAL STAKE": current_total, + "SUGGESTED SATURATION": suggested_sat, + "CURRENT SATURATION": current_sat, + "UPTIME": uptime, + "VERSION": version, + "T&C": bool(accepted_tnc) if accepted_tnc is not None else None, + "BINARY": binary_name, + "ROLE": role, + "WIREGUARD": wg_enabled, + "IP ADDRESS": ip_address, + "HOSTNAME": hostname, + "WSS PORT": wss_port, + "MONIKER": moniker, + "IDENTITY KEY": identity_key, + "BONDING WALLET": bonding_addr, + "EXPLORER URL": explorer_url, + } + +def _sanitize_text(val): + """Collapse whitespace, remove control chars, strip pipes that break CSVs.""" + if val is None: + return None + s = str(val) + s = s.replace("\r", " ").replace("\n", " ").replace("\t", " ") + s = re.sub(r"\s+", " ", s) + s = s.replace("|", " ") + s = "".join(ch for ch in s if ch.isprintable()) + return s.strip() + +def main(): + args = parse_args() + denom = args.denom + + # convert user inputs to uNYM + saturation_unym = to_unym(args.saturation, denom) + step_unym = to_unym(args.adjustment_step, denom) + max_wallet_unym = to_unym(args.max_wallet_delegation, denom) + + node_ids = read_node_ids(args.input) + + # detect duplicates (just report, do not modify order) + dups = pd.Series(node_ids).duplicated(keep=False) + if dups.any(): + dup_ids = sorted(set([nid for nid, d in zip(node_ids, dups.tolist()) if d])) + print(f"warning: These node IDs are duplicated: {dup_ids}") + else: + print("There are no duplicated node IDs.") + + # one call for wallet delegations (then map node_id -> wallet amount) + print("* * * Fetching wallet delegations * * *") + wallet_delegs = fetch_wallet_delegations(args.wallet_address) + wallet_map: dict[int, int] = {} + for d in wallet_delegs: + try: + nid = int(d.get("node_id")) + amt = int((d.get("amount") or {}).get("amount")) + except Exception: + continue + wallet_map[nid] = wallet_map.get(nid, 0) + amt + + # pull nodes from Spectre (paginated) + print("* * * Fetching nodes (Spectre) with pagination * * *") + spectre_nodes = fetch_nodes_spectre() + nodes_map: dict[int, dict] = {} + for n in spectre_nodes: + try: + nid = int(n.get("node_id")) + except Exception: + continue + nodes_map[nid] = n + + # pull nodes from Validator (paginated) + print("* * * Fetching nodes (Validator) with pagination * * *") + validator_nodes = fetch_nodes_validator() + val_map: dict[int, dict] = {} + for n in validator_nodes: + try: + nid = int(n.get("node_id")) + except Exception: + continue + # do not overwrite spectre if present; keep as fallback + if nid not in nodes_map: + val_map[nid] = n + + # build rows + rows = [] + for nid in node_ids: + try: + row = suggest_wallet_delegation( + node_id=nid, + wallet=args.wallet_address, + saturation_unym=saturation_unym, + cap_pct=args.stake_cap, + step_unym=step_unym, + max_wallet_unym=max_wallet_unym, + out_denom=denom, + nodes_map=nodes_map, + val_map=val_map, + wallet_map=wallet_map, + ) + except Exception as e: + row = { + "NODE ID": nid, + "SUGGESTED WALLET DELEGATION": 0, + "CURRENT WALLET DELEGATION": 0, + "SUGGESTED TOTAL STAKE": 0, + "CURRENT TOTAL STAKE": 0, + "SUGGESTED SATURATION": 0, + "CURRENT SATURATION": 0, + "UPTIME": None, + "VERSION": None, + "T&C": None, + "BINARY": None, + "ROLE": None, + "WIREGUARD": None, + "IP ADDRESS": None, + "HOSTNAME": None, + "WSS PORT": None, + "MONIKER": None, + "IDENTITY KEY": None, + "BONDING WALLET": None, + "EXPLORER URL": "", + } + print(f"warning: node {nid}: {e}", file=sys.stderr) + rows.append(row) + + df = pd.DataFrame(rows) + + print("\nResult preview:") + print(df.to_string(index=False)) + + ans = input("\nSave to ./delegations_adjusted.csv ? [y/N]: ").strip().lower() + if ans == "y": + out_path = Path("./delegations_adjusted.csv") + df.to_csv(out_path, index=False) + print(f"Saved: {out_path.resolve()}") + else: + print("Not saved.") + + +if __name__ == "__main__": + main() diff --git a/scripts/nym-node-setup/landing-page.html b/scripts/nym-node-setup/landing-page.html new file mode 100644 index 0000000000..4e09450870 --- /dev/null +++ b/scripts/nym-node-setup/landing-page.html @@ -0,0 +1,24 @@ + + + + + + Nym Node + + + +

Nym Node

+

This is a devrel testing placeholder page for Nym Node landing page.

+ + diff --git a/scripts/nym-node-setup/nym-node-cli.py b/scripts/nym-node-setup/nym-node-cli.py new file mode 100755 index 0000000000..8c97104e61 --- /dev/null +++ b/scripts/nym-node-setup/nym-node-cli.py @@ -0,0 +1,637 @@ +#!/usr/bin/python3 + +__version__ = "1.0.0" +__default_branch__ = "develop" + +import os +import re +import sys +import subprocess +import argparse +import tempfile +import shlex +import time +from datetime import datetime +from pathlib import Path +from typing import Iterable, Optional, Mapping +from typing import Optional, Tuple + +class NodeSetupCLI: + """All CLI main functions""" + + def __init__(self, args): + self.branch = args.dev + self.welcome_message = self.print_welcome_message() + self.mode = self.prompt_mode() + self.prereqs_install_sh = self.fetch_script("nym-node-prereqs-install.sh") + self.env_vars_install_sh = self.fetch_script("setup-env-vars.sh") + self.node_install_sh = self.fetch_script("nym-node-install.sh") + self.service_config_sh = self.fetch_script("setup-systemd-service-file.sh") + self.start_node_systemd_service_sh = self.fetch_script("start-node-systemd-service.sh") + self.landing_page_html = self._check_gwx_mode() and self.fetch_script("landing-page.html") + self.nginx_proxy_wss_sh = self._check_gwx_mode() and self.fetch_script("nginx_proxy_wss_sh") + self.tunnel_manager_sh = self._check_gwx_mode() and self.fetch_script("network_tunnel_manager.sh") + self.wg_ip_tables_manager_sh = self._check_gwx_mode() and self.fetch_script("wireguard-exit-policy-manager.sh") + self.wg_ip_tables_test_sh = self._check_gwx_mode() and self.fetch_script("exit-policy-tests.sh") + + def print_welcome_message(self): + """Welcome user, warns for needed pre-reqs and asks for confimation""" + self.print_character("=", 41) + print(\ + "* * * * * * NYM - NODE - CLI * * * * * *\n" \ + "An interactive tool to download, install\n" \ + "* * * * * setup & run nym-node * * * * *" + ) + self.print_character("=", 41) + msg = \ + "Before you begin, make sure that:\n"\ + "1. You run this setup on Debian based Linux (ie Ubuntu)\n"\ + "2. You run this installation program from a root shell\n"\ + "3. You meet minimal requirements: https://nym.com/docs/operators/nodes\n"\ + "4. You accept Operators Terms & Conditions: https://nym.com/operators-validators-terms\n"\ + "5. You have Nym wallet with at least 101 NYM: https://nym.com/docs/operators/nodes/preliminary-steps/wallet-preparation\n"\ + "6. In case of Gateway behind reverse proxy, you have A and AAAA DNS record pointing to this IP and propagated\n"\ + "\nTo confirm and continue, write 'ACCEPT' and press enter:" + print(msg) + confirmation = input("\n") + if confirmation.upper() == "ACCEPT": + pass + else: + print("Without confirming the points above, we cannot continue.") + exit(1) + + def prompt_mode(self): + """Ask user to insert node functionality and save it in python and bash envs""" + mode = input( + "\nEnter the mode you want to run nym-node in: " + "\n1. mixnode " + "\n2. entry-gateway " + "\n3. exit-gateway (works as entry-gateway as well) " + "\nPress 1, 2 or 3 and enter:\n" + ).strip() + + if mode in ("1", "mixnode"): + mode = "mixnode" + elif mode in ("2", "entry-gateway"): + mode = "entry-gateway" + elif mode in ("3", "exit-gateway"): + mode = "exit-gateway" + else: + print("Only numbers 1, 2 or 3 are accepted.") + raise SystemExit(1) + + # save mode for this Python instance + self.mode = mode + os.environ["MODE"] = mode + + # persist to env.sh so other scripts can source it + env_file = Path("env.sh") + with env_file.open("a") as f: + f.write(f'export MODE="{mode}"\n') + + # source env.sh so future bash subprocesses see it immediately + subprocess.run("source ./env.sh", shell=True, executable="/bin/bash") + + print(f"Mode set to '{mode}' — stored in env.sh and sourced for immediate use.") + return mode + + + def fetch_script(self, script_name): + """Fetches needed scripts according to a defined mode""" + # print header only the first time + if not getattr(self, "_fetched_once", False): + print("\n* * * Fetching required scripts * * *") + self._fetched_once = True + url = self._return_script_url(script_name) + print(f"Fetching file from: {url}") + result = subprocess.run(["wget", "-qO-", url], capture_output=True, text=True) + if result.returncode != 0 or not result.stdout.strip(): + print(f"wget failed to download the file.") + print("stderr:", result.stderr) + raise RuntimeError(f"Failed to fetch {url}") + # Optional sanity check: + first_line = result.stdout.splitlines()[0] if result.stdout else "" + print(f"Downloaded {len(result.stdout)} bytes.") + return result.stdout + + def _return_script_url(self, script_init_name): + """Dictionary pointing to scripts url returning value according to a passed key""" + github_raw_nymtech_nym_scripts_url = f"https://raw.githubusercontent.com/nymtech/nym/refs/heads/{self.branch}/scripts/" + scripts_urls = { + "nym-node-prereqs-install.sh": f"{github_raw_nymtech_nym_scripts_url}nym-node-setup/nym-node-prereqs-install.sh", + "setup-env-vars.sh": f"{github_raw_nymtech_nym_scripts_url}nym-node-setup/setup-env-vars.sh", + "nym-node-install.sh": f"{github_raw_nymtech_nym_scripts_url}nym-node-setup/nym-node-install.sh", + "setup-systemd-service-file.sh": f"{github_raw_nymtech_nym_scripts_url}nym-node-setup/setup-systemd-service-file.sh", + "start-node-systemd-service.sh": f"{github_raw_nymtech_nym_scripts_url}nym-node-setup/start-node-systemd-service.sh", + "nginx_proxy_wss_sh": f"{github_raw_nymtech_nym_scripts_url}nym-node-setup/setup-nginx-proxy-wss.sh", + "landing-page.html": f"{github_raw_nymtech_nym_scripts_url}nym-node-setup/landing-page.html", + "network_tunnel_manager.sh": f"https://raw.githubusercontent.com/nymtech/nym/refs/heads/develop/scripts/network_tunnel_manager.sh", + "wireguard-exit-policy-manager.sh": f"https://raw.githubusercontent.com/nymtech/nym/refs/heads/develop/scripts/wireguard-exit-policy/wireguard-exit-policy-manager.sh", + "exit-policy-tests.sh": f"https://raw.githubusercontent.com/nymtech/nym/refs/heads/develop/scripts/wireguard-exit-policy/exit-policy-tests.sh", + } + return scripts_urls[script_init_name] + + def run_script( + self, + script_text: str, + args: Optional[Iterable[str]] = None, + env: Optional[Mapping[str, str]] = None, + cwd: Optional[str] = None, + sudo: bool = False, # ignored for root; kept for signature compat + detached: bool = False, + ) -> int: + """ + Save script to a temp file and run it + - Automatically injects ENV_FILE= unless already provided + - Adds SYSTEMD_PAGER="" and SYSTEMD_COLORS="0" by default + Returns exit code (0 if detached fire-and-forget) + """ + import os, subprocess + + path = self._write_temp_script(script_text) + try: + # build env with sensible defaults + run_env = dict(os.environ) + if env: + run_env.update(env) + + # ensure ENV_FILE is absolute and present for all scripts + if "ENV_FILE" not in run_env: + # if env.sh is elsewhere, change this to your known base dir + env_file = os.path.abspath(os.path.join(os.getcwd(), "env.sh")) + run_env["ENV_FILE"] = env_file + + # make systemctl non-interactive everywhere + run_env.setdefault("SYSTEMD_PAGER", "") + run_env.setdefault("SYSTEMD_COLORS", "0") + + cmd = [str(path)] + (list(args) if args else []) + + if detached: + subprocess.Popen( + cmd, + env=run_env, + cwd=cwd, + stdin=subprocess.DEVNULL, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + start_new_session=True, + close_fds=True, + ) + return 0 + else: + cp = subprocess.run(cmd, env=run_env, cwd=cwd) + return cp.returncode + finally: + try: + path.unlink(missing_ok=True) + except Exception: + pass + + def _write_temp_script(self, script_text: str) -> Path: + """Helper: write script text to a temp file, ensure bash shebang, chmod +x, return its path""" + if not script_text.lstrip().startswith("#!"): + script_text = "#!/usr/bin/env bash\n" + script_text + with tempfile.NamedTemporaryFile("w", delete=False, suffix=".sh") as f: + f.write(script_text) + path = Path(f.name) + os.chmod(path, 0o700) + return path + + def _check_gwx_mode(self): + """Helper: Several fns run only for GWx - this fn checks this condition""" + if self.mode == "exit-gateway": + return True + else: + return False + + def check_wg_enabled(self): + """Checks if Wireguard is enabled and if not, prompts user if they want to enable it, stores it to env.sh""" + + + env_file = os.path.abspath(os.path.join(os.getcwd(), "env.sh")) + + def norm(v): # -> "true" or "false" + return "true" if str(v).strip().lower() in ("1", "true", "yes", "y") else "false" + + # precedence: process env → env.sh → prompt + val = os.environ.get("WIREGUARD") + + if val is None and os.path.isfile(env_file): + try: + with open(env_file, "r", encoding="utf-8") as f: + m = re.search(r'^\s*export\s+WIREGUARD\s*=\s*"?([^"\n]+)"?', f.read(), re.M) + if m: + val = m.group(1) + except Exception: + pass + + if val is None: + ans = input( + "\nWireGuard is not configured.\n" + "Nodes routing WireGuard can be listed as both entry and exit in the app.\n" + "Enable WireGuard support? (y/n): " + ).strip().lower() + val = "true" if ans in ("y", "yes") else "false" + + val = norm(val) + os.environ["WIREGUARD"] = val + + # persist to env.sh (replace or append) + try: + text = "" + if os.path.isfile(env_file): + with open(env_file, "r", encoding="utf-8") as f: + text = f.read() + if re.search(r'^\s*export\s+WIREGUARD\s*=.*$', text, re.M): + text = re.sub(r'^\s*export\s+WIREGUARD\s*=.*$', f'export WIREGUARD="{val}"', text, flags=re.M) + else: + if text and not text.endswith("\n"): + text += "\n" + text += f'export WIREGUARD="{val}"\n' + with open(env_file, "w", encoding="utf-8") as f: + f.write(text) + print(f'WIREGUARD={val} saved to {env_file}') + except Exception as e: + print(f"Warning: could not write {env_file}: {e}") + + return (val == "true") + + def run_bash_command(self, command, args=None, *, env=None, cwd=None, check=True): + """ + Run a command with optional args (no script stdin) + `command` can be a string (e.g., "ls") or a list (e.g., ["ls", "-la"]). + """ + # normalize command into a list + if isinstance(command, str): + cmd = shlex.split(command) + else: + cmd = list(command) + + if args: + cmd += list(args) + + print("Running:", " ".join(shlex.quote(c) for c in cmd)) + return subprocess.run(cmd, env=env, cwd=cwd, check=check) + + + def run_tunnel_manager_setup(self): + """A standalone fn to pass full cmd list needed for correct setup and test network tunneling, using an external script""" + print( + "\n* * * Setting up network configuration for mixnet IP router and Wireguard tunneling * * *" + "\nMore info: https://nym.com/docs/operators/nodes/nym-node/configuration#1-download-network_tunnel_managersh-make-executable-and-run" + "\nThis may take a while; follow the steps below and don't kill the process..." + ) + + # each entry is the exact argv to pass to the script + steps = [ + ["check_nymtun_iptables"], + ["remove_duplicate_rules", "nymtun0"], + ["remove_duplicate_rules", "nymwg"], + ["check_nymtun_iptables"], + ["adjust_ip_forwarding"], + ["apply_iptables_rules"], + ["check_nymtun_iptables"], + ["apply_iptables_rules_wg"], + ["configure_dns_and_icmp_wg"], + ["adjust_ip_forwarding"], + ["check_ipv6_ipv4_forwarding"], + ["joke_through_the_mixnet"], + ["joke_through_wg_tunnel"], + ] + + for argv in steps: + print("Running: network_tunnel_manager.sh", *argv) + rc = self.run_script(self.tunnel_manager_sh, args=argv) + if rc != 0: + print(f"Step {' '.join(argv)} failed with exit code {rc}. Stopping.") + return rc + + print("Network tunnel manager setup completed successfully.") + return 0 + + def setup_test_wg_ip_tables(self): + """Configuration and test of Wireguard exit policy according to mixnet exit policy using external scripts""" + print( + "Setting up Wireguard IP tables to match Nym exit policy for mixnet, stored at: https://nymtech.net/.wellknown/network-requester/exit-policy.txt" + "\nThis may take a while, follow the steps below and don't kill the process..." + ) + self.run_script(self.wg_ip_tables_manager_sh, args=["install"]) + self.run_script(self.wg_ip_tables_manager_sh, args=["status"]) + self.run_script(self.wg_ip_tables_test_sh) + + + def run_nym_node_as_service(self): + """Starts /etc/systemd/system/nym-node.service based on prompt using external script""" + service = "nym-node.service" + service_path = "/etc/systemd/system/nym-node.service" + print(f"\n* * * We are going to start {service} from systemd config located at: {service_path} * * *") + + # if the service file is missing, run setup non-interactively + if not os.path.isfile(service_path): + print(f"Service file not found at {service_path}. Running setup...") + setup_env = { + **os.environ, + "SYSTEMD_PAGER": "", + "SYSTEMD_COLORS": "0", + "NONINTERACTIVE": "1", + "MODE": os.environ.get("MODE", "mixnode"), + } + self.run_script(self.service_config_sh, env=setup_env) + if not os.path.isfile(service_path): + print("Service file still not found after setup. Aborting.") + return + + run_env = {**os.environ, "SYSTEMD_PAGER": "", "SYSTEMD_COLORS": "0", "WAIT_TIMEOUT": "600"} + is_active = subprocess.run(["systemctl", "is-active", "--quiet", service], env=run_env).returncode == 0 + + if is_active: + while True: + ans = input(f"{service} is already running. Restart it now? (y/n):\n").strip().lower() + if ans == "y": + self.run_script(self.start_node_systemd_service_sh, args=["restart-poll"], env=run_env) + return + elif ans == "n": + print("Continuing without restart.") + return + else: + print("Invalid input. Please press 'y' or 'n' and press enter.") + else: + while True: + ans = input(f"{service} is not running. Start it now? (y/n):\n").strip().lower() + if ans == "y": + self.run_script(self.start_node_systemd_service_sh, args=["start-poll"], env=run_env) + return + elif ans == "n": + print("Okay, not starting it.") + return + else: + print("Invalid input. Please press 'y' or 'n' and press enter.") + + + + def run_bonding_prompt(self): + """Interactive function navigating user to bond node""" + print("\n") + print("* * * Bonding Nym Node * * *") + print("Time to register your node to Nym Network by bonding it using Nym wallet ...") + node_path = os.path.expandvars(os.path.expanduser("$HOME/nym-binaries/nym-node")) + if not (os.path.isfile(node_path) and os.access(node_path, os.X_OK)): + print(f"Nym node not found at {node_path}, we cannot run a bonding prompt!") + exit(1) + else: + while True: + subprocess.run([os.path.expanduser(node_path), "bonding-information"]) + self.run_bash_command(command="curl", args=["-4", "https://ifconfig.me"]) + print("\n") + self.print_character("=", 56) + print("* * * FOLLOW THESE STEPS TO BOND YOUR NODE * * *") + print("If you already bonded your node before, just press enter") + self.print_character("=", 56) + print( + "1. Open your wallet and go to Bonding menu\n" + "2. Paste Identity key and your IP address (printed above)\n" + "3. Setup your operators cost and profit margin\n" + "4. Copy the long contract message from your wallet" + ) + msg = "5. Paste the contract message from clipboard here and press enter:\n" + contract_msg = input(msg).strip() + if contract_msg == "": + print("Skipping bonding process as your node is already bonded\n") + return + else: + subprocess.run([ + os.path.expanduser(node_path), + "sign", + "--contract-msg", + contract_msg + ]) + print( + "6. Copy the last part of the string back to your Nym wallet\n" + "7. Confirm the transaction" + ) + confirmation = input( + "\n* * * Is your node bonded?\n" + "1. YES\n" + "2. NO, try again\n" + "3. Skip bonding for now\n" + "Press 1, 2, or 3 and enter:\n" + ).strip() + + if confirmation == "1": + # NEW: fetch identity + composed message and print it + _, message = self._explorer_message_from_identity(node_path) + self.print_character("*", 42) + print(message) + self.print_character("*", 42) + return + elif confirmation == "3": + print( + "Your node is not bonded, we are skipping this step.\n" + "Note that without bonding network tunnel manager will not work fully!\n" + "You can always bond manually using:\n" + "`$HOME/nym-binaries/nym-node sign --contract-msg `" + ) + return + elif confirmation == "2": + continue + else: + print( + "Your input was wrong, we are skipping this step. You can always bond manually using:\n" + "`$HOME/nym-binaries/nym-node sign --contract-msg `" + ) + return + + def _explorer_message_from_identity(self, node_path: str) -> Tuple[Optional[str], str]: + """ + Runs `$HOME/nym-binaries/nym-node bonding-information` to + extract the id_key and returns explorer URL with a message + else return the message without the URL + """ + try: + cp = subprocess.run( + [os.path.expanduser(node_path), "bonding-information"], + capture_output=True, text=True, check=False, timeout=30 + ) + output = cp.stdout or "" + except Exception as e: + output = "" + # still return the generic message + key = None + msg = ( + "* * * C O N G R A T U L A T I O N ! * * *\n" + "Your Nym node is registered to Nym network\n" + "Wait until the end of epoch for the change\n" + "to propagate (max 60 min)\n" + "(Could not obtain Identity Key automatically.)" + ) + return key, msg + + # parse the id_key + m = re.search(r"^Identity Key:\s*([A-Za-z0-9]+)\s*$", output, flags=re.MULTILINE) + key = m.group(1) if m else None + + base_msg = ( + "* * * C O N G R A T U L A T I O N ! * * *\n" + "Your Nym node is registered to Nym network\n" + "Wait until the end of epoch for the change\n" + "to propagate (max 60 min)\n" + ) + + if key: + url = f"https://explorer.nym.spectredao.net/nodes/{key}" + msg = base_msg + f"Then you can see your node at:\n{url}" + else: + msg = base_msg + "(Could not obtain Identity Key automatically.)" + + return key, msg + + def print_character(self, ch: str, count: int): + """Print `ch` repeated `count` times (no unbounded growth)""" + if not ch: + return + # Use exactly one codepoint char; trim if longer + ch = ch[:1] + # Clamp count to a sensible max to avoid huge outputs + try: + n = int(count) + except Exception: + n = 0 + n = max(0, min(n, 161)) + print(ch * n) + + def _env_with_envfile(self) -> dict: + """Helper for env persistence sanity""" + env = dict(os.environ) + env["SYSTEMD_PAGER"] = "" + env["SYSTEMD_COLORS"] = "0" + env["ENV_FILE"] = os.path.abspath(os.path.join(os.getcwd(), "env.sh")) + return env + + def run_node_installation(self,args): + """Main function called by argparser command install running full node install flow""" + self.run_script(self.prereqs_install_sh) + self.run_script(self.env_vars_install_sh) + self.run_script(self.node_install_sh) + self.run_script(self.service_config_sh) + self._check_gwx_mode() and self.run_script(self.nginx_proxy_wss_sh) + self.run_nym_node_as_service() + self.run_bonding_prompt() + if self._check_gwx_mode(): + self.run_tunnel_manager_setup() + if self.check_wg_enabled(): + self.setup_test_wg_ip_tables() + self.setup_test_wg_ip_tables() + + + +class ArgParser: + """CLI argument interface managing the NodeSetupCLI functions based on user input""" + + def parser_main(self): + # shared options to work before and after subcommands + parent = argparse.ArgumentParser(add_help=False) + parent.add_argument( + "-V", "--version", + action="version", + version=f"nym-node-cli {__version__}" + ) + parent.add_argument("-d", "--dev", metavar="BRANCH", + help="Define github branch", + type=str, + default=argparse.SUPPRESS) + parent.add_argument("-v", "--verbose", action="store_true", + help="Show full error tracebacks") + + parser = argparse.ArgumentParser( + prog="nym-node-cli", + description="An interactive tool to download, install, setup and run nym-node", + epilog="Privacy infrastructure operated by people around the world", + parents=[parent], + ) + + subparsers = parser.add_subparsers(dest="command", help="subcommands") + subparsers.required = True + + p_install = subparsers.add_parser( + "install", parents=[parent], + help="Starts nym-node installation setup CLI", + aliases=["i", "I"], add_help=True + ) + + args = parser.parse_args() + + # assign default manually only if user didn’t supply --dev + if not hasattr(args, "dev"): + args.dev = __default_branch__ + + try: + # build CLI with parsed args to catch errors soon + cli = NodeSetupCLI(args) + + commands = { + "install": cli.run_node_installation, + "i": cli.run_node_installation, + "I": cli.run_node_installation, + } + + func = commands.get(args.command) + if func is None: + parser.print_help() + parser.error(f"Unknown command: {args.command}") + + # execute subcommand within error test + func(args) + + except SystemExit: + raise + except RuntimeError as e: + print(f"{e}\nMake sure that the your BRANCH ('{args.dev}') provided in --dev option contains this program.") + sys.exit(1) + except Exception as e: + if getattr(args, "verbose", False): + traceback.print_exc() + else: + print(f"error: {e}", file=sys.stderr) + sys.exit(1) + + +class SystemSafeGuards: + """A few safe guards to deal with memory usage by this program""" + + def _protect_from_oom(self, score: int = -900): + try: + with open("/proc/self/oom_score_adj", "w") as f: + f.write(str(score)) + except Exception: + pass + + def _trim_memory(self): + """Liberate freeable Python objects and return arenas to the OS if possible""" + try: + import gc, ctypes + gc.collect() + try: + libc = ctypes.CDLL("libc.so.6") + # 0 = “trim as much as possible” + libc.malloc_trim(0) + except Exception: + pass + except Exception: + pass + + def _cap_controller_memory(self, bytes_limit: int = 2 * 1024**3): + # limit this Python process to e.g. 2 GiB virtual memory + try: + import resource + resource.setrlimit(resource.RLIMIT_AS, (bytes_limit, bytes_limit)) + except Exception: + pass + + +if __name__ == '__main__': + safeguards = SystemSafeGuards() + safeguards._protect_from_oom(-900) # de-prioritize controller as OOM victim + safeguards._cap_controller_memory(2 * 1024**3) # optional: cap controller to 2 GiB + app = ArgParser() + app.parser_main() diff --git a/scripts/nym-node-setup/nym-node-install.sh b/scripts/nym-node-setup/nym-node-install.sh new file mode 100644 index 0000000000..13d97ede08 --- /dev/null +++ b/scripts/nym-node-setup/nym-node-install.sh @@ -0,0 +1,227 @@ +#!/bin/bash +set -euo pipefail + +echo -e "\n* * * Ensuring ~/nym-binaries exists * * *" +mkdir -p "$HOME/nym-binaries" + +# Load env.sh via absolute path if provided, else try ./env.sh +if [[ -n "${ENV_FILE:-}" && -f "${ENV_FILE}" ]]; then + set -a + # shellcheck disable=SC1090 + . "${ENV_FILE}" + set +a +elif [[ -f "./env.sh" ]]; then + set -a + # shellcheck disable=SC1091 + . ./env.sh + set +a +fi + +# check for existing node config and optionally reset +NODE_CONFIG_DIR="$HOME/.nym/nym-nodes/default-nym-node" + +check_existing_config() { + # proceed only if dir exists AND has any entries inside + if [[ -d "$NODE_CONFIG_DIR" ]] && find "$NODE_CONFIG_DIR" -mindepth 1 -maxdepth 1 | read -r _; then + echo + echo "Nym node configuration already exist at $NODE_CONFIG_DIR" + echo + echo "Initialising nym-node again will NOT overwrite your existing private keys, only adjust your preferences (like mode, wireguard optionality etc)." + echo + echo "If you want to remove your current node configuration and all data files including nodes keys type 'RESET' and press enter." + echo + read -r -p "To keep your existing node and just change its preferences press enter: " resp + + if [[ "${resp}" =~ ^([Rr][Ee][Ss][Ee][Tt])$ ]]; then + echo + read -r -p "We are going to remove the existing node with configuration $NODE_CONFIG_DIR and replace it with a fresh one, do you want to back up the old one first? (y/n) " backup_ans + if [[ "${backup_ans}" =~ ^[Yy]$ ]]; then + ts="$(date +%Y%m%d-%H%M%S)" + backup_dir="$HOME/.nym/backup/$(basename "$NODE_CONFIG_DIR")-$ts" + echo "Backing up to: $backup_dir" + mkdir -p "$(dirname "$backup_dir")" + cp -a "$NODE_CONFIG_DIR" "$backup_dir" + fi + echo "Removing $NODE_CONFIG_DIR ..." + rm -rf "$NODE_CONFIG_DIR" + echo "Old node removed. Proceeding with fresh initialization..." + else + echo "Keeping existing node configuration. Proceeding to re-configure." + export ASK_WG="1" + fi + fi +} + +# run the check before any initialization +check_existing_config + +echo -e "\n* * * Resolving latest release tag URL * * *" +LATEST_TAG_URL="$(curl -sI -L -o /dev/null -w '%{url_effective}' https://github.com/nymtech/nym/releases/latest)" +# expected example: https://github.com/nymtech/nym/releases/tag/nym-binaries-v2025.13-emmental + +if [[ -z "${LATEST_TAG_URL}" || "${LATEST_TAG_URL}" != *"/releases/tag/"* ]]; then + echo "ERROR: Could not resolve latest tag URL from GitHub." >&2 + exit 1 +fi + +DOWNLOAD_URL="${LATEST_TAG_URL/tag/download}/nym-node" +NYM_NODE="$HOME/nym-binaries/nym-node" + +# if binary already exists, ask to overwrite; if yes, remove first +if [[ -e "${NYM_NODE}" ]]; then + echo + echo -e "\n* * * A nym-node binary already exists at: ${NYM_NODE}" + read -r -p "Overwrite with the latest release? (y/n): " ow_ans + if [[ "${ow_ans}" =~ ^[Yy]$ ]]; then + echo "Removing existing binary to avoid 'text file busy'..." + rm -f "${NYM_NODE}" + else + echo "Keeping existing binary." + fi +fi + +echo -e "\n* * * Downloading nym-node from:" +echo " ${DOWNLOAD_URL}" +# only download if file is missing (or we just removed it) +if [[ ! -e "${NYM_NODE}" ]]; then + curl -fL "${DOWNLOAD_URL}" -o "${NYM_NODE}" +fi + +echo -e "\n * * * Making binary executable * * *" +chmod +x "${NYM_NODE}" + +echo "---------------------------------------------------" +echo "Nym node binary downloaded:" +"${NYM_NODE}" --version || true +echo "---------------------------------------------------" + +# check that MODE is set (after sourcing env.sh) +if [[ -z "${MODE:-}" ]]; then + echo "ERROR: Environment variable MODE is not set." + echo "Please export MODE as one of: mixnode, entry-gateway, exit-gateway" + exit 1 +fi + +# determine public IP (fallback if ifconfig.me fails) +echo -e "\n* * * Discovering public IP (IPv4) * * *" +if ! PUBLIC_IP="$(curl -fsS -4 https://ifconfig.me)"; then + PUBLIC_IP="$(curl -fsS https://api.ipify.org || echo '')" +fi +if [[ -z "${PUBLIC_IP}" ]]; then + echo "WARNING: Could not determine public IP automatically." +fi + +# respect existing WIREGUARD; for gateways: prompt if unset OR if we kept config and ASK_WG=1 +WIREGUARD="${WIREGUARD:-}" +if [[ ( "$MODE" == "entry-gateway" || "$MODE" == "exit-gateway" ) && ( -n "${ASK_WG:-}" || -z "$WIREGUARD" ) ]]; then + echo + echo "Gateways can also route WireGuard in NymVPN." + echo "Enabling it means your node may be listed as both entry and exit in the app." + # show current default in the prompt if present + def_hint="" + [[ -n "${WIREGUARD}" ]] && def_hint=" [current: ${WIREGUARD}]" + read -r -p "Enable WireGuard support? (y/n)${def_hint}: " answer || true + case "${answer:-}" in + [Yy]* ) WIREGUARD="true" ;; + [Nn]* ) WIREGUARD="false" ;; + * ) : ;; # keep existing value if user just pressed enter + esac +fi +# final default only if still empty +WIREGUARD="${WIREGUARD:-false}" + +# persist WIREGUARD to the same env file Python CLI uses +ENV_PATH="${ENV_FILE:-./env.sh}" +if [[ -n "$ENV_PATH" ]]; then + mkdir -p "$(dirname "$ENV_PATH")" + if [[ -f "$ENV_PATH" ]]; then + # replace existing export or append + if grep -qE '^[[:space:]]*export[[:space:]]+WIREGUARD=' "$ENV_PATH"; then + sed -i -E 's|^[[:space:]]*export[[:space:]]+WIREGUARD=.*$|export WIREGUARD="'"$WIREGUARD"'"|' "$ENV_PATH" + else + printf '\nexport WIREGUARD="%s"\n' "$WIREGUARD" >> "$ENV_PATH" + fi + else + printf 'export WIREGUARD="%s"\n' "$WIREGUARD" > "$ENV_PATH" + fi + echo "WIREGUARD=${WIREGUARD} persisted to $ENV_PATH" +fi + +# helpers: ensure optional env vars exist (avoid -u issues) +HOSTNAME="${HOSTNAME:-}" +LOCATION="${LOCATION:-}" +EMAIL="${EMAIL:-}" +MONIKER="${MONIKER:-}" +DESCRIPTION="${DESCRIPTION:-}" + +# initialize node config +case "${MODE}" in + mixnode) + echo -e "\n* * * Initialising nym-node in mode: mixnode * * *" + "${NYM_NODE}" run \ + --mode mixnode \ + ${PUBLIC_IP:+--public-ips "$PUBLIC_IP"} \ + ${HOSTNAME:+--hostname "$HOSTNAME"} \ + ${LOCATION:+--location "$LOCATION"} \ + -w \ + --init-only + ;; + entry-gateway) + echo -e "\n* * * Initialising nym-node in mode: entry-gateway * * *" + "${NYM_NODE}" run \ + --mode entry-gateway \ + ${PUBLIC_IP:+--public-ips "$PUBLIC_IP"} \ + ${HOSTNAME:+--hostname "$HOSTNAME"} \ + ${LOCATION:+--location "$LOCATION"} \ + --wireguard-enabled "${WIREGUARD}" \ + ${HOSTNAME:+--landing-page-assets-path "/var/www/${HOSTNAME}"} \ + -w \ + --init-only + ;; + exit-gateway) + echo -e "\n* * * Initialising nym-node in mode: exit-gateway * * *" + if [[ -z "${HOSTNAME:-}" || -z "${LOCATION:-}" ]]; then + echo "ERROR: HOSTNAME and LOCATION must be exported for exit-gateway." + exit 1 + fi + "${NYM_NODE}" run \ + --mode exit-gateway \ + ${PUBLIC_IP:+--public-ips "$PUBLIC_IP"} \ + --hostname "$HOSTNAME" \ + --location "$LOCATION" \ + --wireguard-enabled "${WIREGUARD:-false}" \ + --announce-wss-port 9001 \ + --landing-page-assets-path "/var/www/${HOSTNAME}" \ + -w \ + --init-only + ;; + *) + echo "ERROR: Unsupported MODE: '${MODE}'" + echo "Valid values: mixnode, entry-gateway, exit-gateway" + exit 1 + ;; +esac + +echo +echo "* * * nym-node initialised. Config path should be:" +echo " $HOME/.nym/nym-nodes/default-nym-node/" + +# setup description.toml (if init created the dir) +DESC_DIR="$HOME/.nym/nym-nodes/default-nym-node/data" +DESC_FILE="$DESC_DIR/description.toml" + +if [[ -d "$DESC_DIR" ]]; then + echo -e "\n* * * Writing node description: $DESC_FILE * * *" + mkdir -p "$DESC_DIR" + cat > "$DESC_FILE" < env.sh + +echo -e "\nVariables saved to ./env.sh" + +if [[ $__SOURCED -eq 1 ]]; then + # shellcheck disable=SC1091 + . ./env.sh + echo "Loaded into current shell (because you sourced this script)." +else + echo "To load them into your current shell, run: source ./env.sh" +fi diff --git a/scripts/nym-node-setup/setup-nginx-proxy-wss.sh b/scripts/nym-node-setup/setup-nginx-proxy-wss.sh new file mode 100644 index 0000000000..7dd613fd2a --- /dev/null +++ b/scripts/nym-node-setup/setup-nginx-proxy-wss.sh @@ -0,0 +1,283 @@ +#!/usr/bin/env bash +set -euo pipefail + +# load env (prefer absolute ENV_FILE injected by Python CLI; fallback to ./env.sh) +if [[ -n "${ENV_FILE:-}" && -f "${ENV_FILE}" ]]; then + set -a; . "${ENV_FILE}"; set +a +elif [[ -f "./env.sh" ]]; then + set -a; . ./env.sh; set +a +fi + +: "${HOSTNAME:?HOSTNAME not set in env.sh}" +: "${EMAIL:?EMAIL not set in env.sh}" + +export SYSTEMD_PAGER="" +export SYSTEMD_COLORS="0" +DEBIAN_FRONTEND=noninteractive + +# sanity check +if [[ "${HOSTNAME}" == "localhost" || "${HOSTNAME}" == "127.0.0.1" ]]; then + echo "ERROR: HOSTNAME cannot be 'localhost'. Use a public FQDN." >&2 + exit 1 +fi + +echo -e "\n* * * Starting nginx configuration for landing page, reverse proxy and WSS * * *" + +# set paths & ports vars +WEBROOT="/var/www/${HOSTNAME}" +LE_ACME_DIR="/var/www/letsencrypt" +SITES_AVAIL="/etc/nginx/sites-available" +SITES_EN="/etc/nginx/sites-enabled" +BASE_HTTP="${SITES_AVAIL}/${HOSTNAME}" # :80 vhost +BASE_HTTPS="${SITES_AVAIL}/${HOSTNAME}-ssl" # :443 vhost (we’ll write it ourselves) +WSS_AVAIL="${SITES_AVAIL}/wss-config-nym" +BACKUP_DIR="/etc/nginx/sites-backups" + +NYM_PORT_HTTP="${NYM_PORT_HTTP:-8080}" +NYM_PORT_WSS="${NYM_PORT_WSS:-9000}" +WSS_LISTEN_PORT="${WSS_LISTEN_PORT:-9001}" + +mkdir -p "${WEBROOT}" "${LE_ACME_DIR}" "${BACKUP_DIR}" "${SITES_AVAIL}" "${SITES_EN}" + +# helpers +neat_backup() { + local file="$1"; [[ -f "$file" ]] || return 0 + local sha_now; sha_now="$(sha256sum "$file" | awk '{print $1}')" || return 0 + local tag; tag="$(basename "$file")" + local latest="${BACKUP_DIR}/${tag}.latest" + if [[ -f "$latest" ]]; then + local sha_prev; sha_prev="$(awk '{print $1}' "$latest")" + [[ "$sha_now" == "$sha_prev" ]] && return 0 + fi + cp -a "$file" "${BACKUP_DIR}/${tag}.bak.$(date +%s)" + echo "$sha_now ${tag}" > "$latest" + ls -1t "${BACKUP_DIR}/${tag}.bak."* 2>/dev/null | tail -n +6 | xargs -r rm -f +} + +ensure_enabled() { + local src="$1"; local name; name="$(basename "$src")" + ln -sf "$src" "${SITES_EN}/${name}" +} + +cert_ok() { + [[ -s "/etc/letsencrypt/live/${HOSTNAME}/fullchain.pem" && -s "/etc/letsencrypt/live/${HOSTNAME}/privkey.pem" ]] +} + +fetch_landing() { + local url="https://raw.githubusercontent.com/nymtech/nym/refs/heads/feature/node-setup-cli/scripts/nym-node-setup/landing-page.html" + if command -v curl >/dev/null 2>&1; then + curl -fsSL "$url" -o "${WEBROOT}/index.html" || true + else + wget -qO "${WEBROOT}/index.html" "$url" || true + fi + if [[ ! -s "${WEBROOT}/index.html" ]]; then + cat > "${WEBROOT}/index.html" <<'HTML' +Nym Node + +

Nym node landing

+

This is a placeholder page served by nginx.

+ +HTML + fi +} + +reload_nginx() { nginx -t && systemctl reload nginx; } + +# landing page (idempotent) +fetch_landing +echo "Landing page at ${WEBROOT}/index.html" + +# disable default and stale SSL configs +[[ -L "${SITES_EN}/default" ]] && unlink "${SITES_EN}/default" || true +for f in "${SITES_EN}"/*; do + [[ -L "$f" ]] || continue + if grep -q "/etc/letsencrypt/live/localhost" "$f"; then + echo "Disabling vhost referencing localhost cert: $f"; unlink "$f" + fi +done +for f in "${SITES_EN}"/*; do + [[ -L "$f" ]] || continue + if grep -qE 'listen\s+.*443' "$f"; then + cert=$(awk '/ssl_certificate[ \t]+/ {print $2}' "$f" | tr -d ';' | head -n1) + key=$(awk '/ssl_certificate_key[ \t]+/ {print $2}' "$f" | tr -d ';' | head -n1) + if [[ -n "${cert:-}" && ! -s "$cert" ]] || [[ -n "${key:-}" && ! -s "$key" ]]; then + echo "Disabling SSL vhost with missing cert/key: $f"; unlink "$f" + fi + fi +done + +# HTTP :80 vhost (ACME-safe, proxy to :8080) +neat_backup "${BASE_HTTP}" +cat > "${BASE_HTTP}" </dev/null; then + echo "WARNING: Can't reach Let's Encrypt directory. We'll still keep HTTP up." >&2 +fi +THIS_IP="$(curl -fsS -4 https://ifconfig.me || true)" +DNS_IP="$(getent ahostsv4 "${HOSTNAME}" 2>/dev/null | awk '{print $1; exit}')" +echo "Public IPv4: ${THIS_IP:-unknown} DNS A(${HOSTNAME}): ${DNS_IP:-unresolved}" +if [[ -n "${THIS_IP:-}" && -n "${DNS_IP:-}" && "${THIS_IP}" != "${DNS_IP}" ]]; then + echo "WARNING: DNS for ${HOSTNAME} does not match this server's public IPv4." +fi +timedatectl show -p NTPSynchronized --value 2>/dev/null | grep -qi yes || timedatectl set-ntp true || true + +# install certbot if missing +if ! command -v certbot >/dev/null 2>&1; then + if command -v snap >/dev/null 2>&1; then + snap install core || true; snap refresh core || true + snap install --classic certbot; ln -sf /snap/bin/certbot /usr/bin/certbot + else + apt-get update -y >/dev/null 2>&1 || true + apt-get install -y certbot >/dev/null 2>&1 || true + fi +fi + +# issue/renew via WEBROOT (no nginx auto-edit), non-fatal if it fails +STAGING_FLAG=""; [[ "${CERTBOT_STAGING:-0}" == "1" ]] && STAGING_FLAG="--staging" && echo "Using Let's Encrypt STAGING." +if ! cert_ok; then + certbot certonly --non-interactive --agree-tos -m "${EMAIL}" -d "${HOSTNAME}" \ + --webroot -w "${LE_ACME_DIR}" ${STAGING_FLAG} || true +fi + +# create own 443 vhost (only if certs exist) +if cert_ok; then + neat_backup "${BASE_HTTPS}" + cat > "${BASE_HTTPS}" <HTTPS (keeps ACME path in HTTP too via separate small server) + neat_backup "${BASE_HTTP}" + cat > "${BASE_HTTP}" < "${WSS_AVAIL}" < "$SERVICE_PATH" </dev/null || echo unknown)" + sub="$(systemctl show -p SubState --value "$SERVICE" 2>/dev/null || echo unknown)" + result="$(systemctl show -p Result --value "$SERVICE" 2>/dev/null || echo unknown)" + local cur="${active}/${sub}/${result}" + if [ "$cur" != "$last" ]; then + echo "state: ActiveState=${active} SubState=${sub} Result=${result}" + last="$cur" + fi + [ "$active" = "active" ] && return 0 + if [ "$active" = "failed" ] || [ "$result" = "failed" ] || [ "$result" = "exit-code" ] || [ "$result" = "timeout" ]; then + return 1 + fi + sleep 1 + done + echo "timeout: ${WAIT_TIMEOUT}s exceeded while waiting for ${SERVICE}" + return 1 +} + +restart_poll() { + reload_and_reset + echo "Restarting $SERVICE (non-blocking) and polling up to ${WAIT_TIMEOUT}s..." + systemctl --no-ask-password restart --no-block "$SERVICE" + wait_until_active_or_fail +} + +start_poll() { + reload_and_reset + echo "Starting $SERVICE (non-blocking) and polling up to ${WAIT_TIMEOUT}s..." + systemctl --no-ask-password start --no-block "$SERVICE" + wait_until_active_or_fail +} + +case "${1:-}" in + restart-poll) restart_poll ;; + start-poll) start_poll ;; + *) echo "Usage: $0 {start-poll|restart-poll}"; exit 2 ;; +esac diff --git a/sdk/rust/nym-sdk/Cargo.toml b/sdk/rust/nym-sdk/Cargo.toml index 9c67bbf898..523e5801c5 100644 --- a/sdk/rust/nym-sdk/Cargo.toml +++ b/sdk/rust/nym-sdk/Cargo.toml @@ -38,6 +38,7 @@ nym-socks5-client-core = { path = "../../../common/socks5-client-core" } nym-validator-client = { path = "../../../common/client-libs/validator-client", features = [ "http-client", ] } +nym-http-api-client = { path = "../../../common/http-api-client" } nym-socks5-requests = { path = "../../../common/socks5/requests" } nym-ordered-buffer = { path = "../../../common/socks5/ordered-buffer" } nym-service-providers-common = { path = "../../../service-providers/common" } diff --git a/sdk/rust/nym-sdk/examples/custom_topology_provider.rs b/sdk/rust/nym-sdk/examples/custom_topology_provider.rs index 7b08e73f24..6f6fe5b730 100644 --- a/sdk/rust/nym-sdk/examples/custom_topology_provider.rs +++ b/sdk/rust/nym-sdk/examples/custom_topology_provider.rs @@ -4,18 +4,22 @@ use nym_sdk::mixnet; use nym_sdk::mixnet::MixnetMessageSender; use nym_topology::provider_trait::{async_trait, ToTopologyMetadata, TopologyProvider}; -use nym_topology::NymTopology; +use nym_topology::{EpochRewardedSet, NymTopology}; +use nym_validator_client::nym_api::NymApiClientExt; use url::Url; struct MyTopologyProvider { - validator_client: nym_validator_client::client::NymApiClient, + validator_client: nym_http_api_client::Client, } impl MyTopologyProvider { fn new(nym_api_url: Url) -> MyTopologyProvider { - MyTopologyProvider { - validator_client: nym_validator_client::client::NymApiClient::new(nym_api_url), - } + let validator_client = nym_http_api_client::Client::builder(nym_api_url) + .expect("Failed to create API client builder") + .build() + .expect("Failed to build API client"); + + MyTopologyProvider { validator_client } } async fn get_topology(&self) -> NymTopology { @@ -33,7 +37,8 @@ impl MyTopologyProvider { let metadata = mixnodes_response.metadata.to_topology_metadata(); - let mut base_topology = NymTopology::new(metadata, rewarded_set, Vec::new()); + let epoch_rewarded_set: EpochRewardedSet = rewarded_set.into(); + let mut base_topology = NymTopology::new(metadata, epoch_rewarded_set, Vec::new()); // in our topology provider only use mixnodes that have node_id divisible by 3 // and has exactly 100 performance score diff --git a/sdk/rust/nym-sdk/src/lib.rs b/sdk/rust/nym-sdk/src/lib.rs index f79798e32e..ddc6088829 100644 --- a/sdk/rust/nym-sdk/src/lib.rs +++ b/sdk/rust/nym-sdk/src/lib.rs @@ -26,7 +26,5 @@ pub use nym_network_defaults::{ ChainDetails, DenomDetails, DenomDetailsOwned, NymContracts, NymNetworkDetails, ValidatorDetails, }; +pub use nym_task::{ShutdownToken, ShutdownTracker}; pub use nym_validator_client::UserAgent; -// we have to re-expose TaskClient since we're allowing custom shutdown in public API -// (which is quite a shame if you ask me...) -pub use nym_task::TaskClient; diff --git a/sdk/rust/nym-sdk/src/mixnet/client.rs b/sdk/rust/nym-sdk/src/mixnet/client.rs index 90d1b218b6..1949ef534a 100644 --- a/sdk/rust/nym-sdk/src/mixnet/client.rs +++ b/sdk/rust/nym-sdk/src/mixnet/client.rs @@ -8,8 +8,6 @@ use crate::mixnet::{CredentialStorage, MixnetClient, Recipient}; use crate::GatewayTransceiver; use crate::NymNetworkDetails; use crate::{Error, Result}; -use futures::channel::mpsc; -use futures::StreamExt; use log::{debug, warn}; use nym_client_core::client::base_client::storage::helpers::{ get_active_gateway_identity, get_all_registered_identities, has_gateway_details, @@ -31,7 +29,7 @@ use nym_client_core::init::types::{GatewaySelectionSpecification, GatewaySetup}; use nym_credentials_interface::TicketType; use nym_crypto::hkdf::DerivationMaterial; use nym_socks5_client_core::config::Socks5; -use nym_task::{TaskClient, TaskHandle, TaskStatus}; +use nym_task::ShutdownTracker; use nym_topology::provider_trait::TopologyProvider; use nym_validator_client::{nyxd, QueryHttpRpcNyxdClient, UserAgent}; use rand::rngs::OsRng; @@ -54,7 +52,7 @@ pub struct MixnetClientBuilder { wait_for_gateway: bool, custom_topology_provider: Option>, custom_gateway_transceiver: Option>, - custom_shutdown: Option, + custom_shutdown: Option, force_tls: bool, user_agent: Option, #[cfg(unix)] @@ -266,7 +264,7 @@ where /// Use an externally managed shutdown mechanism. #[must_use] - pub fn custom_shutdown(mut self, shutdown: TaskClient) -> Self { + pub fn custom_shutdown(mut self, shutdown: ShutdownTracker) -> Self { self.custom_shutdown = Some(shutdown); self } @@ -380,7 +378,7 @@ where force_tls: bool, /// Allows passing an externally controlled shutdown handle. - custom_shutdown: Option, + custom_shutdown: Option, user_agent: Option, @@ -740,7 +738,13 @@ where let debug_config = self.config.debug_config; let packet_type = self.config.debug_config.traffic.packet_type; let (mut started_client, nym_address) = self.connect_to_mixnet_common().await?; - let (socks5_status_tx, mut socks5_status_rx) = mpsc::channel(128); + + // TODO: more graceful handling here, surely both variants should work... I think? + let Some(task_manager) = started_client.shutdown_handle else { + return Err(Error::new_unsupported( + "connecting with socks5 is currently unsupported with custom shutdown", + )); + }; let client_input = started_client.client_input.register_producer(); let client_output = started_client.client_output.register_consumer(); @@ -753,40 +757,14 @@ where client_output, client_state.clone(), nym_address, - started_client.task_handle.get_handle(), + task_manager.shutdown_tracker_owned(), packet_type, ); - // TODO: more graceful handling here, surely both variants should work... I think? - if let TaskHandle::Internal(task_manager) = &mut started_client.task_handle { - task_manager - .start_status_listener(socks5_status_tx, TaskStatus::Ready) - .await; - match socks5_status_rx - .next() - .await - .ok_or(Error::Socks5NotStarted)? - .as_any() - .downcast_ref::() - .ok_or(Error::Socks5NotStarted)? - { - TaskStatus::Ready => { - log::debug!("Socks5 connected"); - } - TaskStatus::ReadyWithGateway(gateway) => { - log::debug!("Socks5 connected to {gateway}"); - } - } - } else { - return Err(Error::new_unsupported( - "connecting with socks5 is currently unsupported with custom shutdown", - )); - } - Ok(Socks5MixnetClient { nym_address, client_state, - task_handle: started_client.task_handle, + task_handle: task_manager, socks5_config, }) } @@ -833,7 +811,7 @@ where client_state, reconstructed_receiver, stats_events_reporter, - started_client.task_handle, + started_client.shutdown_handle, None, started_client.client_request_sender, started_client.forget_me, diff --git a/sdk/rust/nym-sdk/src/mixnet/native_client.rs b/sdk/rust/nym-sdk/src/mixnet/native_client.rs index 7c8089d324..d9c92030f4 100644 --- a/sdk/rust/nym-sdk/src/mixnet/native_client.rs +++ b/sdk/rust/nym-sdk/src/mixnet/native_client.rs @@ -19,7 +19,7 @@ use nym_sphinx::{params::PacketType, receiver::ReconstructedMessage}; use nym_statistics_common::clients::{ClientStatsEvents, ClientStatsSender}; use nym_task::{ connections::{ConnectionCommandSender, LaneQueueLengths}, - TaskHandle, + ShutdownManager, }; use nym_topology::{NymRouteProvider, NymTopology}; use std::pin::Pin; @@ -54,7 +54,7 @@ pub struct MixnetClient { pub(crate) stats_events_reporter: ClientStatsSender, /// The task manager that controls all the spawned tasks that the clients uses to do it's job. - pub(crate) task_handle: TaskHandle, + pub(crate) shutdown_handle: Option, pub(crate) packet_type: Option, // internal state used for the `Stream` implementation @@ -74,7 +74,7 @@ impl MixnetClient { client_state: ClientState, reconstructed_receiver: ReconstructedMessagesReceiver, stats_events_reporter: ClientStatsSender, - task_handle: TaskHandle, + task_handle: Option, packet_type: Option, client_request_sender: ClientRequestSender, forget_me: ForgetMe, @@ -88,7 +88,7 @@ impl MixnetClient { client_state, reconstructed_receiver, stats_events_reporter, - task_handle, + shutdown_handle: task_handle, packet_type, _buffered: Vec::new(), client_request_sender, @@ -221,9 +221,9 @@ impl MixnetClient { self.stats_events_reporter.clone() } - /// Disconnect from the mixnet. Currently it is not supported to reconnect a disconnected + /// Disconnect from the mixnet. Currently, it is not supported to reconnect a disconnected /// client. - pub async fn disconnect(mut self) { + pub async fn disconnect(self) { if self.forget_me.any() { log::debug!("Sending forget me request: {:?}", self.forget_me); match self.send_forget_me().await { @@ -240,13 +240,9 @@ impl MixnetClient { tokio::time::sleep(tokio::time::Duration::from_secs(2)).await; } - if let TaskHandle::Internal(task_manager) = &mut self.task_handle { - task_manager.signal_shutdown().ok(); - task_manager.wait_for_shutdown().await; + if let Some(mut task_manager) = self.shutdown_handle { + task_manager.perform_shutdown().await; } - - // note: it's important to take ownership of the struct as if the shutdown is `TaskHandle::External`, - // it must be dropped to finalize the shutdown } pub async fn send_forget_me(&self) -> Result<()> { diff --git a/sdk/rust/nym-sdk/src/mixnet/socks5_client.rs b/sdk/rust/nym-sdk/src/mixnet/socks5_client.rs index 6cbf837c29..cb4e826bd5 100644 --- a/sdk/rust/nym-sdk/src/mixnet/socks5_client.rs +++ b/sdk/rust/nym-sdk/src/mixnet/socks5_client.rs @@ -1,7 +1,7 @@ use nym_client_core::client::base_client::ClientState; use nym_socks5_client_core::config::Socks5; use nym_sphinx::addressing::clients::Recipient; -use nym_task::{connections::LaneQueueLengths, TaskHandle}; +use nym_task::{connections::LaneQueueLengths, ShutdownManager}; use nym_topology::NymTopology; @@ -18,7 +18,7 @@ pub struct Socks5MixnetClient { pub(crate) client_state: ClientState, /// The task manager that controls all the spawned tasks that the clients uses to do it's job. - pub(crate) task_handle: TaskHandle, + pub(crate) task_handle: ShutdownManager, /// SOCKS5 configuration parameters. pub(crate) socks5_config: Socks5, @@ -82,12 +82,6 @@ impl Socks5MixnetClient { /// Disconnect from the mixnet. Currently it is not supported to reconnect a disconnected /// client. pub async fn disconnect(mut self) { - if let TaskHandle::Internal(task_manager) = &mut self.task_handle { - task_manager.signal_shutdown().ok(); - task_manager.wait_for_shutdown().await; - } - - // note: it's important to take ownership of the struct as if the shutdown is `TaskHandle::External`, - // it must be dropped to finalize the shutdown + self.task_handle.run_until_shutdown().await; } } diff --git a/sdk/rust/nym-sdk/src/tcp_proxy/tcp_proxy_client.rs b/sdk/rust/nym-sdk/src/tcp_proxy/tcp_proxy_client.rs index 85f9682e62..f00a5b30c1 100644 --- a/sdk/rust/nym-sdk/src/tcp_proxy/tcp_proxy_client.rs +++ b/sdk/rust/nym-sdk/src/tcp_proxy/tcp_proxy_client.rs @@ -223,7 +223,7 @@ impl NymProxyClient { loop { tokio::select! { _ = &mut rx => { - info!("Closing write end of session: {} in {} seconds", session_id, close_timeout); + info!("Closing write end of session: {session_id} in {close_timeout} seconds"); break } Some(message) = client.next() => { diff --git a/service-providers/ip-packet-router/src/cli/run.rs b/service-providers/ip-packet-router/src/cli/run.rs index bf71e79dea..2108efcc86 100644 --- a/service-providers/ip-packet-router/src/cli/run.rs +++ b/service-providers/ip-packet-router/src/cli/run.rs @@ -3,6 +3,7 @@ use crate::cli::{override_config, OverrideConfig}; use clap::Args; use nym_client_core::cli_helpers::client_run::CommonClientRunArgs; use nym_ip_packet_router::error::IpPacketRouterError; +use nym_task::ShutdownManager; #[allow(clippy::struct_excessive_bools)] #[derive(Args, Clone)] @@ -27,10 +28,17 @@ pub(crate) async fn execute(args: &Run) -> Result<(), IpPacketRouterError> { log::debug!("Using config: {config:#?}"); log::info!("Starting ip packet router service provider"); - let mut server = nym_ip_packet_router::IpPacketRouter::new(config); + let mut shutdown_manager = ShutdownManager::build_new_default()?; + let mut server = nym_ip_packet_router::IpPacketRouter::new( + config, + shutdown_manager.shutdown_tracker_owned(), + ); if let Some(custom_mixnet) = &args.common_args.custom_mixnet { server = server.with_stored_topology(custom_mixnet)? } - server.run_service_provider().await + tokio::spawn(server.run_service_provider()); + + shutdown_manager.run_until_shutdown().await; + Ok(()) } 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 4e976e96f9..1e38d33595 100644 --- a/service-providers/ip-packet-router/src/ip_packet_router.rs +++ b/service-providers/ip-packet-router/src/ip_packet_router.rs @@ -12,7 +12,7 @@ use nym_client_core::{ TopologyProvider, }; use nym_sdk::mixnet::Recipient; -use nym_task::{TaskClient, TaskHandle}; +use nym_task::ShutdownTracker; use crate::{config::Config, error::IpPacketRouterError, request_filter::RequestFilter}; @@ -39,29 +39,22 @@ pub struct IpPacketRouter { wait_for_gateway: bool, custom_topology_provider: Option>, custom_gateway_transceiver: Option>, - shutdown: Option, + shutdown: ShutdownTracker, on_start: Option>, } impl IpPacketRouter { - pub fn new(config: Config) -> Self { + pub fn new(config: Config, shutdown: ShutdownTracker) -> Self { Self { config, wait_for_gateway: false, custom_topology_provider: None, custom_gateway_transceiver: None, - shutdown: None, + shutdown, on_start: None, } } - #[must_use] - #[allow(unused)] - pub fn with_shutdown(mut self, shutdown: TaskClient) -> Self { - self.shutdown = Some(shutdown); - self - } - #[must_use] #[allow(unused)] pub fn with_custom_gateway_transceiver( @@ -130,12 +123,11 @@ impl IpPacketRouter { clients::ConnectedClients, mixnet_listener::MixnetListener, request_filter::RequestFilter, tun_listener::TunListener, }; - let task_handle: TaskHandle = self.shutdown.map(Into::into).unwrap_or_default(); // Connect to the mixnet let mixnet_client = crate::mixnet_client::create_mixnet_client( &self.config.base, - task_handle.get_handle().named("nym_sdk::MixnetClient[IPR]"), + self.shutdown.clone(), self.custom_gateway_transceiver, self.custom_topology_provider, self.wait_for_gateway, @@ -163,7 +155,7 @@ impl IpPacketRouter { let tun_listener = TunListener { tun_reader, - task_client: task_handle.get_handle(), + shutdown_token: self.shutdown.clone_shutdown_token(), connected_clients: connected_clients_rx, }; tun_listener.start(); @@ -176,7 +168,7 @@ impl IpPacketRouter { request_filter: request_filter.clone(), tun_writer, mixnet_client, - task_handle, + shutdown_token: self.shutdown.clone_shutdown_token(), connected_clients, }; diff --git a/service-providers/ip-packet-router/src/mixnet_client.rs b/service-providers/ip-packet-router/src/mixnet_client.rs index 18bed08c24..8326a5854a 100644 --- a/service-providers/ip-packet-router/src/mixnet_client.rs +++ b/service-providers/ip-packet-router/src/mixnet_client.rs @@ -3,7 +3,7 @@ use nym_client_core::{config::disk_persistence::CommonClientPaths, TopologyProvider}; use nym_sdk::{GatewayTransceiver, NymNetworkDetails}; -use nym_task::TaskClient; +use nym_task::ShutdownTracker; use crate::{config::BaseClientConfig, error::IpPacketRouterError}; @@ -13,7 +13,7 @@ use crate::{config::BaseClientConfig, error::IpPacketRouterError}; // TODO: refactor this function and its arguments pub(crate) async fn create_mixnet_client( config: &BaseClientConfig, - shutdown: TaskClient, + shutdown: ShutdownTracker, custom_transceiver: Option>, custom_topology_provider: Option>, wait_for_gateway: bool, diff --git a/service-providers/ip-packet-router/src/mixnet_listener.rs b/service-providers/ip-packet-router/src/mixnet_listener.rs index fe622f8aa0..b29493da8f 100644 --- a/service-providers/ip-packet-router/src/mixnet_listener.rs +++ b/service-providers/ip-packet-router/src/mixnet_listener.rs @@ -1,15 +1,6 @@ // Copyright 2025 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use futures::StreamExt; -use nym_ip_packet_requests::codec::MultiIpPacketCodec; -use nym_sdk::mixnet::MixnetMessageSender; -use nym_sphinx::receiver::ReconstructedMessage; -use nym_task::TaskHandle; -use std::{net::SocketAddr, time::Duration}; -use tokio::io::AsyncWriteExt; -use tokio_util::codec::FramedRead; - use crate::{ clients::{ConnectedClientHandler, ConnectedClients}, config::Config, @@ -30,6 +21,14 @@ use crate::{ request_filter::RequestFilter, util::parse_ip::ParsedPacket, }; +use futures::StreamExt; +use nym_ip_packet_requests::codec::MultiIpPacketCodec; +use nym_sdk::mixnet::MixnetMessageSender; +use nym_sphinx::receiver::ReconstructedMessage; +use nym_task::ShutdownToken; +use std::{net::SocketAddr, time::Duration}; +use tokio::io::AsyncWriteExt; +use tokio_util::codec::FramedRead; #[cfg(not(target_os = "linux"))] type TunDevice = crate::non_linux_dummy::DummyDevice; @@ -52,7 +51,7 @@ pub(crate) struct MixnetListener { pub(crate) mixnet_client: nym_sdk::mixnet::MixnetClient, // The task handle for the main loop - pub(crate) task_handle: TaskHandle, + pub(crate) shutdown_token: ShutdownToken, // The map of connected clients that the mixnet listener keeps track of. It monitors // activity and disconnects clients that have been inactive for too long. @@ -138,7 +137,7 @@ impl MixnetListener { Ok(responses) } - // Receving a static connect request from a client with an IP provided that we assign to them, + // Receiving a static connect request from a client with an IP provided that we assign to them, // if it's available. If it's not available, we send a failure response. async fn on_static_connect_request( &mut self, @@ -463,13 +462,14 @@ impl MixnetListener { } pub(crate) async fn run(mut self) -> Result<()> { - let mut task_client = self.task_handle.fork("main_loop"); let mut disconnect_timer = tokio::time::interval(DISCONNECT_TIMER_INTERVAL); - while !task_client.is_shutdown() { + loop { tokio::select! { - _ = task_client.recv() => { + biased; + _ = self.shutdown_token.cancelled() => { log::debug!("IpPacketRouter [main loop]: received shutdown"); + break; }, _ = disconnect_timer.tick() => { self.handle_disconnect_timer().await; diff --git a/service-providers/ip-packet-router/src/tun_listener.rs b/service-providers/ip-packet-router/src/tun_listener.rs index 072ade7ab0..f1b663fc03 100644 --- a/service-providers/ip-packet-router/src/tun_listener.rs +++ b/service-providers/ip-packet-router/src/tun_listener.rs @@ -4,15 +4,14 @@ use std::collections::HashMap; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; +use crate::clients::{ConnectEvent, ConnectedClientEvent, DisconnectEvent}; +use crate::{error::Result, util::parse_ip::parse_dst_addr}; use nym_ip_packet_requests::IpPair; -use nym_task::TaskClient; +use nym_task::ShutdownToken; #[cfg(target_os = "linux")] use tokio::io::AsyncReadExt; use tokio::sync::mpsc; -use crate::clients::{ConnectEvent, ConnectedClientEvent, DisconnectEvent}; -use crate::{error::Result, util::parse_ip::parse_dst_addr}; - // The TUN listener keeps a local map of the connected clients that has its state updated by the // mixnet listener. Basically it's just so that we don't have to have mutexes around shared state. // It's even ok if this is slightly out of date @@ -79,7 +78,7 @@ impl ConnectedClientsListener { #[cfg(target_os = "linux")] pub(crate) struct TunListener { pub(crate) tun_reader: tokio::io::ReadHalf, - pub(crate) task_client: TaskClient, + pub(crate) shutdown_token: ShutdownToken, pub(crate) connected_clients: ConnectedClientsListener, } @@ -113,10 +112,12 @@ impl TunListener { async fn run(mut self) -> Result<()> { let mut buf = [0u8; 65535]; - while !self.task_client.is_shutdown() { + loop { tokio::select! { - _ = self.task_client.recv() => { + biased; + _ = self.shutdown_token.cancelled() => { log::trace!("TunListener: received shutdown"); + break; }, // TODO: ConnectedClientsListener::update should poll the channel instead event = self.connected_clients.connected_client_rx.recv() => match event { diff --git a/service-providers/network-requester/src/cli/run.rs b/service-providers/network-requester/src/cli/run.rs index 09938ac46a..3cb96948d0 100644 --- a/service-providers/network-requester/src/cli/run.rs +++ b/service-providers/network-requester/src/cli/run.rs @@ -8,6 +8,7 @@ use crate::{ }; use clap::Args; use nym_client_core::cli_helpers::client_run::CommonClientRunArgs; +use nym_task::ShutdownManager; #[allow(clippy::struct_excessive_bools)] #[derive(Args, Clone)] @@ -58,10 +59,17 @@ pub(crate) async fn execute(args: &Run) -> Result<(), NetworkRequesterError> { } log::info!("Starting socks5 service provider"); - let mut server = crate::core::NRServiceProviderBuilder::new(config); + let mut shutdown_manager = ShutdownManager::build_new_default()?; + let mut server = crate::core::NRServiceProviderBuilder::new( + config, + shutdown_manager.shutdown_tracker_owned(), + ); if let Some(custom_mixnet) = &args.common_args.custom_mixnet { server = server.with_stored_topology(custom_mixnet)? } - server.run_service_provider().await + tokio::spawn(server.run_service_provider()); + + shutdown_manager.run_until_shutdown().await; + Ok(()) } diff --git a/service-providers/network-requester/src/core.rs b/service-providers/network-requester/src/core.rs index 513e187d64..3bf865a91b 100644 --- a/service-providers/network-requester/src/core.rs +++ b/service-providers/network-requester/src/core.rs @@ -9,7 +9,7 @@ use crate::{reply, socks5}; use async_trait::async_trait; use futures::channel::{mpsc, oneshot}; use futures::stream::StreamExt; -use log::{debug, warn}; +use log::{debug, error, warn}; use nym_bin_common::bin_info_owned; use nym_client_core::client::mix_traffic::transceiver::GatewayTransceiver; use nym_client_core::config::disk_persistence::CommonClientPaths; @@ -34,8 +34,7 @@ use nym_sphinx::anonymous_replies::requests::AnonymousSenderTag; use nym_sphinx::params::{PacketSize, PacketType}; use nym_sphinx::receiver::ReconstructedMessage; use nym_task::connections::LaneQueueLengths; -use nym_task::manager::TaskHandle; -use nym_task::TaskClient; +use nym_task::ShutdownTracker; use std::path::Path; use std::sync::atomic::{AtomicUsize, Ordering}; @@ -67,7 +66,7 @@ pub struct NRServiceProviderBuilder { wait_for_gateway: bool, custom_topology_provider: Option>, custom_gateway_transceiver: Option>, - shutdown: Option, + shutdown: ShutdownTracker, on_start: Option>, } @@ -79,7 +78,7 @@ pub struct NRServiceProvider { controller_sender: ControllerSender, mix_input_sender: MixProxySender, - shutdown: TaskHandle, + shutdown: ShutdownTracker, } #[async_trait] @@ -148,26 +147,17 @@ impl ServiceProvider for NRServiceProvider { } impl NRServiceProviderBuilder { - pub fn new(config: Config) -> NRServiceProviderBuilder { + pub fn new(config: Config, shutdown: ShutdownTracker) -> NRServiceProviderBuilder { NRServiceProviderBuilder { config, wait_for_gateway: false, custom_topology_provider: None, custom_gateway_transceiver: None, - shutdown: None, + shutdown, on_start: None, } } - #[must_use] - // this is a false positive, this method is actually called when used as a library - // but clippy complains about it when building the binary - #[allow(unused)] - pub fn with_shutdown(mut self, shutdown: TaskClient) -> Self { - self.shutdown = Some(shutdown); - self - } - #[must_use] // this is a false positive, this method is actually called when used as a library // but clippy complains about it when building the binary @@ -233,13 +223,10 @@ impl NRServiceProviderBuilder { /// Start all subsystems pub async fn run_service_provider(self) -> Result<(), NetworkRequesterError> { - // Used to notify tasks to shutdown. Not all tasks fully supports this (yet). - let shutdown: TaskHandle = self.shutdown.map(Into::into).unwrap_or_default(); - // Connect to the mixnet let mixnet_client = create_mixnet_client( &self.config.base, - shutdown.get_handle().named("nym_sdk::MixnetClient[NR]"), + self.shutdown.clone(), self.custom_gateway_transceiver, self.custom_topology_provider, self.wait_for_gateway, @@ -254,9 +241,7 @@ impl NRServiceProviderBuilder { // Controller for managing all active connections. let (mut active_connections_controller, controller_sender) = Controller::new( mixnet_client.connection_command_sender(), - shutdown - .get_handle() - .named("nym_socks5_proxy_helpers::connection_controller::Controller"), + self.shutdown.clone_shutdown_token(), ); tokio::spawn(async move { @@ -285,7 +270,7 @@ impl NRServiceProviderBuilder { mixnet_client, controller_sender, mix_input_sender, - shutdown, + shutdown: self.shutdown, }; log::info!("The address of this client is: {self_address}"); @@ -307,12 +292,13 @@ impl NRServiceProviderBuilder { impl NRServiceProvider { async fn run(&mut self) -> Result<(), NetworkRequesterError> { - let mut shutdown = self.shutdown.fork("main_loop"); - while !shutdown.is_shutdown() { + let shutdown = self.shutdown.clone_shutdown_token(); + loop { tokio::select! { biased; - _ = shutdown.recv() => { - debug!("NRServiceProvider [main loop]: received shutdown") + _ = shutdown.cancelled() => { + debug!("NRServiceProvider [main loop]: received shutdown"); + break }, msg = self.mixnet_client.next() => match msg { Some(msg) => self.on_message(msg).await, @@ -378,7 +364,7 @@ impl NRServiceProvider { controller_sender: ControllerSender, mix_input_sender: MixProxySender, lane_queue_lengths: LaneQueueLengths, - mut shutdown: TaskClient, + shutdown: ShutdownTracker, ) { let mut conn = match socks5::tcp::Connection::new( connection_id, @@ -390,7 +376,6 @@ impl NRServiceProvider { Ok(conn) => conn, Err(err) => { log::error!("error while connecting to {remote_addr}: {err}",); - shutdown.disarm(); // inform the remote that the connection is closed before it even was established let mixnet_message = MixnetMessage::new_network_data_response( @@ -472,11 +457,12 @@ impl NRServiceProvider { let controller_sender_clone = self.controller_sender.clone(); let mix_input_sender_clone = self.mix_input_sender.clone(); let lane_queue_lengths_clone = self.mixnet_client.shared_lane_queue_lengths(); - let mut shutdown = self.shutdown.get_handle(); // we're just cloning the underlying pointer, nothing expensive is happening here let request_filter = self.request_filter.clone(); + let proxy_shutdown = self.shutdown.clone(); + // at this point move it into the separate task // because we might have to resolve the underlying address and it can take some time // during which we don't want to block other incoming requests @@ -491,11 +477,10 @@ impl NRServiceProvider { log_msg, ); - mix_input_sender_clone - .send(error_msg) - .await - .expect("InputMessageReceiver has stopped receiving!"); - shutdown.disarm(); + if mix_input_sender_clone.send(error_msg).await.is_err() { + // don't disarm the shutdown, do cause global shutdown here! + error!("InputMessageReceiver has stopped receiving!"); + } return; } @@ -509,7 +494,7 @@ impl NRServiceProvider { controller_sender_clone, mix_input_sender_clone, lane_queue_lengths_clone, - shutdown, + proxy_shutdown, ) .await }); @@ -563,7 +548,7 @@ impl NRServiceProvider { // TODO: refactor this function and its arguments async fn create_mixnet_client( config: &BaseClientConfig, - shutdown: TaskClient, + shutdown: ShutdownTracker, custom_transceiver: Option>, custom_topology_provider: Option>, wait_for_gateway: bool, diff --git a/service-providers/network-requester/src/socks5/tcp.rs b/service-providers/network-requester/src/socks5/tcp.rs index a1e3eabec2..63f6f249f3 100644 --- a/service-providers/network-requester/src/socks5/tcp.rs +++ b/service-providers/network-requester/src/socks5/tcp.rs @@ -9,7 +9,7 @@ use nym_socks5_proxy_helpers::proxy_runner::{MixProxySender, ProxyRunner}; use nym_socks5_requests::{ConnectionId, RemoteAddress, Socks5Request}; use nym_sphinx::params::PacketSize; use nym_task::connections::LaneQueueLengths; -use nym_task::TaskClient; +use nym_task::ShutdownTracker; use std::io; use tokio::net::TcpStream; @@ -47,7 +47,7 @@ impl Connection { mix_receiver: ConnectionReceiver, mix_sender: MixProxySender, lane_queue_lengths: LaneQueueLengths, - shutdown: TaskClient, + shutdown: ShutdownTracker, ) { let stream = self.conn.take().unwrap(); let remote_source_address = "???".to_string(); // we don't know ip address of requester diff --git a/tools/internal/testnet-manager/Cargo.toml b/tools/internal/testnet-manager/Cargo.toml index e1a32ec685..6f799d84a4 100644 --- a/tools/internal/testnet-manager/Cargo.toml +++ b/tools/internal/testnet-manager/Cargo.toml @@ -7,6 +7,7 @@ homepage.workspace = true documentation.workspace = true edition.workspace = true license.workspace = true +rust-version.workspace = true [dependencies] anyhow.workspace = true @@ -36,6 +37,7 @@ nym-crypto = { path = "../../../common/crypto", features = ["asymmetric", "rand" nym-config = { path = "../../../common/config" } nym-validator-client = { path = "../../../common/client-libs/validator-client" } nym-compact-ecash = { path = "../../../common/nym_offline_compact_ecash" } +nym-http-api-client = { path = "../../../common/http-api-client" } dkg-bypass-contract = { path = "dkg-bypass-contract", default-features = false } # contracts: @@ -51,5 +53,6 @@ nym-pemstore = { path = "../../../common/pemstore" } [build-dependencies] +anyhow = { workspace = true } tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } sqlx = { workspace = true, features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"] } diff --git a/tools/internal/testnet-manager/build.rs b/tools/internal/testnet-manager/build.rs index 7e969e1e08..be394b3c41 100644 --- a/tools/internal/testnet-manager/build.rs +++ b/tools/internal/testnet-manager/build.rs @@ -1,19 +1,26 @@ +use anyhow::Context; use sqlx::{Connection, SqliteConnection}; use std::env; #[tokio::main] -async fn main() { - let out_dir = env::var("OUT_DIR").unwrap(); +async fn main() -> anyhow::Result<()> { + let out_dir = env::var("OUT_DIR")?; let database_path = format!("{out_dir}/nym-api-example.sqlite"); + // remove the db file if it already existed from previous build + // in case it was from a different branch + if std::fs::exists(&database_path)? { + std::fs::remove_file(&database_path)?; + } + let mut conn = SqliteConnection::connect(&format!("sqlite://{database_path}?mode=rwc")) .await - .expect("Failed to create SQLx database connection"); + .context("Failed to create SQLx database connection")?; sqlx::migrate!("./migrations") .run(&mut conn) .await - .expect("Failed to perform SQLx migrations"); + .context("Failed to perform SQLx migrations")?; #[cfg(target_family = "unix")] println!("cargo:rustc-env=DATABASE_URL=sqlite://{}", &database_path); @@ -22,4 +29,6 @@ async fn main() { // for some strange reason we need to add a leading `/` to the windows path even though it's // not a valid windows path... but hey, it works... println!("cargo:rustc-env=DATABASE_URL=sqlite:///{}", &database_path); + + Ok(()) } diff --git a/tools/internal/testnet-manager/src/manager/local_client.rs b/tools/internal/testnet-manager/src/manager/local_client.rs index 9e1d2025e2..8004c658df 100644 --- a/tools/internal/testnet-manager/src/manager/local_client.rs +++ b/tools/internal/testnet-manager/src/manager/local_client.rs @@ -7,7 +7,7 @@ use crate::manager::network::LoadedNetwork; use crate::manager::NetworkManager; use console::style; use nym_config::{must_get_home, DEFAULT_CONFIG_DIR, DEFAULT_CONFIG_FILENAME, NYM_DIR}; -use nym_validator_client::NymApiClient; +use nym_validator_client::nym_api::NymApiClientExt; use rand::{thread_rng, RngCore}; use std::fs; use std::fs::OpenOptions; @@ -91,7 +91,10 @@ impl NetworkManager { "⌛waiting for any gateway to appear in the directory ({api_url})..." )); - let api_client = NymApiClient::new(api_url); + let api_client = nym_http_api_client::Client::builder(api_url.clone()) + .expect("Failed to create API client builder") + .build() + .expect("Failed to build API client"); let wait_fut = async { let inner_fut = async { diff --git a/tools/internal/validator-status-check/Cargo.toml b/tools/internal/validator-status-check/Cargo.toml index 99eb182621..a0d09e336f 100644 --- a/tools/internal/validator-status-check/Cargo.toml +++ b/tools/internal/validator-status-check/Cargo.toml @@ -25,6 +25,7 @@ time = { workspace = true } nym-validator-client = { path = "../../../common/client-libs/validator-client" } nym-bin-common = { path = "../../../common/bin-common", features = ["output_format", "basic_tracing"] } nym-network-defaults = { path = "../../../common/network-defaults" } +nym-http-api-client = { path = "../../../common/http-api-client" } [lints] workspace = true diff --git a/tools/internal/validator-status-check/src/models.rs b/tools/internal/validator-status-check/src/models.rs index 12b8337f5d..11534118d2 100644 --- a/tools/internal/validator-status-check/src/models.rs +++ b/tools/internal/validator-status-check/src/models.rs @@ -1,8 +1,7 @@ // Copyright 2025 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use nym_validator_client::client::NymApiClientExt; -use nym_validator_client::NymApiClient; +use nym_validator_client::nym_api::NymApiClientExt; use serde::{Deserialize, Serialize}; use std::fmt::Display; use strum::{Display, EnumProperty}; @@ -48,8 +47,8 @@ impl SignerStatus { matches!(self.rpc_status, RpcStatus::Up) } - fn build_api_client(&self) -> Option { - let api_endpoint = match self.api_endpoint.as_str().parse() { + fn build_api_client(&self) -> Option { + let api_endpoint: nym_http_api_client::Url = match self.api_endpoint.as_str().parse() { Ok(endpoint) => endpoint, Err(err) => { error!("{} is not a valid api endpoint: {err}", self.api_endpoint); @@ -57,14 +56,17 @@ impl SignerStatus { } }; - Some(NymApiClient::new(api_endpoint)) + nym_http_api_client::Client::builder(api_endpoint) + .ok()? + .build() + .ok() } pub(crate) async fn try_update_api_version(&mut self) { let Some(client) = self.build_api_client() else { return; }; - match client.nym_api.build_information().await { + match client.build_information().await { Ok(build_info) => { self.api_version = ApiVersion::Available { version: build_info.build_version, @@ -84,7 +86,7 @@ impl SignerStatus { return; }; - match client.nym_api.get_chain_status().await { + match client.get_chain_status().await { Ok(chain_status) => { self.used_rpc_endpoint = RpcEndpoint(chain_status.connected_nyxd); let last_block = diff --git a/tools/nym-cli/src/internal/ecash/mod.rs b/tools/nym-cli/src/internal/ecash/mod.rs index ee9f8ef47c..8973925500 100644 --- a/tools/nym-cli/src/internal/ecash/mod.rs +++ b/tools/nym-cli/src/internal/ecash/mod.rs @@ -7,8 +7,8 @@ use nym_network_defaults::NymNetworkDetails; pub(super) async fn execute( global_args: ClientArgs, - ecash: nym_cli_commands::internal::ecash::InternalEcash, nym_network_details: &NymNetworkDetails, + ecash: nym_cli_commands::internal::ecash::InternalEcash, ) -> anyhow::Result<()> { // I reckon those will be needed later let _ = global_args; diff --git a/tools/nym-cli/src/internal/mod.rs b/tools/nym-cli/src/internal/mod.rs index 253c45b227..5bdb26f78d 100644 --- a/tools/nym-cli/src/internal/mod.rs +++ b/tools/nym-cli/src/internal/mod.rs @@ -6,6 +6,7 @@ use nym_cli_commands::internal::InternalCommands; use nym_network_defaults::NymNetworkDetails; mod ecash; +mod nyx; pub(super) async fn execute( global_args: ClientArgs, @@ -14,7 +15,10 @@ pub(super) async fn execute( ) -> anyhow::Result<()> { match internal.command { InternalCommands::Ecash(ecash_commands) => { - ecash::execute(global_args, ecash_commands, nym_network_details).await + ecash::execute(global_args, nym_network_details, ecash_commands).await + } + InternalCommands::Nyx(nyx_commands) => { + nyx::execute(global_args, nym_network_details, nyx_commands).await } } } diff --git a/tools/nym-cli/src/internal/nyx/mod.rs b/tools/nym-cli/src/internal/nyx/mod.rs new file mode 100644 index 0000000000..ac7bb500fc --- /dev/null +++ b/tools/nym-cli/src/internal/nyx/mod.rs @@ -0,0 +1,22 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use nym_cli_commands::context::{create_signing_client, ClientArgs}; +use nym_cli_commands::internal::nyx::InternalNyxCommands; +use nym_network_defaults::NymNetworkDetails; + +pub(super) async fn execute( + global_args: ClientArgs, + nym_network_details: &NymNetworkDetails, + nyx: nym_cli_commands::internal::nyx::InternalNyx, +) -> anyhow::Result<()> { + match nyx.command { + InternalNyxCommands::ForceAdvanceEpoch(args) => { + nym_cli_commands::internal::nyx::force_advance_epoch::force_advance_epoch( + args, + create_signing_client(global_args, nym_network_details)?, + ) + .await + } + } +} diff --git a/tools/ts-rs-cli/src/main.rs b/tools/ts-rs-cli/src/main.rs index 7f22be416e..9c80766327 100644 --- a/tools/ts-rs-cli/src/main.rs +++ b/tools/ts-rs-cli/src/main.rs @@ -2,10 +2,10 @@ use nym_api_requests::models::{ AnnotationResponse, DeclaredRoles, DescribedNodeType, GatewayCoreStatusResponse, - HistoricalPerformanceResponse, HistoricalUptimeResponse, InclusionProbabilityResponse, - MixnodeCoreStatusResponse, MixnodeStatus, MixnodeStatusResponse, NodeAnnotation, - NodeDatePerformanceResponse, NodePerformanceResponse, PerformanceHistoryResponse, - RewardEstimationResponse, SelectionChance, StakeSaturationResponse, UptimeHistoryResponse, + HistoricalPerformanceResponse, HistoricalUptimeResponse, MixnodeCoreStatusResponse, + MixnodeStatus, MixnodeStatusResponse, NodeAnnotation, NodeDatePerformanceResponse, + NodePerformanceResponse, PerformanceHistoryResponse, StakeSaturationResponse, + UptimeHistoryResponse, }; use nym_api_requests::pagination::{PaginatedResponse, Pagination}; use nym_mixnet_contract_common::nym_node::{NodeConfigUpdate, Role}; @@ -141,12 +141,9 @@ fn main() -> anyhow::Result<()> { // nym-api-requests do_export!(MixnodeCoreStatusResponse); do_export!(GatewayCoreStatusResponse); - do_export!(InclusionProbabilityResponse); do_export!(MixnodeStatus); do_export!(MixnodeStatusResponse); - do_export!(SelectionChance); do_export!(StakeSaturationResponse); - do_export!(RewardEstimationResponse); do_export!(NodeAnnotation); do_export!(AnnotationResponse); do_export!(NodePerformanceResponse); diff --git a/wasm/client/src/client.rs b/wasm/client/src/client.rs index e82a8cb78d..4e70186f73 100644 --- a/wasm/client/src/client.rs +++ b/wasm/client/src/client.rs @@ -28,7 +28,7 @@ use wasm_client_core::helpers::{ add_gateway, generate_new_client_keys, parse_recipient, parse_sender_tag, }; use wasm_client_core::nym_task::connections::TransmissionLane; -use wasm_client_core::nym_task::TaskManager; +use wasm_client_core::nym_task::ShutdownManager; use wasm_client_core::storage::core_client_traits::FullWasmClientStorage; use wasm_client_core::storage::wasm_client_traits::WasmClientStorage; use wasm_client_core::storage::ClientStorage; @@ -59,7 +59,7 @@ pub struct NymClient { // even though we don't use graceful shutdowns, other components rely on existence of this struct // and if it's dropped, everything will start going offline - _task_manager: TaskManager, + _task_manager: ShutdownManager, packet_type: PacketType, } @@ -249,8 +249,10 @@ impl NymClientBuilder { client_input: Arc::new(client_input), client_state: Arc::new(started_client.client_state), _full_topology: None, - // this cannot failed as we haven't passed an external task manager - _task_manager: started_client.task_handle.try_into_task_manager().unwrap(), + // this cannot fail as we haven't passed an external task manager + _task_manager: started_client + .shutdown_handle + .expect("shutdown manager missing"), packet_type, }) } diff --git a/wasm/client/src/config.rs b/wasm/client/src/config.rs index 8c841c4c16..9bb19ad8c1 100644 --- a/wasm/client/src/config.rs +++ b/wasm/client/src/config.rs @@ -16,7 +16,7 @@ use wasm_client_core::config::{new_base_client_config, BaseClientConfig, ConfigD pub const DEFAULT_CLIENT_ID: &str = "nym-mixnet-client"; #[wasm_bindgen] -#[derive(Debug, Deserialize, PartialEq, Serialize)] +#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)] #[serde(deny_unknown_fields)] pub struct ClientConfig { pub(crate) base: BaseClientConfig, diff --git a/wasm/mix-fetch/src/client.rs b/wasm/mix-fetch/src/client.rs index 2a2aeb0ab5..e462d5835e 100644 --- a/wasm/mix-fetch/src/client.rs +++ b/wasm/mix-fetch/src/client.rs @@ -20,7 +20,7 @@ use wasm_client_core::client::base_client::{BaseClientBuilder, ClientInput, Clie use wasm_client_core::client::inbound_messages::InputMessage; use wasm_client_core::helpers::{add_gateway, generate_new_client_keys}; use wasm_client_core::nym_task::connections::TransmissionLane; -use wasm_client_core::nym_task::TaskManager; +use wasm_client_core::nym_task::ShutdownManager; use wasm_client_core::storage::core_client_traits::FullWasmClientStorage; use wasm_client_core::storage::wasm_client_traits::WasmClientStorage; use wasm_client_core::storage::ClientStorage; @@ -41,7 +41,7 @@ pub struct MixFetchClient { requests: ActiveRequests, // this has to be guarded by a mutex to be able to disconnect with an immutable reference - _task_manager: Mutex, + _shutdown_manager: Mutex, } #[wasm_bindgen] @@ -187,8 +187,12 @@ impl MixFetchClientBuilder { self_address, client_input, requests: active_requests, - // this cannot failed as we haven't passed an external task manager - _task_manager: Mutex::new(started_client.task_handle.try_into_task_manager().unwrap()), + // this cannot fail as we haven't passed an external task manager + _shutdown_manager: Mutex::new( + started_client + .shutdown_handle + .expect("shutdown manager missing"), + ), }) } } @@ -229,11 +233,11 @@ impl MixFetchClient { self.invalidated.store(true, Ordering::Relaxed); console_log!("sending shutdown signal"); - let mut shutdown_guard = self._task_manager.lock().await; - shutdown_guard.signal_shutdown().ok(); + let mut shutdown_guard = self._shutdown_manager.lock().await; + shutdown_guard.send_cancellation(); console_log!("waiting for shutdown to complete"); - shutdown_guard.wait_for_shutdown().await; + shutdown_guard.run_until_shutdown().await; self.requests.invalidate_all().await; diff --git a/wasm/node-tester/src/tester.rs b/wasm/node-tester/src/tester.rs index 8ab6b23918..9a48951ea7 100644 --- a/wasm/node-tester/src/tester.rs +++ b/wasm/node-tester/src/tester.rs @@ -15,7 +15,6 @@ use js_sys::Promise; use nym_node_tester_utils::receiver::SimpleMessageReceiver; use nym_node_tester_utils::tester::LegacyMixLayer; use nym_node_tester_utils::{NodeTester, PacketSize, PreparedFragment}; -use nym_task::TaskManager; use rand::rngs::OsRng; use serde::{Deserialize, Serialize}; use std::collections::HashSet; @@ -32,6 +31,7 @@ use wasm_client_core::client::mix_traffic::transceiver::PacketRouter; use wasm_client_core::helpers::{ current_network_topology_async, setup_from_topology, EphemeralCredentialStorage, }; +use wasm_client_core::nym_task::ShutdownManager; use wasm_client_core::storage::ClientStorage; use wasm_client_core::topology::WasmFriendlyNymTopology; use wasm_client_core::{ @@ -70,7 +70,7 @@ pub struct NymNodeTester { // even though we don't use graceful shutdowns, other components rely on existence of this struct // and if it's dropped, everything will start going offline - _task_manager: TaskManager, + _task_manager: ShutdownManager, } #[wasm_bindgen] @@ -159,7 +159,7 @@ impl NymNodeTesterBuilder { } async fn _setup_client(mut self) -> Result { - let task_manager = TaskManager::default(); + let task_manager = ShutdownManager::new_without_signals(); let storage_id = if let Some(client_id) = &self.id { format!("{NODE_TESTER_ID}-{client_id}") @@ -181,17 +181,13 @@ impl NymNodeTesterBuilder { let (mixnet_message_sender, mixnet_message_receiver) = mpsc::unbounded(); let (ack_sender, ack_receiver) = mpsc::unbounded(); - let gateway_task = task_manager.subscribe().named("gateway_client"); - let packet_router = PacketRouter::new( - ack_sender, - mixnet_message_sender, - gateway_task.fork("packet_router"), - ); + let gateway_task = task_manager.clone_shutdown_token(); + let packet_router = + PacketRouter::new(ack_sender, mixnet_message_sender, gateway_task.clone()); let gateway_identity = gateway_info.gateway_id; - let mut stats_sender_task = task_manager.subscribe().named("stats_sender"); - stats_sender_task.disarm(); + let stats_sender_task = task_manager.clone_shutdown_token(); let mut gateway_client = if let Some(existing_client) = initialisation_result.authenticated_ephemeral_client { @@ -199,7 +195,7 @@ impl NymNodeTesterBuilder { packet_router, self.bandwidth_controller.take(), ClientStatsSender::new(None, stats_sender_task), - gateway_task, + gateway_task.clone(), ) } else { let cfg = GatewayConfig::new( @@ -250,10 +246,10 @@ impl NymNodeTesterBuilder { mixnet_message_receiver, ack_receiver, processed_sender, - task_manager.subscribe(), + task_manager.clone_shutdown_token(), ); - nym_task::spawn(async move { receiver.run().await }); + nym_task::spawn_future(async move { receiver.run().await }); Ok(NymNodeTester { test_in_progress: Arc::new(AtomicBool::new(false)), diff --git a/wasm/zknym-lib/src/error.rs b/wasm/zknym-lib/src/error.rs index f232daddf3..5fe8794262 100644 --- a/wasm/zknym-lib/src/error.rs +++ b/wasm/zknym-lib/src/error.rs @@ -1,7 +1,7 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::vpn_api_client::NymVpnApiClientError; +use nym_http_api_client::HttpClientError; use thiserror::Error; use wasm_utils::wasm_error; @@ -16,7 +16,7 @@ pub enum ZkNymError { #[error("failed to contact the vpn api")] HttpClientFailure { #[from] - source: NymVpnApiClientError, + source: Box, }, #[error("the provided shares and issuers are not from the same epoch! {shares} and {issuers}")] InconsistentEpochId { shares: u64, issuers: u64 }, diff --git a/wasm/zknym-lib/src/vpn_api_client/client.rs b/wasm/zknym-lib/src/vpn_api_client/client.rs index e3a7f983b0..843753b7f0 100644 --- a/wasm/zknym-lib/src/vpn_api_client/client.rs +++ b/wasm/zknym-lib/src/vpn_api_client/client.rs @@ -1,14 +1,14 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 - -use super::NymVpnApiClientError; use crate::error::ZkNymError; use crate::vpn_api_client::types::{ AttributesResponse, MasterVerificationKeyResponse, PartialVerificationKeysResponse, }; use async_trait::async_trait; pub use nym_http_api_client::Client; -use nym_http_api_client::{parse_response, ApiClient, IntoUrl, PathSegments, NO_PARAMS}; +use nym_http_api_client::{ + parse_response, ApiClient, HttpClientError, IntoUrl, PathSegments, NO_PARAMS, +}; use serde::de::DeserializeOwned; #[allow(dead_code)] @@ -23,9 +23,11 @@ pub fn new_client( bearer_token: impl Into, ) -> Result { Ok(VpnApiClient { - inner: Client::builder(base_url)? + inner: Client::builder(base_url) + .map_err(Box::new)? .with_user_agent(format!("nym-wasm-znym-lib/{}", env!("CARGO_PKG_VERSION"))) - .build()?, + .build() + .map_err(Box::new)?, bearer_token: bearer_token.into(), }) } @@ -34,13 +36,11 @@ pub fn new_client( #[allow(dead_code)] #[async_trait(?Send)] pub trait NymVpnApiClient { - async fn simple_get(&self, path: PathSegments<'_>) -> Result + async fn simple_get(&self, path: PathSegments<'_>) -> Result where T: DeserializeOwned; - async fn get_prehashed_public_attributes( - &self, - ) -> Result { + async fn get_prehashed_public_attributes(&self) -> Result { self.simple_get(&[ "/api", "/v1", @@ -52,7 +52,7 @@ pub trait NymVpnApiClient { async fn get_partial_verification_keys( &self, - ) -> Result { + ) -> Result { self.simple_get(&[ "/api", "/v1", @@ -64,7 +64,7 @@ pub trait NymVpnApiClient { async fn get_master_verification_key( &self, - ) -> Result { + ) -> Result { self.simple_get(&[ "/api", "/v1", @@ -77,13 +77,13 @@ pub trait NymVpnApiClient { #[async_trait(?Send)] impl NymVpnApiClient for VpnApiClient { - async fn simple_get(&self, path: PathSegments<'_>) -> Result + async fn simple_get(&self, path: PathSegments<'_>) -> Result where T: DeserializeOwned, { let req = self .inner - .create_get_request(path, NO_PARAMS) + .create_get_request(path, NO_PARAMS)? .bearer_auth(&self.bearer_token) .send(); diff --git a/wasm/zknym-lib/src/vpn_api_client/mod.rs b/wasm/zknym-lib/src/vpn_api_client/mod.rs index 041e9f0bc1..366fa05443 100644 --- a/wasm/zknym-lib/src/vpn_api_client/mod.rs +++ b/wasm/zknym-lib/src/vpn_api_client/mod.rs @@ -1,12 +1,7 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::vpn_api_client::types::ErrorResponse; -use nym_http_api_client::HttpClientError; - #[cfg(test)] pub(crate) mod client; pub mod types; - -pub type NymVpnApiClientError = HttpClientError; diff --git a/yarn.lock b/yarn.lock index b6fd2d389b..2c906c0b93 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1156,7 +1156,7 @@ dependencies: regenerator-runtime "^0.13.2" -"@babel/runtime@^7.0.0", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.15.4", "@babel/runtime@^7.17.8", "@babel/runtime@^7.18.3", "@babel/runtime@^7.21.0", "@babel/runtime@^7.23.2", "@babel/runtime@^7.23.9", "@babel/runtime@^7.24.0", "@babel/runtime@^7.25.0", "@babel/runtime@^7.25.7", "@babel/runtime@^7.26.0", "@babel/runtime@^7.26.10", "@babel/runtime@^7.27.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": +"@babel/runtime@^7.0.0", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.15.4", "@babel/runtime@^7.17.8", "@babel/runtime@^7.18.3", "@babel/runtime@^7.21.0", "@babel/runtime@^7.23.2", "@babel/runtime@^7.23.9", "@babel/runtime@^7.25.0", "@babel/runtime@^7.25.7", "@babel/runtime@^7.26.0", "@babel/runtime@^7.26.10", "@babel/runtime@^7.27.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": version "7.27.0" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.27.0.tgz#fbee7cf97c709518ecc1f590984481d5460d4762" integrity sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw== @@ -1334,7 +1334,7 @@ "@cosmjs/math" "^0.32.4" "@cosmjs/utils" "^0.32.4" -"@cosmjs/cosmwasm-stargate@^0.25.5", "@cosmjs/cosmwasm-stargate@^0.29.5", "@cosmjs/cosmwasm-stargate@^0.32.3", "@cosmjs/cosmwasm-stargate@^0.32.4": +"@cosmjs/cosmwasm-stargate@^0.29.5", "@cosmjs/cosmwasm-stargate@^0.32.3", "@cosmjs/cosmwasm-stargate@^0.32.4": version "0.32.4" resolved "https://registry.yarnpkg.com/@cosmjs/cosmwasm-stargate/-/cosmwasm-stargate-0.32.4.tgz#2ee93f2cc0b1c146ac369b2bf8ef9ee2e159fd50" integrity sha512-Fuo9BGEiB+POJ5WeRyBGuhyKR1ordvxZGLPuPosFJOH9U0gKMgcjwKMCgAlWFkMlHaTB+tNdA8AifWiHrI7VgA== @@ -1410,13 +1410,6 @@ "@cosmjs/stream" "^0.32.4" xstream "^11.14.0" -"@cosmjs/math@^0.25.5": - version "0.25.6" - resolved "https://registry.yarnpkg.com/@cosmjs/math/-/math-0.25.6.tgz#25c7b106aaded889a5b80784693caa9e654b0c28" - integrity sha512-Fmyc9FJ8KMU34n7rdapMJrT/8rx5WhMw2F7WLBu7AVLcBh0yWsXIcMSJCoPHTOnMIiABjXsnrrwEaLrOOBfu6A== - dependencies: - bn.js "^4.11.8" - "@cosmjs/math@^0.29.5": version "0.29.5" resolved "https://registry.yarnpkg.com/@cosmjs/math/-/math-0.29.5.tgz#722c96e080d6c2b62215ce9f4c70da7625b241b6" @@ -1431,7 +1424,7 @@ dependencies: bn.js "^5.2.0" -"@cosmjs/proto-signing@^0.25.5", "@cosmjs/proto-signing@^0.29.5", "@cosmjs/proto-signing@^0.32.3", "@cosmjs/proto-signing@^0.32.4": +"@cosmjs/proto-signing@^0.29.5", "@cosmjs/proto-signing@^0.32.3", "@cosmjs/proto-signing@^0.32.4": version "0.32.4" resolved "https://registry.yarnpkg.com/@cosmjs/proto-signing/-/proto-signing-0.32.4.tgz#5a06e087c6d677439c8c9b25b5223d5e72c4cd93" integrity sha512-QdyQDbezvdRI4xxSlyM1rSVBO2st5sqtbEIl3IX03uJ7YiZIQHyv6vaHVf1V4mapusCqguiHJzm4N4gsFdLBbQ== @@ -1463,7 +1456,7 @@ ws "^7" xstream "^11.14.0" -"@cosmjs/stargate@^0.25.5", "@cosmjs/stargate@^0.29.5", "@cosmjs/stargate@^0.32.3", "@cosmjs/stargate@^0.32.4": +"@cosmjs/stargate@^0.29.5", "@cosmjs/stargate@^0.32.3", "@cosmjs/stargate@^0.32.4": version "0.32.4" resolved "https://registry.yarnpkg.com/@cosmjs/stargate/-/stargate-0.32.4.tgz#bd0e4d3bf613b629addbf5f875d3d3b50f640af1" integrity sha512-usj08LxBSsPRq9sbpCeVdyLx2guEcOHfJS9mHGCLCXpdAPEIEQEtWLDpEUc0LEhWOx6+k/ChXTc5NpFkdrtGUQ== @@ -1584,13 +1577,6 @@ "@cosmos-kit/react-lite" "^2.16.0" "@react-icons/all-files" "^4.1.0" -"@cspotcode/source-map-support@^0.8.0": - version "0.8.1" - resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1" - integrity sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw== - dependencies: - "@jridgewell/trace-mapping" "0.3.9" - "@csstools/color-helpers@^5.0.2": version "5.0.2" resolved "https://registry.yarnpkg.com/@csstools/color-helpers/-/color-helpers-5.0.2.tgz#82592c9a7c2b83c293d9161894e2a6471feb97b8" @@ -2520,7 +2506,7 @@ "@jridgewell/sourcemap-codec" "^1.4.10" "@jridgewell/trace-mapping" "^0.3.24" -"@jridgewell/resolve-uri@^3.0.3", "@jridgewell/resolve-uri@^3.1.0": +"@jridgewell/resolve-uri@^3.1.0": version "3.1.2" resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== @@ -2543,14 +2529,6 @@ resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz#3188bcb273a414b0d215fd22a58540b989b9409a" integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ== -"@jridgewell/trace-mapping@0.3.9": - version "0.3.9" - resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9" - integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ== - dependencies: - "@jridgewell/resolve-uri" "^3.0.3" - "@jridgewell/sourcemap-codec" "^1.4.10" - "@jridgewell/trace-mapping@^0.3.12", "@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25": version "0.3.25" resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz#15f190e98895f3fc23276ee14bc76b675c2e50f0" @@ -2787,19 +2765,6 @@ clsx "^2.1.0" prop-types "^15.8.1" -"@mui/base@^5.0.0-beta.40": - version "5.0.0-beta.70" - resolved "https://registry.yarnpkg.com/@mui/base/-/base-5.0.0-beta.70.tgz#cd9755b8ae1b375fb3c985f06a4741505d793ecf" - integrity sha512-Tb/BIhJzb0pa5zv/wu7OdokY9ZKEDqcu1BDFnohyvGCoHuSXbEr90rPq1qeNW3XvTBIbNWHEF7gqge+xpUo6tQ== - dependencies: - "@babel/runtime" "^7.26.0" - "@floating-ui/react-dom" "^2.1.1" - "@mui/types" "~7.2.24" - "@mui/utils" "^6.4.8" - "@popperjs/core" "^2.11.8" - clsx "^2.1.1" - prop-types "^15.8.1" - "@mui/core-downloads-tracker@^5.17.1": version "5.17.1" resolved "https://registry.yarnpkg.com/@mui/core-downloads-tracker/-/core-downloads-tracker-5.17.1.tgz#49b88ecb68b800431b5c2f2bfb71372d1f1478fa" @@ -2936,7 +2901,7 @@ jss-plugin-vendor-prefixer "^10.10.0" prop-types "^15.8.1" -"@mui/system@^5.15.14", "@mui/system@^5.15.15", "@mui/system@^5.17.1": +"@mui/system@^5.15.15", "@mui/system@^5.17.1": version "5.17.1" resolved "https://registry.yarnpkg.com/@mui/system/-/system-5.17.1.tgz#1f987cce91bf738545a8cf5f99152cd2728e6077" integrity sha512-aJrmGfQpyF0U4D4xYwA6ueVtQcEMebET43CUmKMP7e7iFh3sMIF3sBR0l8Urb4pqx1CBjHAaWgB0ojpND4Q3Jg== @@ -3000,7 +2965,7 @@ prop-types "^15.8.1" react-is "^19.1.0" -"@mui/utils@^6.4.8", "@mui/utils@^6.4.9": +"@mui/utils@^6.4.9": version "6.4.9" resolved "https://registry.yarnpkg.com/@mui/utils/-/utils-6.4.9.tgz#b0df01daa254c7c32a1a30b30a5179e19ef071a7" integrity sha512-Y12Q9hbK9g+ZY0T3Rxrx9m2m10gaphDuUMgWxyV5kNJevVxXYCLclYUCC9vXaIk1/NdNDTcW2Yfr2OGvNFNmHg== @@ -3012,67 +2977,6 @@ prop-types "^15.8.1" react-is "^19.0.0" -"@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.28.0" - resolved "https://registry.yarnpkg.com/@mui/x-charts/-/x-charts-7.28.0.tgz#8c3c4b7c02474ef80669c3416d24ebc245da5519" - integrity sha512-TNfq/rQfGKnjTaEITkY6l09NpMxwMwRTgLiDw+JQsS/7gwBBJUmMhEOj67BaFeYTsroFLUYeggiAj+RTSryd4A== - dependencies: - "@babel/runtime" "^7.25.7" - "@mui/utils" "^5.16.6 || ^6.0.0 || ^7.0.0 || ^7.0.0-beta" - "@mui/x-charts-vendor" "7.20.0" - "@mui/x-internals" "7.28.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" - integrity sha512-hNvz927lkAznFdy45QPE7mIZVyQhlqveHmTK9+SD0N1us4sSTij90uUJ/roTNDod0VA9f5GqWmNz+5h8ihpz6Q== - dependencies: - "@babel/runtime" "^7.24.0" - "@mui/system" "^5.15.14" - "@mui/utils" "^5.15.14" - clsx "^2.1.0" - prop-types "^15.8.1" - reselect "^4.1.8" - -"@mui/x-date-pickers@7.1.1": - version "7.1.1" - resolved "https://registry.yarnpkg.com/@mui/x-date-pickers/-/x-date-pickers-7.1.1.tgz#13523f3d1cc9df89def9a6f90b19ae2d8d5d13ea" - integrity sha512-doSaoNfYR4nAXSN2mz5MwktYUmPt37jZ8/t5QrPgFtEFc3KWZoBps0YEcno5qUynY1ISpOjvnVr18zqszzG+RA== - dependencies: - "@babel/runtime" "^7.24.0" - "@mui/base" "^5.0.0-beta.40" - "@mui/system" "^5.15.14" - "@mui/utils" "^5.15.14" - "@types/react-transition-group" "^4.4.10" - clsx "^2.1.0" - prop-types "^15.8.1" - react-transition-group "^4.4.5" - "@mui/x-date-pickers@^7.23.2": version "7.28.3" resolved "https://registry.yarnpkg.com/@mui/x-date-pickers/-/x-date-pickers-7.28.3.tgz#eaf28b86b50b6fb61d0c671ba4e3d89a1b199a57" @@ -3116,23 +3020,11 @@ "@emnapi/runtime" "^1.4.0" "@tybys/wasm-util" "^0.9.0" -"@next/env@14.2.26": - version "14.2.26" - resolved "https://registry.yarnpkg.com/@next/env/-/env-14.2.26.tgz#5d55f72d2edb7246607c78f61e7d3ff21516bc2e" - integrity sha512-vO//GJ/YBco+H7xdQhzJxF7ub3SUwft76jwaeOyVVQFHCi5DCnkP16WHB+JBylo4vOKPoZBlR94Z8xBxNBdNJA== - "@next/env@14.2.28": version "14.2.28" resolved "https://registry.yarnpkg.com/@next/env/-/env-14.2.28.tgz#4bfeac21949743bfc8d09cfc223439112bcd2538" integrity sha512-PAmWhJfJQlP+kxZwCjrVd9QnR5x0R3u0mTXTiZDgSd4h5LdXmjxCCWbN9kq6hkZBOax8Rm3xDW5HagWyJuT37g== -"@next/eslint-plugin-next@14.1.4": - version "14.1.4" - resolved "https://registry.yarnpkg.com/@next/eslint-plugin-next/-/eslint-plugin-next-14.1.4.tgz#d7372b5ffede0e466af8af2ff534386418827fc8" - integrity sha512-n4zYNLSyCo0Ln5b7qxqQeQ34OZKXwgbdcx6kmkQbywr+0k6M3Vinft0T72R6CDAcDrne2IAgSud4uWCzFgc5HA== - dependencies: - glob "10.3.10" - "@next/eslint-plugin-next@15.0.3": version "15.0.3" resolved "https://registry.yarnpkg.com/@next/eslint-plugin-next/-/eslint-plugin-next-15.0.3.tgz#ce953098036d462f6901e423cc6445fc165b78c4" @@ -3140,91 +3032,46 @@ dependencies: fast-glob "3.3.1" -"@next/swc-darwin-arm64@14.2.26": - version "14.2.26" - resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.26.tgz#84b31a22149b2c49f5c5b29cddd7acb3a84d7e1c" - integrity sha512-zDJY8gsKEseGAxG+C2hTMT0w9Nk9N1Sk1qV7vXYz9MEiyRoF5ogQX2+vplyUMIfygnjn9/A04I6yrUTRTuRiyQ== - "@next/swc-darwin-arm64@14.2.28": version "14.2.28" resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.28.tgz#b65bdd4f95eb883ca621d96563baa54ac7df6e3c" integrity sha512-kzGChl9setxYWpk3H6fTZXXPFFjg7urptLq5o5ZgYezCrqlemKttwMT5iFyx/p1e/JeglTwDFRtb923gTJ3R1w== -"@next/swc-darwin-x64@14.2.26": - version "14.2.26" - resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.26.tgz#50a5eb37972d313951f76f36f1f0b7100d063ebd" - integrity sha512-U0adH5ryLfmTDkahLwG9sUQG2L0a9rYux8crQeC92rPhi3jGQEY47nByQHrVrt3prZigadwj/2HZ1LUUimuSbg== - "@next/swc-darwin-x64@14.2.28": version "14.2.28" resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.28.tgz#1dc7d4a27927043ec3259b88044f11cce1219be4" integrity sha512-z6FXYHDJlFOzVEOiiJ/4NG8aLCeayZdcRSMjPDysW297Up6r22xw6Ea9AOwQqbNsth8JNgIK8EkWz2IDwaLQcw== -"@next/swc-linux-arm64-gnu@14.2.26": - version "14.2.26" - resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.26.tgz#c4278c157623b05886e37ff17194811aca1c2d00" - integrity sha512-SINMl1I7UhfHGM7SoRiw0AbwnLEMUnJ/3XXVmhyptzriHbWvPPbbm0OEVG24uUKhuS1t0nvN/DBvm5kz6ZIqpg== - "@next/swc-linux-arm64-gnu@14.2.28": version "14.2.28" resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.28.tgz#a4c6a805a821bb59fc66baa18236ddcfdb62e515" integrity sha512-9ARHLEQXhAilNJ7rgQX8xs9aH3yJSj888ssSjJLeldiZKR4D7N08MfMqljk77fAwZsWwsrp8ohHsMvurvv9liQ== -"@next/swc-linux-arm64-musl@14.2.26": - version "14.2.26" - resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.26.tgz#5751132764b7a1f13a5a3fe447b03d564eb29705" - integrity sha512-s6JaezoyJK2DxrwHWxLWtJKlqKqTdi/zaYigDXUJ/gmx/72CrzdVZfMvUc6VqnZ7YEvRijvYo+0o4Z9DencduA== - "@next/swc-linux-arm64-musl@14.2.28": version "14.2.28" resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.28.tgz#1b8cd8c9acdba9e591661f36dc3e04ef805c6701" integrity sha512-p6gvatI1nX41KCizEe6JkF0FS/cEEF0u23vKDpl+WhPe/fCTBeGkEBh7iW2cUM0rvquPVwPWdiUR6Ebr/kQWxQ== -"@next/swc-linux-x64-gnu@14.2.26": - version "14.2.26" - resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.26.tgz#74312cac45704762faa73e0880be6549027303af" - integrity sha512-FEXeUQi8/pLr/XI0hKbe0tgbLmHFRhgXOUiPScz2hk0hSmbGiU8aUqVslj/6C6KA38RzXnWoJXo4FMo6aBxjzg== - "@next/swc-linux-x64-gnu@14.2.28": version "14.2.28" resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.28.tgz#ba796651b1214b3e8a8aa34398c432b8defbe325" integrity sha512-nsiSnz2wO6GwMAX2o0iucONlVL7dNgKUqt/mDTATGO2NY59EO/ZKnKEr80BJFhuA5UC1KZOMblJHWZoqIJddpA== -"@next/swc-linux-x64-musl@14.2.26": - version "14.2.26" - resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.26.tgz#5d96464d71d2000ec704e650a1a86bb9d73f760d" - integrity sha512-BUsomaO4d2DuXhXhgQCVt2jjX4B4/Thts8nDoIruEJkhE5ifeQFtvW5c9JkdOtYvE5p2G0hcwQ0UbRaQmQwaVg== - "@next/swc-linux-x64-musl@14.2.28": version "14.2.28" resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.28.tgz#d1127560ca2aec303daded021b51d9cd49f9f5ca" integrity sha512-+IuGQKoI3abrXFqx7GtlvNOpeExUH1mTIqCrh1LGFf8DnlUcTmOOCApEnPJUSLrSbzOdsF2ho2KhnQoO0I1RDw== -"@next/swc-win32-arm64-msvc@14.2.26": - version "14.2.26" - resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.26.tgz#859472b532b11499b8f5c2237f54401456286913" - integrity sha512-5auwsMVzT7wbB2CZXQxDctpWbdEnEW/e66DyXO1DcgHxIyhP06awu+rHKshZE+lPLIGiwtjo7bsyeuubewwxMw== - "@next/swc-win32-arm64-msvc@14.2.28": version "14.2.28" resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.28.tgz#de2d115304adc5a576816a25fba872ceef270676" integrity sha512-l61WZ3nevt4BAnGksUVFKy2uJP5DPz2E0Ma/Oklvo3sGj9sw3q7vBWONFRgz+ICiHpW5mV+mBrkB3XEubMrKaA== -"@next/swc-win32-ia32-msvc@14.2.26": - version "14.2.26" - resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.26.tgz#e52e9bd0c43b7a469b03eda6d7a07c3d0c28f549" - integrity sha512-GQWg/Vbz9zUGi9X80lOeGsz1rMH/MtFO/XqigDznhhhTfDlDoynCM6982mPCbSlxJ/aveZcKtTlwfAjwhyxDpg== - "@next/swc-win32-ia32-msvc@14.2.28": version "14.2.28" resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.28.tgz#bd15266c1e24965e71faa31a2596693b5dd61944" integrity sha512-+Kcp1T3jHZnJ9v9VTJ/yf1t/xmtFAc/Sge4v7mVc1z+NYfYzisi8kJ9AsY8itbgq+WgEwMtOpiLLJsUy2qnXZw== -"@next/swc-win32-x64-msvc@14.2.26": - version "14.2.26" - resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.26.tgz#6f42a3ae16ae15c5c5e36efa9b7e291c86ab1275" - integrity sha512-2rdB3T1/Gp7bv1eQTTm9d1Y1sv9UuJ2LAwOE0Pe2prHKe32UNscj7YS13fRB37d0GAiGNR+Y7ZcW8YjDI8Ns0w== - "@next/swc-win32-x64-msvc@14.2.28": version "14.2.28" resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.28.tgz#e9de0aec5cda06bfa0e639ad2799829ae6617bf7" @@ -3554,17 +3401,6 @@ resolved "https://registry.yarnpkg.com/@nymproject/node-tester/-/node-tester-1.2.3.tgz#79fbde8b69e2d1180eed897557c4610a1aa1038f" integrity sha512-VDFdH2ddIcXXamwkMbeHA88Xa/S2iPWh9QxkFggppvHS1d6gmnNHAZxXm3Uuhx7pCpzKldA0OT7qohMg9GW9xg== -"@nymproject/nym-validator-client@0.18.0": - version "0.18.0" - resolved "https://registry.yarnpkg.com/@nymproject/nym-validator-client/-/nym-validator-client-0.18.0.tgz#4dd72bafdf6c72b603242f32c0bb9a1f9e475b98" - integrity sha512-FO1T15S2BJVuMoPA2yOaH40aD3hKJPKJVyX5ix7eJEbBWIdsYNoVeVc/soHhaAU1FIy2uU0G/FZQkaUYJGGb7Q== - dependencies: - "@cosmjs/cosmwasm-stargate" "^0.25.5" - "@cosmjs/math" "^0.25.5" - "@cosmjs/proto-signing" "^0.25.5" - "@cosmjs/stargate" "^0.25.5" - axios "^0.21.1" - "@octokit/auth-token@^3.0.0": version "3.0.4" resolved "https://registry.yarnpkg.com/@octokit/auth-token/-/auth-token-3.0.4.tgz#70e941ba742bdd2b49bdb7393e821dea8520a3db" @@ -4427,7 +4263,7 @@ "@react-spring/shared" "~9.7.5" "@react-spring/types" "~9.7.5" -"@react-spring/rafz@^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== @@ -4445,7 +4281,7 @@ resolved "https://registry.yarnpkg.com/@react-spring/types/-/types-9.7.5.tgz#e5dd180f3ed985b44fd2cd2f32aa9203752ef3e8" integrity sha512-HVj7LrZ4ReHWBimBvu2SKND3cDVUPWKLqRTmWe/fNY6o1owGOX0cAHbdPDTMelgBlVbrTKrre6lFkhqGZErK/g== -"@react-spring/web@9.4.5 || ^9.7.2", "@react-spring/web@^9.7.5": +"@react-spring/web@9.4.5 || ^9.7.2": 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== @@ -4992,7 +4828,7 @@ resolved "https://registry.yarnpkg.com/@rtsao/scc/-/scc-1.1.0.tgz#927dd2fae9bc3361403ac2c7a00c32ddce9ad7e8" integrity sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g== -"@rushstack/eslint-patch@^1.10.3", "@rushstack/eslint-patch@^1.3.3": +"@rushstack/eslint-patch@^1.10.3": version "1.11.0" resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.11.0.tgz#75dce8e972f90bba488e2b0cc677fb233aa357ab" integrity sha512-zxnHvoMQVqewTJr/W4pKjF0bMGiKJv1WX7bSrkl46Hg0QjESbzBROWK0Wg4RphzSOS5Jiy7eFimmM3UgMrMZbQ== @@ -6197,13 +6033,6 @@ dependencies: "@tanstack/query-core" "5.72.2" -"@tanstack/react-table@8.20.5": - version "8.20.5" - resolved "https://registry.yarnpkg.com/@tanstack/react-table/-/react-table-8.20.5.tgz#19987d101e1ea25ef5406dce4352cab3932449d8" - integrity sha512-WEHopKw3znbUZ61s9i0+i9g8drmDo6asTWbrQh8Us63DAk/M0FkmIqERew6P71HI75ksZ2Pxyuf4vvKh9rAkiA== - dependencies: - "@tanstack/table-core" "8.20.5" - "@tanstack/react-table@8.20.6": version "8.20.6" resolved "https://registry.yarnpkg.com/@tanstack/react-table/-/react-table-8.20.6.tgz#a1f3103327aa59aa621931f4087a7604a21054d0" @@ -6218,13 +6047,6 @@ dependencies: "@tanstack/table-core" "8.21.2" -"@tanstack/react-virtual@3.10.6": - version "3.10.6" - resolved "https://registry.yarnpkg.com/@tanstack/react-virtual/-/react-virtual-3.10.6.tgz#f90f97d50a8d83dcd3c3a2d425aadbb55d4837db" - integrity sha512-xaSy6uUxB92O8mngHZ6CvbhGuqxQ5lIZWCBy+FjhrbHmOwc6BnOnKkYm2FsB1/BpKw/+FVctlMbEtI+F6I1aJg== - dependencies: - "@tanstack/virtual-core" "3.10.6" - "@tanstack/react-virtual@3.11.2": version "3.11.2" resolved "https://registry.yarnpkg.com/@tanstack/react-virtual/-/react-virtual-3.11.2.tgz#d6b9bd999c181f0a2edce270c87a2febead04322" @@ -6249,11 +6071,6 @@ resolved "https://registry.yarnpkg.com/@tanstack/table-core/-/table-core-8.21.2.tgz#dd57595a1773652bb6fb437e90a5f5386a49fd7e" integrity sha512-uvXk/U4cBiFMxt+p9/G7yUWI/UbHYbyghLCjlpWZ3mLeIZiUBSKcUnw9UnKkdRz7Z/N4UBuFLWQdJCjUe7HjvA== -"@tanstack/virtual-core@3.10.6": - version "3.10.6" - resolved "https://registry.yarnpkg.com/@tanstack/virtual-core/-/virtual-core-3.10.6.tgz#babe3989b2344a5f12fc64129f9bbed5d3402999" - integrity sha512-1giLc4dzgEKLMx5pgKjL6HlG5fjZMgCjzlKAlpr7yoUtetVPELgER1NtephAI910nMwfPTHNyWKSFmJdHkz2Cw== - "@tanstack/virtual-core@3.11.2": version "3.11.2" resolved "https://registry.yarnpkg.com/@tanstack/virtual-core/-/virtual-core-3.11.2.tgz#00409e743ac4eea9afe5b7708594d5fcebb00212" @@ -6439,26 +6256,6 @@ resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad" integrity sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA== -"@tsconfig/node10@^1.0.7": - version "1.0.11" - resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.11.tgz#6ee46400685f130e278128c7b38b7e031ff5b2f2" - integrity sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw== - -"@tsconfig/node12@^1.0.7": - version "1.0.11" - resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.11.tgz#ee3def1f27d9ed66dac6e46a295cffb0152e058d" - integrity sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag== - -"@tsconfig/node14@^1.0.0": - version "1.0.3" - resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.3.tgz#e4386316284f00b98435bf40f72f75a09dabf6c1" - integrity sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow== - -"@tsconfig/node16@^1.0.2": - version "1.0.4" - resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.4.tgz#0b92dcc0cc1c81f6f306a381f28e31b1a56536e9" - integrity sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA== - "@tufjs/canonical-json@1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@tufjs/canonical-json/-/canonical-json-1.0.0.tgz#eade9fd1f537993bc1f0949f3aea276ecc4fab31" @@ -6565,7 +6362,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@^3.0.0", "@types/d3-color@^3.1.3": +"@types/d3-color@*", "@types/d3-color@^3.0.0": version "3.1.3" resolved "https://registry.yarnpkg.com/@types/d3-color/-/d3-color-3.1.3.tgz#368c961a18de721da8200e80bf3943fb53136af2" integrity sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A== @@ -6585,7 +6382,7 @@ resolved "https://registry.yarnpkg.com/@types/d3-format/-/d3-format-1.4.5.tgz#6392303c2ca3c287c3a1a2046455cd0a0bd50bbe" integrity sha512-mLxrC1MSWupOSncXN/HOlWUAAIffAEBaI4+PKy2uMPsKe4FNZlk7qrbTjmzJXITQQqBHivaks4Td18azgqnotA== -"@types/d3-interpolate@^3.0.1", "@types/d3-interpolate@^3.0.4": +"@types/d3-interpolate@^3.0.1": version "3.0.4" resolved "https://registry.yarnpkg.com/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz#412b90e84870285f2ff8a846c6eb60344f12a41c" integrity sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA== @@ -6626,7 +6423,7 @@ resolved "https://registry.yarnpkg.com/@types/d3-time-format/-/d3-time-format-3.0.4.tgz#f972bdd7be1048184577cf235a44721a78c6bb4b" integrity sha512-or9DiDnYI1h38J9hxKEsw513+KVuFbEVhl7qdxcaudoiqWWepapUen+2vAriFGexr6W5+P4l9+HJrB39GG+oRg== -"@types/d3-time@*", "@types/d3-time@^3.0.0", "@types/d3-time@^3.0.3": +"@types/d3-time@*", "@types/d3-time@^3.0.0": version "3.0.4" resolved "https://registry.yarnpkg.com/@types/d3-time/-/d3-time-3.0.4.tgz#8472feecd639691450dd8000eb33edd444e1323f" integrity sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g== @@ -7220,17 +7017,6 @@ "@typescript-eslint/typescript-estree" "5.62.0" debug "^4.3.4" -"@typescript-eslint/parser@^5.4.2 || ^6.0.0": - version "6.21.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-6.21.0.tgz#af8fcf66feee2edc86bc5d1cf45e33b0630bf35b" - integrity sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ== - dependencies: - "@typescript-eslint/scope-manager" "6.21.0" - "@typescript-eslint/types" "6.21.0" - "@typescript-eslint/typescript-estree" "6.21.0" - "@typescript-eslint/visitor-keys" "6.21.0" - debug "^4.3.4" - "@typescript-eslint/parser@^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0": version "8.29.1" resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.29.1.tgz#10bf37411be0a199c27b6515726e22fe8d3df8d0" @@ -7250,14 +7036,6 @@ "@typescript-eslint/types" "5.62.0" "@typescript-eslint/visitor-keys" "5.62.0" -"@typescript-eslint/scope-manager@6.21.0": - version "6.21.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz#ea8a9bfc8f1504a6ac5d59a6df308d3a0630a2b1" - integrity sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg== - dependencies: - "@typescript-eslint/types" "6.21.0" - "@typescript-eslint/visitor-keys" "6.21.0" - "@typescript-eslint/scope-manager@8.29.1": version "8.29.1" resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.29.1.tgz#cfdfd4144f20c38b9d3e430efd6480e297ef52f6" @@ -7291,11 +7069,6 @@ resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.62.0.tgz#258607e60effa309f067608931c3df6fed41fd2f" integrity sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ== -"@typescript-eslint/types@6.21.0": - version "6.21.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-6.21.0.tgz#205724c5123a8fef7ecd195075fa6e85bac3436d" - integrity sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg== - "@typescript-eslint/types@8.29.1": version "8.29.1" resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.29.1.tgz#984ed1283fedbfb41d3993a9abdcb7b299971500" @@ -7314,20 +7087,6 @@ semver "^7.3.7" tsutils "^3.21.0" -"@typescript-eslint/typescript-estree@6.21.0": - version "6.21.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz#c47ae7901db3b8bddc3ecd73daff2d0895688c46" - integrity sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ== - dependencies: - "@typescript-eslint/types" "6.21.0" - "@typescript-eslint/visitor-keys" "6.21.0" - debug "^4.3.4" - globby "^11.1.0" - is-glob "^4.0.3" - minimatch "9.0.3" - semver "^7.5.4" - ts-api-utils "^1.0.1" - "@typescript-eslint/typescript-estree@8.29.1": version "8.29.1" resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.29.1.tgz#4ac085665ed5390d11c0e3426427978570e3b747" @@ -7374,14 +7133,6 @@ "@typescript-eslint/types" "5.62.0" eslint-visitor-keys "^3.3.0" -"@typescript-eslint/visitor-keys@6.21.0": - version "6.21.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz#87a99d077aa507e20e238b11d56cc26ade45fe47" - integrity sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A== - dependencies: - "@typescript-eslint/types" "6.21.0" - eslint-visitor-keys "^3.4.1" - "@typescript-eslint/visitor-keys@8.29.1": version "8.29.1" resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.29.1.tgz#9b74e5098c71138d42bbf2178fbe4dfad45d6b9a" @@ -7958,13 +7709,6 @@ acorn-walk@^7.1.1, acorn-walk@^7.2.0: resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.2.0.tgz#0de889a601203909b0fbe07b8938dc21d2e967bc" integrity sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA== -acorn-walk@^8.1.1: - version "8.3.4" - resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.3.4.tgz#794dd169c3977edf4ba4ea47583587c5866236b7" - integrity sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g== - dependencies: - acorn "^8.11.0" - acorn@^6.4.1: version "6.4.2" resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.2.tgz#35866fd710528e92de10cf06016498e47e39e1e6" @@ -7975,7 +7719,7 @@ acorn@^7.1.1, acorn@^7.4.0, acorn@^7.4.1: resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== -acorn@^8.11.0, acorn@^8.14.0, acorn@^8.2.4, acorn@^8.4.1, acorn@^8.8.2, acorn@^8.9.0: +acorn@^8.14.0, acorn@^8.2.4, acorn@^8.8.2, acorn@^8.9.0: version "8.14.1" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.14.1.tgz#721d5dc10f7d5b5609a891773d47731796935dfb" integrity sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg== @@ -8216,11 +7960,6 @@ are-we-there-yet@^3.0.0: delegates "^1.0.0" readable-stream "^3.6.0" -arg@^4.1.0: - version "4.1.3" - resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" - integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== - argparse@^1.0.7: version "1.0.10" resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" @@ -8532,7 +8271,7 @@ axe-core@^4.10.0: resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.10.3.tgz#04145965ac7894faddbac30861e5d8f11bfd14fc" integrity sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg== -axios@^0.21.1, axios@^0.21.2: +axios@^0.21.2: version "0.21.4" resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.4.tgz#c67b90dc0568e5c1cf2b0b858c43ba28e2eda575" integrity sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg== @@ -9498,7 +9237,7 @@ chokidar@^2.1.8: optionalDependencies: fsevents "^1.2.7" -chokidar@^3.4.1, chokidar@^3.4.2, chokidar@^3.5.2, chokidar@^3.5.3: +chokidar@^3.4.1, chokidar@^3.4.2, chokidar@^3.5.3: version "3.6.0" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== @@ -10154,7 +9893,7 @@ create-ecdh@^4.0.4: bn.js "^4.1.0" elliptic "^6.5.3" -create-hash@^1.1.0, create-hash@^1.1.2, create-hash@^1.2.0: +create-hash@^1.1.0, create-hash@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.2.0.tgz#889078af11a63756bcfb59bd221996be3a9ef196" integrity sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg== @@ -10165,7 +9904,17 @@ create-hash@^1.1.0, create-hash@^1.1.2, create-hash@^1.2.0: ripemd160 "^2.0.1" sha.js "^2.4.0" -create-hmac@^1.1.4, create-hmac@^1.1.7: +create-hash@~1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.1.3.tgz#606042ac8b9262750f483caddab0f5819172d8fd" + integrity sha512-snRpch/kwQhcdlnZKYanNF1m0RDlrCdSKQaH87w1FCFPVPNCQ/Il9QJKAX2jVBZddRdaHBMC+zXa9Gw9tmkNUA== + dependencies: + cipher-base "^1.0.1" + inherits "^2.0.1" + ripemd160 "^2.0.0" + sha.js "^2.4.0" + +create-hmac@^1.1.7: version "1.1.7" resolved "https://registry.yarnpkg.com/create-hmac/-/create-hmac-1.1.7.tgz#69170c78b3ab957147b2b8b04572e47ead2243ff" integrity sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg== @@ -10177,11 +9926,6 @@ create-hmac@^1.1.4, create-hmac@^1.1.7: safe-buffer "^5.0.1" sha.js "^2.4.8" -create-require@^1.1.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" - integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== - cross-fetch@^3.1.5: version "3.2.0" resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.2.0.tgz#34e9192f53bc757d6614304d9e5e6fb4edb782e3" @@ -10579,7 +10323,7 @@ d3-time-format@^3.0.0: dependencies: d3-array "2" -"d3-time@1 - 3", "d3-time@2.1.1 - 3", d3-time@^3.0.0, d3-time@^3.1.0: +"d3-time@1 - 3", "d3-time@2.1.1 - 3", d3-time@^3.0.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== @@ -10863,7 +10607,7 @@ del@^4.1.1: pify "^4.0.1" rimraf "^2.6.3" -delaunator@5, delaunator@^5.0.1: +delaunator@5: version "5.0.1" resolved "https://registry.yarnpkg.com/delaunator/-/delaunator-5.0.1.tgz#39032b08053923e924d6094fe2cde1a99cc51278" integrity sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw== @@ -10992,11 +10736,6 @@ diff@^3.1.0: resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" integrity sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA== -diff@^4.0.1: - version "4.0.2" - resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" - integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== - diff@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/diff/-/diff-5.2.0.tgz#26ded047cd1179b78b9537d5ef725503ce1ae531" @@ -11623,21 +11362,6 @@ eslint-config-airbnb@^19.0.2, eslint-config-airbnb@^19.0.4: object.assign "^4.1.2" object.entries "^1.1.5" -eslint-config-next@14.1.4: - version "14.1.4" - resolved "https://registry.yarnpkg.com/eslint-config-next/-/eslint-config-next-14.1.4.tgz#22f2ba4c0993e991249d863656a64c204bae542c" - integrity sha512-cihIahbhYAWwXJwZkAaRPpUi5t9aOi/HdfWXOjZeUOqNWXHD8X22kd1KG58Dc3MVaRx3HoR/oMGk2ltcrqDn8g== - dependencies: - "@next/eslint-plugin-next" "14.1.4" - "@rushstack/eslint-patch" "^1.3.3" - "@typescript-eslint/parser" "^5.4.2 || ^6.0.0" - eslint-import-resolver-node "^0.3.6" - eslint-import-resolver-typescript "^3.5.2" - eslint-plugin-import "^2.28.1" - eslint-plugin-jsx-a11y "^6.7.1" - eslint-plugin-react "^7.33.2" - eslint-plugin-react-hooks "^4.5.0 || 5.0.0-canary-7118f5dd7-20230705" - eslint-config-next@15.0.3: version "15.0.3" resolved "https://registry.yarnpkg.com/eslint-config-next/-/eslint-config-next-15.0.3.tgz#b483585260d5e55050d4ab87e053c88089ae12ee" @@ -11696,7 +11420,7 @@ eslint-module-utils@^2.12.0: dependencies: debug "^3.2.7" -eslint-plugin-import@^2.25.4, eslint-plugin-import@^2.28.1, eslint-plugin-import@^2.31.0: +eslint-plugin-import@^2.25.4, eslint-plugin-import@^2.31.0: version "2.31.0" resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz#310ce7e720ca1d9c0bb3f69adfd1c6bdd7d9e0e7" integrity sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A== @@ -11728,7 +11452,7 @@ eslint-plugin-jest@^26.1.1: dependencies: "@typescript-eslint/utils" "^5.10.0" -eslint-plugin-jsx-a11y@^6.10.0, eslint-plugin-jsx-a11y@^6.5.1, eslint-plugin-jsx-a11y@^6.7.1: +eslint-plugin-jsx-a11y@^6.10.0, eslint-plugin-jsx-a11y@^6.5.1: version "6.10.2" resolved "https://registry.yarnpkg.com/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz#d2812bb23bf1ab4665f1718ea442e8372e638483" integrity sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q== @@ -11770,17 +11494,12 @@ eslint-plugin-react-hooks@^4.3.0: resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz#c829eb06c0e6f484b3fbb85a97e57784f328c596" integrity sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ== -"eslint-plugin-react-hooks@^4.5.0 || 5.0.0-canary-7118f5dd7-20230705": - version "5.0.0-canary-7118f5dd7-20230705" - resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.0.0-canary-7118f5dd7-20230705.tgz#4d55c50e186f1a2b0636433d2b0b2f592ddbccfd" - integrity sha512-AZYbMo/NW9chdL7vk6HQzQhT+PvTAEVqWk9ziruUoW2kAOcN5qNyelv70e0F1VNQAbvutOC9oc+xfWycI9FxDw== - eslint-plugin-react-hooks@^5.0.0: version "5.2.0" resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz#1be0080901e6ac31ce7971beed3d3ec0a423d9e3" integrity sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg== -eslint-plugin-react@^7.29.2, eslint-plugin-react@^7.33.2, eslint-plugin-react@^7.35.0: +eslint-plugin-react@^7.29.2, eslint-plugin-react@^7.35.0: version "7.37.5" resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz#2975511472bdda1b272b34d779335c9b0e877065" integrity sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA== @@ -12163,7 +11882,7 @@ exponential-backoff@^3.1.1: resolved "https://registry.yarnpkg.com/exponential-backoff/-/exponential-backoff-3.1.2.tgz#a8f26adb96bf78e8cd8ad1037928d5e5c0679d91" integrity sha512-8QxYTVXUkuy7fIIoitQkPwGonB8F3Zj8eEO8Sqg9Zv/bkI7RJAzowee4gr81Hak/dUTpA2Z7VfQgoijjPNlUZA== -express@^4.17.1, express@^4.17.3, express@^4.18.2: +express@^4.17.1, express@^4.17.3: version "4.21.2" resolved "https://registry.yarnpkg.com/express/-/express-4.21.2.tgz#cf250e48362174ead6cea4a566abef0162c1ec32" integrity sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA== @@ -12998,17 +12717,6 @@ glob-to-regexp@^0.4.1: resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== -glob@10.3.10: - version "10.3.10" - resolved "https://registry.yarnpkg.com/glob/-/glob-10.3.10.tgz#0351ebb809fd187fe421ab96af83d3a70715df4b" - integrity sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g== - dependencies: - foreground-child "^3.1.0" - jackspeak "^2.3.5" - minimatch "^9.0.1" - minipass "^5.0.0 || ^6.0.2 || ^7.0.0" - path-scurry "^1.10.1" - glob@7.1.4: version "7.1.4" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.4.tgz#aa608a2f6c577ad357e1ae5a5c26d9a8d1969255" @@ -13267,6 +12975,13 @@ has-values@^1.0.0: is-number "^3.0.0" kind-of "^4.0.0" +hash-base@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-2.0.2.tgz#66ea1d856db4e8a5470cadf6fce23ae5244ef2e1" + integrity sha512-0TROgQ1/SxE6KmxWSvXHvRj90/Xo1JvZShofnYF+f6ZsGtR4eES7WfrQzPalmyagfKZCXpVnitiRebZulWsbiw== + dependencies: + inherits "^2.0.1" + hash-base@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.1.0.tgz#55c381d9e06e1d2997a883b4a3fddfe7f0d3af33" @@ -13405,11 +13120,6 @@ hex-rgb@^4.1.0: resolved "https://registry.yarnpkg.com/hex-rgb/-/hex-rgb-4.3.0.tgz#af5e974e83bb2fefe44d55182b004ec818c07776" integrity sha512-Ox1pJVrDCyGHMG9CFg1tmrRUMRPRsAWYc/PinY0XzJU4K7y7vjNoLKIQ7BR5UJMCxNN8EM1MNDmHWA/B3aZUuw== -highlight-words@1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/highlight-words/-/highlight-words-1.2.2.tgz#9875b75d11814d7356b24f23feeb7d77761fa867" - integrity sha512-Mf4xfPXYm8Ay1wTibCrHpNWeR2nUMynMVFkXCi4mbl+TEgmNOe+I4hV7W3OCZcSvzGL6kupaqpfHOemliMTGxQ== - highlight-words@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/highlight-words/-/highlight-words-2.0.0.tgz#06853d68f1f7c8e59d6ef2dd072fe2f64fc93936" @@ -13637,7 +13347,7 @@ http-proxy-agent@^7.0.2: agent-base "^7.1.0" debug "^4.3.4" -http-proxy-middleware@^2.0.3, http-proxy-middleware@^2.0.6: +http-proxy-middleware@^2.0.3: version "2.0.9" resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz#e9e63d68afaa4eee3d147f39149ab84c0c2815ef" integrity sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q== @@ -13750,11 +13460,6 @@ iferr@^0.1.5: resolved "https://registry.yarnpkg.com/iferr/-/iferr-0.1.5.tgz#c60eed69e6d8fdb6b3104a1fcbca1c192dc5b501" integrity sha512-DUNFN5j7Tln0D+TxzloUjKB+CtVu6myn0JEFak6dG18mNt9YkQ6lzGCdafwofISZ1lLF3xRHJ98VKy9ynkcFaA== -ignore-by-default@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09" - integrity sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA== - ignore-walk@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-5.0.1.tgz#5f199e23e1288f518d90358d461387788a154776" @@ -14638,15 +14343,6 @@ iterator.prototype@^1.1.4: has-symbols "^1.1.0" set-function-name "^2.0.2" -jackspeak@^2.3.5: - version "2.3.6" - resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-2.3.6.tgz#647ecc472238aee4b06ac0e461acc21a8c505ca8" - integrity sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ== - dependencies: - "@isaacs/cliui" "^8.0.2" - optionalDependencies: - "@pkgjs/parseargs" "^0.11.0" - jackspeak@^3.1.2: version "3.4.3" resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-3.4.3.tgz#8833a9d89ab4acde6188942bd1c53b6390ed5a8a" @@ -16184,16 +15880,6 @@ marked@^4.0.16: resolved "https://registry.yarnpkg.com/marked/-/marked-4.3.0.tgz#796362821b019f734054582038b116481b456cf3" integrity sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A== -material-react-table@^2.12.1: - version "2.13.3" - resolved "https://registry.yarnpkg.com/material-react-table/-/material-react-table-2.13.3.tgz#c61de4105efb3eb09697ed5fc2544d174675de31" - integrity sha512-xeyAEG6UYG3qgBIo17epAP5zsWT1pH0uCEkaUxvhki9sGcP35OqfOMSZJNhISvmqEqXKYHdqKbZI6iOwsg1sYA== - dependencies: - "@tanstack/match-sorter-utils" "8.19.4" - "@tanstack/react-table" "8.20.5" - "@tanstack/react-virtual" "3.10.6" - highlight-words "1.2.2" - material-react-table@^3.0.3: version "3.2.1" resolved "https://registry.yarnpkg.com/material-react-table/-/material-react-table-3.2.1.tgz#56f595755cab3b669b399999fed9eb305fbb6dd7" @@ -16792,13 +16478,6 @@ minimatch@3.0.5: dependencies: brace-expansion "^1.1.7" -minimatch@9.0.3: - version "9.0.3" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.3.tgz#a6e00c3de44c3a542bfaae70abfc22420a6da825" - integrity sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg== - dependencies: - brace-expansion "^2.0.1" - minimatch@^3.0.2, minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" @@ -16820,7 +16499,7 @@ minimatch@^8.0.2: dependencies: brace-expansion "^2.0.1" -minimatch@^9.0.0, minimatch@^9.0.1, minimatch@^9.0.4: +minimatch@^9.0.0, minimatch@^9.0.4: version "9.0.5" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5" integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow== @@ -17131,29 +16810,6 @@ nested-error-stacks@^2.0.0, nested-error-stacks@^2.1.0: resolved "https://registry.yarnpkg.com/nested-error-stacks/-/nested-error-stacks-2.1.1.tgz#26c8a3cee6cc05fbcf1e333cd2fc3e003326c0b5" integrity sha512-9iN1ka/9zmX1ZvLV9ewJYEk9h7RyRRtqdK0woXcqohu8EWIerfPUjYJPg0ULy0UqP7cslmdGc8xKDJcojlKiaw== -next@14.2.26: - version "14.2.26" - resolved "https://registry.yarnpkg.com/next/-/next-14.2.26.tgz#b918b3fc5c55e1a67aada1347907675713687721" - integrity sha512-b81XSLihMwCfwiUVRRja3LphLo4uBBMZEzBBWMaISbKTwOmq3wPknIETy/8000tr7Gq4WmbuFYPS7jOYIf+ZJw== - dependencies: - "@next/env" "14.2.26" - "@swc/helpers" "0.5.5" - busboy "1.6.0" - caniuse-lite "^1.0.30001579" - graceful-fs "^4.2.11" - postcss "8.4.31" - styled-jsx "5.1.1" - optionalDependencies: - "@next/swc-darwin-arm64" "14.2.26" - "@next/swc-darwin-x64" "14.2.26" - "@next/swc-linux-arm64-gnu" "14.2.26" - "@next/swc-linux-arm64-musl" "14.2.26" - "@next/swc-linux-x64-gnu" "14.2.26" - "@next/swc-linux-x64-musl" "14.2.26" - "@next/swc-win32-arm64-msvc" "14.2.26" - "@next/swc-win32-ia32-msvc" "14.2.26" - "@next/swc-win32-x64-msvc" "14.2.26" - next@^14.2.26: version "14.2.28" resolved "https://registry.yarnpkg.com/next/-/next-14.2.28.tgz#fdc2af93544d90a3915e544b73208c18668af6f9" @@ -17311,22 +16967,6 @@ node-releases@^2.0.19: resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.19.tgz#9e445a52950951ec4d177d843af370b411caf314" integrity sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw== -nodemon@^2.0.21: - version "2.0.22" - resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-2.0.22.tgz#182c45c3a78da486f673d6c1702e00728daf5258" - integrity sha512-B8YqaKMmyuCO7BowF1Z1/mkPqLk6cs/l63Ojtd6otKjMx47Dq1utxfRxcavH1I7VSaL8n5BUaoutadnsX3AAVQ== - dependencies: - chokidar "^3.5.2" - debug "^3.2.7" - ignore-by-default "^1.0.1" - minimatch "^3.1.2" - pstree.remy "^1.1.8" - semver "^5.7.1" - simple-update-notifier "^1.0.7" - supports-color "^5.5.0" - touch "^3.1.0" - undefsafe "^2.0.5" - nopt@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/nopt/-/nopt-6.0.0.tgz#245801d8ebf409c6df22ab9d95b65e1309cdb16d" @@ -18230,7 +17870,7 @@ path-parse@^1.0.7: resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== -path-scurry@^1.10.1, path-scurry@^1.11.1, path-scurry@^1.6.1: +path-scurry@^1.11.1, path-scurry@^1.6.1: version "1.11.1" resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.11.1.tgz#7960a668888594a0720b12a911d1a742ab9f11d2" integrity sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA== @@ -18265,15 +17905,16 @@ path-type@^4.0.0: integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== pbkdf2@^3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.1.2.tgz#dd822aa0887580e52f1a039dc3eda108efae3075" - integrity sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA== + version "3.1.3" + resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.1.3.tgz#8be674d591d65658113424592a95d1517318dd4b" + integrity sha512-wfRLBZ0feWRhCIkoMB6ete7czJcnNnqRpcoWQBLqatqXXmelSRqfdDK4F3u9T2s2cXas/hQJcryI/4lAL+XTlA== dependencies: - create-hash "^1.1.2" - create-hmac "^1.1.4" - ripemd160 "^2.0.1" - safe-buffer "^5.0.1" - sha.js "^2.4.8" + create-hash "~1.1.3" + create-hmac "^1.1.7" + ripemd160 "=2.0.1" + safe-buffer "^5.2.1" + sha.js "^2.4.11" + to-buffer "^1.2.0" picocolors@^0.2.1: version "0.2.1" @@ -18962,11 +18603,6 @@ psl@^1.1.33: dependencies: punycode "^2.3.1" -pstree.remy@^1.1.8: - version "1.1.8" - resolved "https://registry.yarnpkg.com/pstree.remy/-/pstree.remy-1.1.8.tgz#c242224f4a67c21f686839bbdb4ac282b8373d3a" - integrity sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w== - public-encrypt@^4.0.3: version "4.0.3" resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.3.tgz#4fcc9d77a07e48ba7527e7cbe0de33d0701331e0" @@ -19204,7 +18840,7 @@ react-docgen@^5.0.0: node-dir "^0.1.10" strip-indent "^3.0.0" -react-dom@^18, react-dom@^18.0.0, react-dom@^18.2.0: +react-dom@^18.0.0, react-dom@^18.2.0: version "18.3.1" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.3.1.tgz#c2265d79511b57d479b3dd3fdfa51536494c5cb4" integrity sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw== @@ -19228,13 +18864,6 @@ react-error-boundary@^3.1.3: dependencies: "@babel/runtime" "^7.12.5" -react-error-boundary@^4.0.13: - version "4.1.2" - resolved "https://registry.yarnpkg.com/react-error-boundary/-/react-error-boundary-4.1.2.tgz#bc750ad962edb8b135d6ae922c046051eb58f289" - integrity sha512-GQDxZ5Jd+Aq/qUxbCm1UtzmL/s++V7zKgE8yMktJiCQXCCFZnMZh9ng+6/Ne6PjNSXH0L9CjeOEREfRnq6Duag== - dependencies: - "@babel/runtime" "^7.12.5" - react-hook-form@^7.14.2: version "7.55.0" resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.55.0.tgz#df3c80a20a68f6811f49bec3406defaefb6dce80" @@ -19394,7 +19023,7 @@ react-world-flags@^1.6.0: svgo "^3.0.2" world-countries "^5.0.0" -react@^18, react@^18.0.0, react@^18.2.0: +react@^18.0.0, react@^18.2.0: version "18.3.1" resolved "https://registry.yarnpkg.com/react/-/react-18.3.1.tgz#49ab892009c53933625bd16b2533fc754cab2891" integrity sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ== @@ -19843,11 +19472,6 @@ requires-port@^1.0.0: resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ== -reselect@^4.1.8: - version "4.1.8" - resolved "https://registry.yarnpkg.com/reselect/-/reselect-4.1.8.tgz#3f5dc671ea168dccdeb3e141236f69f02eaec524" - integrity sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ== - resolve-cwd@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d" @@ -19952,6 +19576,14 @@ rimraf@^4.4.1: dependencies: glob "^9.2.0" +ripemd160@=2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.1.tgz#0f4584295c53a3628af7e6d79aca21ce57d1c6e7" + integrity sha512-J7f4wutN8mdbV08MJnXibYpCOPHR+yzy+iQ/AsjMv2j8cLavQ8VGagDFUwwTAdF8FmRKVeNpbTTEwNHCW1g94w== + dependencies: + hash-base "^2.0.0" + inherits "^2.0.1" + ripemd160@^2.0.0, ripemd160@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.2.tgz#a1c1a6f624751577ba5d07914cbc92850585890c" @@ -20200,7 +19832,7 @@ selfsigned@^2.1.1: "@types/node-forge" "^1.3.0" node-forge "^1" -"semver@2 || 3 || 4 || 5", semver@^5.4.1, semver@^5.5.0, semver@^5.6.0, semver@^5.7.1: +"semver@2 || 3 || 4 || 5", semver@^5.4.1, semver@^5.5.0, semver@^5.6.0: version "5.7.2" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8" integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g== @@ -20222,11 +19854,6 @@ semver@^6.0.0, semver@^6.1.2, semver@^6.3.0, semver@^6.3.1: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== -semver@~7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e" - integrity sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A== - send@0.19.0: version "0.19.0" resolved "https://registry.yarnpkg.com/send/-/send-0.19.0.tgz#bbc5a388c8ea6c048967049dbeac0e4a3f09d7f8" @@ -20363,12 +19990,13 @@ setprototypeof@1.2.0: integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== sha.js@^2.4.0, sha.js@^2.4.11, sha.js@^2.4.8: - version "2.4.11" - resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.11.tgz#37a5cf0b81ecbc6943de109ba2960d1b26584ae7" - integrity sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ== + version "2.4.12" + resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.12.tgz#eb8b568bf383dfd1867a32c3f2b74eb52bdbf23f" + integrity sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w== dependencies: - inherits "^2.0.1" - safe-buffer "^5.0.1" + inherits "^2.0.4" + safe-buffer "^5.2.1" + to-buffer "^1.2.0" shallow-clone@^3.0.0: version "3.0.1" @@ -20512,13 +20140,6 @@ simple-swizzle@^0.2.2: dependencies: is-arrayish "^0.3.1" -simple-update-notifier@^1.0.7: - version "1.1.0" - resolved "https://registry.yarnpkg.com/simple-update-notifier/-/simple-update-notifier-1.1.0.tgz#67694c121de354af592b347cdba798463ed49c82" - integrity sha512-VpsrsJSUcJEseSbMHkrsrAVSdvVS5I96Qo1QAQ4FxQ9wXFcB+pjj7FB7/us9+GcgfW4ziHtYMc1J0PLczb55mg== - dependencies: - semver "~7.0.0" - sisteransi@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed" @@ -21198,7 +20819,7 @@ stylis@4.2.0: resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.2.0.tgz#79daee0208964c8fe695a42fcffcac633a211a51" integrity sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw== -supports-color@^5.3.0, supports-color@^5.5.0: +supports-color@^5.3.0: version "5.5.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== @@ -21579,6 +21200,15 @@ to-arraybuffer@^1.0.0: resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43" integrity sha512-okFlQcoGTi4LQBG/PgSYblw9VOyptsz2KJZqc6qtgGdes8VktzUQkj4BI2blit072iS8VODNcMA+tvnS9dnuMA== +to-buffer@^1.2.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/to-buffer/-/to-buffer-1.2.1.tgz#2ce650cdb262e9112a18e65dc29dcb513c8155e0" + integrity sha512-tB82LpAIWjhLYbqjx3X4zEeHN6M8CiuOEy2JY8SEQVdYRe3CCHOFaqrBW1doLDrfpWhplcW7BL+bO3/6S3pcDQ== + dependencies: + isarray "^2.0.5" + safe-buffer "^5.2.1" + typed-array-buffer "^1.0.3" + to-object-path@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/to-object-path/-/to-object-path-0.3.0.tgz#297588b7b0e7e0ac08e04e672f85c1f4999e17af" @@ -21626,11 +21256,6 @@ toposort@^2.0.2: resolved "https://registry.yarnpkg.com/toposort/-/toposort-2.0.2.tgz#ae21768175d1559d48bef35420b2f4962f09c330" integrity sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg== -touch@^3.1.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/touch/-/touch-3.1.1.tgz#097a23d7b161476435e5c1344a95c0f75b4a5694" - integrity sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA== - tough-cookie@^4.0.0: version "4.1.4" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.1.4.tgz#945f1461b45b5a8c76821c33ea49c3ac192c1b36" @@ -21702,11 +21327,6 @@ trough@^2.0.0: resolved "https://registry.yarnpkg.com/trough/-/trough-2.2.0.tgz#94a60bd6bd375c152c1df911a4b11d5b0256f50f" integrity sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw== -ts-api-utils@^1.0.1: - version "1.4.3" - resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.4.3.tgz#bfc2215fe6528fecab2b0fba570a2e8a4263b064" - integrity sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw== - ts-api-utils@^2.0.1: version "2.1.0" resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-2.1.0.tgz#595f7094e46eed364c13fd23e75f9513d29baf91" @@ -21751,25 +21371,6 @@ ts-mocha@^10.0.0: optionalDependencies: tsconfig-paths "^3.5.0" -ts-node@10: - version "10.9.2" - resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.2.tgz#70f021c9e185bccdca820e26dc413805c101c71f" - integrity sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ== - dependencies: - "@cspotcode/source-map-support" "^0.8.0" - "@tsconfig/node10" "^1.0.7" - "@tsconfig/node12" "^1.0.7" - "@tsconfig/node14" "^1.0.0" - "@tsconfig/node16" "^1.0.2" - acorn "^8.4.1" - acorn-walk "^8.1.1" - arg "^4.1.0" - create-require "^1.1.0" - diff "^4.0.1" - make-error "^1.1.1" - v8-compile-cache-lib "^3.0.1" - yn "3.1.1" - ts-node@7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-7.0.1.tgz#9562dc2d1e6d248d24bc55f773e3f614337d9baf" @@ -22028,11 +21629,6 @@ uncrypto@^0.1.3: resolved "https://registry.yarnpkg.com/uncrypto/-/uncrypto-0.1.3.tgz#e1288d609226f2d02d8d69ee861fa20d8348ef2b" integrity sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q== -undefsafe@^2.0.5: - version "2.0.5" - resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-2.0.5.tgz#38733b9327bdcd226db889fb723a6efd162e6e2c" - integrity sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA== - undici-types@~6.19.2: version "6.19.8" resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.19.8.tgz#35111c9d1437ab83a7cdc0abae2f26d88eda0a02" @@ -22456,11 +22052,6 @@ uuid@^9.0.0, uuid@^9.0.1: resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30" integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA== -v8-compile-cache-lib@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" - integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== - v8-compile-cache@2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" @@ -23322,11 +22913,6 @@ yargs@^17.6.2: y18n "^5.0.5" yargs-parser "^21.1.1" -yn@3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" - integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== - yn@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/yn/-/yn-2.0.0.tgz#e5adabc8acf408f6385fc76495684c88e6af689a"