diff --git a/.github/workflows/audit.yml b/.github/workflows/audit.yml index f43a1feb77..00b13da72b 100644 --- a/.github/workflows/audit.yml +++ b/.github/workflows/audit.yml @@ -5,7 +5,7 @@ on: - cron: '5 9 * * *' jobs: cargo-deny: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - name: Checkout repository code uses: actions/checkout@v2 @@ -26,7 +26,7 @@ jobs: path: .github/workflows/support-files/notifications/deny.message notification: needs: cargo-deny - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - name: Check out repository code uses: actions/checkout@v2 diff --git a/.github/workflows/build_matrix_includes.json b/.github/workflows/build_matrix_includes.json index a75ca76530..8d8f8297bc 100644 --- a/.github/workflows/build_matrix_includes.json +++ b/.github/workflows/build_matrix_includes.json @@ -1,6 +1,6 @@ [ { - "os":"ubuntu-latest", + "os":"ubuntu-20.04", "rust":"stable", "runOnEvent":"always" }, diff --git a/.github/workflows/contracts-build.yml b/.github/workflows/contracts-build.yml index 3921bd3a6d..9ecc6b49d9 100644 --- a/.github/workflows/contracts-build.yml +++ b/.github/workflows/contracts-build.yml @@ -6,7 +6,7 @@ on: jobs: build: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v2 diff --git a/.github/workflows/contracts.yml b/.github/workflows/contracts.yml index 2c1eaf40b2..6d51356a80 100644 --- a/.github/workflows/contracts.yml +++ b/.github/workflows/contracts.yml @@ -10,7 +10,7 @@ on: jobs: matrix_prep: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 outputs: matrix: ${{ steps.set-matrix.outputs.matrix }} steps: @@ -24,7 +24,7 @@ jobs: contracts: # since it's going to be compiled into wasm, there's absolutely # no point in running CI on different OS-es - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 continue-on-error: ${{ matrix.rust == 'nightly' }} needs: matrix_prep strategy: diff --git a/.github/workflows/network-explorer-api.yml b/.github/workflows/network-explorer-api.yml index 2ad184be72..5c60038347 100644 --- a/.github/workflows/network-explorer-api.yml +++ b/.github/workflows/network-explorer-api.yml @@ -13,7 +13,7 @@ jobs: strategy: fail-fast: false matrix: - platform: [ubuntu-latest] + platform: [ubuntu-20.04] runs-on: ${{ matrix.platform }} steps: diff --git a/.github/workflows/nightly_build.yml b/.github/workflows/nightly_build.yml index 8475b83b8b..d103b66278 100644 --- a/.github/workflows/nightly_build.yml +++ b/.github/workflows/nightly_build.yml @@ -5,7 +5,7 @@ on: - cron: '14 1 * * *' jobs: matrix_prep: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 outputs: matrix: ${{ steps.set-matrix.outputs.matrix }} steps: @@ -25,7 +25,7 @@ jobs: steps: - name: Install Dependencies (Linux) run: sudo apt-get update && sudo apt-get install libwebkit2gtk-4.0-dev build-essential curl wget libssl-dev libgtk-3-dev libudev-dev squashfs-tools - if: matrix.os == 'ubuntu-latest' + if: matrix.os == 'ubuntu-20.04' - name: Check out repository code uses: actions/checkout@v2 @@ -96,7 +96,7 @@ jobs: - name: Reclaim some disk space uses: actions-rs/cargo@v1 - if: ${{ matrix.os == 'windows-latest' || matrix.os == 'ubuntu-latest' }} + if: ${{ matrix.os == 'windows-latest' || matrix.os == 'ubuntu-20.04' }} with: command: clean @@ -160,7 +160,7 @@ jobs: notification: needs: build - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - name: Collect jobs status uses: technote-space/workflow-conclusion-action@v2 diff --git a/.github/workflows/nightly_build_matrix_includes.json b/.github/workflows/nightly_build_matrix_includes.json index a979c90a62..172944df28 100644 --- a/.github/workflows/nightly_build_matrix_includes.json +++ b/.github/workflows/nightly_build_matrix_includes.json @@ -1,6 +1,6 @@ [ { - "os":"ubuntu-latest", + "os":"ubuntu-20.04", "rust":"stable", "runOnEvent":"schedule" }, @@ -17,7 +17,7 @@ }, { - "os":"ubuntu-latest", + "os":"ubuntu-20.04", "rust":"beta", "runOnEvent":"schedule" }, @@ -33,7 +33,7 @@ }, { - "os":"ubuntu-latest", + "os":"ubuntu-20.04", "rust":"nightly", "runOnEvent":"schedule" }, diff --git a/.github/workflows/nightly_build_release.yml b/.github/workflows/nightly_build_release.yml index 4474b399b6..30e43d7f90 100644 --- a/.github/workflows/nightly_build_release.yml +++ b/.github/workflows/nightly_build_release.yml @@ -5,7 +5,7 @@ on: - cron: '14 2 * * *' jobs: matrix_prep: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 outputs: matrix: ${{ steps.set-matrix.outputs.matrix }} steps: @@ -17,7 +17,7 @@ jobs: inputFile: '.github/workflows/nightly_build_matrix_includes.json' filter: '[?runOnEvent==`${{ github.event_name }}` || runOnEvent==`always`]' get_release: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 needs: matrix_prep outputs: output1: ${{ steps.step2.outputs.latest_release }} @@ -38,7 +38,7 @@ jobs: steps: - name: Install Dependencies (Linux) run: sudo apt-get update && sudo apt-get install libwebkit2gtk-4.0-dev build-essential curl wget libssl-dev libgtk-3-dev libudev-dev squashfs-tools - if: matrix.os == 'ubuntu-latest' + if: matrix.os == 'ubuntu-20.04' - name: Check out latest release branch uses: actions/checkout@v3 @@ -111,7 +111,7 @@ jobs: - name: Reclaim some disk space uses: actions-rs/cargo@v1 - if: ${{ matrix.os == 'windows-latest' || matrix.os == 'ubuntu-latest' }} + if: ${{ matrix.os == 'windows-latest' || matrix.os == 'ubuntu-20.04' }} with: command: clean @@ -175,7 +175,7 @@ jobs: notification: needs: [build,get_release] - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - name: Collect jobs status uses: technote-space/workflow-conclusion-action@v2 @@ -192,7 +192,7 @@ jobs: NYM_PROJECT_NAME: "Nym nightly build on latest release" GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" GIT_COMMIT_MESSAGE: "${{ github.event.head_commit.message }}" - GIT_BRANCH: "https://github.com/nymtech/nym/tree/${{needs.get_release.outputs.output1}}" + GIT_BRANCH_NAME: "${{needs.get_release.outputs.output1}}" KEYBASE_NYMBOT_USERNAME: "${{ secrets.KEYBASE_NYMBOT_USERNAME }}" KEYBASE_NYMBOT_PAPERKEY: "${{ secrets.KEYBASE_NYMBOT_PAPERKEY }}" KEYBASE_NYMBOT_TEAM: "${{ secrets.KEYBASE_NYMBOT_TEAM }}" diff --git a/.github/workflows/nightly_build_release2.yml b/.github/workflows/nightly_build_release2.yml index 71f89948c9..6ff5368c56 100644 --- a/.github/workflows/nightly_build_release2.yml +++ b/.github/workflows/nightly_build_release2.yml @@ -5,7 +5,7 @@ on: - cron: '24 2 * * *' jobs: matrix_prep: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 outputs: matrix: ${{ steps.set-matrix.outputs.matrix }} steps: @@ -17,7 +17,7 @@ jobs: inputFile: '.github/workflows/nightly_build_matrix_includes.json' filter: '[?runOnEvent==`${{ github.event_name }}` || runOnEvent==`always`]' get_release: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 needs: matrix_prep outputs: output1: ${{ steps.step2.outputs.latest_release }} @@ -38,7 +38,7 @@ jobs: steps: - name: Install Dependencies (Linux) run: sudo apt-get update && sudo apt-get install libwebkit2gtk-4.0-dev build-essential curl wget libssl-dev libgtk-3-dev libudev-dev squashfs-tools - if: matrix.os == 'ubuntu-latest' + if: matrix.os == 'ubuntu-20.04' - name: Check out latest release branch uses: actions/checkout@v3 @@ -111,7 +111,7 @@ jobs: - name: Reclaim some disk space uses: actions-rs/cargo@v1 - if: ${{ matrix.os == 'windows-latest' || matrix.os == 'ubuntu-latest' }} + if: ${{ matrix.os == 'windows-latest' || matrix.os == 'ubuntu-20.04' }} with: command: clean @@ -175,7 +175,7 @@ jobs: notification: needs: [build,get_release] - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - name: Collect jobs status uses: technote-space/workflow-conclusion-action@v2 @@ -192,7 +192,7 @@ jobs: NYM_PROJECT_NAME: "Nym nightly build on latest release" GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" GIT_COMMIT_MESSAGE: "${{ github.event.head_commit.message }}" - GIT_BRANCH: "https://github.com/nymtech/nym/tree/${{needs.get_release.outputs.output1}}" + GIT_BRANCH_NAME: "${{needs.get_release.outputs.output1}}" KEYBASE_NYMBOT_USERNAME: "${{ secrets.KEYBASE_NYMBOT_USERNAME }}" KEYBASE_NYMBOT_PAPERKEY: "${{ secrets.KEYBASE_NYMBOT_PAPERKEY }}" KEYBASE_NYMBOT_TEAM: "${{ secrets.KEYBASE_NYMBOT_TEAM }}" diff --git a/.github/workflows/nym-cli-publish.yml b/.github/workflows/nym-cli-publish.yml index 065e9f1658..5fd532c68b 100644 --- a/.github/workflows/nym-cli-publish.yml +++ b/.github/workflows/nym-cli-publish.yml @@ -13,7 +13,7 @@ jobs: strategy: fail-fast: false matrix: - platform: [ubuntu-latest, windows-latest, macos-latest] + platform: [ubuntu-20.04, windows-latest, macos-latest] runs-on: ${{ matrix.platform }} steps: diff --git a/.github/workflows/nym-connect-publish-ubuntu.yml b/.github/workflows/nym-connect-publish-ubuntu.yml index 52c6ed9c5e..a4eb77cae1 100644 --- a/.github/workflows/nym-connect-publish-ubuntu.yml +++ b/.github/workflows/nym-connect-publish-ubuntu.yml @@ -13,7 +13,7 @@ jobs: strategy: fail-fast: false matrix: - platform: [ubuntu-latest] + platform: [ubuntu-20.04] runs-on: ${{ matrix.platform }} steps: diff --git a/.github/workflows/nym-release-publish.yml b/.github/workflows/nym-release-publish.yml index 700f1d697f..979781652f 100644 --- a/.github/workflows/nym-release-publish.yml +++ b/.github/workflows/nym-release-publish.yml @@ -19,7 +19,7 @@ jobs: strategy: fail-fast: false matrix: - platform: [ubuntu-latest] + platform: [ubuntu-20.04] runs-on: ${{ matrix.platform }} steps: diff --git a/.github/workflows/nym-wallet-publish-ubuntu.yml b/.github/workflows/nym-wallet-publish-ubuntu.yml index 46ed4be076..04ff746851 100644 --- a/.github/workflows/nym-wallet-publish-ubuntu.yml +++ b/.github/workflows/nym-wallet-publish-ubuntu.yml @@ -12,7 +12,7 @@ jobs: strategy: fail-fast: false matrix: - platform: [ubuntu-latest] + platform: [ubuntu-20.04] runs-on: ${{ matrix.platform }} steps: diff --git a/.github/workflows/nym-wallet-release.yml b/.github/workflows/nym-wallet-release.yml index 4e157903e6..34883144f3 100644 --- a/.github/workflows/nym-wallet-release.yml +++ b/.github/workflows/nym-wallet-release.yml @@ -12,7 +12,7 @@ jobs: strategy: fail-fast: false matrix: - platform: [ubuntu-latest] + platform: [ubuntu-20.04] runs-on: ${{ matrix.platform }} steps: - uses: actions/checkout@v2 diff --git a/.github/workflows/nym_wallet.yml b/.github/workflows/nym_wallet.yml index bbc981d0f4..49b2381ed0 100644 --- a/.github/workflows/nym_wallet.yml +++ b/.github/workflows/nym_wallet.yml @@ -12,7 +12,7 @@ defaults: jobs: test: name: wallet tests - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v2 diff --git a/.github/workflows/support-files/nightly/index.js b/.github/workflows/support-files/nightly/index.js index f913539235..3bf2516092 100644 --- a/.github/workflows/support-files/nightly/index.js +++ b/.github/workflows/support-files/nightly/index.js @@ -85,7 +85,7 @@ async function getMessageBody(context) { ... ], check_run_url: 'https://api.github.com/repos/nymtech/nym/check-runs/5182940024', - labels: [ 'ubuntu-latest' ], + labels: [ 'ubuntu-20.04' ], runner_id: 1, runner_name: 'Hosted Agent', runner_group_id: 2, diff --git a/.github/workflows/wasm_client_build.yml b/.github/workflows/wasm_client_build.yml index 2761aabcd2..e1da3b04b0 100644 --- a/.github/workflows/wasm_client_build.yml +++ b/.github/workflows/wasm_client_build.yml @@ -7,7 +7,7 @@ on: jobs: wasm: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v2 diff --git a/.gitignore b/.gitignore index f102743734..b8df49d1a7 100644 --- a/.gitignore +++ b/.gitignore @@ -37,4 +37,5 @@ validator-config *.patch validator-api-config.toml dist -storybook-static \ No newline at end of file +storybook-static +envs/qwerty.env \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a37e4e27a..88337220ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,11 +2,49 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased (multi-surb branch breaking) + +### Added + +-clients: ability to use multi-reply SURBs to send arbitrarily long messages fully anonymously whilst requesting additional reply blocks whenever they're about to run out ([#1796], [#1801], [#1804], [#1835], [#1858], [#1883])) + +### Changed + +- clients: improve message logging when received message fails to get reconstructed ([#1803]) + +[#1796]: https://github.com/nymtech/nym/pull/1796 +[#1801]: https://github.com/nymtech/nym/pull/1801 +[#1803]: https://github.com/nymtech/nym/pull/1803 +[#1804]: https://github.com/nymtech/nym/pull/1804 +[#1835]: https://github.com/nymtech/nym/pull/1835 +[#1858]: https://github.com/nymtech/nym/pull/1858 +[#1883]: https://github.com/nymtech/nym/pull/1883 + ## [Unreleased] ### Changed +- validator-api: can recover from shutdown during DKG process ([#1872]) +- clients: deduplicate gateway inititialization, part of work towards a rust-sdk +- clients: keep all transmission lanes going at all times by making priority probabilistic + +### Fixed + +- network-requester: fix bug where websocket connection disconnect resulted in success error code +- clients: fix a few panics handling the gateway-client +- mixnode, gateway, validator-api: Use mainnet values as defaults for URLs and mixnet contract ([#1884]) + +[#1872]: https://github.com/nymtech/nym/pull/1872 +[#1884]: https://github.com/nymtech/nym/pull/1884 + + +## [v1.1.2] + +### Changed + - gateway: Renamed flag from `enabled/disabled_credentials_mode` to `only-coconut-credentials` +- "Family" feature for node families + layers +- Initial coconut functionality including credentials and distributed key generation ## [v1.1.1](https://github.com/nymtech/nym/tree/v1.1.1) (2022-11-29) diff --git a/Cargo.lock b/Cargo.lock index e81281023f..822fa26726 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -131,9 +131,9 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.53" +version = "0.1.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed6aa3524a2dfcf9fe180c51eae2b58738348d819517ceadf95789c51fff7600" +checksum = "1e805d94e6b5001b651426cf4cd446b1ab5f319d27bab5c644f61de0a804360c" dependencies = [ "proc-macro2", "quote", @@ -586,11 +586,13 @@ dependencies = [ [[package]] name = "client-core" -version = "1.1.1" +version = "1.1.2" dependencies = [ + "async-trait", "client-connections", "config", "crypto", + "dashmap 5.4.0", "dirs", "futures", "gateway-client", @@ -603,18 +605,22 @@ dependencies = [ "pemstore", "rand 0.7.3", "serde", - "sled", + "serde_json", + "sqlx 0.6.2", "tap", "task", "tempfile", "thiserror", + "time 0.3.17", "tokio", + "tokio-stream", "topology", "url", "validator-client", "wasm-bindgen", "wasm-bindgen-futures", "wasm-timer", + "wasm-utils", ] [[package]] @@ -780,7 +786,7 @@ dependencies = [ "rand 0.8.5", "sha2 0.10.2", "subtle 2.4.1", - "time 0.3.14", + "time 0.3.17", "version_check", ] @@ -1348,6 +1354,19 @@ dependencies = [ "num_cpus", ] +[[package]] +name = "dashmap" +version = "5.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "907076dfda823b0b36d2a1bb5f90c96660a5bbcd7729e10727f07858f22c4edc" +dependencies = [ + "cfg-if 1.0.0", + "hashbrown 0.12.3", + "lock_api", + "once_cell", + "parking_lot_core 0.9.4", +] + [[package]] name = "der" version = "0.5.1" @@ -1796,16 +1815,6 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8cbd1169bd7b4a0a20d92b9af7a7e0422888bd38a6f5ec29c1fd8c1558a272e" -[[package]] -name = "fs2" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213" -dependencies = [ - "libc", - "winapi", -] - [[package]] name = "fuchsia-cprng" version = "0.1.1" @@ -1918,15 +1927,6 @@ dependencies = [ "slab", ] -[[package]] -name = "fxhash" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" -dependencies = [ - "byteorder", -] - [[package]] name = "gateway-client" version = "0.1.0" @@ -2272,13 +2272,14 @@ checksum = "7ebdb29d2ea9ed0083cd8cece49bbd968021bd99b0849edb4a9a7ee0fdf6a4e0" [[package]] name = "hidapi" -version = "1.4.2" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d26e1151deaab68f34fbfd16d491a2a0170cf98d69d3efa23873b567a4199e1" +checksum = "798154e4b6570af74899d71155fb0072d5b17e6aa12f39c8ef22c60fb8ec99e7" dependencies = [ "cc", "libc", "pkg-config", + "winapi", ] [[package]] @@ -2609,9 +2610,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.55" +version = "0.3.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cc9ffccd38c451a86bf13657df244e9c3f37493cce8e5e21e940963777acc84" +checksum = "49409df3e3bf0856b916e2ceaca09ee28e6871cf7d9ce97a692cacfdb2a25a47" dependencies = [ "wasm-bindgen", ] @@ -2667,7 +2668,7 @@ checksum = "fe435806c197dfeaa5efcded5e623c4b8230fd28fdf1e91e7a86e40ef2acbf90" dependencies = [ "arrayref", "no-std-compat", - "snafu 0.7.1", + "snafu", ] [[package]] @@ -2757,9 +2758,9 @@ dependencies = [ [[package]] name = "lock_api" -version = "0.4.7" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "327fa5b6a6940e4699ec49a9beae1ea4845c6bab9314e4f84ac68742139d8c53" +checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" dependencies = [ "autocfg 1.1.0", "scopeguard", @@ -2911,7 +2912,7 @@ dependencies = [ "serde_json", "serde_repr", "thiserror", - "time 0.3.14", + "time 0.3.17", "ts-rs", ] @@ -3088,15 +3089,6 @@ dependencies = [ "libc", ] -[[package]] -name = "num_threads" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aba1801fb138d8e85e11d0fc70baf4fe1cdfffda7c6cd34a854905df588e5ed0" -dependencies = [ - "libc", -] - [[package]] name = "nym-bity-integration" version = "0.1.0" @@ -3114,7 +3106,7 @@ dependencies = [ [[package]] name = "nym-cli" -version = "1.1.0" +version = "1.1.2" dependencies = [ "anyhow", "base64", @@ -3159,7 +3151,7 @@ dependencies = [ "serde_json", "tap", "thiserror", - "time 0.3.14", + "time 0.3.17", "toml", "url", "validator-client", @@ -3168,7 +3160,7 @@ dependencies = [ [[package]] name = "nym-client" -version = "1.1.1" +version = "1.1.2" dependencies = [ "clap 3.2.8", "client-connections", @@ -3192,7 +3184,6 @@ dependencies = [ "rand 0.7.3", "serde", "serde_json", - "sled", "tap", "task", "thiserror", @@ -3208,7 +3199,7 @@ dependencies = [ [[package]] name = "nym-gateway" -version = "1.1.1" +version = "1.1.2" dependencies = [ "anyhow", "async-trait", @@ -3221,7 +3212,7 @@ dependencies = [ "config", "credentials", "crypto", - "dashmap", + "dashmap 4.0.2", "dirs", "dotenv", "futures", @@ -3255,7 +3246,7 @@ dependencies = [ [[package]] name = "nym-mixnode" -version = "1.1.1" +version = "1.1.2" dependencies = [ "anyhow", "bs58", @@ -3297,7 +3288,7 @@ dependencies = [ [[package]] name = "nym-network-requester" -version = "1.1.1" +version = "1.1.2" dependencies = [ "async-trait", "clap 3.2.8", @@ -3329,7 +3320,7 @@ dependencies = [ [[package]] name = "nym-network-statistics" -version = "1.0.2" +version = "1.1.2" dependencies = [ "dirs", "log", @@ -3345,7 +3336,7 @@ dependencies = [ [[package]] name = "nym-socks5-client" -version = "1.1.1" +version = "1.1.2" dependencies = [ "clap 3.2.8", "client-connections", @@ -3371,7 +3362,7 @@ dependencies = [ "proxy-helpers", "rand 0.7.3", "serde", - "snafu 0.6.10", + "serde_json", "socks5-requests", "tap", "task", @@ -3412,7 +3403,7 @@ dependencies = [ [[package]] name = "nym-validator-api" -version = "1.1.1" +version = "1.1.2" dependencies = [ "anyhow", "async-trait", @@ -3462,7 +3453,7 @@ dependencies = [ "tap", "task", "thiserror", - "time 0.3.14", + "time 0.3.17", "tokio", "tokio-stream", "topology", @@ -3535,6 +3526,7 @@ dependencies = [ "nymsphinx-types", "rand 0.7.3", "rand_distr", + "thiserror", "tokio", "topology", ] @@ -3560,6 +3552,7 @@ dependencies = [ "nymsphinx-types", "rand 0.7.3", "serde", + "thiserror", ] [[package]] @@ -3573,7 +3566,9 @@ dependencies = [ "nymsphinx-types", "rand 0.7.3", "serde", + "thiserror", "topology", + "wasm-bindgen", ] [[package]] @@ -3585,6 +3580,7 @@ dependencies = [ "nymsphinx-params", "nymsphinx-types", "rand 0.7.3", + "thiserror", ] [[package]] @@ -3599,6 +3595,7 @@ dependencies = [ "nymsphinx-params", "nymsphinx-types", "rand 0.7.3", + "thiserror", "topology", ] @@ -3650,9 +3647,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.13.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18a6dbe30758c9f83eb00cbea4ac95966305f5a7772f3f42ebfc7fc7eddbd8e1" +checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860" [[package]] name = "oorandom" @@ -3754,7 +3751,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87f5ec2493a61ac0506c0f4199f99070cbe83857b0337006a30f3e6719b8ef58" dependencies = [ "lock_api", - "parking_lot_core 0.9.2", + "parking_lot_core 0.9.4", ] [[package]] @@ -3773,15 +3770,15 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.2" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "995f667a6c822200b0433ac218e05582f0e2efa1b922a3fd2fbaadc5f87bab37" +checksum = "4dc9e0dc2adc1c69d09143aff38d3d30c5c3f0df0dad82e6d25547af174ebec0" dependencies = [ "cfg-if 1.0.0", "libc", "redox_syscall", "smallvec 1.8.0", - "windows-sys 0.34.0", + "windows-sys 0.42.0", ] [[package]] @@ -4057,11 +4054,11 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.37" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec757218438d5fda206afc041538b2f6d889286160d649a86a24d37e1235afd1" +checksum = "5ea3d908b0e36316caf9e9e2c4625cdde190a7e6f440d794667ed17a1855e725" dependencies = [ - "unicode-xid", + "unicode-ident", ] [[package]] @@ -4620,7 +4617,7 @@ dependencies = [ "serde_json", "state", "tempfile", - "time 0.3.14", + "time 0.3.17", "tokio", "tokio-stream", "tokio-util 0.7.3", @@ -4682,7 +4679,7 @@ dependencies = [ "smallvec 1.8.0", "stable-pattern", "state", - "time 0.3.14", + "time 0.3.17", "tokio", "uncased", ] @@ -5004,9 +5001,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.83" +version = "1.0.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38dd04e3c8279e75b31ef29dbdceebfe5ad89f4d0937213c53f7d49d01b3d5a7" +checksum = "020ff22c755c2ed3f8cf162dbb41a7268d934702f3ed3631656ea597e08fc3db" dependencies = [ "itoa 1.0.1", "ryu", @@ -5163,22 +5160,6 @@ version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eb703cfe953bccee95685111adeedb76fabe4e97549a58d16f03ea7b9367bb32" -[[package]] -name = "sled" -version = "0.34.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f96b4737c2ce5987354855aed3797279def4ebf734436c6aa4552cf8e169935" -dependencies = [ - "crc32fast", - "crossbeam-epoch", - "crossbeam-utils", - "fs2", - "fxhash", - "libc", - "log", - "parking_lot 0.11.2", -] - [[package]] name = "smallvec" version = "0.6.14" @@ -5196,40 +5177,19 @@ checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83" [[package]] name = "snafu" -version = "0.6.10" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eab12d3c261b2308b0d80c26fffb58d17eba81a4be97890101f416b478c79ca7" +checksum = "a152ba99b054b22972ee794cf04e5ef572da1229e33b65f3c57abbff0525a454" dependencies = [ "doc-comment", - "snafu-derive 0.6.10", -] - -[[package]] -name = "snafu" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5177903bf45656592d9eb5c0e22f408fc023aae51dbe2088889b71633ba451f2" -dependencies = [ - "doc-comment", - "snafu-derive 0.7.1", + "snafu-derive", ] [[package]] name = "snafu-derive" -version = "0.6.10" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1508efa03c362e23817f96cde18abed596a25219a8b2c66e8db33c03543d315b" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "snafu-derive" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "410b26ed97440d90ced3e2488c868d56a86e2064f5d7d6f417909b286afe25e5" +checksum = "d5e79cdebbabaebb06a9bdbaedc7f159b410461f63611d4d0e3fb0fab8fed850" dependencies = [ "heck 0.4.0", "proc-macro2", @@ -5258,7 +5218,7 @@ dependencies = [ [[package]] name = "sphinx" version = "0.1.0" -source = "git+https://github.com/nymtech/sphinx?rev=c494250f2a78bed33a618d470792418eee932859#c494250f2a78bed33a618d470792418eee932859" +source = "git+https://github.com/nymtech/sphinx?rev=e05a1992522ed0afd3c6fcac160313ffc9bb306a#e05a1992522ed0afd3c6fcac160313ffc9bb306a" dependencies = [ "aes 0.7.5", "arrayref", @@ -5616,13 +5576,13 @@ dependencies = [ [[package]] name = "syn" -version = "1.0.91" +version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b683b2b825c8eef438b77c36a06dc262294da3d5a5813fac20da149241dcd44d" +checksum = "4ae548ec36cf198c0ef7710d3c230987c2d6d7bd98ad6edc0274462724c585ce" dependencies = [ "proc-macro2", "quote", - "unicode-xid", + "unicode-ident", ] [[package]] @@ -5666,6 +5626,8 @@ dependencies = [ "log", "thiserror", "tokio", + "wasm-bindgen", + "wasm-bindgen-futures", ] [[package]] @@ -5709,7 +5671,7 @@ dependencies = [ "subtle 2.4.1", "subtle-encoding", "tendermint-proto", - "time 0.3.14", + "time 0.3.17", "zeroize", ] @@ -5742,7 +5704,7 @@ dependencies = [ "serde", "serde_bytes", "subtle-encoding", - "time 0.3.14", + "time 0.3.17", ] [[package]] @@ -5770,7 +5732,7 @@ dependencies = [ "tendermint-config", "tendermint-proto", "thiserror", - "time 0.3.14", + "time 0.3.17", "tokio", "tracing", "url", @@ -5844,22 +5806,31 @@ dependencies = [ [[package]] name = "time" -version = "0.3.14" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c3f9a28b618c3a6b9251b6908e9c99e04b9e5c02e6581ccbb67d59c34ef7f9b" +checksum = "a561bf4617eebd33bca6434b988f39ed798e527f51a1e797d0ee4f61c0a38376" dependencies = [ "itoa 1.0.1", - "libc", - "num_threads", + "js-sys", "serde", + "time-core", "time-macros", ] [[package]] -name = "time-macros" -version = "0.2.4" +name = "time-core" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42657b1a6f4d817cda8e7a0ace261fe0cc946cf3a80314390b22cc61ae080792" +checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd" + +[[package]] +name = "time-macros" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d967f99f534ca7e495c575c62638eebc2898a8c84c119b89e250477bc4ba16b2" +dependencies = [ + "time-core", +] [[package]] name = "tinytemplate" @@ -5873,9 +5844,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.21.2" +version = "1.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9e03c497dc955702ba729190dc4aac6f2a0ce97f913e5b1b5912fc5039d9099" +checksum = "d76ce4a75fb488c605c54bf610f221cea8b0dafb53333c1a67e8ee199dcd2ae3" dependencies = [ "autocfg 1.1.0", "bytes", @@ -6087,6 +6058,7 @@ dependencies = [ "nymsphinx-addressing", "nymsphinx-types", "rand 0.7.3", + "thiserror", "version-checker", ] @@ -6346,6 +6318,12 @@ version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a01404663e3db436ed2746d9fefef640d868edae3cceb81c3b8d5732fda678f" +[[package]] +name = "unicode-ident" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3" + [[package]] name = "unicode-normalization" version = "0.1.9" @@ -6516,7 +6494,7 @@ dependencies = [ "rustc_version 0.4.0", "rustversion", "thiserror", - "time 0.3.14", + "time 0.3.17", ] [[package]] @@ -6534,7 +6512,7 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "vesting-contract" -version = "1.1.0" +version = "1.1.2" dependencies = [ "contracts-common", "cosmwasm-std", @@ -6789,19 +6767,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" -[[package]] -name = "windows-sys" -version = "0.34.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5acdd78cb4ba54c0045ac14f62d8f94a03d10047904ae2a40afa1e99d8f70825" -dependencies = [ - "windows_aarch64_msvc 0.34.0", - "windows_i686_gnu 0.34.0", - "windows_i686_msvc 0.34.0", - "windows_x86_64_gnu 0.34.0", - "windows_x86_64_msvc 0.34.0", -] - [[package]] name = "windows-sys" version = "0.36.1" @@ -6816,10 +6781,25 @@ dependencies = [ ] [[package]] -name = "windows_aarch64_msvc" -version = "0.34.0" +name = "windows-sys" +version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17cffbe740121affb56fad0fc0e421804adf0ae00891205213b5cecd30db881d" +checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc 0.42.0", + "windows_i686_gnu 0.42.0", + "windows_i686_msvc 0.42.0", + "windows_x86_64_gnu 0.42.0", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc 0.42.0", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e" [[package]] name = "windows_aarch64_msvc" @@ -6828,10 +6808,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" [[package]] -name = "windows_i686_gnu" -version = "0.34.0" +name = "windows_aarch64_msvc" +version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2564fde759adb79129d9b4f54be42b32c89970c18ebf93124ca8870a498688ed" +checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4" [[package]] name = "windows_i686_gnu" @@ -6840,10 +6820,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" [[package]] -name = "windows_i686_msvc" -version = "0.34.0" +name = "windows_i686_gnu" +version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cd9d32ba70453522332c14d38814bceeb747d80b3958676007acadd7e166956" +checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7" [[package]] name = "windows_i686_msvc" @@ -6852,10 +6832,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" [[package]] -name = "windows_x86_64_gnu" -version = "0.34.0" +name = "windows_i686_msvc" +version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfce6deae227ee8d356d19effc141a509cc503dfd1f850622ec4b0f84428e1f4" +checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246" [[package]] name = "windows_x86_64_gnu" @@ -6864,10 +6844,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" [[package]] -name = "windows_x86_64_msvc" -version = "0.34.0" +name = "windows_x86_64_gnu" +version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d19538ccc21819d01deaf88d6a17eae6596a12e9aafdbb97916fb49896d89de9" +checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028" [[package]] name = "windows_x86_64_msvc" @@ -6875,6 +6861,12 @@ version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5" + [[package]] name = "winreg" version = "0.10.1" diff --git a/clients/client-core/Cargo.toml b/clients/client-core/Cargo.toml index 62f97d53fe..87595d125f 100644 --- a/clients/client-core/Cargo.toml +++ b/clients/client-core/Cargo.toml @@ -1,22 +1,26 @@ [package] name = "client-core" -version = "1.1.1" +version = "1.1.2" authors = ["Dave Hrycyszyn "] edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +async-trait = { version = "0.1.58" } dirs = "4.0" +dashmap = "5.4.0" futures = "0.3" humantime-serde = "1.0" log = "0.4" rand = { version = "0.7.3", features = ["wasm-bindgen"] } serde = { version = "1.0", features = ["derive"] } -sled = { version = "0.34", optional = true } +serde_json = "1.0.89" tap = "1.0.1" thiserror = "1.0.34" url = { version ="2.2", features = ["serde"] } +tokio = { version = "1.21.2", features = ["macros"]} +time = "0.3.17" # internal config = { path = "../../common/config" } @@ -30,8 +34,20 @@ nymsphinx = { path = "../../common/nymsphinx" } pemstore = { path = "../../common/pemstore" } topology = { path = "../../common/topology" } validator-client = { path = "../../common/client-libs/validator-client", default-features = false } +task = { path = "../../common/task" } -tokio = { version = "1.21.2", features = ["time", "macros"]} +[target."cfg(not(target_arch = \"wasm32\"))".dependencies.tokio-stream] +version = "0.1.9" +features = ["time"] + +[target."cfg(not(target_arch = \"wasm32\"))".dependencies.tokio] +version = "1.21.2" +features = ["time"] + +[target."cfg(not(target_arch = \"wasm32\"))".dependencies.sqlx] +version = "0.6.2" +features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"] +optional = true [target."cfg(target_arch = \"wasm32\")".dependencies.wasm-bindgen-futures] version = "0.4" @@ -47,15 +63,23 @@ rev = "b9d1a54ad514c2f230a026afe0dde341e98cd7b6" version = "0.2.4" features = ["futures"] -[target."cfg(not(target_arch = \"wasm32\"))".dependencies.task] -path = "../../common/task" +[target."cfg(target_arch = \"wasm32\")".dependencies.wasm-utils] +path = "../../common/wasm-utils" + +[target."cfg(target_arch = \"wasm32\")".dependencies.time] +version = "0.3.17" +features = ["wasm-bindgen"] [dev-dependencies] tempfile = "3.1.0" +[build-dependencies] +tokio = { version = "1.21.2", features = ["rt-multi-thread", "macros"] } +sqlx = { version = "0.6.2", features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"] } + [features] -default = ["reply-surb"] +default = [] +fs-surb-storage = ["sqlx"] wasm = ["gateway-client/wasm"] coconut = ["gateway-client/coconut", "gateway-requests/coconut"] -reply-surb = ["sled"] diff --git a/clients/client-core/build.rs b/clients/client-core/build.rs new file mode 100644 index 0000000000..bac1b20c53 --- /dev/null +++ b/clients/client-core/build.rs @@ -0,0 +1,31 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +#[tokio::main] +async fn main() { + #[cfg(feature = "fs-surb-storage")] + { + use sqlx::{Connection, SqliteConnection}; + use std::env; + + let out_dir = env::var("OUT_DIR").unwrap(); + let database_path = format!("{}/fs-surbs-example.sqlite", out_dir); + + let mut conn = SqliteConnection::connect(&format!("sqlite://{}?mode=rwc", database_path)) + .await + .expect("Failed to create SQLx database connection"); + + sqlx::migrate!("./fs_surbs_migrations") + .run(&mut conn) + .await + .expect("Failed to perform SQLx migrations"); + + #[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); + } +} diff --git a/clients/client-core/fs_surbs_migrations/20221130120000_create_initial_tables.sql b/clients/client-core/fs_surbs_migrations/20221130120000_create_initial_tables.sql new file mode 100644 index 0000000000..0143475a37 --- /dev/null +++ b/clients/client-core/fs_surbs_migrations/20221130120000_create_initial_tables.sql @@ -0,0 +1,40 @@ +CREATE TABLE status +( + flush_in_progress INTEGER NOT NULL, + previous_flush_timestamp INTEGER NOT NULL, + client_in_use INTEGER NOT NULL +); + +CREATE TABLE reply_surb_storage_metadata +( + min_reply_surb_threshold INTEGER NOT NULL, + max_reply_surb_threshold INTEGER NOT NULL +); + +CREATE TABLE sender_tag +( + recipient BLOB NOT NULL UNIQUE, + tag BLOB NOT NULL UNIQUE +); + +CREATE TABLE reply_key +( + key_digest BLOB NOT NULL UNIQUE, + reply_key BLOB NOT NULL UNIQUE, + sent_at_timestamp INTEGER NOT NULL +); + +CREATE TABLE reply_surb_sender +( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + last_sent_timestamp INTEGER NOT NULL, + tag BLOB NOT NULL UNIQUE +); + +CREATE TABLE reply_surb +( + reply_surb_sender_id INTEGER NOT NULL, + reply_surb BLOB NOT NULL, + + FOREIGN KEY (reply_surb_sender_id) REFERENCES reply_surb_sender (id) +); \ No newline at end of file diff --git a/clients/client-core/src/client/base_client/mod.rs b/clients/client-core/src/client/base_client/mod.rs new file mode 100644 index 0000000000..932fb77b31 --- /dev/null +++ b/clients/client-core/src/client/base_client/mod.rs @@ -0,0 +1,482 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::client::cover_traffic_stream::LoopCoverTrafficStream; +use crate::client::inbound_messages::{InputMessage, InputMessageReceiver, InputMessageSender}; +use crate::client::key_manager::KeyManager; +use crate::client::mix_traffic::{BatchMixMessageSender, MixTrafficController}; +use crate::client::real_messages_control; +use crate::client::real_messages_control::RealMessagesController; +use crate::client::received_buffer::{ + ReceivedBufferRequestReceiver, ReceivedBufferRequestSender, ReceivedMessagesBufferController, +}; +use crate::client::replies::reply_controller; +use crate::client::replies::reply_controller::{ReplyControllerReceiver, ReplyControllerSender}; +use crate::client::replies::reply_storage::{ + CombinedReplyStorage, PersistentReplyStorage, ReplyStorageBackend, SentReplyKeys, +}; +use crate::client::topology_control::{ + TopologyAccessor, TopologyRefresher, TopologyRefresherConfig, +}; +use crate::config::{Config, DebugConfig, GatewayEndpointConfig}; +use crate::error::ClientCoreError; +use crate::spawn_future; +use client_connections::{ConnectionCommandReceiver, ConnectionCommandSender, LaneQueueLengths}; +use crypto::asymmetric::{encryption, identity}; +use futures::channel::mpsc; +use gateway_client::bandwidth::BandwidthController; +use gateway_client::{ + AcknowledgementReceiver, AcknowledgementSender, GatewayClient, MixnetMessageReceiver, + MixnetMessageSender, +}; +use log::{debug, info}; +use nymsphinx::acknowledgements::AckKey; +use nymsphinx::addressing::clients::Recipient; +use nymsphinx::addressing::nodes::NodeIdentity; +use std::sync::Arc; +use std::time::Duration; +use tap::TapFallible; +use task::{ShutdownListener, ShutdownNotifier}; +use url::Url; + +#[cfg(all(not(target_arch = "wasm32"), feature = "fs-surb-storage"))] +pub mod non_wasm_helpers; + +pub struct ClientInput { + pub shared_lane_queue_lengths: LaneQueueLengths, + pub connection_command_sender: ConnectionCommandSender, + pub input_sender: InputMessageSender, +} + +pub struct ClientOutput { + pub received_buffer_request_sender: ReceivedBufferRequestSender, +} + +pub enum ClientInputStatus { + AwaitingProducer { client_input: ClientInput }, + Connected, +} + +impl ClientInputStatus { + pub fn register_producer(&mut self) -> ClientInput { + match std::mem::replace(self, ClientInputStatus::Connected) { + ClientInputStatus::AwaitingProducer { client_input } => client_input, + ClientInputStatus::Connected => panic!("producer was already registered before"), + } + } +} + +pub enum ClientOutputStatus { + AwaitingConsumer { client_output: ClientOutput }, + Connected, +} + +impl ClientOutputStatus { + pub fn register_consumer(&mut self) -> ClientOutput { + match std::mem::replace(self, ClientOutputStatus::Connected) { + ClientOutputStatus::AwaitingConsumer { client_output } => client_output, + ClientOutputStatus::Connected => panic!("consumer was already registered before"), + } + } +} + +pub struct BaseClientBuilder<'a, B> { + // due to wasm limitations I had to split it like this : ( + gateway_config: &'a GatewayEndpointConfig, + debug_config: &'a DebugConfig, + disabled_credentials: bool, + validator_api_endpoints: Vec, + reply_storage_backend: B, + + bandwidth_controller: Option, + key_manager: KeyManager, +} + +impl<'a, B> BaseClientBuilder<'a, B> +where + B: ReplyStorageBackend + Send + Sync + 'static, +{ + pub fn new_from_base_config( + base_config: &'a Config, + key_manager: KeyManager, + bandwidth_controller: Option, + reply_storage_backend: B, + ) -> BaseClientBuilder<'a, B> { + BaseClientBuilder { + gateway_config: base_config.get_gateway_endpoint_config(), + debug_config: base_config.get_debug_config(), + disabled_credentials: base_config.get_disabled_credentials_mode(), + validator_api_endpoints: base_config.get_validator_api_endpoints(), + bandwidth_controller, + reply_storage_backend, + key_manager, + } + } + + pub fn new( + gateway_config: &'a GatewayEndpointConfig, + debug_config: &'a DebugConfig, + key_manager: KeyManager, + bandwidth_controller: Option, + reply_storage_backend: B, + disabled_credentials: bool, + validator_api_endpoints: Vec, + ) -> BaseClientBuilder<'a, B> { + BaseClientBuilder { + gateway_config, + debug_config, + disabled_credentials, + validator_api_endpoints, + bandwidth_controller, + reply_storage_backend, + key_manager, + } + } + + pub fn as_mix_recipient(&self) -> Recipient { + Recipient::new( + *self.key_manager.identity_keypair().public_key(), + *self.key_manager.encryption_keypair().public_key(), + // TODO: below only works under assumption that gateway address == gateway id + // (which currently is true) + NodeIdentity::from_base58_string(&self.gateway_config.gateway_id).unwrap(), + ) + } + + // future constantly pumping loop cover traffic at some specified average rate + // the pumped traffic goes to the MixTrafficController + fn start_cover_traffic_stream( + debug_config: &DebugConfig, + ack_key: Arc, + self_address: Recipient, + topology_accessor: TopologyAccessor, + mix_tx: BatchMixMessageSender, + shutdown: ShutdownListener, + ) { + info!("Starting loop cover traffic stream..."); + + let mut stream = LoopCoverTrafficStream::new( + ack_key, + debug_config.average_ack_delay, + debug_config.average_packet_delay, + debug_config.loop_cover_traffic_average_delay, + mix_tx, + self_address, + topology_accessor, + ); + + if let Some(size) = debug_config.use_extended_packet_size { + log::debug!("Setting extended packet size: {:?}", size); + stream.set_custom_packet_size(size.into()); + } + + stream.start_with_shutdown(shutdown); + } + + #[allow(clippy::too_many_arguments)] + fn start_real_traffic_controller( + controller_config: real_messages_control::Config, + topology_accessor: TopologyAccessor, + ack_receiver: AcknowledgementReceiver, + input_receiver: InputMessageReceiver, + mix_sender: BatchMixMessageSender, + reply_storage: CombinedReplyStorage, + reply_controller_sender: ReplyControllerSender, + reply_controller_receiver: ReplyControllerReceiver, + lane_queue_lengths: LaneQueueLengths, + client_connection_rx: ConnectionCommandReceiver, + shutdown: ShutdownListener, + ) { + info!("Starting real traffic stream..."); + + RealMessagesController::new( + controller_config, + ack_receiver, + input_receiver, + mix_sender, + topology_accessor, + reply_storage, + reply_controller_sender, + reply_controller_receiver, + lane_queue_lengths, + client_connection_rx, + ) + .start_with_shutdown(shutdown); + } + + // buffer controlling all messages fetched from provider + // required so that other components would be able to use them (say the websocket) + fn start_received_messages_buffer_controller( + local_encryption_keypair: Arc, + query_receiver: ReceivedBufferRequestReceiver, + mixnet_receiver: MixnetMessageReceiver, + reply_key_storage: SentReplyKeys, + reply_controller_sender: ReplyControllerSender, + shutdown: ShutdownListener, + ) { + info!("Starting received messages buffer controller..."); + ReceivedMessagesBufferController::new( + local_encryption_keypair, + query_receiver, + mixnet_receiver, + reply_key_storage, + reply_controller_sender, + ) + .start_with_shutdown(shutdown) + } + + async fn start_gateway_client( + &mut self, + mixnet_message_sender: MixnetMessageSender, + ack_sender: AcknowledgementSender, + shutdown: ShutdownListener, + ) -> Result> { + let gateway_id = self.gateway_config.gateway_id.clone(); + if gateway_id.is_empty() { + return Err(ClientCoreError::GatewayIdUnknown); + } + let gateway_owner = self.gateway_config.gateway_owner.clone(); + if gateway_owner.is_empty() { + return Err(ClientCoreError::GatewayOwnerUnknown); + } + let gateway_address = self.gateway_config.gateway_listener.clone(); + if gateway_address.is_empty() { + return Err(ClientCoreError::GatwayAddressUnknown); + } + + let gateway_identity = identity::PublicKey::from_base58_string(gateway_id) + .map_err(ClientCoreError::UnableToCreatePublicKeyFromGatewayId)?; + + // disgusting wasm workaround since there's no key persistence there (nor `client init`) + let shared_key = if self.key_manager.gateway_key_set() { + Some(self.key_manager.gateway_shared_key()) + } else { + None + }; + + let mut gateway_client = GatewayClient::new( + gateway_address, + self.key_manager.identity_keypair(), + gateway_identity, + gateway_owner, + shared_key, + mixnet_message_sender, + ack_sender, + self.debug_config.gateway_response_timeout, + self.bandwidth_controller.take(), + shutdown, + ); + + gateway_client.set_disabled_credentials_mode(self.disabled_credentials); + + gateway_client + .authenticate_and_start() + .await + .tap_err(|err| { + log::error!("Could not authenticate and start up the gateway connection - {err}") + })?; + Ok(gateway_client) + } + + // future responsible for periodically polling directory server and updating + // the current global view of topology + async fn start_topology_refresher( + validator_api_urls: Vec, + refresh_rate: Duration, + topology_accessor: TopologyAccessor, + shutdown: ShutdownListener, + ) -> Result<(), ClientCoreError> { + let topology_refresher_config = TopologyRefresherConfig::new( + validator_api_urls, + refresh_rate, + env!("CARGO_PKG_VERSION").to_string(), + ); + let mut topology_refresher = + TopologyRefresher::new(topology_refresher_config, topology_accessor); + // before returning, block entire runtime to refresh the current network view so that any + // components depending on topology would see a non-empty view + info!("Obtaining initial network topology"); + topology_refresher.refresh().await; + + if let Err(err) = topology_refresher.ensure_topology_is_routable().await { + log::error!( + "The current network topology seem to be insufficient to route any packets through \ + - check if enough nodes and a gateway are online - source: {err}" + ); + return Err(ClientCoreError::InsufficientNetworkTopology(err)); + } + + info!("Starting topology refresher..."); + topology_refresher.start_with_shutdown(shutdown); + Ok(()) + } + + // controller for sending sphinx packets to mixnet (either real traffic or cover traffic) + // TODO: if we want to send control messages to gateway_client, this CAN'T take the ownership + // over it. Perhaps GatewayClient needs to be thread-shareable or have some channel for + // requests? + fn start_mix_traffic_controller( + gateway_client: GatewayClient, + shutdown: ShutdownListener, + ) -> BatchMixMessageSender { + info!("Starting mix traffic controller..."); + let (mix_traffic_controller, mix_tx) = MixTrafficController::new(gateway_client); + mix_traffic_controller.start_with_shutdown(shutdown); + mix_tx + } + + async fn setup_persistent_reply_storage( + backend: B, + shutdown: ShutdownListener, + ) -> Result> { + let persistent_storage = PersistentReplyStorage::new(backend); + let mem_store = persistent_storage + .load_state_from_backend() + .await + .map_err(|err| ClientCoreError::SurbStorageError { source: err })?; + + let store_clone = mem_store.clone(); + spawn_future(async move { + persistent_storage + .flush_on_shutdown(store_clone, shutdown) + .await + }); + + Ok(mem_store) + } + + pub async fn start_base(mut self) -> Result> { + info!("Starting nym client"); + // channels for inter-component communication + // TODO: make the channels be internally created by the relevant components + // rather than creating them here, so say for example the buffer controller would create the request channels + // and would allow anyone to clone the sender channel + + // unwrapped_sphinx_sender is the transmitter of mixnet messages received from the gateway + // unwrapped_sphinx_receiver is the receiver for said messages - used by ReceivedMessagesBuffer + let (mixnet_messages_sender, mixnet_messages_receiver) = mpsc::unbounded(); + + // used for announcing connection or disconnection of a channel for pushing re-assembled messages to + let (received_buffer_request_sender, received_buffer_request_receiver) = mpsc::unbounded(); + + // channels responsible for controlling real messages + let (input_sender, input_receiver) = tokio::sync::mpsc::channel::(1); + + // channels responsible for controlling ack messages + let (ack_sender, ack_receiver) = mpsc::unbounded(); + let shared_topology_accessor = TopologyAccessor::new(); + + // Shutdown notifier for signalling tasks to stop + let shutdown = ShutdownNotifier::default(); + + // channels responsible for dealing with reply-related fun + let (reply_controller_sender, reply_controller_receiver) = + reply_controller::new_control_channels(); + + let self_address = self.as_mix_recipient(); + + // the components are started in very specific order. Unless you know what you are doing, + // do not change that. + let gateway_client = self + .start_gateway_client(mixnet_messages_sender, ack_sender, shutdown.subscribe()) + .await?; + + let reply_storage = + Self::setup_persistent_reply_storage(self.reply_storage_backend, shutdown.subscribe()) + .await?; + + Self::start_topology_refresher( + self.validator_api_endpoints.clone(), + self.debug_config.topology_refresh_rate, + shared_topology_accessor.clone(), + shutdown.subscribe(), + ) + .await?; + + Self::start_received_messages_buffer_controller( + self.key_manager.encryption_keypair(), + received_buffer_request_receiver, + mixnet_messages_receiver, + reply_storage.key_storage(), + reply_controller_sender.clone(), + shutdown.subscribe(), + ); + + // The sphinx_message_sender is the transmitter for any component generating sphinx packets + // that are to be sent to the mixnet. They are used by cover traffic stream and real + // traffic stream. + // The MixTrafficController then sends the actual traffic + let sphinx_message_sender = + Self::start_mix_traffic_controller(gateway_client, shutdown.subscribe()); + + // Channels that the websocket listener can use to signal downstream to the real traffic + // controller that connections are closed. + let (client_connection_tx, client_connection_rx) = mpsc::unbounded(); + + // Shared queue length data. Published by the `OutQueueController` in the client, and used + // primarily to throttle incoming connections (e.g socks5 for attached network-requesters) + let shared_lane_queue_lengths = LaneQueueLengths::new(); + + let mut controller_config = real_messages_control::Config::new( + self.debug_config, + self.key_manager.ack_key(), + self_address, + ); + + if let Some(size) = self.debug_config.use_extended_packet_size { + log::debug!("Setting extended packet size: {:?}", size); + controller_config.set_custom_packet_size(size.into()); + } + + Self::start_real_traffic_controller( + controller_config, + shared_topology_accessor.clone(), + ack_receiver, + input_receiver, + sphinx_message_sender.clone(), + reply_storage, + reply_controller_sender, + reply_controller_receiver, + shared_lane_queue_lengths.clone(), + client_connection_rx, + shutdown.subscribe(), + ); + + if !self.debug_config.disable_loop_cover_traffic_stream { + Self::start_cover_traffic_stream( + self.debug_config, + self.key_manager.ack_key(), + self_address, + shared_topology_accessor, + sphinx_message_sender, + shutdown.subscribe(), + ); + } + + debug!("Core client startup finished!"); + debug!("The address of this client is: {self_address}"); + + Ok(BaseClient { + client_input: ClientInputStatus::AwaitingProducer { + client_input: ClientInput { + shared_lane_queue_lengths, + connection_command_sender: client_connection_tx, + input_sender, + }, + }, + client_output: ClientOutputStatus::AwaitingConsumer { + client_output: ClientOutput { + received_buffer_request_sender, + }, + }, + shutdown_notifier: shutdown, + }) + } +} + +pub struct BaseClient { + pub client_input: ClientInputStatus, + pub client_output: ClientOutputStatus, + + pub shutdown_notifier: ShutdownNotifier, +} diff --git a/clients/client-core/src/client/base_client/non_wasm_helpers.rs b/clients/client-core/src/client/base_client/non_wasm_helpers.rs new file mode 100644 index 0000000000..8c51c784c8 --- /dev/null +++ b/clients/client-core/src/client/base_client/non_wasm_helpers.rs @@ -0,0 +1,51 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::client::replies::reply_storage::{ + fs_backend, CombinedReplyStorage, ReplyStorageBackend, +}; +use crate::config::DebugConfig; +use crate::error::ClientCoreError; +use log::{error, info}; +use std::path::Path; + +pub async fn setup_fs_reply_surb_backend>( + db_path: P, + debug_config: &DebugConfig, +) -> Result> { + // if the database file doesnt exist, initialise fresh storage, otherwise attempt to load the existing one + let db_path = db_path.as_ref(); + if db_path.exists() { + info!("loading existing surb database"); + match fs_backend::Backend::try_load(db_path).await { + Ok(backend) => Ok(backend), + Err(err) => { + error!("failed to setup persistent storage backend for our reply needs: {err}"); + Err(ClientCoreError::SurbStorageError { source: err }) + } + } + } else { + info!("creating fresh surb database"); + let mut storage_backend = match fs_backend::Backend::init(db_path).await { + Ok(backend) => backend, + Err(err) => { + error!("failed to setup persistent storage backend for our reply needs: {err}"); + return Err(ClientCoreError::SurbStorageError { source: err }); + } + }; + + // while I kinda hate that we're going to be creating `CombinedReplyStorage` twice, + // it will only be happening on the very first run and in practice won't incur huge + // costs since the storage is going to be empty + let mem_store = CombinedReplyStorage::new( + debug_config.minimum_reply_surb_storage_threshold, + debug_config.maximum_reply_surb_storage_threshold, + ); + storage_backend + .init_fresh(&mem_store) + .await + .map_err(|err| ClientCoreError::SurbStorageError { source: err })?; + + Ok(storage_backend) + } +} diff --git a/clients/client-core/src/client/cover_traffic_stream.rs b/clients/client-core/src/client/cover_traffic_stream.rs index 228234663a..e3fd0ca197 100644 --- a/clients/client-core/src/client/cover_traffic_stream.rs +++ b/clients/client-core/src/client/cover_traffic_stream.rs @@ -143,6 +143,16 @@ impl LoopCoverTrafficStream { self.packet_size = packet_size; } + fn set_next_delay(&mut self, amount: Duration) { + #[cfg(not(target_arch = "wasm32"))] + let next_delay = Box::pin(time::sleep(amount)); + + #[cfg(target_arch = "wasm32")] + let next_delay = Box::pin(wasm_timer::Delay::new(amount)); + + self.next_delay = next_delay; + } + async fn on_new_message(&mut self) { trace!("next cover message!"); @@ -151,15 +161,16 @@ impl LoopCoverTrafficStream { // poisson delay, but is it really a problem? let topology_permit = self.topology_access.get_read_permit().await; // the ack is sent back to ourselves (and then ignored) - let topology_ref_option = topology_permit.try_get_valid_topology_ref( + let topology_ref = match topology_permit.try_get_valid_topology_ref( &self.our_full_destination, Some(&self.our_full_destination), - ); - if topology_ref_option.is_none() { - warn!("No valid topology detected - won't send any loop cover message this time"); - return; - } - let topology_ref = topology_ref_option.unwrap(); + ) { + Ok(topology) => topology, + Err(err) => { + warn!("We're not going to send any loop cover message this time, as the current topology seem to be invalid - {err}"); + return; + } + }; let cover_message = generate_loop_cover_packet( &mut self.rng, @@ -181,7 +192,7 @@ impl LoopCoverTrafficStream { // However it's still useful to alert the user that the gateway or the link to // the gateway can't keep up. Either due to insufficient bandwidth on the // client side, or that the gateway is overloaded. - log::warn!("Failed to send: gateway appears to not keep up"); + log::warn!("Failed to send sphinx packet - gateway or connection to gatway can't keep up"); } TrySendError::Closed(_) => { log::warn!("Failed to send cover message - channel closed"); @@ -202,12 +213,11 @@ impl LoopCoverTrafficStream { tokio::task::yield_now().await; } - #[cfg(not(target_arch = "wasm32"))] pub fn start_with_shutdown(mut self, mut shutdown: task::ShutdownListener) { // we should set initial delay only when we actually start the stream let sampled = sample_poisson_duration(&mut self.rng, self.average_cover_message_sending_delay); - self.next_delay = Box::pin(time::sleep(sampled)); + self.set_next_delay(sampled); spawn_future(async move { debug!("Started LoopCoverTrafficStream with graceful shutdown support"); @@ -228,26 +238,8 @@ impl LoopCoverTrafficStream { } } } - tokio::time::timeout(Duration::from_secs(5), shutdown.recv()) - .await - .expect("Task stopped without shutdown called"); + shutdown.recv_timeout().await; log::debug!("LoopCoverTrafficStream: Exiting"); }) } - - #[cfg(target_arch = "wasm32")] - pub fn start(mut self) { - // we should set initial delay only when we actually start the stream - let sampled = - sample_poisson_duration(&mut self.rng, self.average_cover_message_sending_delay); - self.next_delay = Box::pin(wasm_timer::Delay::new(sampled)); - - spawn_future(async move { - debug!("Started LoopCoverTrafficStream without graceful shutdown support"); - - while self.next().await.is_some() { - self.on_new_message().await; - } - }) - } } diff --git a/clients/client-core/src/client/inbound_messages.rs b/clients/client-core/src/client/inbound_messages.rs index 909e3720f9..890787aa60 100644 --- a/clients/client-core/src/client/inbound_messages.rs +++ b/clients/client-core/src/client/inbound_messages.rs @@ -1,40 +1,80 @@ use client_connections::TransmissionLane; use nymsphinx::addressing::clients::Recipient; -use nymsphinx::anonymous_replies::ReplySurb; +use nymsphinx::anonymous_replies::requests::AnonymousSenderTag; pub type InputMessageSender = tokio::sync::mpsc::Sender; pub type InputMessageReceiver = tokio::sync::mpsc::Receiver; #[derive(Debug)] pub enum InputMessage { - Fresh { + /// The simplest message variant where no additional information is attached. + /// You're simply sending your `data` to specified `recipient` without any tagging. + /// + /// Ends up with `NymMessage::Plain` variant + Regular { recipient: Recipient, data: Vec, - with_reply_surb: bool, lane: TransmissionLane, }, - Reply { - reply_surb: ReplySurb, + + /// Creates a message used for a duplex anonymous communication where the recipient + /// will never learn of our true identity. This is achieved by carefully sending `reply_surbs`. + /// + /// Note that if reply_surbs is set to zero then + /// this variant requires the client having sent some reply_surbs in the past + /// (and thus the recipient also knowing our sender tag). + /// + /// Ends up with `NymMessage::Repliable` variant + Anonymous { + recipient: Recipient, data: Vec, + reply_surbs: u32, + lane: TransmissionLane, + }, + + /// Attempt to use our internally received and stored `ReplySurb` to send the message back + /// to specified recipient whilst not knowing its full identity (or even gateway). + /// + /// Ends up with `NymMessage::Reply` variant + Reply { + recipient_tag: AnonymousSenderTag, + data: Vec, + lane: TransmissionLane, }, } impl InputMessage { - pub fn new_fresh( - recipient: Recipient, - data: Vec, - with_reply_surb: bool, - lane: TransmissionLane, - ) -> Self { - InputMessage::Fresh { + pub fn new_regular(recipient: Recipient, data: Vec, lane: TransmissionLane) -> Self { + InputMessage::Regular { recipient, data, - with_reply_surb, lane, } } - pub fn new_reply(reply_surb: ReplySurb, data: Vec) -> Self { - InputMessage::Reply { reply_surb, data } + pub fn new_anonymous( + recipient: Recipient, + data: Vec, + reply_surbs: u32, + lane: TransmissionLane, + ) -> Self { + InputMessage::Anonymous { + recipient, + data, + reply_surbs, + lane, + } + } + + pub fn new_reply( + recipient_tag: AnonymousSenderTag, + data: Vec, + lane: TransmissionLane, + ) -> Self { + InputMessage::Reply { + recipient_tag, + data, + lane, + } } } diff --git a/clients/client-core/src/client/key_manager.rs b/clients/client-core/src/client/key_manager.rs index 2e7069a6a8..6fa264f43f 100644 --- a/clients/client-core/src/client/key_manager.rs +++ b/clients/client-core/src/client/key_manager.rs @@ -149,6 +149,10 @@ impl KeyManager { ) } + pub fn gateway_key_set(&self) -> bool { + self.gateway_shared_key.is_some() + } + /// Gets an atomically reference counted pointer to [`AckKey`]. pub fn ack_key(&self) -> Arc { Arc::clone(&self.ack_key) diff --git a/clients/client-core/src/client/mix_traffic.rs b/clients/client-core/src/client/mix_traffic.rs index 06846316cc..f61a83af95 100644 --- a/clients/client-core/src/client/mix_traffic.rs +++ b/clients/client-core/src/client/mix_traffic.rs @@ -67,10 +67,7 @@ impl MixTrafficController { } } - #[cfg(not(target_arch = "wasm32"))] pub fn start_with_shutdown(mut self, mut shutdown: task::ShutdownListener) { - use std::time::Duration; - spawn_future(async move { debug!("Started MixTrafficController with graceful shutdown support"); @@ -90,21 +87,8 @@ impl MixTrafficController { } } } - tokio::time::timeout(Duration::from_secs(5), shutdown.recv()) - .await - .expect("Task stopped without shutdown called"); + shutdown.recv_timeout().await; log::debug!("MixTrafficController: Exiting"); }) } - - #[cfg(target_arch = "wasm32")] - pub fn start(mut self) { - spawn_future(async move { - debug!("Started MixTrafficController without graceful shutdown support"); - - while let Some(mix_packets) = self.mix_rx.recv().await { - self.on_messages(mix_packets).await; - } - }) - } } diff --git a/clients/client-core/src/client/mod.rs b/clients/client-core/src/client/mod.rs index 309be529f1..8110d52909 100644 --- a/clients/client-core/src/client/mod.rs +++ b/clients/client-core/src/client/mod.rs @@ -1,9 +1,12 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod base_client; pub mod cover_traffic_stream; pub mod inbound_messages; pub mod key_manager; pub mod mix_traffic; pub mod real_messages_control; pub mod received_buffer; -#[cfg(feature = "reply-surb")] -pub mod reply_key_storage; +pub mod replies; pub mod topology_control; diff --git a/clients/client-core/src/client/real_messages_control/acknowledgement_control/acknowledgement_listener.rs b/clients/client-core/src/client/real_messages_control/acknowledgement_control/acknowledgement_listener.rs index dff476ad3c..95c431fd59 100644 --- a/clients/client-core/src/client/real_messages_control/acknowledgement_control/acknowledgement_listener.rs +++ b/clients/client-core/src/client/real_messages_control/acknowledgement_control/acknowledgement_listener.rs @@ -1,7 +1,7 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use super::action_controller::{Action, ActionSender}; +use super::action_controller::{AckActionSender, Action}; use futures::StreamExt; use gateway_client::AcknowledgementReceiver; use log::*; @@ -16,14 +16,14 @@ use std::sync::Arc; pub(super) struct AcknowledgementListener { ack_key: Arc, ack_receiver: AcknowledgementReceiver, - action_sender: ActionSender, + action_sender: AckActionSender, } impl AcknowledgementListener { pub(super) fn new( ack_key: Arc, ack_receiver: AcknowledgementReceiver, - action_sender: ActionSender, + action_sender: AckActionSender, ) -> Self { AcknowledgementListener { ack_key, @@ -49,11 +49,6 @@ impl AcknowledgementListener { if frag_id == COVER_FRAG_ID { trace!("Received an ack for a cover message - no need to do anything"); return; - } else if frag_id.is_reply() { - info!("Received an ack for a reply message - no need to do anything! (don't know what to do!)"); - // TODO: probably there will need to be some extra procedure here, something to notify - // user that his reply reached the recipient (since we got an ack) - return; } trace!("Received {} from the mix network", frag_id); @@ -70,10 +65,7 @@ impl AcknowledgementListener { } } - #[cfg(not(target_arch = "wasm32"))] pub(super) async fn run_with_shutdown(&mut self, mut shutdown: task::ShutdownListener) { - use std::time::Duration; - debug!("Started AcknowledgementListener with graceful shutdown support"); while !shutdown.is_shutdown() { @@ -90,18 +82,7 @@ impl AcknowledgementListener { } } } - tokio::time::timeout(Duration::from_secs(5), shutdown.recv()) - .await - .expect("Task stopped without shutdown called"); + shutdown.recv_timeout().await; log::debug!("AcknowledgementListener: Exiting"); } - - #[cfg(target_arch = "wasm32")] - pub(super) async fn run(&mut self) { - debug!("Started AcknowledgementListener without graceful shutdown support"); - - while let Some(acks) = self.ack_receiver.next().await { - self.handle_ack_receiver_item(acks).await - } - } } diff --git a/clients/client-core/src/client/real_messages_control/acknowledgement_control/action_controller.rs b/clients/client-core/src/client/real_messages_control/acknowledgement_control/action_controller.rs index 822cea0a2f..cb2e0cc7ca 100644 --- a/clients/client-core/src/client/real_messages_control/acknowledgement_control/action_controller.rs +++ b/clients/client-core/src/client/real_messages_control/acknowledgement_control/action_controller.rs @@ -3,7 +3,7 @@ use super::PendingAcknowledgement; use crate::client::real_messages_control::acknowledgement_control::RetransmissionRequestSender; -use futures::channel::mpsc::{self, UnboundedReceiver, UnboundedSender}; +use futures::channel::mpsc; use futures::StreamExt; use log::*; use nonexhaustive_delayqueue::{Expired, NonExhaustiveDelayQueue, QueueKey}; @@ -13,7 +13,8 @@ use std::collections::HashMap; use std::sync::Arc; use std::time::Duration; -pub(crate) type ActionSender = UnboundedSender; +pub(crate) type AckActionSender = mpsc::UnboundedSender; +pub(crate) type AckActionReceiver = mpsc::UnboundedReceiver; // The actual data being sent off as well as potential key to the delay queue type PendingAckEntry = (Arc, Option); @@ -95,7 +96,7 @@ pub(super) struct ActionController { pending_acks_timers: NonExhaustiveDelayQueue, /// Channel for receiving `Action`s from other modules. - incoming_actions: UnboundedReceiver, + incoming_actions: AckActionReceiver, /// Channel for notifying `RetransmissionRequestListener` about expired acknowledgements. retransmission_sender: RetransmissionRequestSender, @@ -105,18 +106,15 @@ impl ActionController { pub(super) fn new( config: Config, retransmission_sender: RetransmissionRequestSender, - ) -> (Self, ActionSender) { - let (sender, receiver) = mpsc::unbounded(); - ( - ActionController { - config, - pending_acks_data: HashMap::new(), - pending_acks_timers: NonExhaustiveDelayQueue::new(), - incoming_actions: receiver, - retransmission_sender, - }, - sender, - ) + incoming_actions: AckActionReceiver, + ) -> Self { + ActionController { + config, + pending_acks_data: HashMap::new(), + pending_acks_timers: NonExhaustiveDelayQueue::new(), + incoming_actions, + retransmission_sender, + } } fn handle_insert(&mut self, pending_acks: Vec) { @@ -138,13 +136,18 @@ impl ActionController { trace!("{} is starting its timer", frag_id); if let Some((pending_ack_data, queue_key)) = self.pending_acks_data.get_mut(&frag_id) { - if queue_key.is_some() { - // this branch should be IMPOSSIBLE under ANY condition. It would imply starting - // timer TWICE for the SAME PendingAcknowledgement - panic!("Tried to start an already started ack timer!") - } - let timeout = (pending_ack_data.delay.clone() * self.config.ack_wait_multiplier) - .to_duration() + // the fact that this branch is now POSSIBLE is a sign of a need to refactor this whole + // retransmission procedure + // + // (it can happen as timer is started when ack expires to make sure it's not stuck in memory + // and the second instance can be fired when we finally get reply surbs for data we failed to retransmit) + + // if queue_key.is_some() { + // // this branch should be IMPOSSIBLE under ANY condition. It would imply starting + // // timer TWICE for the SAME PendingAcknowledgement + // panic!("Tried to start an already started ack timer!") + // } + let timeout = (pending_ack_data.delay * self.config.ack_wait_multiplier).to_duration() + self.config.ack_wait_addition; let new_queue_key = self.pending_acks_timers.insert(frag_id, timeout); @@ -192,7 +195,8 @@ impl ActionController { trace!("{} is updating its delay", frag_id); // TODO: is it possible to solve this without either locking or temporarily removing the value? if let Some((pending_ack_data, queue_key)) = self.pending_acks_data.remove(&frag_id) { - // this Action is triggered by `RetransmissionRequestListener` which held the other potential + // this Action is triggered by `RetransmissionRequestListener` (for 'normal' packets) + // or `ReplyController` (for 'reply' packets) which held the other potential // reference to this Arc. HOWEVER, before the Action was pushed onto the queue, the reference // was dropped hence this unwrap is safe. let mut inner_data = Arc::try_unwrap(pending_ack_data).unwrap(); @@ -245,7 +249,6 @@ impl ActionController { } } - #[cfg(not(target_arch = "wasm32"))] pub(super) async fn run_with_shutdown(&mut self, mut shutdown: task::ShutdownListener) { debug!("Started ActionController with graceful shutdown support"); @@ -272,21 +275,10 @@ impl ActionController { } } } + #[cfg(not(target_arch = "wasm32"))] tokio::time::timeout(Duration::from_secs(5), shutdown.recv()) .await .expect("Task stopped without shutdown called"); log::debug!("ActionController: Exiting"); } - - #[cfg(target_arch = "wasm32")] - pub(super) async fn run(&mut self) { - debug!("Started ActionController without graceful shutdown support"); - - loop { - tokio::select! { - action = self.incoming_actions.next() => self.process_action(action.unwrap()), - expired_ack = self.pending_acks_timers.next() => self.handle_expired_ack_timer(expired_ack.unwrap()) - } - } - } } diff --git a/clients/client-core/src/client/real_messages_control/acknowledgement_control/input_message_listener.rs b/clients/client-core/src/client/real_messages_control/acknowledgement_control/input_message_listener.rs index c58967104c..cff6b6d719 100644 --- a/clients/client-core/src/client/real_messages_control/acknowledgement_control/input_message_listener.rs +++ b/clients/client-core/src/client/real_messages_control/acknowledgement_control/input_message_listener.rs @@ -1,23 +1,14 @@ -// Copyright 2021 - Nym Technologies SA +// Copyright 2021-2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use super::action_controller::{Action, ActionSender}; -use super::PendingAcknowledgement; -use crate::client::{ - inbound_messages::{InputMessage, InputMessageReceiver}, - real_messages_control::real_traffic_stream::{BatchRealMessageSender, RealMessage}, - topology_control::TopologyAccessor, -}; +use crate::client::inbound_messages::{InputMessage, InputMessageReceiver}; +use crate::client::real_messages_control::message_handler::MessageHandler; +use crate::client::replies::reply_controller::ReplyControllerSender; use client_connections::TransmissionLane; use log::*; -use nymsphinx::anonymous_replies::ReplySurb; -use nymsphinx::preparer::MessagePreparer; -use nymsphinx::{acknowledgements::AckKey, addressing::clients::Recipient}; +use nymsphinx::addressing::clients::Recipient; +use nymsphinx::anonymous_replies::requests::AnonymousSenderTag; use rand::{CryptoRng, Rng}; -use std::sync::Arc; - -#[cfg(feature = "reply-surb")] -use crate::client::reply_key_storage::ReplyKeyStorage; /// Module responsible for dealing with the received messages: splitting them, creating acknowledgements, /// putting everything into sphinx packets, etc. @@ -26,15 +17,9 @@ pub(super) struct InputMessageListener where R: CryptoRng + Rng, { - ack_key: Arc, - ack_recipient: Recipient, input_receiver: InputMessageReceiver, - message_preparer: MessagePreparer, - action_sender: ActionSender, - real_message_sender: BatchRealMessageSender, - topology_access: TopologyAccessor, - #[cfg(feature = "reply-surb")] - reply_key_storage: ReplyKeyStorage, + message_handler: MessageHandler, + reply_controller_sender: ReplyControllerSender, } impl InputMessageListener @@ -45,159 +30,86 @@ where // some considerable refactoring #[allow(clippy::too_many_arguments)] pub(super) fn new( - ack_key: Arc, - ack_recipient: Recipient, input_receiver: InputMessageReceiver, - message_preparer: MessagePreparer, - action_sender: ActionSender, - real_message_sender: BatchRealMessageSender, - topology_access: TopologyAccessor, - #[cfg(feature = "reply-surb")] reply_key_storage: ReplyKeyStorage, + message_handler: MessageHandler, + reply_controller_sender: ReplyControllerSender, ) -> Self { InputMessageListener { - ack_key, - ack_recipient, input_receiver, - message_preparer, - action_sender, - real_message_sender, - topology_access, - #[cfg(feature = "reply-surb")] - reply_key_storage, + message_handler, + reply_controller_sender, } } - // we require topology for replies to generate surb_acks - async fn handle_reply(&mut self, reply_surb: ReplySurb, data: Vec) -> Option { - let topology_permit = self.topology_access.get_read_permit().await; - let topology = match topology_permit.try_get_valid_topology_ref(&self.ack_recipient, None) { - Some(topology_ref) => topology_ref, - None => { - warn!("Could not process the message - the network topology is invalid"); - return None; - } - }; - - match self - .message_preparer - .prepare_reply_for_use(data, reply_surb, topology, &self.ack_key) - .await - { - Ok((mix_packet, reply_id)) => { - // TODO: later probably write pending ack here - // and deal with them.... - // ... somehow - Some(RealMessage::new(mix_packet, reply_id)) - } - Err(err) => { - // TODO: should we have some mechanism to indicate to the user that the `reply_surb` - // could be reused since technically it wasn't used up here? - warn!("failed to deal with received reply surb - {:?}", err); - None - } - } + async fn handle_reply( + &mut self, + recipient_tag: AnonymousSenderTag, + data: Vec, + lane: TransmissionLane, + ) { + // offload reply handling to the dedicated task + self.reply_controller_sender + .send_reply(recipient_tag, data, lane) } - async fn handle_fresh_message( + async fn handle_plain_message( &mut self, recipient: Recipient, content: Vec, - with_reply_surb: bool, - ) -> Option> { - log::trace!("handling msg size: {}", content.len()); - let topology_permit = self.topology_access.get_read_permit().await; - let topology = match topology_permit - .try_get_valid_topology_ref(&self.ack_recipient, Some(&recipient)) + lane: TransmissionLane, + ) { + if let Err(err) = self + .message_handler + .try_send_plain_message(recipient, content, lane) + .await { - Some(topology_ref) => topology_ref, - None => { - warn!("Could not process the message - the network topology is invalid"); - return None; - } - }; - - // split the message, attach optional reply surb - let (split_message, reply_key) = self - .message_preparer - .prepare_and_split_message(content, with_reply_surb, topology) - .expect("somehow the topology was invalid after all!"); - - #[cfg(feature = "reply-surb")] - if let Some(reply_key) = reply_key { - self.reply_key_storage - .insert_encryption_key(reply_key) - .expect("Failed to insert surb reply key to the store!") + warn!("failed to send a plain message - {err}") } + } - #[cfg(not(feature = "reply-surb"))] - let _reply_key = reply_key; - - // encrypt chunks, put them inside sphinx packets and generate acks - let mut pending_acks = Vec::with_capacity(split_message.len()); - let mut real_messages = Vec::with_capacity(split_message.len()); - for message_chunk in split_message { - // we need to clone it because we need to keep it in memory in case we had to retransmit - // it. And then we'd need to recreate entire ACK again. - let chunk_clone = message_chunk.clone(); - let prepared_fragment = self - .message_preparer - .prepare_chunk_for_sending(chunk_clone, topology, &self.ack_key, &recipient) - .unwrap(); - - real_messages.push(RealMessage::new( - prepared_fragment.mix_packet, - message_chunk.fragment_identifier(), - )); - - pending_acks.push(PendingAcknowledgement::new( - message_chunk, - prepared_fragment.total_delay, - recipient, - )); + async fn handle_repliable_message( + &mut self, + recipient: Recipient, + content: Vec, + reply_surbs: u32, + lane: TransmissionLane, + ) { + if let Err(err) = self + .message_handler + .try_send_message_with_reply_surbs(recipient, content, reply_surbs, lane) + .await + { + warn!("failed to send a repliable message - {err}") } - - // tells the controller to put this into the hashmap - self.action_sender - .unbounded_send(Action::new_insert(pending_acks)) - .unwrap(); - - Some(real_messages) } async fn on_input_message(&mut self, msg: InputMessage) { - let (real_messages, lane) = match msg { - InputMessage::Fresh { + match msg { + InputMessage::Regular { recipient, data, - with_reply_surb, lane, - } => ( - self.handle_fresh_message(recipient, data, with_reply_surb) - .await, + } => self.handle_plain_message(recipient, data, lane).await, + InputMessage::Anonymous { + recipient, + data, + reply_surbs, lane, - ), - InputMessage::Reply { reply_surb, data } => ( - self.handle_reply(reply_surb, data) + } => { + self.handle_repliable_message(recipient, data, reply_surbs, lane) .await - .map(|message| vec![message]), - TransmissionLane::Reply, - ), + } + InputMessage::Reply { + recipient_tag, + data, + lane, + } => { + self.handle_reply(recipient_tag, data, lane).await; + } }; - - // there's no point in trying to send nothing - if let Some(real_messages) = real_messages { - // tells real message sender (with the poisson timer) to send this to the mix network - self.real_message_sender - .send((real_messages, lane)) - .await - .expect("BatchRealMessageReceiver has stopped receiving!"); - } } - #[cfg(not(target_arch = "wasm32"))] pub(super) async fn run_with_shutdown(&mut self, mut shutdown: task::ShutdownListener) { - use std::time::Duration; - debug!("Started InputMessageListener with graceful shutdown support"); while !shutdown.is_shutdown() { @@ -216,17 +128,7 @@ where } } } - tokio::time::timeout(Duration::from_secs(5), shutdown.recv()) - .await - .expect("Task stopped without shutdown called"); + shutdown.recv_timeout().await; log::debug!("InputMessageListener: Exiting"); } - - #[cfg(target_arch = "wasm32")] - pub(super) async fn run(&mut self) { - debug!("Started InputMessageListener without graceful shutdown support"); - while let Some(input_msg) = self.input_receiver.recv().await { - self.on_input_message(input_msg).await; - } - } } diff --git a/clients/client-core/src/client/real_messages_control/acknowledgement_control/mod.rs b/clients/client-core/src/client/real_messages_control/acknowledgement_control/mod.rs index c946716091..5587958f33 100644 --- a/clients/client-core/src/client/real_messages_control/acknowledgement_control/mod.rs +++ b/clients/client-core/src/client/real_messages_control/acknowledgement_control/mod.rs @@ -7,18 +7,20 @@ use self::{ retransmission_request_listener::RetransmissionRequestListener, sent_notification_listener::SentNotificationListener, }; -use super::real_traffic_stream::BatchRealMessageSender; -use crate::client::{inbound_messages::InputMessageReceiver, topology_control::TopologyAccessor}; +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 gateway_client::AcknowledgementReceiver; use log::*; +use nymsphinx::anonymous_replies::requests::AnonymousSenderTag; use nymsphinx::params::PacketSize; use nymsphinx::{ acknowledgements::AckKey, addressing::clients::Recipient, chunking::fragment::{Fragment, FragmentIdentifier}, - preparer::MessagePreparer, Delay as SphinxDelay, }; use rand::{CryptoRng, Rng}; @@ -27,8 +29,7 @@ use std::{ time::Duration, }; -#[cfg(feature = "reply-surb")] -use crate::client::reply_key_storage::ReplyKeyStorage; +pub(crate) use action_controller::{AckActionSender, Action}; mod acknowledgement_listener; mod action_controller; @@ -50,24 +51,64 @@ pub(super) type SentPacketNotificationSender = mpsc::UnboundedSender; +#[derive(Debug)] +pub(crate) enum PacketDestination { + Anonymous { + recipient_tag: AnonymousSenderTag, + // special flag to indicate whether this was an ack for requesting additional surbs, + // in that case we have to do everything we can to get it through, even if it means going + // below our stored reply surb threshold + extra_surb_request: bool, + }, + KnownRecipient(Box), +} + /// Structure representing a data `Fragment` that is on-route to the specified `Recipient` #[derive(Debug)] pub(crate) struct PendingAcknowledgement { message_chunk: Fragment, delay: SphinxDelay, - recipient: Recipient, + destination: PacketDestination, } impl PendingAcknowledgement { /// Creates new instance of `PendingAcknowledgement` using the provided data. - fn new(message_chunk: Fragment, delay: SphinxDelay, recipient: Recipient) -> Self { + pub(crate) fn new_known( + message_chunk: Fragment, + delay: SphinxDelay, + recipient: Recipient, + ) -> Self { PendingAcknowledgement { message_chunk, delay, - recipient, + destination: PacketDestination::KnownRecipient(recipient.into()), } } + pub(crate) fn new_anonymous( + message_chunk: Fragment, + delay: SphinxDelay, + recipient_tag: AnonymousSenderTag, + extra_surb_request: bool, + ) -> Self { + PendingAcknowledgement { + message_chunk, + delay, + destination: PacketDestination::Anonymous { + recipient_tag, + extra_surb_request, + }, + } + } + + pub(crate) fn inner_fragment_identifier(&self) -> FragmentIdentifier { + self.message_chunk.fragment_identifier() + } + + pub(crate) fn fragment_data(&self) -> Fragment { + self.message_chunk.clone() + } + fn update_delay(&mut self, new_delay: SphinxDelay) { self.delay = new_delay; } @@ -76,10 +117,6 @@ impl PendingAcknowledgement { /// AcknowledgementControllerConnectors represents set of channels for communication with /// other parts of the system in order to support acknowledgements and retransmission. pub(super) struct AcknowledgementControllerConnectors { - /// Channel used for forwarding prepared sphinx messages into the poisson sender - /// to be sent to the mix network. - real_message_sender: BatchRealMessageSender, - /// Channel used for receiving raw messages from a client. The messages need to be put /// into sphinx packets first. input_receiver: InputMessageReceiver, @@ -91,20 +128,28 @@ pub(super) struct AcknowledgementControllerConnectors { /// Channel used for receiving acknowledgements from the mix network. ack_receiver: AcknowledgementReceiver, + + /// Channel used for sending request to `ActionController` to deal with anything ack-related, + ack_action_sender: AckActionSender, + + /// Channel used for receiving request by `ActionController` to deal with anything ack-related, + ack_action_receiver: AckActionReceiver, } impl AcknowledgementControllerConnectors { pub(super) fn new( - real_message_sender: BatchRealMessageSender, input_receiver: InputMessageReceiver, sent_notifier: SentPacketNotificationReceiver, ack_receiver: AcknowledgementReceiver, + ack_action_sender: AckActionSender, + ack_action_receiver: AckActionReceiver, ) -> Self { AcknowledgementControllerConnectors { - real_message_sender, input_receiver, sent_notifier, ack_receiver, + ack_action_sender, + ack_action_receiver, } } } @@ -117,28 +162,15 @@ pub(super) struct Config { /// Given ack timeout in the form a * BASE_DELAY + b, it specifies the multiplier `a` ack_wait_multiplier: f64, - /// Average delay an acknowledgement packet is going to get delayed at a single mixnode. - average_ack_delay: Duration, - - /// Average delay a data packet is going to get delayed at a single mixnode. - average_packet_delay: Duration, - /// Predefined packet size used for the encapsulated messages. packet_size: PacketSize, } impl Config { - pub(super) fn new( - ack_wait_addition: Duration, - ack_wait_multiplier: f64, - average_ack_delay: Duration, - average_packet_delay: Duration, - ) -> Self { + pub(super) fn new(ack_wait_addition: Duration, ack_wait_multiplier: f64) -> Self { Config { ack_wait_addition, ack_wait_multiplier, - average_ack_delay, - average_packet_delay, packet_size: Default::default(), } } @@ -162,68 +194,51 @@ where impl AcknowledgementController where - R: 'static + CryptoRng + Rng + Clone + Send, + R: 'static + CryptoRng + Rng + Clone + Send + Sync, { - #[allow(clippy::too_many_arguments)] pub(super) fn new( config: Config, - rng: R, - topology_access: TopologyAccessor, ack_key: Arc, - ack_recipient: Recipient, connectors: AcknowledgementControllerConnectors, - #[cfg(feature = "reply-surb")] reply_key_storage: ReplyKeyStorage, + message_handler: MessageHandler, + reply_controller_sender: ReplyControllerSender, ) -> Self { let (retransmission_tx, retransmission_rx) = mpsc::unbounded(); let action_config = action_controller::Config::new(config.ack_wait_addition, config.ack_wait_multiplier); - let (action_controller, action_sender) = - ActionController::new(action_config, retransmission_tx); - - let message_preparer = MessagePreparer::new( - rng, - ack_recipient, - config.average_packet_delay, - config.average_ack_delay, - ) - .with_custom_real_message_packet_size(config.packet_size); + let action_controller = ActionController::new( + action_config, + retransmission_tx, + connectors.ack_action_receiver, + ); // will listen for any acks coming from the network let acknowledgement_listener = AcknowledgementListener::new( Arc::clone(&ack_key), connectors.ack_receiver, - action_sender.clone(), + connectors.ack_action_sender.clone(), ); // will listen for any new messages from the client let input_message_listener = InputMessageListener::new( - Arc::clone(&ack_key), - ack_recipient, connectors.input_receiver, - message_preparer.clone(), - action_sender.clone(), - connectors.real_message_sender.clone(), - topology_access.clone(), - #[cfg(feature = "reply-surb")] - reply_key_storage, + message_handler.clone(), + reply_controller_sender.clone(), ); // will listen for any ack timeouts and trigger retransmission let retransmission_request_listener = RetransmissionRequestListener::new( - Arc::clone(&ack_key), - ack_recipient, - message_preparer, - action_sender.clone(), - connectors.real_message_sender, + connectors.ack_action_sender.clone(), + message_handler, retransmission_rx, - topology_access, + reply_controller_sender, ); // 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, action_sender); + SentNotificationListener::new(connectors.sent_notifier, connectors.ack_action_sender); AcknowledgementController { acknowledgement_listener, @@ -234,7 +249,6 @@ where } } - #[cfg(not(target_arch = "wasm32"))] pub(super) fn start_with_shutdown(self, shutdown: task::ShutdownListener) { let mut acknowledgement_listener = self.acknowledgement_listener; let mut input_message_listener = self.input_message_listener; @@ -279,34 +293,4 @@ where debug!("The controller has finished execution!"); }); } - - #[cfg(target_arch = "wasm32")] - pub(super) fn start(self) { - 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; - error!("The acknowledgement listener has finished execution!"); - }); - spawn_future(async move { - input_message_listener.run().await; - error!("The input listener has finished execution!"); - }); - spawn_future(async move { - retransmission_request_listener.run().await; - error!("The retransmission request listener has finished execution!"); - }); - spawn_future(async move { - sent_notification_listener.run().await; - error!("The sent notification listener has finished execution!"); - }); - spawn_future(async move { - action_controller.run().await; - error!("The controller has finished execution!"); - }); - } } diff --git a/clients/client-core/src/client/real_messages_control/acknowledgement_control/retransmission_request_listener.rs b/clients/client-core/src/client/real_messages_control/acknowledgement_control/retransmission_request_listener.rs index d0fd805d22..b5c6e6a2a8 100644 --- a/clients/client-core/src/client/real_messages_control/acknowledgement_control/retransmission_request_listener.rs +++ b/clients/client-core/src/client/real_messages_control/acknowledgement_control/retransmission_request_listener.rs @@ -1,82 +1,101 @@ -// Copyright 2021 - Nym Technologies SA +// Copyright 2021-2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 use super::{ - action_controller::{Action, ActionSender}, + action_controller::{AckActionSender, Action}, PendingAcknowledgement, RetransmissionRequestReceiver, }; -use crate::client::{ - real_messages_control::real_traffic_stream::{BatchRealMessageSender, RealMessage}, - topology_control::TopologyAccessor, -}; - +use crate::client::real_messages_control::acknowledgement_control::PacketDestination; +use crate::client::real_messages_control::message_handler::{MessageHandler, PreparationError}; +use crate::client::real_messages_control::real_traffic_stream::RealMessage; +use crate::client::replies::reply_controller::ReplyControllerSender; use client_connections::TransmissionLane; use futures::StreamExt; use log::*; -use nymsphinx::{ - acknowledgements::AckKey, addressing::clients::Recipient, preparer::MessagePreparer, -}; +use nymsphinx::addressing::clients::Recipient; +use nymsphinx::chunking::fragment::Fragment; +use nymsphinx::preparer::PreparedFragment; use rand::{CryptoRng, Rng}; use std::sync::{Arc, Weak}; // responsible for packet retransmission upon fired timer -pub(super) struct RetransmissionRequestListener -where - R: CryptoRng + Rng, -{ - ack_key: Arc, - ack_recipient: Recipient, - message_preparer: MessagePreparer, - action_sender: ActionSender, - real_message_sender: BatchRealMessageSender, +pub(super) struct RetransmissionRequestListener { + action_sender: AckActionSender, + message_handler: MessageHandler, request_receiver: RetransmissionRequestReceiver, - topology_access: TopologyAccessor, + reply_controller_sender: ReplyControllerSender, } impl RetransmissionRequestListener where R: CryptoRng + Rng, { - #[allow(clippy::too_many_arguments)] pub(super) fn new( - ack_key: Arc, - ack_recipient: Recipient, - message_preparer: MessagePreparer, - action_sender: ActionSender, - real_message_sender: BatchRealMessageSender, + action_sender: AckActionSender, + message_handler: MessageHandler, request_receiver: RetransmissionRequestReceiver, - topology_access: TopologyAccessor, + reply_controller_sender: ReplyControllerSender, ) -> Self { RetransmissionRequestListener { - ack_key, - ack_recipient, - message_preparer, action_sender, - real_message_sender, + message_handler, request_receiver, - topology_access, + reply_controller_sender, } } - async fn on_retransmission_request(&mut self, timed_out_ack: Weak) { - let timed_out_ack = match timed_out_ack.upgrade() { + async fn prepare_normal_retransmission_chunk( + &mut self, + packet_recipient: Recipient, + chunk_data: Fragment, + ) -> Result { + debug!("retransmitting normal packet..."); + + self.message_handler + .try_prepare_single_chunk_for_sending(packet_recipient, chunk_data) + .await + } + + async fn on_retransmission_request( + &mut self, + weak_timed_out_ack: Weak, + ) { + let timed_out_ack = match weak_timed_out_ack.upgrade() { Some(timed_out_ack) => timed_out_ack, None => { debug!("We received an ack JUST as we were about to retransmit [1]"); return; } }; - let packet_recipient = &timed_out_ack.recipient; - let chunk_clone = timed_out_ack.message_chunk.clone(); - let frag_id = chunk_clone.fragment_identifier(); - let topology_permit = self.topology_access.get_read_permit().await; - let topology_ref = match topology_permit - .try_get_valid_topology_ref(&self.ack_recipient, Some(packet_recipient)) - { - Some(topology_ref) => topology_ref, - None => { - warn!("Could not retransmit the packet - the network topology is invalid"); + let maybe_prepared_fragment = match &timed_out_ack.destination { + PacketDestination::Anonymous { + recipient_tag, + extra_surb_request, + } => { + // if this is retransmission for reply, offload it to the dedicated task + // that deals with all the surbs + return self.reply_controller_sender.send_retransmission_data( + *recipient_tag, + weak_timed_out_ack, + *extra_surb_request, + ); + } + PacketDestination::KnownRecipient(recipient) => { + self.prepare_normal_retransmission_chunk( + **recipient, + timed_out_ack.message_chunk.clone(), + ) + .await + } + }; + + let frag_id = timed_out_ack.message_chunk.fragment_identifier(); + + let prepared_fragment = match maybe_prepared_fragment { + Ok(prepared_fragment) => prepared_fragment, + Err(err) => { + warn!("Could not retransmit the packet - {err}"); // we NEED to start timer here otherwise we will have this guy permanently stuck in memory self.action_sender .unbounded_send(Action::new_start_timer(frag_id)) @@ -85,11 +104,6 @@ where } }; - let prepared_fragment = self - .message_preparer - .prepare_chunk_for_sending(chunk_clone, topology_ref, &self.ack_key, packet_recipient) - .unwrap(); - // if we have the ONLY strong reference to the ack data, it means it was removed from the // pending acks if Arc::strong_count(&timed_out_ack) == 1 { @@ -101,7 +115,6 @@ where // we no longer need the reference - let's drop it so that if somehow `UpdateTimer` action // reached the controller before this function terminated, the controller would not panic. drop(timed_out_ack); - let new_delay = prepared_fragment.total_delay; // We know this update will be reflected by the `StartTimer` Action performed when this @@ -116,19 +129,15 @@ where .unwrap(); // send to `OutQueueControl` to eventually send to the mix network - self.real_message_sender - .send(( + self.message_handler + .forward_messages( vec![RealMessage::new(prepared_fragment.mix_packet, frag_id)], TransmissionLane::Retransmission, - )) + ) .await - .expect("BatchRealMessageReceiver has stopped receiving!"); } - #[cfg(not(target_arch = "wasm32"))] pub(super) async fn run_with_shutdown(&mut self, mut shutdown: task::ShutdownListener) { - use std::time::Duration; - debug!("Started RetransmissionRequestListener with graceful shutdown support"); while !shutdown.is_shutdown() { @@ -145,18 +154,7 @@ where } } } - tokio::time::timeout(Duration::from_secs(5), shutdown.recv()) - .await - .expect("Task stopped without shutdown called"); + shutdown.recv_timeout().await; log::debug!("RetransmissionRequestListener: Exiting"); } - - #[cfg(target_arch = "wasm32")] - pub(super) async fn run(&mut self) { - debug!("Started RetransmissionRequestListener without graceful shutdown support"); - - while let Some(timed_out_ack) = self.request_receiver.next().await { - self.on_retransmission_request(timed_out_ack).await; - } - } } diff --git a/clients/client-core/src/client/real_messages_control/acknowledgement_control/sent_notification_listener.rs b/clients/client-core/src/client/real_messages_control/acknowledgement_control/sent_notification_listener.rs index 5206a602ce..c273182d59 100644 --- a/clients/client-core/src/client/real_messages_control/acknowledgement_control/sent_notification_listener.rs +++ b/clients/client-core/src/client/real_messages_control/acknowledgement_control/sent_notification_listener.rs @@ -1,7 +1,7 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use super::action_controller::{Action, ActionSender}; +use super::action_controller::{AckActionSender, Action}; use super::SentPacketNotificationReceiver; use futures::StreamExt; use log::*; @@ -13,13 +13,13 @@ use nymsphinx::chunking::fragment::{FragmentIdentifier, COVER_FRAG_ID}; /// accidentally fire retransmission way quicker than we should have. pub(super) struct SentNotificationListener { sent_notifier: SentPacketNotificationReceiver, - action_sender: ActionSender, + action_sender: AckActionSender, } impl SentNotificationListener { pub(super) fn new( sent_notifier: SentPacketNotificationReceiver, - action_sender: ActionSender, + action_sender: AckActionSender, ) -> Self { SentNotificationListener { sent_notifier, @@ -31,18 +31,12 @@ impl SentNotificationListener { if frag_id == COVER_FRAG_ID { trace!("sent off a cover message - no need to start retransmission timer!"); return; - } else if frag_id.is_reply() { - debug!("sent off a reply message - no need to start retransmission timer!"); - // TODO: probably there will need to be some extra procedure here, like it would - // be nice to know that our reply actually reached the recipient (i.e. we got the ack) - return; } self.action_sender .unbounded_send(Action::new_start_timer(frag_id)) .unwrap(); } - #[cfg(not(target_arch = "wasm32"))] pub(super) async fn run_with_shutdown(&mut self, mut shutdown: task::ShutdownListener) { debug!("Started SentNotificationListener with graceful shutdown support"); @@ -65,13 +59,4 @@ impl SentNotificationListener { assert!(shutdown.is_shutdown_poll()); log::debug!("SentNotificationListener: Exiting"); } - - #[cfg(target_arch = "wasm32")] - pub(super) async fn run(&mut self) { - debug!("Started SentNotificationListener without graceful shutdown support"); - - while let Some(frag_id) = self.sent_notifier.next().await { - self.on_sent_message(frag_id).await; - } - } } diff --git a/clients/client-core/src/client/real_messages_control/message_handler.rs b/clients/client-core/src/client/real_messages_control/message_handler.rs new file mode 100644 index 0000000000..1c80ef519f --- /dev/null +++ b/clients/client-core/src/client/real_messages_control/message_handler.rs @@ -0,0 +1,548 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::client::real_messages_control::acknowledgement_control::PendingAcknowledgement; +use crate::client::real_messages_control::real_traffic_stream::{ + BatchRealMessageSender, RealMessage, +}; +use crate::client::real_messages_control::{AckActionSender, Action}; +use crate::client::replies::reply_storage::{ReceivedReplySurbsMap, SentReplyKeys, UsedSenderTags}; +use crate::client::topology_control::{TopologyAccessor, TopologyReadPermit}; +use client_connections::TransmissionLane; +use log::{debug, error, info, trace, warn}; +use nymsphinx::acknowledgements::AckKey; +use nymsphinx::addressing::clients::Recipient; +use nymsphinx::anonymous_replies::requests::{AnonymousSenderTag, RepliableMessage, ReplyMessage}; +use nymsphinx::anonymous_replies::{ReplySurb, SurbEncryptionKey}; +use nymsphinx::chunking::fragment::{Fragment, FragmentIdentifier}; +use nymsphinx::message::NymMessage; +use nymsphinx::params::{PacketSize, DEFAULT_NUM_MIX_HOPS}; +use nymsphinx::preparer::{MessagePreparer, PreparedFragment}; +use nymsphinx::Delay; +use rand::{CryptoRng, Rng}; +use std::sync::Arc; +use std::time::Duration; +use thiserror::Error; +use topology::{NymTopology, NymTopologyError}; + +// TODO: move that error elsewhere since it seems to be contaminating different files +#[derive(Debug, Clone, Error)] +pub enum PreparationError { + #[error(transparent)] + NymTopologyError(#[from] NymTopologyError), + + #[error("The received message cannot be sent using a single reply surb. It ended up getting split into {fragments} fragments.")] + MessageTooLongForSingleSurb { fragments: usize }, + + #[error("Not enough reply SURBs to send the message. We have {available} available and require at least {required}.")] + NotEnoughSurbs { available: usize, required: usize }, +} + +impl PreparationError { + fn return_surbs(self, returned_surbs: Vec) -> SurbWrappedPreparationError { + SurbWrappedPreparationError { + source: self, + returned_surbs: Some(returned_surbs), + } + } +} + +#[derive(Debug, Error)] +#[error("Failed to prepare packets - {source}. {} reply surbs will be returned", .returned_surbs.as_ref().map(|s| s.len()).unwrap_or_default())] +pub struct SurbWrappedPreparationError { + #[source] + source: PreparationError, + + returned_surbs: Option>, +} + +impl From for SurbWrappedPreparationError +where + T: Into, +{ + fn from(err: T) -> Self { + SurbWrappedPreparationError { + source: err.into(), + returned_surbs: None, + } + } +} + +impl SurbWrappedPreparationError { + pub(crate) fn return_unused_surbs( + self, + surb_storage: &ReceivedReplySurbsMap, + target: &AnonymousSenderTag, + ) -> PreparationError { + if let Some(reply_surbs) = self.returned_surbs { + surb_storage.insert_surbs(target, reply_surbs) + } + self.source + } +} + +#[derive(Clone)] +pub(crate) struct Config { + /// Key used to decrypt contents of received SURBAcks + ack_key: Arc, + + /// Address of this client which also represent an address to which all acknowledgements + /// and surb-based are going to be sent. + sender_address: Recipient, + + /// Average delay a data packet is going to get delay at a single mixnode. + average_packet_delay: Duration, + + /// Average delay an acknowledgement packet is going to get delay at a single mixnode. + average_ack_delay: Duration, + + /// Number of mix hops each packet ('real' message, ack, reply) is expected to take. + /// Note that it does not include gateway hops. + num_mix_hops: u8, + + /// Predefined packet size used for the encapsulated messages. + packet_size: PacketSize, +} + +impl Config { + pub fn new( + ack_key: Arc, + sender_address: Recipient, + average_packet_delay: Duration, + average_ack_delay: Duration, + ) -> Self { + Config { + ack_key, + sender_address, + average_packet_delay, + average_ack_delay, + num_mix_hops: DEFAULT_NUM_MIX_HOPS, + packet_size: PacketSize::default(), + } + } + + /// Allows setting non-default number of expected mix hops in the network. + #[allow(dead_code)] + pub fn with_mix_hops(mut self, hops: u8) -> Self { + self.num_mix_hops = hops; + self + } + + /// Allows setting non-default size of the sphinx packets sent out. + pub fn with_custom_packet_size(mut self, packet_size: PacketSize) -> Self { + self.packet_size = packet_size; + self + } +} + +#[derive(Clone)] +pub(crate) struct MessageHandler { + config: Config, + rng: R, + message_preparer: MessagePreparer, + action_sender: AckActionSender, + real_message_sender: BatchRealMessageSender, + topology_access: TopologyAccessor, + reply_key_storage: SentReplyKeys, + tag_storage: UsedSenderTags, +} + +impl MessageHandler +where + R: CryptoRng + Rng, +{ + pub(crate) fn new( + config: Config, + rng: R, + action_sender: AckActionSender, + real_message_sender: BatchRealMessageSender, + topology_access: TopologyAccessor, + reply_key_storage: SentReplyKeys, + tag_storage: UsedSenderTags, + ) -> Self + where + R: Copy, + { + let message_preparer = MessagePreparer::new( + rng, + config.sender_address, + config.average_packet_delay, + config.average_ack_delay, + ) + .with_custom_real_message_packet_size(config.packet_size) + .with_mix_hops(config.num_mix_hops); + + MessageHandler { + config, + rng, + message_preparer, + action_sender, + real_message_sender, + topology_access, + reply_key_storage, + tag_storage, + } + } + + fn get_or_create_sender_tag(&mut self, recipient: &Recipient) -> AnonymousSenderTag { + if let Some(existing) = self.tag_storage.try_get_existing(recipient) { + trace!("we already had sender tag for {recipient}"); + existing + } else { + info!("creating new sender tag for {recipient}"); + let new_tag = AnonymousSenderTag::new_random(&mut self.rng); + self.tag_storage.insert_new(recipient, new_tag); + info!("we'll be using {new_tag} for all anonymous messages sent to {recipient}"); + new_tag + } + } + + fn get_topology<'a>( + &self, + permit: &'a TopologyReadPermit<'a>, + ) -> Result<&'a NymTopology, PreparationError> { + match permit.try_get_valid_topology_ref(&self.config.sender_address, None) { + Ok(topology_ref) => Ok(topology_ref), + Err(err) => { + warn!("Could not process the packet - the network topology is invalid - {err}"); + Err(err.into()) + } + } + } + + async fn generate_reply_surbs_with_keys( + &mut self, + amount: usize, + ) -> Result<(Vec, Vec), PreparationError> { + let topology_permit = self.topology_access.get_read_permit().await; + let topology = self.get_topology(&topology_permit)?; + + let reply_surbs = self + .message_preparer + .generate_reply_surbs(amount, topology)?; + + let reply_keys = reply_surbs + .iter() + .map(|s| *s.encryption_key()) + .collect::>(); + + Ok((reply_surbs, reply_keys)) + } + + pub(crate) async fn try_send_single_surb_message( + &mut self, + target: AnonymousSenderTag, + message: ReplyMessage, + reply_surb: ReplySurb, + is_extra_surb_request: bool, + ) -> Result<(), SurbWrappedPreparationError> { + let mut fragment = self + .message_preparer + .pad_and_split_message(NymMessage::new_reply(message)); + if fragment.len() > 1 { + // well, it's not a single surb message + return Err(SurbWrappedPreparationError { + source: PreparationError::MessageTooLongForSingleSurb { + fragments: fragment.len(), + }, + returned_surbs: Some(vec![reply_surb]), + }); + } + + let chunk = fragment.pop().unwrap(); + let chunk_clone = chunk.clone(); + let prepared_fragment = self + .try_prepare_single_reply_chunk_for_sending(reply_surb, chunk_clone) + .await?; + + let real_messages = + RealMessage::new(prepared_fragment.mix_packet, chunk.fragment_identifier()); + let delay = prepared_fragment.total_delay; + let pending_ack = + PendingAcknowledgement::new_anonymous(chunk, delay, target, is_extra_surb_request); + + let lane = if is_extra_surb_request { + TransmissionLane::ReplySurbRequest + } else { + TransmissionLane::General + }; + + self.forward_messages(vec![real_messages], lane).await; + self.insert_pending_acks(vec![pending_ack]); + Ok(()) + } + + pub(crate) async fn try_request_additional_reply_surbs( + &mut self, + from: AnonymousSenderTag, + reply_surb: ReplySurb, + amount: u32, + ) -> Result<(), SurbWrappedPreparationError> { + debug!("requesting {amount} reply SURBs from {from:?}"); + + let surbs_request = + ReplyMessage::new_surb_request_message(self.config.sender_address, amount); + self.try_send_single_surb_message(from, surbs_request, reply_surb, true) + .await + } + + // // TODO: this will require additional argument to make it use different variant of `ReplyMessage` + pub(crate) fn split_reply_message(&mut self, message: Vec) -> Vec { + self.message_preparer + .pad_and_split_message(NymMessage::new_reply(ReplyMessage::new_data_message( + message, + ))) + } + + // the only difference between this method and `try_send_reply_chunks` is that + // here we are not creating acks as acks are already in memory waiting to get cleared. + // we are only updating their existing delays + pub(crate) async fn try_send_retransmission_reply_chunks( + &mut self, + fragments: Vec, + reply_surbs: Vec, + lane: TransmissionLane, + ) -> Result<(), SurbWrappedPreparationError> { + let prepared_fragments = self + .prepare_reply_chunks_for_sending(fragments.clone(), reply_surbs) + .await?; + + let mut real_messages = Vec::with_capacity(prepared_fragments.len()); + + for prepared in prepared_fragments { + self.update_ack_delay(prepared.fragment_identifier, prepared.total_delay); + real_messages.push(prepared.into()) + } + + self.forward_messages(real_messages, lane).await; + Ok(()) + } + + pub(crate) async fn try_send_reply_chunks( + &mut self, + target: AnonymousSenderTag, + fragments: Vec, + reply_surbs: Vec, + lane: TransmissionLane, + ) -> Result<(), SurbWrappedPreparationError> { + let prepared_fragments = self + .prepare_reply_chunks_for_sending(fragments.clone(), reply_surbs) + .await?; + + let mut pending_acks = Vec::with_capacity(fragments.len()); + let mut real_messages = Vec::with_capacity(fragments.len()); + + for (raw, prepared) in fragments.into_iter().zip(prepared_fragments.into_iter()) { + let real_message = RealMessage::new(prepared.mix_packet, prepared.fragment_identifier); + let delay = prepared.total_delay; + let pending_ack = PendingAcknowledgement::new_anonymous(raw, delay, target, false); + + real_messages.push(real_message); + pending_acks.push(pending_ack); + } + + self.forward_messages(real_messages, lane).await; + self.insert_pending_acks(pending_acks); + Ok(()) + } + + pub(crate) async fn try_send_plain_message( + &mut self, + recipient: Recipient, + message: Vec, + lane: TransmissionLane, + ) -> Result<(), PreparationError> { + let message = NymMessage::new_plain(message); + self.try_split_and_send_non_reply_message(message, recipient, lane) + .await + } + + pub(crate) async fn try_split_and_send_non_reply_message( + &mut self, + message: NymMessage, + recipient: Recipient, + lane: TransmissionLane, + ) -> Result<(), PreparationError> { + // TODO: I really dislike existence of this assertion, it implies code has to be re-organised + debug_assert!(!matches!(message, NymMessage::Reply(_))); + + // TODO2: it's really annoying we have to get topology permit again here due to borrow-checker + let topology_permit = self.topology_access.get_read_permit().await; + let topology = self.get_topology(&topology_permit)?; + + let fragments = self.message_preparer.pad_and_split_message(message); + + let mut pending_acks = Vec::with_capacity(fragments.len()); + let mut real_messages = Vec::with_capacity(fragments.len()); + for fragment in fragments { + // we need to clone it because we need to keep it in memory in case we had to retransmit + // it. And then we'd need to recreate entire ACK again. + let chunk_clone = fragment.clone(); + let prepared_fragment = self.message_preparer.prepare_chunk_for_sending( + chunk_clone, + topology, + &self.config.ack_key, + &recipient, + )?; + + let real_message = + RealMessage::new(prepared_fragment.mix_packet, fragment.fragment_identifier()); + let delay = prepared_fragment.total_delay; + let pending_ack = PendingAcknowledgement::new_known(fragment, delay, recipient); + + real_messages.push(real_message); + pending_acks.push(pending_ack); + } + + self.insert_pending_acks(pending_acks); + self.forward_messages(real_messages, lane).await; + + Ok(()) + } + + pub(crate) async fn try_send_additional_reply_surbs( + &mut self, + recipient: Recipient, + amount: u32, + ) -> Result<(), PreparationError> { + let sender_tag = self.get_or_create_sender_tag(&recipient); + let (reply_surbs, reply_keys) = + self.generate_reply_surbs_with_keys(amount as usize).await?; + + let message = NymMessage::new_repliable(RepliableMessage::new_additional_surbs( + sender_tag, + reply_surbs, + )); + + self.try_split_and_send_non_reply_message( + message, + recipient, + TransmissionLane::AdditionalReplySurbs, + ) + .await?; + + log::trace!("storing {} reply keys", reply_keys.len()); + self.reply_key_storage.insert_multiple(reply_keys); + + Ok(()) + } + + pub(crate) async fn try_send_message_with_reply_surbs( + &mut self, + recipient: Recipient, + message: Vec, + num_reply_surbs: u32, + lane: TransmissionLane, + ) -> Result<(), SurbWrappedPreparationError> { + let sender_tag = self.get_or_create_sender_tag(&recipient); + let (reply_surbs, reply_keys) = self + .generate_reply_surbs_with_keys(num_reply_surbs as usize) + .await?; + + let message = + NymMessage::new_repliable(RepliableMessage::new_data(message, sender_tag, reply_surbs)); + + self.try_split_and_send_non_reply_message(message, recipient, lane) + .await?; + + log::trace!("storing {} reply keys", reply_keys.len()); + self.reply_key_storage.insert_multiple(reply_keys); + + Ok(()) + } + + pub(crate) async fn try_prepare_single_chunk_for_sending( + &mut self, + recipient: Recipient, + chunk: Fragment, + ) -> Result { + let topology_permit = self.topology_access.get_read_permit().await; + let topology = self.get_topology(&topology_permit)?; + + let prepared_fragment = self + .message_preparer + .prepare_chunk_for_sending(chunk, topology, &self.config.ack_key, &recipient) + .unwrap(); + + Ok(prepared_fragment) + } + + async fn prepare_reply_chunks_for_sending( + &mut self, + fragments: Vec, + reply_surbs: Vec, + ) -> Result, SurbWrappedPreparationError> { + debug_assert_ne!( + fragments.len(), + reply_surbs.len(), + "attempted to send {} fragments with {} reply surbs", + fragments.len(), + reply_surbs.len() + ); + + let topology_permit = self.topology_access.get_read_permit().await; + let topology = match self.get_topology(&topology_permit) { + Ok(topology) => topology, + Err(err) => return Err(err.return_surbs(reply_surbs)), + }; + + Ok(fragments + .into_iter() + .zip(reply_surbs.into_iter()) + .map(|(fragment, reply_surb)| { + // unwrap here is fine as we know we have a valid topology + self.message_preparer + .prepare_reply_chunk_for_sending( + fragment, + topology, + &self.config.ack_key, + reply_surb, + ) + .unwrap() + }) + .collect()) + } + + pub(crate) async fn try_prepare_single_reply_chunk_for_sending( + &mut self, + reply_surb: ReplySurb, + chunk: Fragment, + ) -> Result { + let topology_permit = self.topology_access.get_read_permit().await; + let topology = match self.get_topology(&topology_permit) { + Ok(topology) => topology, + Err(err) => return Err(err.return_surbs(vec![reply_surb])), + }; + + let prepared_fragment = self + .message_preparer + .prepare_reply_chunk_for_sending(chunk, topology, &self.config.ack_key, reply_surb) + .unwrap(); + + Ok(prepared_fragment) + } + + pub(crate) fn update_ack_delay(&self, id: FragmentIdentifier, new_delay: Delay) { + self.action_sender + .unbounded_send(Action::UpdateDelay(id, new_delay)) + .expect("action control task has died") + } + + pub(crate) fn insert_pending_acks(&self, pending_acks: Vec) { + self.action_sender + .unbounded_send(Action::new_insert(pending_acks)) + .expect("action control task has died") + } + + // tells real message sender (with the poisson timer) to send this to the mix network + pub(crate) async fn forward_messages( + &self, + messages: Vec, + transmission_lane: TransmissionLane, + ) { + self.real_message_sender + .send((messages, transmission_lane)) + .await + .expect("real message receiver task (OutQueueControl) has died"); + } +} diff --git a/clients/client-core/src/client/real_messages_control/mod.rs b/clients/client-core/src/client/real_messages_control/mod.rs index ffeeea61e3..8789cbdf6a 100644 --- a/clients/client-core/src/client/real_messages_control/mod.rs +++ b/clients/client-core/src/client/real_messages_control/mod.rs @@ -8,6 +8,11 @@ use self::{ acknowledgement_control::AcknowledgementController, real_traffic_stream::OutQueueControl, }; +use crate::client::real_messages_control::message_handler::MessageHandler; +use crate::client::replies::reply_controller::{ + ReplyController, ReplyControllerReceiver, ReplyControllerSender, +}; +use crate::client::replies::reply_storage::CombinedReplyStorage; use crate::{ client::{ inbound_messages::InputMessageReceiver, mix_traffic::BatchMixMessageSender, @@ -27,11 +32,13 @@ use rand::{rngs::OsRng, CryptoRng, Rng}; use std::sync::Arc; use std::time::Duration; -#[cfg(feature = "reply-surb")] -use crate::client::reply_key_storage::ReplyKeyStorage; +use crate::client::replies::reply_controller; +use crate::config; +pub(crate) use acknowledgement_control::{AckActionSender, Action}; -mod acknowledgement_control; -mod real_traffic_stream; +pub(crate) mod acknowledgement_control; +pub(crate) mod message_handler; +pub(crate) mod real_traffic_stream; // TODO: ack_key and self_recipient shouldn't really be part of this config pub struct Config { @@ -62,31 +69,102 @@ pub struct Config { /// Predefined packet size used for the encapsulated messages. packet_size: PacketSize, + + /// Defines the minimum number of reply surbs the client would request. + minimum_reply_surb_request_size: u32, + + /// Defines the maximum number of reply surbs the client would request. + maximum_reply_surb_request_size: u32, + + /// Defines the maximum number of reply surbs a remote party is allowed to request from this client at once. + maximum_allowed_reply_surb_request_size: u32, + + /// Defines maximum amount of time the client is going to wait for reply surbs before explicitly asking + /// for more even though in theory they wouldn't need to. + maximum_reply_surb_waiting_period: Duration, + + /// Defines maximum amount of time given reply surb is going to be valid for. + /// This is going to be superseded by key rotation once implemented. + maximum_reply_surb_age: Duration, + + /// Defines maximum amount of time given reply key is going to be valid for. + /// This is going to be superseded by key rotation once implemented. + maximum_reply_key_age: Duration, +} + +impl<'a> From<&'a Config> for acknowledgement_control::Config { + fn from(cfg: &'a Config) -> Self { + acknowledgement_control::Config::new(cfg.ack_wait_addition, cfg.ack_wait_multiplier) + .with_custom_packet_size(cfg.packet_size) + } +} + +impl<'a> From<&'a Config> for real_traffic_stream::Config { + fn from(cfg: &'a Config) -> Self { + real_traffic_stream::Config::new( + Arc::clone(&cfg.ack_key), + cfg.self_recipient, + cfg.average_ack_delay_duration, + cfg.average_packet_delay_duration, + cfg.average_message_sending_delay, + cfg.disable_main_poisson_packet_distribution, + ) + .with_custom_cover_packet_size(cfg.packet_size) + } +} + +impl<'a> From<&'a Config> for reply_controller::Config { + fn from(cfg: &'a Config) -> Self { + reply_controller::Config::new( + cfg.minimum_reply_surb_request_size, + cfg.maximum_reply_surb_request_size, + cfg.maximum_allowed_reply_surb_request_size, + cfg.maximum_reply_surb_waiting_period, + cfg.maximum_reply_surb_age, + cfg.maximum_reply_key_age, + ) + } +} + +impl<'a> From<&'a Config> for message_handler::Config { + fn from(cfg: &'a Config) -> Self { + message_handler::Config::new( + Arc::clone(&cfg.ack_key), + cfg.self_recipient, + cfg.average_packet_delay_duration, + cfg.average_ack_delay_duration, + ) + .with_custom_packet_size(cfg.packet_size) + } } impl Config { - // TODO: change the config into a builder - #[allow(clippy::too_many_arguments)] pub fn new( + base_client_debug_config: &config::DebugConfig, ack_key: Arc, - ack_wait_multiplier: f64, - ack_wait_addition: Duration, - average_ack_delay_duration: Duration, - average_message_sending_delay: Duration, - average_packet_delay_duration: Duration, - disable_main_poisson_packet_distribution: bool, self_recipient: Recipient, ) -> Self { Config { ack_key, - ack_wait_addition, - ack_wait_multiplier, self_recipient, - average_message_sending_delay, - average_packet_delay_duration, - average_ack_delay_duration, - disable_main_poisson_packet_distribution, packet_size: Default::default(), + ack_wait_addition: base_client_debug_config.ack_wait_addition, + ack_wait_multiplier: base_client_debug_config.ack_wait_multiplier, + average_message_sending_delay: base_client_debug_config.message_sending_average_delay, + average_packet_delay_duration: base_client_debug_config.average_packet_delay, + average_ack_delay_duration: base_client_debug_config.average_ack_delay, + disable_main_poisson_packet_distribution: base_client_debug_config + .disable_main_poisson_packet_distribution, + minimum_reply_surb_request_size: base_client_debug_config + .minimum_reply_surb_request_size, + maximum_reply_surb_request_size: base_client_debug_config + .maximum_reply_surb_request_size, + maximum_allowed_reply_surb_request_size: base_client_debug_config + .maximum_allowed_reply_surb_request_size, + maximum_reply_surb_waiting_period: base_client_debug_config + .maximum_reply_surb_waiting_period, + maximum_reply_surb_age: base_client_debug_config.maximum_reply_surb_age, + maximum_reply_key_age: base_client_debug_config.maximum_reply_key_age, } } @@ -95,75 +173,84 @@ impl Config { } } -pub struct RealMessagesController +pub(crate) struct RealMessagesController where R: CryptoRng + Rng, { out_queue_control: OutQueueControl, ack_control: AcknowledgementController, + reply_control: ReplyController, } // obviously when we finally make shared rng that is on 'higher' level, this should become // generic `R` impl RealMessagesController { #[allow(clippy::too_many_arguments)] - pub fn new( + pub(crate) fn new( config: Config, ack_receiver: AcknowledgementReceiver, input_receiver: InputMessageReceiver, mix_sender: BatchMixMessageSender, topology_access: TopologyAccessor, - #[cfg(feature = "reply-surb")] reply_key_storage: ReplyKeyStorage, + reply_storage: CombinedReplyStorage, + // so much refactoring needed, but this is temporary just to test things out + reply_controller_sender: ReplyControllerSender, + reply_controller_receiver: ReplyControllerReceiver, lane_queue_lengths: LaneQueueLengths, client_connection_rx: ConnectionCommandReceiver, ) -> Self { let rng = OsRng; + // create channels for inter-task communication let (real_message_sender, real_message_receiver) = tokio::sync::mpsc::channel(1); let (sent_notifier_tx, sent_notifier_rx) = mpsc::unbounded(); - + let (ack_action_tx, ack_action_rx) = mpsc::unbounded(); let ack_controller_connectors = AcknowledgementControllerConnectors::new( - real_message_sender, input_receiver, sent_notifier_rx, ack_receiver, + ack_action_tx.clone(), + ack_action_rx, ); - let ack_control_config = acknowledgement_control::Config::new( - config.ack_wait_addition, - config.ack_wait_multiplier, - config.average_ack_delay_duration, - config.average_packet_delay_duration, - ) - .with_custom_packet_size(config.packet_size); + // create all configs for the components + let ack_control_config = (&config).into(); + let out_queue_config = (&config).into(); + let reply_controller_config = (&config).into(); + let message_handler_config = (&config).into(); + + // create the actual components + let message_handler = MessageHandler::new( + message_handler_config, + rng, + ack_action_tx, + real_message_sender, + topology_access.clone(), + reply_storage.key_storage(), + reply_storage.tags_storage(), + ); let ack_control = AcknowledgementController::new( ack_control_config, - rng, - topology_access.clone(), Arc::clone(&config.ack_key), - config.self_recipient, ack_controller_connectors, - #[cfg(feature = "reply-surb")] - reply_key_storage, + message_handler.clone(), + reply_controller_sender, ); - let out_queue_config = real_traffic_stream::Config::new( - config.average_ack_delay_duration, - config.average_packet_delay_duration, - config.average_message_sending_delay, - config.disable_main_poisson_packet_distribution, - ) - .with_custom_cover_packet_size(config.packet_size); + let reply_control = ReplyController::new( + reply_controller_config, + message_handler, + reply_storage, + reply_controller_receiver, + ); let out_queue_control = OutQueueControl::new( out_queue_config, - Arc::clone(&config.ack_key), + rng, sent_notifier_tx, mix_sender, real_message_receiver, - rng, - config.self_recipient, topology_access, lane_queue_lengths, client_connection_rx, @@ -172,31 +259,26 @@ impl RealMessagesController { RealMessagesController { out_queue_control, ack_control, + reply_control, } } - #[cfg(not(target_arch = "wasm32"))] pub fn start_with_shutdown(self, shutdown: task::ShutdownListener) { let mut out_queue_control = self.out_queue_control; let ack_control = self.ack_control; + let mut reply_control = self.reply_control; let shutdown_handle = shutdown.clone(); spawn_future(async move { out_queue_control.run_with_shutdown(shutdown_handle).await; debug!("The out queue controller has finished execution!"); }); + let shutdown_handle = shutdown.clone(); + spawn_future(async move { + reply_control.run_with_shutdown(shutdown_handle).await; + debug!("The reply controller has finished execution!"); + }); + ack_control.start_with_shutdown(shutdown); } - - #[cfg(target_arch = "wasm32")] - pub fn start(self) { - let mut out_queue_control = self.out_queue_control; - let ack_control = self.ack_control; - - spawn_future(async move { - out_queue_control.run().await; - debug!("The out queue controller has finished execution!"); - }); - ack_control.start(); - } } diff --git a/clients/client-core/src/client/real_messages_control/real_traffic_stream.rs b/clients/client-core/src/client/real_messages_control/real_traffic_stream.rs index 5b9bf6ed5b..cb74fe674b 100644 --- a/clients/client-core/src/client/real_messages_control/real_traffic_stream.rs +++ b/clients/client-core/src/client/real_messages_control/real_traffic_stream.rs @@ -16,6 +16,7 @@ use nymsphinx::chunking::fragment::FragmentIdentifier; use nymsphinx::cover::generate_loop_cover_packet; use nymsphinx::forwarding::packet::MixPacket; use nymsphinx::params::PacketSize; +use nymsphinx::preparer::PreparedFragment; use nymsphinx::utils::sample_poisson_duration; use rand::{CryptoRng, Rng}; use std::pin::Pin; @@ -47,6 +48,12 @@ fn get_time_now() -> wasm_timer::Instant { /// Configurable parameters of the `OutQueueControl` pub(crate) struct Config { + /// Key used to encrypt and decrypt content of an ACK packet. + ack_key: Arc, + + /// Represents full address of this client. + our_full_destination: Recipient, + /// Average delay an acknowledgement packet is going to get delay at a single mixnode. average_ack_delay: Duration, @@ -66,12 +73,16 @@ pub(crate) struct Config { impl Config { pub(crate) fn new( + ack_key: Arc, + our_full_destination: Recipient, average_ack_delay: Duration, average_packet_delay: Duration, average_message_sending_delay: Duration, disable_poisson_packet_distribution: bool, ) -> Self { Config { + ack_key, + our_full_destination, average_ack_delay, average_packet_delay, average_message_sending_delay, @@ -93,9 +104,6 @@ where /// Configurable parameters of the `ActionController` config: Config, - /// Key used to encrypt and decrypt content of an ACK packet. - ack_key: Arc, - /// Channel used for notifying of a real packet being sent out. Used to start up retransmission timer. sent_notifier: SentPacketNotificationSender, @@ -119,9 +127,6 @@ where /// before being sent out into the network. real_receiver: BatchRealMessageReceiver, - /// Represents full address of this client. - our_full_destination: Recipient, - /// Instance of a cryptographically secure random number generator. rng: R, @@ -144,6 +149,16 @@ where pub(crate) struct RealMessage { mix_packet: MixPacket, fragment_id: FragmentIdentifier, + // TODO: add info about it being constructed with reply-surb +} + +impl From for RealMessage { + fn from(fragment: PreparedFragment) -> Self { + RealMessage { + mix_packet: fragment.mix_packet, + fragment_id: fragment.fragment_identifier, + } + } } impl RealMessage { @@ -175,25 +190,21 @@ where #[allow(clippy::too_many_arguments)] pub(crate) fn new( config: Config, - ack_key: Arc, + rng: R, sent_notifier: SentPacketNotificationSender, mix_tx: BatchMixMessageSender, real_receiver: BatchRealMessageReceiver, - rng: R, - our_full_destination: Recipient, topology_access: TopologyAccessor, lane_queue_lengths: LaneQueueLengths, client_connection_rx: ConnectionCommandReceiver, ) -> Self { OutQueueControl { config, - ack_key, sent_notifier, next_delay: None, sending_delay_controller: Default::default(), mix_tx, real_receiver, - our_full_destination, rng, topology_access, transmission_buffer: Default::default(), @@ -220,24 +231,23 @@ where // poisson delay, but is it really a problem? let topology_permit = self.topology_access.get_read_permit().await; // the ack is sent back to ourselves (and then ignored) - let topology_ref_option = topology_permit.try_get_valid_topology_ref( - &self.our_full_destination, - Some(&self.our_full_destination), - ); - if topology_ref_option.is_none() { - warn!( - "No valid topology detected - won't send any loop cover message this time" - ); - return; - } - let topology_ref = topology_ref_option.unwrap(); + let topology_ref = match topology_permit.try_get_valid_topology_ref( + &self.config.our_full_destination, + Some(&self.config.our_full_destination), + ) { + Ok(topology) => topology, + Err(err) => { + warn!("We're not going to send any loop cover message this time, as the current topology seem to be invalid - {err}"); + return; + } + }; ( generate_loop_cover_packet( &mut self.rng, topology_ref, - &self.ack_key, - &self.our_full_destination, + &self.config.ack_key, + &self.config.our_full_destination, self.config.average_ack_delay, self.config.average_packet_delay, self.config.cover_packet_size, @@ -489,53 +499,64 @@ where fn log_status_infrequent(&self) { if self.sending_delay_controller.current_multiplier() > 1 { log::warn!( - "Unable to send packets fast enough - sending delay multiplier set to: {}", + "Unable to send packets at the default rate - rate reduced by setting the delay multiplier set to: {}", self.sending_delay_controller.current_multiplier() ); } } - #[cfg(not(target_arch = "wasm32"))] pub(super) async fn run_with_shutdown(&mut self, mut shutdown: task::ShutdownListener) { debug!("Started OutQueueControl with graceful shutdown support"); - let mut status_timer = tokio::time::interval(Duration::from_secs(5)); - let mut infrequent_status_timer = tokio::time::interval(Duration::from_secs(60)); + #[cfg(not(target_arch = "wasm32"))] + { + let mut status_timer = tokio::time::interval(Duration::from_secs(5)); + let mut infrequent_status_timer = tokio::time::interval(Duration::from_secs(60)); - while !shutdown.is_shutdown() { - tokio::select! { - biased; - _ = shutdown.recv() => { - log::trace!("OutQueueControl: Received shutdown"); + while !shutdown.is_shutdown() { + tokio::select! { + biased; + _ = shutdown.recv() => { + log::trace!("OutQueueControl: Received shutdown"); + } + _ = status_timer.tick() => { + self.log_status(); + } + _ = infrequent_status_timer.tick() => { + self.log_status_infrequent(); + } + next_message = self.next() => if let Some(next_message) = next_message { + self.on_message(next_message).await; + } else { + log::trace!("OutQueueControl: Stopping since channel closed"); + break; + } } - _ = status_timer.tick() => { - self.log_status(); - } - _ = infrequent_status_timer.tick() => { - self.log_status_infrequent(); - } - next_message = self.next() => if let Some(next_message) = next_message { - self.on_message(next_message).await; - } else { - log::trace!("OutQueueControl: Stopping since channel closed"); - break; + } + tokio::time::timeout(Duration::from_secs(5), shutdown.recv()) + .await + .expect("Task stopped without shutdown called"); + } + + #[cfg(target_arch = "wasm32")] + { + while !shutdown.is_shutdown() { + tokio::select! { + biased; + _ = shutdown.recv() => { + log::trace!("OutQueueControl: Received shutdown"); + } + next_message = self.next() => if let Some(next_message) = next_message { + self.on_message(next_message).await; + } else { + log::trace!("OutQueueControl: Stopping since channel closed"); + break; + } } } } - tokio::time::timeout(Duration::from_secs(5), shutdown.recv()) - .await - .expect("Task stopped without shutdown called"); log::debug!("OutQueueControl: Exiting"); } - - #[cfg(target_arch = "wasm32")] - pub(super) async fn run(&mut self) { - debug!("Started OutQueueControl without graceful shutdown support"); - - while let Some(next_message) = self.next().await { - self.on_message(next_message).await; - } - } } impl Stream for OutQueueControl diff --git a/clients/client-core/src/client/real_messages_control/real_traffic_stream/transmission_buffer.rs b/clients/client-core/src/client/real_messages_control/real_traffic_stream/transmission_buffer.rs index b47aaf470e..20c7ce6cb7 100644 --- a/clients/client-core/src/client/real_messages_control/real_traffic_stream/transmission_buffer.rs +++ b/clients/client-core/src/client/real_messages_control/real_traffic_stream/transmission_buffer.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use client_connections::TransmissionLane; -use rand::seq::SliceRandom; +use rand::{seq::SliceRandom, Rng}; use std::{ collections::{HashMap, HashSet, VecDeque}, time::Duration, @@ -116,9 +116,15 @@ impl TransmissionBuffer { lanes.choose(&mut rand::thread_rng()).copied() } + // 2/3 chance to pick from the old lanes fn pick_random_old_lane(&self) -> Option { - let lanes = self.get_oldest_set(); - lanes.choose(&mut rand::thread_rng()).copied() + let rand = &mut rand::thread_rng(); + if rand.gen_ratio(2, 3) { + let lanes = self.get_oldest_set(); + lanes.choose(rand).copied() + } else { + self.pick_random_lane().copied() + } } fn pop_front_from_lane(&mut self, lane: &TransmissionLane) -> Option { diff --git a/clients/client-core/src/client/received_buffer.rs b/clients/client-core/src/client/received_buffer.rs index 7e5a0d7db8..2e7876ae62 100644 --- a/clients/client-core/src/client/received_buffer.rs +++ b/clients/client-core/src/client/received_buffer.rs @@ -1,26 +1,26 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use crate::client::replies::reply_controller::ReplyControllerSender; +use crate::client::replies::reply_storage::SentReplyKeys; use crate::spawn_future; use crypto::asymmetric::encryption; +use crypto::Digest; use futures::channel::mpsc; use futures::lock::Mutex; use futures::StreamExt; use gateway_client::MixnetMessageReceiver; use log::*; +use nymsphinx::anonymous_replies::requests::{ + RepliableMessage, RepliableMessageContent, ReplyMessage, ReplyMessageContent, +}; +use nymsphinx::anonymous_replies::{encryption_key::EncryptionKeyDigest, SurbEncryptionKey}; +use nymsphinx::message::{NymMessage, PlainMessage}; +use nymsphinx::params::ReplySurbKeyDigestAlgorithm; use nymsphinx::receiver::{MessageReceiver, MessageRecoveryError, ReconstructedMessage}; use std::collections::HashSet; use std::sync::Arc; -#[cfg(feature = "reply-surb")] -use crate::client::reply_key_storage::ReplyKeyStorage; -#[cfg(feature = "reply-surb")] -use crypto::{symmetric::stream_cipher, Digest}; -#[cfg(feature = "reply-surb")] -use nymsphinx::anonymous_replies::{encryption_key::EncryptionKeyDigest, SurbEncryptionKey}; -#[cfg(feature = "reply-surb")] -use nymsphinx::params::{ReplySurbEncryptionAlgorithm, ReplySurbKeyDigestAlgorithm}; - // Buffer Requests to say "hey, send any reconstructed messages to this channel" // or to say "hey, I'm going offline, don't send anything more to me. Just buffer them instead" pub type ReceivedBufferRequestSender = mpsc::UnboundedSender; @@ -46,26 +46,15 @@ struct ReceivedMessagesBufferInner { } impl ReceivedMessagesBufferInner { - fn process_received_fragment(&mut self, raw_fragment: Vec) -> Option { - let fragment_data = match self - .message_receiver - .recover_plaintext(self.local_encryption_keypair.private_key(), raw_fragment) - { - Err(e) => { - warn!("failed to recover fragment data: {:?}. The whole underlying message might be corrupted and unrecoverable!", e); - return None; - } - Ok(frag_data) => frag_data, - }; - - if nymsphinx::cover::is_cover(&fragment_data) { + fn recover_from_fragment(&mut self, fragment_data: &[u8]) -> Option { + if nymsphinx::cover::is_cover(fragment_data) { trace!("The message was a loop cover message! Skipping it"); return None; } - let fragment = match self.message_receiver.recover_fragment(&fragment_data) { - Err(e) => { - warn!("failed to recover fragment from raw data: {:?}. The whole underlying message might be corrupted and unrecoverable!", e); + let fragment = match self.message_receiver.recover_fragment(fragment_data) { + Err(err) => { + warn!("failed to recover fragment from raw data: {err}. The whole underlying message might be corrupted and unrecoverable!"); return None; } Ok(frag) => frag, @@ -79,9 +68,10 @@ impl ReceivedMessagesBufferInner { // if we returned an error the underlying message is malformed in some way match self.message_receiver.insert_new_fragment(fragment) { Err(err) => match err { - MessageRecoveryError::MalformedReconstructedMessage(message_sets) => { + MessageRecoveryError::MalformedReconstructedMessage { source, used_sets } => { + error!("message reconstruction failed - {source}. Attempting to re-use the message sets..."); // TODO: should we really insert reconstructed sets? could this be abused for some attack? - for set_id in message_sets { + for set_id in used_sets { if !self.recently_reconstructed.insert(set_id) { // or perhaps we should even panic at this point? error!("Reconstructed another message containing already used set id!") @@ -107,6 +97,34 @@ impl ReceivedMessagesBufferInner { }, } } + + fn process_received_reply( + &mut self, + reply_ciphertext: &mut [u8], + reply_key: SurbEncryptionKey, + ) -> Option { + // note: this performs decryption IN PLACE without extra allocation + self.message_receiver + .recover_plaintext_from_reply(reply_ciphertext, reply_key); + let fragment_data = reply_ciphertext; + + self.recover_from_fragment(fragment_data) + } + + fn process_received_regular_packet(&mut self, mut raw_fragment: Vec) -> Option { + let fragment_data = match self.message_receiver.recover_plaintext_from_regular_packet( + self.local_encryption_keypair.private_key(), + &mut raw_fragment, + ) { + Err(err) => { + warn!("failed to recover fragment data: {err}. The whole underlying message might be corrupted and unrecoverable!"); + return None; + } + Ok(frag_data) => frag_data, + }; + + self.recover_from_fragment(fragment_data) + } } #[derive(Debug, Clone)] @@ -114,17 +132,15 @@ impl ReceivedMessagesBufferInner { // You should always use .clone() to create additional instances struct ReceivedMessagesBuffer { inner: Arc>, - - /// Storage containing keys to all [`ReplySURB`]s ever sent out that we did not receive back. - // There's no need to put it behind a Mutex since it's already properly concurrent - #[cfg(feature = "reply-surb")] - reply_key_storage: ReplyKeyStorage, + reply_key_storage: SentReplyKeys, + reply_controller_sender: ReplyControllerSender, } impl ReceivedMessagesBuffer { fn new( local_encryption_keypair: Arc, - #[cfg(feature = "reply-surb")] reply_key_storage: ReplyKeyStorage, + reply_key_storage: SentReplyKeys, + reply_controller_sender: ReplyControllerSender, ) -> Self { ReceivedMessagesBuffer { inner: Arc::new(Mutex::new(ReceivedMessagesBufferInner { @@ -134,8 +150,8 @@ impl ReceivedMessagesBuffer { message_sender: None, recently_reconstructed: HashSet::new(), })), - #[cfg(feature = "reply-surb")] reply_key_storage, + reply_controller_sender, } } @@ -177,34 +193,139 @@ impl ReceivedMessagesBuffer { guard.message_sender = Some(sender); } - async fn add_reconstructed_messages(&mut self, msgs: Vec) { - debug!("Adding {:?} new messages to the buffer!", msgs.len()); - trace!("Adding new messages to the buffer! {:?}", msgs); - self.inner.lock().await.messages.extend(msgs) + fn handle_reconstructed_plain_messages( + &mut self, + msgs: Vec, + ) -> Vec { + msgs.into_iter().map(Into::into).collect() } - #[cfg(feature = "reply-surb")] - fn process_received_reply( - reply_ciphertext: &[u8], - reply_key: SurbEncryptionKey, - ) -> Option { - let zero_iv = stream_cipher::zero_iv::(); + fn handle_reconstructed_repliable_messages( + &mut self, + msgs: Vec, + ) -> Vec { + let mut reconstructed = Vec::new(); + for msg in msgs { + let (reply_surbs, from_surb_request) = match msg.content { + RepliableMessageContent::Data { + message, + reply_surbs, + } => { + trace!( + "received message that also contained additional {} reply surbs from {:?}!", + reply_surbs.len(), + msg.sender_tag + ); - let mut reply_msg = stream_cipher::decrypt::( - reply_key.inner(), - &zero_iv, - reply_ciphertext, - ); - if let Err(err) = MessageReceiver::remove_padding(&mut reply_msg) { - warn!("Received reply had malformed padding! - {:?}", err); - None - } else { - // TODO: perhaps having to say it doesn't have a surb an indication the type should be changed? - Some(ReconstructedMessage { - message: reply_msg, - reply_surb: None, - }) + reconstructed.push(ReconstructedMessage::new(message, msg.sender_tag)); + + (reply_surbs, false) + } + RepliableMessageContent::AdditionalSurbs { reply_surbs } => { + trace!( + "received additional {} reply surbs from {:?}!", + reply_surbs.len(), + msg.sender_tag + ); + (reply_surbs, true) + } + RepliableMessageContent::Heartbeat { + additional_reply_surbs, + } => { + error!("received a repliable heartbeat message - we don't know how to handle it yet (and we won't know until future PRs)"); + (additional_reply_surbs, false) + } + }; + + self.reply_controller_sender.send_additional_surbs( + msg.sender_tag, + reply_surbs, + from_surb_request, + ) } + reconstructed + } + + fn handle_reconstructed_reply_messages( + &mut self, + msgs: Vec, + ) -> Vec { + let mut reconstructed = Vec::new(); + for msg in msgs { + match msg.content { + ReplyMessageContent::Data { message } => reconstructed.push(message.into()), + ReplyMessageContent::SurbRequest { recipient, amount } => { + debug!("received request for {amount} additional reply SURBs from {recipient}"); + self.reply_controller_sender + .send_additional_surbs_request(*recipient, amount); + } + } + } + reconstructed + } + + async fn handle_reconstructed_messages(&mut self, msgs: Vec) { + if msgs.is_empty() { + return; + } + + let mut plain_messages = Vec::new(); + let mut repliable_messages = Vec::new(); + let mut reply_messages = Vec::new(); + + for msg in msgs { + match msg { + NymMessage::Plain(plain) => plain_messages.push(plain), + NymMessage::Repliable(repliable) => repliable_messages.push(repliable), + NymMessage::Reply(reply) => reply_messages.push(reply), + } + } + + let mut reconstructed_messages = self.handle_reconstructed_plain_messages(plain_messages); + reconstructed_messages + .append(&mut self.handle_reconstructed_repliable_messages(repliable_messages)); + reconstructed_messages + .append(&mut self.handle_reconstructed_reply_messages(reply_messages)); + + let mut inner_guard = self.inner.lock().await; + debug!( + "Adding {:?} new messages to the buffer!", + reconstructed_messages.len() + ); + + if let Some(sender) = &inner_guard.message_sender { + trace!("Sending reconstructed messages to announced sender"); + if let Err(err) = sender.unbounded_send(reconstructed_messages) { + warn!("The reconstructed message receiver went offline without explicit notification (relevant error: - {err})"); + inner_guard.message_sender = None; + inner_guard.messages.extend(err.into_inner()); + } + } else { + trace!("No sender available - buffering reconstructed messages"); + inner_guard.messages.extend(reconstructed_messages) + } + } + + // this function doesn't really belong here... + fn get_reply_key<'a>( + &self, + raw_message: &'a mut [u8], + ) -> Option<(SurbEncryptionKey, &'a mut [u8])> { + let reply_surb_digest_size = ReplySurbKeyDigestAlgorithm::output_size(); + if raw_message.len() < reply_surb_digest_size { + return None; + } + + let possible_key_digest = + EncryptionKeyDigest::clone_from_slice(&raw_message[..reply_surb_digest_size]); + self.reply_key_storage + .try_pop(possible_key_digest) + .map(|reply_encryption_key| { + ( + *reply_encryption_key, + &mut raw_message[reply_surb_digest_size..], + ) + }) } async fn handle_new_received(&mut self, msgs: Vec>) { @@ -217,69 +338,27 @@ impl ReceivedMessagesBuffer { let mut inner_guard = self.inner.lock().await; // first check if this is a reply or a chunked message - // TODO: verify with @AP if this way of doing it is safe or whether it could - // cause some attacks due to, I don't know, stupid edge case collisions? - // Update: this DOES introduce a possible leakage: https://github.com/nymtech/nym/issues/296 - for msg in msgs { - // TODO: - // 1. make it nicer - // 2. make it not feature-locked - - #[cfg(feature = "reply-surb")] - { - let reply_surb_digest_size = ReplySurbKeyDigestAlgorithm::output_size(); - - let possible_key_digest = - EncryptionKeyDigest::clone_from_slice(&msg[..reply_surb_digest_size]); - - // check first `HasherOutputSize` bytes if they correspond to known encryption key - // if yes - this is a reply message - - // TODO: this might be a bottleneck - since the keys are stored on disk we, presumably, - // are doing a disk operation every single received fragment - if let Some(reply_encryption_key) = self - .reply_key_storage - .get_and_remove_encryption_key(possible_key_digest) - .expect("storage operation failed!") - { - if let Some(completed_message) = Self::process_received_reply( - &msg[reply_surb_digest_size..], - reply_encryption_key, - ) { - completed_messages.push(completed_message) - } + // note: there's a possible information leakage associated with this check https://github.com/nymtech/nym/issues/296 + for mut msg in msgs { + // check first `HasherOutputSize` bytes if they correspond to known encryption key + // if yes - this is a reply message + let completed_message = + if let Some((reply_key, reply_message)) = self.get_reply_key(&mut msg) { + inner_guard.process_received_reply(reply_message, reply_key) } else { - // otherwise - it's a 'normal' message - if let Some(completed_message) = inner_guard.process_received_fragment(msg) { - completed_messages.push(completed_message) - } - } - } + inner_guard.process_received_regular_packet(msg) + }; - #[cfg(not(feature = "reply-surb"))] - if let Some(completed_message) = inner_guard.process_received_fragment(msg) { - completed_messages.push(completed_message) + if let Some(completed) = completed_message { + info!("received {completed}"); + completed_messages.push(completed) } } + drop(inner_guard); + if !completed_messages.is_empty() { - if let Some(sender) = &inner_guard.message_sender { - trace!("Sending reconstructed messages to announced sender"); - if let Err(err) = sender.unbounded_send(completed_messages) { - warn!("The reconstructed message receiver went offline without explicit notification (relevant error: - {:?})", err); - // make sure to drop the lock to not deadlock - // (it is required by `add_reconstructed_messages`) - inner_guard.message_sender = None; - drop(inner_guard); - self.add_reconstructed_messages(err.into_inner()).await; - } - } else { - // make sure to drop the lock to not deadlock - // (it is required by `add_reconstructed_messages`) - drop(inner_guard); - trace!("No sender available - buffering reconstructed messages"); - self.add_reconstructed_messages(completed_messages).await; - } + self.handle_reconstructed_messages(completed_messages).await } } } @@ -320,10 +399,7 @@ impl RequestReceiver { } } - #[cfg(not(target_arch = "wasm32"))] async fn run_with_shutdown(&mut self, mut shutdown: task::ShutdownListener) { - use std::time::Duration; - debug!("Started RequestReceiver with graceful shutdown support"); while !shutdown.is_shutdown() { tokio::select! { @@ -342,20 +418,9 @@ impl RequestReceiver { }, } } - tokio::time::timeout(Duration::from_secs(5), shutdown.recv()) - .await - .expect("Task stopped without shutdown called"); + shutdown.recv_timeout().await; log::debug!("RequestReceiver: Exiting"); } - - #[cfg(target_arch = "wasm32")] - async fn run(&mut self) { - debug!("Started RequestReceiver without graceful shutdown support"); - - while let Some(message) = self.query_receiver.next().await { - self.handle_message(message).await - } - } } struct FragmentedMessageReceiver { @@ -374,10 +439,7 @@ impl FragmentedMessageReceiver { } } - #[cfg(not(target_arch = "wasm32"))] async fn run_with_shutdown(&mut self, mut shutdown: task::ShutdownListener) { - use std::time::Duration; - debug!("Started FragmentedMessageReceiver with graceful shutdown support"); while !shutdown.is_shutdown() { tokio::select! { @@ -395,38 +457,28 @@ impl FragmentedMessageReceiver { } } } - tokio::time::timeout(Duration::from_secs(5), shutdown.recv()) - .await - .expect("Task stopped without shutdown called"); + shutdown.recv_timeout().await; log::debug!("FragmentedMessageReceiver: Exiting"); } - - #[cfg(target_arch = "wasm32")] - async fn run(&mut self) { - debug!("Started FragmentedMessageReceiver without graceful shutdown support"); - - while let Some(new_messages) = self.mixnet_packet_receiver.next().await { - self.received_buffer.handle_new_received(new_messages).await; - } - } } -pub struct ReceivedMessagesBufferController { +pub(crate) struct ReceivedMessagesBufferController { fragmented_message_receiver: FragmentedMessageReceiver, request_receiver: RequestReceiver, } impl ReceivedMessagesBufferController { - pub fn new( + pub(crate) fn new( local_encryption_keypair: Arc, query_receiver: ReceivedBufferRequestReceiver, mixnet_packet_receiver: MixnetMessageReceiver, - #[cfg(feature = "reply-surb")] reply_key_storage: ReplyKeyStorage, + reply_key_storage: SentReplyKeys, + reply_controller_sender: ReplyControllerSender, ) -> Self { let received_buffer = ReceivedMessagesBuffer::new( local_encryption_keypair, - #[cfg(feature = "reply-surb")] reply_key_storage, + reply_controller_sender, ); ReceivedMessagesBufferController { @@ -438,7 +490,6 @@ impl ReceivedMessagesBufferController { } } - #[cfg(not(target_arch = "wasm32"))] pub fn start_with_shutdown(self, shutdown: task::ShutdownListener) { let mut fragmented_message_receiver = self.fragmented_message_receiver; let mut request_receiver = self.request_receiver; @@ -453,16 +504,4 @@ impl ReceivedMessagesBufferController { request_receiver.run_with_shutdown(shutdown).await; }); } - - #[cfg(target_arch = "wasm32")] - pub fn start(self) { - let mut fragmented_message_receiver = self.fragmented_message_receiver; - let mut request_receiver = self.request_receiver; - spawn_future(async move { - fragmented_message_receiver.run().await; - }); - spawn_future(async move { - request_receiver.run().await; - }); - } } diff --git a/clients/client-core/src/client/replies/mod.rs b/clients/client-core/src/client/replies/mod.rs new file mode 100644 index 0000000000..61b5f19abf --- /dev/null +++ b/clients/client-core/src/client/replies/mod.rs @@ -0,0 +1,5 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod reply_controller; +pub mod reply_storage; diff --git a/clients/client-core/src/client/replies/reply_controller.rs b/clients/client-core/src/client/replies/reply_controller.rs new file mode 100644 index 0000000000..99687527f0 --- /dev/null +++ b/clients/client-core/src/client/replies/reply_controller.rs @@ -0,0 +1,928 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::client::real_messages_control::acknowledgement_control::PendingAcknowledgement; +use crate::client::real_messages_control::message_handler::{MessageHandler, PreparationError}; +use crate::client::replies::reply_storage::CombinedReplyStorage; +use client_connections::TransmissionLane; +use futures::channel::mpsc; +use futures::StreamExt; +use log::{debug, error, info, trace, warn}; +use nymsphinx::addressing::clients::Recipient; +use nymsphinx::anonymous_replies::requests::AnonymousSenderTag; +use nymsphinx::anonymous_replies::ReplySurb; +use nymsphinx::chunking::fragment::{Fragment, FragmentIdentifier}; +use rand::{CryptoRng, Rng}; +use std::cmp::{max, min}; +use std::collections::btree_map::Entry; +use std::collections::{BTreeMap, HashMap, VecDeque}; +use std::sync::{Arc, Weak}; +use std::time::Duration; +use time::OffsetDateTime; + +#[cfg(not(target_arch = "wasm32"))] +type IntervalStream = tokio_stream::wrappers::IntervalStream; + +#[cfg(target_arch = "wasm32")] +type IntervalStream = gloo_timers::future::IntervalStream; + +pub(crate) fn new_control_channels() -> (ReplyControllerSender, ReplyControllerReceiver) { + let (tx, rx) = mpsc::unbounded(); + (tx.into(), rx) +} + +#[derive(Debug, Clone)] +pub(crate) struct ReplyControllerSender(mpsc::UnboundedSender); + +impl From> for ReplyControllerSender { + fn from(inner: mpsc::UnboundedSender) -> Self { + ReplyControllerSender(inner) + } +} + +impl ReplyControllerSender { + pub(crate) fn send_retransmission_data( + &self, + recipient: AnonymousSenderTag, + timed_out_ack: Weak, + extra_surb_request: bool, + ) { + self.0 + .unbounded_send(ReplyControllerMessage::RetransmitReply { + recipient, + timed_out_ack, + extra_surb_request, + }) + .expect("ReplyControllerReceiver has died!") + } + + pub(crate) fn send_reply( + &self, + recipient: AnonymousSenderTag, + message: Vec, + lane: TransmissionLane, + ) { + self.0 + .unbounded_send(ReplyControllerMessage::SendReply { + recipient, + message, + lane, + }) + .expect("ReplyControllerReceiver has died!") + } + + pub(crate) fn send_additional_surbs( + &self, + sender_tag: AnonymousSenderTag, + reply_surbs: Vec, + from_surb_request: bool, + ) { + self.0 + .unbounded_send(ReplyControllerMessage::AdditionalSurbs { + sender_tag, + reply_surbs, + from_surb_request, + }) + .expect("ReplyControllerReceiver has died!") + } + + pub(crate) fn send_additional_surbs_request(&self, recipient: Recipient, amount: u32) { + self.0 + .unbounded_send(ReplyControllerMessage::AdditionalSurbsRequest { + recipient: Box::new(recipient), + amount, + }) + .expect("ReplyControllerReceiver has died!") + } +} + +pub(crate) type ReplyControllerReceiver = mpsc::UnboundedReceiver; + +#[derive(Debug)] +pub(crate) enum ReplyControllerMessage { + RetransmitReply { + recipient: AnonymousSenderTag, + timed_out_ack: Weak, + extra_surb_request: bool, + }, + + SendReply { + recipient: AnonymousSenderTag, + message: Vec, + lane: TransmissionLane, + }, + + AdditionalSurbs { + sender_tag: AnonymousSenderTag, + reply_surbs: Vec, + from_surb_request: bool, + }, + + // Should this also be handled in here? it's technically a completely different side of the pipe + // let's see how it works when combined, might split it before creating PR + AdditionalSurbsRequest { + recipient: Box, + amount: u32, + }, +} + +pub struct Config { + min_surb_request_size: u32, + max_surb_request_size: u32, + maximum_allowed_reply_surb_request_size: u32, + max_surb_waiting_period: Duration, + max_reply_surb_age: Duration, + max_reply_key_age: Duration, +} + +impl Config { + pub(crate) fn new( + min_surb_request_size: u32, + max_surb_request_size: u32, + maximum_allowed_reply_surb_request_size: u32, + max_surb_waiting_period: Duration, + max_reply_surb_age: Duration, + max_reply_key_age: Duration, + ) -> Self { + Self { + min_surb_request_size, + max_surb_request_size, + maximum_allowed_reply_surb_request_size, + max_surb_waiting_period, + max_reply_surb_age, + max_reply_key_age, + } + } +} + +// the purpose of this task: +// - buffers split messages from input message listener if there were insufficient surbs to send them +// - upon getting extra surbs, resends them +// - so I guess it will handle all 'RepliableMessage' and requests from 'ReplyMessage' +// - replies to "give additional surbs" requests +// - will reply to future heartbeats + +// TODO: this should be split into ingress and egress controllers +// because currently its trying to perform two distinct jobs +pub struct ReplyController { + config: Config, + + // TODO: incorporate that field at some point + // and use binomial distribution to determine the expected required number + // of surbs required to send the message through + // expected_reliability: f32, + request_receiver: ReplyControllerReceiver, + pending_replies: HashMap>, + + /// Retransmission packets that have already timed out and are waiting for additional reply SURBs + /// so that they could be sent back to the network. Once we receive more SURBs, we should send them ASAP. + // TODO: when purging stale entries, we must take extra care to also purge all pending ACK data!! + pending_retransmissions: + HashMap>>, + + message_handler: MessageHandler, + full_reply_storage: CombinedReplyStorage, +} + +impl ReplyController +where + R: CryptoRng + Rng, +{ + pub(crate) fn new( + config: Config, + message_handler: MessageHandler, + full_reply_storage: CombinedReplyStorage, + request_receiver: ReplyControllerReceiver, + ) -> Self { + ReplyController { + config, + request_receiver, + pending_replies: HashMap::new(), + pending_retransmissions: HashMap::new(), + message_handler, + full_reply_storage, + } + } + + /// Inserts the pending replies into the BACK of the queue fn insert_pending_replies>>( + fn insert_pending_replies>>( + &mut self, + recipient: &AnonymousSenderTag, + fragments: V, + ) { + if let Some(existing) = self.pending_replies.get_mut(recipient) { + existing.append(&mut fragments.into()) + } else { + self.pending_replies.insert(*recipient, fragments.into()); + } + } + + fn re_insert_pending_retransmission( + &mut self, + recipient: &AnonymousSenderTag, + data: Vec>, + ) { + // the underlying entry MUST exist as we've just got data from there + let map_entry = self + .pending_retransmissions + .get_mut(recipient) + .expect("our pending retransmission entry is somehow gone!"); + + for pending in data { + // if it's 0, we don't need to do anything - we just got that ack! + if Arc::strong_count(&pending) > 1 { + let id = pending.inner_fragment_identifier(); + let downgraded = Arc::downgrade(&pending); + map_entry.insert(id, downgraded); + } + } + } + + fn should_request_more_surbs(&self, target: &AnonymousSenderTag) -> bool { + trace!("checking if we should request more surbs from {:?}", target); + + let pending_queue_size = self + .pending_replies + .get(target) + .map(|pending_queue| pending_queue.len()) + .unwrap_or_default(); + + let retransmission_queue = self + .pending_retransmissions + .get(target) + .map(|pending_queue| pending_queue.len()) + .unwrap_or_default(); + + let total_queue = pending_queue_size + retransmission_queue; + + // simple as that - there's absolutely nothing to retransmit + if total_queue == 0 { + return false; + } + + let available_surbs = self + .full_reply_storage + .surbs_storage_ref() + .available_surbs(target); + let pending_surbs = self + .full_reply_storage + .surbs_storage_ref() + .pending_reception(target) as usize; + let min_surbs_threshold = self + .full_reply_storage + .surbs_storage_ref() + .min_surb_threshold(); + let max_surbs_threshold = self + .full_reply_storage + .surbs_storage_ref() + .max_surb_threshold(); + + debug!("total queue size: {total_queue} = pending data {pending_queue_size} + pending retransmission {retransmission_queue}, available surbs: {available_surbs} pending surbs: {pending_surbs} threshold range: {min_surbs_threshold}..{max_surbs_threshold}"); + + (pending_surbs + available_surbs) < max_surbs_threshold + && (pending_surbs + available_surbs) < (total_queue + min_surbs_threshold) + } + + async fn handle_send_reply( + &mut self, + recipient_tag: AnonymousSenderTag, + data: Vec, + lane: TransmissionLane, + ) { + if !self + .full_reply_storage + .surbs_storage_ref() + .contains_surbs_for(&recipient_tag) + { + warn!("received reply request for {:?} but we don't have any surbs stored for that recipient!", recipient_tag); + return; + } + + trace!("handling reply to {:?}", recipient_tag); + let fragments = self.message_handler.split_reply_message(data); + + let required_surbs = fragments.len(); + trace!("This reply requires {:?} SURBs", required_surbs); + + // TODO: edge case: + // we're making a lot of requests and have to request a lot of surbs + // (but at some point we run out of surbs for surb requests) + + let (surbs, _surbs_left) = self + .full_reply_storage + .surbs_storage_ref() + .get_reply_surbs(&recipient_tag, required_surbs); + + if let Some(reply_surbs) = surbs { + if let Err(err) = self + .message_handler + .try_send_reply_chunks(recipient_tag, fragments, reply_surbs, lane) + .await + { + let err = err.return_unused_surbs( + self.full_reply_storage.surbs_storage_ref(), + &recipient_tag, + ); + warn!("failed to send reply to {:?} - {err}", recipient_tag); + + // TODO: should we buffer that data to try again? + } + } else { + // we don't have enough surbs for this reply + self.insert_pending_replies(&recipient_tag, fragments); + + if self.should_request_more_surbs(&recipient_tag) { + self.request_reply_surbs_for_queue_clearing(recipient_tag) + .await; + } + } + } + + async fn request_additional_reply_surbs( + &mut self, + target: AnonymousSenderTag, + amount: u32, + ) -> Result<(), PreparationError> { + let reply_surb = self + .full_reply_storage + .surbs_storage_ref() + .get_reply_surb_ignoring_threshold(&target) + .and_then(|(reply_surb, _)| reply_surb) + .ok_or(PreparationError::NotEnoughSurbs { + available: 0, + required: 1, + })?; + + if let Err(err) = self + .message_handler + .try_request_additional_reply_surbs(target, reply_surb, amount) + .await + { + let err = err.return_unused_surbs(self.full_reply_storage.surbs_storage_ref(), &target); + warn!( + "failed to request additional surbs from {:?} - {err}", + target + ); + return Err(err); + } else { + self.full_reply_storage + .surbs_storage_ref() + .increment_pending_reception(&target, amount); + } + + Ok(()) + } + + async fn try_clear_pending_retransmission(&mut self, target: AnonymousSenderTag) { + trace!("trying to clear pending retransmission queue"); + let available_surbs = self + .full_reply_storage + .surbs_storage_ref() + .available_surbs(&target); + let min_surbs_threshold = self + .full_reply_storage + .surbs_storage_ref() + .min_surb_threshold(); + + let max_to_clear = if available_surbs > min_surbs_threshold { + available_surbs - min_surbs_threshold + } else { + trace!("we don't have enough surbs for retransmission queue clearing..."); + return; + }; + trace!("we can clear up to {max_to_clear} entries"); + + let Some(pending) = self.pending_retransmissions.get_mut(&target) else { + trace!("there are no pending retransmissions for {target}!"); + return; + }; + + let mut to_take = Vec::new(); + let mut to_remove = Vec::new(); + + // TODO: once rust 1.66.0 is stabilised on 15.12.22, just change it to + // `.pop_front()` to directly take ownership + for (k, data) in pending.iter() { + let upgraded = match data.upgrade() { + Some(upgraded) => upgraded, + None => { + // we got the ack while the data was waiting in the queue + to_remove.push(*k); + continue; + } + }; + + to_take.push(upgraded); + + // we have taken as many entries as we could have + if to_take.len() >= max_to_clear { + break; + } + // TODO: use if upgraded.is_extra_surb_request() to bypass the limit + } + + for ack in &to_take { + pending.remove(&ack.inner_fragment_identifier()); + } + + for id in to_remove { + pending.remove(&id); + } + + if to_take.is_empty() { + // no need to do anything + return; + } + + let (surbs_for_reply, _) = self + .full_reply_storage + .surbs_storage_ref() + .get_reply_surbs(&target, to_take.len()); + + let Some(surbs_for_reply) = surbs_for_reply else { + error!("somehow different task has stolen our reply surbs! - this should have been impossible"); + self.re_insert_pending_retransmission(&target, to_take); + return; + }; + + let to_send_vec = to_take.iter().map(|ack| ack.fragment_data()).collect(); + + if let Err(err) = self + .message_handler + .try_send_retransmission_reply_chunks( + to_send_vec, + surbs_for_reply, + TransmissionLane::Retransmission, + ) + .await + { + let err = err.return_unused_surbs(self.full_reply_storage.surbs_storage_ref(), &target); + self.re_insert_pending_retransmission(&target, to_take); + + warn!( + "failed to clear pending retransmission queue for {:?} - {err}", + target + ); + } + } + + fn pop_at_most_pending_replies( + &mut self, + from: &AnonymousSenderTag, + amount: usize, + ) -> Option> { + // if possible, pop all pending replies, if not, pop only entries for which we'd have a reply surb + let total = self.pending_replies.get(from)?.len(); + trace!("pending queue has {total} elements"); + if total == 0 { + return None; + } + if total < amount { + self.pending_replies.remove(from) + } else { + Some( + self.pending_replies + .get_mut(from)? + .drain(..amount) + .collect(), + ) + } + } + + async fn try_clear_pending_queue(&mut self, target: AnonymousSenderTag) { + trace!("trying to clear pending queue"); + let available_surbs = self + .full_reply_storage + .surbs_storage_ref() + .available_surbs(&target); + let min_surbs_threshold = self + .full_reply_storage + .surbs_storage_ref() + .min_surb_threshold(); + + let max_to_clear = if available_surbs > min_surbs_threshold { + available_surbs - min_surbs_threshold + } else { + trace!("we don't have enough surbs for queue clearing..."); + return; + }; + trace!("we can clear up to {max_to_clear} entries"); + + // we're guaranteed to not get more entries than we have reply surbs for + if let Some(to_send) = self.pop_at_most_pending_replies(&target, max_to_clear) { + let to_send_vec = to_send.iter().cloned().collect::>(); + + if to_send_vec.is_empty() { + panic!( + "please let the devs know if you ever see this message (reply_controller.rs)" + ); + } + + let (surbs_for_reply, _) = self + .full_reply_storage + .surbs_storage_ref() + .get_reply_surbs(&target, to_send_vec.len()); + + let Some(surbs_for_reply) = surbs_for_reply else { + error!("somehow different task has stolen our reply surbs! - this should have been impossible"); + self.insert_pending_replies(&target, to_send); + return; + }; + + if let Err(err) = self + .message_handler + .try_send_reply_chunks( + target, + to_send_vec, + surbs_for_reply, + TransmissionLane::General, + ) + .await + { + let err = + err.return_unused_surbs(self.full_reply_storage.surbs_storage_ref(), &target); + self.insert_pending_replies(&target, to_send); + warn!("failed to clear pending queue for {:?} - {err}", target); + } + } else { + trace!("the pending queue is empty"); + } + } + + async fn handle_received_surbs( + &mut self, + from: AnonymousSenderTag, + reply_surbs: Vec, + from_surb_request: bool, + ) { + trace!("handling received surbs"); + + // clear the requesting flag since we should have been asking for surbs + self.full_reply_storage + .surbs_storage_ref() + .reset_surbs_last_received_at(&from); + if from_surb_request { + self.full_reply_storage + .surbs_storage_ref() + .decrement_pending_reception(&from, reply_surbs.len() as u32); + } + + // store received surbs + self.full_reply_storage + .surbs_storage_ref() + .insert_surbs(&from, reply_surbs); + + // use as many as we can for clearing pending retransmission queue + self.try_clear_pending_retransmission(from).await; + + // use as many as we can for clearing pending 'normal' queue + self.try_clear_pending_queue(from).await; + + // if we have to, request more + if self.should_request_more_surbs(&from) { + self.request_reply_surbs_for_queue_clearing(from).await; + } + } + + async fn handle_surb_request(&mut self, recipient: Recipient, mut amount: u32) { + // 1. check whether we sent any surbs in the past to this recipient, otherwise + // they have no business in asking for more + if !self + .full_reply_storage + .tags_storage_ref() + .exists(&recipient) + { + warn!("{recipient} asked us for reply SURBs even though we never sent them any anonymous messages before!"); + return; + } + + // 2. check whether the requested amount is within sane range + if amount > self.config.maximum_allowed_reply_surb_request_size { + warn!("The requested reply surb amount is larger than our maximum allowed ({amount} > {}). Lowering it to a more sane value...", self.config.maximum_allowed_reply_surb_request_size); + amount = self.config.maximum_allowed_reply_surb_request_size; + } + + // 3. construct and send the surbs away + // (send them in smaller batches to make the experience a bit smoother + let mut remaining = amount; + while remaining > 0 { + let to_send = min(remaining, 100); + if let Err(err) = self + .message_handler + .try_send_additional_reply_surbs(recipient, to_send) + .await + { + warn!("failed to send additional surbs to {recipient} - {err}"); + } else { + trace!("sent {to_send} reply SURBs to {recipient}"); + } + + remaining -= to_send; + } + } + + fn buffer_pending_ack( + &mut self, + recipient: AnonymousSenderTag, + ack_ref: Arc, + weak_ack_ref: Weak, + ) { + let frag_id = ack_ref.inner_fragment_identifier(); + if let Some(existing) = self.pending_retransmissions.get_mut(&recipient) { + if let Entry::Vacant(e) = existing.entry(frag_id) { + e.insert(weak_ack_ref); + } else { + warn!("we're already trying to retransmit {frag_id}. We must be really behind in surbs!"); + } + } else { + let mut inner = BTreeMap::new(); + inner.insert(frag_id, weak_ack_ref); + self.pending_retransmissions.insert(recipient, inner); + } + } + + async fn handle_reply_retransmission( + &mut self, + recipient_tag: AnonymousSenderTag, + timed_out_ack: Weak, + extra_surbs_request: bool, + ) { + // seems we got the ack in the end + let ack_ref = match timed_out_ack.upgrade() { + Some(ack) => ack, + None => { + debug!("we received the ack for one of the reply packets as we were putting it in the retransmission queue"); + return; + } + }; + + // if this is retransmission for obtaining additional reply surbs, + // we can dip below the storage threshold + let (maybe_reply_surb, _) = if extra_surbs_request { + self.full_reply_storage + .surbs_storage_ref() + .get_reply_surb_ignoring_threshold(&recipient_tag) + } else { + self.full_reply_storage + .surbs_storage_ref() + .get_reply_surb(&recipient_tag) + } + .expect("attempted to retransmit a packet to an unknown recipient - we shouldn't have sent the original packet in the first place!"); + + if let Some(reply_surb) = maybe_reply_surb { + match self + .message_handler + .try_prepare_single_reply_chunk_for_sending(reply_surb, ack_ref.fragment_data()) + .await + { + Ok(prepared) => { + // drop the ack ref so that controller would not panic on `UpdateTimer` if that task + // got to handle the action before this function terminated (which is very much + // possible if `forward_messages` takes a while) + drop(ack_ref); + + self.message_handler + .update_ack_delay(prepared.fragment_identifier, prepared.total_delay); + self.message_handler + .forward_messages(vec![prepared.into()], TransmissionLane::Retransmission) + .await; + } + Err(err) => { + let err = err.return_unused_surbs( + self.full_reply_storage.surbs_storage_ref(), + &recipient_tag, + ); + warn!("failed to prepare message for retransmission - {err}"); + // we buffer that packet and to try another day + self.buffer_pending_ack(recipient_tag, ack_ref, timed_out_ack); + + if self.should_request_more_surbs(&recipient_tag) { + self.request_reply_surbs_for_queue_clearing(recipient_tag) + .await; + } + } + }; + } else { + self.buffer_pending_ack(recipient_tag, ack_ref, timed_out_ack); + + if self.should_request_more_surbs(&recipient_tag) { + self.request_reply_surbs_for_queue_clearing(recipient_tag) + .await; + } + } + } + + async fn handle_request(&mut self, request: ReplyControllerMessage) { + match request { + ReplyControllerMessage::RetransmitReply { + recipient, + timed_out_ack, + extra_surb_request, + } => { + self.handle_reply_retransmission(recipient, timed_out_ack, extra_surb_request) + .await + } + ReplyControllerMessage::SendReply { + recipient, + message, + lane, + } => self.handle_send_reply(recipient, message, lane).await, + ReplyControllerMessage::AdditionalSurbs { + sender_tag, + reply_surbs, + from_surb_request, + } => { + self.handle_received_surbs(sender_tag, reply_surbs, from_surb_request) + .await + } + ReplyControllerMessage::AdditionalSurbsRequest { recipient, amount } => { + self.handle_surb_request(*recipient, amount).await + } + } + } + + async fn request_reply_surbs_for_queue_clearing(&mut self, target: AnonymousSenderTag) { + trace!("requesting surbs for queues clearing"); + + let pending_queue_size = self + .pending_replies + .get(&target) + .map(|pending_queue| pending_queue.len()) + .unwrap_or_default(); + + let retransmission_queue = self + .pending_retransmissions + .get(&target) + .map(|pending_queue| pending_queue.len()) + .unwrap_or_default(); + + let total_queue = (pending_queue_size + retransmission_queue) as u32; + + if total_queue == 0 { + trace!("the pending queues for {:?} are already empty", target); + return; + } + + let request_size = min( + self.config.max_surb_request_size, + max(total_queue, self.config.min_surb_request_size), + ); + + if let Err(err) = self + .request_additional_reply_surbs(target, request_size) + .await + { + warn!("failed to request additional surbs... - {err}") + } + } + + async fn inspect_stale_entries(&mut self) { + let mut to_request = Vec::new(); + let mut to_remove = Vec::new(); + + let now = OffsetDateTime::now_utc(); + for (pending_reply_target, vals) in &self.pending_replies { + if vals.is_empty() { + continue; + } + + let Some(last_received) = self.full_reply_storage.surbs_storage_ref().surbs_last_received_at(pending_reply_target) else { + error!("we have {} pending replies for {pending_reply_target}, but we somehow never received any reply surbs from them!", vals.len()); + to_remove.push(*pending_reply_target); + continue; + }; + + // this should never ever happen (famous last words, eh?), but in case it DOES happen eventually + // purge that malformed data + let Ok(last_received_time) = OffsetDateTime::from_unix_timestamp(last_received) else { + error!("somehow our stored timestamp ({last_received}) for surbs from {pending_reply_target} is corrupted!. Going to remove all the associated entries"); + to_remove.push(*pending_reply_target); + continue; + }; + + let diff = now - last_received_time; + + if diff > self.config.max_surb_waiting_period { + warn!("We haven't received any surbs in {:?} from {pending_reply_target}. Going to explicitly ask for more", diff); + to_request.push(*pending_reply_target); + } + } + + for pending_reply_target in to_request { + self.request_reply_surbs_for_queue_clearing(pending_reply_target) + .await; + self.full_reply_storage + .surbs_storage_ref() + .reset_pending_reception(&pending_reply_target) + } + for to_remove in to_remove { + self.pending_replies.remove(&to_remove); + } + } + + async fn invalidate_old_data(&self) { + let now = OffsetDateTime::now_utc(); + + let mut to_remove_surbs = Vec::new(); + let mut to_remove_keys = Vec::new(); + for map_ref in self.full_reply_storage.surbs_storage_ref().as_raw_iter() { + let (sender, received) = map_ref.pair(); + // TODO: handle the following edge case: + // there's a malicious client sending us exactly one reply surb just before we should have invalidated + // the data thus making us keep everything in memory + // possible solution: keep timestamp PER reply surb (but that seems like an overkill) + // but I doubt this is ever going to be a problem... + // ... + // However, if you're reading this message, it probably became a legit problem, + // so I guess add timestamp per surb then? chop-chop. + + let last_received = received.surbs_last_received_at(); + // this should never ever happen (famous last words, eh?), but in case it DOES happen eventually + // purge that malformed data + let Ok(last_received_time) = OffsetDateTime::from_unix_timestamp(last_received) else { + error!("somehow our stored timestamp ({last_received}) for surbs from {sender} is corrupted!. Going to remove all the associated entries"); + to_remove_surbs.push(*sender); + continue; + }; + let diff = now - last_received_time; + + if diff > self.config.max_reply_surb_age { + info!("it's been {diff:?} since we last received any reply surb from {sender}. Going to remove all stored entries..."); + + to_remove_surbs.push(*sender); + } + } + + for map_ref in self.full_reply_storage.key_storage_ref().as_raw_iter() { + let (digest, reply_key) = map_ref.pair(); + + // this should never ever happen (famous last words, eh?), but in case it DOES happen eventually + // purge that malformed data + let Ok(sent_at) = OffsetDateTime::from_unix_timestamp(reply_key.sent_at_timestamp) else { + error!("somehow our stored timestamp ({}) for one of our reply key is corrupted!. Going to remove all the entry", reply_key.sent_at_timestamp); + to_remove_keys.push(*digest); + continue; + }; + + let diff = now - sent_at; + + if diff > self.config.max_reply_key_age { + debug!("it's been {diff:?} since we created this reply key. it's probably never going to get used, so we're going to purge it..."); + to_remove_keys.push(*digest); + } + } + + for to_remove in to_remove_surbs { + self.full_reply_storage + .surbs_storage_ref() + .remove(&to_remove); + } + + for to_remove in to_remove_keys { + self.full_reply_storage.key_storage().remove(to_remove) + } + } + + fn create_interval_stream(polling_rate: Duration) -> IntervalStream { + #[cfg(not(target_arch = "wasm32"))] + return tokio_stream::wrappers::IntervalStream::new(tokio::time::interval(polling_rate)); + + #[cfg(target_arch = "wasm32")] + return gloo_timers::future::IntervalStream::new(polling_rate.as_millis() as u32); + } + + pub(crate) async fn run_with_shutdown(&mut self, mut shutdown: task::ShutdownListener) { + debug!("Started ReplyController with graceful shutdown support"); + + let polling_rate = Duration::from_secs(5); + let mut stale_inspection = Self::create_interval_stream(polling_rate); + + // this is in the order of hours/days so we don't have to poll it that often + let polling_rate = Duration::from_secs(self.config.max_reply_surb_age.as_secs() / 10); + let mut invalidation_inspection = Self::create_interval_stream(polling_rate); + + while !shutdown.is_shutdown() { + tokio::select! { + biased; + _ = shutdown.recv() => { + log::trace!("ReplyController: Received shutdown"); + }, + req = self.request_receiver.next() => match req { + Some(req) => self.handle_request(req).await, + None => { + log::trace!("ReplyController: Stopping since channel closed"); + break; + } + }, + _ = stale_inspection.next() => { + self.inspect_stale_entries().await + }, + _ = invalidation_inspection.next() => { + self.invalidate_old_data().await + } + } + } + assert!(shutdown.is_shutdown_poll()); + log::debug!("ReplyController: Exiting"); + } +} diff --git a/clients/client-core/src/client/replies/reply_storage/backend/browser_backend.rs b/clients/client-core/src/client/replies/reply_storage/backend/browser_backend.rs new file mode 100644 index 0000000000..9711ea759f --- /dev/null +++ b/clients/client-core/src/client/replies/reply_storage/backend/browser_backend.rs @@ -0,0 +1,43 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::client::replies::reply_storage::backend::Empty; +use crate::client::replies::reply_storage::{CombinedReplyStorage, ReplyStorageBackend}; +use async_trait::async_trait; + +// well, right now we don't have the browser storage : ( +// so we keep everything in memory +pub struct Backend { + empty: Empty, +} + +impl Backend { + pub fn new(min_surb_threshold: usize, max_surb_threshold: usize) -> Self { + Backend { + empty: Empty { + min_surb_threshold, + max_surb_threshold, + }, + } + } +} + +#[async_trait] +impl ReplyStorageBackend for Backend { + type StorageError = ::StorageError; + + async fn flush_surb_storage( + &mut self, + storage: &CombinedReplyStorage, + ) -> Result<(), Self::StorageError> { + self.empty.flush_surb_storage(storage).await + } + + async fn init_fresh(&mut self, fresh: &CombinedReplyStorage) -> Result<(), Self::StorageError> { + self.empty.init_fresh(fresh).await + } + + async fn load_surb_storage(&self) -> Result { + self.empty.load_surb_storage().await + } +} diff --git a/clients/client-core/src/client/replies/reply_storage/backend/fs_backend/error.rs b/clients/client-core/src/client/replies/reply_storage/backend/fs_backend/error.rs new file mode 100644 index 0000000000..8164762123 --- /dev/null +++ b/clients/client-core/src/client/replies/reply_storage/backend/fs_backend/error.rs @@ -0,0 +1,53 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use std::io; +use std::path::PathBuf; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum StorageError { + #[error("the provided database path doesn't have a filename defined")] + DatabasePathWithoutFilename { provided_path: PathBuf }, + + #[error("failed to rename our databse file - {source}")] + DatabaseRenameError { + #[source] + source: io::Error, + }, + + #[error("failed to rename our old databse file - {source}")] + DatabaseOldFileRemoveError { + #[source] + source: io::Error, + }, + + #[error("failed to perform sqlx migration: {source}")] + MigrationError { + #[source] + #[from] + source: sqlx::migrate::MigrateError, + }, + + #[error("failed to connect to the underlying connection pool: {source}")] + DatabaseConnectionError { + #[source] + source: sqlx::error::Error, + }, + + #[error("failed to run the SQL query: {source}")] + QueryError { + #[source] + #[from] + source: sqlx::error::Error, + }, + + #[error("The loaded data is inconsistent - it seems that on the last shutdown the client hasn't finished the data flush. You may have to remove the entire storage manually")] + IncompleteDataFlush, + + #[error("data retrieved from the underlying storage is corrupted: {details}")] + CorruptedData { + details: String, + // err: Option> + }, +} diff --git a/clients/client-core/src/client/replies/reply_storage/backend/fs_backend/manager.rs b/clients/client-core/src/client/replies/reply_storage/backend/fs_backend/manager.rs new file mode 100644 index 0000000000..578c0021a3 --- /dev/null +++ b/clients/client-core/src/client/replies/reply_storage/backend/fs_backend/manager.rs @@ -0,0 +1,257 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::client::replies::reply_storage::backend::fs_backend::error::StorageError; +use crate::client::replies::reply_storage::backend::fs_backend::models::{ + ReplySurbStorageMetadata, StoredReplyKey, StoredReplySurb, StoredSenderTag, StoredSurbSender, +}; +use log::{error, info}; +use sqlx::ConnectOptions; +use std::path::Path; + +#[derive(Debug, Clone)] +pub(crate) struct StorageManager { + pub(crate) connection_pool: sqlx::SqlitePool, +} + +// all SQL goes here +impl StorageManager { + pub(crate) async fn init>( + database_path: P, + fresh: bool, + ) -> Result { + let mut opts = sqlx::sqlite::SqliteConnectOptions::new() + .filename(database_path) + .create_if_missing(fresh); + + opts.disable_statement_logging(); + + let connection_pool = match sqlx::SqlitePool::connect_with(opts).await { + Ok(pool) => pool, + Err(err) => { + error!("Failed to connect to SQLx database: {err}"); + return Err(StorageError::DatabaseConnectionError { source: err }); + } + }; + + if let Err(err) = sqlx::migrate!("./fs_surbs_migrations") + .run(&connection_pool) + .await + { + error!("Failed to initialize SQLx database: {err}"); + return Err(err.into()); + } + + info!("Database migration finished!"); + Ok(StorageManager { connection_pool }) + } + + #[allow(dead_code)] + pub(crate) async fn status_table_exists(&self) -> Result { + sqlx::query!("SELECT name FROM sqlite_master WHERE type='table' AND name='status'") + .fetch_optional(&self.connection_pool) + .await + .map(|r| r.is_some()) + } + + pub(crate) async fn create_status_table(&self) -> Result<(), sqlx::Error> { + sqlx::query!("INSERT INTO status(flush_in_progress, previous_flush_timestamp, client_in_use) VALUES (0, 0, 1)") + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + pub(crate) async fn get_flush_status(&self) -> Result { + sqlx::query!("SELECT flush_in_progress FROM status;") + .fetch_one(&self.connection_pool) + .await + .map(|r| r.flush_in_progress > 0) + } + + pub(crate) async fn set_previous_flush_timestamp( + &self, + timestamp: i64, + ) -> Result<(), sqlx::Error> { + sqlx::query!("UPDATE status SET previous_flush_timestamp = ?", timestamp) + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + pub(crate) async fn get_previous_flush_timestamp(&self) -> Result { + sqlx::query!("SELECT previous_flush_timestamp FROM status;") + .fetch_one(&self.connection_pool) + .await + .map(|r| r.previous_flush_timestamp) + } + + pub(crate) async fn set_flush_status(&self, in_progress: bool) -> Result<(), sqlx::Error> { + let in_progress_int = i64::from(in_progress); + sqlx::query!("UPDATE status SET flush_in_progress = ?", in_progress_int) + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + pub(crate) async fn get_client_in_use_status(&self) -> Result { + sqlx::query!("SELECT client_in_use FROM status;") + .fetch_one(&self.connection_pool) + .await + .map(|r| r.client_in_use > 0) + } + + pub(crate) async fn set_client_in_use_status(&self, in_use: bool) -> Result<(), sqlx::Error> { + let in_use_int = i64::from(in_use); + sqlx::query!("UPDATE status SET client_in_use = ?", in_use_int) + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + pub(crate) async fn delete_all_tags(&self) -> Result<(), sqlx::Error> { + sqlx::query!("DELETE FROM sender_tag;") + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + pub(crate) async fn get_tags(&self) -> Result, sqlx::Error> { + sqlx::query_as!(StoredSenderTag, "SELECT * FROM sender_tag;",) + .fetch_all(&self.connection_pool) + .await + } + + pub(crate) async fn insert_tag(&self, stored_tag: StoredSenderTag) -> Result<(), sqlx::Error> { + sqlx::query!( + r#" + INSERT INTO sender_tag(recipient, tag) VALUES (?, ?); + "#, + stored_tag.recipient, + stored_tag.tag + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + pub(crate) async fn delete_all_reply_keys(&self) -> Result<(), sqlx::Error> { + sqlx::query!("DELETE FROM reply_key;") + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + pub(crate) async fn get_reply_keys(&self) -> Result, sqlx::Error> { + sqlx::query_as!(StoredReplyKey, "SELECT * FROM reply_key;",) + .fetch_all(&self.connection_pool) + .await + } + + pub(crate) async fn insert_reply_key( + &self, + stored_reply_key: StoredReplyKey, + ) -> Result<(), sqlx::Error> { + sqlx::query!( + r#" + INSERT INTO reply_key(key_digest, reply_key, sent_at_timestamp) VALUES (?, ?, ?); + "#, + stored_reply_key.key_digest, + stored_reply_key.reply_key, + stored_reply_key.sent_at_timestamp + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + pub(crate) async fn get_surb_senders(&self) -> Result, sqlx::Error> { + sqlx::query_as!(StoredSurbSender, "SELECT * FROM reply_surb_sender;",) + .fetch_all(&self.connection_pool) + .await + } + + pub(crate) async fn insert_surb_sender( + &self, + stored_surb_sender: StoredSurbSender, + ) -> Result { + let id = sqlx::query!( + r#" + INSERT INTO reply_surb_sender(tag, last_sent_timestamp) VALUES (?, ?); + "#, + stored_surb_sender.tag, + stored_surb_sender.last_sent_timestamp + ) + .execute(&self.connection_pool) + .await? + .last_insert_rowid(); + Ok(id) + } + + pub(crate) async fn get_reply_surbs( + &self, + sender_id: i64, + ) -> Result, sqlx::Error> { + sqlx::query_as!( + StoredReplySurb, + "SELECT * FROM reply_surb WHERE reply_surb_sender_id = ?", + sender_id + ) + .fetch_all(&self.connection_pool) + .await + } + + pub(crate) async fn delete_all_reply_surb_data(&self) -> Result<(), sqlx::Error> { + sqlx::query!("DELETE FROM reply_surb;") + .execute(&self.connection_pool) + .await?; + + sqlx::query!("DELETE FROM reply_surb_sender;") + .execute(&self.connection_pool) + .await?; + + Ok(()) + } + + pub(crate) async fn insert_reply_surb( + &self, + stored_reply_surb: StoredReplySurb, + ) -> Result<(), sqlx::Error> { + sqlx::query!( + r#" + INSERT INTO reply_surb(reply_surb_sender_id, reply_surb) VALUES (?, ?); + "#, + stored_reply_surb.reply_surb_sender_id, + stored_reply_surb.reply_surb + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + pub(crate) async fn get_reply_surb_storage_metadata( + &self, + ) -> Result { + sqlx::query_as!( + ReplySurbStorageMetadata, + r#" + SELECT min_reply_surb_threshold as "min_reply_surb_threshold: u32", max_reply_surb_threshold as "max_reply_surb_threshold: u32" FROM reply_surb_storage_metadata; + "#, + ) + .fetch_one(&self.connection_pool) + .await + } + + pub(crate) async fn insert_reply_surb_storage_metadata( + &self, + metadata: ReplySurbStorageMetadata, + ) -> Result<(), sqlx::Error> { + sqlx::query!(r#" + INSERT INTO reply_surb_storage_metadata(min_reply_surb_threshold, max_reply_surb_threshold) + VALUES (?, ?); + "#, + metadata.min_reply_surb_threshold, + metadata.max_reply_surb_threshold, + ).execute(&self.connection_pool).await?; + Ok(()) + } +} diff --git a/clients/client-core/src/client/replies/reply_storage/backend/fs_backend/mod.rs b/clients/client-core/src/client/replies/reply_storage/backend/fs_backend/mod.rs new file mode 100644 index 0000000000..50916e00c3 --- /dev/null +++ b/clients/client-core/src/client/replies/reply_storage/backend/fs_backend/mod.rs @@ -0,0 +1,336 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub use self::error::StorageError; +use crate::client::replies::reply_storage::backend::fs_backend::manager::StorageManager; +use crate::client::replies::reply_storage::backend::fs_backend::models::{ + ReplySurbStorageMetadata, StoredReplyKey, StoredReplySurb, StoredSenderTag, StoredSurbSender, +}; +use crate::client::replies::reply_storage::surb_storage::ReceivedReplySurbs; +use crate::client::replies::reply_storage::{ + CombinedReplyStorage, ReceivedReplySurbsMap, ReplyStorageBackend, SentReplyKeys, UsedSenderTags, +}; +use async_trait::async_trait; +use log::{error, info, warn}; +use nymsphinx::anonymous_replies::requests::AnonymousSenderTag; +use std::fs; +use std::path::{Path, PathBuf}; +use time::OffsetDateTime; + +mod error; +mod manager; +mod models; + +#[derive(Debug)] +pub struct Backend { + temporary_old_path: Option, + database_path: PathBuf, + manager: StorageManager, +} + +impl Backend { + const OLD_EXTENSION: &'static str = "old"; + + pub async fn init>(database_path: P) -> Result { + let owned_path: PathBuf = database_path.as_ref().into(); + if owned_path.file_name().is_none() { + return Err(StorageError::DatabasePathWithoutFilename { + provided_path: owned_path, + }); + } + + let backend = Backend { + temporary_old_path: None, + database_path: owned_path, + manager: StorageManager::init(database_path, true).await?, + }; + + backend.manager.create_status_table().await?; + + Ok(backend) + } + + pub async fn try_load>(database_path: P) -> Result { + let owned_path: PathBuf = database_path.as_ref().into(); + if owned_path.file_name().is_none() { + return Err(StorageError::DatabasePathWithoutFilename { + provided_path: owned_path, + }); + } + + let manager = StorageManager::init(database_path, false).await?; + + // the database flush wasn't fully finished and thus the data is in inconsistent state + // (we don't really know what's properly saved or what's not) + if manager.get_flush_status().await? { + return Err(StorageError::IncompleteDataFlush); + } + + // the process has gone down without full graceful shutdown, + // meaning the database doesn't contain valid data anymore + // so we have to purge it + if manager.get_client_in_use_status().await? { + error!("the client hasn't undergone through graceful shutdown the last time it's gone down - we can't trust its reply surbs or stored encryption keys. They shall get purged"); + manager.delete_all_reply_surb_data().await?; + manager.delete_all_reply_keys().await?; + } + + let last_flush_timestamp = manager.get_previous_flush_timestamp().await?; + let last_flush = match OffsetDateTime::from_unix_timestamp(last_flush_timestamp) { + Ok(last_flush) => last_flush, + Err(err) => { + return Err(StorageError::CorruptedData { + details: format!("failed to parse stored timestamp - {err}"), + }); + } + }; + + // in theory clients can use our reply surbs whenever they want, even a year in the future + // (assuming no key rotation has happened) + // but the way it's currently coded, everyone will purge old data + let since_last_flush = OffsetDateTime::now_utc() - last_flush; + if since_last_flush.whole_days() > 0 { + info!("it's been over {} days and {} hours since we last used our data store. our reply surbs are already outdated - we're going to purge them now.", since_last_flush.whole_days(), since_last_flush.whole_hours()); + manager.delete_all_reply_surb_data().await?; + } + + if since_last_flush.whole_days() > 1 { + info!("it's been over {} days and {} hours since we last used our data store. our reply keys are already outdated - we're going to purge them now.", since_last_flush.whole_days(), since_last_flush.whole_hours()); + manager.delete_all_reply_keys().await?; + } + + if since_last_flush.whole_days() > 2 { + info!("it's been over {} days and {} hours since we last used our data store. our used sender tags are already outdated - we're going to purge them now.", since_last_flush.whole_days(), since_last_flush.whole_hours()); + manager.delete_all_tags().await?; + } + + Ok(Backend { + temporary_old_path: None, + database_path: owned_path, + manager, + }) + } + + async fn close_pool(&mut self) { + self.manager.connection_pool.close().await; + } + + async fn rotate(&mut self) -> Result<(), StorageError> { + self.close_pool().await; + + let new_extension = if let Some(existing_extension) = + self.database_path.extension().and_then(|ext| ext.to_str()) + { + format!("{existing_extension}.{}", Self::OLD_EXTENSION) + } else { + Self::OLD_EXTENSION.to_string() + }; + + let mut temp_old = self.database_path.clone(); + temp_old.set_extension(new_extension); + + fs::rename(&self.database_path, &temp_old) + .map_err(|err| StorageError::DatabaseRenameError { source: err })?; + self.manager = StorageManager::init(&self.database_path, true).await?; + self.manager.create_status_table().await?; + + self.temporary_old_path = Some(temp_old); + Ok(()) + } + + fn remove_old(&mut self) -> Result<(), StorageError> { + if let Some(old_path) = self.temporary_old_path.take() { + fs::remove_file(old_path) + .map_err(|err| StorageError::DatabaseOldFileRemoveError { source: err }) + } else { + warn!("the old database file doesn't seem to exist!"); + Ok(()) + } + } + + async fn start_storage_flush(&self) -> Result<(), StorageError> { + Ok(self.manager.set_flush_status(true).await?) + } + + async fn end_storage_flush(&self) -> Result<(), StorageError> { + self.manager + .set_previous_flush_timestamp(OffsetDateTime::now_utc().unix_timestamp()) + .await?; + Ok(self.manager.set_flush_status(false).await?) + } + + async fn start_client_use(&self) -> Result<(), StorageError> { + Ok(self.manager.set_client_in_use_status(true).await?) + } + + async fn stop_client_use(&self) -> Result<(), StorageError> { + Ok(self.manager.set_client_in_use_status(false).await?) + } + + async fn get_stored_tags(&self) -> Result { + let stored = self.manager.get_tags().await?; + + // stop at the first instance of corruption. if even a single entry is malformed, + // something weird has happened and we can't trust the rest of the data + let raw = stored + .into_iter() + .map(TryInto::try_into) + .collect::>()?; + + Ok(UsedSenderTags::from_raw(raw)) + } + + async fn dump_sender_tags(&self, tags: &UsedSenderTags) -> Result<(), StorageError> { + for map_ref in tags.as_raw_iter() { + let (recipient, tag) = map_ref.pair(); + self.manager + .insert_tag(StoredSenderTag::new(*recipient, *tag)) + .await?; + } + Ok(()) + } + + async fn get_stored_reply_keys(&self) -> Result { + let stored = self.manager.get_reply_keys().await?; + + // stop at the first instance of corruption. if even a single entry is malformed, + // something weird has happened and we can't trust the rest of the data + let raw = stored + .into_iter() + .map(TryInto::try_into) + .collect::>()?; + + Ok(SentReplyKeys::from_raw(raw)) + } + + async fn dump_sender_reply_keys(&self, reply_keys: &SentReplyKeys) -> Result<(), StorageError> { + for map_ref in reply_keys.as_raw_iter() { + let (digest, key) = map_ref.pair(); + self.manager + .insert_reply_key(StoredReplyKey::new(*digest, *key)) + .await?; + } + Ok(()) + } + + async fn get_stored_reply_surbs(&self) -> Result { + let surb_senders = self.manager.get_surb_senders().await?; + + let metadata = self.get_reply_surb_storage_metadata().await?; + let mut received_surbs = Vec::with_capacity(surb_senders.len()); + for sender in surb_senders { + let sender_id = sender.id; + let (sender_tag, surbs_last_received_at_timestamp): (AnonymousSenderTag, i64) = + sender.try_into()?; + let stored_surbs = self + .manager + .get_reply_surbs(sender_id) + .await? + .into_iter() + .map(|raw| raw.try_into()) + .collect::>()?; + + received_surbs.push(( + sender_tag, + ReceivedReplySurbs::new_retrieved(stored_surbs, surbs_last_received_at_timestamp), + )) + } + + Ok(ReceivedReplySurbsMap::from_raw( + metadata.min_reply_surb_threshold as usize, + metadata.max_reply_surb_threshold as usize, + received_surbs, + )) + } + + async fn dump_reply_surbs( + &self, + reply_surbs: &ReceivedReplySurbsMap, + ) -> Result<(), StorageError> { + for map_ref in reply_surbs.as_raw_iter() { + let (tag, received_surbs) = map_ref.pair(); + let sender_id = self + .manager + .insert_surb_sender(StoredSurbSender::new( + *tag, + received_surbs.surbs_last_received_at(), + )) + .await?; + + for reply_surb in received_surbs.surbs_ref() { + self.manager + .insert_reply_surb(StoredReplySurb::new(sender_id, reply_surb)) + .await? + } + } + Ok(()) + } + + async fn get_reply_surb_storage_metadata( + &self, + ) -> Result { + self.manager + .get_reply_surb_storage_metadata() + .await + .map_err(Into::into) + } + + async fn dump_reply_surb_storage_metadata( + &self, + reply_surbs: &ReceivedReplySurbsMap, + ) -> Result<(), StorageError> { + self.manager + .insert_reply_surb_storage_metadata(ReplySurbStorageMetadata::new( + reply_surbs.min_surb_threshold(), + reply_surbs.max_surb_threshold(), + )) + .await + .map_err(Into::into) + } +} + +#[async_trait] +impl ReplyStorageBackend for Backend { + type StorageError = error::StorageError; + + async fn start_storage_session(&self) -> Result<(), Self::StorageError> { + self.start_client_use().await + } + + async fn flush_surb_storage( + &mut self, + storage: &CombinedReplyStorage, + ) -> Result<(), Self::StorageError> { + // close all connections (there should be none! and rename the file to contain .old extension) + self.rotate().await?; + self.start_storage_flush().await?; + + self.dump_sender_tags(storage.tags_storage_ref()).await?; + self.dump_sender_reply_keys(storage.key_storage_ref()) + .await?; + let surbs_ref = storage.surbs_storage_ref(); + self.dump_reply_surb_storage_metadata(surbs_ref).await?; + self.dump_reply_surbs(surbs_ref).await?; + + self.remove_old()?; + self.end_storage_flush().await + } + + async fn init_fresh(&mut self, fresh: &CombinedReplyStorage) -> Result<(), Self::StorageError> { + // for now nothing more to do apart from dumping the metadata + self.dump_reply_surb_storage_metadata(fresh.surbs_storage_ref()) + .await + } + + async fn load_surb_storage(&self) -> Result { + let reply_keys = self.get_stored_reply_keys().await?; + let tags = self.get_stored_tags().await?; + let reply_surbs = self.get_stored_reply_surbs().await?; + + Ok(CombinedReplyStorage::load(reply_keys, reply_surbs, tags)) + } + + async fn stop_storage_session(self) -> Result<(), Self::StorageError> { + self.stop_client_use().await + } +} diff --git a/clients/client-core/src/client/replies/reply_storage/backend/fs_backend/models.rs b/clients/client-core/src/client/replies/reply_storage/backend/fs_backend/models.rs new file mode 100644 index 0000000000..65088ad05e --- /dev/null +++ b/clients/client-core/src/client/replies/reply_storage/backend/fs_backend/models.rs @@ -0,0 +1,185 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::client::replies::reply_storage::backend::fs_backend::error::StorageError; +use crate::client::replies::reply_storage::key_storage::UsedReplyKey; +use crypto::generic_array::typenum::Unsigned; +use crypto::Digest; +use nymsphinx::addressing::clients::{Recipient, RecipientBytes}; +use nymsphinx::anonymous_replies::encryption_key::EncryptionKeyDigest; +use nymsphinx::anonymous_replies::requests::{AnonymousSenderTag, SENDER_TAG_SIZE}; +use nymsphinx::anonymous_replies::{ReplySurb, SurbEncryptionKey, SurbEncryptionKeySize}; +use nymsphinx::params::ReplySurbKeyDigestAlgorithm; + +#[derive(Debug, Clone)] +pub(crate) struct StoredSenderTag { + pub(crate) recipient: Vec, + pub(crate) tag: Vec, +} + +impl StoredSenderTag { + pub(crate) fn new(recipient: RecipientBytes, tag: AnonymousSenderTag) -> StoredSenderTag { + StoredSenderTag { + recipient: recipient.to_vec(), + tag: tag.to_bytes().to_vec(), + } + } +} + +impl TryFrom for (RecipientBytes, AnonymousSenderTag) { + type Error = StorageError; + + fn try_from(value: StoredSenderTag) -> Result { + let recipient_len = value.recipient.len(); + let Ok(recipient_bytes) = value.recipient.try_into() else { + return Err(StorageError::CorruptedData { + details: format!( + "the retrieved recipient has length of {recipient_len} while {} was expected", + Recipient::LEN + ), + }); + }; + + let tag_len = value.tag.len(); + let Ok(sender_tag_bytes) = value.tag.try_into() else { + return Err(StorageError::CorruptedData { + details: format!( + "the retrieved sender tag has length of {tag_len} while {} was expected", + SENDER_TAG_SIZE + ), + }); + }; + + Ok(( + recipient_bytes, + AnonymousSenderTag::from_bytes(sender_tag_bytes), + )) + } +} + +#[derive(Debug, Clone)] +pub(crate) struct StoredReplyKey { + pub(crate) key_digest: Vec, + pub(crate) reply_key: Vec, + pub(crate) sent_at_timestamp: i64, +} + +impl StoredReplyKey { + pub(crate) fn new(key_digest: EncryptionKeyDigest, reply_key: UsedReplyKey) -> StoredReplyKey { + StoredReplyKey { + key_digest: key_digest.to_vec(), + reply_key: (*reply_key).to_bytes(), + sent_at_timestamp: reply_key.sent_at_timestamp, + } + } +} + +impl TryFrom for (EncryptionKeyDigest, UsedReplyKey) { + type Error = StorageError; + + fn try_from(value: StoredReplyKey) -> Result { + let expected_reply_key_digest_size = ReplySurbKeyDigestAlgorithm::output_size(); + let reply_key_digest_size = value.key_digest.len(); + + let Some(digest) = EncryptionKeyDigest::from_exact_iter(value.key_digest) else { + return Err(StorageError::CorruptedData { + details: format!( + "the reply surb digest has length of {reply_key_digest_size} while {expected_reply_key_digest_size} was expected", + ), + }); + }; + + let reply_key_len = value.reply_key.len(); + let Ok(reply_key) = SurbEncryptionKey::try_from_bytes(&value.reply_key) else { + return Err(StorageError::CorruptedData { + details: format!( + "the reply key has length of {reply_key_len} while {} was expected", + SurbEncryptionKeySize::USIZE + ), + }); + }; + + Ok(( + digest, + UsedReplyKey::new(reply_key, value.sent_at_timestamp), + )) + } +} + +pub(crate) struct StoredSurbSender { + pub(crate) id: i64, + pub(crate) tag: Vec, + pub(crate) last_sent_timestamp: i64, +} + +impl StoredSurbSender { + pub(crate) fn new(tag: AnonymousSenderTag, last_sent_timestamp: i64) -> Self { + StoredSurbSender { + // for the purposes of STORING data, + // we ignore that field anyway + id: 0, + tag: tag.to_bytes().to_vec(), + last_sent_timestamp, + } + } +} + +impl TryFrom for (AnonymousSenderTag, i64) { + type Error = StorageError; + + fn try_from(value: StoredSurbSender) -> Result { + let tag_len = value.tag.len(); + let Ok(sender_tag_bytes) = value.tag.try_into() else { + return Err(StorageError::CorruptedData { + details: format!( + "the retrieved sender tag has length of {tag_len} while {} was expected", + SENDER_TAG_SIZE + ), + }); + }; + + Ok(( + AnonymousSenderTag::from_bytes(sender_tag_bytes), + value.last_sent_timestamp, + )) + } +} + +pub(crate) struct StoredReplySurb { + pub(crate) reply_surb_sender_id: i64, + pub(crate) reply_surb: Vec, +} + +impl StoredReplySurb { + pub(crate) fn new(reply_surb_sender_id: i64, reply_surb: &ReplySurb) -> Self { + StoredReplySurb { + reply_surb_sender_id, + reply_surb: reply_surb.to_bytes(), + } + } +} + +impl TryFrom for ReplySurb { + type Error = StorageError; + + fn try_from(value: StoredReplySurb) -> Result { + ReplySurb::from_bytes(&value.reply_surb).map_err(|err| StorageError::CorruptedData { + details: format!("failed to recover the reply surb: {err}"), + }) + } +} + +#[derive(Copy, Clone)] +pub(crate) struct ReplySurbStorageMetadata { + pub(crate) min_reply_surb_threshold: u32, + pub(crate) max_reply_surb_threshold: u32, +} + +impl ReplySurbStorageMetadata { + pub(crate) fn new(min_reply_surb_threshold: usize, max_reply_surb_threshold: usize) -> Self { + Self { + min_reply_surb_threshold: min_reply_surb_threshold as u32, + max_reply_surb_threshold: max_reply_surb_threshold as u32, + } + } +} diff --git a/clients/client-core/src/client/replies/reply_storage/backend/mod.rs b/clients/client-core/src/client/replies/reply_storage/backend/mod.rs new file mode 100644 index 0000000000..d80231ae1e --- /dev/null +++ b/clients/client-core/src/client/replies/reply_storage/backend/mod.rs @@ -0,0 +1,78 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::client::replies::reply_storage::CombinedReplyStorage; +use async_trait::async_trait; +use std::error::Error; +use thiserror::Error; + +#[cfg(target_arch = "wasm32")] +pub mod browser_backend; + +#[cfg(all(not(target_arch = "wasm32"), feature = "fs-surb-storage"))] +pub mod fs_backend; + +// #[cfg(all(test, feature = "std"))] +// third case: node with actual filesystem + +#[derive(Debug, Error)] +#[error("no information provided")] +pub struct UndefinedError; + +pub struct Empty { + // we need to keep 'basic' metadata here to "load" the CombinedReplyStorage + min_surb_threshold: usize, + max_surb_threshold: usize, +} + +#[async_trait] +impl ReplyStorageBackend for Empty { + type StorageError = UndefinedError; + + async fn flush_surb_storage( + &mut self, + _storage: &CombinedReplyStorage, + ) -> Result<(), Self::StorageError> { + Ok(()) + } + + async fn init_fresh( + &mut self, + _fresh: &CombinedReplyStorage, + ) -> Result<(), Self::StorageError> { + Ok(()) + } + + async fn load_surb_storage(&self) -> Result { + Ok(CombinedReplyStorage::new( + self.min_surb_threshold, + self.max_surb_threshold, + )) + } +} + +#[async_trait] +pub trait ReplyStorageBackend: Sized { + type StorageError: Error + 'static; + + async fn start_storage_session(&self) -> Result<(), Self::StorageError> { + Ok(()) + } + + // reply keys and surbs would need additional field set when data is loaded + // so if there's some failure, we'd trash it all + async fn flush_surb_storage( + &mut self, + storage: &CombinedReplyStorage, + ) -> Result<(), Self::StorageError>; + + /// The purpose of this call is to save any metadata that might be present. + /// (such as surb thresholds) + async fn init_fresh(&mut self, fresh: &CombinedReplyStorage) -> Result<(), Self::StorageError>; + + async fn load_surb_storage(&self) -> Result; + + async fn stop_storage_session(self) -> Result<(), Self::StorageError> { + Ok(()) + } +} diff --git a/clients/client-core/src/client/replies/reply_storage/combined.rs b/clients/client-core/src/client/replies/reply_storage/combined.rs new file mode 100644 index 0000000000..c9fb9e4a5c --- /dev/null +++ b/clients/client-core/src/client/replies/reply_storage/combined.rs @@ -0,0 +1,60 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::client::replies::reply_storage::{ReceivedReplySurbsMap, SentReplyKeys, UsedSenderTags}; + +#[derive(Debug, Clone)] +pub struct CombinedReplyStorage { + sent_reply_keys: SentReplyKeys, + received_reply_surbs: ReceivedReplySurbsMap, + used_tags: UsedSenderTags, +} + +impl CombinedReplyStorage { + pub fn new(min_surb_threshold: usize, max_surb_threshold: usize) -> CombinedReplyStorage { + CombinedReplyStorage { + sent_reply_keys: SentReplyKeys::new(), + received_reply_surbs: ReceivedReplySurbsMap::new( + min_surb_threshold, + max_surb_threshold, + ), + used_tags: UsedSenderTags::new(), + } + } + + pub fn load( + sent_reply_keys: SentReplyKeys, + received_reply_surbs: ReceivedReplySurbsMap, + used_tags: UsedSenderTags, + ) -> Self { + CombinedReplyStorage { + sent_reply_keys, + received_reply_surbs, + used_tags, + } + } + + pub fn key_storage(&self) -> SentReplyKeys { + self.sent_reply_keys.clone() + } + + pub fn surbs_storage(&self) -> ReceivedReplySurbsMap { + self.received_reply_surbs.clone() + } + + pub fn tags_storage(&self) -> UsedSenderTags { + self.used_tags.clone() + } + + pub fn key_storage_ref(&self) -> &SentReplyKeys { + &self.sent_reply_keys + } + + pub fn surbs_storage_ref(&self) -> &ReceivedReplySurbsMap { + &self.received_reply_surbs + } + + pub fn tags_storage_ref(&self) -> &UsedSenderTags { + &self.used_tags + } +} diff --git a/clients/client-core/src/client/replies/reply_storage/key_storage.rs b/clients/client-core/src/client/replies/reply_storage/key_storage.rs new file mode 100644 index 0000000000..9c68f40e06 --- /dev/null +++ b/clients/client-core/src/client/replies/reply_storage/key_storage.rs @@ -0,0 +1,86 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use dashmap::iter::Iter; +use dashmap::DashMap; +use nymsphinx::anonymous_replies::encryption_key::EncryptionKeyDigest; +use nymsphinx::anonymous_replies::SurbEncryptionKey; +use std::ops::Deref; +use std::sync::Arc; +use time::OffsetDateTime; + +#[derive(Debug, Clone)] +pub struct SentReplyKeys { + inner: Arc, +} + +#[derive(Debug)] +struct SentReplyKeysInner { + data: DashMap, +} + +impl SentReplyKeys { + pub(crate) fn new() -> SentReplyKeys { + SentReplyKeys { + inner: Arc::new(SentReplyKeysInner { + data: DashMap::new(), + }), + } + } + + #[cfg(all(not(target_arch = "wasm32"), feature = "fs-surb-storage"))] + pub(crate) fn from_raw(raw: Vec<(EncryptionKeyDigest, UsedReplyKey)>) -> SentReplyKeys { + SentReplyKeys { + inner: Arc::new(SentReplyKeysInner { + data: raw.into_iter().collect(), + }), + } + } + + pub(crate) fn as_raw_iter(&self) -> Iter<'_, EncryptionKeyDigest, UsedReplyKey> { + self.inner.data.iter() + } + + pub(crate) fn insert_multiple(&self, keys: Vec) { + let now = OffsetDateTime::now_utc().unix_timestamp(); + for key in keys { + self.insert(UsedReplyKey::new(key, now)) + } + } + + pub(crate) fn insert(&self, key: UsedReplyKey) { + self.inner.data.insert(key.compute_digest(), key); + } + + pub(crate) fn try_pop(&self, digest: EncryptionKeyDigest) -> Option { + self.inner.data.remove(&digest).map(|(_k, v)| v) + } + + pub(crate) fn remove(&self, digest: EncryptionKeyDigest) { + self.inner.data.remove(&digest); + } +} + +#[derive(Debug, Copy, Clone)] +pub(crate) struct UsedReplyKey { + key: SurbEncryptionKey, + // the purpose of this field is to perform invalidation at relatively very long intervals + pub(crate) sent_at_timestamp: i64, +} + +impl UsedReplyKey { + pub(crate) fn new(key: SurbEncryptionKey, sent_at_timestamp: i64) -> Self { + UsedReplyKey { + key, + sent_at_timestamp, + } + } +} + +impl Deref for UsedReplyKey { + type Target = SurbEncryptionKey; + + fn deref(&self) -> &Self::Target { + &self.key + } +} diff --git a/clients/client-core/src/client/replies/reply_storage/mod.rs b/clients/client-core/src/client/replies/reply_storage/mod.rs new file mode 100644 index 0000000000..83c0e0e34f --- /dev/null +++ b/clients/client-core/src/client/replies/reply_storage/mod.rs @@ -0,0 +1,64 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub use crate::client::replies::reply_storage::combined::CombinedReplyStorage; +pub use crate::client::replies::reply_storage::key_storage::SentReplyKeys; +pub use crate::client::replies::reply_storage::surb_storage::ReceivedReplySurbsMap; +pub use crate::client::replies::reply_storage::tag_storage::UsedSenderTags; +pub use backend::*; + +mod backend; +mod combined; +mod key_storage; +mod surb_storage; +mod tag_storage; + +// only really exists to get information about shutdown and save data to the backing storage +pub struct PersistentReplyStorage +where + T: ReplyStorageBackend, +{ + backend: T, +} + +impl PersistentReplyStorage +where + T: ReplyStorageBackend + Send + Sync, +{ + pub fn new(backend: T) -> Self { + PersistentReplyStorage { backend } + } + + pub async fn load_state_from_backend(&self) -> Result { + self.backend.load_surb_storage().await + } + + // this will have to get enabled after merging develop + pub async fn flush_on_shutdown( + mut self, + mem_state: CombinedReplyStorage, + mut shutdown: task::ShutdownListener, + ) { + use log::{debug, error, info, warn}; + + debug!("Started PersistentReplyStorage"); + if let Err(err) = self.backend.start_storage_session().await { + error!("failed to start the storage session - {err}"); + return; + } + + shutdown.recv().await; + + info!("PersistentReplyStorage is flushing all reply-related data to underlying storage"); + warn!("you MUST NOT forcefully shutdown now or you risk data corruption!"); + if let Err(err) = self.backend.flush_surb_storage(&mem_state).await { + error!("failed to flush our reply-related data to the persistent storage: {err}") + } else { + info!("Data flush is complete") + } + + if let Err(err) = self.backend.stop_storage_session().await { + error!("failed to properly stop the storage session - {err}. We might not be able to smoothly restore it") + } + } +} diff --git a/clients/client-core/src/client/replies/reply_storage/surb_storage.rs b/clients/client-core/src/client/replies/reply_storage/surb_storage.rs new file mode 100644 index 0000000000..ed2fd8d846 --- /dev/null +++ b/clients/client-core/src/client/replies/reply_storage/surb_storage.rs @@ -0,0 +1,278 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use dashmap::iter::Iter; +use dashmap::DashMap; +use log::trace; +use nymsphinx::anonymous_replies::requests::AnonymousSenderTag; +use nymsphinx::anonymous_replies::ReplySurb; +use std::collections::VecDeque; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::Arc; +use time::OffsetDateTime; + +#[derive(Debug, Clone)] +pub struct ReceivedReplySurbsMap { + inner: Arc, +} + +#[derive(Debug)] +struct ReceivedReplySurbsMapInner { + data: DashMap, + + // the minimum amount of surbs that have to be kept in storage for requests for more surbs + min_surb_threshold: AtomicUsize, + + // the maximum amount of surbs that we want to keep in storage so that we don't over-request them + max_surb_threshold: AtomicUsize, +} + +impl ReceivedReplySurbsMap { + pub(crate) fn new( + min_surb_threshold: usize, + max_surb_threshold: usize, + ) -> ReceivedReplySurbsMap { + ReceivedReplySurbsMap { + inner: Arc::new(ReceivedReplySurbsMapInner { + data: DashMap::new(), + min_surb_threshold: AtomicUsize::new(min_surb_threshold), + max_surb_threshold: AtomicUsize::new(max_surb_threshold), + }), + } + } + + #[cfg(all(not(target_arch = "wasm32"), feature = "fs-surb-storage"))] + pub(crate) fn from_raw( + min_surb_threshold: usize, + max_surb_threshold: usize, + raw: Vec<(AnonymousSenderTag, ReceivedReplySurbs)>, + ) -> ReceivedReplySurbsMap { + ReceivedReplySurbsMap { + inner: Arc::new(ReceivedReplySurbsMapInner { + data: raw.into_iter().collect(), + min_surb_threshold: AtomicUsize::new(min_surb_threshold), + max_surb_threshold: AtomicUsize::new(max_surb_threshold), + }), + } + } + + pub(crate) fn as_raw_iter(&self) -> Iter<'_, AnonymousSenderTag, ReceivedReplySurbs> { + self.inner.data.iter() + } + + pub(crate) fn remove(&self, target: &AnonymousSenderTag) { + self.inner.data.remove(target); + } + + pub(crate) fn reset_surbs_last_received_at(&self, target: &AnonymousSenderTag) { + if let Some(mut entry) = self.inner.data.get_mut(target) { + entry.surbs_last_received_at_timestamp = OffsetDateTime::now_utc().unix_timestamp(); + } + } + + pub(crate) fn surbs_last_received_at(&self, target: &AnonymousSenderTag) -> Option { + self.inner + .data + .get(target) + .map(|e| e.surbs_last_received_at()) + } + + pub(crate) fn pending_reception(&self, target: &AnonymousSenderTag) -> u32 { + self.inner + .data + .get(target) + .map(|e| e.pending_reception()) + .unwrap_or_default() + } + + pub(crate) fn increment_pending_reception( + &self, + target: &AnonymousSenderTag, + amount: u32, + ) -> Option { + self.inner + .data + .get_mut(target) + .map(|mut e| e.increment_pending_reception(amount)) + } + + pub(crate) fn decrement_pending_reception( + &self, + target: &AnonymousSenderTag, + amount: u32, + ) -> Option { + self.inner + .data + .get_mut(target) + .map(|mut e| e.decrement_pending_reception(amount)) + } + + pub(crate) fn reset_pending_reception(&self, target: &AnonymousSenderTag) { + if let Some(mut e) = self.inner.data.get_mut(target) { + e.reset_pending_reception() + } + } + + pub(crate) fn min_surb_threshold(&self) -> usize { + self.inner.min_surb_threshold.load(Ordering::Relaxed) + } + + pub(crate) fn max_surb_threshold(&self) -> usize { + self.inner.max_surb_threshold.load(Ordering::Relaxed) + } + + pub(crate) fn available_surbs(&self, target: &AnonymousSenderTag) -> usize { + self.inner + .data + .get(target) + .map(|entry| entry.items_left()) + .unwrap_or_default() + } + + pub(crate) fn contains_surbs_for(&self, target: &AnonymousSenderTag) -> bool { + self.inner.data.contains_key(target) + } + + pub(crate) fn get_reply_surbs( + &self, + target: &AnonymousSenderTag, + amount: usize, + ) -> (Option>, usize) { + if let Some(mut entry) = self.inner.data.get_mut(target) { + let surbs_left = entry.items_left(); + if surbs_left < self.min_surb_threshold() + amount { + (None, surbs_left) + } else { + entry.get_reply_surbs(amount) + } + } else { + (None, 0) + } + } + + pub(crate) fn get_reply_surb_ignoring_threshold( + &self, + target: &AnonymousSenderTag, + ) -> Option<(Option, usize)> { + self.inner + .data + .get_mut(target) + .map(|mut s| s.get_reply_surb()) + } + + pub(crate) fn get_reply_surb( + &self, + target: &AnonymousSenderTag, + ) -> Option<(Option, usize)> { + self.inner.data.get_mut(target).map(|mut entry| { + let surbs_left = entry.items_left(); + if surbs_left < self.min_surb_threshold() { + (None, surbs_left) + } else { + entry.get_reply_surb() + } + }) + } + + pub(crate) fn insert_surbs>( + &self, + target: &AnonymousSenderTag, + surbs: I, + ) { + if let Some(mut existing_data) = self.inner.data.get_mut(target) { + existing_data.insert_reply_surbs(surbs) + } else { + let new_entry = ReceivedReplySurbs::new(surbs.into_iter().collect()); + self.inner.data.insert(*target, new_entry); + } + } +} + +#[derive(Debug)] +pub(crate) struct ReceivedReplySurbs { + // in the future we'd probably want to put extra data here to indicate when the SURBs got received + // so we could invalidate entries from the previous key rotations + data: VecDeque, + + pending_reception: u32, + surbs_last_received_at_timestamp: i64, +} + +impl ReceivedReplySurbs { + fn new(initial_surbs: VecDeque) -> Self { + ReceivedReplySurbs { + data: initial_surbs, + pending_reception: 0, + surbs_last_received_at_timestamp: OffsetDateTime::now_utc().unix_timestamp(), + } + } + + #[cfg(all(not(target_arch = "wasm32"), feature = "fs-surb-storage"))] + pub(crate) fn new_retrieved( + surbs: Vec, + surbs_last_received_at_timestamp: i64, + ) -> ReceivedReplySurbs { + ReceivedReplySurbs { + data: surbs.into(), + pending_reception: 0, + surbs_last_received_at_timestamp, + } + } + + #[cfg(all(not(target_arch = "wasm32"), feature = "fs-surb-storage"))] + pub(crate) fn surbs_ref(&self) -> &VecDeque { + &self.data + } + + pub(crate) fn surbs_last_received_at(&self) -> i64 { + self.surbs_last_received_at_timestamp + } + + pub(crate) fn pending_reception(&self) -> u32 { + self.pending_reception + } + + pub(crate) fn increment_pending_reception(&mut self, amount: u32) -> u32 { + self.pending_reception += amount; + self.pending_reception + } + + pub(crate) fn decrement_pending_reception(&mut self, amount: u32) -> u32 { + self.pending_reception = self.pending_reception.saturating_sub(amount); + self.pending_reception + } + + pub(crate) fn reset_pending_reception(&mut self) { + self.pending_reception = 0; + } + + pub(crate) fn get_reply_surbs(&mut self, amount: usize) -> (Option>, usize) { + if self.items_left() < amount { + (None, self.items_left()) + } else { + let surbs = self.data.drain(..amount).collect(); + (Some(surbs), self.items_left()) + } + } + + pub(crate) fn get_reply_surb(&mut self) -> (Option, usize) { + (self.pop_surb(), self.items_left()) + } + + fn pop_surb(&mut self) -> Option { + self.data.pop_front() + } + + fn items_left(&self) -> usize { + self.data.len() + } + + // realistically we're always going to be getting multiple surbs at once + pub(crate) fn insert_reply_surbs>(&mut self, surbs: I) { + let mut v = surbs.into_iter().collect::>(); + trace!("storing {} surbs in the storage", v.len()); + self.data.append(&mut v); + self.surbs_last_received_at_timestamp = OffsetDateTime::now_utc().unix_timestamp(); + trace!("we now have {} surbs!", self.data.len()); + } +} diff --git a/clients/client-core/src/client/replies/reply_storage/tag_storage.rs b/clients/client-core/src/client/replies/reply_storage/tag_storage.rs new file mode 100644 index 0000000000..c91d2b003e --- /dev/null +++ b/clients/client-core/src/client/replies/reply_storage/tag_storage.rs @@ -0,0 +1,59 @@ +// Copyright 2021 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use dashmap::DashMap; +use nymsphinx::addressing::clients::{Recipient, RecipientBytes}; +use nymsphinx::anonymous_replies::requests::AnonymousSenderTag; +use std::sync::Arc; + +#[cfg(all(not(target_arch = "wasm32"), feature = "fs-surb-storage"))] +use dashmap::iter::Iter; + +#[derive(Debug, Clone)] +pub struct UsedSenderTags { + inner: Arc, +} + +#[derive(Debug)] +struct UsedSenderTagsInner { + data: DashMap, +} + +impl UsedSenderTags { + pub(crate) fn new() -> UsedSenderTags { + UsedSenderTags { + inner: Arc::new(UsedSenderTagsInner { + data: DashMap::new(), + }), + } + } + + #[cfg(all(not(target_arch = "wasm32"), feature = "fs-surb-storage"))] + pub(crate) fn from_raw(raw: Vec<(RecipientBytes, AnonymousSenderTag)>) -> UsedSenderTags { + UsedSenderTags { + inner: Arc::new(UsedSenderTagsInner { + data: raw.into_iter().collect(), + }), + } + } + + #[cfg(all(not(target_arch = "wasm32"), feature = "fs-surb-storage"))] + pub(crate) fn as_raw_iter(&self) -> Iter<'_, RecipientBytes, AnonymousSenderTag> { + self.inner.data.iter() + } + + pub(crate) fn insert_new(&self, recipient: &Recipient, tag: AnonymousSenderTag) { + self.inner.data.insert(recipient.to_bytes(), tag); + } + + pub(crate) fn try_get_existing(&self, recipient: &Recipient) -> Option { + self.inner + .data + .get(&recipient.to_bytes()) + .map(|r| *r.value()) + } + + pub(crate) fn exists(&self, recipient: &Recipient) -> bool { + self.inner.data.contains_key(&recipient.to_bytes()) + } +} diff --git a/clients/client-core/src/client/reply_key_storage.rs b/clients/client-core/src/client/reply_key_storage.rs deleted file mode 100644 index bfe1c43b9b..0000000000 --- a/clients/client-core/src/client/reply_key_storage.rs +++ /dev/null @@ -1,97 +0,0 @@ -// Copyright 2021 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crypto::generic_array::typenum::Unsigned; -use log::*; -use nymsphinx::anonymous_replies::{ - encryption_key::EncryptionKeyDigest, SurbEncryptionKey, SurbEncryptionKeySize, -}; -use std::path::Path; - -#[derive(Debug, thiserror::Error)] -pub enum ReplyKeyStorageError { - #[error("DB Read Error: {0}")] - DbReadError(sled::Error), - #[error("DB Write Error: {0}")] - DbWriteError(sled::Error), - #[error("DB Open Error: {0}")] - DbOpenError(sled::Error), -} - -/// Permanent storage for keys in all sent [`ReplySURB`] -/// -/// Each sent out [`ReplySURB`] has a new key associated with it that is going to be used for -/// payload encryption. In order to -decrypt whatever reply we receive, we need to know which -/// key to use for that purpose. We do it based on received `H(t)` which has to be included -/// with each reply. -/// Moreover, there is no restriction when the [`ReplySURB`] might get used so we need to -/// have a permanent storage for all the keys that we might ever see in the future. -#[derive(Debug, Clone)] -pub struct ReplyKeyStorage { - db: sled::Db, -} - -impl ReplyKeyStorage { - pub fn load>(path: P) -> Result { - let db = match sled::open(path) { - Err(e) => return Err(ReplyKeyStorageError::DbOpenError(e)), - Ok(db) => db, - }; - - Ok(ReplyKeyStorage { db }) - } - - fn read_encryption_key(&self, raw_key: sled::IVec) -> SurbEncryptionKey { - let key_bytes_ref = raw_key.as_ref(); - // if this fails it means we have some database corruption and we - // absolutely can't continue - - if key_bytes_ref.len() != SurbEncryptionKeySize::USIZE { - error!("REPLY KEY STORAGE DATA CORRUPTION - ENCRYPTION KEY HAS INVALID LENGTH"); - panic!("REPLY KEY STORAGE DATA CORRUPTION - ENCRYPTION KEY HAS INVALID LENGTH"); - } - - // this can only fail if the bytes have invalid length but we already asserted it - SurbEncryptionKey::try_from_bytes(key_bytes_ref).unwrap() - } - - // TOOD: perhaps we could also store some part of original message here too? - pub fn insert_encryption_key( - &mut self, - encryption_key: SurbEncryptionKey, - ) -> Result<(), ReplyKeyStorageError> { - let digest = encryption_key.compute_digest(); - - let insertion_result = match self.db.insert(digest, encryption_key.to_bytes()) { - Err(e) => Err(ReplyKeyStorageError::DbWriteError(e)), - Ok(existing_key) => { - if existing_key.is_some() { - panic!("HASH COLLISION DETECTED") - }; - Ok(()) - } - }; - - // TODO: perhaps we could implement some batching mechanism to avoid frequent flushes? - self.db.flush().unwrap(); - insertion_result - } - - // Once we use key once, we do not expect to use it again - pub fn get_and_remove_encryption_key( - &self, - key_digest: EncryptionKeyDigest, - ) -> Result, ReplyKeyStorageError> { - let removal_result = match self.db.remove(key_digest) { - Err(e) => Err(ReplyKeyStorageError::DbReadError(e)), - Ok(existing_key) => { - Ok(existing_key.map(|existing_key| self.read_encryption_key(existing_key))) - } - }; - - // TODO: not sure how to feel about flushing it every single time here... - // same with insertion - self.db.flush().unwrap(); - removal_result - } -} diff --git a/clients/client-core/src/client/topology_control.rs b/clients/client-core/src/client/topology_control.rs index 9f35f6c3f1..ad9e4a4ce6 100644 --- a/clients/client-core/src/client/topology_control.rs +++ b/clients/client-core/src/client/topology_control.rs @@ -1,7 +1,8 @@ -// Copyright 2021 - Nym Technologies SA +// Copyright 2021-2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 use crate::spawn_future; +use futures::StreamExt; use log::*; use nymsphinx::addressing::clients::Recipient; use nymsphinx::params::DEFAULT_NUM_MIX_HOPS; @@ -9,10 +10,9 @@ use rand::seq::SliceRandom; use rand::thread_rng; use std::ops::Deref; use std::sync::Arc; -use std::time; use std::time::Duration; use tokio::sync::{RwLock, RwLockReadGuard}; -use topology::{nym_topology_from_detailed, NymTopology}; +use topology::{nym_topology_from_detailed, NymTopology, NymTopologyError}; use url::Url; // I'm extremely curious why compiler NEVER complained about lack of Debug here before @@ -54,18 +54,36 @@ impl<'a> TopologyReadPermit<'a> { &'a self, ack_recipient: &Recipient, packet_recipient: Option<&Recipient>, - ) -> Option<&'a NymTopology> { - // Note: implicit deref with Deref for TopologyReadPermit is happening here - let topology_ref_option = self.permit.as_ref(); - topology_ref_option.as_ref().filter(|topology_ref| { - !(!topology_ref.can_construct_path_through(DEFAULT_NUM_MIX_HOPS) - || !topology_ref.gateway_exists(ack_recipient.gateway()) - || if let Some(packet_recipient) = packet_recipient { - !topology_ref.gateway_exists(packet_recipient.gateway()) - } else { - false - }) - }) + ) -> Result<&'a NymTopology, NymTopologyError> { + // 1. Have we managed to get anything from the refresher, i.e. have the nym-api queries gone through? + let topology = self + .permit + .as_ref() + .as_ref() + .ok_or(NymTopologyError::EmptyNetworkTopology)?; + + // 2. does it have any mixnode at all? + // 3. does it have any gateways at all? + // 4. does it have a mixnode on each layer? + topology.ensure_can_construct_path_through(DEFAULT_NUM_MIX_HOPS)?; + + // 5. does it contain OUR gateway (so that we could create an ack packet)? + if !topology.gateway_exists(ack_recipient.gateway()) { + return Err(NymTopologyError::NonExistentGatewayError { + identity_key: ack_recipient.gateway().to_base58_string(), + }); + } + + // 6. for our target recipient, does it contain THEIR gateway (so that we could create + if let Some(recipient) = packet_recipient { + if !topology.gateway_exists(recipient.gateway()) { + return Err(NymTopologyError::NonExistentGatewayError { + identity_key: recipient.gateway().to_base58_string(), + }); + } + } + + Ok(topology) } } @@ -103,10 +121,10 @@ impl TopologyAccessor { // only used by the client at startup to get a slightly more reasonable error message // (currently displays as unused because health checker is disabled due to required changes) - pub async fn is_routable(&self) -> bool { + pub async fn ensure_is_routable(&self) -> Result<(), NymTopologyError> { match &self.inner.read().await.0 { - None => false, - Some(ref topology) => topology.can_construct_path_through(DEFAULT_NUM_MIX_HOPS), + None => Err(NymTopologyError::EmptyNetworkTopology), + Some(ref topology) => topology.ensure_can_construct_path_through(DEFAULT_NUM_MIX_HOPS), } } } @@ -119,14 +137,14 @@ impl Default for TopologyAccessor { pub struct TopologyRefresherConfig { validator_api_urls: Vec, - refresh_rate: time::Duration, + refresh_rate: Duration, client_version: String, } impl TopologyRefresherConfig { pub fn new( validator_api_urls: Vec, - refresh_rate: time::Duration, + refresh_rate: Duration, client_version: String, ) -> Self { TopologyRefresherConfig { @@ -240,7 +258,7 @@ impl TopologyRefresher { let mixnodes = match self.validator_client.get_cached_active_mixnodes().await { Err(err) => { - error!("failed to get network mixnodes - {}", err); + error!("failed to get network mixnodes - {err}"); return None; } Ok(mixes) => mixes, @@ -248,7 +266,7 @@ impl TopologyRefresher { let gateways = match self.validator_client.get_cached_gateways().await { Err(err) => { - error!("failed to get network gateways - {}", err); + error!("failed to get network gateways - {err}"); return None; } Ok(gateways) => gateways, @@ -288,18 +306,26 @@ impl TopologyRefresher { .await; } - pub async fn is_topology_routable(&self) -> bool { - self.topology_accessor.is_routable().await + pub async fn ensure_topology_is_routable(&self) -> Result<(), NymTopologyError> { + self.topology_accessor.ensure_is_routable().await } - #[cfg(not(target_arch = "wasm32"))] pub fn start_with_shutdown(mut self, mut shutdown: task::ShutdownListener) { spawn_future(async move { debug!("Started TopologyRefresher with graceful shutdown support"); + #[cfg(not(target_arch = "wasm32"))] + let mut interval = tokio_stream::wrappers::IntervalStream::new(tokio::time::interval( + self.refresh_rate, + )); + + #[cfg(target_arch = "wasm32")] + let mut interval = + gloo_timers::future::IntervalStream::new(self.refresh_rate.as_millis() as u32); + while !shutdown.is_shutdown() { tokio::select! { - _ = tokio::time::sleep(self.refresh_rate) => { + _ = interval.next() => { self.refresh().await; }, _ = shutdown.recv() => { @@ -307,23 +333,8 @@ impl TopologyRefresher { }, } } - tokio::time::timeout(Duration::from_secs(5), shutdown.recv()) - .await - .expect("Task stopped without shutdown called"); + shutdown.recv_timeout().await; log::debug!("TopologyRefresher: Exiting"); }) } - - #[cfg(target_arch = "wasm32")] - pub fn start(mut self) { - use futures::StreamExt; - - spawn_future(async move { - let mut interval = - gloo_timers::future::IntervalStream::new(self.refresh_rate.as_millis() as u32); - while let Some(_) = interval.next().await { - self.refresh().await; - } - }) - } } diff --git a/clients/client-core/src/config/mod.rs b/clients/client-core/src/config/mod.rs index b3dc70eb28..0ce94ba646 100644 --- a/clients/client-core/src/config/mod.rs +++ b/clients/client-core/src/config/mod.rs @@ -30,11 +30,37 @@ const DEFAULT_TOPOLOGY_RESOLUTION_TIMEOUT: Duration = Duration::from_millis(5_00 // bandwidth bridging protocol, we can come back to a smaller timeout value const DEFAULT_GATEWAY_RESPONSE_TIMEOUT: Duration = Duration::from_secs(5 * 60); +// reply-surbs related: + +// define when to request +// clients/client-core/src/client/replies/reply_storage/surb_storage.rs +const DEFAULT_MINIMUM_REPLY_SURB_STORAGE_THRESHOLD: usize = 10; +const DEFAULT_MAXIMUM_REPLY_SURB_STORAGE_THRESHOLD: usize = 200; + +// define how much to request at once +// clients/client-core/src/client/replies/reply_controller.rs +const DEFAULT_MINIMUM_REPLY_SURB_REQUEST_SIZE: u32 = 10; +const DEFAULT_MAXIMUM_REPLY_SURB_REQUEST_SIZE: u32 = 100; + +const DEFAULT_MAXIMUM_ALLOWED_SURB_REQUEST_SIZE: u32 = 500; + +const DEFAULT_MAXIMUM_REPLY_SURB_WAITING_PERIOD: Duration = Duration::from_secs(10); + +// 12 hours +const DEFAULT_MAXIMUM_REPLY_SURB_AGE: Duration = Duration::from_secs(12 * 60 * 60); + +// 24 hours +const DEFAULT_MAXIMUM_REPLY_KEY_AGE: Duration = Duration::from_secs(24 * 60 * 60); + pub fn missing_string_value() -> String { MISSING_VALUE.to_string() } -#[derive(Debug, Deserialize, PartialEq, Serialize)] +pub trait ClientCoreConfigTrait { + fn get_gateway_endpoint(&self) -> &GatewayEndpointConfig; +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)] #[serde(deny_unknown_fields)] pub struct Config { client: Client, @@ -42,17 +68,26 @@ pub struct Config { #[serde(default)] logging: Logging, #[serde(default)] - debug: Debug, + debug: DebugConfig, +} +impl ClientCoreConfigTrait for Config { + fn get_gateway_endpoint(&self) -> &GatewayEndpointConfig { + &self.client.gateway_endpoint + } } -impl Config { - pub fn new>(id: S) -> Self { - let mut cfg = Config::default(); - cfg.with_id(id); - cfg +impl Config { + pub fn new>(id: S) -> Self + where + T: NymConfig, + { + Config::default().with_id(id) } - pub fn with_id>(&mut self, id: S) { + pub fn with_id>(mut self, id: S) -> Self + where + T: NymConfig, + { let id = id.into(); // identity key setting @@ -96,14 +131,9 @@ impl Config { self.client.ack_key_file = self::Client::::default_ack_key_file(&id); } - if self - .client - .reply_encryption_key_store_path - .as_os_str() - .is_empty() - { - self.client.reply_encryption_key_store_path = - self::Client::::default_reply_encryption_key_store_path(&id); + if self.client.reply_surb_database_path.as_os_str().is_empty() { + self.client.reply_surb_database_path = + self::Client::::default_reply_surb_database_path(&id); } if self.client.database_path.as_os_str().is_empty() { @@ -111,13 +141,14 @@ impl Config { } self.client.id = id; + self } pub fn with_disabled_credentials(&mut self, disabled_credentials_mode: bool) { self.client.disabled_credentials_mode = disabled_credentials_mode; } - pub fn with_gateway_endpoint(&mut self, gateway_endpoint: GatewayEndpoint) { + pub fn with_gateway_endpoint(&mut self, gateway_endpoint: GatewayEndpointConfig) { self.client.gateway_endpoint = gateway_endpoint; } @@ -135,8 +166,15 @@ impl Config { pub fn set_high_default_traffic_volume(&mut self) { self.debug.average_packet_delay = Duration::from_millis(10); - self.debug.loop_cover_traffic_average_delay = Duration::from_millis(2_000_000); // basically don't really send cover messages - self.debug.message_sending_average_delay = Duration::from_millis(4); // 250 "real" messages / s + // basically don't really send cover messages + self.debug.loop_cover_traffic_average_delay = Duration::from_millis(2_000_000); + // 250 "real" messages / s + self.debug.message_sending_average_delay = Duration::from_millis(4); + } + + pub fn set_no_cover_traffic(&mut self) { + self.debug.disable_loop_cover_traffic_stream = true; + self.debug.disable_main_poisson_packet_distribution = true; } pub fn set_custom_version(&mut self, version: &str) { @@ -175,10 +213,6 @@ impl Config { self.client.gateway_shared_key_file.clone() } - pub fn get_reply_encryption_key_store_path(&self) -> PathBuf { - self.client.reply_encryption_key_store_path.clone() - } - pub fn get_ack_key_file(&self) -> PathBuf { self.client.ack_key_file.clone() } @@ -203,7 +237,7 @@ impl Config { self.client.gateway_endpoint.gateway_listener.clone() } - pub fn get_gateway_endpoint(&self) -> &GatewayEndpoint { + pub fn get_gateway_endpoint_config(&self) -> &GatewayEndpointConfig { &self.client.gateway_endpoint } @@ -211,7 +245,19 @@ impl Config { self.client.database_path.clone() } + pub fn get_reply_surb_database_path(&self) -> PathBuf { + self.client.reply_surb_database_path.clone() + } + + pub fn get_version(&self) -> &str { + &self.client.version + } + // Debug getters + pub fn get_debug_config(&self) -> &DebugConfig { + &self.debug + } + pub fn get_average_packet_delay(&self) -> Duration { self.debug.average_packet_delay } @@ -257,11 +303,39 @@ impl Config { } pub fn get_use_extended_packet_size(&self) -> Option { - self.debug.use_extended_packet_size.clone() + self.debug.use_extended_packet_size } - pub fn get_version(&self) -> &str { - &self.client.version + pub fn get_minimum_reply_surb_storage_threshold(&self) -> usize { + self.debug.minimum_reply_surb_storage_threshold + } + + pub fn get_maximum_reply_surb_storage_threshold(&self) -> usize { + self.debug.maximum_reply_surb_storage_threshold + } + + pub fn get_minimum_reply_surb_request_size(&self) -> u32 { + self.debug.minimum_reply_surb_request_size + } + + pub fn get_maximum_reply_surb_request_size(&self) -> u32 { + self.debug.maximum_reply_surb_request_size + } + + pub fn get_maximum_allowed_reply_surb_request_size(&self) -> u32 { + self.debug.maximum_allowed_reply_surb_request_size + } + + pub fn get_maximum_reply_surb_waiting_period(&self) -> Duration { + self.debug.maximum_reply_surb_waiting_period + } + + pub fn get_maximum_reply_surb_age(&self) -> Duration { + self.debug.maximum_reply_surb_age + } + + pub fn get_maximum_reply_key_age(&self) -> Duration { + self.debug.maximum_reply_key_age } } @@ -277,7 +351,7 @@ impl Default for Config { #[derive(Clone, Debug, Default, Deserialize, PartialEq, Eq, Serialize)] #[cfg_attr(target_arch = "wasm32", wasm_bindgen(getter_with_clone))] -pub struct GatewayEndpoint { +pub struct GatewayEndpointConfig { /// gateway_id specifies ID of the gateway to which the client should send messages. /// If initially omitted, a random gateway will be chosen from the available topology. pub gateway_id: String, @@ -289,10 +363,26 @@ pub struct GatewayEndpoint { pub gateway_listener: String, } -impl From for GatewayEndpoint { - fn from(node: topology::gateway::Node) -> GatewayEndpoint { +#[cfg_attr(target_arch = "wasm32", wasm_bindgen)] +impl GatewayEndpointConfig { + #[cfg_attr(target_arch = "wasm32", wasm_bindgen(constructor))] + pub fn new( + gateway_id: String, + gateway_owner: String, + gateway_listener: String, + ) -> GatewayEndpointConfig { + GatewayEndpointConfig { + gateway_id, + gateway_owner, + gateway_listener, + } + } +} + +impl From for GatewayEndpointConfig { + fn from(node: topology::gateway::Node) -> GatewayEndpointConfig { let gateway_listener = node.clients_address(); - GatewayEndpoint { + GatewayEndpointConfig { gateway_id: node.identity_key.to_base58_string(), gateway_owner: node.owner, gateway_listener, @@ -300,7 +390,7 @@ impl From for GatewayEndpoint { } } -#[derive(Debug, Deserialize, PartialEq, Eq, Serialize)] +#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)] pub struct Client { /// Version of the client for which this configuration was created. #[serde(default = "missing_string_value")] @@ -341,16 +431,15 @@ pub struct Client { /// acknowledgement so that nobody besides the client knows which packet it refers to. ack_key_file: PathBuf, - /// Full path to file containing reply encryption keys of all reply-SURBs we have ever - /// sent but not received back. - reply_encryption_key_store_path: PathBuf, - /// Information regarding how the client should send data to gateway. - gateway_endpoint: GatewayEndpoint, + gateway_endpoint: GatewayEndpointConfig, /// Path to the database containing bandwidth credentials of this client. database_path: PathBuf, + /// Path to the persistent store for received reply surbs, unused encryption keys and used sender tags. + reply_surb_database_path: PathBuf, + /// nym_home_directory specifies absolute path to the home nym Clients directory. /// It is expected to use default value and hence .toml file should not redefine this field. nym_root_directory: PathBuf, @@ -374,9 +463,9 @@ impl Default for Client { public_encryption_key_file: Default::default(), gateway_shared_key_file: Default::default(), ack_key_file: Default::default(), - reply_encryption_key_store_path: Default::default(), gateway_endpoint: Default::default(), database_path: Default::default(), + reply_surb_database_path: Default::default(), nym_root_directory: T::default_root_directory(), super_struct: Default::default(), } @@ -408,21 +497,22 @@ impl Client { T::default_data_directory(Some(id)).join("ack_key.pem") } - fn default_reply_encryption_key_store_path(id: &str) -> PathBuf { - T::default_data_directory(Some(id)).join("reply_key_store") + fn default_reply_surb_database_path(id: &str) -> PathBuf { + T::default_data_directory(Some(id)).join("persistent_reply_store.sqlite") } + fn default_database_path(id: &str) -> PathBuf { T::default_data_directory(Some(id)).join(DB_FILE_NAME) } } -#[derive(Debug, Default, Deserialize, PartialEq, Eq, Serialize)] +#[derive(Debug, Clone, Default, Deserialize, PartialEq, Eq, Serialize)] #[serde(deny_unknown_fields)] pub struct Logging {} -#[derive(Debug, Deserialize, PartialEq, Serialize)] +#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)] #[serde(default, deny_unknown_fields)] -pub struct Debug { +pub struct DebugConfig { /// The parameter of Poisson distribution determining how long, on average, /// sent packet is going to be delayed at any given mix node. /// So for a packet going through three mix nodes, on average, it will take three times this value @@ -486,9 +576,40 @@ pub struct Debug { /// Controls whether the sent sphinx packet use a NON-DEFAULT bigger size. pub use_extended_packet_size: Option, + + /// Defines the minimum number of reply surbs the client wants to keep in its storage at all times. + /// It can only allow to go below that value if its to request additional reply surbs. + pub minimum_reply_surb_storage_threshold: usize, + + /// Defines the maximum number of reply surbs the client wants to keep in its storage at any times. + pub maximum_reply_surb_storage_threshold: usize, + + /// Defines the minimum number of reply surbs the client would request. + pub minimum_reply_surb_request_size: u32, + + /// Defines the maximum number of reply surbs the client would request. + pub maximum_reply_surb_request_size: u32, + + /// Defines the maximum number of reply surbs a remote party is allowed to request from this client at once. + pub maximum_allowed_reply_surb_request_size: u32, + + /// Defines maximum amount of time the client is going to wait for reply surbs before explicitly asking + /// for more even though in theory they wouldn't need to. + #[serde(with = "humantime_serde")] + pub maximum_reply_surb_waiting_period: Duration, + + /// Defines maximum amount of time given reply surb is going to be valid for. + /// This is going to be superseded by key rotation once implemented. + #[serde(with = "humantime_serde")] + pub maximum_reply_surb_age: Duration, + + /// Defines maximum amount of time given reply key is going to be valid for. + /// This is going to be superseded by key rotation once implemented. + #[serde(with = "humantime_serde")] + pub maximum_reply_key_age: Duration, } -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "lowercase")] pub enum ExtendedPacketSize { Extended8, @@ -496,9 +617,9 @@ pub enum ExtendedPacketSize { Extended32, } -impl Default for Debug { +impl Default for DebugConfig { fn default() -> Self { - Debug { + DebugConfig { average_packet_delay: DEFAULT_AVERAGE_PACKET_DELAY, average_ack_delay: DEFAULT_AVERAGE_PACKET_DELAY, ack_wait_multiplier: DEFAULT_ACK_WAIT_MULTIPLIER, @@ -511,6 +632,14 @@ impl Default for Debug { disable_loop_cover_traffic_stream: false, disable_main_poisson_packet_distribution: false, use_extended_packet_size: None, + minimum_reply_surb_storage_threshold: DEFAULT_MINIMUM_REPLY_SURB_STORAGE_THRESHOLD, + maximum_reply_surb_storage_threshold: DEFAULT_MAXIMUM_REPLY_SURB_STORAGE_THRESHOLD, + minimum_reply_surb_request_size: DEFAULT_MINIMUM_REPLY_SURB_REQUEST_SIZE, + maximum_reply_surb_request_size: DEFAULT_MAXIMUM_REPLY_SURB_REQUEST_SIZE, + maximum_allowed_reply_surb_request_size: DEFAULT_MAXIMUM_ALLOWED_SURB_REQUEST_SIZE, + maximum_reply_surb_waiting_period: DEFAULT_MAXIMUM_REPLY_SURB_WAITING_PERIOD, + maximum_reply_surb_age: DEFAULT_MAXIMUM_REPLY_SURB_AGE, + maximum_reply_key_age: DEFAULT_MAXIMUM_REPLY_KEY_AGE, } } } diff --git a/clients/client-core/src/error.rs b/clients/client-core/src/error.rs index 0313db076b..81d4b58686 100644 --- a/clients/client-core/src/error.rs +++ b/clients/client-core/src/error.rs @@ -1,31 +1,58 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use crate::client::replies::reply_storage::ReplyStorageBackend; use crypto::asymmetric::identity::Ed25519RecoveryError; use gateway_client::error::GatewayClientError; +use topology::NymTopologyError; use validator_client::ValidatorClientError; #[derive(thiserror::Error, Debug)] -pub enum ClientCoreError { +pub enum ClientCoreError { #[error("I/O error: {0}")] IoError(#[from] std::io::Error), + #[error("Gateway client error: {0}")] GatewayClientError(#[from] GatewayClientError), + #[error("Ed25519 error: {0}")] Ed25519RecoveryError(#[from] Ed25519RecoveryError), + #[error("Validator client error: {0}")] ValidatorClientError(#[from] ValidatorClientError), #[error("No gateway with id: {0}")] NoGatewayWithId(String), + #[error("No gateways on network")] NoGatewaysOnNetwork, + + #[error("Failed to setup gateway")] + FailedToSetupGateway, + #[error("List of validator apis is empty")] ListOfValidatorApisIsEmpty, + #[error("Could not load existing gateway configuration: {0}")] CouldNotLoadExistingGatewayConfiguration(std::io::Error), + #[error("The current network topology seem to be insufficient to route any packets through")] - InsufficientNetworkTopology, + InsufficientNetworkTopology(#[from] NymTopologyError), + + #[error("experienced a failure with our reply surb persistent storage: {source}")] + SurbStorageError { source: B::StorageError }, + + #[error("The gateway id is invalid - {0}")] + UnableToCreatePublicKeyFromGatewayId(Ed25519RecoveryError), + + #[error("The identity of the gateway is unknwown - did you run init?")] + GatewayIdUnknown, + + #[error("The owner of the gateway is unknown - did you run init?")] + GatewayOwnerUnknown, + + #[error("The address of the gateway is unknown - did you run init?")] + GatwayAddressUnknown, #[error("Unexpected exit")] UnexpectedExit, diff --git a/clients/client-core/src/init.rs b/clients/client-core/src/init/helpers.rs similarity index 56% rename from clients/client-core/src/init.rs rename to clients/client-core/src/init/helpers.rs index be9758d363..7947d614a1 100644 --- a/clients/client-core/src/init.rs +++ b/clients/client-core/src/init/helpers.rs @@ -1,33 +1,29 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -//! Collection of initialization steps used by client implementations - -use std::{sync::Arc, time::Duration}; - -use config::NymConfig; -use crypto::asymmetric::{encryption, identity}; -use gateway_client::GatewayClient; -use gateway_requests::registration::handshake::SharedKeys; -use nymsphinx::addressing::clients::Recipient; -use nymsphinx::addressing::nodes::NodeIdentity; -use rand::rngs::OsRng; -use rand::seq::SliceRandom; -use rand::thread_rng; -use tap::TapFallible; -use topology::{filter::VersionFilterable, gateway}; -use url::Url; - +use crate::client::replies::reply_storage::ReplyStorageBackend; use crate::{ client::key_manager::KeyManager, config::{persistence::key_pathfinder::ClientKeyPathfinder, Config}, error::ClientCoreError, }; +use config::NymConfig; +use crypto::asymmetric::identity; +use gateway_client::GatewayClient; +use gateway_requests::registration::handshake::SharedKeys; +use rand::{rngs::OsRng, seq::SliceRandom, thread_rng}; +use std::{sync::Arc, time::Duration}; +use tap::TapFallible; +use topology::{filter::VersionFilterable, gateway}; +use url::Url; -pub async fn query_gateway_details( +pub(super) async fn query_gateway_details( validator_servers: Vec, - chosen_gateway_id: Option<&str>, -) -> Result { + chosen_gateway_id: Option, +) -> Result> +where + B: ReplyStorageBackend, +{ let validator_api = validator_servers .choose(&mut thread_rng()) .ok_or(ClientCoreError::ListOfValidatorApisIsEmpty)?; @@ -59,12 +55,39 @@ pub async fn query_gateway_details( } } -pub async fn register_with_gateway_and_store_keys( +async fn register_with_gateway( + gateway: &gateway::Node, + our_identity: Arc, +) -> Result, ClientCoreError> +where + B: ReplyStorageBackend, +{ + let timeout = Duration::from_millis(1500); + let mut gateway_client = GatewayClient::new_init( + gateway.clients_address(), + gateway.identity_key, + gateway.owner.clone(), + our_identity.clone(), + timeout, + ); + gateway_client + .establish_connection() + .await + .tap_err(|_| log::warn!("Failed to establish connection with gateway!"))?; + let shared_keys = gateway_client + .perform_initial_authentication() + .await + .tap_err(|_| log::warn!("Failed to register with the gateway!"))?; + Ok(shared_keys) +} + +pub(super) async fn register_with_gateway_and_store_keys( gateway_details: gateway::Node, config: &Config, -) -> Result<(), ClientCoreError> +) -> Result<(), ClientCoreError> where T: NymConfig, + B: ReplyStorageBackend, { let mut rng = OsRng; let mut key_manager = KeyManager::new(&mut rng); @@ -78,72 +101,3 @@ where .store_keys(&pathfinder) .tap_err(|err| log::error!("Failed to generate keys: {err}"))?) } - -async fn register_with_gateway( - gateway: &gateway::Node, - our_identity: Arc, -) -> Result, ClientCoreError> { - let timeout = Duration::from_millis(1500); - let mut gateway_client = GatewayClient::new_init( - gateway.clients_address(), - gateway.identity_key, - gateway.owner.clone(), - our_identity.clone(), - timeout, - #[cfg(not(target_arch = "wasm32"))] - None, - ); - gateway_client - .establish_connection() - .await - .tap_err(|_| log::warn!("Failed to establish connection with gateway!"))?; - let shared_keys = gateway_client - .perform_initial_authentication() - .await - .tap_err(|_| log::warn!("Failed to register with the gateway!"))?; - Ok(shared_keys) -} - -pub fn show_address(config: &Config) -> Result<(), ClientCoreError> -where - T: config::NymConfig, -{ - fn load_identity_keys( - pathfinder: &ClientKeyPathfinder, - ) -> Result { - let identity_keypair: identity::KeyPair = - pemstore::load_keypair(&pemstore::KeyPairPath::new( - pathfinder.private_identity_key().to_owned(), - pathfinder.public_identity_key().to_owned(), - )) - .tap_err(|_| log::error!("Failed to read stored identity key files"))?; - Ok(identity_keypair) - } - - fn load_sphinx_keys( - pathfinder: &ClientKeyPathfinder, - ) -> Result { - let sphinx_keypair: encryption::KeyPair = - pemstore::load_keypair(&pemstore::KeyPairPath::new( - pathfinder.private_encryption_key().to_owned(), - pathfinder.public_encryption_key().to_owned(), - )) - .tap_err(|_| log::error!("Failed to read stored sphinx key files"))?; - Ok(sphinx_keypair) - } - - let pathfinder = ClientKeyPathfinder::new_from_config(config); - let identity_keypair = load_identity_keys(&pathfinder)?; - let sphinx_keypair = load_sphinx_keys(&pathfinder)?; - - let client_recipient = Recipient::new( - *identity_keypair.public_key(), - *sphinx_keypair.public_key(), - // TODO: below only works under assumption that gateway address == gateway id - // (which currently is true) - NodeIdentity::from_base58_string(config.get_gateway_id())?, - ); - - println!("\nThe address of this client is: {}", client_recipient); - Ok(()) -} diff --git a/clients/client-core/src/init/mod.rs b/clients/client-core/src/init/mod.rs new file mode 100644 index 0000000000..cdf210a27e --- /dev/null +++ b/clients/client-core/src/init/mod.rs @@ -0,0 +1,216 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +//! Collection of initialization steps used by client implementations + +use std::fmt::Display; + +use nymsphinx::addressing::{clients::Recipient, nodes::NodeIdentity}; +use serde::Serialize; +use tap::TapFallible; + +use config::NymConfig; +use crypto::asymmetric::{encryption, identity}; + +use crate::client::replies::reply_storage::ReplyStorageBackend; +use crate::{ + config::{ + persistence::key_pathfinder::ClientKeyPathfinder, ClientCoreConfigTrait, Config, + GatewayEndpointConfig, + }, + error::ClientCoreError, + init::helpers::{query_gateway_details, register_with_gateway_and_store_keys}, +}; + +mod helpers; + +#[derive(Debug, Serialize)] +pub struct InitResults { + version: String, + id: String, + identity_key: String, + encryption_key: String, + gateway_id: String, + gateway_listener: String, +} + +impl InitResults { + pub fn new(config: &Config, address: &Recipient) -> Self + where + T: NymConfig, + { + Self { + version: config.get_version().to_string(), + id: config.get_id(), + identity_key: address.identity().to_base58_string(), + encryption_key: address.encryption_key().to_base58_string(), + gateway_id: config.get_gateway_id(), + gateway_listener: config.get_gateway_listener(), + } + } +} + +impl Display for InitResults { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + writeln!(f, "Version: {}", self.version)?; + writeln!(f, "ID: {}", self.id)?; + writeln!(f, "Identity key: {}", self.identity_key)?; + writeln!(f, "Encryption: {}", self.encryption_key)?; + writeln!(f, "Gateway ID: {}", self.gateway_id)?; + write!(f, "Gateway: {}", self.gateway_listener) + } +} + +/// Convenience function for setting up the gateway for a client. Depending on the arguments given +/// it will do the sensible thing. +pub async fn setup_gateway( + register_gateway: bool, + user_chosen_gateway_id: Option, + config: &Config, +) -> Result> +where + B: ReplyStorageBackend, + C: NymConfig + ClientCoreConfigTrait, + T: NymConfig, +{ + let id = config.get_id(); + if register_gateway { + register_with_gateway(user_chosen_gateway_id, config).await + } else if let Some(user_chosen_gateway_id) = user_chosen_gateway_id { + config_gateway_with_existing_keys(user_chosen_gateway_id, config).await + } else { + reuse_existing_gateway_config::(&id) + } +} + +/// Get the gateway details by querying the validator-api. Either pick one at random or use +/// the chosen one if it's among the available ones. +/// Saves keys to disk, specified by the paths in `config`. +pub async fn register_with_gateway( + user_chosen_gateway_id: Option, + config: &Config, +) -> Result> +where + B: ReplyStorageBackend, + T: NymConfig, +{ + println!("Configuring gateway"); + let gateway = + query_gateway_details(config.get_validator_api_endpoints(), user_chosen_gateway_id).await?; + log::debug!("Querying gateway gives: {}", gateway); + + // Registering with gateway by setting up and writing shared keys to disk + log::trace!("Registering gateway"); + register_with_gateway_and_store_keys(gateway.clone(), config).await?; + println!("Saved all generated keys"); + + Ok(gateway.into()) +} + +/// Set the gateway using the usual procedue of querying the validator-api, but don't register or +/// create any keys. +/// This assumes that the user knows what they are doing, and that the existing keys are valid for +/// the gateway being used +pub async fn config_gateway_with_existing_keys( + user_chosen_gateway_id: String, + config: &Config, +) -> Result> +where + B: ReplyStorageBackend, + T: NymConfig, +{ + println!("Using gateway provided by user, keeping existing keys"); + let gateway = query_gateway_details( + config.get_validator_api_endpoints(), + Some(user_chosen_gateway_id), + ) + .await?; + log::debug!("Querying gateway gives: {}", gateway); + Ok(gateway.into()) +} + +/// Read and reuse the existing gateway configuration from a file that was generate earlier. +pub fn reuse_existing_gateway_config( + id: &str, +) -> Result> +where + B: ReplyStorageBackend, + T: NymConfig + ClientCoreConfigTrait, +{ + println!("Not registering gateway, will reuse existing config and keys"); + T::load_from_file(Some(id)) + .map(|existing_config| existing_config.get_gateway_endpoint().clone()) + .map_err(|err| { + log::error!( + "Unable to configure gateway: {err}. \n + Seems like the client was already initialized but it was not possible to read \ + the existing configuration file. \n + CAUTION: Consider backing up your gateway keys and try force gateway registration, or \ + removing the existing configuration and starting over." + ); + ClientCoreError::CouldNotLoadExistingGatewayConfiguration(err) + }) +} + +/// Get the client address by loading the keys from stored files. +pub fn get_client_address_from_stored_keys( + config: &Config, +) -> Result> +where + T: config::NymConfig, + B: ReplyStorageBackend, +{ + fn load_identity_keys( + pathfinder: &ClientKeyPathfinder, + ) -> Result> + where + B: ReplyStorageBackend, + { + let identity_keypair: identity::KeyPair = + pemstore::load_keypair(&pemstore::KeyPairPath::new( + pathfinder.private_identity_key().to_owned(), + pathfinder.public_identity_key().to_owned(), + )) + .tap_err(|_| log::error!("Failed to read stored identity key files"))?; + Ok(identity_keypair) + } + + fn load_sphinx_keys( + pathfinder: &ClientKeyPathfinder, + ) -> Result> + where + B: ReplyStorageBackend, + { + let sphinx_keypair: encryption::KeyPair = + pemstore::load_keypair(&pemstore::KeyPairPath::new( + pathfinder.private_encryption_key().to_owned(), + pathfinder.public_encryption_key().to_owned(), + )) + .tap_err(|_| log::error!("Failed to read stored sphinx key files"))?; + Ok(sphinx_keypair) + } + + let pathfinder = ClientKeyPathfinder::new_from_config(config); + let identity_keypair = load_identity_keys(&pathfinder)?; + let sphinx_keypair = load_sphinx_keys(&pathfinder)?; + + let client_recipient = Recipient::new( + *identity_keypair.public_key(), + *sphinx_keypair.public_key(), + // TODO: below only works under assumption that gateway address == gateway id + // (which currently is true) + NodeIdentity::from_base58_string(config.get_gateway_id())?, + ); + + Ok(client_recipient) +} + +pub fn output_to_json(init_results: &T, output_file: &str) { + match std::fs::File::create(output_file) { + Ok(file) => match serde_json::to_writer_pretty(file, init_results) { + Ok(_) => println!("Saved: {}", output_file), + Err(err) => eprintln!("Could not save {}: {}", output_file, err), + }, + Err(err) => eprintln!("Could not save {}: {}", output_file, err), + } +} diff --git a/clients/credential/src/commands.rs b/clients/credential/src/commands.rs index 5ed4bd8dad..106b2c318e 100644 --- a/clients/credential/src/commands.rs +++ b/clients/credential/src/commands.rs @@ -68,7 +68,7 @@ pub(crate) async fn deposit(nymd_url: &str, mnemonic: &str, amount: u64) -> Resu let state = State { amount, - tx_hash: tx_hash.clone(), + tx_hash, signing_keypair, encryption_keypair, }; @@ -98,6 +98,7 @@ pub(crate) async fn get_credential(state: &State, shared_storage: PersistentStor &coconut_api_clients, ) .await?; + println!("Signature: {:?}", signature.to_bs58()); shared_storage .insert_coconut_credential( state.amount.to_string(), diff --git a/clients/native/Cargo.toml b/clients/native/Cargo.toml index eb4c722501..97e2a8d504 100644 --- a/clients/native/Cargo.toml +++ b/clients/native/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nym-client" -version = "1.1.1" +version = "1.1.2" authors = ["Dave Hrycyszyn ", "Jędrzej Stuczyński "] description = "Implementation of the Nym Client" edition = "2021" @@ -26,13 +26,14 @@ log = "0.4" # self explanatory pretty_env_logger = "0.4" # for formatting log messages rand = { version = "0.7.3", features = ["wasm-bindgen"] } # rng-related traits + some rng implementation to use serde = { version = "1.0.104", features = ["derive"] } # for config serialization/deserialization -sled = "0.34" # for storage of replySURB decryption keys +serde_json = "1.0" thiserror = "1.0.34" +tap = "1.0.1" tokio = { version = "1.21.2", features = ["rt-multi-thread", "net", "signal"] } # async runtime tokio-tungstenite = "0.14" # websocket ## internal -client-core = { path = "../client-core" } +client-core = { path = "../client-core", features = ["fs-surb-storage"] } client-connections = { path = "../../common/client-connections" } coconut-interface = { path = "../../common/coconut-interface", optional = true } config = { path = "../../common/config" } @@ -51,7 +52,6 @@ topology = { path = "../../common/topology" } validator-client = { path = "../../common/client-libs/validator-client", features = ["nymd-client"] } version-checker = { path = "../../common/version-checker" } websocket-requests = { path = "websocket-requests" } -tap = "1.0.1" [features] coconut = ["coconut-interface", "credentials", "credentials/coconut", "gateway-requests/coconut", "gateway-client/coconut", "client-core/coconut"] diff --git a/clients/native/examples/websocket_binarysend.rs b/clients/native/examples/websocket_binarysend.rs index f103815af2..2c22f0d03b 100644 --- a/clients/native/examples/websocket_binarysend.rs +++ b/clients/native/examples/websocket_binarysend.rs @@ -25,58 +25,59 @@ async fn get_self_address(ws_stream: &mut WebSocketStream recipient, + ServerResponse::SelfAddress(recipient) => *recipient, _ => panic!("received an unexpected response!"), } } async fn send_file_with_reply() { - let uri = "ws://localhost:1977"; - let (mut ws_stream, _) = connect_async(uri).await.unwrap(); - - let recipient = get_self_address(&mut ws_stream).await; - println!("our full address is: {}", recipient); - - let read_data = std::fs::read("examples/dummy_file").unwrap(); - - let send_request = ClientRequest::Send { - recipient, - message: read_data, - with_reply_surb: true, - connection_id: Some(0), - }; - - println!("sending content of 'dummy_file' over the mix network..."); - let response = send_message_and_get_response(&mut ws_stream, send_request.serialize()).await; - - let received = match response { - ServerResponse::Received(received) => received, - _ => panic!("received an unexpected response!"), - }; - - println!("writing the file back to the disk!"); - std::fs::write("examples/received_file_withreply", received.message).unwrap(); - - let reply_message = b"hello from reply SURB! - thanks for sending me the file!".to_vec(); - let reply_request = ClientRequest::Reply { - message: reply_message.clone(), - reply_surb: received.reply_surb.unwrap(), - }; - - println!( - "sending {:?} (using reply SURB!) over the mix network...", - String::from_utf8(reply_message).unwrap() - ); - let response = send_message_and_get_response(&mut ws_stream, reply_request.serialize()).await; - let received = match response { - ServerResponse::Received(received) => received, - _ => panic!("received an unexpected response!"), - }; - - println!( - "received {:#?} from the mix network!", - String::from_utf8(received.message).unwrap() - ); + todo!("reimplement surb usage here : )") + // let uri = "ws://localhost:1977"; + // let (mut ws_stream, _) = connect_async(uri).await.unwrap(); + // + // let recipient = get_self_address(&mut ws_stream).await; + // println!("our full address is: {}", recipient); + // + // let read_data = std::fs::read("examples/dummy_file").unwrap(); + // + // let send_request = ClientRequest::Send { + // recipient, + // message: read_data, + // with_reply_surb: true, + // connection_id: Some(0), + // }; + // + // println!("sending content of 'dummy_file' over the mix network..."); + // let response = send_message_and_get_response(&mut ws_stream, send_request.serialize()).await; + // + // let received = match response { + // ServerResponse::Received(received) => received, + // _ => panic!("received an unexpected response!"), + // }; + // + // println!("writing the file back to the disk!"); + // std::fs::write("examples/received_file_withreply", received.message).unwrap(); + // + // let reply_message = b"hello from reply SURB! - thanks for sending me the file!".to_vec(); + // let reply_request = ClientRequest::Reply { + // message: reply_message.clone(), + // reply_surb: received.reply_surb.unwrap(), + // }; + // + // println!( + // "sending {:?} (using reply SURB!) over the mix network...", + // String::from_utf8(reply_message).unwrap() + // ); + // let response = send_message_and_get_response(&mut ws_stream, reply_request.serialize()).await; + // let received = match response { + // ServerResponse::Received(received) => received, + // _ => panic!("received an unexpected response!"), + // }; + // + // println!( + // "received {:#?} from the mix network!", + // String::from_utf8(received.message).unwrap() + // ); } async fn send_file_without_reply() { @@ -91,7 +92,6 @@ async fn send_file_without_reply() { let send_request = ClientRequest::Send { recipient, message: read_data, - with_reply_surb: false, connection_id: Some(0), }; diff --git a/clients/native/src/client/config/mod.rs b/clients/native/src/client/config/mod.rs index f9c04ca1d7..67902138ce 100644 --- a/clients/native/src/client/config/mod.rs +++ b/clients/native/src/client/config/mod.rs @@ -2,8 +2,8 @@ // SPDX-License-Identifier: Apache-2.0 use crate::client::config::template::config_template; -use client_core::config::Config as BaseConfig; pub use client_core::config::MISSING_VALUE; +use client_core::config::{ClientCoreConfigTrait, Config as BaseConfig, DebugConfig}; use config::defaults::DEFAULT_WEBSOCKET_LISTENING_PORT; use config::NymConfig; use serde::{Deserialize, Serialize}; @@ -27,6 +27,10 @@ impl SocketType { _ => SocketType::None, } } + + pub fn is_websocket(&self) -> bool { + matches!(self, SocketType::WebSocket) + } } #[derive(Debug, Default, Deserialize, PartialEq, Serialize)] @@ -69,6 +73,12 @@ impl NymConfig for Config { } } +impl ClientCoreConfigTrait for Config { + fn get_gateway_endpoint(&self) -> &client_core::config::GatewayEndpointConfig { + self.base.get_gateway_endpoint() + } +} + impl Config { pub fn new>(id: S) -> Self { Config { @@ -100,6 +110,10 @@ impl Config { &mut self.base } + pub fn get_debug_settings(&self) -> &DebugConfig { + self.get_base().get_debug_config() + } + pub fn get_socket_type(&self) -> SocketType { self.socket.socket_type } diff --git a/clients/native/src/client/config/template.rs b/clients/native/src/client/config/template.rs index c327d286ff..31ca091282 100644 --- a/clients/native/src/client/config/template.rs +++ b/clients/native/src/client/config/template.rs @@ -49,13 +49,12 @@ private_encryption_key_file = '{{ client.private_encryption_key_file }}' # Path to file containing public encryption key. public_encryption_key_file = '{{ client.public_encryption_key_file }}' -# Full path to file containing reply encryption keys of all reply-SURBs we have ever -# sent but not received back. -reply_encryption_key_store_path = '{{ client.reply_encryption_key_store_path }}' - # Path to the database containing bandwidth credentials database_path = '{{ client.database_path }}' +# Path to the persistent store for received reply surbs, unused encryption keys and used sender tags. +reply_surb_database_path = '{{ client.reply_surb_database_path }}' + ##### additional client config options ##### # A gateway specific, optional, base58 stringified shared key used for diff --git a/clients/native/src/client/mod.rs b/clients/native/src/client/mod.rs index 5569665730..cabe049c45 100644 --- a/clients/native/src/client/mod.rs +++ b/clients/native/src/client/mod.rs @@ -1,219 +1,60 @@ -// Copyright 2021 - Nym Technologies SA +// Copyright 2021-2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use client_connections::{ - ConnectionCommandReceiver, ConnectionCommandSender, LaneQueueLengths, TransmissionLane, -}; -use client_core::client::cover_traffic_stream::LoopCoverTrafficStream; -use client_core::client::inbound_messages::{ - InputMessage, InputMessageReceiver, InputMessageSender, -}; -use client_core::client::key_manager::KeyManager; -use client_core::client::mix_traffic::{BatchMixMessageSender, MixTrafficController}; -use client_core::client::real_messages_control; -use client_core::client::real_messages_control::RealMessagesController; -use client_core::client::received_buffer::{ - ReceivedBufferMessage, ReceivedBufferRequestReceiver, ReceivedBufferRequestSender, - ReceivedMessagesBufferController, ReconstructedMessagesReceiver, -}; -use client_core::client::reply_key_storage::ReplyKeyStorage; -use client_core::client::topology_control::{ - TopologyAccessor, TopologyRefresher, TopologyRefresherConfig, -}; -use client_core::config::persistence::key_pathfinder::ClientKeyPathfinder; -use client_core::error::ClientCoreError; -use crypto::asymmetric::identity; -use futures::channel::mpsc; -use gateway_client::bandwidth::BandwidthController; -use gateway_client::{ - AcknowledgementReceiver, AcknowledgementSender, GatewayClient, MixnetMessageReceiver, - MixnetMessageSender, -}; -use log::*; -use nymsphinx::addressing::clients::Recipient; -use nymsphinx::addressing::nodes::NodeIdentity; -use nymsphinx::anonymous_replies::ReplySurb; -use nymsphinx::receiver::ReconstructedMessage; -use tap::TapFallible; -use task::{wait_for_signal, ShutdownListener, ShutdownNotifier}; - -use crate::client::config::{Config, SocketType}; +use crate::client::config::Config; use crate::error::ClientError; use crate::websocket; +use client_connections::TransmissionLane; +use client_core::client::base_client::{ + non_wasm_helpers, BaseClientBuilder, ClientInput, ClientOutput, +}; +use client_core::client::inbound_messages::InputMessage; +use client_core::client::key_manager::KeyManager; +use client_core::client::received_buffer::{ReceivedBufferMessage, ReconstructedMessagesReceiver}; +use client_core::config::persistence::key_pathfinder::ClientKeyPathfinder; +use futures::channel::mpsc; +use gateway_client::bandwidth::BandwidthController; +use log::*; +use nymsphinx::addressing::clients::Recipient; +use nymsphinx::anonymous_replies::requests::AnonymousSenderTag; +use nymsphinx::receiver::ReconstructedMessage; +use task::{wait_for_signal, ShutdownNotifier}; pub(crate) mod config; -pub struct NymClient { +pub struct SocketClient { /// Client configuration options, including, among other things, packet sending rates, /// key filepaths, etc. config: Config, /// KeyManager object containing smart pointers to all relevant keys used by the client. key_manager: KeyManager, - - /// Channel used for transforming 'raw' messages into sphinx packets and sending them - /// through the mix network. - /// It is only available if the client started with the websocket listener disabled. - input_tx: Option, - - /// Channel used for obtaining reconstructed messages received from the mix network. - /// It is only available if the client started with the websocket listener disabled. - receive_tx: Option, } -impl NymClient { +impl SocketClient { pub fn new(config: Config) -> Self { let pathfinder = ClientKeyPathfinder::new_from_config(config.get_base()); let key_manager = KeyManager::load_keys(&pathfinder).expect("failed to load stored keys"); - NymClient { + SocketClient { config, key_manager, - input_tx: None, - receive_tx: None, } } - pub fn as_mix_recipient(&self) -> Recipient { - Recipient::new( - *self.key_manager.identity_keypair().public_key(), - *self.key_manager.encryption_keypair().public_key(), - // TODO: below only works under assumption that gateway address == gateway id - // (which currently is true) - NodeIdentity::from_base58_string(self.config.get_base().get_gateway_id()).unwrap(), - ) - } - - // future constantly pumping loop cover traffic at some specified average rate - // the pumped traffic goes to the MixTrafficController - fn start_cover_traffic_stream( - &self, - topology_accessor: TopologyAccessor, - mix_tx: BatchMixMessageSender, - shutdown: ShutdownListener, - ) { - info!("Starting loop cover traffic stream..."); - - let mut stream = LoopCoverTrafficStream::new( - self.key_manager.ack_key(), - self.config.get_base().get_average_ack_delay(), - self.config.get_base().get_average_packet_delay(), - self.config - .get_base() - .get_loop_cover_traffic_average_delay(), - mix_tx, - self.as_mix_recipient(), - topology_accessor, - ); - - if let Some(size) = self.config.get_base().get_use_extended_packet_size() { - log::debug!("Setting extended packet size: {:?}", size); - stream.set_custom_packet_size(size.into()); - } - - stream.start_with_shutdown(shutdown); - } - - #[allow(clippy::too_many_arguments)] - fn start_real_traffic_controller( - &self, - topology_accessor: TopologyAccessor, - reply_key_storage: ReplyKeyStorage, - ack_receiver: AcknowledgementReceiver, - input_receiver: InputMessageReceiver, - mix_sender: BatchMixMessageSender, - lane_queue_lengths: LaneQueueLengths, - client_connection_rx: ConnectionCommandReceiver, - shutdown: ShutdownListener, - ) { - let mut controller_config = real_messages_control::Config::new( - self.key_manager.ack_key(), - self.config.get_base().get_ack_wait_multiplier(), - self.config.get_base().get_ack_wait_addition(), - self.config.get_base().get_average_ack_delay(), - self.config.get_base().get_message_sending_average_delay(), - self.config.get_base().get_average_packet_delay(), - self.config - .get_base() - .get_disabled_main_poisson_packet_distribution(), - self.as_mix_recipient(), - ); - - if let Some(size) = self.config.get_base().get_use_extended_packet_size() { - log::debug!("Setting extended packet size: {:?}", size); - controller_config.set_custom_packet_size(size.into()); - } - - info!("Starting real traffic stream..."); - - RealMessagesController::new( - controller_config, - ack_receiver, - input_receiver, - mix_sender, - topology_accessor, - reply_key_storage, - lane_queue_lengths, - client_connection_rx, - ) - .start_with_shutdown(shutdown); - } - - // buffer controlling all messages fetched from provider - // required so that other components would be able to use them (say the websocket) - fn start_received_messages_buffer_controller( - &self, - query_receiver: ReceivedBufferRequestReceiver, - mixnet_receiver: MixnetMessageReceiver, - reply_key_storage: ReplyKeyStorage, - shutdown: ShutdownListener, - ) { - info!("Starting received messages buffer controller..."); - ReceivedMessagesBufferController::new( - self.key_manager.encryption_keypair(), - query_receiver, - mixnet_receiver, - reply_key_storage, - ) - .start_with_shutdown(shutdown) - } - - async fn start_gateway_client( - &mut self, - mixnet_message_sender: MixnetMessageSender, - ack_sender: AcknowledgementSender, - shutdown: ShutdownListener, - ) -> GatewayClient { - let gateway_id = self.config.get_base().get_gateway_id(); - if gateway_id.is_empty() { - panic!("The identity of the gateway is unknown - did you run `nym-client` init?") - } - let gateway_owner = self.config.get_base().get_gateway_owner(); - if gateway_owner.is_empty() { - panic!("The owner of the gateway is unknown - did you run `nym-client` init?") - } - let gateway_address = self.config.get_base().get_gateway_listener(); - if gateway_address.is_empty() { - panic!("The address of the gateway is unknown - did you run `nym-client` init?") - } - - let gateway_identity = identity::PublicKey::from_base58_string(gateway_id) - .expect("provided gateway id is invalid!"); - + async fn create_bandwidth_controller(config: &Config) -> BandwidthController { #[cfg(feature = "coconut")] let bandwidth_controller = { let details = network_defaults::NymNetworkDetails::new_from_env(); let mut client_config = validator_client::Config::try_from_nym_network_details(&details) .expect("failed to construct validator client config"); - let nymd_url = self - .config + let nymd_url = config .get_base() .get_validator_endpoints() .pop() .expect("No nymd validator endpoint provided"); - let api_url = self - .config + let api_url = config .get_base() .get_validator_api_endpoints() .pop() @@ -227,124 +68,153 @@ impl NymClient { .await .expect("Could not query api clients"); BandwidthController::new( - credential_storage::initialise_storage(self.config.get_base().get_database_path()) - .await, + credential_storage::initialise_storage(config.get_base().get_database_path()).await, coconut_api_clients, ) }; #[cfg(not(feature = "coconut"))] let bandwidth_controller = BandwidthController::new( - credential_storage::initialise_storage(self.config.get_base().get_database_path()) - .await, + credential_storage::initialise_storage(config.get_base().get_database_path()).await, ) .expect("Could not create bandwidth controller"); - - let mut gateway_client = GatewayClient::new( - gateway_address, - self.key_manager.identity_keypair(), - gateway_identity, - gateway_owner, - Some(self.key_manager.gateway_shared_key()), - mixnet_message_sender, - ack_sender, - self.config.get_base().get_gateway_response_timeout(), - Some(bandwidth_controller), - Some(shutdown), - ); - - gateway_client - .set_disabled_credentials_mode(self.config.get_base().get_disabled_credentials_mode()); - - gateway_client - .authenticate_and_start() - .await - .expect("could not authenticate and start up the gateway connection"); - - gateway_client - } - - // future responsible for periodically polling directory server and updating - // the current global view of topology - async fn start_topology_refresher( - &mut self, - topology_accessor: TopologyAccessor, - shutdown: ShutdownListener, - ) -> Result<(), ClientError> { - let topology_refresher_config = TopologyRefresherConfig::new( - self.config.get_base().get_validator_api_endpoints(), - self.config.get_base().get_topology_refresh_rate(), - env!("CARGO_PKG_VERSION").to_string(), - ); - let mut topology_refresher = - TopologyRefresher::new(topology_refresher_config, topology_accessor); - // before returning, block entire runtime to refresh the current network view so that any - // components depending on topology would see a non-empty view - info!("Obtaining initial network topology"); - topology_refresher.refresh().await; - - // TODO: a slightly more graceful termination here - if !topology_refresher.is_topology_routable().await { - log::error!( - "The current network topology seem to be insufficient to route any packets through \ - - check if enough nodes and a gateway are online" - ); - return Err(ClientCoreError::InsufficientNetworkTopology.into()); - } - - info!("Starting topology refresher..."); - topology_refresher.start_with_shutdown(shutdown); - Ok(()) - } - - // controller for sending sphinx packets to mixnet (either real traffic or cover traffic) - // TODO: if we want to send control messages to gateway_client, this CAN'T take the ownership - // over it. Perhaps GatewayClient needs to be thread-shareable or have some channel for - // requests? - fn start_mix_traffic_controller( - gateway_client: GatewayClient, - shutdown: ShutdownListener, - ) -> BatchMixMessageSender { - info!("Starting mix traffic controller..."); - let (mix_traffic_controller, mix_tx) = MixTrafficController::new(gateway_client); - mix_traffic_controller.start_with_shutdown(shutdown); - mix_tx + bandwidth_controller } fn start_websocket_listener( - &self, - buffer_requester: ReceivedBufferRequestSender, - msg_input: InputMessageSender, - shared_lane_queue_lengths: LaneQueueLengths, - client_connection_tx: ConnectionCommandSender, + config: &Config, + client_input: ClientInput, + client_output: ClientOutput, + self_address: Recipient, ) { info!("Starting websocket listener..."); + let ClientInput { + shared_lane_queue_lengths, + connection_command_sender, + input_sender, + } = client_input; + + let received_buffer_request_sender = client_output.received_buffer_request_sender; + let websocket_handler = websocket::Handler::new( - msg_input, - client_connection_tx, - buffer_requester, - &self.as_mix_recipient(), + input_sender, + connection_command_sender, + received_buffer_request_sender, + self_address, shared_lane_queue_lengths, ); - websocket::Listener::new(self.config.get_listening_port()).start(websocket_handler); + websocket::Listener::new(config.get_listening_port()).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<(), ClientError> { + let mut shutdown = self.start_socket().await?; + wait_for_signal().await; + + println!( + "Received signal - the client will terminate now (threads are not yet nicely stopped, if you see stack traces that's alright)." + ); + + log::info!("Sending shutdown"); + shutdown.signal_shutdown().ok(); + + // Some of these components have shutdown signalling implemented as part of socks5 work, + // but since it's not fully implemented (yet) for all the components of the native client, + // we don't try to wait and instead just stop immediately. + log::info!("Waiting for tasks to finish... (Press ctrl-c to force)"); + shutdown.wait_for_shutdown().await; + + log::info!("Stopping nym-client"); + Ok(()) + } + + pub async fn start_socket(self) -> Result { + if !self.config.get_socket_type().is_websocket() { + return Err(ClientError::InvalidSocketMode); + } + + let base_builder = BaseClientBuilder::new_from_base_config( + self.config.get_base(), + self.key_manager, + Some(Self::create_bandwidth_controller(&self.config).await), + non_wasm_helpers::setup_fs_reply_surb_backend( + self.config.get_base().get_reply_surb_database_path(), + self.config.get_debug_settings(), + ) + .await?, + ); + + let self_address = base_builder.as_mix_recipient(); + let mut started_client = base_builder.start_base().await?; + let client_input = started_client.client_input.register_producer(); + let client_output = started_client.client_output.register_consumer(); + + Self::start_websocket_listener(&self.config, client_input, client_output, self_address); + + info!("Client startup finished!"); + info!("The address of this client is: {}", self_address); + + Ok(started_client.shutdown_notifier) + } + + pub async fn start_direct(self) -> Result { + if self.config.get_socket_type().is_websocket() { + return Err(ClientError::InvalidSocketMode); + } + + let base_client = BaseClientBuilder::new_from_base_config( + self.config.get_base(), + self.key_manager, + Some(Self::create_bandwidth_controller(&self.config).await), + non_wasm_helpers::setup_fs_reply_surb_backend( + self.config.get_base().get_reply_surb_database_path(), + self.config.get_debug_settings(), + ) + .await?, + ); + + let mut started_client = base_client.start_base().await?; + let client_input = started_client.client_input.register_producer(); + let client_output = started_client.client_output.register_consumer(); + + // register our receiver + let (reconstructed_sender, reconstructed_receiver) = mpsc::unbounded(); + + // tell the buffer to start sending stuff to us + client_output + .received_buffer_request_sender + .unbounded_send(ReceivedBufferMessage::ReceiverAnnounce( + reconstructed_sender, + )) + .expect("the buffer request failed!"); + + Ok(DirectClient { + client_input, + reconstructed_receiver, + _shutdown_notifier: started_client.shutdown_notifier, + }) + } +} + +pub struct DirectClient { + client_input: ClientInput, + reconstructed_receiver: ReconstructedMessagesReceiver, + + // we need to keep reference to this guy otherwise things will start dropping + _shutdown_notifier: ShutdownNotifier, +} + +impl DirectClient { /// EXPERIMENTAL DIRECT RUST API /// It's untested and there are absolutely no guarantees about it (but seems to have worked /// well enough in local tests) - pub async fn send_message( - &mut self, - recipient: Recipient, - message: Vec, - with_reply_surb: bool, - ) { + pub async fn send_regular_message(&mut self, recipient: Recipient, message: Vec) { let lane = TransmissionLane::General; - let input_msg = InputMessage::new_fresh(recipient, message, with_reply_surb, lane); + let input_msg = InputMessage::new_regular(recipient, message, lane); - self.input_tx - .as_ref() - .expect("start method was not called before!") + self.client_input + .input_sender .send(input_msg) .await .expect("InputMessageReceiver has stopped receiving!"); @@ -353,12 +223,31 @@ impl NymClient { /// EXPERIMENTAL DIRECT RUST API /// It's untested and there are absolutely no guarantees about it (but seems to have worked /// well enough in local tests) - pub async fn send_reply(&mut self, reply_surb: ReplySurb, message: Vec) { - let input_msg = InputMessage::new_reply(reply_surb, message); + pub async fn send_anonymous_message( + &mut self, + recipient: Recipient, + message: Vec, + reply_surbs: u32, + ) { + let lane = TransmissionLane::General; + let input_msg = InputMessage::new_anonymous(recipient, message, reply_surbs, lane); - self.input_tx - .as_ref() - .expect("start method was not called before!") + self.client_input + .input_sender + .send(input_msg) + .await + .expect("InputMessageReceiver has stopped receiving!"); + } + + /// EXPERIMENTAL DIRECT RUST API + /// It's untested and there are absolutely no guarantees about it (but seems to have worked + /// well enough in local tests) + pub async fn send_reply(&mut self, recipient_tag: AnonymousSenderTag, message: Vec) { + let lane = TransmissionLane::General; + let input_msg = InputMessage::new_reply(recipient_tag, message, lane); + + self.client_input + .input_sender .send(input_msg) .await .expect("InputMessageReceiver has stopped receiving!"); @@ -373,147 +262,9 @@ impl NymClient { pub async fn wait_for_messages(&mut self) -> Vec { use futures::StreamExt; - self.receive_tx - .as_mut() - .expect("start method was not called before!") + self.reconstructed_receiver .next() .await .expect("buffer controller seems to have somehow died!") } - - /// blocking version of `start` method. Will run forever (or until SIGINT is sent) - pub async fn run_forever(&mut self) -> Result<(), ClientError> { - let shutdown = self.start().await?; - wait_for_signal().await; - - println!( - "Received signal - the client will terminate now (threads are not yet nicely stopped, if you see stack traces that's alright)." - ); - - log::info!("Sending shutdown"); - shutdown.signal_shutdown().ok(); - - // Some of these components have shutdown signalling implemented as part of socks5 work, - // but since it's not fully implemented (yet) for all the components of the native client, - // we don't try to wait and instead just stop immediately. - //log::info!("Waiting for tasks to finish... (Press ctrl-c to force)"); - //shutdown.wait_for_shutdown().await; - - log::info!("Stopping nym-client"); - Ok(()) - } - - pub async fn start(&mut self) -> Result { - info!("Starting nym client"); - // channels for inter-component communication - // TODO: make the channels be internally created by the relevant components - // rather than creating them here, so say for example the buffer controller would create the request channels - // and would allow anyone to clone the sender channel - - // unwrapped_sphinx_sender is the transmitter of mixnet messages received from the gateway - // unwrapped_sphinx_receiver is the receiver for said messages - used by ReceivedMessagesBuffer - let (mixnet_messages_sender, mixnet_messages_receiver) = mpsc::unbounded(); - - // used for announcing connection or disconnection of a channel for pushing re-assembled messages to - let (received_buffer_request_sender, received_buffer_request_receiver) = mpsc::unbounded(); - - // channels responsible for controlling real messages - let (input_sender, input_receiver) = tokio::sync::mpsc::channel::(1); - - // channels responsible for controlling ack messages - let (ack_sender, ack_receiver) = mpsc::unbounded(); - let shared_topology_accessor = TopologyAccessor::new(); - - let reply_key_storage = - ReplyKeyStorage::load(self.config.get_base().get_reply_encryption_key_store_path()) - .tap_err(|err| { - log::error!("Failed to load reply key storage - is it perhaps already in use?"); - log::error!("{}", err); - })?; - - // Shutdown notifier for signalling tasks to stop - let shutdown = ShutdownNotifier::default(); - - // the components are started in very specific order. Unless you know what you are doing, - // do not change that. - self.start_topology_refresher(shared_topology_accessor.clone(), shutdown.subscribe()) - .await?; - self.start_received_messages_buffer_controller( - received_buffer_request_receiver, - mixnet_messages_receiver, - reply_key_storage.clone(), - shutdown.subscribe(), - ); - - let gateway_client = self - .start_gateway_client(mixnet_messages_sender, ack_sender, shutdown.subscribe()) - .await; - - // The sphinx_message_sender is the transmitter for any component generating sphinx packets - // that are to be sent to the mixnet. They are used by cover traffic stream and real - // traffic stream. - // The MixTrafficController then sends the actual traffic - let sphinx_message_sender = - Self::start_mix_traffic_controller(gateway_client, shutdown.subscribe()); - - // Channels that the websocket listener can use to signal downstream to the real traffic - // controller that connections are closed. - let (client_connection_tx, client_connection_rx) = mpsc::unbounded(); - - // Shared queue length data. Published by the `OutQueueController` in the client, and used - // primarily to throttle incoming connections (e.g socks5 for attached network-requesters) - let shared_lane_queue_lengths = LaneQueueLengths::new(); - - self.start_real_traffic_controller( - shared_topology_accessor.clone(), - reply_key_storage, - ack_receiver, - input_receiver, - sphinx_message_sender.clone(), - shared_lane_queue_lengths.clone(), - client_connection_rx, - shutdown.subscribe(), - ); - - if !self - .config - .get_base() - .get_disabled_loop_cover_traffic_stream() - { - self.start_cover_traffic_stream( - shared_topology_accessor, - sphinx_message_sender, - shutdown.subscribe(), - ); - } - - match self.config.get_socket_type() { - SocketType::WebSocket => self.start_websocket_listener( - received_buffer_request_sender, - input_sender, - shared_lane_queue_lengths, - client_connection_tx, - ), - SocketType::None => { - // if we did not start the socket, it means we're running (supposedly) in the native mode - // and hence we should announce 'ourselves' to the buffer - let (reconstructed_sender, reconstructed_receiver) = mpsc::unbounded(); - - // tell the buffer to start sending stuff to us - received_buffer_request_sender - .unbounded_send(ReceivedBufferMessage::ReceiverAnnounce( - reconstructed_sender, - )) - .expect("the buffer request failed!"); - - self.receive_tx = Some(reconstructed_receiver); - self.input_tx = Some(input_sender); - } - } - - info!("Client startup finished!"); - info!("The address of this client is: {}", self.as_mix_recipient()); - - Ok(shutdown) - } } diff --git a/clients/native/src/commands/init.rs b/clients/native/src/commands/init.rs index 6a38770eec..3caf3a5d0d 100644 --- a/clients/native/src/commands/init.rs +++ b/clients/native/src/commands/init.rs @@ -1,14 +1,17 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use clap::Args; -use client_core::{config::GatewayEndpoint, error::ClientCoreError}; -use config::NymConfig; - use crate::{ client::config::Config, commands::{override_config, OverrideConfig}, + error::ClientError, }; +use clap::Args; +use config::NymConfig; +use nymsphinx::addressing::clients::Recipient; +use serde::Serialize; +use std::fmt::Display; +use tap::TapFallible; #[derive(Args, Clone)] pub(crate) struct Init { @@ -46,11 +49,19 @@ pub(crate) struct Init { #[clap(long, hidden = true)] fastmode: bool, + /// Disable loop cover traffic and the Poisson rate limiter (for debugging only) + #[clap(long, hidden = true)] + no_cover: bool, + /// Set this client to work in a enabled credentials mode that would attempt to use gateway /// with bandwidth credential requirement. #[cfg(feature = "coconut")] #[clap(long)] enabled_credentials_mode: bool, + + /// Save a summary of the initialization to a json file + #[clap(long)] + output_json: bool, } impl From for OverrideConfig { @@ -61,6 +72,7 @@ impl From for OverrideConfig { disable_socket: init_config.disable_socket, port: init_config.port, fastmode: init_config.fastmode, + no_cover: init_config.no_cover, #[cfg(feature = "coconut")] enabled_credentials_mode: init_config.enabled_credentials_mode, @@ -68,7 +80,30 @@ impl From for OverrideConfig { } } -pub(crate) async fn execute(args: &Init) { +#[derive(Debug, Serialize)] +pub struct InitResults { + #[serde(flatten)] + client_core: client_core::init::InitResults, + client_listening_port: String, +} + +impl InitResults { + fn new(config: &Config, address: &Recipient) -> Self { + Self { + client_core: client_core::init::InitResults::new(config.get_base(), address), + client_listening_port: config.get_listening_port().to_string(), + } + } +} + +impl Display for InitResults { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + writeln!(f, "{}", self.client_core)?; + write!(f, "Client listening port: {}", self.client_listening_port) + } +} + +pub(crate) async fn execute(args: &Init) -> Result<(), ClientError> { println!("Initialising client..."); let id = &args.id; @@ -92,25 +127,44 @@ pub(crate) async fn execute(args: &Init) { let register_gateway = !already_init || user_wants_force_register; // Attempt to use a user-provided gateway, if possible - let user_chosen_gateway_id = args.gateway.as_deref(); + let user_chosen_gateway_id = args.gateway.clone(); - let mut config = Config::new(id); - let override_config_fields = OverrideConfig::from(args.clone()); - config = override_config(config, override_config_fields); + // Load and potentially override config + let mut config = override_config(Config::new(id), OverrideConfig::from(args.clone())); + + // Setup gateway by either registering a new one, or creating a new config from the selected + // one but with keys kept, or reusing the gateway configuration. + let gateway = client_core::init::setup_gateway::<_, Config, _>( + register_gateway, + user_chosen_gateway_id, + config.get_base(), + ) + .await + .tap_err(|err| eprintln!("Failed to setup gateway\nError: {err}"))?; - let gateway = setup_gateway(id, register_gateway, user_chosen_gateway_id, &config) - .await - .unwrap_or_else(|err| { - eprintln!("Failed to setup gateway\nError: {err}"); - std::process::exit(1) - }); config.get_base_mut().with_gateway_endpoint(gateway); - let config_save_location = config.get_config_file_save_location(); - config - .save_to_file(None) - .expect("Failed to save the config file"); + config.save_to_file(None).tap_err(|_| { + log::error!("Failed to save the config file"); + })?; + print_saved_config(&config); + + let address = client_core::init::get_client_address_from_stored_keys(config.get_base())?; + let init_results = InitResults::new(&config, &address); + println!("{}", init_results); + + // Output summary to a json file, if specified + if args.output_json { + client_core::init::output_to_json(&init_results, "client_init_results.json"); + } + + println!("\nThe address of this client is: {}\n", address); + Ok(()) +} + +fn print_saved_config(config: &Config) { + let config_save_location = config.get_config_file_save_location(); println!("Saved configuration file to {:?}", config_save_location); println!("Using gateway: {}", config.get_base().get_gateway_id()); log::debug!("Gateway id: {}", config.get_base().get_gateway_id()); @@ -119,63 +173,5 @@ pub(crate) async fn execute(args: &Init) { "Gateway listener: {}", config.get_base().get_gateway_listener() ); - println!("Client configuration completed."); - - client_core::init::show_address(config.get_base()).unwrap_or_else(|err| { - eprintln!("Failed to show address\nError: {err}"); - std::process::exit(1) - }); -} - -async fn setup_gateway( - id: &str, - register: bool, - user_chosen_gateway_id: Option<&str>, - config: &Config, -) -> Result { - if register { - // Get the gateway details by querying the validator-api. Either pick one at random or use - // the chosen one if it's among the available ones. - println!("Configuring gateway"); - let gateway = client_core::init::query_gateway_details( - config.get_base().get_validator_api_endpoints(), - user_chosen_gateway_id, - ) - .await?; - log::debug!("Querying gateway gives: {}", gateway); - - // Registering with gateway by setting up and writing shared keys to disk - log::trace!("Registering gateway"); - client_core::init::register_with_gateway_and_store_keys(gateway.clone(), config.get_base()) - .await?; - println!("Saved all generated keys"); - - Ok(gateway.into()) - } else if user_chosen_gateway_id.is_some() { - // Just set the config, don't register or create any keys - // This assumes that the user knows what they are doing, and that the existing keys are - // valid for the gateway being used - println!("Using gateway provided by user, keeping existing keys"); - let gateway = client_core::init::query_gateway_details( - config.get_base().get_validator_api_endpoints(), - user_chosen_gateway_id, - ) - .await?; - log::debug!("Querying gateway gives: {}", gateway); - Ok(gateway.into()) - } else { - println!("Not registering gateway, will reuse existing config and keys"); - let existing_config = Config::load_from_file(Some(id)).map_err(|err| { - log::error!( - "Unable to configure gateway: {err}. \n - Seems like the client was already initialized but it was not possible to read \ - the existing configuration file. \n - CAUTION: Consider backing up your gateway keys and try force gateway registration, or \ - removing the existing configuration and starting over." - ); - ClientCoreError::CouldNotLoadExistingGatewayConfiguration(err) - })?; - - Ok(existing_config.get_base().get_gateway_endpoint().clone()) - } + println!("Client configuration completed.\n"); } diff --git a/clients/native/src/commands/mod.rs b/clients/native/src/commands/mod.rs index 648a1d6002..9dc6a2ac33 100644 --- a/clients/native/src/commands/mod.rs +++ b/clients/native/src/commands/mod.rs @@ -80,6 +80,7 @@ pub(crate) struct OverrideConfig { disable_socket: bool, port: Option, fastmode: bool, + no_cover: bool, #[cfg(feature = "coconut")] enabled_credentials_mode: bool, @@ -89,7 +90,7 @@ pub(crate) async fn execute(args: &Cli) -> Result<(), ClientError> { let bin_name = "nym-native-client"; match &args.command { - Commands::Init(m) => init::execute(m).await, + Commands::Init(m) => init::execute(m).await?, Commands::Run(m) => run::execute(m).await?, Commands::Upgrade(m) => upgrade::execute(m), Commands::Completions(s) => s.generate(&mut Cli::into_app(), bin_name), @@ -141,6 +142,10 @@ pub(crate) fn override_config(mut config: Config, args: OverrideConfig) -> Confi config.get_base_mut().set_high_default_traffic_volume(); } + if args.no_cover { + config.get_base_mut().set_no_cover_traffic(); + } + config } diff --git a/clients/native/src/commands/run.rs b/clients/native/src/commands/run.rs index d5c892fadc..4267fcbac5 100644 --- a/clients/native/src/commands/run.rs +++ b/clients/native/src/commands/run.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use crate::{ - client::{config::Config, NymClient}, + client::{config::Config, SocketClient}, commands::{override_config, OverrideConfig}, error::ClientError, }; @@ -39,6 +39,15 @@ pub(crate) struct Run { #[clap(short, long)] port: Option, + /// Mostly debug-related option to increase default traffic rate so that you would not need to + /// modify config post init + #[clap(long, hidden = true)] + fastmode: bool, + + /// Disable loop cover traffic and the Poisson rate limiter (for debugging only) + #[clap(long, hidden = true)] + no_cover: bool, + /// Set this client to work in a enabled credentials mode that would attempt to use gateway /// with bandwidth credential requirement. #[cfg(feature = "coconut")] @@ -53,7 +62,8 @@ impl From for OverrideConfig { api_validators: run_config.api_validators, disable_socket: run_config.disable_socket, port: run_config.port, - fastmode: false, + fastmode: run_config.fastmode, + no_cover: run_config.no_cover, #[cfg(feature = "coconut")] enabled_credentials_mode: run_config.enabled_credentials_mode, } @@ -98,5 +108,5 @@ pub(crate) async fn execute(args: &Run) -> Result<(), ClientError> { return Err(ClientError::FailedLocalVersionCheck); } - NymClient::new(config).run_forever().await + SocketClient::new(config).run_socket_forever().await } diff --git a/clients/native/src/error.rs b/clients/native/src/error.rs index 6ffdc86e99..050e79ebc5 100644 --- a/clients/native/src/error.rs +++ b/clients/native/src/error.rs @@ -1,25 +1,20 @@ -use client_core::{client::reply_key_storage::ReplyKeyStorageError, error::ClientCoreError}; -use crypto::asymmetric::identity::Ed25519RecoveryError; -use gateway_client::error::GatewayClientError; -use validator_client::ValidatorClientError; +use client_core::client::replies::reply_storage::fs_backend; +use client_core::error::ClientCoreError; #[derive(thiserror::Error, Debug)] pub enum ClientError { #[error("I/O error: {0}")] IoError(#[from] std::io::Error), - #[error("Gateway client error: {0}")] - GatewayClientError(#[from] GatewayClientError), - #[error("Ed25519 error: {0}")] - Ed25519RecoveryError(#[from] Ed25519RecoveryError), - #[error("Validator client error: {0}")] - ValidatorClientError(#[from] ValidatorClientError), + #[error("client-core error: {0}")] - ClientCoreError(#[from] ClientCoreError), - #[error("Reply key storage error: {0}")] - ReplyKeyStorageError(#[from] ReplyKeyStorageError), + ClientCoreError(#[from] ClientCoreError), #[error("Failed to load config for: {0}")] FailedToLoadConfig(String), + #[error("Failed local version check, client and config mismatch")] FailedLocalVersionCheck, + + #[error("Attempted to start the client in invalid socket mode")] + InvalidSocketMode, } diff --git a/clients/native/src/websocket/handler.rs b/clients/native/src/websocket/handler.rs index 11d0dda55f..7d8d16e97f 100644 --- a/clients/native/src/websocket/handler.rs +++ b/clients/native/src/websocket/handler.rs @@ -14,7 +14,7 @@ use futures::channel::mpsc; use futures::{SinkExt, StreamExt}; use log::*; use nymsphinx::addressing::clients::Recipient; -use nymsphinx::anonymous_replies::ReplySurb; +use nymsphinx::anonymous_replies::requests::AnonymousSenderTag; use nymsphinx::receiver::ReconstructedMessage; use tokio::net::TcpStream; use tokio_tungstenite::{ @@ -62,9 +62,13 @@ impl Clone for Handler { impl Drop for Handler { fn drop(&mut self) { - self.buffer_requester + if self + .buffer_requester .unbounded_send(ReceivedBufferMessage::ReceiverDisconnect) - .expect("the buffer request failed!") + .is_err() + { + error!("we failed to disconnect the receiver from the buffer! presumably the shutdown procedure has been initiated!") + } } } @@ -73,14 +77,14 @@ impl Handler { msg_input: InputMessageSender, client_connection_tx: ConnectionCommandSender, buffer_requester: ReceivedBufferRequestSender, - self_full_address: &Recipient, + self_full_address: Recipient, lane_queue_lengths: LaneQueueLengths, ) -> Self { Handler { msg_input, client_connection_tx, buffer_requester, - self_full_address: *self_full_address, + self_full_address, socket: None, received_response_type: Default::default(), lane_queue_lengths, @@ -89,18 +93,22 @@ impl Handler { async fn handle_send( &mut self, - recipient: &Recipient, + recipient: Recipient, message: Vec, - with_reply_surb: bool, connection_id: Option, ) -> Option { + info!( + "Attempting to send {:.2} kiB message to {recipient} on connection_id {connection_id:?}", + message.len() as f64 / 1024.0 + ); + // We map the absence of a connection id as going into the general lane. let lane = connection_id.map_or(TransmissionLane::General, |id| { TransmissionLane::ConnectionId(id) }); // the ack control is now responsible for chunking, etc. - let input_msg = InputMessage::new_fresh(*recipient, message, with_reply_surb, lane); + let input_msg = InputMessage::new_regular(recipient, message, lane); self.msg_input .send(input_msg) .await @@ -109,53 +117,119 @@ impl Handler { // Only reply back with a `LaneQueueLength` if the sender providided a connection id let connection_id = match lane { TransmissionLane::General - | TransmissionLane::Reply + | TransmissionLane::ReplySurbRequest | TransmissionLane::Retransmission - | TransmissionLane::Control => return None, + | TransmissionLane::AdditionalReplySurbs => return None, TransmissionLane::ConnectionId(id) => id, }; // on receiving a send, we reply back the current lane queue length for that connection id. // Note that this does _NOT_ take into account the packets that have been received but not // yet reach `OutQueueControl`, so it might be a tad low. - let Ok(lane_queue_lengths) = self.lane_queue_lengths.lock() else { - log::warn!( - "Failed to get the lane queue length lock, \ - not responding back with the current queue length" - ); - return None; - }; - - let queue_length = lane_queue_lengths.get(&lane).unwrap_or(0); - Some(ServerResponse::LaneQueueLength(connection_id, queue_length)) - } - - async fn handle_reply( - &mut self, - reply_surb: ReplySurb, - message: Vec, - ) -> Option { - if message.len() > ReplySurb::max_msg_len(Default::default()) { - return Some( - ServerResponse::new_error( - format!( - "too long message to put inside a reply SURB. Received: {} bytes and maximum is {} bytes", - message.len(), ReplySurb::max_msg_len(Default::default())) - ) - ); + if let Ok(lane_queue_lengths) = self.lane_queue_lengths.lock() { + let queue_length = lane_queue_lengths.get(&lane).unwrap_or(0); + return Some(ServerResponse::LaneQueueLength { + lane: connection_id, + queue_length, + }); } - let input_msg = InputMessage::new_reply(reply_surb, message); + log::warn!("Failed to get the lane queue length lock, not responding back with the current queue length"); + None + } + + async fn handle_send_anonymous( + &mut self, + recipient: Recipient, + message: Vec, + reply_surbs: u32, + connection_id: Option, + ) -> Option { + info!( + "Attempting to anonymously send {:.2} kiB message to {recipient} on connection_id {connection_id:?} while attaching {reply_surbs} replySURBs.", + message.len() as f64 / 1024.0 + ); + + // We map the absence of a connection id as going into the general lane. + let lane = connection_id.map_or(TransmissionLane::General, |id| { + TransmissionLane::ConnectionId(id) + }); + + let input_msg = InputMessage::new_anonymous(recipient, message, reply_surbs, lane); self.msg_input .send(input_msg) .await .expect("InputMessageReceiver has stopped receiving!"); + // Only reply back with a `LaneQueueLength` if the sender providided a connection id + let connection_id = match lane { + TransmissionLane::General + | TransmissionLane::ReplySurbRequest + | TransmissionLane::Retransmission + | TransmissionLane::AdditionalReplySurbs => return None, + TransmissionLane::ConnectionId(id) => id, + }; + + // on receiving a send, we reply back the current lane queue length for that connection id. + // Note that this does _NOT_ take into account the packets that have been received but not + // yet reach `OutQueueControl`, so it might be a tad low. + if let Ok(lane_queue_lengths) = self.lane_queue_lengths.lock() { + let queue_length = lane_queue_lengths.get(&lane).unwrap_or(0); + return Some(ServerResponse::LaneQueueLength { + lane: connection_id, + queue_length, + }); + } + + log::warn!("Failed to get the lane queue length lock, not responding back with the current queue length"); + None + } + + async fn handle_reply( + &mut self, + recipient_tag: AnonymousSenderTag, + message: Vec, + connection_id: Option, + ) -> Option { + info!("Attempting to send {:.2} kiB reply message to {recipient_tag} on connection_id {connection_id:?}", message.len() as f64 / 1024.0); + + // We map the absence of a connection id as going into the general lane. + let lane = connection_id.map_or(TransmissionLane::General, |id| { + TransmissionLane::ConnectionId(id) + }); + + let input_msg = InputMessage::new_reply(recipient_tag, message, lane); + self.msg_input + .send(input_msg) + .await + .expect("InputMessageReceiver has stopped receiving!"); + + // Only reply back with a `LaneQueueLength` if the sender providided a connection id + let connection_id = match lane { + TransmissionLane::General + | TransmissionLane::ReplySurbRequest + | TransmissionLane::Retransmission + | TransmissionLane::AdditionalReplySurbs => return None, + TransmissionLane::ConnectionId(id) => id, + }; + + // on receiving a send, we reply back the current lane queue length for that connection id. + // Note that this does _NOT_ take into account the packets that have been received but not + // yet reach `OutQueueControl`, so it might be a tad low. + if let Ok(lane_queue_lengths) = self.lane_queue_lengths.lock() { + let queue_length = lane_queue_lengths.get(&lane).unwrap_or(0); + return Some(ServerResponse::LaneQueueLength { + lane: connection_id, + queue_length, + }); + } + + log::warn!("Failed to get the lane queue length lock, not responding back with the current queue length"); None } fn handle_self_address(&self) -> ServerResponse { - ServerResponse::SelfAddress(self.self_full_address) + ServerResponse::SelfAddress(Box::new(self.self_full_address)) } fn handle_closed_connection(&self, connection_id: u64) -> Option { @@ -175,7 +249,10 @@ impl Handler { let lane = TransmissionLane::ConnectionId(connection_id); let queue_length = lane_queue_lengths.get(&lane).unwrap_or(0); - Some(ServerResponse::LaneQueueLength(connection_id, queue_length)) + Some(ServerResponse::LaneQueueLength { + lane: connection_id, + queue_length, + }) } async fn handle_request(&mut self, request: ClientRequest) -> Option { @@ -183,16 +260,25 @@ impl Handler { ClientRequest::Send { recipient, message, - with_reply_surb, + connection_id, + } => self.handle_send(recipient, message, connection_id).await, + + ClientRequest::SendAnonymous { + recipient, + message, + reply_surbs, connection_id, } => { - self.handle_send(&recipient, message, with_reply_surb, connection_id) + self.handle_send_anonymous(recipient, message, reply_surbs, connection_id) .await } + ClientRequest::Reply { message, - reply_surb, - } => self.handle_reply(reply_surb, message).await, + sender_tag, + connection_id, + } => self.handle_reply(sender_tag, message, connection_id).await, + ClientRequest::SelfAddress => Some(self.handle_self_address()), ClientRequest::ClosedConnection(id) => self.handle_closed_connection(id), ClientRequest::GetLaneQueueLength(id) => self.handle_get_lane_queue_length(id), @@ -299,8 +385,7 @@ impl Handler { if let Some(response) = self.handle_ws_request(socket_msg).await { if let Err(err) = self.send_websocket_response(response).await { warn!( - "Failed to send message over websocket: {}. Assuming the connection is dead.", - err + "Failed to send message over websocket: {err}. Assuming the connection is dead.", ); break; } @@ -308,9 +393,10 @@ impl Handler { } // or a reconstructed mix message that we need to push back to the client mix_messages = msg_receiver.next() => { - let mix_messages = mix_messages.expect( - "mix messages sender was unexpectedly closed! this shouldn't have ever happened!", - ); + let Some(mix_messages) = mix_messages else { + error!("mix messages sender was unexpectedly closed! this shouldn't have ever happened! (unless we're shutting down - TODO: implement proper graceful shutdown handler)"); + return + }; if let Err(e) = self.push_websocket_received_plaintexts(mix_messages).await { warn!("failed to send sphinx packets back to the client - {:?}, assuming the connection is dead", e); break; diff --git a/clients/native/src/websocket/listener.rs b/clients/native/src/websocket/listener.rs index bdaa27d4fc..c7825fb345 100644 --- a/clients/native/src/websocket/listener.rs +++ b/clients/native/src/websocket/listener.rs @@ -54,9 +54,8 @@ impl Listener { Ok((mut socket, remote_addr)) => { debug!("Received connection from {:?}", remote_addr); if self.state.is_connected() { - warn!("tried to duplicate!"); + warn!("Tried to open a duplicate websocket connection. The request came from {}", remote_addr); // if we've already got a connection, don't allow another one - debug!("but there was already a connection present!"); // while we only ever want to accept a single connection, we don't want // to leave clients hanging (and also allow for reconnection if it somehow // was dropped) diff --git a/clients/native/websocket-requests/src/error.rs b/clients/native/websocket-requests/src/error.rs index f9fdd029cf..2d9035c19d 100644 --- a/clients/native/websocket-requests/src/error.rs +++ b/clients/native/websocket-requests/src/error.rs @@ -24,8 +24,11 @@ impl fmt::Debug for Error { } impl Error { - pub fn new(kind: ErrorKind, message: String) -> Self { - Error { kind, message } + pub fn new>(kind: ErrorKind, message: S) -> Self { + Error { + kind, + message: message.into(), + } } } @@ -62,6 +65,31 @@ pub enum ErrorKind { Other = 0xFF, } +impl TryFrom for ErrorKind { + type Error = Error; + + fn try_from(value: u8) -> Result { + match value { + _ if value == (ErrorKind::EmptyRequest as u8) => Ok(ErrorKind::EmptyRequest), + _ if value == (ErrorKind::TooShortRequest as u8) => Ok(ErrorKind::TooShortRequest), + _ if value == (ErrorKind::UnknownRequest as u8) => Ok(ErrorKind::UnknownRequest), + _ if value == (ErrorKind::MalformedRequest as u8) => Ok(ErrorKind::MalformedRequest), + + _ if value == (ErrorKind::EmptyResponse as u8) => Ok(ErrorKind::EmptyResponse), + _ if value == (ErrorKind::TooShortResponse as u8) => Ok(ErrorKind::TooShortResponse), + _ if value == (ErrorKind::UnknownResponse as u8) => Ok(ErrorKind::UnknownResponse), + _ if value == (ErrorKind::MalformedResponse as u8) => Ok(ErrorKind::MalformedResponse), + + _ if value == (ErrorKind::Other as u8) => Ok(ErrorKind::Other), + + n => Err(Error::new( + ErrorKind::MalformedResponse, + format!("invalid error code {}", n), + )), + } + } +} + impl ErrorKind { pub(crate) fn as_str(&self) -> &'static str { match *self { diff --git a/clients/native/websocket-requests/src/requests.rs b/clients/native/websocket-requests/src/requests.rs index 315399ae3d..f50364a3cf 100644 --- a/clients/native/websocket-requests/src/requests.rs +++ b/clients/native/websocket-requests/src/requests.rs @@ -1,4 +1,4 @@ -// Copyright 2021 - Nym Technologies SA +// Copyright 2021-2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 // all variable size data is always prefixed with u64 length @@ -7,69 +7,115 @@ use crate::error::{self, ErrorKind}; use crate::text::ClientRequestText; use nymsphinx::addressing::clients::Recipient; -use nymsphinx::anonymous_replies::ReplySurb; +use nymsphinx::anonymous_replies::requests::{AnonymousSenderTag, SENDER_TAG_SIZE}; use std::convert::{TryFrom, TryInto}; use std::mem::size_of; -/// Value tag representing [`Send`] variant of the [`ClientRequest`] -pub const SEND_REQUEST_TAG: u8 = 0x00; +#[repr(u8)] +enum ClientRequestTag { + /// Value tag representing [`Send`] variant of the [`ClientRequest`] + Send = 0x00, -/// Value tag representing [`Reply`] variant of the [`ClientRequest`] -pub const REPLY_REQUEST_TAG: u8 = 0x01; + /// Value tag representing [`SendAnonymous`] variant of the [`ClientRequest`] + SendAnonymous = 0x01, -/// Value tag representing [`SelfAddress`] variant of the [`ClientRequest`] -pub const SELF_ADDRESS_REQUEST_TAG: u8 = 0x02; + /// Value tag representing [`Reply`] variant of the [`ClientRequest`] + Reply = 0x02, -/// Value tag representing [`ClosedConnection`] variant of the [`ClientRequest`] -pub const CLOSED_CONNECTION_REQUEST_TAG: u8 = 0x03; + /// Value tag representing [`SelfAddress`] variant of the [`ClientRequest`] + SelfAddress = 0x03, -/// Value tag representing [`GetLaneQueueLength`] variant of the [`ClientRequest`] -pub const GET_LANE_QUEUE_LENGHT_TAG: u8 = 0x04; + /// Value tag representing [`ClosedConnection`] variant of the [`ClientRequest`] + ClosedConnection = 0x04, + + /// Value tag representing [`GetLaneQueueLength`] variant of the [`ClientRequest`] + GetLaneQueueLength = 0x05, +} + +impl TryFrom for ClientRequestTag { + type Error = error::Error; + + fn try_from(value: u8) -> Result { + match value { + _ if value == (Self::Send as u8) => Ok(Self::Send), + _ if value == (Self::SendAnonymous as u8) => Ok(Self::SendAnonymous), + _ if value == (Self::Reply as u8) => Ok(Self::Reply), + _ if value == (Self::SelfAddress as u8) => Ok(Self::SelfAddress), + _ if value == (Self::ClosedConnection as u8) => Ok(Self::ClosedConnection), + _ if value == (Self::GetLaneQueueLength as u8) => Ok(Self::GetLaneQueueLength), + n => Err(error::Error::new( + ErrorKind::UnknownRequest, + format!("{n} does not correspond to any valid request tag"), + )), + } + } +} #[allow(non_snake_case)] #[derive(Debug)] pub enum ClientRequest { + /// The simplest message variant where no additional information is attached. + /// You're simply sending your `data` to specified `recipient` without any tagging. + /// + /// Ends up with `NymMessage::Plain` variant Send { recipient: Recipient, message: Vec, - // Perhaps we could change it to a number to indicate how many reply_SURBs we want to include? - with_reply_surb: bool, connection_id: Option, }, - Reply { + + /// Create a message used for a duplex anonymous communication where the recipient + /// will never learn of our true identity. This is achieved by carefully sending `reply_surbs`. + /// + /// Note that if reply_surbs is set to zero then + /// this variant requires the client having sent some reply_surbs in the past + /// (and thus the recipient also knowing our sender tag). + /// + /// Ends up with `NymMessage::Repliable` variant + SendAnonymous { + recipient: Recipient, message: Vec, - reply_surb: ReplySurb, + reply_surbs: u32, + connection_id: Option, }, + + /// Attempt to use our internally received and stored `ReplySurb` to send the message back + /// to specified recipient whilst not knowing its full identity (or even gateway). + /// + /// Ends up with `NymMessage::Reply` variant + Reply { + sender_tag: AnonymousSenderTag, + message: Vec, + connection_id: Option, + }, + SelfAddress, + ClosedConnection(u64), + GetLaneQueueLength(u64), } // we could have been parsing it directly TryFrom, but we want to retain // information about whether it came from binary or text to send appropriate response back impl ClientRequest { - // SEND_REQUEST_TAG || with_surb || recipient || conn_id || data_len || data - fn serialize_send( - recipient: Recipient, - data: Vec, - with_reply_surb: bool, - connection_id: Option, - ) -> Vec { + // SEND_REQUEST_TAG || recipient || conn_id || data_len || data + fn serialize_send(recipient: Recipient, data: Vec, connection_id: Option) -> Vec { let data_len_bytes = (data.len() as u64).to_be_bytes(); let conn_id_bytes = connection_id.unwrap_or(0).to_be_bytes(); - std::iter::once(SEND_REQUEST_TAG) - .chain(std::iter::once(with_reply_surb as u8)) - .chain(recipient.to_bytes().iter().cloned()) // will not be length prefixed because the length is constant - .chain(conn_id_bytes.iter().cloned()) - .chain(data_len_bytes.iter().cloned()) + + std::iter::once(ClientRequestTag::Send as u8) + .chain(recipient.to_bytes().into_iter()) // will not be length prefixed because the length is constant + .chain(conn_id_bytes.into_iter()) + .chain(data_len_bytes.into_iter()) .chain(data.into_iter()) .collect() } - // SEND_REQUEST_TAG || with_reply || recipient || conn_id || data_len || data + // SEND_REQUEST_TAG || recipient || conn_id || data_len || data fn deserialize_send(b: &[u8]) -> Result { - // we need to have at least 1 (tag) + 1 (reply flag) + Recipient::LEN + 2*sizeof bytes - if b.len() < 2 + Recipient::LEN + 2 * size_of::() { + // we need to have at least 1 (tag) + Recipient::LEN + 2*sizeof bytes + if b.len() < 1 + Recipient::LEN + 2 * size_of::() { return Err(error::Error::new( ErrorKind::TooShortRequest, "not enough data provided to recover 'send'".to_string(), @@ -77,21 +123,10 @@ impl ClientRequest { } // this MUST match because it was called by 'deserialize' - debug_assert_eq!(b[0], SEND_REQUEST_TAG); - - let with_reply_surb = match b[1] { - 0 => false, - 1 => true, - n => { - return Err(error::Error::new( - ErrorKind::MalformedRequest, - format!("invalid reply surb flag {}", n), - )) - } - }; + debug_assert_eq!(b[0], ClientRequestTag::Send as u8); let mut recipient_bytes = [0u8; Recipient::LEN]; - recipient_bytes.copy_from_slice(&b[2..2 + Recipient::LEN]); + recipient_bytes.copy_from_slice(&b[1..1 + Recipient::LEN]); let recipient = match Recipient::try_from_bytes(recipient_bytes) { Ok(recipient) => recipient, Err(err) => { @@ -104,7 +139,7 @@ impl ClientRequest { let mut connection_id_bytes = [0u8; size_of::()]; connection_id_bytes - .copy_from_slice(&b[2 + Recipient::LEN..2 + Recipient::LEN + size_of::()]); + .copy_from_slice(&b[1 + Recipient::LEN..1 + Recipient::LEN + size_of::()]); let connection_id = u64::from_be_bytes(connection_id_bytes); let connection_id = if connection_id == 0 { None @@ -113,9 +148,9 @@ impl ClientRequest { }; let data_len_bytes = - &b[2 + Recipient::LEN + size_of::()..2 + Recipient::LEN + 2 * size_of::()]; + &b[1 + Recipient::LEN + size_of::()..1 + Recipient::LEN + 2 * size_of::()]; let data_len = u64::from_be_bytes(data_len_bytes.try_into().unwrap()); - let data = &b[2 + Recipient::LEN + 2 * size_of::()..]; + let data = &b[1 + Recipient::LEN + 2 * size_of::()..]; if data.len() as u64 != data_len { return Err(error::Error::new( ErrorKind::MalformedRequest, @@ -128,33 +163,111 @@ impl ClientRequest { } Ok(ClientRequest::Send { - with_reply_surb, recipient, message: data.to_vec(), connection_id, }) } - // REPLY_REQUEST_TAG || surb_len || surb || message_len || message - fn serialize_reply(message: Vec, reply_surb: &ReplySurb) -> Vec { - let reply_surb_bytes = reply_surb.to_bytes(); - let surb_len_bytes = (reply_surb_bytes.len() as u64).to_be_bytes(); - let message_len_bytes = (message.len() as u64).to_be_bytes(); + // SEND_ANONYMOUS_REQUEST_TAG || reply_surbs || recipient || conn_id || data_len || data + fn serialize_send_anonymous( + recipient: Recipient, + data: Vec, + reply_surbs: u32, + connection_id: Option, + ) -> Vec { + let data_len_bytes = (data.len() as u64).to_be_bytes(); + let conn_id_bytes = connection_id.unwrap_or(0).to_be_bytes(); - std::iter::once(REPLY_REQUEST_TAG) - .chain(surb_len_bytes.iter().cloned()) - .chain(reply_surb_bytes.into_iter()) - .chain(message_len_bytes.iter().cloned()) + std::iter::once(ClientRequestTag::SendAnonymous as u8) + .chain(reply_surbs.to_be_bytes().into_iter()) + .chain(recipient.to_bytes().into_iter()) // will not be length prefixed because the length is constant + .chain(conn_id_bytes.into_iter()) + .chain(data_len_bytes.into_iter()) + .chain(data.into_iter()) + .collect() + } + + // SEND_ANONYMOUS_REQUEST_TAG || reply_surbs || recipient || data_len || data + fn deserialize_send_anonymous(b: &[u8]) -> Result { + // we need to have at least 1 (tag) + sizeof (num surbs) + Recipient::LEN + 2 *sizeof bytes + if b.len() < 1 + size_of::() + Recipient::LEN + 2 * size_of::() { + return Err(error::Error::new( + ErrorKind::TooShortRequest, + "not enough data provided to recover 'send_anonymous'".to_string(), + )); + } + + // this MUST match because it was called by 'deserialize' + debug_assert_eq!(b[0], ClientRequestTag::SendAnonymous as u8); + + let reply_surbs = u32::from_be_bytes([b[1], b[2], b[3], b[4]]); + + let mut recipient_bytes = [0u8; Recipient::LEN]; + recipient_bytes.copy_from_slice(&b[5..5 + Recipient::LEN]); + let recipient = match Recipient::try_from_bytes(recipient_bytes) { + Ok(recipient) => recipient, + Err(err) => { + return Err(error::Error::new( + ErrorKind::MalformedRequest, + format!("malformed recipient: {:?}", err), + )) + } + }; + + let mut connection_id_bytes = [0u8; size_of::()]; + connection_id_bytes + .copy_from_slice(&b[5 + Recipient::LEN..5 + Recipient::LEN + size_of::()]); + let connection_id = u64::from_be_bytes(connection_id_bytes); + let connection_id = if connection_id == 0 { + None + } else { + Some(connection_id) + }; + + let data_len_bytes = + &b[5 + Recipient::LEN + size_of::()..5 + Recipient::LEN + 2 * size_of::()]; + let data_len = u64::from_be_bytes(data_len_bytes.try_into().unwrap()); + let data = &b[5 + Recipient::LEN + 2 * size_of::()..]; + if data.len() as u64 != data_len { + return Err(error::Error::new( + ErrorKind::MalformedRequest, + format!( + "data len has inconsistent length. specified: {} got: {}", + data_len, + data.len() + ), + )); + } + + Ok(ClientRequest::SendAnonymous { + reply_surbs, + recipient, + message: data.to_vec(), + connection_id, + }) + } + + // REPLY_REQUEST_TAG || SENDER_TAG || conn_id || message_len || message + fn serialize_reply( + message: Vec, + sender_tag: AnonymousSenderTag, + connection_id: Option, + ) -> Vec { + let message_len_bytes = (message.len() as u64).to_be_bytes(); + let conn_id_bytes = connection_id.unwrap_or(0).to_be_bytes(); + + std::iter::once(ClientRequestTag::Reply as u8) + .chain(sender_tag.to_bytes().into_iter()) + .chain(conn_id_bytes.into_iter()) + .chain(message_len_bytes.into_iter()) .chain(message.into_iter()) .collect() } - // REPLY_REQUEST_TAG || surb_len || surb || message_len || message + // REPLY_REQUEST_TAG || SENDER_TAG || conn_id || message_len || message fn deserialize_reply(b: &[u8]) -> Result { - // we need to have at the very least 2 * sizeof bytes (in case, for some peculiar reason - // message and reply surb were 0 len - the request would still be malformed, but would in theory - // be parse'able) - if b.len() < 1 + 2 * size_of::() { + if b.len() < 1 + SENDER_TAG_SIZE + 2 * size_of::() { return Err(error::Error::new( ErrorKind::TooShortRequest, "not enough data provided to recover 'reply'".to_string(), @@ -162,42 +275,28 @@ impl ClientRequest { } // this MUST match because it was called by 'deserialize' - debug_assert_eq!(b[0], REPLY_REQUEST_TAG); + debug_assert_eq!(b[0], ClientRequestTag::Reply as u8); - let reply_surb_len = - u64::from_be_bytes(b[1..1 + size_of::()].as_ref().try_into().unwrap()); + // the unwrap here is fine as we're definitely using exactly SENDER_TAG_SIZE bytes + let sender_tag = + AnonymousSenderTag::from_bytes(b[1..1 + SENDER_TAG_SIZE].try_into().unwrap()); - // make sure we won't go out of bounds here - if reply_surb_len > (b.len() - 1 + 2 * size_of::()) as u64 { - return Err(error::Error::new( - ErrorKind::MalformedRequest, - format!( - "not enough data to recover reply surb with specified length {}", - reply_surb_len - ), - )); - } - - let surb_bound = 1 + size_of::() + reply_surb_len as usize; - - let reply_surb_bytes = &b[1 + size_of::()..surb_bound]; - let reply_surb = match ReplySurb::from_bytes(reply_surb_bytes) { - Ok(reply_surb) => reply_surb, - Err(err) => { - return Err(error::Error::new( - ErrorKind::MalformedRequest, - format!("malformed reply surb: {:?}", err), - )) - } + let mut connection_id_bytes = [0u8; size_of::()]; + connection_id_bytes + .copy_from_slice(&b[1 + SENDER_TAG_SIZE..1 + SENDER_TAG_SIZE + size_of::()]); + let connection_id = u64::from_be_bytes(connection_id_bytes); + let connection_id = if connection_id == 0 { + None + } else { + Some(connection_id) }; let message_len = u64::from_be_bytes( - b[surb_bound..surb_bound + size_of::()] - .as_ref() + b[1 + SENDER_TAG_SIZE + size_of::()..1 + SENDER_TAG_SIZE + 2 * size_of::()] .try_into() .unwrap(), ); - let message = &b[surb_bound + size_of::()..]; + let message = &b[1 + SENDER_TAG_SIZE + 2 * size_of::()..]; if message.len() as u64 != message_len { return Err(error::Error::new( ErrorKind::MalformedRequest, @@ -208,33 +307,32 @@ impl ClientRequest { ), )); } - // TODO: should this blow HERE, i.e. during deserialization that the data you're trying - // to send via reply is too long? Ok(ClientRequest::Reply { - reply_surb, message: message.to_vec(), + sender_tag, + connection_id, }) } // SELF_ADDRESS_REQUEST_TAG fn serialize_self_address() -> Vec { - std::iter::once(SELF_ADDRESS_REQUEST_TAG).collect() + vec![ClientRequestTag::SelfAddress as u8] } // SELF_ADDRESS_REQUEST_TAG - fn deserialize_self_address(b: &[u8]) -> Self { + fn deserialize_self_address(b: &[u8]) -> Result { // this MUST match because it was called by 'deserialize' - debug_assert_eq!(b[0], SELF_ADDRESS_REQUEST_TAG); + debug_assert_eq!(b[0], ClientRequestTag::SelfAddress as u8); - ClientRequest::SelfAddress + Ok(ClientRequest::SelfAddress) } // CLOSED_CONNECTION_REQUEST_TAG fn serialize_closed_connection(connection_id: u64) -> Vec { let conn_id_bytes = connection_id.to_be_bytes(); - std::iter::once(CLOSED_CONNECTION_REQUEST_TAG) - .chain(conn_id_bytes.iter().copied()) + std::iter::once(ClientRequestTag::ClosedConnection as u8) + .chain(conn_id_bytes.into_iter()) .collect() } @@ -243,12 +341,12 @@ impl ClientRequest { if b.len() != 1 + size_of::() { return Err(error::Error::new( ErrorKind::MalformedRequest, - "the received closed connection has invalid length".to_string(), + "The received closed connection has invalid length", )); } // this MUST match because it was called by 'deserialize' - debug_assert_eq!(b[0], CLOSED_CONNECTION_REQUEST_TAG); + debug_assert_eq!(b[0], ClientRequestTag::ClosedConnection as u8); let mut connection_id_bytes = [0u8; size_of::()]; connection_id_bytes.copy_from_slice(&b[1..=size_of::()]); @@ -260,8 +358,8 @@ impl ClientRequest { // GET_LANE_QUEUE_LENGHT_TAG fn serialize_get_lane_queue_lengths(connection_id: u64) -> Vec { let conn_id_bytes = connection_id.to_be_bytes(); - std::iter::once(GET_LANE_QUEUE_LENGHT_TAG) - .chain(conn_id_bytes.iter().copied()) + std::iter::once(ClientRequestTag::GetLaneQueueLength as u8) + .chain(conn_id_bytes.into_iter()) .collect() } @@ -270,12 +368,12 @@ impl ClientRequest { if b.len() != 1 + size_of::() { return Err(error::Error::new( ErrorKind::MalformedRequest, - "the received get lane queue length has invalid length".to_string(), + "The received get lane queue lengths has invalid length", )); } // this MUST match because it was called by 'deserialize' - debug_assert_eq!(b[0], GET_LANE_QUEUE_LENGHT_TAG); + debug_assert_eq!(b[0], ClientRequestTag::GetLaneQueueLength as u8); let mut connection_id_bytes = [0u8; size_of::()]; connection_id_bytes.copy_from_slice(&b[1..=size_of::()]); @@ -289,14 +387,21 @@ impl ClientRequest { ClientRequest::Send { recipient, message, - with_reply_surb, connection_id, - } => Self::serialize_send(recipient, message, with_reply_surb, connection_id), + } => Self::serialize_send(recipient, message, connection_id), + + ClientRequest::SendAnonymous { + recipient, + message, + reply_surbs, + connection_id, + } => Self::serialize_send_anonymous(recipient, message, reply_surbs, connection_id), ClientRequest::Reply { message, - reply_surb, - } => Self::serialize_reply(message, &reply_surb), + sender_tag, + connection_id, + } => Self::serialize_reply(message, sender_tag, connection_id), ClientRequest::SelfAddress => Self::serialize_self_address(), @@ -316,28 +421,16 @@ impl ClientRequest { )); } - if b.len() < size_of::() { - return Err(error::Error::new( - ErrorKind::TooShortRequest, - format!( - "not enough data provided to recover request tag. Provided only {} bytes", - b.len() - ), - )); - } - let request_tag = b[0]; + let request_tag = ClientRequestTag::try_from(b[0])?; // determine what kind of request that is and try to deserialize it match request_tag { - SEND_REQUEST_TAG => Self::deserialize_send(b), - REPLY_REQUEST_TAG => Self::deserialize_reply(b), - SELF_ADDRESS_REQUEST_TAG => Ok(Self::deserialize_self_address(b)), - CLOSED_CONNECTION_REQUEST_TAG => Self::deserialize_closed_connection(b), - GET_LANE_QUEUE_LENGHT_TAG => Self::deserialize_get_lane_queue_length(b), - n => Err(error::Error::new( - ErrorKind::UnknownRequest, - format!("type {n}"), - )), + ClientRequestTag::Send => Self::deserialize_send(b), + ClientRequestTag::SendAnonymous => Self::deserialize_send_anonymous(b), + ClientRequestTag::Reply => Self::deserialize_reply(b), + ClientRequestTag::SelfAddress => Self::deserialize_self_address(b), + ClientRequestTag::ClosedConnection => Self::deserialize_closed_connection(b), + ClientRequestTag::GetLaneQueueLength => Self::deserialize_get_lane_queue_length(b), } } @@ -365,50 +458,52 @@ mod tests { let recipient = Recipient::try_from_base58_string("CytBseW6yFXUMzz4SGAKdNLGR7q3sJLLYxyBGvutNEQV.4QXYyEVc5fUDjmmi8PrHN9tdUFV4PCvSJE1278cHyvoe@4sBbL1ngf1vtNqykydQKTFh26sQCw888GpUqvPvyNB4f").unwrap(); let recipient_string = recipient.to_string(); - let send_request_no_surb = ClientRequest::Send { + let send_request = ClientRequest::Send { recipient, message: b"foomp".to_vec(), - with_reply_surb: false, connection_id: Some(42), }; - let bytes = send_request_no_surb.serialize(); + let bytes = send_request.serialize(); let recovered = ClientRequest::deserialize(&bytes).unwrap(); match recovered { ClientRequest::Send { recipient, message, - with_reply_surb, connection_id, } => { assert_eq!(recipient.to_string(), recipient_string); assert_eq!(message, b"foomp".to_vec()); - assert!(!with_reply_surb); assert_eq!(connection_id, Some(42)) } _ => unreachable!(), } + } - let send_request_surb = ClientRequest::Send { - recipient, + #[test] + fn send_anonymous_request_serialization_works() { + let original_recipient = Recipient::try_from_base58_string("CytBseW6yFXUMzz4SGAKdNLGR7q3sJLLYxyBGvutNEQV.4QXYyEVc5fUDjmmi8PrHN9tdUFV4PCvSJE1278cHyvoe@4sBbL1ngf1vtNqykydQKTFh26sQCw888GpUqvPvyNB4f").unwrap(); + + let send_anonymous_request = ClientRequest::SendAnonymous { + recipient: original_recipient, message: b"foomp".to_vec(), - with_reply_surb: true, - connection_id: None, + reply_surbs: 666, + connection_id: Some(42), }; - let bytes = send_request_surb.serialize(); + let bytes = send_anonymous_request.serialize(); let recovered = ClientRequest::deserialize(&bytes).unwrap(); match recovered { - ClientRequest::Send { + ClientRequest::SendAnonymous { recipient, message, - with_reply_surb, + reply_surbs, connection_id, } => { - assert_eq!(recipient.to_string(), recipient_string); + assert_eq!(recipient, original_recipient); assert_eq!(message, b"foomp".to_vec()); - assert!(with_reply_surb); - assert_eq!(connection_id, None) + assert_eq!(connection_id, Some(42)); + assert_eq!(reply_surbs, 666) } _ => unreachable!(), } @@ -416,22 +511,23 @@ mod tests { #[test] fn reply_request_serialization_works() { - let reply_surb_string = "CjfVbHbfAjbC3W1BvNHGXmM8KNAnDNYGaHMLqVDxRYeo352csAihstup9bvqXam4dTWgfHak6KYwL9STaxWJ47E8XFZbSEvs7hEsfCkxr6K9WJuSBPK84GDDEvad8ZAuMCoaXsAd5S2Lj9a5eYyzG4SL1jHzhSMni55LyJwumxo1ZTGZNXggxw1RREosvyzNrW9Rsi3owyPqLCwXpiei2tHZty8w8midVvg8vDa7ZEJD842CLv8D4ohynSG7gDpqTrhkRaqYAuz7dzqNbMXLJRM7v823Jn16fA1L7YQxmcaUdUigyRSgTdb4i9ebiLGSyJ1iDe6Acz613PQZh6Ua3bZ2zVKq3dSycpDm9ngarRK4zJrAaUxRkdih8YzW3BY4nL9eqkfKA4N1TWCLaRU7zpSaf8yMEwrAZReU3d5zLV8c5KBfa2w8R5anhQeBojduZEGEad8kkHuKU52Zg93FeWHvH1qgZaEJMHH4nN7gKXz9mvWDhYwyF4vt3Uy2NhCHC3N5pL1gMme27YcoPcTEia1fxKZtnt6rtEozzTrAgCJGswigkFbkafiV5QaJwLKTUxtzhkZ57eEuLPte9UvJHzhhXUQ2CV7R2BUkJjYZy3Zsx6YYvdYWiAFFkWUwNEGA4QpShUHciBfsQVHQ7pN41YcyYUhbywQDFnTVgEmdUZ1XCBi3gyK5U3tDQmFzP1u9m3mWrUA8qB9mRDE7ptNDm5c3c1458L6uXLUth7sdMaa1Was5LCmCdmNDtvNpCDAEt1in6q6mrZFR85aCSU9b1baNGwZoCqPpPvydkVe63gXWoi8ebvdyxARrqACFrSB3ZdY3uJBw8CTMNkKK6MvcefMkSVVsbLd36TQAtYSCqrpiMc5dQuKcEu5QfciwvWYXYx8WFNAgKwP2mv49KCTvfozNDUCbjzDwSx92Zv5zjG8HbFpB13bY9UZGeyTPvv7gGxCzjGjJGbW6FRAheRQaaje5fUgCNM95Tv7wBmAMRHHFgWafeK1sdFH7dtCX9u898HucGTaboSKLsVh8J78gbbkHErwjMh7y9YRkceq5TTYS5da4kHnyNKYWSbxgZrmFg44XGKoeYcqoHB3XTZrdsf7F5fFeNwnihkmADvhAcaxXUmVqq4rQFZH84a1iC3WBWXYcqiZH2L7ujGWV7mMDT4HBEerDYjc8rNY4xGTPfivCrBCJW1i14aqW8xRdsdgTM88eTksvC3WPJLJ7iMzfKXeL7fMW1Ek6QGyQtLBW98vEESpdcDg6DeZ5rMz6VqjTGGqcCaFGfHoqtfxMDaBAEsyQ8h7XDX6dg1wq9wH6j4Tw7Tj1MEv1b8uj5NJkozZdzVdYA2QyE2Dp8vuurQG6uVdTDNww2d88RBQ8sVgjxN8gR45y4woJLhFAaNTAtrY6wDTxyXST13ni6oyqdYxjFVk9Am4v3DzH7Y2K8iRVSHfTk4FRbPULyaeK6wt2anvMJH1XdvVRgc14h67MnBxMgMD1UFk8AErN7CDj26fppe3c5G6KozJe4cSqQUGbBjVzBnrHCruqrfZBn5hNZHTV37bQiomqhRQXohxhuKEnNrGbAe1xNvJr9X"; - let reply_surb = ReplySurb::from_base58_string(reply_surb_string).unwrap(); let reply_request = ClientRequest::Reply { + sender_tag: [8u8; SENDER_TAG_SIZE].into(), message: b"foomp".to_vec(), - reply_surb, + connection_id: Some(42), }; let bytes = reply_request.serialize(); let recovered = ClientRequest::deserialize(&bytes).unwrap(); match recovered { ClientRequest::Reply { - reply_surb, + sender_tag, message, + connection_id, } => { - assert_eq!(reply_surb.to_base58_string(), reply_surb_string); + assert_eq!(sender_tag, [8u8; SENDER_TAG_SIZE].into()); assert_eq!(message, b"foomp".to_vec()); + assert_eq!(connection_id, Some(42)); } _ => unreachable!(), } @@ -458,4 +554,15 @@ mod tests { _ => unreachable!(), } } + + #[test] + fn get_lane_queue_length_request_serialization_works() { + let close_connection_request = ClientRequest::GetLaneQueueLength(42); + let bytes = close_connection_request.serialize(); + let recovered = ClientRequest::deserialize(&bytes).unwrap(); + match recovered { + ClientRequest::GetLaneQueueLength(id) => assert_eq!(id, 42), + _ => unreachable!(), + } + } } diff --git a/clients/native/websocket-requests/src/responses.rs b/clients/native/websocket-requests/src/responses.rs index 7a32645957..cbefe54242 100644 --- a/clients/native/websocket-requests/src/responses.rs +++ b/clients/native/websocket-requests/src/responses.rs @@ -1,36 +1,54 @@ -// Copyright 2021 - Nym Technologies SA +// Copyright 2021-2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 // all variable size data is always prefixed with u64 length // tags are u8 -#![allow(unknown_lints)] // due to using `clippy::branches_sharing_code` which does not exist on `stable` just yet - use crate::error::{self, ErrorKind}; use crate::text::ServerResponseText; use nymsphinx::addressing::clients::Recipient; -use nymsphinx::anonymous_replies::ReplySurb; +use nymsphinx::anonymous_replies::requests::{AnonymousSenderTag, SENDER_TAG_SIZE}; use nymsphinx::receiver::ReconstructedMessage; use std::convert::TryInto; use std::mem::size_of; -/// Value tag representing [`Error`] variant of the [`ServerResponse`] -pub const ERROR_RESPONSE_TAG: u8 = 0x00; +#[repr(u8)] +enum ServerResponseTag { + /// Value tag representing [`Error`] variant of the [`ServerResponse`] + Error = 0x00, -/// Value tag representing [`Received`] variant of the [`ServerResponse`] -pub const RECEIVED_RESPONSE_TAG: u8 = 0x01; + /// Value tag representing [`Received`] variant of the [`ServerResponse`] + Received = 0x01, -/// Value tag representing [`SelfAddress`] variant of the [`ServerResponse`] -pub const SELF_ADDRESS_RESPONSE_TAG: u8 = 0x02; + /// Value tag representing [`SelfAddress`] variant of the [`ServerResponse`] + SelfAddress = 0x02, -/// Value tag representing [`LaneQueueLength`] variant of the [`ServerResponse`] -pub const LANE_QUEUE_LENGTH_RESPONSE_TAG: u8 = 0x03; + /// Value tag representing [`LaneQueueLength`] variant of the [`ServerResponse`] + LaneQueueLength = 0x03, +} + +impl TryFrom for ServerResponseTag { + type Error = error::Error; + + fn try_from(value: u8) -> Result { + match value { + _ if value == (Self::Error as u8) => Ok(Self::Error), + _ if value == (Self::Received as u8) => Ok(Self::Received), + _ if value == (Self::SelfAddress as u8) => Ok(Self::SelfAddress), + _ if value == (Self::LaneQueueLength as u8) => Ok(Self::LaneQueueLength), + n => Err(error::Error::new( + ErrorKind::UnknownResponse, + format!("{n} does not correspond to any valid response tag"), + )), + } + } +} #[derive(Debug)] pub enum ServerResponse { Received(ReconstructedMessage), - SelfAddress(Recipient), - LaneQueueLength(u64, usize), + SelfAddress(Box), + LaneQueueLength { lane: u64, queue_length: usize }, Error(error::Error), } @@ -42,24 +60,19 @@ impl ServerResponse { }) } - // RECEIVED_RESPONSE_TAG || with_reply || (surb_len || surb) || msg_len || msg + // RECEIVED_RESPONSE_TAG || 1 | 0 indicating sender_tag || Option || msg_len || msg fn serialize_received(reconstructed_message: ReconstructedMessage) -> Vec { let message_len_bytes = (reconstructed_message.message.len() as u64).to_be_bytes(); - if let Some(reply_surb) = reconstructed_message.reply_surb { - let reply_surb_bytes = reply_surb.to_bytes(); - let surb_len_bytes = (reply_surb_bytes.len() as u64).to_be_bytes(); - // with_reply || surb_len || surb || msg_len || msg - std::iter::once(RECEIVED_RESPONSE_TAG) + if let Some(sender_tag) = reconstructed_message.sender_tag { + std::iter::once(ServerResponseTag::Received as u8) .chain(std::iter::once(true as u8)) - .chain(surb_len_bytes.iter().cloned()) - .chain(reply_surb_bytes.iter().cloned()) + .chain(sender_tag.to_bytes().into_iter()) .chain(message_len_bytes.iter().cloned()) .chain(reconstructed_message.message.into_iter()) .collect() } else { - // without_reply || msg_len || msg - std::iter::once(RECEIVED_RESPONSE_TAG) + std::iter::once(ServerResponseTag::Received as u8) .chain(std::iter::once(false as u8)) .chain(message_len_bytes.iter().cloned()) .chain(reconstructed_message.message.into_iter()) @@ -67,10 +80,9 @@ impl ServerResponse { } } - // RECEIVED_RESPONSE_TAG || with_reply || (surb_len || surb) || msg_len || msg + // RECEIVED_RESPONSE_TAG || 1 | 0 indicating sender_tag || Option || msg_len || msg fn deserialize_received(b: &[u8]) -> Result { // this MUST match because it was called by 'deserialize' - debug_assert_eq!(b[0], RECEIVED_RESPONSE_TAG); // we must be able to read at the very least if it has a reply_surb and length of some field if b.len() < 2 + size_of::() { @@ -79,101 +91,70 @@ impl ServerResponse { "not enough data provided to recover 'received'".to_string(), )); } + debug_assert_eq!(b[0], ServerResponseTag::Received as u8); - let with_reply_surb = match b[1] { + let has_sender_tag = match b[1] { 0 => false, 1 => true, n => { return Err(error::Error::new( ErrorKind::MalformedResponse, - format!("invalid reply flag {}", n), + format!("invalid sender tag flag {n}"), )) } }; - // this is a false positive as even though the code is the same, it refers to different things - #[allow(clippy::branches_sharing_code)] - if with_reply_surb { - let reply_surb_len = - u64::from_be_bytes(b[2..2 + size_of::()].as_ref().try_into().unwrap()); - - // make sure we won't go out of bounds here - if reply_surb_len > (b.len() - 2 + 2 * size_of::()) as u64 { + let mut i = 2; + let sender_tag = if has_sender_tag { + if b[2..].len() < SENDER_TAG_SIZE { return Err(error::Error::new( - ErrorKind::MalformedResponse, - "not enough bytes to read reply_surb bytes!".to_string(), + ErrorKind::TooShortResponse, + "not enough data provided to recover 'received'".to_string(), )); } - - let surb_bound = 2 + size_of::() + reply_surb_len as usize; - - let reply_surb_bytes = &b[2 + size_of::()..surb_bound]; - let reply_surb = match ReplySurb::from_bytes(reply_surb_bytes) { - Ok(reply_surb) => reply_surb, - Err(err) => { - return Err(error::Error::new( - ErrorKind::MalformedResponse, - format!("malformed reply SURB: {:?}", err), - )) - } - }; - - let message_len = u64::from_be_bytes( - b[surb_bound..surb_bound + size_of::()] - .as_ref() - .try_into() - .unwrap(), - ); - let message = &b[surb_bound + size_of::()..]; - if message.len() as u64 != message_len { - return Err(error::Error::new( - ErrorKind::MalformedResponse, - format!( - "message len has inconsistent length. specified: {} got: {}", - message_len, - message.len() - ), - )); - } - - Ok(ServerResponse::Received(ReconstructedMessage { - message: message.to_vec(), - reply_surb: Some(reply_surb), - })) + i += SENDER_TAG_SIZE; + Some(AnonymousSenderTag::from_bytes( + b[2..2 + SENDER_TAG_SIZE].try_into().unwrap(), + )) } else { - let message_len = - u64::from_be_bytes(b[2..2 + size_of::()].as_ref().try_into().unwrap()); - let message = &b[2 + size_of::()..]; - if message.len() as u64 != message_len { - return Err(error::Error::new( - ErrorKind::MalformedResponse, - format!( - "message len has inconsistent length. specified: {} got: {}", - message_len, - message.len() - ), - )); - } + None + }; - Ok(ServerResponse::Received(ReconstructedMessage { - message: message.to_vec(), - reply_surb: None, - })) + if b[i..].len() < size_of::() { + return Err(error::Error::new( + ErrorKind::TooShortResponse, + "not enough data provided to recover 'received'".to_string(), + )); } + + let message_len = u64::from_be_bytes(b[i..i + size_of::()].try_into().unwrap()); + let message = &b[i + size_of::()..]; + if message.len() as u64 != message_len { + return Err(error::Error::new( + ErrorKind::MalformedResponse, + format!( + "message len has inconsistent length. specified: {} got: {}", + message_len, + message.len() + ), + )); + } + + Ok(ServerResponse::Received(ReconstructedMessage { + message: message.to_vec(), + sender_tag, + })) } // SELF_ADDRESS_RESPONSE_TAG || self_address fn serialize_self_address(address: Recipient) -> Vec { - std::iter::once(SELF_ADDRESS_RESPONSE_TAG) - .chain(address.to_bytes().iter().cloned()) + std::iter::once(ServerResponseTag::SelfAddress as u8) + .chain(address.to_bytes().into_iter()) .collect() } // SELF_ADDRESS_RESPONSE_TAG || self_address fn deserialize_self_address(b: &[u8]) -> Result { - // this MUST match because it was called by 'deserialize' - debug_assert_eq!(b[0], SELF_ADDRESS_RESPONSE_TAG); - if b.len() != 1 + Recipient::LEN { return Err(error::Error::new( ErrorKind::TooShortResponse, @@ -181,6 +162,9 @@ impl ServerResponse { )); } + // this MUST match because it was called by 'deserialize' + debug_assert_eq!(b[0], ServerResponseTag::SelfAddress as u8); + let mut recipient_bytes = [0u8; Recipient::LEN]; recipient_bytes.copy_from_slice(&b[1..1 + Recipient::LEN]); @@ -194,12 +178,12 @@ impl ServerResponse { } }; - Ok(ServerResponse::SelfAddress(recipient)) + Ok(ServerResponse::SelfAddress(Box::new(recipient))) } // LANE_QUEUE_LENGTH_RESPONSE_TAG || lane || queue_length fn serialize_lane_queue_length(lane: u64, queue_length: usize) -> Vec { - std::iter::once(LANE_QUEUE_LENGTH_RESPONSE_TAG) + std::iter::once(ServerResponseTag::LaneQueueLength as u8) .chain(lane.to_be_bytes().iter().cloned()) .chain(queue_length.to_be_bytes().iter().cloned()) .collect() @@ -208,7 +192,7 @@ impl ServerResponse { // LANE_QUEUE_LENGTH_RESPONSE_TAG || lane || queue_length fn deserialize_lane_queue_length(b: &[u8]) -> Result { // this MUST match because it was called by 'deserialize' - debug_assert_eq!(b[0], LANE_QUEUE_LENGTH_RESPONSE_TAG); + debug_assert_eq!(b[0], ServerResponseTag::LaneQueueLength as u8); let mut lane_bytes = [0u8; size_of::()]; lane_bytes.copy_from_slice(&b[1..=size_of::()]); @@ -219,15 +203,15 @@ impl ServerResponse { .copy_from_slice(&b[1 + size_of::()..1 + size_of::() + size_of::()]); let queue_length = usize::from_be_bytes(queue_length_bytes); - Ok(ServerResponse::LaneQueueLength(lane, queue_length)) + Ok(ServerResponse::LaneQueueLength { lane, queue_length }) } // ERROR_RESPONSE_TAG || err_code || msg_len || msg fn serialize_error(error: error::Error) -> Vec { let message_len_bytes = (error.message.len() as u64).to_be_bytes(); - std::iter::once(ERROR_RESPONSE_TAG) + std::iter::once(ServerResponseTag::Error as u8) .chain(std::iter::once(error.kind as u8)) - .chain(message_len_bytes.iter().cloned()) + .chain(message_len_bytes.into_iter()) .chain(error.message.into_bytes().into_iter()) .collect() } @@ -235,7 +219,7 @@ impl ServerResponse { // ERROR_RESPONSE_TAG || err_code || msg_len || msg fn deserialize_error(b: &[u8]) -> Result { // this MUST match because it was called by 'deserialize' - debug_assert_eq!(b[0], ERROR_RESPONSE_TAG); + debug_assert_eq!(b[0], ServerResponseTag::Error as u8); if b.len() < size_of::() + size_of::() { return Err(error::Error::new( @@ -244,26 +228,7 @@ impl ServerResponse { )); } - let error_kind = match b[1] { - _ if b[1] == (ErrorKind::EmptyRequest as u8) => ErrorKind::EmptyRequest, - _ if b[1] == (ErrorKind::TooShortRequest as u8) => ErrorKind::TooShortRequest, - _ if b[1] == (ErrorKind::UnknownRequest as u8) => ErrorKind::UnknownRequest, - _ if b[1] == (ErrorKind::MalformedRequest as u8) => ErrorKind::MalformedRequest, - - _ if b[1] == (ErrorKind::EmptyResponse as u8) => ErrorKind::EmptyResponse, - _ if b[1] == (ErrorKind::TooShortResponse as u8) => ErrorKind::TooShortResponse, - _ if b[1] == (ErrorKind::UnknownResponse as u8) => ErrorKind::UnknownResponse, - _ if b[1] == (ErrorKind::MalformedResponse as u8) => ErrorKind::MalformedResponse, - - _ if b[1] == (ErrorKind::Other as u8) => ErrorKind::Other, - - n => { - return Err(error::Error::new( - ErrorKind::MalformedResponse, - format!("invalid error code {}", n), - )) - } - }; + let error_kind = ErrorKind::try_from(b[1])?; let message_len = u64::from_be_bytes(b[2..2 + size_of::()].as_ref().try_into().unwrap()); @@ -300,8 +265,8 @@ impl ServerResponse { ServerResponse::Received(reconstructed_message) => { Self::serialize_received(reconstructed_message) } - ServerResponse::SelfAddress(address) => Self::serialize_self_address(address), - ServerResponse::LaneQueueLength(lane, queue_length) => { + ServerResponse::SelfAddress(address) => Self::serialize_self_address(*address), + ServerResponse::LaneQueueLength { lane, queue_length } => { Self::serialize_lane_queue_length(lane, queue_length) } ServerResponse::Error(err) => Self::serialize_error(err), @@ -328,18 +293,14 @@ impl ServerResponse { )); } - let response_tag = b[0]; + let response_tag = ServerResponseTag::try_from(b[0])?; // determine what kind of response that is and try to deserialize it match response_tag { - RECEIVED_RESPONSE_TAG => Self::deserialize_received(b), - SELF_ADDRESS_RESPONSE_TAG => Self::deserialize_self_address(b), - LANE_QUEUE_LENGTH_RESPONSE_TAG => Self::deserialize_lane_queue_length(b), - ERROR_RESPONSE_TAG => Self::deserialize_error(b), - n => Err(error::Error::new( - ErrorKind::UnknownResponse, - format!("type {}", n), - )), + ServerResponseTag::Received => Self::deserialize_received(b), + ServerResponseTag::SelfAddress => Self::deserialize_self_address(b), + ServerResponseTag::LaneQueueLength => Self::deserialize_lane_queue_length(b), + ServerResponseTag::Error => Self::deserialize_error(b), } } @@ -361,35 +322,33 @@ mod tests { #[test] fn received_response_serialization_works() { - let reply_surb_string = "CjfVbHbfAjbC3W1BvNHGXmM8KNAnDNYGaHMLqVDxRYeo352csAihstup9bvqXam4dTWgfHak6KYwL9STaxWJ47E8XFZbSEvs7hEsfCkxr6K9WJuSBPK84GDDEvad8ZAuMCoaXsAd5S2Lj9a5eYyzG4SL1jHzhSMni55LyJwumxo1ZTGZNXggxw1RREosvyzNrW9Rsi3owyPqLCwXpiei2tHZty8w8midVvg8vDa7ZEJD842CLv8D4ohynSG7gDpqTrhkRaqYAuz7dzqNbMXLJRM7v823Jn16fA1L7YQxmcaUdUigyRSgTdb4i9ebiLGSyJ1iDe6Acz613PQZh6Ua3bZ2zVKq3dSycpDm9ngarRK4zJrAaUxRkdih8YzW3BY4nL9eqkfKA4N1TWCLaRU7zpSaf8yMEwrAZReU3d5zLV8c5KBfa2w8R5anhQeBojduZEGEad8kkHuKU52Zg93FeWHvH1qgZaEJMHH4nN7gKXz9mvWDhYwyF4vt3Uy2NhCHC3N5pL1gMme27YcoPcTEia1fxKZtnt6rtEozzTrAgCJGswigkFbkafiV5QaJwLKTUxtzhkZ57eEuLPte9UvJHzhhXUQ2CV7R2BUkJjYZy3Zsx6YYvdYWiAFFkWUwNEGA4QpShUHciBfsQVHQ7pN41YcyYUhbywQDFnTVgEmdUZ1XCBi3gyK5U3tDQmFzP1u9m3mWrUA8qB9mRDE7ptNDm5c3c1458L6uXLUth7sdMaa1Was5LCmCdmNDtvNpCDAEt1in6q6mrZFR85aCSU9b1baNGwZoCqPpPvydkVe63gXWoi8ebvdyxARrqACFrSB3ZdY3uJBw8CTMNkKK6MvcefMkSVVsbLd36TQAtYSCqrpiMc5dQuKcEu5QfciwvWYXYx8WFNAgKwP2mv49KCTvfozNDUCbjzDwSx92Zv5zjG8HbFpB13bY9UZGeyTPvv7gGxCzjGjJGbW6FRAheRQaaje5fUgCNM95Tv7wBmAMRHHFgWafeK1sdFH7dtCX9u898HucGTaboSKLsVh8J78gbbkHErwjMh7y9YRkceq5TTYS5da4kHnyNKYWSbxgZrmFg44XGKoeYcqoHB3XTZrdsf7F5fFeNwnihkmADvhAcaxXUmVqq4rQFZH84a1iC3WBWXYcqiZH2L7ujGWV7mMDT4HBEerDYjc8rNY4xGTPfivCrBCJW1i14aqW8xRdsdgTM88eTksvC3WPJLJ7iMzfKXeL7fMW1Ek6QGyQtLBW98vEESpdcDg6DeZ5rMz6VqjTGGqcCaFGfHoqtfxMDaBAEsyQ8h7XDX6dg1wq9wH6j4Tw7Tj1MEv1b8uj5NJkozZdzVdYA2QyE2Dp8vuurQG6uVdTDNww2d88RBQ8sVgjxN8gR45y4woJLhFAaNTAtrY6wDTxyXST13ni6oyqdYxjFVk9Am4v3DzH7Y2K8iRVSHfTk4FRbPULyaeK6wt2anvMJH1XdvVRgc14h67MnBxMgMD1UFk8AErN7CDj26fppe3c5G6KozJe4cSqQUGbBjVzBnrHCruqrfZBn5hNZHTV37bQiomqhRQXohxhuKEnNrGbAe1xNvJr9X"; - - let received_with_surb = ServerResponse::Received(ReconstructedMessage { + let received_with_sender_tag = ServerResponse::Received(ReconstructedMessage { message: b"foomp".to_vec(), - reply_surb: Some(ReplySurb::from_base58_string(reply_surb_string).unwrap()), + sender_tag: Some([42u8; SENDER_TAG_SIZE].into()), }); - let bytes = received_with_surb.serialize(); + let bytes = received_with_sender_tag.serialize(); let recovered = ServerResponse::deserialize(&bytes).unwrap(); match recovered { ServerResponse::Received(reconstructed) => { assert_eq!(reconstructed.message, b"foomp".to_vec()); assert_eq!( - reconstructed.reply_surb.unwrap().to_base58_string(), - reply_surb_string + reconstructed.sender_tag, + Some([42u8; SENDER_TAG_SIZE].into()) ) } _ => unreachable!(), } - let received_without_surb = ServerResponse::Received(ReconstructedMessage { + let received_without_sender_tag = ServerResponse::Received(ReconstructedMessage { message: b"foomp".to_vec(), - reply_surb: None, + sender_tag: None, }); - let bytes = received_without_surb.serialize(); + let bytes = received_without_sender_tag.serialize(); let recovered = ServerResponse::deserialize(&bytes).unwrap(); match recovered { ServerResponse::Received(reconstructed) => { assert_eq!(reconstructed.message, b"foomp".to_vec()); - assert!(reconstructed.reply_surb.is_none()) + assert!(reconstructed.sender_tag.is_none()) } _ => unreachable!(), } @@ -400,7 +359,7 @@ mod tests { let recipient = Recipient::try_from_base58_string("CytBseW6yFXUMzz4SGAKdNLGR7q3sJLLYxyBGvutNEQV.4QXYyEVc5fUDjmmi8PrHN9tdUFV4PCvSJE1278cHyvoe@4sBbL1ngf1vtNqykydQKTFh26sQCw888GpUqvPvyNB4f").unwrap(); let recipient_string = recipient.to_string(); - let self_address_response = ServerResponse::SelfAddress(recipient); + let self_address_response = ServerResponse::SelfAddress(Box::new(recipient)); let bytes = self_address_response.serialize(); let recovered = ServerResponse::deserialize(&bytes).unwrap(); match recovered { @@ -413,11 +372,14 @@ mod tests { #[test] fn lane_queue_length_response_serialization_works() { - let lane_queue_length_response = ServerResponse::LaneQueueLength(13, 42); + let lane_queue_length_response = ServerResponse::LaneQueueLength { + lane: 13, + queue_length: 42, + }; let bytes = lane_queue_length_response.serialize(); let recovered = ServerResponse::deserialize(&bytes).unwrap(); match recovered { - ServerResponse::LaneQueueLength(lane, queue_length) => { + ServerResponse::LaneQueueLength { lane, queue_length } => { assert_eq!(lane, 13); assert_eq!(queue_length, 42) } diff --git a/clients/native/websocket-requests/src/text.rs b/clients/native/websocket-requests/src/text.rs index 1083ecd9a8..68091dbecf 100644 --- a/clients/native/websocket-requests/src/text.rs +++ b/clients/native/websocket-requests/src/text.rs @@ -1,11 +1,11 @@ -// Copyright 2021 - Nym Technologies SA +// Copyright 2021-2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 use crate::error::ErrorKind; use crate::requests::ClientRequest; use crate::responses::ServerResponse; use nymsphinx::addressing::clients::Recipient; -use nymsphinx::anonymous_replies::ReplySurb; +use nymsphinx::anonymous_replies::requests::AnonymousSenderTag; use serde::{Deserialize, Serialize}; use std::convert::{TryFrom, TryInto}; @@ -19,15 +19,22 @@ pub(super) enum ClientRequestText { Send { message: String, recipient: String, - with_reply_surb: bool, + connection_id: Option, + }, + #[serde(rename_all = "camelCase")] + SendAnonymous { + recipient: String, + message: String, + reply_surbs: u32, + connection_id: Option, + }, + #[serde(rename_all = "camelCase")] + Reply { + sender_tag: String, + message: String, connection_id: Option, }, SelfAddress, - #[serde(rename_all = "camelCase")] - Reply { - message: String, - reply_surb: String, - }, } impl TryFrom for ClientRequestText { @@ -46,7 +53,6 @@ impl TryInto for ClientRequestText { ClientRequestText::Send { message, recipient, - with_reply_surb, connection_id, } => { let message_bytes = message.into_bytes(); @@ -57,23 +63,42 @@ impl TryInto for ClientRequestText { Ok(ClientRequest::Send { message: message_bytes, recipient, - with_reply_surb, + connection_id, + }) + } + ClientRequestText::SendAnonymous { + recipient, + message, + reply_surbs, + connection_id, + } => { + let message_bytes = message.into_bytes(); + let recipient = Recipient::try_from_base58_string(recipient).map_err(|err| { + Self::Error::new(ErrorKind::MalformedRequest, err.to_string()) + })?; + Ok(ClientRequest::SendAnonymous { + recipient, + message: message_bytes, + reply_surbs, connection_id, }) } ClientRequestText::SelfAddress => Ok(ClientRequest::SelfAddress), ClientRequestText::Reply { + sender_tag, message, - reply_surb, + connection_id, } => { let message_bytes = message.into_bytes(); - let reply_surb = ReplySurb::from_base58_string(reply_surb).map_err(|err| { - Self::Error::new(ErrorKind::MalformedRequest, err.to_string()) - })?; + let sender_tag = + AnonymousSenderTag::try_from_base58_string(sender_tag).map_err(|err| { + Self::Error::new(ErrorKind::MalformedRequest, err.to_string()) + })?; Ok(ClientRequest::Reply { + sender_tag, message: message_bytes, - reply_surb, + connection_id, }) } } @@ -89,7 +114,7 @@ pub(super) enum ServerResponseText { #[serde(rename_all = "camelCase")] Received { message: String, - reply_surb: Option, + sender_tag: Option, }, SelfAddress { address: String, @@ -131,15 +156,13 @@ impl From for ServerResponseText { // TODO: ask DH what is more appropriate, lossy utf8 conversion or returning error and then // pure binary later message: String::from_utf8_lossy(&reconstructed.message).into_owned(), - reply_surb: reconstructed - .reply_surb - .map(|reply_surb| reply_surb.to_base58_string()), + sender_tag: reconstructed.sender_tag.map(|tag| tag.to_base58_string()), } } ServerResponse::SelfAddress(recipient) => ServerResponseText::SelfAddress { address: recipient.to_string(), }, - ServerResponse::LaneQueueLength(lane, queue_length) => { + ServerResponse::LaneQueueLength { lane, queue_length } => { ServerResponseText::LaneQueueLength { lane, queue_length } } ServerResponse::Error(err) => ServerResponseText::Error { diff --git a/clients/socks5/Cargo.toml b/clients/socks5/Cargo.toml index b5eb3a1a9d..4a4e8edaff 100644 --- a/clients/socks5/Cargo.toml +++ b/clients/socks5/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nym-socks5-client" -version = "1.1.1" +version = "1.1.2" authors = ["Dave Hrycyszyn "] description = "A SOCKS5 localhost proxy that converts incoming messages to Sphinx and sends them to a Nym address" edition = "2021" @@ -19,13 +19,14 @@ pin-project = "1.0" pretty_env_logger = "0.4" rand = { version = "0.7.3", features = ["wasm-bindgen"] } serde = { version = "1.0", features = ["derive"] } # for config serialization/deserialization -snafu = "0.6" +serde_json = "1.0.89" +tap = "1.0.1" thiserror = "1.0.34" tokio = { version = "1.21.2", features = ["rt-multi-thread", "net", "signal"] } url = "2.2" # internal -client-core = { path = "../client-core" } +client-core = { path = "../client-core", features = ["fs-surb-storage"] } client-connections = { path = "../../common/client-connections" } coconut-interface = { path = "../../common/coconut-interface", optional = true } config = { path = "../../common/config" } @@ -46,7 +47,6 @@ task = { path = "../../common/task" } topology = { path = "../../common/topology" } validator-client = { path = "../../common/client-libs/validator-client", features = ["nymd-client"] } version-checker = { path = "../../common/version-checker" } -tap = "1.0.1" [features] coconut = ["coconut-interface", "credentials", "gateway-requests/coconut", "gateway-client/coconut", "credentials/coconut", "client-core/coconut"] diff --git a/clients/socks5/src/client/config/mod.rs b/clients/socks5/src/client/config/mod.rs index 4c43984f35..97372e4c86 100644 --- a/clients/socks5/src/client/config/mod.rs +++ b/clients/socks5/src/client/config/mod.rs @@ -2,8 +2,8 @@ // SPDX-License-Identifier: Apache-2.0 use crate::client::config::template::config_template; -use client_core::config::Config as BaseConfig; pub use client_core::config::MISSING_VALUE; +use client_core::config::{ClientCoreConfigTrait, Config as BaseConfig, DebugConfig}; use config::defaults::DEFAULT_SOCKS5_LISTENING_PORT; use config::NymConfig; use nymsphinx::addressing::clients::Recipient; @@ -12,6 +12,9 @@ use std::path::PathBuf; mod template; +const DEFAULT_CONNECTION_START_SURBS: u32 = 20; +const DEFAULT_PER_REQUEST_SURBS: u32 = 3; + #[derive(Debug, Default, Deserialize, PartialEq, Serialize)] #[serde(deny_unknown_fields)] pub struct Config { @@ -19,6 +22,9 @@ pub struct Config { base: BaseConfig, socks5: Socks5, + + #[serde(default)] + socks5_debug: Socks5Debug, } impl NymConfig for Config { @@ -52,25 +58,51 @@ impl NymConfig for Config { } } +impl ClientCoreConfigTrait for Config { + fn get_gateway_endpoint(&self) -> &client_core::config::GatewayEndpointConfig { + self.base.get_gateway_endpoint() + } +} + impl Config { pub fn new>(id: S, provider_mix_address: S) -> Self { Config { base: BaseConfig::new(id), socks5: Socks5::new(provider_mix_address), + socks5_debug: Socks5Debug::default(), } } + #[must_use] pub fn with_port(mut self, port: u16) -> Self { self.socks5.listening_port = port; self } + #[must_use] pub fn with_provider_mix_address(mut self, address: String) -> Self { self.socks5.provider_mix_address = address; self } + pub fn with_anonymous_replies(mut self, anonymous_replies: bool) -> Self { + self.socks5.send_anonymously = anonymous_replies; + self + } + // getters + pub fn get_base(&self) -> &BaseConfig { + &self.base + } + + pub fn get_base_mut(&mut self) -> &mut BaseConfig { + &mut self.base + } + + pub fn get_debug_settings(&self) -> &DebugConfig { + self.get_base().get_debug_config() + } + pub fn get_config_file_save_location(&self) -> PathBuf { self.config_directory().join(Self::config_file_name()) } @@ -80,17 +112,21 @@ impl Config { .expect("malformed provider address") } - pub fn get_base(&self) -> &BaseConfig { - &self.base - } - - pub fn get_base_mut(&mut self) -> &mut BaseConfig { - &mut self.base + pub fn get_send_anonymously(&self) -> bool { + self.socks5.send_anonymously } pub fn get_listening_port(&self) -> u16 { self.socks5.listening_port } + + pub fn get_connection_start_surbs(&self) -> u32 { + self.socks5_debug.connection_start_surbs + } + + pub fn get_per_request_surbs(&self) -> u32 { + self.socks5_debug.per_request_surbs + } } #[derive(Debug, Deserialize, PartialEq, Eq, Serialize)] @@ -101,6 +137,13 @@ pub struct Socks5 { /// The mix address of the provider to which all requests are going to be sent. provider_mix_address: String, + + /// Specifies whether this client is going to use an anonymous sender tag for communication with the service provider. + /// While this is going to hide its actual address information, it will make the actual communication + /// slower and consume nearly double the bandwidth as it will require sending reply SURBs. + /// + /// Note that some service providers might not support this. + send_anonymously: bool, } impl Socks5 { @@ -108,6 +151,7 @@ impl Socks5 { Socks5 { listening_port: DEFAULT_SOCKS5_LISTENING_PORT, provider_mix_address: provider_mix_address.into(), + send_anonymously: false, } } } @@ -117,6 +161,26 @@ impl Default for Socks5 { Socks5 { listening_port: DEFAULT_SOCKS5_LISTENING_PORT, provider_mix_address: "".into(), + send_anonymously: false, + } + } +} + +#[derive(Debug, Deserialize, PartialEq, Eq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct Socks5Debug { + /// Number of reply SURBs attached to each `Request::Connect` message. + connection_start_surbs: u32, + + /// Number of reply SURBs attached to each `Request::Send` message. + per_request_surbs: u32, +} + +impl Default for Socks5Debug { + fn default() -> Self { + Socks5Debug { + connection_start_surbs: DEFAULT_CONNECTION_START_SURBS, + per_request_surbs: DEFAULT_PER_REQUEST_SURBS, } } } diff --git a/clients/socks5/src/client/config/template.rs b/clients/socks5/src/client/config/template.rs index 11d3185f56..527da38274 100644 --- a/clients/socks5/src/client/config/template.rs +++ b/clients/socks5/src/client/config/template.rs @@ -49,13 +49,12 @@ private_encryption_key_file = '{{ client.private_encryption_key_file }}' # Path to file containing public encryption key. public_encryption_key_file = '{{ client.public_encryption_key_file }}' -# Full path to file containing reply encryption keys of all reply-SURBs we have ever -# sent but not received back. -reply_encryption_key_store_path = '{{ client.reply_encryption_key_store_path }}' - # Path to the database containing bandwidth credentials database_path = '{{ client.database_path }}' +# Path to the persistent store for received reply surbs, unused encryption keys and used sender tags. +reply_surb_database_path = '{{ client.reply_surb_database_path }}' + ##### additional client config options ##### # A gateway specific, optional, base58 stringified shared key used for @@ -92,6 +91,12 @@ provider_mix_address = '{{ socks5.provider_mix_address }}' # The port on which the client will be listening for incoming requests listening_port = {{ socks5.listening_port }} +# Specifies whether this client is going to use an anonymous sender tag for communication with the service provider. +# While this is going to hide its actual address information, it will make the actual communication +# slower and consume nearly double the bandwidth as it will require sending reply SURBs. +# +# Note that some service providers might not support this. +send_anonymously = {{ socks5.send_anonymously }} ##### logging configuration options ##### @@ -104,6 +109,9 @@ listening_port = {{ socks5.listening_port }} # The following options should not be modified unless you know EXACTLY what you are doing # as if set incorrectly, they may impact your anonymity. +# [socks5_debug] + + [debug] average_packet_delay = '{{ debug.average_packet_delay }}' diff --git a/clients/socks5/src/client/mod.rs b/clients/socks5/src/client/mod.rs index 47577507bb..8c20cc651e 100644 --- a/clients/socks5/src/client/mod.rs +++ b/clients/socks5/src/client/mod.rs @@ -1,44 +1,24 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use std::error::Error; -use std::str::FromStr; - use crate::client::config::Config; use crate::error::Socks5ClientError; +use crate::socks; use crate::socks::{ authentication::{AuthenticationMethods, Authenticator, User}, server::SphinxSocksServer, }; -use client_connections::{ConnectionCommandReceiver, ConnectionCommandSender, LaneQueueLengths}; -use client_core::client::cover_traffic_stream::LoopCoverTrafficStream; -use client_core::client::inbound_messages::{ - InputMessage, InputMessageReceiver, InputMessageSender, +use client_core::client::base_client::{ + non_wasm_helpers, BaseClientBuilder, ClientInput, ClientOutput, }; use client_core::client::key_manager::KeyManager; -use client_core::client::mix_traffic::{BatchMixMessageSender, MixTrafficController}; -use client_core::client::real_messages_control::RealMessagesController; -use client_core::client::received_buffer::{ - ReceivedBufferRequestReceiver, ReceivedBufferRequestSender, ReceivedMessagesBufferController, -}; -use client_core::client::reply_key_storage::ReplyKeyStorage; -use client_core::client::topology_control::{ - TopologyAccessor, TopologyRefresher, TopologyRefresherConfig, -}; use client_core::config::persistence::key_pathfinder::ClientKeyPathfinder; -use client_core::error::ClientCoreError; -use crypto::asymmetric::identity; use futures::channel::mpsc; use futures::StreamExt; use gateway_client::bandwidth::BandwidthController; -use gateway_client::{ - AcknowledgementReceiver, AcknowledgementSender, GatewayClient, MixnetMessageReceiver, - MixnetMessageSender, -}; use log::*; use nymsphinx::addressing::clients::Recipient; -use nymsphinx::addressing::nodes::NodeIdentity; -use tap::TapFallible; +use std::error::Error; use task::{wait_for_signal_and_error, ShutdownListener, ShutdownNotifier}; use url::Url; @@ -74,132 +54,7 @@ impl NymClient { } } - pub fn as_mix_recipient(&self) -> Recipient { - Recipient::new( - *self.key_manager.identity_keypair().public_key(), - *self.key_manager.encryption_keypair().public_key(), - // TODO: below only works under assumption that gateway address == gateway id - // (which currently is true) - NodeIdentity::from_base58_string(self.config.get_base().get_gateway_id()).unwrap(), - ) - } - - // future constantly pumping loop cover traffic at some specified average rate - // the pumped traffic goes to the MixTrafficController - fn start_cover_traffic_stream( - &self, - topology_accessor: TopologyAccessor, - mix_tx: BatchMixMessageSender, - shutdown: ShutdownListener, - ) { - info!("Starting loop cover traffic stream..."); - - let mut stream = LoopCoverTrafficStream::new( - self.key_manager.ack_key(), - self.config.get_base().get_average_ack_delay(), - self.config.get_base().get_average_packet_delay(), - self.config - .get_base() - .get_loop_cover_traffic_average_delay(), - mix_tx, - self.as_mix_recipient(), - topology_accessor, - ); - - if let Some(size) = self.config.get_base().get_use_extended_packet_size() { - log::debug!("Setting extended packet size: {:?}", size); - stream.set_custom_packet_size(size.into()); - } - - stream.start_with_shutdown(shutdown); - } - - #[allow(clippy::too_many_arguments)] - fn start_real_traffic_controller( - &self, - topology_accessor: TopologyAccessor, - reply_key_storage: ReplyKeyStorage, - ack_receiver: AcknowledgementReceiver, - input_receiver: InputMessageReceiver, - mix_sender: BatchMixMessageSender, - client_connection_rx: ConnectionCommandReceiver, - lane_queue_lengths: LaneQueueLengths, - shutdown: ShutdownListener, - ) { - let mut controller_config = client_core::client::real_messages_control::Config::new( - self.key_manager.ack_key(), - self.config.get_base().get_ack_wait_multiplier(), - self.config.get_base().get_ack_wait_addition(), - self.config.get_base().get_average_ack_delay(), - self.config.get_base().get_message_sending_average_delay(), - self.config.get_base().get_average_packet_delay(), - self.config - .get_base() - .get_disabled_main_poisson_packet_distribution(), - self.as_mix_recipient(), - ); - - if let Some(size) = self.config.get_base().get_use_extended_packet_size() { - log::debug!("Setting extended packet size: {:?}", size); - controller_config.set_custom_packet_size(size.into()); - } - - info!("Starting real traffic stream..."); - - RealMessagesController::new( - controller_config, - ack_receiver, - input_receiver, - mix_sender, - topology_accessor, - reply_key_storage, - lane_queue_lengths, - client_connection_rx, - ) - .start_with_shutdown(shutdown); - } - - // buffer controlling all messages fetched from provider - // required so that other components would be able to use them (say the websocket) - fn start_received_messages_buffer_controller( - &self, - query_receiver: ReceivedBufferRequestReceiver, - mixnet_receiver: MixnetMessageReceiver, - reply_key_storage: ReplyKeyStorage, - shutdown: ShutdownListener, - ) { - info!("Starting received messages buffer controller..."); - ReceivedMessagesBufferController::new( - self.key_manager.encryption_keypair(), - query_receiver, - mixnet_receiver, - reply_key_storage, - ) - .start_with_shutdown(shutdown); - } - - async fn start_gateway_client( - &mut self, - mixnet_message_sender: MixnetMessageSender, - ack_sender: AcknowledgementSender, - shutdown: ShutdownListener, - ) -> GatewayClient { - let gateway_id = self.config.get_base().get_gateway_id(); - if gateway_id.is_empty() { - panic!("The identity of the gateway is unknown - did you run `nym-client` init?") - } - let gateway_owner = self.config.get_base().get_gateway_owner(); - if gateway_owner.is_empty() { - panic!("The owner of the gateway is unknown - did you run `nym-client` init?") - } - let gateway_address = self.config.get_base().get_gateway_listener(); - if gateway_address.is_empty() { - panic!("The address of the gateway is unknown - did you run `nym-client` init?") - } - - let gateway_identity = identity::PublicKey::from_base58_string(gateway_id) - .expect("provided gateway id is invalid!"); - + async fn create_bandwidth_controller(config: &Config) -> BandwidthController { #[cfg(feature = "coconut")] //temp hard code these values for testing let qwerty_validator: Url = @@ -225,133 +80,68 @@ impl NymClient { .await .expect("Could not query api clients"); BandwidthController::new( - credential_storage::initialise_storage(self.config.get_base().get_database_path()) - .await, + credential_storage::initialise_storage(config.get_base().get_database_path()).await, coconut_api_clients, ) }; #[cfg(not(feature = "coconut"))] let bandwidth_controller = BandwidthController::new( - credential_storage::initialise_storage(self.config.get_base().get_database_path()) - .await, + credential_storage::initialise_storage(config.get_base().get_database_path()).await, ) .expect("Could not create bandwidth controller"); - - let mut gateway_client = GatewayClient::new( - gateway_address, - self.key_manager.identity_keypair(), - gateway_identity, - gateway_owner, - Some(self.key_manager.gateway_shared_key()), - mixnet_message_sender, - ack_sender, - self.config.get_base().get_gateway_response_timeout(), - Some(bandwidth_controller), - Some(shutdown), - ); - - gateway_client - .set_disabled_credentials_mode(self.config.get_base().get_disabled_credentials_mode()); - - gateway_client - .authenticate_and_start() - .await - .expect("could not authenticate and start up the gateway connection"); - - gateway_client - } - - // future responsible for periodically polling directory server and updating - // the current global view of topology - async fn start_topology_refresher( - &mut self, - topology_accessor: TopologyAccessor, - shutdown: ShutdownListener, - ) -> Result<(), Socks5ClientError> { - let topology_refresher_config = TopologyRefresherConfig::new( - self.config.get_base().get_validator_api_endpoints(), - self.config.get_base().get_topology_refresh_rate(), - env!("CARGO_PKG_VERSION").to_string(), - ); - let mut topology_refresher = - TopologyRefresher::new(topology_refresher_config, topology_accessor); - // before returning, block entire runtime to refresh the current network view so that any - // components depending on topology would see a non-empty view - info!("Obtaining initial network topology"); - topology_refresher.refresh().await; - - // TODO: a slightly more graceful termination here - if !topology_refresher.is_topology_routable().await { - log::error!( - "The current network topology seem to be insufficient to route any packets through \ - - check if enough nodes and a gateway are online" - ); - return Err(ClientCoreError::InsufficientNetworkTopology.into()); - } - - info!("Starting topology refresher..."); - topology_refresher.start_with_shutdown(shutdown); - Ok(()) - } - - // controller for sending sphinx packets to mixnet (either real traffic or cover traffic) - // TODO: if we want to send control messages to gateway_client, this CAN'T take the ownership - // over it. Perhaps GatewayClient needs to be thread-shareable or have some channel for - // requests? - fn start_mix_traffic_controller( - gateway_client: GatewayClient, - shutdown: ShutdownListener, - ) -> BatchMixMessageSender { - info!("Starting mix traffic controller..."); - let (mix_traffic_controller, mix_tx) = MixTrafficController::new(gateway_client); - mix_traffic_controller.start_with_shutdown(shutdown); - mix_tx + bandwidth_controller } fn start_socks5_listener( - &self, - buffer_requester: ReceivedBufferRequestSender, - msg_input: InputMessageSender, - client_connection_tx: ConnectionCommandSender, - lane_queue_lengths: LaneQueueLengths, - mut shutdown: ShutdownListener, + config: &Config, + client_input: ClientInput, + client_output: ClientOutput, + self_address: Recipient, + shutdown: ShutdownListener, ) { info!("Starting socks5 listener..."); let auth_methods = vec![AuthenticationMethods::NoAuth as u8]; let allowed_users: Vec = Vec::new(); + let ClientInput { + shared_lane_queue_lengths, + connection_command_sender, + input_sender, + } = client_input; + + let received_buffer_request_sender = client_output.received_buffer_request_sender; + let authenticator = Authenticator::new(auth_methods, allowed_users); let mut sphinx_socks = SphinxSocksServer::new( - self.config.get_listening_port(), + config.get_listening_port(), authenticator, - self.config.get_provider_mix_address(), - self.as_mix_recipient(), - lane_queue_lengths, + config.get_provider_mix_address(), + self_address, + shared_lane_queue_lengths, + socks::client::Config::new( + config.get_send_anonymously(), + config.get_connection_start_surbs(), + config.get_per_request_surbs(), + ), shutdown.clone(), ); - tokio::spawn(async move { - // Ideally we should have a fully fledged task manager to check for errors in all - // tasks. - // However, pragmatically, we start out by at least reporting errors for some of the - // tasks that interact with the outside world and can fail in normal operation, such as - // network issues. - // TODO: replace this by a generic solution, such as a task manager that stores all - // JoinHandles of all spawned tasks. - if let Err(res) = sphinx_socks - .serve(msg_input, buffer_requester, client_connection_tx) - .await - { - shutdown.send_we_stopped(Box::new(res)); - } - }); + task::spawn_with_report_error( + async move { + sphinx_socks + .serve( + input_sender, + received_buffer_request_sender, + connection_command_sender, + ) + .await + }, + shutdown, + ); } /// blocking version of `start` method. Will run forever (or until SIGINT is sent) - pub async fn run_forever(&mut self) -> Result<(), Box> { - let mut shutdown = self - .start() - .await - .map_err(|err| Box::new(err) as Box)?; + pub async fn run_forever(self) -> Result<(), Box> { + let mut shutdown = self.start().await?; let res = wait_for_signal_and_error(&mut shutdown).await; @@ -367,14 +157,11 @@ impl NymClient { // Variant of `run_forever` that listends for remote control messages pub async fn run_and_listen( - &mut self, + self, mut receiver: Socks5ControlMessageReceiver, - ) -> Result<(), Box> { + ) -> Result<(), Box> { // Start the main task - let mut shutdown = self - .start() - .await - .map_err(|err| Box::new(err) as Box)?; + let mut shutdown = self.start().await?; let res = tokio::select! { biased; @@ -410,101 +197,34 @@ impl NymClient { res } - pub async fn start(&mut self) -> Result { - info!("Starting nym client"); - // channels for inter-component communication - // TODO: make the channels be internally created by the relevant components - // rather than creating them here, so say for example the buffer controller would create the request channels - // and would allow anyone to clone the sender channel - - // unwrapped_sphinx_sender is the transmitter of mixnet messages received from the gateway - // unwrapped_sphinx_receiver is the receiver for said messages - used by ReceivedMessagesBuffer - let (mixnet_messages_sender, mixnet_messages_receiver) = mpsc::unbounded(); - - // used for announcing connection or disconnection of a channel for pushing re-assembled messages to - let (received_buffer_request_sender, received_buffer_request_receiver) = mpsc::unbounded(); - - // channels responsible for controlling real messages - let (input_sender, input_receiver) = tokio::sync::mpsc::channel::(1); - - // channels responsible for controlling ack messages - let (ack_sender, ack_receiver) = mpsc::unbounded(); - let shared_topology_accessor = TopologyAccessor::new(); - - let reply_key_storage = - ReplyKeyStorage::load(self.config.get_base().get_reply_encryption_key_store_path()) - .tap_err(|err| { - log::error!("Failed to load reply key storage - is it perhaps already in use?"); - log::error!("{}", err); - })?; - - // Shutdown notifier for signalling tasks to stop - let shutdown = ShutdownNotifier::default(); - - // the components are started in very specific order. Unless you know what you are doing, - // do not change that. - self.start_topology_refresher(shared_topology_accessor.clone(), shutdown.subscribe()) - .await?; - self.start_received_messages_buffer_controller( - received_buffer_request_receiver, - mixnet_messages_receiver, - reply_key_storage.clone(), - shutdown.subscribe(), + pub async fn start(self) -> Result { + let base_builder = BaseClientBuilder::new_from_base_config( + self.config.get_base(), + self.key_manager, + Some(Self::create_bandwidth_controller(&self.config).await), + non_wasm_helpers::setup_fs_reply_surb_backend( + self.config.get_base().get_reply_surb_database_path(), + self.config.get_debug_settings(), + ) + .await?, ); - let gateway_client = self - .start_gateway_client(mixnet_messages_sender, ack_sender, shutdown.subscribe()) - .await; + let self_address = base_builder.as_mix_recipient(); + let mut started_client = base_builder.start_base().await?; + let client_input = started_client.client_input.register_producer(); + let client_output = started_client.client_output.register_consumer(); - // The sphinx_message_sender is the transmitter for any component generating sphinx packets - // that are to be sent to the mixnet. They are used by cover traffic stream and real - // traffic stream. - // The MixTrafficController then sends the actual traffic - let sphinx_message_sender = - Self::start_mix_traffic_controller(gateway_client, shutdown.subscribe()); - - // Channel for announcing closed (socks5) connections by the controller. - // This will be forwarded to `OutQueueControl` - let (client_connection_tx, client_connection_rx) = mpsc::unbounded(); - - // Shared queue length data. Published by the `OutQueueController` in the client, and used - // primarily to throttle incoming connections - let shared_lane_queue_lengths = LaneQueueLengths::new(); - - self.start_real_traffic_controller( - shared_topology_accessor.clone(), - reply_key_storage, - ack_receiver, - input_receiver, - sphinx_message_sender.clone(), - client_connection_rx, - shared_lane_queue_lengths.clone(), - shutdown.subscribe(), - ); - - if !self - .config - .get_base() - .get_disabled_loop_cover_traffic_stream() - { - self.start_cover_traffic_stream( - shared_topology_accessor, - sphinx_message_sender, - shutdown.subscribe(), - ); - } - - self.start_socks5_listener( - received_buffer_request_sender, - input_sender, - client_connection_tx, - shared_lane_queue_lengths, - shutdown.subscribe(), + Self::start_socks5_listener( + &self.config, + client_input, + client_output, + self_address, + started_client.shutdown_notifier.subscribe(), ); info!("Client startup finished!"); - info!("The address of this client is: {}", self.as_mix_recipient()); + info!("The address of this client is: {}", self_address); - Ok(shutdown) + Ok(started_client.shutdown_notifier) } } diff --git a/clients/socks5/src/commands/init.rs b/clients/socks5/src/commands/init.rs index 58b10002c4..70e8935ca3 100644 --- a/clients/socks5/src/commands/init.rs +++ b/clients/socks5/src/commands/init.rs @@ -1,14 +1,17 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use clap::Args; -use client_core::{config::GatewayEndpoint, error::ClientCoreError}; -use config::NymConfig; - use crate::{ client::config::Config, commands::{override_config, OverrideConfig}, + error::Socks5ClientError, }; +use clap::Args; +use config::NymConfig; +use nymsphinx::addressing::clients::Recipient; +use serde::Serialize; +use std::fmt::Display; +use tap::TapFallible; #[derive(Args, Clone)] pub(crate) struct Init { @@ -20,6 +23,14 @@ pub(crate) struct Init { #[clap(long)] provider: String, + /// Specifies whether this client is going to use an anonymous sender tag for communication with the service provider. + /// While this is going to hide its actual address information, it will make the actual communication + /// slower and consume nearly double the bandwidth as it will require sending reply SURBs. + /// + /// Note that some service providers might not support this. + #[clap(long)] + use_anonymous_sender_tag: bool, + /// Id of the gateway we are going to connect to. #[clap(long)] gateway: Option, @@ -46,11 +57,19 @@ pub(crate) struct Init { #[clap(long, hidden = true)] fastmode: bool, + /// Disable loop cover traffic and the Poisson rate limiter (for debugging only) + #[clap(long, hidden = true)] + no_cover: bool, + /// Set this client to work in a enabled credentials mode that would attempt to use gateway /// with bandwidth credential requirement. #[cfg(feature = "coconut")] #[clap(long)] enabled_credentials_mode: bool, + + /// Save a summary of the initialization to a json file + #[clap(long)] + output_json: bool, } impl From for OverrideConfig { @@ -59,14 +78,39 @@ impl From for OverrideConfig { nymd_validators: init_config.nymd_validators, api_validators: init_config.api_validators, port: init_config.port, + use_anonymous_sender_tag: init_config.use_anonymous_sender_tag, fastmode: init_config.fastmode, + no_cover: init_config.no_cover, #[cfg(feature = "coconut")] enabled_credentials_mode: init_config.enabled_credentials_mode, } } } -pub(crate) async fn execute(args: &Init) { +#[derive(Debug, Serialize)] +pub struct InitResults { + #[serde(flatten)] + client_core: client_core::init::InitResults, + socks5_listening_port: String, +} + +impl InitResults { + fn new(config: &Config, address: &Recipient) -> Self { + Self { + client_core: client_core::init::InitResults::new(config.get_base(), address), + socks5_listening_port: config.get_listening_port().to_string(), + } + } +} + +impl Display for InitResults { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + writeln!(f, "{}", self.client_core)?; + write!(f, "SOCKS5 listening port: {}", self.socks5_listening_port) + } +} + +pub(crate) async fn execute(args: &Init) -> Result<(), Socks5ClientError> { println!("Initialising client..."); let id = &args.id; @@ -91,25 +135,47 @@ pub(crate) async fn execute(args: &Init) { let register_gateway = !already_init || user_wants_force_register; // Attempt to use a user-provided gateway, if possible - let user_chosen_gateway_id = args.gateway.as_deref(); + let user_chosen_gateway_id = args.gateway.clone(); - let mut config = Config::new(id, provider_address); - let override_config_fields = OverrideConfig::from(args.clone()); - config = override_config(config, override_config_fields); + // Load and potentially override config + let mut config = override_config( + Config::new(id, provider_address), + OverrideConfig::from(args.clone()), + ); + + // Setup gateway by either registering a new one, or creating a new config from the selected + // one but with keys kept, or reusing the gateway configuration. + let gateway = client_core::init::setup_gateway::<_, Config, _>( + register_gateway, + user_chosen_gateway_id, + config.get_base(), + ) + .await + .tap_err(|err| eprintln!("Failed to setup gateway\nError: {err}"))?; - let gateway = setup_gateway(id, register_gateway, user_chosen_gateway_id, &config) - .await - .unwrap_or_else(|err| { - eprintln!("Failed to setup gateway\nError: {err}"); - std::process::exit(1) - }); config.get_base_mut().with_gateway_endpoint(gateway); - let config_save_location = config.get_config_file_save_location(); - config - .save_to_file(None) - .expect("Failed to save the config file"); + config.save_to_file(None).tap_err(|_| { + log::error!("Failed to save the config file"); + })?; + print_saved_config(&config); + + let address = client_core::init::get_client_address_from_stored_keys(config.get_base())?; + let init_results = InitResults::new(&config, &address); + println!("{}", init_results); + + // Output summary to a json file, if specified + if args.output_json { + client_core::init::output_to_json(&init_results, "socks5_client_init_results.json"); + } + + println!("\nThe address of this client is: {}\n", address); + Ok(()) +} + +fn print_saved_config(config: &Config) { + let config_save_location = config.get_config_file_save_location(); println!("Saved configuration file to {:?}", config_save_location); println!("Using gateway: {}", config.get_base().get_gateway_id()); log::debug!("Gateway id: {}", config.get_base().get_gateway_id()); @@ -118,62 +184,5 @@ pub(crate) async fn execute(args: &Init) { "Gateway listener: {}", config.get_base().get_gateway_listener() ); - println!("Client configuration completed."); - - client_core::init::show_address(config.get_base()).unwrap_or_else(|err| { - eprintln!("Failed to show address\nError: {err}"); - std::process::exit(1) - }); -} - -async fn setup_gateway( - id: &str, - register: bool, - user_chosen_gateway_id: Option<&str>, - config: &Config, -) -> Result { - if register { - // Get the gateway details by querying the validator-api. Either pick one at random or use - // the chosen one if it's among the available ones. - println!("Configuring gateway"); - let gateway = client_core::init::query_gateway_details( - config.get_base().get_validator_api_endpoints(), - user_chosen_gateway_id, - ) - .await?; - log::debug!("Querying gateway gives: {}", gateway); - - // Registering with gateway by setting up and writing shared keys to disk - log::trace!("Registering gateway"); - client_core::init::register_with_gateway_and_store_keys(gateway.clone(), config.get_base()) - .await?; - println!("Saved all generated keys"); - - Ok(gateway.into()) - } else if user_chosen_gateway_id.is_some() { - // Just set the config, don't register or create any keys - // This assumes that the user knows what they are doing, and that the existing keys are - // valid for the gateway being used - println!("Using gateway provided by user, keeping existing keys"); - let gateway = client_core::init::query_gateway_details( - config.get_base().get_validator_api_endpoints(), - user_chosen_gateway_id, - ) - .await?; - log::debug!("Querying gateway gives: {}", gateway); - Ok(gateway.into()) - } else { - println!("Not registering gateway, will reuse existing config and keys"); - let existing_config = Config::load_from_file(Some(id)).map_err(|err| { - log::error!( - "Unable to configure gateway: {err}. \n - Seems like the client was already initialized but it was not possible to read \ - the existing configuration file. \n - CAUTION: Consider backing up your gateway keys and try force gateway registration, or \ - removing the existing configuration and starting over." - ); - ClientCoreError::CouldNotLoadExistingGatewayConfiguration(err) - })?; - Ok(existing_config.get_base().get_gateway_endpoint().clone()) - } + println!("Client configuration completed.\n"); } diff --git a/clients/socks5/src/commands/mod.rs b/clients/socks5/src/commands/mod.rs index e1d42e0fb1..5f5bfda1de 100644 --- a/clients/socks5/src/commands/mod.rs +++ b/clients/socks5/src/commands/mod.rs @@ -82,17 +82,19 @@ pub(crate) struct OverrideConfig { nymd_validators: Option, api_validators: Option, port: Option, + use_anonymous_sender_tag: bool, fastmode: bool, + no_cover: bool, #[cfg(feature = "coconut")] enabled_credentials_mode: bool, } -pub(crate) async fn execute(args: &Cli) -> Result<(), Box> { +pub(crate) async fn execute(args: &Cli) -> Result<(), Box> { let bin_name = "nym-socks5-client"; match &args.command { - Commands::Init(m) => init::execute(m).await, + Commands::Init(m) => init::execute(m).await?, Commands::Run(m) => run::execute(m).await?, Commands::Upgrade(m) => upgrade::execute(m), Commands::Completions(s) => s.generate(&mut Cli::into_app(), bin_name), @@ -121,6 +123,10 @@ pub(crate) fn override_config(mut config: Config, args: OverrideConfig) -> Confi .set_custom_validator_apis(parse_validators(&raw_validators)); } + if args.use_anonymous_sender_tag { + config = config.with_anonymous_replies(true) + } + if let Some(port) = args.port { config = config.with_port(port); } @@ -136,6 +142,10 @@ pub(crate) fn override_config(mut config: Config, args: OverrideConfig) -> Confi config.get_base_mut().set_high_default_traffic_volume(); } + if args.no_cover { + config.get_base_mut().set_no_cover_traffic(); + } + config } diff --git a/clients/socks5/src/commands/run.rs b/clients/socks5/src/commands/run.rs index d74e6678fe..612ecc02c2 100644 --- a/clients/socks5/src/commands/run.rs +++ b/clients/socks5/src/commands/run.rs @@ -22,6 +22,14 @@ pub(crate) struct Run { #[clap(long)] config: Option, + /// Specifies whether this client is going to use an anonymous sender tag for communication with the service provider. + /// While this is going to hide its actual address information, it will make the actual communication + /// slower and consume nearly double the bandwidth as it will require sending reply SURBs. + /// + /// Note that some service providers might not support this. + #[clap(long)] + use_anonymous_sender_tag: bool, + /// Address of the socks5 provider to send messages to. #[clap(long)] provider: Option, @@ -43,6 +51,15 @@ pub(crate) struct Run { #[clap(short, long)] port: Option, + /// Mostly debug-related option to increase default traffic rate so that you would not need to + /// modify config post init + #[clap(long, hidden = true)] + fastmode: bool, + + /// Disable loop cover traffic and the Poisson rate limiter (for debugging only) + #[clap(long, hidden = true)] + no_cover: bool, + /// Set this client to work in a enabled credentials mode that would attempt to use gateway /// with bandwidth credential requirement. #[cfg(feature = "coconut")] @@ -56,8 +73,9 @@ impl From for OverrideConfig { nymd_validators: run_config.nymd_validators, api_validators: run_config.api_validators, port: run_config.port, - fastmode: false, - + use_anonymous_sender_tag: run_config.use_anonymous_sender_tag, + fastmode: run_config.fastmode, + no_cover: run_config.no_cover, #[cfg(feature = "coconut")] enabled_credentials_mode: run_config.enabled_credentials_mode, } @@ -86,7 +104,7 @@ fn version_check(cfg: &Config) -> bool { } } -pub(crate) async fn execute(args: &Run) -> Result<(), Box> { +pub(crate) async fn execute(args: &Run) -> Result<(), Box> { let id = &args.id; let mut config = match Config::load_from_file(Some(id)) { diff --git a/clients/socks5/src/error.rs b/clients/socks5/src/error.rs index 6b67e6a339..23cd715cf5 100644 --- a/clients/socks5/src/error.rs +++ b/clients/socks5/src/error.rs @@ -1,32 +1,24 @@ -use client_core::{client::reply_key_storage::ReplyKeyStorageError, error::ClientCoreError}; -use crypto::asymmetric::identity::Ed25519RecoveryError; -use gateway_client::error::GatewayClientError; -use validator_client::ValidatorClientError; - use crate::socks::types::SocksProxyError; +use client_core::client::replies::reply_storage::fs_backend; +use client_core::error::ClientCoreError; #[derive(thiserror::Error, Debug)] pub enum Socks5ClientError { #[error("I/O error: {0}")] IoError(#[from] std::io::Error), - #[error("Gateway client error: {0}")] - GatewayClientError(#[from] GatewayClientError), - #[error("Ed25519 error: {0}")] - Ed25519RecoveryError(#[from] Ed25519RecoveryError), - #[error("Validator client error: {0}")] - ValidatorClientError(#[from] ValidatorClientError), + #[error("client-core error: {0}")] - ClientCoreError(#[from] ClientCoreError), - #[error("Reply key storage error: {0}")] - ReplyKeyStorageError(#[from] ReplyKeyStorageError), + ClientCoreError(#[from] ClientCoreError), #[error("SOCKS proxy error")] SocksProxyError(SocksProxyError), #[error("Failed to load config for: {0}")] FailedToLoadConfig(String), + #[error("Failed local version check, client and config mismatch")] FailedLocalVersionCheck, + #[error("Fail to bind address")] FailToBindAddress, } diff --git a/clients/socks5/src/main.rs b/clients/socks5/src/main.rs index d41e5e4bd8..ea0071f538 100644 --- a/clients/socks5/src/main.rs +++ b/clients/socks5/src/main.rs @@ -13,7 +13,7 @@ pub mod error; pub mod socks; #[tokio::main] -async fn main() -> Result<(), Box> { +async fn main() -> Result<(), Box> { setup_logging(); println!("{}", banner()); diff --git a/clients/socks5/src/socks/client.rs b/clients/socks5/src/socks/client.rs index 739bd84987..3b35a4caf9 100644 --- a/clients/socks5/src/socks/client.rs +++ b/clients/socks5/src/socks/client.rs @@ -2,8 +2,8 @@ use super::authentication::{AuthenticationMethods, Authenticator, User}; use super::request::{SocksCommand, SocksRequest}; -use super::types::{ResponseCode, SocksProxyError}; -use super::{RESERVED, SOCKS_VERSION}; +use super::types::{ResponseCodeV4, ResponseCodeV5, SocksProxyError}; +use super::{SocksVersion, RESERVED, SOCKS4_VERSION, SOCKS5_VERSION}; use client_connections::{LaneQueueLengths, TransmissionLane}; use client_core::client::inbound_messages::{InputMessage, InputMessageSender}; use futures::channel::mpsc; @@ -126,16 +126,38 @@ impl AsyncWrite for StreamState { } } +#[derive(Debug, Copy, Clone)] +pub(crate) struct Config { + use_surbs_for_responses: bool, + connection_start_surbs: u32, + per_request_surbs: u32, +} + +impl Config { + pub(crate) fn new( + use_surbs_for_responses: bool, + connection_start_surbs: u32, + per_request_surbs: u32, + ) -> Self { + Self { + use_surbs_for_responses, + connection_start_surbs, + per_request_surbs, + } + } +} + /// A client connecting to the Socks proxy server, because /// it wants to make a Nym-protected outbound request. Typically, this is /// something like e.g. a wallet app running on your laptop connecting to -/// SphinxSocksServer. +/// `SphinxSocksServer`. pub(crate) struct SocksClient { + config: Config, controller_sender: ControllerSender, stream: StreamState, auth_nmethods: u8, authenticator: Authenticator, - socks_version: u8, + socks_version: Option, input_sender: InputMessageSender, connection_id: ConnectionId, service_provider: Recipient, @@ -158,15 +180,15 @@ impl Drop for SocksClient { } impl SocksClient { - /// Create a new SOCKClient #[allow(clippy::too_many_arguments)] pub fn new( + config: Config, stream: TcpStream, authenticator: Authenticator, input_sender: InputMessageSender, - service_provider: Recipient, + service_provider: &Recipient, controller_sender: ControllerSender, - self_address: Recipient, + self_address: &Recipient, lane_queue_lengths: LaneQueueLengths, mut shutdown_listener: ShutdownListener, ) -> Self { @@ -176,15 +198,16 @@ impl SocksClient { let connection_id = Self::generate_random(); SocksClient { + config, controller_sender, connection_id, stream: StreamState::Available(stream), auth_nmethods: 0, - socks_version: 0, + socks_version: None, authenticator, input_sender, - service_provider, - self_address, + service_provider: *service_provider, + self_address: *self_address, started_proxy: false, lane_queue_lengths, shutdown_listener, @@ -196,13 +219,45 @@ impl SocksClient { rng.next_u64() } + pub async fn send_error(&mut self, err: SocksProxyError) -> Result<(), SocksProxyError> { + let error_text = format!("{}", err); + let Some(ref version) = self.socks_version else { + log::error!("Trying to send error without knowing the version"); + return Ok(()); + }; + + match version { + SocksVersion::V4 => { + let response = ResponseCodeV4::RequestRejected; + self.send_error_v4(response).await + } + SocksVersion::V5 => { + let response = if error_text.contains("Host") { + ResponseCodeV5::HostUnreachable + } else if error_text.contains("Network") { + ResponseCodeV5::NetworkUnreachable + } else if error_text.contains("ttl") { + ResponseCodeV5::TtlExpired + } else { + ResponseCodeV5::Failure + }; + self.send_error_v5(response).await + } + } + } + // Send an error back to the client - pub async fn error(&mut self, r: ResponseCode) -> Result<(), SocksProxyError> { - self.stream.write_all(&[5, r as u8]).await?; + pub async fn send_error_v4(&mut self, r: ResponseCodeV4) -> Result<(), SocksProxyError> { + self.stream.write_all(&[SOCKS4_VERSION, r as u8]).await?; Ok(()) } - /// Shutdown the TcpStream to the client and end the session + pub async fn send_error_v5(&mut self, r: ResponseCodeV5) -> Result<(), SocksProxyError> { + self.stream.write_all(&[SOCKS5_VERSION, r as u8]).await?; + Ok(()) + } + + /// Shutdown the `TcpStream` to the client and end the session pub async fn shutdown(&mut self) -> Result<(), SocksProxyError> { info!("client is shutting down its TCP stream"); self.stream.shutdown().await?; @@ -214,35 +269,37 @@ impl SocksClient { /// is in use and that the client is authenticated, then runs the request. pub async fn run(&mut self) -> Result<(), SocksProxyError> { debug!("New connection from: {}", self.stream.peer_addr()?.ip()); - let mut header = [0u8; 2]; + // Read a byte from the stream and determine the version being requested + let mut header = [0u8]; self.stream.read_exact(&mut header).await?; - self.socks_version = header[0]; - self.auth_nmethods = header[1]; + self.socks_version = match SocksVersion::try_from(header[0]) { + Ok(version) => Some(version), + Err(_err) => { + warn!("Init: Unsupported version: SOCKS{}", header[0]); + return self.shutdown().await; + } + }; - // Handle SOCKS4 requests - if header[0] != SOCKS_VERSION { - warn!("Init: Unsupported version: SOCKS{}", self.socks_version); - self.shutdown().await - } - // Valid SOCKS5 - else { - // Authenticate w/ client - self.authenticate().await?; - // Handle requests - self.handle_request().await + if self.socks_version == Some(SocksVersion::V5) { + let mut auth = [0u8]; + self.stream.read_exact(&mut auth).await?; + self.auth_nmethods = auth[0]; + self.authenticate_socks5().await?; } + + self.handle_request().await } - async fn send_connect_to_mixnet(&mut self, remote_address: RemoteAddress) { - let req = Request::new_connect(self.connection_id, remote_address, self.self_address); + async fn send_anonymous_connect_to_mixnet(&mut self, remote_address: RemoteAddress) { + let req = Request::new_connect(self.connection_id, remote_address, None); let msg = Message::Request(req); - let input_message = InputMessage::new_fresh( + let input_message = InputMessage::new_anonymous( self.service_provider, msg.into_bytes(), - false, + self.config.connection_start_surbs, TransmissionLane::ConnectionId(self.connection_id), ); self.input_sender @@ -251,6 +308,30 @@ impl SocksClient { .expect("InputMessageReceiver has stopped receiving!"); } + async fn send_connect_to_mixnet_with_return_address(&mut self, remote_address: RemoteAddress) { + let req = Request::new_connect(self.connection_id, remote_address, Some(self.self_address)); + let msg = Message::Request(req); + + let input_message = InputMessage::new_regular( + self.service_provider, + msg.into_bytes(), + TransmissionLane::ConnectionId(self.connection_id), + ); + self.input_sender + .send(input_message) + .await + .expect("InputMessageReceiver has stopped receiving!"); + } + + async fn send_connect_to_mixnet(&mut self, remote_address: RemoteAddress) { + if self.config.use_surbs_for_responses { + self.send_anonymous_connect_to_mixnet(remote_address).await + } else { + self.send_connect_to_mixnet_with_return_address(remote_address) + .await + } + } + async fn run_proxy(&mut self, conn_receiver: ConnectionReceiver, remote_proxy_target: String) { self.send_connect_to_mixnet(remote_proxy_target.clone()) .await; @@ -267,6 +348,8 @@ impl SocksClient { let connection_id = self.connection_id; let input_sender = self.input_sender.clone(); + let anonymous = self.config.use_surbs_for_responses; + let per_request_surbs = self.config.per_request_surbs; let recipient = self.service_provider; let (stream, _) = ProxyRunner::new( @@ -283,7 +366,16 @@ impl SocksClient { let provider_request = Request::new_send(conn_id, read_data, socket_closed); let provider_message = Message::Request(provider_request); let lane = TransmissionLane::ConnectionId(conn_id); - InputMessage::new_fresh(recipient, provider_message.into_bytes(), false, lane) + if anonymous { + InputMessage::new_anonymous( + recipient, + provider_message.into_bytes(), + per_request_surbs, + lane, + ) + } else { + InputMessage::new_regular(recipient, provider_message.into_bytes(), lane) + } }) .await .into_inner(); @@ -295,8 +387,17 @@ impl SocksClient { async fn handle_request(&mut self) -> Result<(), SocksProxyError> { debug!("Handling CONNECT Command"); - let request = SocksRequest::from_stream(&mut self.stream).await?; - let remote_address = request.to_string(); + let version = self + .socks_version + .as_ref() + .expect("Must read version before parsing request"); + + let request = match version { + SocksVersion::V4 => SocksRequest::from_stream_socks4(&mut self.stream).await?, + SocksVersion::V5 => SocksRequest::from_stream_socks5(&mut self.stream).await?, + }; + + let remote_address = request.address_string(); // setup for receiving from the mixnet let (mix_sender, mix_receiver) = mpsc::unbounded(); @@ -305,7 +406,10 @@ impl SocksClient { // Use the Proxy to connect to the specified addr/port SocksCommand::Connect => { trace!("Connecting to: {:?}", remote_address.clone()); - self.acknowledge_socks5().await; + match version { + SocksVersion::V4 => self.acknowledge_socks4().await, + SocksVersion::V5 => self.acknowledge_socks5().await, + } self.started_proxy = true; self.controller_sender @@ -336,8 +440,8 @@ impl SocksClient { async fn acknowledge_socks5(&mut self) { self.stream .write_all(&[ - SOCKS_VERSION, - ResponseCode::Success as u8, + SOCKS5_VERSION, + ResponseCodeV5::Success as u8, RESERVED, 1, 127, @@ -351,13 +455,30 @@ impl SocksClient { .unwrap(); } + /// Writes a Socks4 header back to the requesting client's TCP stream, + async fn acknowledge_socks4(&mut self) { + self.stream + .write_all(&[ + 0, //SOCKS4_VERSION, + ResponseCodeV4::Granted as u8, + 0, + 0, + 127, + 0, + 0, + 1, + ]) + .await + .unwrap(); + } + /// Authenticate the incoming request. Each request is checked for its /// authentication method. A user/password request will extract the /// username and password from the stream, then check with the Authenticator /// to see if the resulting user is allowed. /// /// A lot of this could probably be put into the `SocksRequest::from_stream()` - /// constructor, and/or cleaned up with tokio::codec. It's mostly just + /// constructor, and/or cleaned up with `tokio::codec`. It's mostly just /// read-a-byte-or-two. The bytes being extracted look like this: /// /// +----+------+----------+------+------------+ @@ -369,7 +490,7 @@ impl SocksClient { /// Pulling out the stream code into its own home, and moving the if/else logic /// into the Authenticator (where it'll be more easily testable) /// would be a good next step. - async fn authenticate(&mut self) -> Result<(), SocksProxyError> { + async fn authenticate_socks5(&mut self) -> Result<(), SocksProxyError> { debug!("Authenticating w/ {}", self.stream.peer_addr()?.ip()); // Get valid auth methods let methods = self.get_available_methods().await?; @@ -378,7 +499,7 @@ impl SocksClient { let mut response = [0u8; 2]; // Set the version in the response - response[0] = SOCKS_VERSION; + response[0] = SOCKS5_VERSION; if methods.contains(&(AuthenticationMethods::UserPass as u8)) { // Set the default auth method (NO AUTH) response[1] = AuthenticationMethods::UserPass as u8; @@ -414,11 +535,11 @@ impl SocksClient { // Authenticate passwords if self.authenticator.is_allowed(&user) { debug!("Access Granted. User: {}", user.username); - let response = [1, ResponseCode::Success as u8]; + let response = [1, ResponseCodeV5::Success as u8]; self.stream.write_all(&response).await?; } else { debug!("Access Denied. User: {}", user.username); - let response = [1, ResponseCode::Failure as u8]; + let response = [1, ResponseCodeV5::Failure as u8]; self.stream.write_all(&response).await?; // Shutdown @@ -437,7 +558,7 @@ impl SocksClient { response[1] = AuthenticationMethods::NoMethods as u8; self.stream.write_all(&response).await?; self.shutdown().await?; - Err(ResponseCode::Failure.into()) + Err(ResponseCodeV5::Failure.into()) } } diff --git a/clients/socks5/src/socks/mixnet_responses.rs b/clients/socks5/src/socks/mixnet_responses.rs index fa9ad1abe8..7613a32019 100644 --- a/clients/socks5/src/socks/mixnet_responses.rs +++ b/clients/socks5/src/socks/mixnet_responses.rs @@ -54,8 +54,8 @@ impl MixnetResponseListener { async fn on_message(&self, reconstructed_message: ReconstructedMessage) { let raw_message = reconstructed_message.message; - if reconstructed_message.reply_surb.is_some() { - warn!("this message had a surb - we didn't do anything with it"); + if reconstructed_message.sender_tag.is_some() { + warn!("this message was sent anonymously - it couldn't have come from the service provider"); } let response = match Message::try_from_bytes(&raw_message) { @@ -105,6 +105,7 @@ impl MixnetResponseListener { } } } + #[cfg(not(target_arch = "wasm32"))] tokio::time::timeout(Duration::from_secs(5), self.shutdown.recv()) .await .expect("Task stopped without shutdown called"); diff --git a/clients/socks5/src/socks/mod.rs b/clients/socks5/src/socks/mod.rs index 20dd99f125..16603839ff 100644 --- a/clients/socks5/src/socks/mod.rs +++ b/clients/socks5/src/socks/mod.rs @@ -1,7 +1,11 @@ #![forbid(unsafe_code)] +use std::convert::TryFrom; + +use self::types::SocksProxyError; + pub mod authentication; -mod client; +pub(crate) mod client; pub(crate) mod mixnet_responses; mod request; pub mod server; @@ -9,6 +13,27 @@ pub mod types; pub mod utils; /// Version of socks -const SOCKS_VERSION: u8 = 0x05; +const SOCKS4_VERSION: u8 = 0x04; +const SOCKS5_VERSION: u8 = 0x05; const RESERVED: u8 = 0x00; + +#[derive(Clone, PartialEq, Eq)] +pub enum SocksVersion { + V4 = 0x04, + V5 = 0x05, +} + +pub struct InvalidSocksVersion; + +impl TryFrom for SocksVersion { + type Error = SocksProxyError; + + fn try_from(version: u8) -> Result { + match version { + SOCKS4_VERSION => Ok(Self::V4), + SOCKS5_VERSION => Ok(Self::V5), + _ => Err(SocksProxyError::UnsupportedProxyVersion(version)), + } + } +} diff --git a/clients/socks5/src/socks/request.rs b/clients/socks5/src/socks/request.rs index 92d687aa90..ecf9b656f3 100644 --- a/clients/socks5/src/socks/request.rs +++ b/clients/socks5/src/socks/request.rs @@ -1,5 +1,7 @@ -use super::types::{AddrType, ResponseCode, SocksProxyError}; -use super::{utils as socks_utils, SOCKS_VERSION}; +use crate::socks::SOCKS4_VERSION; + +use super::types::{AddrType, ResponseCodeV5, SocksProxyError}; +use super::{utils as socks_utils, SOCKS5_VERSION}; use log::*; use std::fmt::{self, Display}; use tokio::io::{AsyncRead, AsyncReadExt}; @@ -15,80 +17,114 @@ pub(crate) struct SocksRequest { } impl SocksRequest { - /// Parse a SOCKS5 request from a TcpStream - pub async fn from_stream(stream: &mut R) -> Result + /// Parse a SOCKS4 request from a `TcpStream` + /// From documents at: + /// - SOCKS4: https://www.openssh.com/txt/socks4.protocol + /// - SOCKS4a: https://www.openssh.com/txt/socks4a.protocol + pub async fn from_stream_socks4(stream: &mut R) -> Result where R: AsyncRead + Unpin, { + log::trace!("read from stream socks4"); + + let mut packet = [0u8; 3]; + stream.read_exact(&mut packet).await?; + + // CD (command) + let Some(command) = SocksCommand::from(packet[0] as usize) else { + log::warn!("Invalid Command"); + return Err(ResponseCodeV5::CommandNotSupported.into()); + }; + + // DSTPORT + let mut port = [0u8; 2]; + port.copy_from_slice(&packet[1..]); + let port = merge_u8_into_u16(port[0], port[1]); + + // DSTIP + let mut ip = [0u8; 4]; + stream.read_exact(&mut ip).await?; + + // USERID + let _userid = read_until_zero(stream).await; + + // SOCKS4a extension + // https://www.openssh.com/txt/socks4a.protocol + // If the IP is 0.0.0.x with x nonzero, read the domain name + let (addr, addr_type) = if ip[..3] == [0, 0, 0] && ip[3] != 0 { + (read_until_zero(stream).await?, AddrType::Domain) + } else { + (ip.to_vec(), AddrType::V4) + }; + + // Return parsed request + Ok(SocksRequest { + version: SOCKS4_VERSION, + command, + addr_type, + addr, + port, + }) + } + /// Parse a SOCKS5 request from a `TcpStream` + /// From: https://www.rfc-editor.org/rfc/rfc1928 + pub async fn from_stream_socks5(stream: &mut R) -> Result + where + R: AsyncRead + Unpin, + { + log::info!("read from stream socks5"); + let mut packet = [0u8; 4]; // Read a byte from the stream and determine the version being requested stream.read_exact(&mut packet).await?; - if packet[0] != SOCKS_VERSION { - warn!("from_stream Unsupported version: SOCKS{}", packet[0]); + // VER + if packet[0] != SOCKS5_VERSION { + warn!("Unsupported version: SOCKS{}", packet[0]); return Err(SocksProxyError::UnsupportedProxyVersion(packet[0])); } - // Get command - let mut command: SocksCommand = SocksCommand::Connect; - match SocksCommand::from(packet[1] as usize) { - Some(com) => { - command = com; - Ok(()) - } - None => { - warn!("Invalid Command"); - Err(ResponseCode::CommandNotSupported) - } - }?; + // CMD + let Some(command) = SocksCommand::from(packet[1] as usize) else { + warn!("Invalid Command"); + return Err(ResponseCodeV5::CommandNotSupported.into()); + }; - // DST.address + // RSV + // packet[2] is reserved - let mut addr_type: AddrType = AddrType::V6; - match AddrType::from(packet[3] as usize) { - Some(addr) => { - addr_type = addr; - Ok(()) - } - None => { - error!("No Addr"); - Err(ResponseCode::AddrTypeNotSupported) - } - }?; + // ATYP + let Some(addr_type) = AddrType::from(packet[3] as usize) else { + error!("No Addr"); + return Err(ResponseCodeV5::AddrTypeNotSupported.into()) + }; - trace!("Getting Addr"); - // Get Addr from addr_type and stream - let addr: Result, SocksProxyError> = match addr_type { + // DST.ADDR + let addr = match addr_type { AddrType::Domain => { - let mut domain_length = [0u8; 1]; + let mut domain_length = [0u8]; stream.read_exact(&mut domain_length).await?; - let mut domain = vec![0u8; domain_length[0] as usize]; stream.read_exact(&mut domain).await?; - - Ok(domain) + domain } AddrType::V4 => { let mut addr = [0u8; 4]; stream.read_exact(&mut addr).await?; - Ok(addr.to_vec()) + addr.to_vec() } AddrType::V6 => { let mut addr = [0u8; 16]; stream.read_exact(&mut addr).await?; - Ok(addr.to_vec()) + addr.to_vec() } }; - let addr = addr?; - - // read DST.port + // DST.PORT let mut port = [0u8; 2]; stream.read_exact(&mut port).await?; - // Merge two u8s into u16 - let port = (u16::from(port[0]) << 8) | u16::from(port[1]); + let port = merge_u8_into_u16(port[0], port[1]); - // Return parsed request Ok(SocksRequest { version: packet[0], command, @@ -97,14 +133,18 @@ impl SocksRequest { port, }) } + + /// Print out the address and port to a String. + /// This might return domain:port, ipv6:port, or ipv4:port. + pub fn address_string(&self) -> String { + let address = socks_utils::pretty_print_addr(&self.addr_type, &self.addr); + format!("{}:{}", address, self.port) + } } impl Display for SocksRequest { - /// Print out the address and port to a String. - /// This might return domain:port, ipv6:port, or ipv4:port. fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let address = socks_utils::pretty_print_addr(&self.addr_type, &self.addr); - write!(f, "{}:{}", address, self.port) + write!(f, "{}", self.address_string()) } } @@ -127,3 +167,23 @@ impl SocksCommand { } } } + +fn merge_u8_into_u16(a: u8, b: u8) -> u16 { + (u16::from(a) << 8) | u16::from(b) +} + +async fn read_until_zero(stream: &mut R) -> Result, SocksProxyError> +where + R: AsyncRead + Unpin, +{ + let mut result = Vec::new(); + let mut char = [0u8]; + loop { + stream.read_exact(&mut char).await?; + if char[0] == 0 { + break; + } + result.push(char[0]); + } + Ok(result) +} diff --git a/clients/socks5/src/socks/server.rs b/clients/socks5/src/socks/server.rs index 551f82b788..0bba59ee0b 100644 --- a/clients/socks5/src/socks/server.rs +++ b/clients/socks5/src/socks/server.rs @@ -1,8 +1,9 @@ use crate::error::Socks5ClientError; -use super::authentication::Authenticator; -use super::client::SocksClient; -use super::{mixnet_responses::MixnetResponseListener, types::ResponseCode}; +use super::{ + authentication::Authenticator, client::SocksClient, mixnet_responses::MixnetResponseListener, +}; +use crate::socks::client; use client_connections::{ConnectionCommandSender, LaneQueueLengths}; use client_core::client::{ inbound_messages::InputMessageSender, received_buffer::ReceivedBufferRequestSender, @@ -21,6 +22,7 @@ pub struct SphinxSocksServer { listening_address: SocketAddr, service_provider: Recipient, self_address: Recipient, + client_config: client::Config, lane_queue_lengths: LaneQueueLengths, shutdown: ShutdownListener, } @@ -33,6 +35,7 @@ impl SphinxSocksServer { service_provider: Recipient, self_address: Recipient, lane_queue_lengths: LaneQueueLengths, + client_config: client::Config, shutdown: ShutdownListener, ) -> Self { // hardcode ip as we (presumably) ONLY want to listen locally. If we change it, we can @@ -44,6 +47,7 @@ impl SphinxSocksServer { listening_address: format!("{}:{}", ip, port).parse().unwrap(), service_provider, self_address, + client_config, lane_queue_lengths, shutdown, } @@ -85,47 +89,27 @@ impl SphinxSocksServer { loop { tokio::select! { Ok((stream, _remote)) = listener.accept() => { - // TODO Optimize this let mut client = SocksClient::new( + self.client_config, stream, self.authenticator.clone(), input_sender.clone(), - self.service_provider, + &self.service_provider, controller_sender.clone(), - self.self_address, + &self.self_address, self.lane_queue_lengths.clone(), self.shutdown.clone(), ); tokio::spawn(async move { - { - match client.run().await { - Ok(_) => {} - Err(error) => { - error!("Error! {}", error); - let error_text = format!("{}", error); - - let response: ResponseCode; - - if error_text.contains("Host") { - response = ResponseCode::HostUnreachable; - } else if error_text.contains("Network") { - response = ResponseCode::NetworkUnreachable; - } else if error_text.contains("ttl") { - response = ResponseCode::TtlExpired - } else { - response = ResponseCode::Failure - } - - if client.error(response).await.is_err() { - warn!("Failed to send error code"); - }; - if client.shutdown().await.is_err() { - warn!("Failed to shutdown TcpStream"); - }; - } + 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"); }; - // client gets dropped here } }); }, diff --git a/clients/socks5/src/socks/types.rs b/clients/socks5/src/socks/types.rs index b043da5ccc..669a6b1093 100644 --- a/clients/socks5/src/socks/types.rs +++ b/clients/socks5/src/socks/types.rs @@ -1,23 +1,33 @@ -use snafu::Snafu; -#[derive(Debug, Snafu)] +/// SOCKS4 Response codes +#[allow(dead_code)] +pub(crate) enum ResponseCodeV4 { + Granted = 0x5a, + RequestRejected = 0x5b, + CannotConnectToIdent = 0x5c, + DifferentUserId = 0x5d, +} + /// Possible SOCKS5 Response Codes -pub(crate) enum ResponseCode { +#[allow(dead_code)] +#[derive(Debug, thiserror::Error)] +pub(crate) enum ResponseCodeV5 { + #[error("SOCKS5 Server Success")] Success = 0x00, - #[snafu(display("SOCKS5 Server Failure"))] + #[error("SOCKS5 Server Failure")] Failure = 0x01, - #[snafu(display("SOCKS5 Rule failure"))] + #[error("SOCKS5 Rule failure")] RuleFailure = 0x02, - #[snafu(display("network unreachable"))] + #[error("network unreachable")] NetworkUnreachable = 0x03, - #[snafu(display("host unreachable"))] + #[error("host unreachable")] HostUnreachable = 0x04, - #[snafu(display("connection refused"))] + #[error("connection refused")] ConnectionRefused = 0x05, - #[snafu(display("TTL expired"))] + #[error("TTL expired")] TtlExpired = 0x06, - #[snafu(display("Command not supported"))] + #[error("Command not supported")] CommandNotSupported = 0x07, - #[snafu(display("Addr Type not supported"))] + #[error("Addr Type not supported")] AddrTypeNotSupported = 0x08, } @@ -48,7 +58,7 @@ where } /// DST.addr variant types -#[derive(PartialEq)] +#[derive(Debug, PartialEq)] pub(crate) enum AddrType { V4 = 0x01, Domain = 0x03, diff --git a/clients/webassembly/Cargo.toml b/clients/webassembly/Cargo.toml index 2833598a16..cd60eb7dfa 100644 --- a/clients/webassembly/Cargo.toml +++ b/clients/webassembly/Cargo.toml @@ -39,7 +39,7 @@ topology = { path = "../../common/topology" } gateway-client = { path = "../../common/client-libs/gateway-client", default-features = false, features = ["wasm", "coconut"] } validator-client = { path = "../../common/client-libs/validator-client", default-features = false } wasm-utils = { path = "../../common/wasm-utils" } - +task = { path = "../../common/task" } # The `console_error_panic_hook` crate provides better debugging of panics by # logging them with `console.error`. This is great for development, but requires # all the `std::fmt` and `std::panicking` infrastructure, so isn't great for diff --git a/clients/webassembly/js-example/index.js b/clients/webassembly/js-example/index.js index 4a28435219..f0890bf12e 100644 --- a/clients/webassembly/js-example/index.js +++ b/clients/webassembly/js-example/index.js @@ -65,7 +65,6 @@ async function main() { * * Message and recipient are taken from the values in the user interface. * - * @param {Client} nymClient the nym client to use for message sending */ async function sendMessageTo() { const message = document.getElementById('message').value; @@ -96,10 +95,13 @@ function displaySend(message) { /** * Display received text messages in the browser. Colour them green. * - * @param {string} message + * @param {Uint8Array} raw */ -function displayReceived(message) { - const content = message; +function displayReceived(raw, sender_tag) { + const content = new TextDecoder().decode(raw); + if (sender_tag !== undefined) { + console.log("this message also contained some surbs from", sender_tag) + } let timestamp = new Date().toISOString().substr(11, 12); let receivedDiv = document.createElement('div'); @@ -116,7 +118,7 @@ function displayReceived(message) { /** * Display the nymClient's sender address in the user interface * - * @param {Client} nymClient + * @param {String} address */ function displaySenderAddress(address) { document.getElementById('sender').value = address; diff --git a/clients/webassembly/js-example/package-lock.json b/clients/webassembly/js-example/package-lock.json index 06359b2f03..ea8023655b 100644 --- a/clients/webassembly/js-example/package-lock.json +++ b/clients/webassembly/js-example/package-lock.json @@ -24,7 +24,7 @@ }, "../pkg": { "name": "@nymproject/nym-client-wasm", - "version": "1.0.0", + "version": "1.1.0", "license": "Apache-2.0" }, "node_modules/@discoveryjs/json-ext": { diff --git a/clients/webassembly/js-example/worker.js b/clients/webassembly/js-example/worker.js index 6889109911..da066d6ea2 100644 --- a/clients/webassembly/js-example/worker.js +++ b/clients/webassembly/js-example/worker.js @@ -17,38 +17,7 @@ importScripts('nym_client_wasm.js'); console.log('Initializing worker'); // wasm_bindgen creates a global variable (with the exports attached) that is in scope after `importScripts` -const { default_debug, get_gateway, NymClient, set_panic_hook, Config } = wasm_bindgen; - -class ClientWrapper { - constructor(config, onMessageHandler) { - this.rustClient = new NymClient(config); - this.rustClient.set_on_message(onMessageHandler); - this.rustClient.set_on_gateway_connect(this.onConnect); - } - - selfAddress = () => { - return this.rustClient.self_address(); - }; - - onConnect = () => { - console.log('Established (and authenticated) gateway connection!'); - }; - - start = async () => { - // this is current limitation of wasm in rust - for async methods you can't take self by reference... - // I'm trying to figure out if I can somehow hack my way around it, but for time being you have to re-assign - // the object (it's the same one) - this.rustClient = await this.rustClient.start(); - }; - - sendMessage = async (recipient, message) => { - this.rustClient = await this.rustClient.send_message(recipient, message); - }; - - sendBinaryMessage = async (recipient, message) => { - this.rustClient = await this.rustClient.send_binary_message(recipient, message); - }; -} +const { default_debug, NymClientBuilder, set_panic_hook, Config, GatewayEndpointConfig } = wasm_bindgen; let client = null; @@ -61,19 +30,13 @@ async function main() { // sets up better stack traces in case of in-rust panics set_panic_hook(); - console.error("the current mainnet is not compatible with v2! - either use the pre-merge branch or explicitly set the client to use one of V2 QA networks") - return - // validator server we will use to get topology from - // MAINNET (V1): - const validator = 'https://validator.nymtech.net/api'; //"http://localhost:8081"; - const preferredGateway = 'E3mvZTHQCdBvhfr178Swx9g4QG3kkRUun7YnToLMcMbM'; - // QA (V2): - // const validator = 'https://qa-validator-api.nymtech.net/api'; //"http://localhost:8081"; - // const preferredGateway = 'CgQrYP8etksSBf4nALNqp93SHPpgFwEUyTsjBNNLj5WM'; - - const gatewayEndpoint = await get_gateway(validator, preferredGateway); - gatewayEndpoint.gateway_listener = "wss://gateway1.nymtech.net:443"; // this is needed if we want it to work on the web. However this gateway is a v1 gateway, we will need to change for v2 once we get there + const validator = 'https://qwerty-validator-api.qa.nymte.ch/api'; + + const gatewayId = 'EVupP2tRUeZo5Y6RpBHAbm8kSntpgNyZNL6yCr7BDEoG'; + const gatewayOwner = 'n1rmlew3euapuq7rs4s4j9apv00whrsazr764kl7'; + const gatewayListener = 'ws://176.58.120.72:9000'; + const gatewayEndpoint = new GatewayEndpointConfig(gatewayId, gatewayOwner, gatewayListener) // only really useful if you want to adjust some settings like traffic rate // (if not needed you can just pass a null) @@ -101,12 +64,17 @@ async function main() { }; console.log('Instantiating WASM client...'); - client = new ClientWrapper(config, onMessageHandler); + + let clientBuilder = new NymClientBuilder(config, onMessageHandler) console.log('Web worker creating WASM client...'); - await client.start(); + let local_client = await clientBuilder.start_client(); console.log('WASM client running!'); - - const selfAddress = client.rustClient.self_address(); + + const selfAddress = local_client.self_address(); + + // set the global (I guess we don't have to anymore?) + client = local_client; + console.log(`Client address is ${selfAddress}`); self.postMessage({ kind: 'Ready', @@ -121,7 +89,8 @@ async function main() { switch (event.data.kind) { case 'SendMessage': { const { message, recipient } = event.data.args; - await client.sendMessage(message, recipient); + let uint8Array = new TextEncoder().encode(message); + await client.send_regular_message(uint8Array, recipient); } } } diff --git a/clients/webassembly/src/client/config.rs b/clients/webassembly/src/client/config.rs index 2b8b8bd644..4740a02b2a 100644 --- a/clients/webassembly/src/client/config.rs +++ b/clients/webassembly/src/client/config.rs @@ -4,12 +4,15 @@ // due to expansion of #[wasm_bindgen] macro on `Debug` Config struct #![allow(clippy::drop_non_drop)] -use client_core::config::{Debug as ConfigDebug, ExtendedPacketSize, GatewayEndpoint}; +use client_core::config::{DebugConfig as ConfigDebug, ExtendedPacketSize, GatewayEndpointConfig}; +use serde::{Deserialize, Serialize}; use std::time::Duration; use url::Url; use wasm_bindgen::prelude::*; #[wasm_bindgen] +#[derive(Debug, Deserialize, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] pub struct Config { /// ID specifies the human readable ID of this particular client. pub(crate) id: String, @@ -19,7 +22,7 @@ pub struct Config { pub(crate) disabled_credentials_mode: bool, /// Information regarding how the client should send data to gateway. - pub(crate) gateway_endpoint: GatewayEndpoint, + pub(crate) gateway_endpoint: GatewayEndpointConfig, pub(crate) debug: ConfigDebug, } @@ -30,7 +33,7 @@ impl Config { pub fn new( id: String, validator_server: String, - gateway_endpoint: GatewayEndpoint, + gateway_endpoint: GatewayEndpointConfig, debug: Option, ) -> Self { Config { @@ -103,6 +106,34 @@ pub struct Debug { /// Controls whether the sent sphinx packet use the NON-DEFAULT bigger size. pub use_extended_packet_size: bool, + + /// Defines the minimum number of reply surbs the client wants to keep in its storage at all times. + /// It can only allow to go below that value if its to request additional reply surbs. + pub minimum_reply_surb_storage_threshold: usize, + + /// Defines the maximum number of reply surbs the client wants to keep in its storage at any times. + pub maximum_reply_surb_storage_threshold: usize, + + /// Defines the minimum number of reply surbs the client would request. + pub minimum_reply_surb_request_size: u32, + + /// Defines the maximum number of reply surbs the client would request. + pub maximum_reply_surb_request_size: u32, + + /// Defines the maximum number of reply surbs a remote party is allowed to request from this client at once. + pub maximum_allowed_reply_surb_request_size: u32, + + /// Defines maximum amount of time the client is going to wait for reply surbs before explicitly asking + /// for more even though in theory they wouldn't need to. + pub maximum_reply_surb_waiting_period_ms: u64, + + /// Defines maximum amount of time given reply surb is going to be valid for. + /// This is going to be superseded by key rotation once implemented. + pub maximum_reply_surb_age_ms: u64, + + /// Defines maximum amount of time given reply key is going to be valid for. + /// This is going to be superseded by key rotation once implemented. + pub maximum_reply_key_age_ms: u64, } impl From for ConfigDebug { @@ -132,6 +163,16 @@ impl From for ConfigDebug { disable_main_poisson_packet_distribution: debug .disable_main_poisson_packet_distribution, use_extended_packet_size, + minimum_reply_surb_storage_threshold: debug.minimum_reply_surb_storage_threshold, + maximum_reply_surb_storage_threshold: debug.maximum_reply_surb_storage_threshold, + minimum_reply_surb_request_size: debug.minimum_reply_surb_request_size, + maximum_reply_surb_request_size: debug.maximum_reply_surb_request_size, + maximum_allowed_reply_surb_request_size: debug.maximum_allowed_reply_surb_request_size, + maximum_reply_surb_waiting_period: Duration::from_millis( + debug.maximum_reply_surb_waiting_period_ms, + ), + maximum_reply_surb_age: Duration::from_millis(debug.maximum_reply_surb_age_ms), + maximum_reply_key_age: Duration::from_millis(debug.maximum_reply_key_age_ms), } } } @@ -154,6 +195,16 @@ impl From for Debug { disable_main_poisson_packet_distribution: debug .disable_main_poisson_packet_distribution, use_extended_packet_size: debug.use_extended_packet_size.is_some(), + minimum_reply_surb_storage_threshold: debug.minimum_reply_surb_storage_threshold, + maximum_reply_surb_storage_threshold: debug.maximum_reply_surb_storage_threshold, + minimum_reply_surb_request_size: debug.minimum_reply_surb_request_size, + maximum_reply_surb_request_size: debug.maximum_reply_surb_request_size, + maximum_allowed_reply_surb_request_size: debug.maximum_allowed_reply_surb_request_size, + maximum_reply_surb_waiting_period_ms: debug + .maximum_reply_surb_waiting_period + .as_millis() as u64, + maximum_reply_surb_age_ms: debug.maximum_reply_surb_age.as_millis() as u64, + maximum_reply_key_age_ms: debug.maximum_reply_key_age.as_millis() as u64, } } } diff --git a/clients/webassembly/src/client/helpers.rs b/clients/webassembly/src/client/helpers.rs new file mode 100644 index 0000000000..4349fd5f78 --- /dev/null +++ b/clients/webassembly/src/client/helpers.rs @@ -0,0 +1,30 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use client_core::client::base_client::ClientInput; +use client_core::client::inbound_messages::InputMessage; +use js_sys::Promise; +use std::sync::Arc; +use wasm_bindgen::JsValue; +use wasm_bindgen_futures::future_to_promise; + +// defining helper trait as we could directly call the method on the wrapper +pub(crate) trait InputSender { + fn send_message(&self, message: InputMessage) -> Promise; +} + +impl InputSender for Arc { + fn send_message(&self, message: InputMessage) -> Promise { + let this = Arc::clone(self); + future_to_promise(async move { + match this.input_sender.send(message).await { + Ok(_) => Ok(JsValue::null()), + Err(_) => { + let js_error = + js_sys::Error::new("InputMessageReceiver has stopped receiving!"); + Err(JsValue::from(js_error)) + } + } + }) + } +} diff --git a/clients/webassembly/src/client/mod.rs b/clients/webassembly/src/client/mod.rs index c64dbe69e0..9c3aefe8c6 100644 --- a/clients/webassembly/src/client/mod.rs +++ b/clients/webassembly/src/client/mod.rs @@ -2,67 +2,71 @@ // SPDX-License-Identifier: Apache-2.0 use self::config::Config; -use client_connections::{ConnectionCommandReceiver, LaneQueueLengths, TransmissionLane}; -use client_core::client::{ - cover_traffic_stream::LoopCoverTrafficStream, - inbound_messages::{InputMessage, InputMessageReceiver, InputMessageSender}, - key_manager::KeyManager, - mix_traffic::{BatchMixMessageSender, MixTrafficController}, - real_messages_control::{self, RealMessagesController}, - received_buffer::{ - ReceivedBufferMessage, ReceivedBufferRequestReceiver, ReceivedBufferRequestSender, - ReceivedMessagesBufferController, - }, - topology_control::{TopologyAccessor, TopologyRefresher, TopologyRefresherConfig}, -}; -use crypto::asymmetric::identity; -use futures::channel::mpsc; -use futures::StreamExt; -use gateway_client::{ - AcknowledgementReceiver, AcknowledgementSender, GatewayClient, MixnetMessageReceiver, - MixnetMessageSender, -}; +use crate::client::helpers::InputSender; +use crate::client::response_pusher::ResponsePusher; +use client_connections::TransmissionLane; +use client_core::client::base_client::{BaseClientBuilder, ClientInput, ClientOutput}; +use client_core::client::replies::reply_storage::browser_backend; +use client_core::client::{inbound_messages::InputMessage, key_manager::KeyManager}; +use gateway_client::bandwidth::BandwidthController; +use js_sys::Promise; use nymsphinx::addressing::clients::Recipient; +use nymsphinx::anonymous_replies::requests::AnonymousSenderTag; use rand::rngs::OsRng; +use std::sync::Arc; +use task::ShutdownNotifier; use wasm_bindgen::prelude::*; -use wasm_bindgen_futures::spawn_local; -use wasm_utils::console_log; +use wasm_bindgen_futures::future_to_promise; +use wasm_utils::{console_error, console_log}; pub mod config; +mod helpers; +mod response_pusher; #[wasm_bindgen] pub struct NymClient { + self_address: String, + client_input: Arc, + + // 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 + _shutdown: ShutdownNotifier, +} + +#[wasm_bindgen] +pub struct NymClientBuilder { config: Config, /// KeyManager object containing smart pointers to all relevant keys used by the client. key_manager: KeyManager, - // TODO: this should be stored somewhere persistently - // received_keys: HashSet, - /// Channel used for transforming 'raw' messages into sphinx packets and sending them - /// through the mix network. - input_tx: Option, + reply_surb_storage_backend: browser_backend::Backend, - // callbacks - on_message: Option, - on_binary_message: Option, - on_gateway_connect: Option, + on_message: js_sys::Function, + + // unimplemented: + bandwidth_controller: Option, + disabled_credentials: bool, } #[wasm_bindgen] -impl NymClient { +impl NymClientBuilder { #[wasm_bindgen(constructor)] - pub fn new(config: Config) -> Self { - Self { + pub fn new(config: Config, on_message: js_sys::Function) -> Self { + //, key_manager: Option) { + NymClientBuilder { + reply_surb_storage_backend: Self::setup_reply_surb_storage_backend(&config), config, key_manager: Self::setup_key_manager(), - on_message: None, - on_binary_message: None, - on_gateway_connect: None, - input_tx: None, + on_message, + bandwidth_controller: None, + disabled_credentials: true, } } + // TODO: once we make keys persistent, we'll require some kind of `init` method to generate + // a prior shared keypair between the client and the gateway + // perhaps this should be public? fn setup_key_manager() -> KeyManager { let mut rng = OsRng; @@ -71,337 +75,154 @@ impl NymClient { KeyManager::new(&mut rng) } - pub fn set_on_message(&mut self, on_message: js_sys::Function) { - self.on_message = Some(on_message); - } - - pub fn set_on_binary_message(&mut self, on_binary_message: js_sys::Function) { - self.on_binary_message = Some(on_binary_message); - } - - pub fn set_on_gateway_connect(&mut self, on_connect: js_sys::Function) { - self.on_gateway_connect = Some(on_connect) - } - - fn as_mix_recipient(&self) -> Recipient { - Recipient::new( - *self.key_manager.identity_keypair().public_key(), - *self.key_manager.encryption_keypair().public_key(), - identity::PublicKey::from_base58_string(&self.config.gateway_endpoint.gateway_id) - .expect("no gateway has been selected"), + // don't get too excited about the name, under the hood it's just a big fat placeholder + // with no persistence + fn setup_reply_surb_storage_backend(config: &Config) -> browser_backend::Backend { + browser_backend::Backend::new( + config.debug.minimum_reply_surb_storage_threshold, + config.debug.maximum_reply_surb_storage_threshold, ) } - pub fn self_address(&self) -> String { - self.as_mix_recipient().to_string() + fn start_reconstructed_pusher(client_output: ClientOutput, on_message: js_sys::Function) { + ResponsePusher::new(client_output, on_message).start() } - // future constantly pumping loop cover traffic at some specified average rate - // the pumped traffic goes to the MixTrafficController - fn start_cover_traffic_stream( - &self, - topology_accessor: TopologyAccessor, - mix_tx: BatchMixMessageSender, - ) { - console_log!("Starting loop cover traffic stream..."); + pub async fn start_client(self) -> Promise { + future_to_promise(async move { + console_log!("Starting the wasm client"); - let mut stream = LoopCoverTrafficStream::new( - self.key_manager.ack_key(), - self.config.debug.average_ack_delay, - self.config.debug.average_packet_delay, - self.config.debug.loop_cover_traffic_average_delay, - mix_tx, - self.as_mix_recipient(), - topology_accessor, - ); - - if let Some(size) = &self.config.debug.use_extended_packet_size { - stream.set_custom_packet_size(size.clone().into()); - } - - stream.start(); - } - - fn start_real_traffic_controller( - &self, - topology_accessor: TopologyAccessor, - ack_receiver: AcknowledgementReceiver, - input_receiver: InputMessageReceiver, - mix_sender: BatchMixMessageSender, - client_connection_rx: ConnectionCommandReceiver, - lane_queue_lengths: LaneQueueLengths, - ) { - let mut controller_config = real_messages_control::Config::new( - self.key_manager.ack_key(), - self.config.debug.ack_wait_multiplier, - self.config.debug.ack_wait_addition, - self.config.debug.average_ack_delay, - self.config.debug.message_sending_average_delay, - self.config.debug.average_packet_delay, - self.config.debug.disable_main_poisson_packet_distribution, - self.as_mix_recipient(), - ); - - if let Some(size) = &self.config.debug.use_extended_packet_size { - controller_config.set_custom_packet_size(size.clone().into()); - } - - console_log!("Starting real traffic stream..."); - - RealMessagesController::new( - controller_config, - ack_receiver, - input_receiver, - mix_sender, - topology_accessor, - lane_queue_lengths, - client_connection_rx, - ) - .start(); - } - - // buffer controlling all messages fetched from provider - // required so that other components would be able to use them (say the websocket) - fn start_received_messages_buffer_controller( - &self, - query_receiver: ReceivedBufferRequestReceiver, - mixnet_receiver: MixnetMessageReceiver, - ) { - console_log!("Starting received messages buffer controller..."); - ReceivedMessagesBufferController::new( - self.key_manager.encryption_keypair(), - query_receiver, - mixnet_receiver, - ) - .start() - } - - async fn start_gateway_client( - &mut self, - mixnet_message_sender: MixnetMessageSender, - ack_sender: AcknowledgementSender, - ) -> GatewayClient { - let gateway_id = self.config.gateway_endpoint.gateway_id.clone(); - if gateway_id.is_empty() { - panic!("The identity of the gateway is unknown - did you run `get_gateway()`?") - } - let gateway_owner = self.config.gateway_endpoint.gateway_owner.clone(); - if gateway_owner.is_empty() { - panic!("The owner of the gateway is unknown - did you run `get_gateway()`?") - } - let gateway_address = self.config.gateway_endpoint.gateway_listener.clone(); - if gateway_address.is_empty() { - panic!("The address of the gateway is unknown - did you run `get_gateway()`?") - } - - let gateway_identity = identity::PublicKey::from_base58_string(gateway_id) - .expect("provided gateway id is invalid!"); - - let mut gateway_client = GatewayClient::new( - gateway_address, - self.key_manager.identity_keypair(), - gateway_identity, - gateway_owner, - None, - mixnet_message_sender, - ack_sender, - self.config.debug.gateway_response_timeout, - None, - ); - - gateway_client.set_disabled_credentials_mode(self.config.disabled_credentials_mode); - - let shared_keys = gateway_client - .authenticate_and_start() - .await - .expect("could not authenticate and start up the gateway connection"); - self.key_manager.insert_gateway_shared_key(shared_keys); - - match self.on_gateway_connect.as_ref() { - Some(callback) => { - callback - .call0(&JsValue::null()) - .expect("on connect callback failed!"); - } - None => console_log!("Gateway connection established - no callback specified"), - }; - - gateway_client - } - - // future responsible for periodically polling directory server and updating - // the current global view of topology - async fn start_topology_refresher(&mut self, topology_accessor: TopologyAccessor) { - let topology_refresher_config = TopologyRefresherConfig::new( - vec![self.config.validator_api_url.clone()], - self.config.debug.topology_refresh_rate, - env!("CARGO_PKG_VERSION").to_string(), - ); - let mut topology_refresher = - TopologyRefresher::new(topology_refresher_config, topology_accessor); - // before returning, block entire runtime to refresh the current network view so that any - // components depending on topology would see a non-empty view - console_log!("Obtaining initial network topology"); - topology_refresher.refresh().await; - - // TODO: a slightly more graceful termination here - if !topology_refresher.is_topology_routable().await { - panic!( - "The current network topology seem to be insufficient to route any packets through\ - - check if enough nodes and a gateway are online" + let base_builder = BaseClientBuilder::new( + &self.config.gateway_endpoint, + &self.config.debug, + self.key_manager, + self.bandwidth_controller, + self.reply_surb_storage_backend, + self.disabled_credentials, + vec![self.config.validator_api_url.clone()], ); - } - console_log!("Starting topology refresher..."); - - // TODO: re-enable - topology_refresher.start(); - } - - // controller for sending sphinx packets to mixnet (either real traffic or cover traffic) - // TODO: if we want to send control messages to gateway_client, this CAN'T take the ownership - // over it. Perhaps GatewayClient needs to be thread-shareable or have some channel for - // requests? - fn start_mix_traffic_controller(gateway_client: GatewayClient) -> BatchMixMessageSender { - console_log!("Starting mix traffic controller..."); - let (mix_traffic_controller, mix_tx) = MixTrafficController::new(gateway_client); - mix_traffic_controller.start(); - mix_tx - } - - // TODO: this procedure is extremely overcomplicated, because it's based off native client's behaviour - // which doesn't fully apply in this case - fn start_reconstructed_pusher( - &mut self, - received_buffer_request_sender: ReceivedBufferRequestSender, - ) { - let on_message = self.on_message.take(); - let on_binary_message = self.on_binary_message.take(); - - spawn_local(async move { - let (reconstructed_sender, mut reconstructed_receiver) = mpsc::unbounded(); - - // tell the buffer to start sending stuff to us - received_buffer_request_sender - .unbounded_send(ReceivedBufferMessage::ReceiverAnnounce( - reconstructed_sender, - )) - .expect("the buffer request failed!"); - - let this = JsValue::null(); - - while let Some(reconstructed) = reconstructed_receiver.next().await { - for msg in reconstructed { - if let Some(ref callback_binary) = on_binary_message { - let arg1 = serde_wasm_bindgen::to_value(&msg.message).unwrap(); - callback_binary - .call1(&this, &arg1) - .expect("on binary message failed!"); - } - if let Some(ref callback) = on_message { - if msg.reply_surb.is_some() { - console_log!("the received message contained a reply-surb that we do not know how to handle (yet)") - } - let stringified = String::from_utf8_lossy(&msg.message).into_owned(); - let arg1 = serde_wasm_bindgen::to_value(&stringified).unwrap(); - callback.call1(&this, &arg1).expect("on message failed!"); - } + let self_address = base_builder.as_mix_recipient().to_string(); + let mut started_client = match base_builder.start_base().await { + Ok(base_client) => base_client, + Err(err) => { + let error_msg = format!("failed to start the base client components - {err}"); + console_error!("{}", error_msg); + let js_error = js_sys::Error::new(&error_msg); + return Err(JsValue::from(js_error)); } - } - }); - } + }; - pub async fn start(mut self) -> NymClient { - console_log!("Starting wasm client '{}'", self.config.id); - // channels for inter-component communication - // TODO: make the channels be internally created by the relevant components - // rather than creating them here, so say for example the buffer controller would create the request channels - // and would allow anyone to clone the sender channel + let client_input = started_client.client_input.register_producer(); + let client_output = started_client.client_output.register_consumer(); - // unwrapped_sphinx_sender is the transmitter of mixnet messages received from the gateway - // unwrapped_sphinx_receiver is the receiver for said messages - used by ReceivedMessagesBuffer - let (mixnet_messages_sender, mixnet_messages_receiver) = mpsc::unbounded(); + Self::start_reconstructed_pusher(client_output, self.on_message); - // used for announcing connection or disconnection of a channel for pushing re-assembled messages to - let (received_buffer_request_sender, received_buffer_request_receiver) = mpsc::unbounded(); - - // channels responsible for controlling real messages - let (input_sender, input_receiver) = tokio::sync::mpsc::channel::(1); - - // channels responsible for controlling ack messages - let (ack_sender, ack_receiver) = mpsc::unbounded(); - let shared_topology_accessor = TopologyAccessor::new(); - - // Channel that the real traffix controller can listed to for closing connections. - // Currently unused in the wasm client. - let (_client_connection_tx, client_connection_rx) = mpsc::unbounded(); - - // the components are started in very specific order. Unless you know what you are doing, - // do not change that. - self.start_topology_refresher(shared_topology_accessor.clone()) - .await; - self.start_received_messages_buffer_controller( - received_buffer_request_receiver, - mixnet_messages_receiver, - ); - - let gateway_client = self - .start_gateway_client(mixnet_messages_sender, ack_sender) - .await; - - // The sphinx_message_sender is the transmitter for any component generating sphinx packets - // that are to be sent to the mixnet. They are used by cover traffic stream and real - // traffic stream. - // The MixTrafficController then sends the actual traffic - let sphinx_message_sender = Self::start_mix_traffic_controller(gateway_client); - - // Shared queue length data. Published by the `OutQueueController` in the client, and used - // primarily to throttle incoming connections - let shared_lane_queue_lengths = LaneQueueLengths::new(); - - self.start_real_traffic_controller( - shared_topology_accessor.clone(), - ack_receiver, - input_receiver, - sphinx_message_sender.clone(), - client_connection_rx, - shared_lane_queue_lengths, - ); - - if !self.config.debug.disable_loop_cover_traffic_stream { - self.start_cover_traffic_stream(shared_topology_accessor, sphinx_message_sender); - } - - self.start_reconstructed_pusher(received_buffer_request_sender); - self.input_tx = Some(input_sender); - - self - } - - // Right now it's impossible to have async exported functions to take `&mut self` rather than mut self - // TODO: try Rc> approach? - pub async fn send_message(self, message: String, recipient: String) -> Self { - console_log!("Sending {} to {}", message, recipient); - - let message_bytes = message.into_bytes(); - self.send_binary_message(message_bytes, recipient).await - } - - pub async fn send_binary_message(self, message: Vec, recipient: String) -> Self { - console_log!("Sending {} bytes to {}", message.len(), recipient); - - let recipient = Recipient::try_from_base58_string(recipient).unwrap(); - let lane = TransmissionLane::General; - - let input_msg = InputMessage::new_fresh(recipient, message, false, lane); - - self.input_tx - .as_ref() - .expect("start method was not called before!") - .send(input_msg) - .await - .expect("InputMessageReceiver has stopped receiving!"); - - self + Ok(JsValue::from(NymClient { + self_address, + client_input: Arc::new(client_input), + _shutdown: started_client.shutdown_notifier, + })) + }) + } +} + +#[wasm_bindgen] +impl NymClient { + pub fn self_address(&self) -> String { + self.self_address.clone() + } + + fn parse_recipient(recipient: &str) -> Result { + match Recipient::try_from_base58_string(recipient) { + Ok(recipient) => Ok(recipient), + Err(err) => { + let error_msg = format!("{recipient} is not a valid Nym network recipient - {err}"); + console_error!("{}", error_msg); + let js_error = js_sys::Error::new(&error_msg); + Err(JsValue::from(js_error)) + } + } + } + + fn parse_sender_tag(tag: &str) -> Result { + match AnonymousSenderTag::try_from_base58_string(tag) { + Ok(tag) => Ok(tag), + Err(err) => { + let error_msg = format!("{tag} is not a valid Nym AnonymousSenderTag - {err}"); + console_error!("{}", error_msg); + let js_error = js_sys::Error::new(&error_msg); + Err(JsValue::from(js_error)) + } + } + } + + /// The simplest message variant where no additional information is attached. + /// You're simply sending your `data` to specified `recipient` without any tagging. + /// + /// Ends up with `NymMessage::Plain` variant + pub fn send_regular_message(&self, message: Vec, recipient: String) -> Promise { + console_log!( + "Attempting to send {:.2} kiB message to {recipient}", + message.len() as f64 / 1024.0 + ); + + let recipient = match Self::parse_recipient(&recipient) { + Ok(recipient) => recipient, + Err(err) => return Promise::reject(&err), + }; + let lane = TransmissionLane::General; + + let input_msg = InputMessage::new_regular(recipient, message, lane); + self.client_input.send_message(input_msg) + } + + /// Creates a message used for a duplex anonymous communication where the recipient + /// will never learn of our true identity. This is achieved by carefully sending `reply_surbs`. + /// + /// Note that if reply_surbs is set to zero then + /// this variant requires the client having sent some reply_surbs in the past + /// (and thus the recipient also knowing our sender tag). + /// + /// Ends up with `NymMessage::Repliable` variant + pub fn send_anonymous_message( + &self, + message: Vec, + recipient: String, + reply_surbs: u32, + ) -> Promise { + console_log!( + "Attempting to anonymously send {:.2} kiB message to {recipient} while attaching {reply_surbs} replySURBs.", + message.len() as f64 / 1024.0 + ); + + let recipient = match Self::parse_recipient(&recipient) { + Ok(recipient) => recipient, + Err(err) => return Promise::reject(&err), + }; + let lane = TransmissionLane::General; + + let input_msg = InputMessage::new_anonymous(recipient, message, reply_surbs, lane); + self.client_input.send_message(input_msg) + } + + /// Attempt to use our internally received and stored `ReplySurb` to send the message back + /// to specified recipient whilst not knowing its full identity (or even gateway). + /// + /// Ends up with `NymMessage::Reply` variant + pub fn send_reply(&self, message: Vec, recipient_tag: String) -> Promise { + console_log!( + "Attempting to send {:.2} kiB reply message to {recipient_tag}", + message.len() as f64 / 1024.0 + ); + + let sender_tag = match Self::parse_sender_tag(&recipient_tag) { + Ok(recipient) => recipient, + Err(err) => return Promise::reject(&err), + }; + let lane = TransmissionLane::General; + + let input_msg = InputMessage::new_reply(sender_tag, message, lane); + self.client_input.send_message(input_msg) } } diff --git a/clients/webassembly/src/client/response_pusher.rs b/clients/webassembly/src/client/response_pusher.rs new file mode 100644 index 0000000000..fbe04c66ed --- /dev/null +++ b/clients/webassembly/src/client/response_pusher.rs @@ -0,0 +1,58 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use client_core::client::base_client::ClientOutput; +use client_core::client::received_buffer::{ReceivedBufferMessage, ReconstructedMessagesReceiver}; +use futures::channel::mpsc; +use futures::StreamExt; +use js_sys::Uint8Array; +use wasm_bindgen::JsValue; +use wasm_bindgen_futures::spawn_local; +use wasm_utils::console_error; + +pub(crate) struct ResponsePusher { + reconstructed_receiver: ReconstructedMessagesReceiver, + on_message: js_sys::Function, +} + +impl ResponsePusher { + pub(crate) fn new(client_output: ClientOutput, on_message: js_sys::Function) -> Self { + // register our output + let (reconstructed_sender, reconstructed_receiver) = mpsc::unbounded(); + + // tell the buffer to start sending stuff to us + client_output + .received_buffer_request_sender + .unbounded_send(ReceivedBufferMessage::ReceiverAnnounce( + reconstructed_sender, + )) + .expect("the buffer request failed!"); + + ResponsePusher { + reconstructed_receiver, + on_message, + } + } + + pub(crate) fn start(mut self) { + spawn_local(async move { + let this = JsValue::null(); + + while let Some(reconstructed) = self.reconstructed_receiver.next().await { + for reconstructed_msg in reconstructed { + let (msg, tag) = reconstructed_msg.into_inner(); + + let msg_slice: &[u8] = &msg; + let array = Uint8Array::from(msg_slice); + let arg1 = JsValue::from(array); + let arg2 = JsValue::from(tag); + self.on_message + .call2(&this, &arg1, &arg2) + .expect("on binary message failed!"); + } + } + + console_error!("we stopped receiving reconstructed messages!") + }) + } +} diff --git a/clients/webassembly/src/gateway_selector.rs b/clients/webassembly/src/gateway_selector.rs index 7e15252b8a..77f5a5c6f7 100644 --- a/clients/webassembly/src/gateway_selector.rs +++ b/clients/webassembly/src/gateway_selector.rs @@ -1,15 +1,15 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use client_core::config::GatewayEndpoint; +use client_core::config::GatewayEndpointConfig; use wasm_bindgen::prelude::*; #[wasm_bindgen] -pub async fn get_gateway(api_server: String, preferred: Option) -> GatewayEndpoint { +pub async fn get_gateway(api_server: String, preferred: Option) -> GatewayEndpointConfig { let validator_client = validator_client::client::ApiClient::new(api_server.parse().unwrap()); let gateways = match validator_client.get_cached_gateways().await { - Err(err) => panic!("failed to obtain list of all gateways - {}", err), + Err(err) => panic!("failed to obtain list of all gateways - {err}"), Ok(gateways) => gateways, }; @@ -18,7 +18,7 @@ pub async fn get_gateway(api_server: String, preferred: Option) -> Gatew .iter() .find(|g| g.gateway.identity_key == preferred) { - return GatewayEndpoint { + return GatewayEndpointConfig { gateway_id: details.gateway.identity_key.clone(), gateway_owner: details.owner.to_string(), gateway_listener: format!( @@ -33,7 +33,7 @@ pub async fn get_gateway(api_server: String, preferred: Option) -> Gatew .first() .expect("current topology holds no gateways"); - GatewayEndpoint { + GatewayEndpointConfig { gateway_id: details.gateway.identity_key.clone(), gateway_owner: details.owner.to_string(), gateway_listener: format!( diff --git a/common/client-connections/src/lib.rs b/common/client-connections/src/lib.rs index 515938ed54..f86270da63 100644 --- a/common/client-connections/src/lib.rs +++ b/common/client-connections/src/lib.rs @@ -10,9 +10,11 @@ pub type ConnectionId = u64; #[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)] pub enum TransmissionLane { General, - Reply, + // we need to treat surb-related requests and responses at higher priority + // so that the rest of underlying communication could actually continue + ReplySurbRequest, + AdditionalReplySurbs, Retransmission, - Control, ConnectionId(ConnectionId), } diff --git a/common/client-libs/gateway-client/Cargo.toml b/common/client-libs/gateway-client/Cargo.toml index 01557331fa..8f1de308d3 100644 --- a/common/client-libs/gateway-client/Cargo.toml +++ b/common/client-libs/gateway-client/Cargo.toml @@ -26,6 +26,7 @@ network-defaults = { path = "../../network-defaults" } nymsphinx = { path = "../../nymsphinx" } pemstore = { path = "../../pemstore" } validator-client = { path = "../validator-client", optional = true } +task = { path = "../../task" } [dependencies.tungstenite] @@ -47,9 +48,6 @@ version = "0.14" [target."cfg(not(target_arch = \"wasm32\"))".dependencies.credential-storage] path = "../../credential-storage" -[target."cfg(not(target_arch = \"wasm32\"))".dependencies.task] -path = "../../task" - # wasm-only dependencies [target."cfg(target_arch = \"wasm32\")".dependencies.wasm-bindgen] version = "0.2" @@ -59,6 +57,7 @@ version = "0.4" [target."cfg(target_arch = \"wasm32\")".dependencies.wasm-utils] path = "../../wasm-utils" +features = ["websocket"] # only import it in wasm. Prefer proper tokio timer in non-wasm [target."cfg(target_arch = \"wasm32\")".dependencies.wasm-timer] diff --git a/common/client-libs/gateway-client/src/bandwidth.rs b/common/client-libs/gateway-client/src/bandwidth.rs index 6f3b7722dc..d6d1cb04f0 100644 --- a/common/client-libs/gateway-client/src/bandwidth.rs +++ b/common/client-libs/gateway-client/src/bandwidth.rs @@ -26,8 +26,15 @@ use { }, }; +// TODO: make it nicer for wasm (I don't want to touch it for this experiment) +#[cfg(target_arch = "wasm32")] +use crate::wasm_storage::PersistentStorage; + +#[cfg(not(target_arch = "wasm32"))] +use credential_storage::PersistentStorage; + #[derive(Clone)] -pub struct BandwidthController { +pub struct BandwidthController { #[allow(dead_code)] storage: St, #[cfg(feature = "coconut")] @@ -54,7 +61,7 @@ where #[cfg(feature = "coconut")] pub async fn prepare_coconut_credential( &self, - ) -> Result { + ) -> Result<(coconut_interface::Credential, i64), GatewayClientError> { let verification_key = obtain_aggregate_verification_key(&self.coconut_api_clients).await?; let bandwidth_credential = self.storage.get_next_coconut_credential().await?; let voucher_value = u64::from_str(&bandwidth_credential.voucher_value) @@ -68,13 +75,21 @@ where coconut_interface::Signature::try_from_bs58(bandwidth_credential.signature)?; // the below would only be executed once we know where we want to spend it (i.e. which gateway and stuff) - Ok(prepare_for_spending( - voucher_value, - voucher_info, - serial_number, - binding_number, - &signature, - &verification_key, - )?) + Ok(( + prepare_for_spending( + voucher_value, + voucher_info, + serial_number, + binding_number, + &signature, + &verification_key, + )?, + bandwidth_credential.id, + )) + } + + #[cfg(feature = "coconut")] + pub async fn consume_credential(&self, id: i64) -> Result<(), GatewayClientError> { + Ok(self.storage.consume_coconut_credential(id).await?) } } diff --git a/common/client-libs/gateway-client/src/client.rs b/common/client-libs/gateway-client/src/client.rs index e6b89c13f9..e1c2654b80 100644 --- a/common/client-libs/gateway-client/src/client.rs +++ b/common/client-libs/gateway-client/src/client.rs @@ -10,7 +10,7 @@ pub use crate::packet_router::{ use crate::socket_state::{PartiallyDelegated, SocketState}; use crate::{cleanup_socket_message, try_decrypt_binary_message}; use crypto::asymmetric::identity; -use futures::{FutureExt, SinkExt, StreamExt}; +use futures::{SinkExt, StreamExt}; use gateway_requests::authentication::encrypted_address::EncryptedAddressBytes; use gateway_requests::iv::IV; use gateway_requests::registration::handshake::{client_handshake, SharedKeys}; @@ -22,6 +22,7 @@ use rand::rngs::OsRng; use std::convert::TryFrom; use std::sync::Arc; use std::time::Duration; +use task::ShutdownListener; use tungstenite::protocol::Message; #[cfg(feature = "coconut")] @@ -30,8 +31,6 @@ use coconut_interface::Credential; #[cfg(not(target_arch = "wasm32"))] use credential_storage::PersistentStorage; #[cfg(not(target_arch = "wasm32"))] -use task::ShutdownListener; -#[cfg(not(target_arch = "wasm32"))] use tokio_tungstenite::connect_async; #[cfg(target_arch = "wasm32")] @@ -67,9 +66,8 @@ pub struct GatewayClient { /// Delay between each subsequent reconnection attempt. reconnection_backoff: Duration, - #[cfg(not(target_arch = "wasm32"))] /// Listen to shutdown messages. - shutdown: Option, + shutdown: ShutdownListener, } impl GatewayClient { @@ -85,7 +83,7 @@ impl GatewayClient { ack_sender: AcknowledgementSender, response_timeout_duration: Duration, bandwidth_controller: Option>, - #[cfg(not(target_arch = "wasm32"))] shutdown: Option, + shutdown: ShutdownListener, ) -> Self { GatewayClient { authenticated: false, @@ -97,18 +95,12 @@ impl GatewayClient { local_identity, shared_key, connection: SocketState::NotConnected, - packet_router: PacketRouter::new( - ack_sender, - mixnet_message_sender, - #[cfg(not(target_arch = "wasm32"))] - shutdown.clone(), - ), + packet_router: PacketRouter::new(ack_sender, mixnet_message_sender, shutdown.clone()), response_timeout_duration, bandwidth_controller, should_reconnect_on_failure: true, reconnection_attempts: DEFAULT_RECONNECTION_ATTEMPTS, reconnection_backoff: DEFAULT_RECONNECTION_BACKOFF, - #[cfg(not(target_arch = "wasm32"))] shutdown, } } @@ -136,7 +128,6 @@ impl GatewayClient { gateway_owner: String, local_identity: Arc, response_timeout_duration: Duration, - #[cfg(not(target_arch = "wasm32"))] shutdown: Option, ) -> Self { use futures::channel::mpsc; @@ -144,12 +135,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 packet_router = PacketRouter::new( - ack_tx, - mix_tx, - #[cfg(not(target_arch = "wasm32"))] - shutdown.clone(), - ); + let shutdown = ShutdownListener::dummy(); + let packet_router = PacketRouter::new(ack_tx, mix_tx, shutdown.clone()); GatewayClient { authenticated: false, @@ -167,7 +154,6 @@ impl GatewayClient { should_reconnect_on_failure: false, reconnection_attempts: DEFAULT_RECONNECTION_ATTEMPTS, reconnection_backoff: DEFAULT_RECONNECTION_BACKOFF, - #[cfg(not(target_arch = "wasm32"))] shutdown, } } @@ -295,44 +281,19 @@ impl GatewayClient { // technically the `wasm_timer` also works outside wasm, but unless required, // I really prefer to just stick to tokio #[cfg(target_arch = "wasm32")] - let timeout = wasm_timer::Delay::new(self.response_timeout_duration); - - let mut fused_timeout = timeout.fuse(); - let mut fused_stream = conn.fuse(); - - // Bit of an ugly workaround for selecting on an `Option` without having access to - // `tokio::select` - #[cfg(not(target_arch = "wasm32"))] - let shutdown = { - let m_shutdown = self.shutdown.clone(); - async { - if let Some(mut s) = m_shutdown { - // TODO: fix this by marking as success _after_ the select - s.mark_as_success(); - s.recv().await - } else { - std::future::pending::<()>().await - } - } - .fuse() - }; - #[cfg(not(target_arch = "wasm32"))] - tokio::pin!(shutdown); - - #[cfg(target_arch = "wasm32")] - let mut shutdown = std::future::pending::<()>().fuse(); + let mut timeout = wasm_timer::Delay::new(self.response_timeout_duration); loop { - futures::select! { - _ = shutdown => { + tokio::select! { + _ = self.shutdown.recv() => { log::trace!("GatewayClient control response: Received shutdown"); log::debug!("GatewayClient control response: Exiting"); break Err(GatewayClientError::ConnectionClosedGatewayShutdown); } - _ = &mut fused_timeout => { + _ = &mut timeout => { break Err(GatewayClientError::Timeout); } - msg = fused_stream.next() => { + msg = conn.next() => { let ws_msg = match cleanup_socket_message(msg) { Err(err) => break Err(err), Ok(msg) => msg @@ -403,13 +364,10 @@ impl GatewayClient { .batch_send_without_response(messages) .await { - error!("failed to batch send messages - {}...", err); + error!("failed to batch send messages - {err}..."); // we must ensure we do not leave the task still active if let Err(err) = self.recover_socket_connection().await { - error!( - "... and the delegated stream has also errored out - {}", - err - ) + error!("... and the delegated stream has also errored out - {err}") } Err(err) } else { @@ -429,13 +387,10 @@ impl GatewayClient { SocketState::Available(ref mut conn) => Ok(conn.send(msg).await?), SocketState::PartiallyDelegated(ref mut partially_delegated) => { if let Err(err) = partially_delegated.send_without_response(msg).await { - error!("failed to send message without response - {}...", err); + error!("failed to send message without response - {err}..."); // we must ensure we do not leave the task still active if let Err(err) = self.recover_socket_connection().await { - error!( - "... and the delegated stream has also errored out - {}", - err - ) + error!("... and the delegated stream has also errored out - {err}") } Err(err) } else { @@ -455,7 +410,7 @@ impl GatewayClient { match gateway_protocol { None => { warn!("the gateway we're connected to has not specified its protocol version. It's probably running version < 1.1.X, but that's still fine for now. It will become a hard error in 1.2.0"); - // note: in 1.2.0 we will have to return a hard error here + // note: in +1.2.0 we will have to return a hard error here Ok(()) } Some(v) if v != PROTOCOL_VERSION => { @@ -632,7 +587,7 @@ impl GatewayClient { let _gateway_owner = self.gateway_owner.clone(); #[cfg(feature = "coconut")] - let credential = self + let (credential, credential_id) = self .bandwidth_controller .as_ref() .unwrap() @@ -642,7 +597,15 @@ impl GatewayClient { return self.try_claim_testnet_bandwidth().await; #[cfg(feature = "coconut")] - return self.claim_coconut_bandwidth(credential).await; + { + self.claim_coconut_bandwidth(credential).await?; + self.bandwidth_controller + .as_ref() + .unwrap() + .consume_credential(credential_id) + .await?; + Ok(()) + } } fn estimate_required_bandwidth(&self, packets: &[MixPacket]) -> i64 { @@ -788,7 +751,6 @@ impl GatewayClient { .as_ref() .expect("no shared key present even though we're authenticated!"), ), - #[cfg(not(target_arch = "wasm32"))] self.shutdown.clone(), ) } diff --git a/common/client-libs/gateway-client/src/lib.rs b/common/client-libs/gateway-client/src/lib.rs index 8671a7363b..ac209f0920 100644 --- a/common/client-libs/gateway-client/src/lib.rs +++ b/common/client-libs/gateway-client/src/lib.rs @@ -16,7 +16,7 @@ pub mod client; pub mod error; pub mod packet_router; pub mod socket_state; -#[cfg(feature = "wasm")] +#[cfg(target_arch = "wasm32")] mod wasm_storage; /// Helper method for reading from websocket stream. Helps to flatten the structure. diff --git a/common/client-libs/gateway-client/src/packet_router.rs b/common/client-libs/gateway-client/src/packet_router.rs index 6732d2202e..fd63077ee6 100644 --- a/common/client-libs/gateway-client/src/packet_router.rs +++ b/common/client-libs/gateway-client/src/packet_router.rs @@ -4,15 +4,13 @@ // JS: I personally don't like this name very much, but could not think of anything better. // I will gladly take any suggestions on how to rename this. +use crate::error::GatewayClientError; use futures::channel::mpsc; use log::*; use nymsphinx::addressing::nodes::MAX_NODE_ADDRESS_UNPADDED_LEN; use nymsphinx::params::packet_sizes::PacketSize; -#[cfg(not(target_arch = "wasm32"))] use task::ShutdownListener; -use crate::error::GatewayClientError; - pub type MixnetMessageSender = mpsc::UnboundedSender>>; pub type MixnetMessageReceiver = mpsc::UnboundedReceiver>>; @@ -23,20 +21,18 @@ pub type AcknowledgementReceiver = mpsc::UnboundedReceiver>>; pub struct PacketRouter { ack_sender: AcknowledgementSender, mixnet_message_sender: MixnetMessageSender, - #[cfg(not(target_arch = "wasm32"))] - shutdown: Option, + shutdown: ShutdownListener, } impl PacketRouter { pub fn new( ack_sender: AcknowledgementSender, mixnet_message_sender: MixnetMessageSender, - #[cfg(not(target_arch = "wasm32"))] shutdown: Option, + shutdown: ShutdownListener, ) -> Self { PacketRouter { ack_sender, mixnet_message_sender, - #[cfg(not(target_arch = "wasm32"))] shutdown, } } @@ -86,13 +82,10 @@ impl PacketRouter { if !received_messages.is_empty() { trace!("routing 'real'"); if let Err(err) = self.mixnet_message_sender.unbounded_send(received_messages) { - #[cfg(not(target_arch = "wasm32"))] - if let Some(shutdown) = &mut self.shutdown { - if shutdown.is_shutdown_poll() { - // This should ideally not happen, but it's ok - log::warn!("Failed to send mixnet message due to receiver task shutdown"); - return Err(GatewayClientError::MixnetMsgSenderFailedToSend); - } + if self.shutdown.is_shutdown_poll() || self.shutdown.is_dummy() { + // This should ideally not happen, but it's ok + log::warn!("Failed to send mixnet message due to receiver task shutdown"); + return Err(GatewayClientError::MixnetMsgSenderFailedToSend); } // This should never happen during ordinary operation the way it's currently used. // Abort to be on the safe side diff --git a/common/client-libs/gateway-client/src/socket_state.rs b/common/client-libs/gateway-client/src/socket_state.rs index 2d66e782e8..aa96b9a4fa 100644 --- a/common/client-libs/gateway-client/src/socket_state.rs +++ b/common/client-libs/gateway-client/src/socket_state.rs @@ -10,7 +10,6 @@ use futures::{SinkExt, StreamExt}; use gateway_requests::registration::handshake::SharedKeys; use log::*; use std::sync::Arc; -#[cfg(not(target_arch = "wasm32"))] use task::ShutdownListener; use tungstenite::Message; @@ -85,7 +84,7 @@ impl PartiallyDelegated { conn: WsConn, packet_router: PacketRouter, shared_key: Arc, - #[cfg(not(target_arch = "wasm32"))] shutdown: Option, + mut shutdown: ShutdownListener, ) -> Self { // when called for, it NEEDS TO yield back the stream so that we could merge it and // read control request responses. @@ -99,27 +98,9 @@ impl PartiallyDelegated { let mut chunk_stream = (&mut stream).ready_chunks(8); let mut packet_router = packet_router; - // Bit of an ugly workaround for selecting on an `Option` without having access to - // `tokio::select` - #[cfg(not(target_arch = "wasm32"))] - let shutdown = { - async { - if let Some(mut s) = shutdown { - s.recv().await - } else { - std::future::pending::<()>().await - } - } - }; - #[cfg(not(target_arch = "wasm32"))] - tokio::pin!(shutdown); - - #[cfg(target_arch = "wasm32")] - let mut shutdown = std::future::pending::<()>(); - let ret_err = loop { tokio::select! { - _ = &mut shutdown => { + _ = shutdown.recv() => { log::trace!("GatewayClient listener: Received shutdown"); log::debug!("GatewayClient listener: Exiting"); return; @@ -142,7 +123,10 @@ impl PartiallyDelegated { if match ret_err { Err(err) => stream_sender.send(Err(err)), - Ok(_) => stream_sender.send(Ok(stream)), + Ok(_) => { + shutdown.mark_as_success(); + stream_sender.send(Ok(stream)) + } } .is_err() { diff --git a/common/client-libs/gateway-client/src/wasm_storage.rs b/common/client-libs/gateway-client/src/wasm_storage.rs index 53a9a1b30e..644560b9ca 100644 --- a/common/client-libs/gateway-client/src/wasm_storage.rs +++ b/common/client-libs/gateway-client/src/wasm_storage.rs @@ -39,7 +39,7 @@ pub trait Storage: Send + Sync { async fn get_next_coconut_credential(&self) -> Result; - async fn remove_coconut_credential(&self, id: i64) -> Result<(), StorageError>; + async fn consume_coconut_credential(&self, id: i64) -> Result<(), StorageError>; } #[async_trait] @@ -59,7 +59,7 @@ impl Storage for PersistentStorage { Err(StorageError::WasmNotSupported) } - async fn remove_coconut_credential(&self, _id: i64) -> Result<(), StorageError> { + async fn consume_coconut_credential(&self, _id: i64) -> Result<(), StorageError> { Err(StorageError::WasmNotSupported) } } diff --git a/common/client-libs/validator-client/src/client.rs b/common/client-libs/validator-client/src/client.rs index 6a218f1fa2..b5746ea06c 100644 --- a/common/client-libs/validator-client/src/client.rs +++ b/common/client-libs/validator-client/src/client.rs @@ -1,20 +1,12 @@ -// Copyright 2021 - Nym Technologies SA +// Copyright 2021-2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 + use crate::{validator_api, ValidatorClientError}; use coconut_dkg_common::types::NodeIndex; -#[cfg(feature = "nymd-client")] -use coconut_dkg_common::{ - dealer::ContractDealing, types::DealerDetails, verification_key::ContractVKShare, -}; -#[cfg(feature = "nymd-client")] -use coconut_interface::Base58; use coconut_interface::VerificationKey; -use mixnet_contract_common::families::{Family, FamilyHead}; use mixnet_contract_common::mixnode::MixNodeDetails; +use mixnet_contract_common::MixId; use mixnet_contract_common::{GatewayBond, IdentityKeyRef}; -use mixnet_contract_common::{IdentityKey, MixId}; -#[cfg(feature = "nymd-client")] -use std::str::FromStr; use validator_api_requests::coconut::{ BlindSignRequestBody, BlindedSignatureResponse, VerifyCredentialBody, VerifyCredentialResponse, }; @@ -28,15 +20,24 @@ use crate::nymd::traits::{DkgQueryClient, MixnetQueryClient, MultisigQueryClient #[cfg(feature = "nymd-client")] use crate::nymd::{self, CosmWasmClient, NymdClient, QueryNymdClient, SigningNymdClient}; #[cfg(feature = "nymd-client")] +use coconut_dkg_common::{ + dealer::ContractDealing, types::DealerDetails, verification_key::ContractVKShare, +}; +#[cfg(feature = "nymd-client")] +use coconut_interface::Base58; +#[cfg(feature = "nymd-client")] use cw3::ProposalResponse; #[cfg(feature = "nymd-client")] use mixnet_contract_common::{ + families::{Family, FamilyHead}, mixnode::MixNodeBond, pending_events::{PendingEpochEvent, PendingIntervalEvent}, - Delegation, RewardedSetNodeStatus, UnbondedMixnode, + Delegation, IdentityKey, RewardedSetNodeStatus, UnbondedMixnode, }; #[cfg(feature = "nymd-client")] use network_defaults::NymNetworkDetails; +#[cfg(feature = "nymd-client")] +use std::str::FromStr; use url::Url; #[cfg(feature = "nymd-client")] use validator_api_requests::models::MixNodeBondAnnotated; diff --git a/common/client-libs/validator-client/src/validator_api/mod.rs b/common/client-libs/validator-client/src/validator_api/mod.rs index 4a7b7af3cd..3cb7ae2ce9 100644 --- a/common/client-libs/validator-client/src/validator_api/mod.rs +++ b/common/client-libs/validator-client/src/validator_api/mod.rs @@ -136,7 +136,12 @@ impl Client { &self, ) -> Result, ValidatorAPIError> { self.query_validator_api( - &[routes::API_VERSION, routes::MIXNODES, routes::DETAILED], + &[ + routes::API_VERSION, + routes::STATUS, + routes::MIXNODES, + routes::DETAILED, + ], NO_PARAMS, ) .await @@ -161,6 +166,7 @@ impl Client { self.query_validator_api( &[ routes::API_VERSION, + routes::STATUS, routes::MIXNODES, routes::ACTIVE, routes::DETAILED, @@ -252,6 +258,7 @@ impl Client { self.query_validator_api( &[ routes::API_VERSION, + routes::STATUS, routes::MIXNODES, routes::REWARDED, routes::DETAILED, diff --git a/common/credential-storage/migrations/20221208120000_add_consumed_to_coconut.sql b/common/credential-storage/migrations/20221208120000_add_consumed_to_coconut.sql new file mode 100644 index 0000000000..f3994cf2b6 --- /dev/null +++ b/common/credential-storage/migrations/20221208120000_add_consumed_to_coconut.sql @@ -0,0 +1,16 @@ +/* + * Copyright 2022 - Nym Technologies SA + * SPDX-License-Identifier: Apache-2.0 + */ + +DROP TABLE coconut_credentials; +CREATE TABLE coconut_credentials +( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + voucher_value TEXT NOT NULL, + voucher_info TEXT NOT NULL, + serial_number TEXT NOT NULL, + binding_number TEXT NOT NULL, + signature TEXT NOT NULL UNIQUE, + consumed BOOLEAN NOT NULL +); \ No newline at end of file diff --git a/common/credential-storage/src/coconut.rs b/common/credential-storage/src/coconut.rs index e6512dd7fc..103f8760d2 100644 --- a/common/credential-storage/src/coconut.rs +++ b/common/credential-storage/src/coconut.rs @@ -36,8 +36,8 @@ impl CoconutCredentialManager { signature: String, ) -> Result<(), sqlx::Error> { sqlx::query!( - "INSERT INTO coconut_credentials(voucher_value, voucher_info, serial_number, binding_number, signature) VALUES (?, ?, ?, ?, ?)", - voucher_value, voucher_info, serial_number, binding_number, signature + "INSERT INTO coconut_credentials(voucher_value, voucher_info, serial_number, binding_number, signature, consumed) VALUES (?, ?, ?, ?, ?, ?)", + voucher_value, voucher_info, serial_number, binding_number, signature, false ) .execute(&self.connection_pool) .await?; @@ -48,20 +48,26 @@ impl CoconutCredentialManager { pub(crate) async fn get_next_coconut_credential( &self, ) -> Result { - sqlx::query_as!(CoconutCredential, "SELECT * FROM coconut_credentials") - .fetch_one(&self.connection_pool) - .await + sqlx::query_as!( + CoconutCredential, + "SELECT * FROM coconut_credentials WHERE NOT consumed" + ) + .fetch_one(&self.connection_pool) + .await } - /// Removes from the database the specified credential. + /// Consumes in the database the specified credential. /// /// # Arguments /// /// * `id`: Database id. - pub(crate) async fn remove_coconut_credential(&self, id: i64) -> Result<(), sqlx::Error> { - sqlx::query!("DELETE FROM coconut_credentials WHERE id = ?", id) - .execute(&self.connection_pool) - .await?; + pub(crate) async fn consume_coconut_credential(&self, id: i64) -> Result<(), sqlx::Error> { + sqlx::query!( + "UPDATE coconut_credentials SET consumed = TRUE WHERE id = ?", + id + ) + .execute(&self.connection_pool) + .await?; Ok(()) } } diff --git a/common/credential-storage/src/lib.rs b/common/credential-storage/src/lib.rs index 7a4ecb5358..3b683026ce 100644 --- a/common/credential-storage/src/lib.rs +++ b/common/credential-storage/src/lib.rs @@ -93,9 +93,9 @@ impl Storage for PersistentStorage { Ok(credential) } - async fn remove_coconut_credential(&self, id: i64) -> Result<(), StorageError> { + async fn consume_coconut_credential(&self, id: i64) -> Result<(), StorageError> { self.coconut_credential_manager - .remove_coconut_credential(id) + .consume_coconut_credential(id) .await?; Ok(()) diff --git a/common/credential-storage/src/models.rs b/common/credential-storage/src/models.rs index 9684df056f..6ce7a9dd4b 100644 --- a/common/credential-storage/src/models.rs +++ b/common/credential-storage/src/models.rs @@ -9,4 +9,5 @@ pub struct CoconutCredential { pub serial_number: String, pub binding_number: String, pub signature: String, + pub consumed: bool, } diff --git a/common/credential-storage/src/storage.rs b/common/credential-storage/src/storage.rs index 892e561442..77df96f4c8 100644 --- a/common/credential-storage/src/storage.rs +++ b/common/credential-storage/src/storage.rs @@ -25,10 +25,10 @@ pub trait Storage: Send + Sync { /// Tries to retrieve one of the stored, unused credentials. async fn get_next_coconut_credential(&self) -> Result; - /// Removes from the database the specified credential. + /// Marks as consumed in the database the specified credential. /// /// # Arguments /// - /// * `signature`: Coconut credential in the form of a signature. - async fn remove_coconut_credential(&self, id: i64) -> Result<(), StorageError>; + /// * `id`: Id of the credential to be consumed. + async fn consume_coconut_credential(&self, id: i64) -> Result<(), StorageError>; } diff --git a/common/crypto/dkg/src/bte/keys.rs b/common/crypto/dkg/src/bte/keys.rs index 9412790db8..098e9638c7 100644 --- a/common/crypto/dkg/src/bte/keys.rs +++ b/common/crypto/dkg/src/bte/keys.rs @@ -53,7 +53,7 @@ impl PublicKey { } } -#[derive(Debug)] +#[derive(Clone, Debug)] #[cfg_attr(test, derive(PartialEq, Eq))] pub struct PublicKeyWithProof { pub(crate) key: PublicKey, diff --git a/common/crypto/dkg/src/bte/proof_discrete_log.rs b/common/crypto/dkg/src/bte/proof_discrete_log.rs index 5dbdbba702..489eb13244 100644 --- a/common/crypto/dkg/src/bte/proof_discrete_log.rs +++ b/common/crypto/dkg/src/bte/proof_discrete_log.rs @@ -13,7 +13,7 @@ use zeroize::Zeroize; const DISCRETE_LOG_DOMAIN: &[u8] = b"NYM_COCONUT_NIDKG_V01_CS01_WITH_BLS12381_XMD:SHA-256_SSWU_RO_PROOF_DISCRETE_LOG"; -#[derive(Debug)] +#[derive(Clone, Debug)] #[cfg_attr(test, derive(PartialEq, Eq))] pub struct ProofOfDiscreteLog { pub(crate) rand_commitment: G1Projective, diff --git a/common/crypto/dkg/src/dealing.rs b/common/crypto/dkg/src/dealing.rs index 817bffdf12..b3633ec82d 100644 --- a/common/crypto/dkg/src/dealing.rs +++ b/common/crypto/dkg/src/dealing.rs @@ -9,19 +9,79 @@ use crate::interpolation::polynomial::{Polynomial, PublicCoefficients}; use crate::interpolation::{ perform_lagrangian_interpolation_at_origin, perform_lagrangian_interpolation_at_x, }; +use crate::utils::deserialize_g2; use crate::{NodeIndex, Share, Threshold}; use bls12_381::{G2Projective, Scalar}; +use group::GroupEncoding; use rand_core::RngCore; use std::collections::BTreeMap; use zeroize::Zeroize; -#[derive(Debug)] +#[derive(Clone, Debug)] #[cfg_attr(test, derive(PartialEq, Eq))] pub struct RecoveredVerificationKeys { pub recovered_master: G2Projective, pub recovered_partials: Vec, } +impl RecoveredVerificationKeys { + pub fn to_bytes(&self) -> Vec { + let partials = self.recovered_partials.len(); + let mut bytes = Vec::with_capacity(96 + 4 + 96 * partials); + bytes.extend_from_slice(self.recovered_master.to_bytes().as_ref()); + bytes.extend_from_slice(&((partials as u32).to_be_bytes())); + for partial in &self.recovered_partials { + bytes.extend_from_slice(partial.to_bytes().as_ref()); + } + + bytes + } + + pub fn try_from_bytes(b: &[u8]) -> Result { + if b.len() < 96 + 4 { + return Err(DkgError::new_deserialization_failure( + "RecoveredVerificationKeys", + "insufficient number of bytes provided", + )); + } + + let recovered_master = deserialize_g2(&b[..96]).ok_or_else(|| { + DkgError::new_deserialization_failure( + "RecoveredVerificationKeys.recovered_master", + "invalid curve point", + ) + })?; + + let partials = u32::from_be_bytes([b[96], b[97], b[98], b[99]]) as usize; + let mut recovered_partials = Vec::with_capacity(partials); + + if b.len() != 96 + 4 + 96 * partials { + return Err(DkgError::new_deserialization_failure( + "RecoveredVerificationKeys", + "insufficient number of bytes provided", + )); + } + + let mut i = 96 + 4; + for _ in 0..partials { + let partial = deserialize_g2(&b[i..i + 96]).ok_or_else(|| { + DkgError::new_deserialization_failure( + "RecoveredVerificationKeys.recovered_partials", + "invalid curve point", + ) + })?; + + recovered_partials.push(partial); + i += 96; + } + + Ok(RecoveredVerificationKeys { + recovered_master, + recovered_partials, + }) + } +} + #[derive(Debug)] #[cfg_attr(test, derive(PartialEq, Eq))] pub struct Dealing { @@ -355,6 +415,17 @@ mod tests { use crate::combine_shares; use rand_core::SeedableRng; + #[test] + fn recovered_verification_keys_serde() { + let keys = RecoveredVerificationKeys { + recovered_master: Default::default(), + recovered_partials: vec![Default::default(), Default::default()], + }; + let bytes = keys.to_bytes(); + let recovered_keys = RecoveredVerificationKeys::try_from_bytes(&bytes).unwrap(); + assert_eq!(keys, recovered_keys); + } + #[test] #[ignore] // expensive test fn recovering_partial_verification_keys() { diff --git a/common/mixnode-common/src/packet_processor/processor.rs b/common/mixnode-common/src/packet_processor/processor.rs index 918e001fec..aaadfe0171 100644 --- a/common/mixnode-common/src/packet_processor/processor.rs +++ b/common/mixnode-common/src/packet_processor/processor.rs @@ -161,7 +161,7 @@ impl SphinxPacketProcessor { ) -> Result { match packet { ProcessedPacket::ForwardHop(packet, address, delay) => { - self.process_forward_hop(packet, address, delay, packet_mode) + self.process_forward_hop(*packet, address, delay, packet_mode) } // right now there's no use for the surb_id included in the header - probably it should get removed from the // sphinx all together? diff --git a/common/network-defaults/src/mainnet.rs b/common/network-defaults/src/mainnet.rs index 59242376f9..ebacd0ff0d 100644 --- a/common/network-defaults/src/mainnet.rs +++ b/common/network-defaults/src/mainnet.rs @@ -9,7 +9,7 @@ pub(crate) const BECH32_PREFIX: &str = "n"; pub const MIX_DENOM: DenomDetails = DenomDetails::new("unym", "nym", 6); pub const STAKE_DENOM: DenomDetails = DenomDetails::new("unyx", "nyx", 6); -pub(crate) const MIXNET_CONTRACT_ADDRESS: &str = +pub const MIXNET_CONTRACT_ADDRESS: &str = "n17srjznxl9dvzdkpwpw24gg668wc73val88a6m5ajg6ankwvz9wtst0cznr"; pub(crate) const VESTING_CONTRACT_ADDRESS: &str = "n1nc5tatafv6eyq7llkr2gv50ff9e22mnf70qgjlv737ktmt4eswrq73f2nw"; @@ -25,7 +25,7 @@ pub(crate) const _ETH_ERC20_CONTRACT_ADDRESS: [u8; 20] = hex_literal::hex!("0000000000000000000000000000000000000000"); pub(crate) const REWARDING_VALIDATOR_ADDRESS: &str = "n10yyd98e2tuwu0f7ypz9dy3hhjw7v772q6287gy"; -pub(crate) const STATISTICS_SERVICE_DOMAIN_ADDRESS: &str = "https://mainnet-stats.nymte.ch:8090/"; +pub const STATISTICS_SERVICE_DOMAIN_ADDRESS: &str = "https://mainnet-stats.nymte.ch:8090/"; pub const NYMD_VALIDATOR: &str = "https://rpc.nymtech.net"; pub const API_VALIDATOR: &str = "https://validator.nymtech.net/api/"; pub(crate) fn validators() -> Vec { diff --git a/common/nymcoconut/src/tests/e2e.rs b/common/nymcoconut/src/tests/e2e.rs index 235b2adbab..db950d0c3f 100644 --- a/common/nymcoconut/src/tests/e2e.rs +++ b/common/nymcoconut/src/tests/e2e.rs @@ -45,6 +45,7 @@ fn keygen() -> Result<(), CoconutError> { } #[test] +#[ignore] // expensive test fn dkg() -> Result<(), CoconutError> { let params = setup(5)?; let node_indices = vec![15u64, 248, 33521]; diff --git a/common/nymsphinx/Cargo.toml b/common/nymsphinx/Cargo.toml index f0f24ac665..d9d81aecd4 100644 --- a/common/nymsphinx/Cargo.toml +++ b/common/nymsphinx/Cargo.toml @@ -9,6 +9,7 @@ edition = "2021" [dependencies] rand = { version = "0.7.3", features = ["wasm-bindgen"] } rand_distr = "0.3" +thiserror = "1.0.37" nymsphinx-acknowledgements = { path = "acknowledgements" } nymsphinx-addressing = { path = "addressing" } diff --git a/common/nymsphinx/acknowledgements/src/surb_ack.rs b/common/nymsphinx/acknowledgements/src/surb_ack.rs index 22bd4bc468..67e863d1f6 100644 --- a/common/nymsphinx/acknowledgements/src/surb_ack.rs +++ b/common/nymsphinx/acknowledgements/src/surb_ack.rs @@ -72,6 +72,10 @@ impl SurbAck { PacketSize::AckPacket.size() + MAX_NODE_ADDRESS_UNPADDED_LEN } + pub fn expected_total_delay(&self) -> Delay { + self.expected_total_delay + } + pub fn prepare_for_sending(self) -> (Delay, Vec) { // SURB_FIRST_HOP || SURB_ACK let surb_bytes: Vec<_> = self diff --git a/common/nymsphinx/addressing/Cargo.toml b/common/nymsphinx/addressing/Cargo.toml index a67849b128..7a85486e4f 100644 --- a/common/nymsphinx/addressing/Cargo.toml +++ b/common/nymsphinx/addressing/Cargo.toml @@ -10,6 +10,7 @@ edition = "2021" crypto = { path = "../../crypto", features = ["asymmetric"] } # all addresses are expressed in terms on their crypto keys nymsphinx-types = { path = "../types" } # we need to be able to refer to some types defined inside sphinx crate serde = "1.0" # implementing serialization/deserialization for some types, like `Recipient` +thiserror = "1.0.37" [dev-dependencies] rand = "0.7" diff --git a/common/nymsphinx/addressing/src/clients.rs b/common/nymsphinx/addressing/src/clients.rs index 819f272efd..fd7235a9b7 100644 --- a/common/nymsphinx/addressing/src/clients.rs +++ b/common/nymsphinx/addressing/src/clients.rs @@ -10,6 +10,7 @@ use nymsphinx_types::Destination; use serde::de::{Error as SerdeError, Unexpected, Visitor}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use std::fmt::{self, Formatter}; +use thiserror::Error; // Not entirely sure whether this is the correct place for those, but let's see how it's going // to work out @@ -19,46 +20,25 @@ const CLIENT_ENCRYPTION_KEY_SIZE: usize = encryption::PUBLIC_KEY_SIZE; pub type ClientIdentity = identity::PublicKey; const CLIENT_IDENTITY_SIZE: usize = identity::PUBLIC_KEY_LENGTH; -#[derive(Debug)] +pub type RecipientBytes = [u8; Recipient::LEN]; + +#[derive(Debug, Error)] pub enum RecipientFormattingError { - MalformedRecipientError, + #[error("recipient is malformed - {reason} ")] + MalformedRecipientError { reason: String }, + + #[error("recipient's identity key is malformed: {0}")] MalformedIdentityError(identity::Ed25519RecoveryError), - MalformedEncryptionKeyError(encryption::KeyRecoveryError), + + #[error("recipient's encryption key is malformed: {0}")] + MalformedEncryptionKeyError(#[from] encryption::KeyRecoveryError), + + #[error("recipient gateway's identity key is malformed: {0}")] MalformedGatewayError(identity::Ed25519RecoveryError), } -impl fmt::Display for RecipientFormattingError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> fmt::Result { - match self { - RecipientFormattingError::MalformedRecipientError => { - write!(f, "recipient is malformed") - } - RecipientFormattingError::MalformedIdentityError(id_err) => { - write!(f, "recipient's identity key is malformed: {}", id_err) - } - RecipientFormattingError::MalformedEncryptionKeyError(enc_err) => { - write!(f, "recipient's encryption key is malformed: {}", enc_err) - } - RecipientFormattingError::MalformedGatewayError(id_err) => write!( - f, - "recipient gateway's identity key is malformed: {}", - id_err - ), - } - } -} - -// since we have Debug and Display might as well slap Error on top of it too -impl std::error::Error for RecipientFormattingError {} - -impl From for RecipientFormattingError { - fn from(err: encryption::KeyRecoveryError) -> Self { - RecipientFormattingError::MalformedEncryptionKeyError(err) - } -} - // TODO: this should a different home... somewhere, but where? -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct Recipient { client_identity: ClientIdentity, client_encryption_key: ClientEncryptionKey, @@ -154,7 +134,7 @@ impl Recipient { &self.gateway } - pub fn to_bytes(self) -> [u8; Self::LEN] { + pub fn to_bytes(self) -> RecipientBytes { let mut out = [0u8; Self::LEN]; out[..CLIENT_IDENTITY_SIZE].copy_from_slice(&self.client_identity.to_bytes()); out[CLIENT_IDENTITY_SIZE..CLIENT_IDENTITY_SIZE + CLIENT_ENCRYPTION_KEY_SIZE] @@ -165,7 +145,7 @@ impl Recipient { out } - pub fn try_from_bytes(bytes: [u8; Self::LEN]) -> Result { + pub fn try_from_bytes(bytes: RecipientBytes) -> Result { let identity_bytes = &bytes[..CLIENT_IDENTITY_SIZE]; let enc_key_bytes = &bytes[CLIENT_IDENTITY_SIZE..CLIENT_IDENTITY_SIZE + CLIENT_ENCRYPTION_KEY_SIZE]; @@ -196,14 +176,20 @@ impl Recipient { let string_address = full_address.into(); let split: Vec<_> = string_address.split('@').collect(); if split.len() != 2 { - return Err(RecipientFormattingError::MalformedRecipientError); + return Err(RecipientFormattingError::MalformedRecipientError { + reason: "the string address does not contain exactly a single '@' character" + .to_string(), + }); } let client_half = split[0]; let gateway_half = split[1]; let split_client: Vec<_> = client_half.split('.').collect(); if split_client.len() != 2 { - return Err(RecipientFormattingError::MalformedRecipientError); + return Err(RecipientFormattingError::MalformedRecipientError { + reason: "the string address does not contain exactly a single '.' character" + .to_string(), + }); } let client_identity = match ClientIdentity::from_base58_string(split_client[0]) { diff --git a/common/nymsphinx/addressing/src/nodes.rs b/common/nymsphinx/addressing/src/nodes.rs index a01f7202bd..cf43625c5a 100644 --- a/common/nymsphinx/addressing/src/nodes.rs +++ b/common/nymsphinx/addressing/src/nodes.rs @@ -5,6 +5,7 @@ use crypto::asymmetric::identity; use nymsphinx_types::{NodeAddressBytes, NODE_ADDRESS_LENGTH}; use std::convert::{TryFrom, TryInto}; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; +use thiserror::Error; // Not entirely sure whether this is the correct place for those, but let's see how it's going // to work out @@ -20,10 +21,23 @@ pub const NODE_IDENTITY_SIZE: usize = identity::PUBLIC_KEY_LENGTH; /// In this case it's an ipv6 socket address (with version prefix) pub const MAX_NODE_ADDRESS_UNPADDED_LEN: usize = 19; -#[derive(Debug)] +#[derive(Debug, Error)] pub enum NymNodeRoutingAddressError { - InsufficientNumberOfBytesAvailableError, - InvalidIpVersion, + #[error("Attempted to deserialize NymNodeRoutingAddress without providing any bytes")] + NoBytesProvided, + + #[error("Provided insufficient amount of few bytes to deserialize a valid NymNodeRoutingAddress for IPv{protocol_version} variant. Received {received} and required {required}")] + TooFewBytesProvided { + protocol_version: u8, + received: usize, + required: usize, + }, + + #[error("{received} is not a valid version of the Internet Protocol (IP). Expected either '4' or '6'")] + InvalidIpVersion { received: u8 }, + + #[error("Could not serialize NymNodeRoutingAddress into NodeAddressBytes as that requires using at least {required} bytes and only {NODE_ADDRESS_LENGTH} are available")] + TooSmallBytesRepresentation { required: usize }, } /// Current representation of Node routing information used in Nym system. @@ -84,28 +98,39 @@ impl NymNodeRoutingAddress { /// Tries to recover `Self` from a bytes slice. /// Does not care if it's zero-padded or not. pub fn try_from_bytes(b: &[u8]) -> Result { - // the bare minimum to represent `Self` is 7 bytes (for the shorter V4 version) - if b.len() < 7 { - return Err(NymNodeRoutingAddressError::InsufficientNumberOfBytesAvailableError); + if b.is_empty() { + return Err(NymNodeRoutingAddressError::NoBytesProvided); } let ip_version = b[0]; - let port: u16 = u16::from_be_bytes([b[1], b[2]]); let ip = match ip_version { - 4 => IpAddr::V4(Ipv4Addr::new(b[3], b[4], b[5], b[6])), + 4 => { + if b.len() < 7 { + return Err(NymNodeRoutingAddressError::TooFewBytesProvided { + protocol_version: 4, + received: b.len(), + required: 7, + }); + } + IpAddr::V4(Ipv4Addr::new(b[3], b[4], b[5], b[6])) + } 6 => { if b.len() < 19 { - return Err( - NymNodeRoutingAddressError::InsufficientNumberOfBytesAvailableError, - ); + return Err(NymNodeRoutingAddressError::TooFewBytesProvided { + protocol_version: 6, + received: b.len(), + required: 19, + }); } let mut address_octets = [0u8; 16]; address_octets.copy_from_slice(&b[3..19]); IpAddr::V6(Ipv6Addr::from(address_octets)) } - _ => return Err(NymNodeRoutingAddressError::InvalidIpVersion), + v => return Err(NymNodeRoutingAddressError::InvalidIpVersion { received: v }), }; + let port: u16 = u16::from_be_bytes([b[1], b[2]]); + Ok(Self(SocketAddr::new(ip, port))) } @@ -148,7 +173,9 @@ impl TryInto for NymNodeRoutingAddress { fn try_into(self) -> Result { // first check if we have enough bytes to represent `self`: if self.bytes_min_len() > NODE_ADDRESS_LENGTH { - return Err(NymNodeRoutingAddressError::InsufficientNumberOfBytesAvailableError); + return Err(NymNodeRoutingAddressError::TooSmallBytesRepresentation { + required: self.bytes_min_len(), + }); } let padded_address = self.as_zero_padded_bytes(NODE_ADDRESS_LENGTH); diff --git a/common/nymsphinx/anonymous-replies/Cargo.toml b/common/nymsphinx/anonymous-replies/Cargo.toml index 246f6de509..5ddb069e19 100644 --- a/common/nymsphinx/anonymous-replies/Cargo.toml +++ b/common/nymsphinx/anonymous-replies/Cargo.toml @@ -7,12 +7,16 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -rand = {version = "0.7.3", features = ["wasm-bindgen"]} +rand = { version = "0.7.3", features = ["wasm-bindgen"] } bs58 = "0.4" serde = "1.0" +thiserror = "1" crypto = { path = "../../crypto" } nymsphinx-addressing = { path = "../addressing" } nymsphinx-params = { path = "../params" } nymsphinx-types = { path = "../types" } topology = { path = "../../topology" } + +[target."cfg(target_arch = \"wasm32\")".dependencies.wasm-bindgen] +version = "0.2.83" \ No newline at end of file diff --git a/common/nymsphinx/anonymous-replies/src/encryption_key.rs b/common/nymsphinx/anonymous-replies/src/encryption_key.rs index b52b8cea60..7a6689f4a7 100644 --- a/common/nymsphinx/anonymous-replies/src/encryption_key.rs +++ b/common/nymsphinx/anonymous-replies/src/encryption_key.rs @@ -16,7 +16,7 @@ pub type EncryptionKeyDigest = pub type SurbEncryptionKeySize = ::KeySize; -#[derive(Clone, Debug)] +#[derive(Clone, Copy, Debug)] pub struct SurbEncryptionKey(CipherKey); #[derive(Debug)] diff --git a/common/nymsphinx/anonymous-replies/src/lib.rs b/common/nymsphinx/anonymous-replies/src/lib.rs index ce93c4099b..3e48d0ea1b 100644 --- a/common/nymsphinx/anonymous-replies/src/lib.rs +++ b/common/nymsphinx/anonymous-replies/src/lib.rs @@ -3,6 +3,7 @@ pub mod encryption_key; pub mod reply_surb; +pub mod requests; pub use encryption_key::{SurbEncryptionKey, SurbEncryptionKeySize}; pub use reply_surb::{ReplySurb, ReplySurbError}; diff --git a/common/nymsphinx/anonymous-replies/src/reply_surb.rs b/common/nymsphinx/anonymous-replies/src/reply_surb.rs index b8b8ec04cf..1fb29b2910 100644 --- a/common/nymsphinx/anonymous-replies/src/reply_surb.rs +++ b/common/nymsphinx/anonymous-replies/src/reply_surb.rs @@ -1,4 +1,4 @@ -// Copyright 2021 - Nym Technologies SA +// Copyright 2021-2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 use crate::encryption_key::{SurbEncryptionKey, SurbEncryptionKeyError, SurbEncryptionKeySize}; @@ -14,44 +14,22 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer}; use std::convert::TryFrom; use std::fmt::{self, Formatter}; use std::time; +use thiserror::Error; use topology::{NymTopology, NymTopologyError}; -#[derive(Debug)] +#[derive(Debug, Error)] pub enum ReplySurbError { + #[error("tried to use reply SURB with an unpadded message")] UnpaddedMessageError, - MalformedStringError(bs58::decode::Error), - RecoveryError(SphinxError), - InvalidEncryptionKeyData(SurbEncryptionKeyError), -} -impl fmt::Display for ReplySurbError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> fmt::Result { - match self { - ReplySurbError::UnpaddedMessageError => { - write!(f, "tried to use reply SURB with an unpadded message") - } - ReplySurbError::MalformedStringError(decode_err) => { - write!(f, "reply SURB is incorrectly formatted: {}", decode_err) - } - ReplySurbError::RecoveryError(sphinx_err) => { - write!(f, "failed to recover reply SURB from bytes: {}", sphinx_err) - } - ReplySurbError::InvalidEncryptionKeyData(surb_key_err) => write!( - f, - "failed to recover reply SURB encryption key from bytes: {}", - surb_key_err - ), - } - } -} + #[error("reply SURB is incorrectly formatted: {0}")] + MalformedStringError(#[from] bs58::decode::Error), -// since we have Debug and Display might as well slap Error on top of it too -impl std::error::Error for ReplySurbError {} + #[error("failed to recover reply SURB from bytes: {0}")] + RecoveryError(#[from] SphinxError), -impl From for ReplySurbError { - fn from(err: SurbEncryptionKeyError) -> Self { - ReplySurbError::InvalidEncryptionKeyData(err) - } + #[error("failed to recover reply SURB encryption key from bytes: {0}")] + InvalidEncryptionKeyData(#[from] SurbEncryptionKeyError), } #[derive(Debug)] @@ -157,6 +135,8 @@ impl ReplySurb { } pub fn from_bytes(bytes: &[u8]) -> Result { + // TODO: introduce bound checks to guard us against out of bound reads + let encryption_key = SurbEncryptionKey::try_from_bytes(&bytes[..SurbEncryptionKeySize::USIZE])?; @@ -189,21 +169,22 @@ impl ReplySurb { // - surb-ack // - key digest // - encrypted plaintext with padding to constant length - pub fn apply_surb( + pub fn apply_surb>( self, - message: &[u8], + message: M, packet_size: Option, ) -> Result<(SphinxPacket, NymNodeRoutingAddress), ReplySurbError> { let packet_size = packet_size.unwrap_or_default(); - if message.len() != packet_size.plaintext_size() { + let message_bytes = message.as_ref(); + if message_bytes.len() != packet_size.plaintext_size() { return Err(ReplySurbError::UnpaddedMessageError); } // this can realistically only fail on too long messages and we just checked for that let (packet, first_hop) = self .surb - .use_surb(message, packet_size.payload_size()) + .use_surb(message_bytes, packet_size.payload_size()) .expect("this error indicates inconsistent message length checking - it shouldn't have happened!"); let first_hop_address = NymNodeRoutingAddress::try_from(first_hop).unwrap(); diff --git a/common/nymsphinx/anonymous-replies/src/requests.rs b/common/nymsphinx/anonymous-replies/src/requests.rs new file mode 100644 index 0000000000..686f65aa79 --- /dev/null +++ b/common/nymsphinx/anonymous-replies/src/requests.rs @@ -0,0 +1,476 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::{ReplySurb, ReplySurbError}; +use nymsphinx_addressing::clients::{Recipient, RecipientFormattingError}; +use rand::{CryptoRng, RngCore}; +use std::fmt::{Display, Formatter}; +use std::mem; +use thiserror::Error; + +#[cfg(target_arch = "wasm32")] +use wasm_bindgen::prelude::*; + +pub const SENDER_TAG_SIZE: usize = 16; + +#[derive(Debug, Error)] +pub enum InvalidAnonymousSenderTagRepresentation { + #[error("Failed to decode the base58-encoded string - {0}")] + MalformedString(#[from] bs58::decode::Error), + + #[error( + "Decoded AnonymousSenderTag has invalid length. Expected {expected}, but got {received}" + )] + InvalidLength { received: usize, expected: usize }, +} + +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +#[cfg_attr(target_arch = "wasm32", wasm_bindgen)] +pub struct AnonymousSenderTag([u8; SENDER_TAG_SIZE]); + +impl From<[u8; SENDER_TAG_SIZE]> for AnonymousSenderTag { + fn from(bytes: [u8; SENDER_TAG_SIZE]) -> Self { + AnonymousSenderTag(bytes) + } +} + +impl Display for AnonymousSenderTag { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.to_base58_string()) + } +} + +impl AnonymousSenderTag { + pub fn new_random(rng: &mut R) -> Self { + let mut bytes = [0u8; SENDER_TAG_SIZE]; + rng.fill_bytes(&mut bytes); + AnonymousSenderTag(bytes) + } + + pub fn to_bytes(&self) -> [u8; SENDER_TAG_SIZE] { + self.0 + } + + pub fn from_bytes(bytes: [u8; SENDER_TAG_SIZE]) -> Self { + AnonymousSenderTag(bytes) + } + + pub fn to_base58_string(self) -> String { + bs58::encode(self.to_bytes()).into_string() + } + + pub fn try_from_base58_string>( + val: I, + ) -> Result { + let bytes = bs58::decode(val).into_vec()?; + if bytes.len() != SENDER_TAG_SIZE { + return Err(InvalidAnonymousSenderTagRepresentation::InvalidLength { + received: bytes.len(), + expected: SENDER_TAG_SIZE, + }); + } + + // the unwrap here is fine as we just asserted the bytes are of exactly SENDER_TAG_SIZE length + let byte_array: [u8; SENDER_TAG_SIZE] = bytes.try_into().unwrap(); + Ok(AnonymousSenderTag::from_bytes(byte_array)) + } +} + +#[derive(Debug, Error)] +pub enum InvalidReplyRequestError { + #[error("Did not provide sufficient number of bytes to deserialize a valid request")] + RequestTooShortToDeserialize, + + #[error("{received} is not a valid content tag for a repliable message")] + InvalidRepliableContentTag { received: u8 }, + + #[error("{received} is not a valid content tag for a reply message")] + InvalidReplyContentTag { received: u8 }, + + #[error("failed to deserialize recipient information - {0}")] + MalformedRecipient(#[from] RecipientFormattingError), + + #[error("failed to deserialize replySURB - {0}")] + MalformedReplySurb(#[from] ReplySurbError), +} + +#[derive(Debug)] +pub struct RepliableMessage { + pub sender_tag: AnonymousSenderTag, + pub content: RepliableMessageContent, +} + +impl Display for RepliableMessage { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match &self.content { + RepliableMessageContent::Data { + message, + reply_surbs, + } => write!( + f, + "repliable {:.2} kiB data message with {} reply surbs attached from {}", + message.len() as f64 / 1024.0, + reply_surbs.len(), + self.sender_tag, + ), + RepliableMessageContent::AdditionalSurbs { reply_surbs } => write!( + f, + "repliable additional surbs message ({} reply surbs attached) from {}", + reply_surbs.len(), + self.sender_tag, + ), + RepliableMessageContent::Heartbeat { + additional_reply_surbs, + } => { + write!( + f, + "repliable heartbeat message ({} reply surbs attached) from {}", + additional_reply_surbs.len(), + self.sender_tag, + ) + } + } + } +} + +impl RepliableMessage { + pub fn new_data( + data: Vec, + sender_tag: AnonymousSenderTag, + reply_surbs: Vec, + ) -> Self { + RepliableMessage { + sender_tag, + content: RepliableMessageContent::Data { + message: data, + reply_surbs, + }, + } + } + + pub fn new_additional_surbs( + sender_tag: AnonymousSenderTag, + reply_surbs: Vec, + ) -> Self { + RepliableMessage { + sender_tag, + content: RepliableMessageContent::AdditionalSurbs { reply_surbs }, + } + } + + pub fn into_bytes(self) -> Vec { + let content_tag = self.content.tag(); + + self.sender_tag + .to_bytes() + .into_iter() + .chain(std::iter::once(content_tag as u8)) + .chain(self.content.into_bytes()) + .collect() + } + + pub fn try_from_bytes( + bytes: &[u8], + num_mix_hops: u8, + ) -> Result { + if bytes.len() < SENDER_TAG_SIZE + 1 { + return Err(InvalidReplyRequestError::RequestTooShortToDeserialize); + } + let sender_tag = + AnonymousSenderTag::from_bytes(bytes[..SENDER_TAG_SIZE].try_into().unwrap()); + let content_tag = RepliableMessageContentTag::try_from(bytes[SENDER_TAG_SIZE])?; + + let content = RepliableMessageContent::try_from_bytes( + &bytes[SENDER_TAG_SIZE + 1..], + num_mix_hops, + content_tag, + )?; + + Ok(RepliableMessage { + sender_tag, + content, + }) + } +} + +// this recovery code is shared between all variants containing reply surbs +fn recover_reply_surbs( + bytes: &[u8], + num_mix_hops: u8, +) -> Result<(Vec, usize), InvalidReplyRequestError> { + let mut consumed = mem::size_of::(); + if bytes.len() < consumed { + return Err(InvalidReplyRequestError::RequestTooShortToDeserialize); + } + let num_surbs = u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]); + let surb_size = ReplySurb::serialized_len(num_mix_hops); + if bytes[consumed..].len() < num_surbs as usize * surb_size { + return Err(InvalidReplyRequestError::RequestTooShortToDeserialize); + } + + let mut reply_surbs = Vec::with_capacity(num_surbs as usize); + for _ in 0..num_surbs as usize { + let surb_bytes = &bytes[consumed..consumed + surb_size]; + let reply_surb = ReplySurb::from_bytes(surb_bytes)?; + reply_surbs.push(reply_surb); + + consumed += surb_size; + } + + Ok((reply_surbs, consumed)) +} + +#[repr(u8)] +enum RepliableMessageContentTag { + Data = 0, + AdditionalSurbs = 1, + Heartbeat = 2, +} + +impl TryFrom for RepliableMessageContentTag { + type Error = InvalidReplyRequestError; + + fn try_from(value: u8) -> Result { + match value { + _ if value == (RepliableMessageContentTag::Data as u8) => Ok(Self::Data), + _ if value == (RepliableMessageContentTag::AdditionalSurbs as u8) => { + Ok(Self::AdditionalSurbs) + } + _ if value == (RepliableMessageContentTag::Heartbeat as u8) => Ok(Self::Heartbeat), + val => Err(InvalidReplyRequestError::InvalidRepliableContentTag { received: val }), + } + } +} + +// sent by original sender that initialised the communication that knows address of the remote +#[derive(Debug)] +pub enum RepliableMessageContent { + Data { + message: Vec, + reply_surbs: Vec, + }, + AdditionalSurbs { + reply_surbs: Vec, + }, + Heartbeat { + additional_reply_surbs: Vec, + }, +} + +impl RepliableMessageContent { + pub fn into_bytes(self) -> Vec { + match self { + RepliableMessageContent::Data { + message, + reply_surbs, + } => { + let num_surbs = reply_surbs.len() as u32; + + num_surbs + .to_be_bytes() + .into_iter() + .chain(reply_surbs.into_iter().flat_map(|s| s.to_bytes())) + .chain(message.into_iter()) + .collect() + } + RepliableMessageContent::AdditionalSurbs { reply_surbs } => { + let num_surbs = reply_surbs.len() as u32; + + num_surbs + .to_be_bytes() + .into_iter() + .chain(reply_surbs.into_iter().flat_map(|s| s.to_bytes())) + .collect() + } + RepliableMessageContent::Heartbeat { + additional_reply_surbs, + } => { + let num_surbs = additional_reply_surbs.len() as u32; + + num_surbs + .to_be_bytes() + .into_iter() + .chain( + additional_reply_surbs + .into_iter() + .flat_map(|s| s.to_bytes()), + ) + .collect() + } + } + } + + fn try_from_bytes( + bytes: &[u8], + num_mix_hops: u8, + tag: RepliableMessageContentTag, + ) -> Result { + if bytes.is_empty() { + return Err(InvalidReplyRequestError::RequestTooShortToDeserialize); + } + + let (reply_surbs, n) = recover_reply_surbs(bytes, num_mix_hops)?; + + match tag { + RepliableMessageContentTag::Data => Ok(RepliableMessageContent::Data { + message: bytes[n..].to_vec(), + reply_surbs, + }), + RepliableMessageContentTag::AdditionalSurbs => { + Ok(RepliableMessageContent::AdditionalSurbs { reply_surbs }) + } + RepliableMessageContentTag::Heartbeat => Ok(RepliableMessageContent::Heartbeat { + additional_reply_surbs: reply_surbs, + }), + } + } + + fn tag(&self) -> RepliableMessageContentTag { + match self { + RepliableMessageContent::Data { .. } => RepliableMessageContentTag::Data, + RepliableMessageContent::AdditionalSurbs { .. } => { + RepliableMessageContentTag::AdditionalSurbs + } + RepliableMessageContent::Heartbeat { .. } => RepliableMessageContentTag::Heartbeat, + } + } +} + +// sent by the remote party who does **NOT** know the original sender's identity +#[derive(Debug)] +pub struct ReplyMessage { + pub content: ReplyMessageContent, +} + +impl Display for ReplyMessage { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match &self.content { + ReplyMessageContent::Data { message } => write!( + f, + "{:.2} kiB reply data message", + message.len() as f64 / 1024.0 + ), + ReplyMessageContent::SurbRequest { recipient, amount } => write!( + f, + "request for {amount} additional reply SURBs from {recipient}", + ), + } + } +} + +impl ReplyMessage { + pub fn new_data_message(message: Vec) -> Self { + ReplyMessage { + content: ReplyMessageContent::Data { message }, + } + } + + pub fn new_surb_request_message(recipient: Recipient, amount: u32) -> Self { + ReplyMessage { + content: ReplyMessageContent::SurbRequest { + recipient: Box::new(recipient), + amount, + }, + } + } + + pub fn into_bytes(self) -> Vec { + let content_tag = self.content.tag(); + + std::iter::once(content_tag as u8) + .chain(self.content.into_bytes()) + .collect() + } + + pub fn try_from_bytes(bytes: &[u8]) -> Result { + if bytes.is_empty() { + return Err(InvalidReplyRequestError::RequestTooShortToDeserialize); + } + let tag = ReplyMessageContentTag::try_from(bytes[0])?; + let content = ReplyMessageContent::try_from_bytes(&bytes[1..], tag)?; + + Ok(ReplyMessage { content }) + } +} + +#[repr(u8)] +enum ReplyMessageContentTag { + Data = 0, + SurbRequest = 1, +} + +impl TryFrom for ReplyMessageContentTag { + type Error = InvalidReplyRequestError; + + fn try_from(value: u8) -> Result { + match value { + _ if value == (ReplyMessageContentTag::Data as u8) => Ok(Self::Data), + _ if value == (ReplyMessageContentTag::SurbRequest as u8) => Ok(Self::SurbRequest), + val => Err(InvalidReplyRequestError::InvalidReplyContentTag { received: val }), + } + } +} + +#[derive(Debug)] +pub enum ReplyMessageContent { + // TODO: later allow to request surbs whilst sending data + Data { + message: Vec, + }, + SurbRequest { + recipient: Box, + amount: u32, + }, +} + +impl ReplyMessageContent { + pub fn into_bytes(self) -> Vec { + match self { + ReplyMessageContent::Data { message } => message, + ReplyMessageContent::SurbRequest { recipient, amount } => recipient + .to_bytes() + .into_iter() + .chain(amount.to_be_bytes().into_iter()) + .collect(), + } + } + + fn try_from_bytes( + bytes: &[u8], + tag: ReplyMessageContentTag, + ) -> Result { + if bytes.is_empty() { + return Err(InvalidReplyRequestError::RequestTooShortToDeserialize); + } + + match tag { + ReplyMessageContentTag::Data => Ok(ReplyMessageContent::Data { + message: bytes.to_vec(), + }), + ReplyMessageContentTag::SurbRequest => { + if bytes.len() != Recipient::LEN + std::mem::size_of::() { + return Err(InvalidReplyRequestError::RequestTooShortToDeserialize); + } + let mut recipient_bytes = [0u8; Recipient::LEN]; + recipient_bytes.copy_from_slice(&bytes[..Recipient::LEN]); + + Ok(ReplyMessageContent::SurbRequest { + recipient: Box::new(Recipient::try_from_bytes(recipient_bytes)?), + amount: u32::from_be_bytes([ + bytes[Recipient::LEN], + bytes[Recipient::LEN + 1], + bytes[Recipient::LEN + 2], + bytes[Recipient::LEN + 3], + ]), + }) + } + } + } + + fn tag(&self) -> ReplyMessageContentTag { + match self { + ReplyMessageContent::Data { .. } => ReplyMessageContentTag::Data, + ReplyMessageContent::SurbRequest { .. } => ReplyMessageContentTag::SurbRequest, + } + } +} diff --git a/common/nymsphinx/chunking/Cargo.toml b/common/nymsphinx/chunking/Cargo.toml index 5f47ab5358..3d6bacab61 100644 --- a/common/nymsphinx/chunking/Cargo.toml +++ b/common/nymsphinx/chunking/Cargo.toml @@ -9,6 +9,7 @@ edition = "2021" [dependencies] log = "0.4.8" rand = { version = "0.7.3", features = ["wasm-bindgen"] } +thiserror = "1.0.37" nymsphinx-addressing = { path = "../addressing" } nymsphinx-params = { path = "../params" } diff --git a/common/nymsphinx/chunking/src/fragment.rs b/common/nymsphinx/chunking/src/fragment.rs index 58c1baa78a..3f102282dd 100644 --- a/common/nymsphinx/chunking/src/fragment.rs +++ b/common/nymsphinx/chunking/src/fragment.rs @@ -1,12 +1,10 @@ -// Copyright 2021 - Nym Technologies SA +// Copyright 2021-2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::set::generate_set_id; use crate::ChunkingError; use nymsphinx_params::{SerializedFragmentIdentifier, FRAG_ID_LEN}; -use rand::Rng; use std::convert::TryInto; -use std::fmt::{self, Formatter}; +use std::fmt::{self, Debug, Formatter}; // Personal reflection: In hindsight I've spent too much time on relatively too little // gain here, as even though I might have saved couple of bytes per packet, the gain @@ -60,7 +58,7 @@ pub const COVER_FRAG_ID: FragmentIdentifier = FragmentIdentifier { /// and u8 position of the `Fragment` in the set. // TODO: this should really be redesigned, especially how cover and reply messages are really // "abusing" this. They should work with it natively instead. -#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq)] +#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq, Ord, PartialOrd)] pub struct FragmentIdentifier { set_id: i32, fragment_position: u8, @@ -77,20 +75,6 @@ impl fmt::Display for FragmentIdentifier { } impl FragmentIdentifier { - // I really dislike how 'hacky' this function seems - // refer to: https://github.com/nymtech/nym/issues/294 for further discussion - pub fn new_reply(rng: &mut R) -> Self { - FragmentIdentifier { - set_id: generate_set_id(rng), - fragment_position: 0, - } - } - - // and this one - pub fn is_reply(self) -> bool { - self.set_id > 0 && self.fragment_position == 0 - } - pub fn to_bytes(self) -> SerializedFragmentIdentifier { debug_assert_eq!(FRAG_ID_LEN, 5); @@ -110,7 +94,7 @@ impl FragmentIdentifier { let set_id = i32::from_be_bytes([b[0], b[1], b[2], b[3]]); // set_id == 0 is valid for COVER_FRAG_ID and replies if set_id < 0 { - return Err(ChunkingError::MalformedFragmentIdentifier); + return Err(ChunkingError::MalformedFragmentIdentifier { received: set_id }); } Ok(FragmentIdentifier { @@ -124,12 +108,22 @@ impl FragmentIdentifier { /// Each `Fragment` after being marshaled is guaranteed to fit into a single sphinx packet. /// The `Fragment` itself consists of part, or whole of, message to be sent as well as additional /// header used to reconstruct the message after being received. -#[derive(PartialEq, Clone, Debug)] +#[derive(PartialEq, Clone)] pub struct Fragment { header: FragmentHeader, payload: Vec, } +// manual implementation to hide detailed payload that we don't care about +impl Debug for Fragment { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.debug_struct("Fragment") + .field("header", &self.header) + .field("payload length", &self.payload.len()) + .finish() + } +} + impl Fragment { /// Tries to encapsulate provided payload slice and metadata into a `Fragment`. /// It can fail if payload would not fully fit in a single `Fragment` or some of the metadata @@ -153,24 +147,42 @@ impl Fragment { // check for whether payload has expected length, which depend on whether fragment is linked // and if it's the only one or the last one in the set (then lower bound is removed) + let max_linked_len = linked_fragment_payload_max_len(max_plaintext_size); + let max_unlinked_len = unlinked_fragment_payload_max_len(max_plaintext_size); + if previous_fragments_set_id.is_some() { if total_fragments > 1 { - if payload.len() != linked_fragment_payload_max_len(max_plaintext_size) { - return Err(ChunkingError::InvalidPayloadLengthError); + if payload.len() != max_linked_len { + return Err(ChunkingError::InvalidPayloadLengthError { + received: payload.len(), + expected: max_linked_len, + }); } - } else if payload.len() > linked_fragment_payload_max_len(max_plaintext_size) { - return Err(ChunkingError::InvalidPayloadLengthError); + } else if payload.len() > max_linked_len { + return Err(ChunkingError::TooLongPayloadLengthError { + received: payload.len(), + expected_at_most: max_linked_len, + }); } } else if next_fragments_set_id.is_some() { - if payload.len() != linked_fragment_payload_max_len(max_plaintext_size) { - return Err(ChunkingError::InvalidPayloadLengthError); + if payload.len() != max_linked_len { + return Err(ChunkingError::InvalidPayloadLengthError { + received: payload.len(), + expected: max_linked_len, + }); } } else if total_fragments != current_fragment { - if payload.len() != unlinked_fragment_payload_max_len(max_plaintext_size) { - return Err(ChunkingError::InvalidPayloadLengthError); + if payload.len() != max_unlinked_len { + return Err(ChunkingError::InvalidPayloadLengthError { + received: payload.len(), + expected: max_unlinked_len, + }); } - } else if payload.len() > unlinked_fragment_payload_max_len(max_plaintext_size) { - return Err(ChunkingError::InvalidPayloadLengthError); + } else if payload.len() > max_unlinked_len { + return Err(ChunkingError::TooLongPayloadLengthError { + received: payload.len(), + expected_at_most: max_unlinked_len, + }); } Ok(Fragment { @@ -347,7 +359,10 @@ impl FragmentHeader { fn try_from_bytes(b: &[u8]) -> Result<(Self, usize), ChunkingError> { // header needs to be at least 7 bytes long if b.len() < UNLINKED_FRAGMENTED_HEADER_LEN { - return Err(ChunkingError::TooShortFragmentData); + return Err(ChunkingError::TooShortFragmentHeader { + received: b.len(), + expected: UNLINKED_FRAGMENTED_HEADER_LEN, + }); } let frag_id = i32::from_be_bytes(b[0..4].try_into().unwrap()); // sanity check for the fragmentation flag @@ -370,7 +385,10 @@ impl FragmentHeader { let read_bytes = if b[6] != 0 { // there's linking ID supposedly attached, make sure we have enough bytes to parse if b.len() < LINKED_FRAGMENTED_HEADER_LEN { - return Err(ChunkingError::TooShortFragmentData); + return Err(ChunkingError::TooShortFragmentHeader { + received: b.len(), + expected: LINKED_FRAGMENTED_HEADER_LEN, + }); } let flagged_linked_id = i32::from_be_bytes(b[6..10].try_into().unwrap()); diff --git a/common/nymsphinx/chunking/src/lib.rs b/common/nymsphinx/chunking/src/lib.rs index 62a246955b..60316edb15 100644 --- a/common/nymsphinx/chunking/src/lib.rs +++ b/common/nymsphinx/chunking/src/lib.rs @@ -3,6 +3,7 @@ use crate::fragment::{linked_fragment_payload_max_len, unlinked_fragment_payload_max_len}; pub use set::split_into_sets; +use thiserror::Error; // Future consideration: currently in a lot of places, the payloads have randomised content // which is not a perfect testing strategy as it might not detect some edge cases I never would @@ -45,18 +46,26 @@ pub mod set; /// Both of those concepts as well as their structures, i.e. `Set` and `Fragment` /// are further explained in the respective files. -#[derive(PartialEq, Eq, Debug)] +#[derive(PartialEq, Eq, Debug, Error)] pub enum ChunkingError { - InvalidPayloadLengthError, - TooBigMessageToSplit, + #[error("Received payload is too long. Got {received}, expected {expected}")] + InvalidPayloadLengthError { received: usize, expected: usize }, + + #[error("Received payload is too long. Got {received}, expected at most {expected_at_most}")] + TooLongPayloadLengthError { + received: usize, + expected_at_most: usize, + }, + + // this should really be split into multiple variants to provide better error information + #[error("Provided header was malformed or contained self-contradicting fields")] MalformedHeaderError, - NoValidProvidersError, - NoValidRoutesAvailableError, - InvalidTopologyError, - TooShortFragmentData, - MalformedFragmentData, - UnexpectedFragmentCount, - MalformedFragmentIdentifier, + + #[error("Received too few bytes to deserialize fragment header. Got {received}, expected {expected}")] + TooShortFragmentHeader { received: usize, expected: usize }, + + #[error("Received fragment identifier ({received}) is not a valid value!")] + MalformedFragmentIdentifier { received: i32 }, } /// Returns number of fragments the message will be split to as well as number of available @@ -94,7 +103,7 @@ pub fn number_of_required_fragments( // we know for sure that all fragments in all but last set are definitely full // (last one has single 'linked' fragment) - let without_last = (n - 1) * (u8::max_value() as usize); + let without_last = (n - 1) * (u8::MAX as usize); let linked_fragments_without_last = (2 * n - 2) - 1; let unlinked_fragments_without_last = without_last - linked_fragments_without_last; diff --git a/common/nymsphinx/cover/Cargo.toml b/common/nymsphinx/cover/Cargo.toml index 6fd629ade7..064ebf1298 100644 --- a/common/nymsphinx/cover/Cargo.toml +++ b/common/nymsphinx/cover/Cargo.toml @@ -8,6 +8,7 @@ edition = "2021" [dependencies] rand = { version = "0.7.3", features = ["wasm-bindgen"] } +thiserror = "1.0.37" crypto = { path = "../../crypto" } nymsphinx-acknowledgements = { path = "../acknowledgements" } diff --git a/common/nymsphinx/cover/src/lib.rs b/common/nymsphinx/cover/src/lib.rs index aaaff5a9b5..2ce9454d5c 100644 --- a/common/nymsphinx/cover/src/lib.rs +++ b/common/nymsphinx/cover/src/lib.rs @@ -6,7 +6,7 @@ use crypto::symmetric::stream_cipher; use nymsphinx_acknowledgements::surb_ack::SurbAck; use nymsphinx_acknowledgements::AckKey; use nymsphinx_addressing::clients::Recipient; -use nymsphinx_addressing::nodes::{NymNodeRoutingAddress, NymNodeRoutingAddressError}; +use nymsphinx_addressing::nodes::NymNodeRoutingAddress; use nymsphinx_chunking::fragment::COVER_FRAG_ID; use nymsphinx_forwarding::packet::MixPacket; use nymsphinx_params::packet_sizes::PacketSize; @@ -18,35 +18,18 @@ use nymsphinx_types::{delays, Error as SphinxError}; use rand::{CryptoRng, RngCore}; use std::convert::TryFrom; use std::time; +use thiserror::Error; use topology::{NymTopology, NymTopologyError}; pub const LOOP_COVER_MESSAGE_PAYLOAD: &[u8] = b"The cake is a lie!"; -#[derive(Debug)] +#[derive(Debug, Error)] pub enum CoverMessageError { - NoValidProvidersError, - InvalidTopologyError, - SphinxError(SphinxError), - InvalidFirstMixAddress, -} + #[error("Could not construct cover message due to invalid topology - {0}")] + InvalidTopologyError(#[from] NymTopologyError), -impl From for CoverMessageError { - fn from(err: SphinxError) -> Self { - CoverMessageError::SphinxError(err) - } -} - -impl From for CoverMessageError { - fn from(_: NymNodeRoutingAddressError) -> Self { - use CoverMessageError::*; - InvalidFirstMixAddress - } -} - -impl From for CoverMessageError { - fn from(_: NymTopologyError) -> Self { - CoverMessageError::InvalidTopologyError - } + #[error("Could not construct a valid sphinx packet - {0}")] + SphinxError(#[from] SphinxError), } pub fn generate_loop_cover_surb_ack( diff --git a/common/nymsphinx/framing/src/codec.rs b/common/nymsphinx/framing/src/codec.rs index 53bd77c3b7..5794a88544 100644 --- a/common/nymsphinx/framing/src/codec.rs +++ b/common/nymsphinx/framing/src/codec.rs @@ -178,7 +178,7 @@ mod packet_encoding { ]; SphinxPacketBuilder::new() .with_payload_size(size.payload_size()) - .build_packet(b"foomp".to_vec(), &route, &destination, &delays) + .build_packet(b"foomp", &route, &destination, &delays) .unwrap() } diff --git a/common/nymsphinx/src/lib.rs b/common/nymsphinx/src/lib.rs index e3e7c51fac..bbc69c2682 100644 --- a/common/nymsphinx/src/lib.rs +++ b/common/nymsphinx/src/lib.rs @@ -1,6 +1,7 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +pub mod message; pub mod preparer; pub mod receiver; pub mod utils; @@ -16,3 +17,6 @@ pub use nymsphinx_forwarding as forwarding; pub use nymsphinx_framing as framing; pub use nymsphinx_params as params; pub use nymsphinx_types::*; + +// TEMP UNTIL FURTHER REFACTORING +pub use preparer::payload::NymsphinxPayloadBuilder; diff --git a/common/nymsphinx/src/message.rs b/common/nymsphinx/src/message.rs new file mode 100644 index 0000000000..f9b32daa35 --- /dev/null +++ b/common/nymsphinx/src/message.rs @@ -0,0 +1,238 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::chunking; +use crypto::asymmetric::encryption; +use crypto::Digest; +use nymsphinx_addressing::clients::Recipient; +use nymsphinx_addressing::nodes::MAX_NODE_ADDRESS_UNPADDED_LEN; +use nymsphinx_anonymous_replies::requests::{ + InvalidReplyRequestError, RepliableMessage, RepliableMessageContent, ReplyMessage, + ReplyMessageContent, +}; +use nymsphinx_chunking::fragment::Fragment; +use nymsphinx_params::{PacketSize, ReplySurbKeyDigestAlgorithm}; +use rand::Rng; +use std::fmt::{Display, Formatter}; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum NymMessageError { + #[error("{received} is not a valid type tag for a NymMessage")] + InvalidMessageType { received: u8 }, + + #[error(transparent)] + InvalidReplyRequest(#[from] InvalidReplyRequestError), + + #[error("The received message seems to have incorrect zero padding (no '1' byte found)")] + InvalidMessagePadding, + + #[error("Received empty message for deserialization")] + EmptyMessage, +} + +#[repr(u8)] +enum NymMessageType { + Plain = 0, + Repliable = 1, + Reply = 2, +} + +impl TryFrom for NymMessageType { + type Error = NymMessageError; + + fn try_from(value: u8) -> Result { + match value { + _ if value == (NymMessageType::Plain as u8) => Ok(Self::Plain), + _ if value == (NymMessageType::Repliable as u8) => Ok(Self::Repliable), + _ if value == (NymMessageType::Reply as u8) => Ok(Self::Reply), + val => Err(NymMessageError::InvalidMessageType { received: val }), + } + } +} + +pub type PlainMessage = Vec; + +#[derive(Debug)] +pub enum NymMessage { + Plain(PlainMessage), + Repliable(RepliableMessage), + Reply(ReplyMessage), +} + +impl Display for NymMessage { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + NymMessage::Plain(plain_message) => write!( + f, + "plain {:.2} kiB message", + plain_message.len() as f64 / 1024.0 + ), + NymMessage::Repliable(repliable_message) => repliable_message.fmt(f), + NymMessage::Reply(reply_message) => reply_message.fmt(f), + } + } +} + +impl NymMessage { + pub fn new_additional_surbs_request(recipient: Recipient, amount: u32) -> Self { + NymMessage::Reply(ReplyMessage { + content: ReplyMessageContent::SurbRequest { + recipient: Box::new(recipient), + amount, + }, + }) + } + + pub fn new_plain(msg: Vec) -> Self { + NymMessage::Plain(msg) + } + + pub fn new_repliable(msg: RepliableMessage) -> Self { + NymMessage::Repliable(msg) + } + + pub fn new_reply(msg: ReplyMessage) -> Self { + NymMessage::Reply(msg) + } + + pub fn is_reply_surb_request(&self) -> bool { + match self { + NymMessage::Reply(reply_msg) => { + matches!(reply_msg.content, ReplyMessageContent::SurbRequest { .. }) + } + _ => false, + } + } + + pub fn into_inner_data(self) -> Vec { + match self { + NymMessage::Plain(data) => data, + NymMessage::Repliable(repliable) => match repliable.content { + RepliableMessageContent::Data { message, .. } => message, + _ => Vec::new(), + }, + NymMessage::Reply(reply) => match reply.content { + ReplyMessageContent::Data { message } => message, + _ => Vec::new(), + }, + } + } + + fn typ(&self) -> NymMessageType { + match self { + NymMessage::Plain(_) => NymMessageType::Plain, + NymMessage::Repliable(_) => NymMessageType::Repliable, + NymMessage::Reply(_) => NymMessageType::Reply, + } + } + + fn inner_bytes(self) -> Vec { + match self { + NymMessage::Plain(msg) => msg, + NymMessage::Repliable(msg) => msg.into_bytes(), + NymMessage::Reply(msg) => msg.into_bytes(), + } + } + + // the message is in the format of: + // typ || msg + fn into_bytes(self) -> Vec { + let typ = self.typ(); + + std::iter::once(typ as u8) + .chain(self.inner_bytes()) + .collect() + } + + fn try_from_bytes(bytes: &[u8], num_mix_hops: u8) -> Result { + if bytes.is_empty() { + return Err(NymMessageError::EmptyMessage); + } + + let typ_tag = NymMessageType::try_from(bytes[0])?; + match typ_tag { + NymMessageType::Plain => Ok(NymMessage::Plain(bytes[1..].to_vec())), + NymMessageType::Repliable => Ok(NymMessage::Repliable( + RepliableMessage::try_from_bytes(&bytes[1..], num_mix_hops)?, + )), + NymMessageType::Reply => Ok(NymMessage::Reply(ReplyMessage::try_from_bytes( + &bytes[1..], + )?)), + } + } + + /// Length of plaintext (from the sphinx point of view) data that is available per sphinx + /// packet. + pub fn available_plaintext_per_packet(&self, packet_size: PacketSize) -> usize { + let ack_overhead = MAX_NODE_ADDRESS_UNPADDED_LEN + PacketSize::AckPacket.size(); + + let variant_overhead = match self { + // each plain or repliable packet attaches an ephemeral public key so that the recipient + // could perform diffie-hellman with its own keys followed by a kdf to re-derive + // the packet encryption key + NymMessage::Plain(_) | NymMessage::Repliable(_) => encryption::PUBLIC_KEY_SIZE, + // each reply attaches the digest of the encryption key so that the recipient could + // lookup correct key for decryption, + NymMessage::Reply(_) => ReplySurbKeyDigestAlgorithm::output_size(), + }; + + packet_size.plaintext_size() - ack_overhead - variant_overhead + } + + /// Pads the message so that after it gets chunked, it will occupy exactly N sphinx packets. + /// Produces new_message = message || 1 || 0000.... + pub fn pad_to_full_packet_lengths(self, plaintext_per_packet: usize) -> PaddedMessage { + let bytes = self.into_bytes(); + + // 1 is added as there will always have to be at least a single byte of padding (1) added + // to be able to later distinguish the actual padding from the underlying message + let (_, space_left) = + chunking::number_of_required_fragments(bytes.len() + 1, plaintext_per_packet); + + bytes + .into_iter() + .chain(std::iter::once(1u8)) + .chain(std::iter::repeat(0u8).take(space_left)) + .collect::>() + .into() + } +} + +pub struct PaddedMessage(Vec); + +impl PaddedMessage { + pub fn new_reconstructed(bytes: Vec) -> Self { + PaddedMessage(bytes) + } + + /// Splits the padded message into [`Fragment`] that when serialized are going to become + /// sphinx packet payloads. + pub fn split_into_fragments( + self, + rng: &mut R, + plaintext_per_packet: usize, + ) -> Vec { + chunking::split_into_sets(rng, &self.0, plaintext_per_packet) + .into_iter() + .flat_map(|fragment_set| fragment_set.into_iter()) + .collect() + } + + // reverse of NymMessage::pad_to_full_packet_lengths + pub fn remove_padding(self, num_mix_hops: u8) -> Result { + // we are looking for first occurrence of 1 in the tail and we get its index + if let Some(padding_end) = self.0.iter().rposition(|b| *b == 1) { + // and now we only take bytes until that point (but not including it) + NymMessage::try_from_bytes(&self.0[..padding_end], num_mix_hops) + } else { + Err(NymMessageError::InvalidMessagePadding) + } + } +} + +impl From> for PaddedMessage { + fn from(bytes: Vec) -> Self { + PaddedMessage(bytes) + } +} diff --git a/common/nymsphinx/src/preparer/mod.rs b/common/nymsphinx/src/preparer/mod.rs index d91a7c85a5..19f68e30bc 100644 --- a/common/nymsphinx/src/preparer/mod.rs +++ b/common/nymsphinx/src/preparer/mod.rs @@ -1,24 +1,17 @@ -// Copyright 2021 - Nym Technologies SA +// Copyright 2021-2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::chunking; -use crypto::asymmetric::encryption; -use crypto::shared_key::new_ephemeral_shared_key; -use crypto::symmetric::stream_cipher; -use crypto::Digest; +use crate::message::NymMessage; +use crate::NymsphinxPayloadBuilder; use nymsphinx_acknowledgements::surb_ack::SurbAck; use nymsphinx_acknowledgements::AckKey; use nymsphinx_addressing::clients::Recipient; -use nymsphinx_addressing::nodes::{NymNodeRoutingAddress, MAX_NODE_ADDRESS_UNPADDED_LEN}; -use nymsphinx_anonymous_replies::encryption_key::SurbEncryptionKey; +use nymsphinx_addressing::nodes::NymNodeRoutingAddress; use nymsphinx_anonymous_replies::reply_surb::ReplySurb; use nymsphinx_chunking::fragment::{Fragment, FragmentIdentifier}; use nymsphinx_forwarding::packet::MixPacket; use nymsphinx_params::packet_sizes::PacketSize; -use nymsphinx_params::{ - PacketEncryptionAlgorithm, PacketHkdfAlgorithm, ReplySurbEncryptionAlgorithm, - ReplySurbKeyDigestAlgorithm, DEFAULT_NUM_MIX_HOPS, -}; +use nymsphinx_params::DEFAULT_NUM_MIX_HOPS; use nymsphinx_types::builder::SphinxPacketBuilder; use nymsphinx_types::{delays, Delay}; use rand::{CryptoRng, Rng}; @@ -26,6 +19,8 @@ use std::convert::TryFrom; use std::time::Duration; use topology::{NymTopology, NymTopologyError}; +pub(crate) mod payload; + /// Represents fully packed and prepared [`Fragment`] that can be sent through the mix network. pub struct PreparedFragment { /// Indicates the total expected round-trip time, i.e. delay from the sending of this message @@ -36,27 +31,17 @@ pub struct PreparedFragment { /// address of the node to which the message should be sent, the actual 'chunk' of the message /// going through the mix network and also the 'mode' of the packet, i.e. VPN or Mix. pub mix_packet: MixPacket, -} -#[derive(Debug)] -pub enum PreparationError { - TopologyError(NymTopologyError), - TooLongReplyMessageError, -} - -impl From for PreparationError { - fn from(err: NymTopologyError) -> Self { - PreparationError::TopologyError(err) - } + /// Identifier to uniquely identify a fragment. + pub fragment_identifier: FragmentIdentifier, } /// Prepares the message that is to be sent through the mix network by attaching /// an optional reply-SURB, padding it to appropriate length, encrypting its content, /// and chunking into appropriate size [`Fragment`]s. -// #[cfg_attr(not(target_arch = "wasm32"), derive(Clone))] #[derive(Clone)] #[must_use] -pub struct MessagePreparer { +pub struct MessagePreparer { /// Instance of a cryptographically secure random number generator. rng: R, @@ -115,86 +100,70 @@ where self.sender_address = sender_address; } - /// Length of plaintext (from the sphinx point of view) data that is available per sphinx - /// packet. - fn available_plaintext_per_packet(&self) -> usize { - // we need to put first hop's destination alongside the actual ack data - // TODO: a possible optimization way down the line: currently we're always assuming that - // the addresses will have `MAX_NODE_ADDRESS_UNPADDED_LEN`, i.e. be ipv6. In most cases - // they're actually going to be ipv4 hence wasting few bytes every packet. - // To fully utilise all available space, I guess first we'd need to generate routes for ACKs - // and only then perform the chunking with `available_plaintext_size` being called per chunk. - // However this will probably introduce bunch of complexity - // for relatively not a lot of gain, so it shouldn't be done just yet. - let ack_overhead = MAX_NODE_ADDRESS_UNPADDED_LEN + PacketSize::AckPacket.size(); - let ephemeral_public_key_overhead = encryption::PUBLIC_KEY_SIZE; - - self.packet_size.plaintext_size() - ack_overhead - ephemeral_public_key_overhead - } - - /// Pads the message so that after it gets chunked, it will occupy exactly N sphinx packets. - /// Produces new_message = message || 1 || 0000.... - fn pad_message(&self, message: Vec) -> Vec { - // 1 is added as there will always have to be at least a single byte of padding (1) added - // to be able to later distinguish the actual padding from the underlying message - let (_, space_left) = chunking::number_of_required_fragments( - message.len() + 1, - self.available_plaintext_per_packet(), - ); - - message - .into_iter() - .chain(std::iter::once(1u8)) - .chain(std::iter::repeat(0u8).take(space_left)) - .collect() - } - - /// Attaches reply-SURB to the message alongside the reply key. - /// Results in: - /// new_message = 0 || message - /// OR - /// new_message = 1 || REPLY_KEY || REPLY_SURB || message - fn optionally_attach_reply_surb( + pub fn generate_reply_surbs( &mut self, - message: Vec, - should_attach: bool, + amount: usize, topology: &NymTopology, - ) -> Result<(Vec, Option), PreparationError> { - if should_attach { + ) -> Result, NymTopologyError> { + let mut reply_surbs = Vec::with_capacity(amount); + for _ in 0..amount { let reply_surb = ReplySurb::construct( &mut self.rng, &self.sender_address, self.average_packet_delay, topology, )?; - - let reply_key = reply_surb.encryption_key(); - // if there's a reply surb, the message takes form of `1 || REPLY_KEY || REPLY_SURB || MSG` - Ok(( - std::iter::once(true as u8) - .chain(reply_surb.to_bytes().iter().cloned()) - .chain(message.into_iter()) - .collect(), - Some(reply_key.clone()), - )) - } else { - // but if there's no reply surb, the message takes form of `0 || MSG` - Ok(( - std::iter::once(false as u8) - .chain(message.into_iter()) - .collect(), - None, - )) + reply_surbs.push(reply_surb) } + + Ok(reply_surbs) } - /// Splits the message into [`Fragment`] that are going to be put later put into sphinx packets. - fn split_message(&mut self, message: Vec) -> Vec { - let plaintext_per_packet = self.available_plaintext_per_packet(); - chunking::split_into_sets(&mut self.rng, &message, plaintext_per_packet) - .into_iter() - .flat_map(|fragment_set| fragment_set.into_iter()) - .collect() + /// The procedure is as follows: + /// For each fragment: + /// - compute SURB_ACK + /// - generate (x, g^x) + /// - obtain key k from the reply-surb which was computed as follows: + /// k = KDF(remote encryption key ^ x) this is equivalent to KDF( dh(remote, x) ) + /// - compute v_b = AES-128-CTR(k, serialized_fragment) + /// - compute vk_b = H(k) || v_b + /// - compute sphinx_plaintext = SURB_ACK || H(k) || v_b + /// - compute sphinx_packet by applying the reply surb on the sphinx_plaintext + pub fn prepare_reply_chunk_for_sending( + &mut self, + fragment: Fragment, + topology: &NymTopology, + ack_key: &AckKey, + reply_surb: ReplySurb, + ) -> Result { + // this is not going to be accurate by any means. but that's the best estimation we can do + let expected_forward_delay = Delay::new_from_millis( + (self.average_packet_delay.as_millis() * self.num_mix_hops as u128) as u64, + ); + + let fragment_identifier = fragment.fragment_identifier(); + + // create an ack + let surb_ack = self.generate_surb_ack(fragment_identifier, topology, ack_key)?; + let ack_delay = surb_ack.expected_total_delay(); + + let packet_payload = NymsphinxPayloadBuilder::new(fragment, surb_ack) + .build_reply(reply_surb.encryption_key()); + + // the unwrap here is fine as the failures can only originate from attempting to use invalid payload lenghts + // and we just very carefully constructed a (presumably) valid one + let (sphinx_packet, first_hop_address) = reply_surb + .apply_surb(packet_payload, Some(self.packet_size)) + .unwrap(); + + Ok(PreparedFragment { + // the round-trip delay is the sum of delays of all hops on the forward route as + // well as the total delay of the ack packet. + // we don't know the delays inside the reply surbs so we use best-effort estimation from our poisson distribution + total_delay: expected_forward_delay + ack_delay, + mix_packet: MixPacket::new(first_hop_address, sphinx_packet, Default::default()), + fragment_identifier, + }) } /// Tries to convert this [`Fragment`] into a [`SphinxPacket`] that can be sent through the Nym mix-network, @@ -221,46 +190,14 @@ where ack_key: &AckKey, packet_recipient: &Recipient, ) -> Result { + let fragment_identifier = fragment.fragment_identifier(); + // create an ack - let (ack_delay, surb_ack_bytes) = self - .generate_surb_ack(fragment.fragment_identifier(), topology, ack_key)? - .prepare_for_sending(); + let surb_ack = self.generate_surb_ack(fragment_identifier, topology, ack_key)?; + let ack_delay = surb_ack.expected_total_delay(); - // TODO: - // TODO: - // TODO: - // TODO: - // TODO: ASK @AP AND @DH WHETHER THOSE KEYS CAN/SHOULD ALSO BE REUSED IN VPN MODE!! - // TODO: - // TODO: - // TODO: - // TODO: - - // create keys for 'payload' encryption - let (ephemeral_keypair, shared_key) = - new_ephemeral_shared_key::( - &mut self.rng, - packet_recipient.encryption_key(), - ); - - // serialize fragment and encrypt its content - let mut chunk_data = fragment.into_bytes(); - - let zero_iv = stream_cipher::zero_iv::(); - stream_cipher::encrypt_in_place::( - &shared_key, - &zero_iv, - &mut chunk_data, - ); - - // combine it together as follows: - // SURB_ACK_FIRST_HOP || SURB_ACK_DATA || EPHEMERAL_KEY || CHUNK_DATA - // (note: surb_ack_bytes contains SURB_ACK_FIRST_HOP || SURB_ACK_DATA ) - let packet_payload: Vec<_> = surb_ack_bytes - .into_iter() - .chain(ephemeral_keypair.public_key().to_bytes().iter().cloned()) - .chain(chunk_data.into_iter()) - .collect(); + let packet_payload = NymsphinxPayloadBuilder::new(fragment, surb_ack) + .build_regular(&mut self.rng, packet_recipient.encryption_key()); // generate pseudorandom route for the packet let route = topology.random_route_to_gateway( @@ -290,6 +227,7 @@ where // note that the last hop of the packet is a gateway that does not do any delays total_delay: delays.iter().take(delays.len() - 1).sum::() + ack_delay, mix_packet: MixPacket::new(first_hop_address, sphinx_packet, Default::default()), + fragment_identifier, }) } @@ -310,113 +248,12 @@ where ) } - /// Attaches an optional reply-surb and correct padding to the underlying message - /// and splits it into [`Fragment`] that can be later packed into sphinx packets to be - /// sent through the mix network. - pub fn prepare_and_split_message( - &mut self, - message: Vec, - with_reply_surb: bool, - topology: &NymTopology, - ) -> Result<(Vec, Option), PreparationError> { - let (message, reply_key) = - self.optionally_attach_reply_surb(message, with_reply_surb, topology)?; + pub fn pad_and_split_message(&mut self, message: NymMessage) -> Vec { + let plaintext_per_packet = message.available_plaintext_per_packet(self.packet_size); - let message = self.pad_message(message); - - Ok((self.split_message(message), reply_key)) - } - - // TODO: perhaps the return type could somehow be combined with [`PreparedFragment`] ? - pub async fn prepare_reply_for_use( - &mut self, - message: Vec, - reply_surb: ReplySurb, - topology: &NymTopology, - ack_key: &AckKey, - ) -> Result<(MixPacket, FragmentIdentifier), PreparationError> { - // there's no chunking in reply-surbs so there's a hard limit on message, - // we also need to put the key digest into the message (same size as ephemeral key) - // and need 1 byte to indicate padding length (this is not the case for 'normal' messages - // as there the padding is added for the whole message) - // so before doing any processing, let's see if we have enough space for it all - let ack_overhead = MAX_NODE_ADDRESS_UNPADDED_LEN + PacketSize::AckPacket.size(); - if message.len() - > self.packet_size.plaintext_size() - - ack_overhead - - ReplySurbKeyDigestAlgorithm::output_size() - - 1 - { - return Err(PreparationError::TooLongReplyMessageError); - } - - let reply_id = FragmentIdentifier::new_reply(&mut self.rng); - - // create an ack - // even though it won't be used for retransmission, it must be present so that - // gateways could not distinguish reply packets from normal messages due to lack of said acks - // note: the ack delay is irrelevant since we do not know the delay of actual surb - let (_, surb_ack_bytes) = self - .generate_surb_ack(reply_id, topology, ack_key)? - .prepare_for_sending(); - - let zero_pad_len = self.packet_size.plaintext_size() - - message.len() - - ack_overhead - - ReplySurbKeyDigestAlgorithm::output_size() - - 1; - - // create reply message that will reach the recipient: - let mut reply_content: Vec<_> = message - .into_iter() - .chain(std::iter::once(1)) - .chain(std::iter::repeat(0).take(zero_pad_len)) - .collect(); - - // encrypt the reply message - let zero_iv = stream_cipher::zero_iv::(); - stream_cipher::encrypt_in_place::( - reply_surb.encryption_key().inner(), - &zero_iv, - &mut reply_content, - ); - - // combine it together as follows: - // SURB_ACK_FIRST_HOP || SURB_ACK_DATA || KEY_DIGEST || E (REPLY_MESSAGE || 1 || 0*) - // (note: surb_ack_bytes contains SURB_ACK_FIRST_HOP || SURB_ACK_DATA ) - let packet_payload: Vec<_> = surb_ack_bytes - .into_iter() - .chain(reply_surb.encryption_key().compute_digest().iter().copied()) - .chain(reply_content.into_iter()) - .collect(); - - // finally put it all inside a sphinx packet - // this can only fail if packet payload has incorrect size, but if it does, it means - // there's a bug in the above code - let (packet, first_hop) = reply_surb - .apply_surb(&packet_payload, Some(self.packet_size)) - .unwrap(); - - Ok(( - MixPacket::new(first_hop, packet, Default::default()), - reply_id, - )) - } - - #[allow(dead_code)] - #[cfg(test)] - pub(crate) fn test_fixture() -> MessagePreparer { - let rng = rand::rngs::OsRng; - let dummy_address = Recipient::try_from_base58_string("CytBseW6yFXUMzz4SGAKdNLGR7q3sJLLYxyBGvutNEQV.4QXYyEVc5fUDjmmi8PrHN9tdUFV4PCvSJE1278cHyvoe@4sBbL1ngf1vtNqykydQKTFh26sQCw888GpUqvPvyNB4f").unwrap(); - - MessagePreparer { - rng, - packet_size: Default::default(), - sender_address: dummy_address, - average_packet_delay: Default::default(), - average_ack_delay: Default::default(), - num_mix_hops: DEFAULT_NUM_MIX_HOPS, - } + message + .pad_to_full_packet_lengths(plaintext_per_packet) + .split_into_fragments(&mut self.rng, plaintext_per_packet) } } diff --git a/common/nymsphinx/src/preparer/payload.rs b/common/nymsphinx/src/preparer/payload.rs new file mode 100644 index 0000000000..94acde03c8 --- /dev/null +++ b/common/nymsphinx/src/preparer/payload.rs @@ -0,0 +1,97 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crypto::aes::cipher::{KeyIvInit, StreamCipher}; +use crypto::asymmetric::encryption; +use crypto::shared_key::new_ephemeral_shared_key; +use crypto::symmetric::stream_cipher; +use crypto::symmetric::stream_cipher::CipherKey; +use nymsphinx_acknowledgements::surb_ack::SurbAck; +use nymsphinx_anonymous_replies::SurbEncryptionKey; +use nymsphinx_chunking::fragment::Fragment; +use nymsphinx_params::{ + PacketEncryptionAlgorithm, PacketHkdfAlgorithm, ReplySurbEncryptionAlgorithm, +}; +use rand::{CryptoRng, RngCore}; + +pub struct NymsphinxPayloadBuilder { + fragment: Fragment, + surb_ack: SurbAck, +} + +impl NymsphinxPayloadBuilder { + pub fn new(fragment: Fragment, surb_ack: SurbAck) -> Self { + NymsphinxPayloadBuilder { fragment, surb_ack } + } + + fn build( + self, + packet_encryption_key: &CipherKey, + variant_data: impl IntoIterator, + ) -> NymsphinxPayload + where + C: StreamCipher + KeyIvInit, + { + let (_, surb_ack_bytes) = self.surb_ack.prepare_for_sending(); + + let mut fragment_data = self.fragment.into_bytes(); + stream_cipher::encrypt_in_place::( + packet_encryption_key, + &stream_cipher::zero_iv::(), + &mut fragment_data, + ); + + // combines all the data as follows: + // SURB_ACK || VARIANT_SPECIFIC_DATA || CHUNK_DATA + // where variant-specific data is as follows: + // for replies it would be the digest of the encryption key used + // for 'regular' messages it would be the public component used in DH later used in the KDF + NymsphinxPayload( + surb_ack_bytes + .into_iter() + .chain(variant_data.into_iter()) + .chain(fragment_data.into_iter()) + .collect(), + ) + } + + pub fn build_reply(self, packet_encryption_key: &SurbEncryptionKey) -> NymsphinxPayload { + let key_digest = packet_encryption_key.compute_digest(); + self.build::( + packet_encryption_key.inner(), + key_digest.into_iter(), + ) + } + + pub fn build_regular( + self, + rng: &mut R, + recipient_encryption_key: &encryption::PublicKey, + ) -> NymsphinxPayload + where + R: RngCore + CryptoRng, + { + // create keys for 'payload' encryption + let (ephemeral_keypair, shared_key) = new_ephemeral_shared_key::< + PacketEncryptionAlgorithm, + PacketHkdfAlgorithm, + _, + >(rng, recipient_encryption_key); + + self.build::( + &shared_key, + ephemeral_keypair.public_key().to_bytes(), + ) + } +} + +// the actual byte data that will be put into the sphinx packet paylaod. +// no more transformations are going to happen to it +// TODO: use that fact for some better compile time assertions +pub struct NymsphinxPayload(Vec); + +impl AsRef<[u8]> for NymsphinxPayload { + fn as_ref(&self) -> &[u8] { + &self.0 + } +} diff --git a/common/nymsphinx/src/receiver.rs b/common/nymsphinx/src/receiver.rs index 0b6a8e96dd..453f632f76 100644 --- a/common/nymsphinx/src/receiver.rs +++ b/common/nymsphinx/src/receiver.rs @@ -1,13 +1,22 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use crate::message::{NymMessage, NymMessageError, PaddedMessage, PlainMessage}; +use crypto::aes::cipher::{KeyIvInit, StreamCipher}; use crypto::asymmetric::encryption; use crypto::shared_key::recompute_shared_key; use crypto::symmetric::stream_cipher; -use nymsphinx_anonymous_replies::reply_surb::{ReplySurb, ReplySurbError}; +use crypto::symmetric::stream_cipher::CipherKey; +use nymsphinx_anonymous_replies::requests::AnonymousSenderTag; +use nymsphinx_anonymous_replies::SurbEncryptionKey; use nymsphinx_chunking::fragment::Fragment; use nymsphinx_chunking::reconstruction::MessageReconstructor; -use nymsphinx_params::{PacketEncryptionAlgorithm, PacketHkdfAlgorithm, DEFAULT_NUM_MIX_HOPS}; +use nymsphinx_chunking::ChunkingError; +use nymsphinx_params::{ + PacketEncryptionAlgorithm, PacketHkdfAlgorithm, ReplySurbEncryptionAlgorithm, + DEFAULT_NUM_MIX_HOPS, +}; +use thiserror::Error; // TODO: should this live in this file? #[derive(Debug)] @@ -15,31 +24,53 @@ pub struct ReconstructedMessage { /// The actual plaintext message that was received. pub message: Vec, - /// Optional ReplySURB to allow for an anonymous reply to the sender. - pub reply_surb: Option, + /// Optional ephemeral sender tag indicating pseudo-identity of the party who sent us the message + /// (alongside any reply SURBs) + pub sender_tag: Option, } -#[derive(Debug)] +impl From for (Vec, Option) { + fn from(msg: ReconstructedMessage) -> Self { + (msg.message, msg.sender_tag) + } +} + +impl ReconstructedMessage { + pub fn new(message: Vec, sender_tag: AnonymousSenderTag) -> Self { + Self { + message, + sender_tag: Some(sender_tag), + } + } + + pub fn into_inner(self) -> (Vec, Option) { + self.into() + } +} + +impl From for ReconstructedMessage { + fn from(message: PlainMessage) -> Self { + ReconstructedMessage { + message, + sender_tag: None, + } + } +} + +#[derive(Debug, Error)] pub enum MessageRecoveryError { - InvalidSurbPrefixError, - MalformedSurbError(ReplySurbError), - InvalidRemoteEphemeralKey(encryption::KeyRecoveryError), - MalformedFragmentError, - InvalidMessagePaddingError, - MalformedReconstructedMessage(Vec), - TooShortMessageError, -} + #[error("Recovered remote x25519 public key is invalid - {0}")] + InvalidRemoteEphemeralKey(#[from] encryption::KeyRecoveryError), -impl From for MessageRecoveryError { - fn from(err: ReplySurbError) -> Self { - MessageRecoveryError::MalformedSurbError(err) - } -} + #[error("The reconstructed message was malformed - {source}")] + MalformedReconstructedMessage { + #[source] + source: NymMessageError, + used_sets: Vec, + }, -impl From for MessageRecoveryError { - fn from(err: encryption::KeyRecoveryError) -> Self { - MessageRecoveryError::InvalidRemoteEphemeralKey(err) - } + #[error("Failed to recover message fragment - {0}")] + FragmentRecoveryError(#[from] ChunkingError), } pub struct MessageReceiver { @@ -64,36 +95,34 @@ impl MessageReceiver { self } - /// Parses the message to strip and optionally recover reply SURB. - fn recover_reply_surb_from_message( - &self, - message: &mut Vec, - ) -> Result, MessageRecoveryError> { - match message[0] { - n if n == false as u8 => { - message.remove(0); - Ok(None) - } - n if n == true as u8 => { - let surb_len: usize = ReplySurb::serialized_len(self.num_mix_hops); - // note the extra +1 (due to 0/1 message prefix) - let surb_bytes = &message[1..1 + surb_len]; - let reply_surb = ReplySurb::from_bytes(surb_bytes)?; + fn decrypt_raw_message(&self, message: &mut [u8], key: &CipherKey) + where + C: StreamCipher + KeyIvInit, + { + let zero_iv = stream_cipher::zero_iv::(); + stream_cipher::decrypt_in_place::(key, &zero_iv, message) + } - *message = message.drain(1 + surb_len..).collect(); - Ok(Some(reply_surb)) - } - _ => Err(MessageRecoveryError::InvalidSurbPrefixError), - } + /// Given raw fragment data, **WITH KEY DIGEST PREFIX ALREADY REMOVED!!**, uses looked up + /// key to decrypt fragment data + pub fn recover_plaintext_from_reply( + &self, + reply_ciphertext: &mut [u8], + reply_key: SurbEncryptionKey, + ) { + self.decrypt_raw_message::( + reply_ciphertext, + reply_key.inner(), + ) } /// Given raw fragment data, recovers the remote ephemeral key, recomputes shared secret, /// uses it to decrypt fragment data - pub fn recover_plaintext( + pub fn recover_plaintext_from_regular_packet<'a>( &self, local_key: &encryption::PrivateKey, - mut raw_enc_frag: Vec, - ) -> Result, MessageRecoveryError> { + raw_enc_frag: &'a mut [u8], + ) -> Result<&'a mut [u8], MessageRecoveryError> { // 1. recover remote encryption key let remote_key_bytes = &raw_enc_frag[..encryption::PUBLIC_KEY_SIZE]; let remote_ephemeral_key = encryption::PublicKey::from_bytes(remote_key_bytes)?; @@ -105,33 +134,17 @@ impl MessageReceiver { ); // 3. decrypt fragment data - let fragment_bytes = &mut raw_enc_frag[encryption::PUBLIC_KEY_SIZE..]; + let fragment_ciphertext = &mut raw_enc_frag[encryption::PUBLIC_KEY_SIZE..]; - let zero_iv = stream_cipher::zero_iv::(); - Ok(stream_cipher::decrypt::( - &encryption_key, - &zero_iv, - fragment_bytes, - )) + self.decrypt_raw_message::(fragment_ciphertext, &encryption_key); + let fragment_data = fragment_ciphertext; + + Ok(fragment_data) } /// Given fragment data recovers [`Fragment`] itself. pub fn recover_fragment(&self, frag_data: &[u8]) -> Result { - Fragment::try_from_bytes(frag_data) - .map_err(|_| MessageRecoveryError::MalformedFragmentError) - } - - /// Removes the zero padding from the message that was initially included to ensure same length - /// sphinx payloads. - pub fn remove_padding(message: &mut Vec) -> Result<(), MessageRecoveryError> { - // we are looking for first occurrence of 1 in the tail and we get its index - if let Some(i) = message.iter().rposition(|b| *b == 1) { - // and now we only take bytes until that point (but not including it) - *message = message.drain(..i).collect(); - Ok(()) - } else { - Err(MessageRecoveryError::InvalidMessagePaddingError) - } + Ok(Fragment::try_from_bytes(frag_data)?) } /// Inserts given [`Fragment`] into the reconstructor. @@ -144,30 +157,15 @@ impl MessageReceiver { pub fn insert_new_fragment( &mut self, fragment: Fragment, - ) -> Result)>, MessageRecoveryError> { - if let Some((mut message, used_sets)) = self.reconstructor.insert_new_fragment(fragment) { - // Split message into plaintext and reply-SURB - let reply_surb = match self.recover_reply_surb_from_message(&mut message) { - Ok(reply_surb) => reply_surb, - Err(_) => { - return Err(MessageRecoveryError::MalformedReconstructedMessage( - used_sets, - )); - } - }; - - // Finally, remove the zero padding from the message - Self::remove_padding(&mut message).map_err(|_| { - MessageRecoveryError::MalformedReconstructedMessage(used_sets.clone()) - })?; - - Ok(Some(( - ReconstructedMessage { - message, - reply_surb, - }, - used_sets, - ))) + ) -> Result)>, MessageRecoveryError> { + if let Some((message, used_sets)) = self.reconstructor.insert_new_fragment(fragment) { + match PaddedMessage::new_reconstructed(message).remove_padding(self.num_mix_hops) { + Ok(message) => Ok(Some((message, used_sets))), + Err(err) => Err(MessageRecoveryError::MalformedReconstructedMessage { + source: err, + used_sets, + }), + } } else { Ok(None) } @@ -188,16 +186,14 @@ mod message_receiver { use super::*; use crypto::asymmetric::identity; use mixnet_contract_common::Layer; - use nymsphinx_addressing::clients::Recipient; - use rand::rngs::OsRng; use std::collections::HashMap; - use std::time::Duration; use topology::{gateway, mix, NymTopology}; // TODO: is it somehow maybe possible to move it to `topology` and have if conditionally // available to other modules? /// Returns a hardcoded, valid instance of [`NymTopology`] that is to be used in /// tests requiring instance of topology. + #[allow(dead_code)] fn topology_fixture() -> NymTopology { let mut mixes = HashMap::new(); mixes.insert( @@ -282,41 +278,4 @@ mod message_receiver { }], ) } - - #[test] - fn correctly_splits_message_into_plaintext_and_surb() { - let message_receiver: MessageReceiver = Default::default(); - - // the actual 'correctness' of the underlying message doesn't matter for this test - let message = vec![42; 100]; - let dummy_recipient = Recipient::try_from_base58_string("CytBseW6yFXUMzz4SGAKdNLGR7q3sJLLYxyBGvutNEQV.4QXYyEVc5fUDjmmi8PrHN9tdUFV4PCvSJE1278cHyvoe@FioFa8nMmPpQnYi7JyojoTuwGLeyNS8BF4ChPr29zUML").unwrap(); - let average_delay = Duration::from_millis(500); - let topology = topology_fixture(); - - let reply_surb = - ReplySurb::construct(&mut OsRng, &dummy_recipient, average_delay, &topology).unwrap(); - - let reply_surb_bytes = reply_surb.to_bytes(); - - // this is not exactly what is 'received' but rather after "some" processing, however, - // this is the expected argument to the function - let mut received_without_surb: Vec<_> = - std::iter::once(0).chain(message.iter().cloned()).collect(); - - let reply_surb = message_receiver - .recover_reply_surb_from_message(&mut received_without_surb) - .unwrap(); - assert_eq!(received_without_surb, message); - assert!(reply_surb.is_none()); - - let mut received_with_surb: Vec<_> = std::iter::once(1) - .chain(reply_surb_bytes.iter().cloned()) - .chain(message.iter().cloned()) - .collect(); - let reply_surb = message_receiver - .recover_reply_surb_from_message(&mut received_with_surb) - .unwrap(); - assert_eq!(received_with_surb, message); - assert_eq!(reply_surb_bytes, reply_surb.unwrap().to_bytes()); - } } diff --git a/common/nymsphinx/types/Cargo.toml b/common/nymsphinx/types/Cargo.toml index 34a916bbf5..353de2b1b2 100644 --- a/common/nymsphinx/types/Cargo.toml +++ b/common/nymsphinx/types/Cargo.toml @@ -7,5 +7,5 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -sphinx = { git = "https://github.com/nymtech/sphinx", rev="c494250f2a78bed33a618d470792418eee932859" } +sphinx = { git = "https://github.com/nymtech/sphinx", rev="e05a1992522ed0afd3c6fcac160313ffc9bb306a" } #sphinx = { path = "../../../../sphinx"} diff --git a/common/socks5/proxy-helpers/src/connection_controller.rs b/common/socks5/proxy-helpers/src/connection_controller.rs index f0f552d83c..97c4487af6 100644 --- a/common/socks5/proxy-helpers/src/connection_controller.rs +++ b/common/socks5/proxy-helpers/src/connection_controller.rs @@ -254,6 +254,7 @@ impl Controller { }, } } + #[cfg(not(target_arch = "wasm32"))] tokio::time::timeout(Duration::from_secs(5), self.shutdown.recv()) .await .expect("Task stopped without shutdown called"); diff --git a/common/socks5/requests/src/request.rs b/common/socks5/requests/src/request.rs index f6921e362a..3148f6cd84 100644 --- a/common/socks5/requests/src/request.rs +++ b/common/socks5/requests/src/request.rs @@ -15,6 +15,18 @@ pub enum RequestFlag { Send = 1, } +impl TryFrom for RequestFlag { + type Error = RequestError; + + fn try_from(value: u8) -> Result { + match value { + _ if value == (RequestFlag::Connect as u8) => Ok(Self::Connect), + _ if value == (RequestFlag::Send as u8) => Ok(Self::Send), + _ => Err(RequestError::UnknownRequestFlag), + } + } +} + #[derive(Debug, Error)] pub enum RequestError { #[error("not enough bytes to recover the length of the address")] @@ -45,23 +57,12 @@ impl RequestError { } } -impl TryFrom for RequestFlag { - type Error = RequestError; - - fn try_from(value: u8) -> Result { - match value { - _ if value == (RequestFlag::Connect as u8) => Ok(Self::Connect), - _ if value == (RequestFlag::Send as u8) => Ok(Self::Send), - _ => Err(RequestError::UnknownRequestFlag), - } - } -} - #[derive(Debug)] pub struct ConnectRequest { + // TODO: is connection_id redundant now? pub conn_id: ConnectionId, pub remote_addr: RemoteAddress, - pub return_address: Recipient, + pub return_address: Option, } /// A request from a SOCKS5 client that a Nym Socks5 service provider should @@ -82,7 +83,7 @@ impl Request { pub fn new_connect( conn_id: ConnectionId, remote_addr: RemoteAddress, - return_address: Recipient, + return_address: Option, ) -> Request { Request::Connect(Box::new(ConnectRequest { conn_id, @@ -99,12 +100,13 @@ impl Request { /// Deserialize the request type, connection id, destination address and port, /// and the request body from bytes. /// - /// Serialized bytes looks like this: - /// - /// -------------------------------------------------------------------------------------- - /// request_flag | connection_id | address_length | remote_address_bytes | request_data | - /// 1 | 8 | 2 | address_length | ... | - /// -------------------------------------------------------------------------------------- + // TODO: this was already inaccurate + // /// Serialized bytes looks like this: + // /// + // /// -------------------------------------------------------------------------------------- + // /// request_flag | connection_id | address_length | remote_address_bytes | request_data | + // /// 1 | 8 | 2 | address_length | ... | + // /// -------------------------------------------------------------------------------------- /// /// The request_flag tells us whether this is a new connection request (`new_connect`), /// an already-established connection we should send up (`new_send`), or @@ -144,14 +146,20 @@ impl Request { // just a temporary reference to mid-slice for ease of use let recipient_data_bytes = &connect_request_bytes[address_end..]; - if recipient_data_bytes.len() != Recipient::LEN { - return Err(RequestError::ReturnAddressTooShort); - } + let return_address = if recipient_data_bytes.is_empty() { + None + } else { + if recipient_data_bytes.len() != Recipient::LEN { + return Err(RequestError::ReturnAddressTooShort); + } - let mut return_bytes = [0u8; Recipient::LEN]; - return_bytes.copy_from_slice(&recipient_data_bytes[..Recipient::LEN]); - let return_address = Recipient::try_from_bytes(return_bytes) - .map_err(RequestError::MalformedReturnAddress)?; + let mut return_bytes = [0u8; Recipient::LEN]; + return_bytes.copy_from_slice(&recipient_data_bytes[..Recipient::LEN]); + Some( + Recipient::try_from_bytes(return_bytes) + .map_err(RequestError::MalformedReturnAddress)?, + ) + }; Ok(Request::new_connect( connection_id, @@ -161,7 +169,7 @@ impl Request { } RequestFlag::Send => { let local_closed = b[9] != 0; - let data = b[10..].as_ref().to_vec(); + let data = b[10..].to_vec(); Ok(Request::Send(connection_id, data, local_closed)) } @@ -178,15 +186,19 @@ impl Request { let remote_address_bytes = req.remote_addr.into_bytes(); let remote_address_bytes_len = remote_address_bytes.len() as u16; - std::iter::once(RequestFlag::Connect as u8) - .chain(req.conn_id.to_be_bytes().iter().cloned()) - .chain(remote_address_bytes_len.to_be_bytes().iter().cloned()) - .chain(remote_address_bytes.into_iter()) - .chain(req.return_address.to_bytes().iter().cloned()) - .collect() + let iter = std::iter::once(RequestFlag::Connect as u8) + .chain(req.conn_id.to_be_bytes().into_iter()) + .chain(remote_address_bytes_len.to_be_bytes().into_iter()) + .chain(remote_address_bytes.into_iter()); + + if let Some(return_address) = req.return_address { + iter.chain(return_address.to_bytes().into_iter()).collect() + } else { + iter.collect() + } } Request::Send(conn_id, data, local_closed) => std::iter::once(RequestFlag::Send as u8) - .chain(conn_id.to_be_bytes().iter().cloned()) + .chain(conn_id.to_be_bytes().into_iter()) .chain(std::iter::once(local_closed as u8)) .chain(data.into_iter()) .collect(), @@ -324,7 +336,7 @@ mod request_deserialization_tests { let request_bytes: Vec<_> = request_bytes_prefix .iter() .cloned() - .chain(recipient_bytes.iter().cloned()) + .chain(recipient_bytes.into_iter()) .collect(); assert!(Request::try_from_bytes(&request_bytes) .unwrap_err() @@ -361,7 +373,7 @@ mod request_deserialization_tests { let request_bytes: Vec<_> = request_bytes .into_iter() - .chain(recipient_bytes.iter().cloned()) + .chain(recipient_bytes.into_iter()) .collect(); let request = Request::try_from_bytes(&request_bytes).unwrap(); @@ -370,7 +382,7 @@ mod request_deserialization_tests { assert_eq!("foo.com".to_string(), req.remote_addr); assert_eq!(u64::from_be_bytes([1, 2, 3, 4, 5, 6, 7, 8]), req.conn_id); assert_eq!( - req.return_address.to_bytes().to_vec(), + req.return_address.unwrap().to_bytes().to_vec(), recipient.to_bytes().to_vec() ); } @@ -408,7 +420,7 @@ mod request_deserialization_tests { let request_bytes: Vec<_> = request_bytes .into_iter() - .chain(recipient_bytes.iter().cloned()) + .chain(recipient_bytes.into_iter()) .collect(); let request = Request::try_from_bytes(&request_bytes).unwrap(); @@ -417,7 +429,7 @@ mod request_deserialization_tests { assert_eq!("foo.com".to_string(), req.remote_addr); assert_eq!(u64::from_be_bytes([1, 2, 3, 4, 5, 6, 7, 8]), req.conn_id); assert_eq!( - req.return_address.to_bytes().to_vec(), + req.return_address.unwrap().to_bytes().to_vec(), recipient.to_bytes().to_vec() ); } diff --git a/common/task/Cargo.toml b/common/task/Cargo.toml index f074cd2abd..ee83423f5f 100644 --- a/common/task/Cargo.toml +++ b/common/task/Cargo.toml @@ -9,7 +9,17 @@ edition = "2021" futures = "0.3" log = "0.4" thiserror = "1.0.37" -tokio = { version = "1.21.2", features = ["macros", "signal", "time", "sync"] } +tokio = { version = "1.21.2", features = ["macros", "sync"] } + +[target."cfg(not(target_arch = \"wasm32\"))".dependencies.tokio] +version = "1.21.2" +features = ["signal", "time"] + +[target."cfg(target_arch = \"wasm32\")".dependencies.wasm-bindgen-futures] +version = "0.4" + +[target."cfg(target_arch = \"wasm32\")".dependencies.wasm-bindgen] +version = "0.2.83" [dev-dependencies] tokio = { version = "1.21.2", features = ["rt-multi-thread", "net", "signal", "test-util", "macros"] } diff --git a/common/task/src/lib.rs b/common/task/src/lib.rs index 2d0f91f59a..667936a830 100644 --- a/common/task/src/lib.rs +++ b/common/task/src/lib.rs @@ -2,7 +2,12 @@ // SPDX-License-Identifier: Apache-2.0 pub mod shutdown; +#[cfg(not(target_arch = "wasm32"))] pub mod signal; +pub mod spawn; pub use shutdown::{ShutdownListener, ShutdownNotifier}; +#[cfg(not(target_arch = "wasm32"))] pub use signal::{wait_for_signal, wait_for_signal_and_error}; + +pub use spawn::spawn_with_report_error; diff --git a/common/task/src/shutdown.rs b/common/task/src/shutdown.rs index 5d4f2c77bb..93b0d925a7 100644 --- a/common/task/src/shutdown.rs +++ b/common/task/src/shutdown.rs @@ -3,7 +3,7 @@ use std::{error::Error, time::Duration}; -use futures::FutureExt; +use futures::{future::pending, FutureExt}; use tokio::{ sync::{ mpsc, @@ -14,7 +14,7 @@ use tokio::{ const DEFAULT_SHUTDOWN_TIMER_SECS: u64 = 5; -pub(crate) type SentError = Box; +pub(crate) type SentError = Box; type ErrorSender = mpsc::UnboundedSender; type ErrorReceiver = mpsc::UnboundedReceiver; @@ -31,6 +31,7 @@ pub struct ShutdownNotifier { // track of which tasks we are still waiting for. notify_tx: watch::Sender<()>, notify_rx: Option>, + #[cfg_attr(target_arch = "wasm32", allow(dead_code))] shutdown_timer_secs: u64, // If any task failed, it needs to report separately @@ -113,6 +114,11 @@ impl ShutdownNotifier { drop(notify_rx); } + // in wasm we'll never get our shutdown anyway... + #[cfg(target_arch = "wasm32")] + futures::future::pending::<()>().await; + + #[cfg(not(target_arch = "wasm32"))] tokio::select! { _ = self.notify_tx.closed() => { log::info!("All registered tasks succesfully shutdown"); @@ -143,12 +149,14 @@ pub struct ShutdownListener { // Also notify if we dropped without shutdown being registered drop_error: ErrorSender, - // Sometimes it's necessary to clone and drop the shutdown listener during normal operation, - // for those situations we need to explicitly not drop (and trigger shutdown). - set_not_drop: bool, + // The current operating mode + mode: ShutdownListenerMode, } impl ShutdownListener { + #[cfg(not(target_arch = "wasm32"))] + const SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(5); + fn new( notify: watch::Receiver<()>, return_error: ErrorSender, @@ -159,15 +167,40 @@ impl ShutdownListener { notify, return_error, drop_error, - set_not_drop: false, + mode: ShutdownListenerMode::Listening, } } + // Create a dummy that will never report that we should shutdown. + pub fn dummy() -> ShutdownListener { + let (_notify_tx, notify_rx) = watch::channel(()); + let (task_halt_tx, _task_halt_rx) = mpsc::unbounded_channel(); + let (task_drop_tx, _task_drop_rx) = mpsc::unbounded_channel(); + ShutdownListener { + shutdown: false, + notify: notify_rx, + return_error: task_halt_tx, + drop_error: task_drop_tx, + mode: ShutdownListenerMode::Dummy, + } + } + + pub fn is_dummy(&self) -> bool { + self.mode.is_dummy() + } + pub fn is_shutdown(&self) -> bool { - self.shutdown + if self.mode.is_dummy() { + false + } else { + self.shutdown + } } pub async fn recv(&mut self) { + if self.mode.is_dummy() { + return pending().await; + } if self.shutdown { return; } @@ -175,7 +208,20 @@ impl ShutdownListener { self.shutdown = true; } + pub async fn recv_timeout(&mut self) { + if self.mode.is_dummy() { + return pending().await; + } + #[cfg(not(target_arch = "wasm32"))] + tokio::time::timeout(Self::SHUTDOWN_TIMEOUT, self.recv()) + .await + .expect("Task stopped without shutdown called"); + } + pub fn is_shutdown_poll(&mut self) -> bool { + if self.mode.is_dummy() { + return false; + } if self.shutdown { return true; } @@ -194,21 +240,30 @@ impl ShutdownListener { } } + // This listener should to *not* notify the ShutdownNotifier to shutdown when dropped. For + // example when we clone the listener for a task handling connections, we often want to drop + // without signal failure. + pub fn mark_as_success(&mut self) { + self.mode.set_should_not_signal_on_drop(); + } + pub fn send_we_stopped(&mut self, err: SentError) { + if self.mode.is_dummy() { + return; + } log::trace!("Notifying we stopped: {:?}", err); if self.return_error.send(err).is_err() { log::error!("Failed to send back error message"); } } - - pub fn mark_as_success(&mut self) { - self.set_not_drop = true; - } } impl Drop for ShutdownListener { fn drop(&mut self) { - if !self.set_not_drop && !self.is_shutdown_poll() { + if !self.mode.should_signal_on_drop() { + return; + } + if !self.is_shutdown_poll() { log::trace!("Notifying stop on unexpected drop"); // If we can't send, well then there is not much to do self.drop_error @@ -218,6 +273,37 @@ impl Drop for ShutdownListener { } } +#[derive(Clone, Debug, PartialEq, Eq)] +enum ShutdownListenerMode { + // Normal operations + Listening, + // Normal operations, but we don't report back if the we stop by getting dropped. + ListeningButDontReportHalt, + // Dummy mode, for when we don't do anything at all. + Dummy, +} + +impl ShutdownListenerMode { + fn is_dummy(&self) -> bool { + self == &ShutdownListenerMode::Dummy + } + + fn should_signal_on_drop(&self) -> bool { + match self { + ShutdownListenerMode::Listening => true, + ShutdownListenerMode::ListeningButDontReportHalt | ShutdownListenerMode::Dummy => false, + } + } + + fn set_should_not_signal_on_drop(&mut self) { + use ShutdownListenerMode::{Dummy, Listening, ListeningButDontReportHalt}; + *self = match &self { + ListeningButDontReportHalt | Listening => ListeningButDontReportHalt, + Dummy => Dummy, + }; + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/common/task/src/spawn.rs b/common/task/src/spawn.rs new file mode 100644 index 0000000000..ba66ea7f16 --- /dev/null +++ b/common/task/src/spawn.rs @@ -0,0 +1,33 @@ +use crate::ShutdownListener; +use std::future::Future; + +#[cfg(target_arch = "wasm32")] +pub(crate) fn spawn(future: F) +where + F: Future + 'static, +{ + wasm_bindgen_futures::spawn_local(future); +} + +#[cfg(not(target_arch = "wasm32"))] +pub(crate) fn spawn(future: F) +where + F: Future + Send + 'static, + F::Output: Send + 'static, +{ + tokio::spawn(future); +} + +pub fn spawn_with_report_error(future: F, mut shutdown: ShutdownListener) +where + F: Future> + Send + 'static, + T: 'static, + E: std::error::Error + Send + Sync + 'static, +{ + let future_that_sends = async move { + if let Err(err) = future.await { + shutdown.send_we_stopped(Box::new(err)); + } + }; + spawn(future_that_sends); +} diff --git a/common/topology/Cargo.toml b/common/topology/Cargo.toml index f2a8a899fd..2996aeb639 100644 --- a/common/topology/Cargo.toml +++ b/common/topology/Cargo.toml @@ -10,6 +10,7 @@ edition = "2021" bs58 = "0.4" log = "0.4" rand = { version = "0.7.3", features = ["wasm-bindgen"] } +thiserror = "1.0.37" ## internal crypto = { path = "../crypto" } diff --git a/common/topology/src/lib.rs b/common/topology/src/lib.rs index 2e9cde725b..ff3ba55c83 100644 --- a/common/topology/src/lib.rs +++ b/common/topology/src/lib.rs @@ -1,4 +1,4 @@ -// Copyright 2021 - Nym Technologies SA +// Copyright 2021-2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 use crate::filter::VersionFilterable; @@ -14,19 +14,31 @@ use std::fmt::{self, Display, Formatter}; use std::io; use std::net::{IpAddr, SocketAddr, ToSocketAddrs}; use std::str::FromStr; +use thiserror::Error; pub mod filter; pub mod gateway; pub mod mix; -#[derive(Debug)] +#[derive(Debug, Clone, Error)] pub enum NymTopologyError { - InvalidMixLayerError, - MissingLayerError(Vec), - NonExistentGatewayError, + #[error("The provided network topology is empty - there are no mixnodes and no gateways on it - the network request(s) probably failed")] + EmptyNetworkTopology, - InvalidNumberOfHopsError, - NoMixesOnLayerAvailable(MixLayer), + #[error("The provided network topology has no gateways available")] + NoGatewaysAvailable, + + #[error("The provided network topology has no mixnodes available")] + NoMixnodesAvailable, + + #[error("Gateway with identity key {identity_key} doesn't exist")] + NonExistentGatewayError { identity_key: String }, + + #[error("Wanted to create a mix route with {requested} hops, while only {available} layers are available")] + InvalidNumberOfHopsError { available: usize, requested: usize }, + + #[error("No mixnodes available on layer {layer}")] + EmptyMixLayer { layer: MixLayer }, } #[derive(Debug, Clone)] @@ -134,7 +146,10 @@ impl NymTopology { use rand::seq::SliceRandom; if self.mixes.len() < num_mix_hops as usize { - return Err(NymTopologyError::InvalidNumberOfHopsError); + return Err(NymTopologyError::InvalidNumberOfHopsError { + available: self.mixes.len(), + requested: num_mix_hops as usize, + }); } let mut route = Vec::with_capacity(num_mix_hops as usize); @@ -144,13 +159,13 @@ impl NymTopology { let layer_mixes = self .mixes .get(&layer) - .ok_or(NymTopologyError::NoMixesOnLayerAvailable(layer))?; + .ok_or(NymTopologyError::EmptyMixLayer { layer })?; // choose a random mix from the above list // this can return a 'None' only if slice is empty let random_mix = layer_mixes .choose(rng) - .ok_or(NymTopologyError::NoMixesOnLayerAvailable(layer))?; + .ok_or(NymTopologyError::EmptyMixLayer { layer })?; route.push(random_mix.into()); } @@ -169,9 +184,11 @@ impl NymTopology { // I don't think there's a need for this RNG to be crypto-secure R: Rng + ?Sized, { - let gateway = self - .get_gateway(gateway_identity) - .ok_or(NymTopologyError::NonExistentGatewayError)?; + let gateway = self.get_gateway(gateway_identity).ok_or( + NymTopologyError::NonExistentGatewayError { + identity_key: gateway_identity.to_base58_string(), + }, + )?; Ok(self .random_mix_route(rng, num_mix_hops)? @@ -186,29 +203,39 @@ impl NymTopology { } /// Checks if a mixnet path can be constructed using the specified number of hops - pub fn can_construct_path_through(&self, num_mix_hops: u8) -> bool { - // if there are no gateways present, we can't do anything - if self.gateways.is_empty() { - return false; + pub fn ensure_can_construct_path_through( + &self, + num_mix_hops: u8, + ) -> Result<(), NymTopologyError> { + let mixnodes = self.mixes(); + // 1. is it completely empty? + if mixnodes.is_empty() && self.gateways().is_empty() { + return Err(NymTopologyError::EmptyNetworkTopology); } - // early termination - if self.mixes.is_empty() { - return false; + // 2. does it have any mixnode at all? + if mixnodes.is_empty() { + return Err(NymTopologyError::NoMixnodesAvailable); } - // make sure there's at least one mix per layer - for i in 1..=num_mix_hops { - match self.mixes.get(&i) { - None => return false, - Some(layer_entry) => { - if layer_entry.is_empty() { - return false; + // 3. does it have any gateways at all? + if self.gateways().is_empty() { + return Err(NymTopologyError::NoGatewaysAvailable); + } + + // 4. does it have a mixnode on each layer? + for layer in 1..=num_mix_hops { + match mixnodes.get(&layer) { + None => return Err(NymTopologyError::EmptyMixLayer { layer }), + Some(layer_nodes) => { + if layer_nodes.is_empty() { + return Err(NymTopologyError::EmptyMixLayer { layer }); } } } } - true + + Ok(()) } #[must_use] diff --git a/common/wasm-utils/Cargo.toml b/common/wasm-utils/Cargo.toml index 75021ddad9..be39421ce2 100644 --- a/common/wasm-utils/Cargo.toml +++ b/common/wasm-utils/Cargo.toml @@ -16,6 +16,7 @@ wasm-bindgen-futures = "0.4" [dependencies.tungstenite] version = "0.13" default-features = false +optional = true [dependencies.web-sys] version = "0.3" @@ -30,3 +31,9 @@ features = [ "WebSocket", "Window", ] +optional = true + +[features] +default = ["sleep"] +websocket = ["tungstenite", "web-sys"] +sleep = ["web-sys"] \ No newline at end of file diff --git a/common/wasm-utils/src/lib.rs b/common/wasm-utils/src/lib.rs index 94b8faae05..8e0205c6de 100644 --- a/common/wasm-utils/src/lib.rs +++ b/common/wasm-utils/src/lib.rs @@ -5,8 +5,8 @@ use js_sys::Promise; use wasm_bindgen::prelude::*; use wasm_bindgen::JsValue; use wasm_bindgen_futures::JsFuture; -use web_sys::window; +#[cfg(feature = "websocket")] pub mod websocket; // will cause messages to be written as if console.log("...") was called @@ -39,9 +39,10 @@ extern "C" { pub fn error(s: &str); } +#[cfg(feature = "sleep")] pub async fn sleep(ms: i32) -> Result<(), JsValue> { let promise = Promise::new(&mut |yes, _| { - let win = window().expect("no window available!"); + let win = web_sys::window().expect("no window available!"); win.set_timeout_with_callback_and_timeout_and_arguments_0(&yes, ms) .unwrap(); }); diff --git a/contracts/CHANGELOG.md b/contracts/CHANGELOG.md index c192eab3ae..d52edc427d 100644 --- a/contracts/CHANGELOG.md +++ b/contracts/CHANGELOG.md @@ -1,10 +1,14 @@ ## Unreleased -## Added +## [nym-contracts-v1.1.2](https://github.com/nymtech/nym/tree/nym-contracts-v1.1.2) (2022-12-07) + +### Added - Added migration code to the mixnet contract to allow updating stored vesting contract address to make it easier to deploy any future environments ([#1759],[#1769]) - Added an option to pledge additional tokens without the need to rebond minxode ([#1679]) +- Added support for node families ([#1670]) +[#1670]: https://github.com/nymtech/nym/pull/1670 [#1679]: https://github.com/nymtech/nym/pull/1679 [#1759]: https://github.com/nymtech/nym/pull/1759 [#1769]: https://github.com/nymtech/nym/pull/1769 @@ -95,4 +99,4 @@ [#1292]: https://github.com/nymtech/nym/pull/1292 [#1331]: https://github.com/nymtech/nym/pull/1331 [#1369]: https://github.com/nymtech/nym/pull/1369 -[#1373]: https://github.com/nymtech/nym/pull/1373 \ No newline at end of file +[#1373]: https://github.com/nymtech/nym/pull/1373 diff --git a/contracts/Cargo.lock b/contracts/Cargo.lock new file mode 100644 index 0000000000..28820f259c --- /dev/null +++ b/contracts/Cargo.lock @@ -0,0 +1,1786 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "aes" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e8b47f52ea9bae42228d07ec09eb676433d7c4ed1ebdf0f1d1c29ed446f1ab8" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", + "ctr", + "opaque-debug 0.3.0", +] + +[[package]] +name = "anyhow" +version = "1.0.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84450d0b4a8bd1ba4144ce8ce718fbc5d071358b1e5384bace6536b3d1f2d5b3" + +[[package]] +name = "arrayref" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544" + +[[package]] +name = "autocfg" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" + +[[package]] +name = "bandwidth-claim-contract" +version = "0.1.0" +dependencies = [ + "schemars", + "serde", +] + +[[package]] +name = "base16ct" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349a06037c7bf932dd7e7d1f653678b2038b9ad46a74102f1fc7bd7872678cce" + +[[package]] +name = "base64" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" + +[[package]] +name = "base64ct" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dea908e7347a8c64e378c17e30ef880ad73e3b4498346b055c2c00ea342f3179" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "blake2" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94cb07b0da6a73955f8fb85d24c466778e70cda767a568229b104f0264089330" +dependencies = [ + "byte-tools", + "crypto-mac 0.7.0", + "digest 0.8.1", + "opaque-debug 0.2.3", +] + +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array 0.14.5", +] + +[[package]] +name = "bs58" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "771fe0050b883fcc3ea2359b1a96bcfbc090b7116eae7c3c512c7a083fdf23d3" + +[[package]] +name = "bumpalo" +version = "3.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a45a46ab1f2412e53d3a0ade76ffad2025804294569aae387231a0cd6e0899" + +[[package]] +name = "byte-tools" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "bytes" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" + +[[package]] +name = "cc" +version = "1.0.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22a9137b95ea06864e018375b72adfb7db6e6f68cfc8df5a04d00288050485ee" +dependencies = [ + "jobserver", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chacha" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddf3c081b5fba1e5615640aae998e0fbd10c24cbd897ee39ed754a77601a4862" +dependencies = [ + "byteorder", + "keystream", +] + +[[package]] +name = "chrono" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" +dependencies = [ + "libc", + "num-integer", + "num-traits", + "time 0.1.43", + "winapi", +] + +[[package]] +name = "cipher" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ee52072ec15386f770805afd189a01c8841be8696bed250fa2f13c4c0d6dfb7" +dependencies = [ + "generic-array 0.14.5", +] + +[[package]] +name = "coconut-bandwidth" +version = "0.1.0" +dependencies = [ + "bandwidth-claim-contract", + "coconut-bandwidth-contract-common", + "cosmwasm-std", + "cosmwasm-storage", + "cw-controllers", + "cw-storage-plus", + "multisig-contract-common", + "schemars", + "serde", + "thiserror", +] + +[[package]] +name = "coconut-bandwidth-contract-common" +version = "0.1.0" +dependencies = [ + "cosmwasm-std", + "multisig-contract-common", + "schemars", + "serde", +] + +[[package]] +name = "coconut-dkg" +version = "0.1.0" +dependencies = [ + "coconut-dkg-common", + "cosmwasm-std", + "cosmwasm-storage", + "cw-controllers", + "cw-multi-test", + "cw-storage-plus", + "cw4", + "cw4-group", + "schemars", + "serde", + "thiserror", +] + +[[package]] +name = "coconut-dkg-common" +version = "0.1.0" +dependencies = [ + "contracts-common", + "cosmwasm-std", + "cw-utils", + "multisig-contract-common", + "schemars", + "serde", +] + +[[package]] +name = "coconut-test" +version = "0.1.0" +dependencies = [ + "bandwidth-claim-contract", + "coconut-bandwidth", + "coconut-bandwidth-contract-common", + "coconut-dkg", + "coconut-dkg-common", + "cosmwasm-std", + "cosmwasm-storage", + "cw-controllers", + "cw-multi-test", + "cw-storage-plus", + "cw-utils", + "cw3", + "cw3-flex-multisig", + "cw4", + "cw4-group", + "multisig-contract-common", + "schemars", + "serde", + "thiserror", +] + +[[package]] +name = "const-oid" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4c78c047431fee22c1a7bb92e00ad095a02a983affe4d8a72e2a2c62c1b94f3" + +[[package]] +name = "contracts-common" +version = "0.1.0" +dependencies = [ + "bs58", + "cosmwasm-std", + "schemars", + "serde", + "thiserror", +] + +[[package]] +name = "cosmwasm-crypto" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb0afef2325df81aadbf9be1233f522ed8f6e91df870c764bc44cca2b1415bd" +dependencies = [ + "digest 0.9.0", + "ed25519-zebra", + "k256", + "rand_core 0.6.3", + "thiserror", +] + +[[package]] +name = "cosmwasm-derive" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b36e527620a2a3e00e46b6e731ab6c9b68d11069c986f7d7be8eba79ef081a4" +dependencies = [ + "syn", +] + +[[package]] +name = "cosmwasm-schema" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "772e80bbad231a47a2068812b723a1ff81dd4a0d56c9391ac748177bea3a61da" +dependencies = [ + "schemars", + "serde_json", +] + +[[package]] +name = "cosmwasm-std" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "875994993c2082a6fcd406937bf0fca21c349e4a624f3810253a14fa83a3a195" +dependencies = [ + "base64", + "cosmwasm-crypto", + "cosmwasm-derive", + "forward_ref", + "schemars", + "serde", + "serde-json-wasm", + "thiserror", + "uint", +] + +[[package]] +name = "cosmwasm-storage" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d18403b07304d15d304dad11040d45bbcaf78d603b4be3fb5e2685c16f9229b5" +dependencies = [ + "cosmwasm-std", + "serde", +] + +[[package]] +name = "cpufeatures" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320" +dependencies = [ + "libc", +] + +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + +[[package]] +name = "crypto" +version = "0.1.0" +dependencies = [ + "bs58", + "ed25519-dalek", + "nymsphinx-types", + "pemstore", + "rand", + "subtle-encoding", + "x25519-dalek", +] + +[[package]] +name = "crypto-bigint" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c6a1d5fa1de37e071642dfa44ec552ca5b299adb128fab16138e24b548fd21" +dependencies = [ + "generic-array 0.14.5", + "rand_core 0.6.3", + "subtle 2.4.1", + "zeroize", +] + +[[package]] +name = "crypto-mac" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4434400df11d95d556bac068ddfedd482915eb18fe8bea89bc80b6e4b1c179e5" +dependencies = [ + "generic-array 0.12.4", + "subtle 1.0.0", +] + +[[package]] +name = "crypto-mac" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1d1a86f49236c215f271d40892d5fc950490551400b02ef360692c29815c714" +dependencies = [ + "generic-array 0.14.5", + "subtle 2.4.1", +] + +[[package]] +name = "ctr" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "049bb91fb4aaf0e3c7efa6cd5ef877dbbbd15b39dad06d9948de4ec8a75761ea" +dependencies = [ + "cipher", +] + +[[package]] +name = "curve25519-dalek" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b9fdf9972b2bd6af2d913799d9ebc165ea4d2e65878e329d9c6b372c4491b61" +dependencies = [ + "byteorder", + "digest 0.9.0", + "rand_core 0.5.1", + "subtle 2.4.1", + "zeroize", +] + +[[package]] +name = "cw-controllers" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f0bc6019b4d3d81e11f5c384bcce7173e2210bd654d75c6c9668e12cca05dfa" +dependencies = [ + "cosmwasm-std", + "cw-storage-plus", + "cw-utils", + "schemars", + "serde", + "thiserror", +] + +[[package]] +name = "cw-multi-test" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3f9a8ab7c3c29ec93cb7a39ce4b14a05e053153b4a17ef7cf2246af1b7c087e" +dependencies = [ + "anyhow", + "cosmwasm-std", + "cosmwasm-storage", + "cw-storage-plus", + "cw-utils", + "derivative", + "itertools", + "prost", + "schemars", + "serde", + "thiserror", +] + +[[package]] +name = "cw-storage-plus" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "648b1507290bbc03a8d88463d7cd9b04b1fa0155e5eef366c4fa052b9caaac7a" +dependencies = [ + "cosmwasm-std", + "schemars", + "serde", +] + +[[package]] +name = "cw-utils" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dbaecb78c8e8abfd6b4258c7f4fbeb5c49a5e45ee4d910d3240ee8e1d714e1b" +dependencies = [ + "cosmwasm-std", + "schemars", + "serde", + "thiserror", +] + +[[package]] +name = "cw2" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04cf4639517490dd36b333bbd6c4fbd92e325fd0acf4683b41753bc5eb63bfc1" +dependencies = [ + "cosmwasm-std", + "cw-storage-plus", + "schemars", + "serde", +] + +[[package]] +name = "cw3" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe19462a7f644ba60c19d3443cb90d00c50d9b6b3b0a3a7fca93df8261af979b" +dependencies = [ + "cosmwasm-std", + "cw-utils", + "schemars", + "serde", +] + +[[package]] +name = "cw3-fixed-multisig" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df54aa54c13f405ec4ab36b6217538bc957d439eee58f89312db05a79caf6706" +dependencies = [ + "cosmwasm-std", + "cw-storage-plus", + "cw-utils", + "cw2", + "cw3", + "schemars", + "serde", + "thiserror", +] + +[[package]] +name = "cw3-flex-multisig" +version = "0.13.1" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-multi-test", + "cw-storage-plus", + "cw-utils", + "cw2", + "cw3", + "cw3-fixed-multisig", + "cw4", + "cw4-group", + "multisig-contract-common", + "schemars", + "serde", + "thiserror", +] + +[[package]] +name = "cw4" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0acc3549d5ce11c6901b3a676f2e2628684722197054d97cd0101ea174ed5cbd" +dependencies = [ + "cosmwasm-std", + "cw-storage-plus", + "schemars", + "serde", +] + +[[package]] +name = "cw4-group" +version = "0.13.4" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-controllers", + "cw-storage-plus", + "cw-utils", + "cw2", + "cw4", + "schemars", + "serde", + "thiserror", +] + +[[package]] +name = "der" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6919815d73839e7ad218de758883aae3a257ba6759ce7a9992501efbb53d705c" +dependencies = [ + "const-oid", +] + +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "digest" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5" +dependencies = [ + "generic-array 0.12.4", +] + +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array 0.14.5", +] + +[[package]] +name = "dyn-clone" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee2626afccd7561a06cf1367e2950c4718ea04565e20fb5029b6c7d8ad09abcf" + +[[package]] +name = "ecdsa" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0d69ae62e0ce582d56380743515fefaf1a8c70cec685d9677636d7e30ae9dc9" +dependencies = [ + "der", + "elliptic-curve", + "rfc6979", + "signature", +] + +[[package]] +name = "ed25519" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9c280362032ea4203659fc489832d0204ef09f247a0506f170dafcac08c369" +dependencies = [ + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c762bae6dcaf24c4c84667b8579785430908723d5c889f469d76a41d59cc7a9d" +dependencies = [ + "curve25519-dalek", + "ed25519", + "rand", + "serde", + "sha2", + "zeroize", +] + +[[package]] +name = "ed25519-zebra" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "403ef3e961ab98f0ba902771d29f842058578bb1ce7e3c59dad5a6a93e784c69" +dependencies = [ + "curve25519-dalek", + "hex", + "rand_core 0.6.3", + "serde", + "sha2", + "thiserror", + "zeroize", +] + +[[package]] +name = "either" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" + +[[package]] +name = "elliptic-curve" +version = "0.11.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25b477563c2bfed38a3b7a60964c49e058b2510ad3f12ba3483fd8f62c2306d6" +dependencies = [ + "base16ct", + "crypto-bigint", + "der", + "ff", + "generic-array 0.14.5", + "group", + "rand_core 0.6.3", + "sec1", + "subtle 2.4.1", + "zeroize", +] + +[[package]] +name = "enum-iterator" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4eeac5c5edb79e4e39fe8439ef35207780a11f69c52cbe424ce3dfad4cb78de6" +dependencies = [ + "enum-iterator-derive", +] + +[[package]] +name = "enum-iterator-derive" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c134c37760b27a871ba422106eedbb8247da973a09e82558bf26d619c882b159" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "ff" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "131655483be284720a17d74ff97592b8e76576dc25563148601df2d7c9080924" +dependencies = [ + "rand_core 0.6.3", + "subtle 2.4.1", +] + +[[package]] +name = "form_urlencoded" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" +dependencies = [ + "matches", + "percent-encoding", +] + +[[package]] +name = "forward_ref" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8cbd1169bd7b4a0a20d92b9af7a7e0422888bd38a6f5ec29c1fd8c1558a272e" + +[[package]] +name = "generic-array" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffdf9f34f1447443d37393cc6c2b8313aebddcd96906caf34e54c68d8e57d7bd" +dependencies = [ + "typenum", +] + +[[package]] +name = "generic-array" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.10.2+wasi-snapshot-preview1", +] + +[[package]] +name = "getset" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e45727250e75cc04ff2846a66397da8ef2b3db8e40e0cef4df67950a07621eb9" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "git2" +version = "0.13.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f29229cc1b24c0e6062f6e742aa3e256492a5323365e5ed3413599f8a5eff7d6" +dependencies = [ + "bitflags", + "libc", + "libgit2-sys", + "log", + "url", +] + +[[package]] +name = "group" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc5ac374b108929de78460075f3dc439fa66df9d8fc77e8f12caa5165fcf0c89" +dependencies = [ + "ff", + "rand_core 0.6.3", + "subtle 2.4.1", +] + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hkdf" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01706d578d5c281058480e673ae4086a9f4710d8df1ad80a5b03e39ece5f886b" +dependencies = [ + "digest 0.9.0", + "hmac", +] + +[[package]] +name = "hmac" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2a2320eb7ec0ebe8da8f744d7812d9fc4cb4d09344ac01898dbcb6a20ae69b" +dependencies = [ + "crypto-mac 0.11.1", + "digest 0.9.0", +] + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "humantime-serde" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57a3db5ea5923d99402c94e9feb261dc5ee9b4efa158b0315f788cf549cc200c" +dependencies = [ + "humantime", + "serde", +] + +[[package]] +name = "idna" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" +dependencies = [ + "matches", + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "itertools" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9a9d19fa1e79b6215ff29b9d6880b706147f16e9b1dbb1e4e5947b5b02bc5e3" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35" + +[[package]] +name = "jobserver" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af25a77299a7f711a01975c35a6a424eb6862092cc2d6c72c4ed6cbc56dfc1fa" +dependencies = [ + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cc9ffccd38c451a86bf13657df244e9c3f37493cce8e5e21e940963777acc84" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "k256" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19c3a5e0a0b8450278feda242592512e09f61c72e018b8cd5c859482802daf2d" +dependencies = [ + "cfg-if", + "ecdsa", + "elliptic-curve", + "sec1", + "sha2", +] + +[[package]] +name = "keystream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33070833c9ee02266356de0c43f723152bd38bd96ddf52c82b3af10c9138b28" + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.112" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b03d17f364a3a042d5e5d46b053bbbf82c92c9430c592dd4c064dc6ee997125" + +[[package]] +name = "libgit2-sys" +version = "0.12.26+1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19e1c899248e606fbfe68dcb31d8b0176ebab833b103824af31bddf4b7457494" +dependencies = [ + "cc", + "libc", + "libz-sys", + "pkg-config", +] + +[[package]] +name = "libm" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7d73b3f436185384286bd8098d17ec07c9a7d2388a6599f824d8502b529702a" + +[[package]] +name = "libz-sys" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de5435b8549c16d423ed0c03dbaafe57cf6c3344744f1242520d59c9d8ecec66" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "lioness" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ae926706ba42c425c9457121178330d75e273df2e82e28b758faf3de3a9acb9" +dependencies = [ + "arrayref", + "blake2", + "chacha", + "keystream", +] + +[[package]] +name = "log" +version = "0.4.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6389c490849ff5bc16be905ae24bc913a9c8892e19b2341dbc175e14c341c2b8" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "matches" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" + +[[package]] +name = "mixnet-contract" +version = "1.1.2" +dependencies = [ + "bs58", + "cosmwasm-schema", + "cosmwasm-std", + "cosmwasm-storage", + "crypto", + "cw-storage-plus", + "mixnet-contract-common", + "rand_chacha", + "schemars", + "serde", + "thiserror", + "time 0.3.6", + "vergen", + "vesting-contract-common", +] + +[[package]] +name = "mixnet-contract-common" +version = "0.1.0" +dependencies = [ + "bs58", + "contracts-common", + "cosmwasm-std", + "humantime-serde", + "log", + "schemars", + "serde", + "serde_json", + "serde_repr", + "thiserror", + "time 0.3.6", +] + +[[package]] +name = "multisig-contract-common" +version = "0.1.0" +dependencies = [ + "cosmwasm-std", + "cw-utils", + "cw3", + "cw3-fixed-multisig", + "cw4", + "schemars", + "serde", +] + +[[package]] +name = "num-integer" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "num_threads" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71a1eb3a36534514077c1e079ada2fb170ef30c47d203aa6916138cf882ecd52" +dependencies = [ + "libc", +] + +[[package]] +name = "nymsphinx-types" +version = "0.1.0" +dependencies = [ + "sphinx", +] + +[[package]] +name = "once_cell" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da32515d9f6e6e489d7bc9d84c71b060db7247dc035bbe44eac88cf87486d8d5" + +[[package]] +name = "opaque-debug" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c" + +[[package]] +name = "opaque-debug" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" + +[[package]] +name = "pem" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd56cbd21fea48d0c440b41cd69c589faacade08c992d9a54e471b79d0fd13eb" +dependencies = [ + "base64", + "once_cell", + "regex", +] + +[[package]] +name = "pemstore" +version = "0.1.0" +dependencies = [ + "pem", +] + +[[package]] +name = "percent-encoding" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" + +[[package]] +name = "pkcs8" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cabda3fb821068a9a4fab19a683eac3af12edf0f34b94a8be53c4972b8149d0" +dependencies = [ + "der", + "spki", + "zeroize", +] + +[[package]] +name = "pkg-config" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58893f751c9b0412871a09abd62ecd2a00298c6c83befa223ef98c52aef40cbe" + +[[package]] +name = "ppv-lite86" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "prost" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "444879275cb4fd84958b1a1d5420d15e6fcf7c235fe47f053c9c2a80aceb6001" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-derive" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9cc1a3263e07e0bf68e96268f37665207b49560d98739662cdfaae215c720fe" +dependencies = [ + "anyhow", + "itertools", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "quote" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47aa80447ce4daf1717500037052af176af5d38cc3e571d9ec1c7353fc10c87d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha", + "rand_core 0.5.1", + "rand_hc", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", +] + +[[package]] +name = "rand_core" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" +dependencies = [ + "getrandom 0.2.3", +] + +[[package]] +name = "rand_distr" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9e9532ada3929fb8b2e9dbe28d1e06c9b2cc65813f074fcb6bd5fbefeff9d56" +dependencies = [ + "num-traits", + "rand", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "regex" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b" +dependencies = [ + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244" + +[[package]] +name = "rfc6979" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96ef608575f6392792f9ecf7890c00086591d29a83910939d430753f7c050525" +dependencies = [ + "crypto-bigint", + "hmac", + "zeroize", +] + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + +[[package]] +name = "rustversion" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2cc38e8fa666e2de3c4aba7edeb5ffc5246c1c2ed0e3d17e560aeeba736b23f" + +[[package]] +name = "ryu" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f" + +[[package]] +name = "schemars" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6b5a3c80cea1ab61f4260238409510e814e38b4b563c06044edf91e7dc070e3" +dependencies = [ + "dyn-clone", + "schemars_derive", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41ae4dce13e8614c46ac3c38ef1c0d668b101df6ac39817aebdaa26642ddae9b" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn", +] + +[[package]] +name = "sec1" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08da66b8b0965a5555b6bd6639e68ccba85e1e2506f5fbb089e93f8a04e1a2d1" +dependencies = [ + "der", + "generic-array 0.14.5", + "pkcs8", + "subtle 2.4.1", + "zeroize", +] + +[[package]] +name = "semver" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "568a8e6258aa33c13358f81fd834adb854c6f7c9468520910a9b1e8fac068012" + +[[package]] +name = "serde" +version = "1.0.133" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97565067517b60e2d1ea8b268e59ce036de907ac523ad83a0475da04e818989a" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde-json-wasm" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479b4dbc401ca13ee8ce902851b834893251404c4f3c65370a49e047a6be09a5" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_derive" +version = "1.0.133" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed201699328568d8d08208fdd080e3ff594e6c422e438b6705905da01005d537" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_derive_internals" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dbab34ca63057a1f15280bdf3c39f2b1eb1b54c17e98360e511637aef7418c6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee2bb9cd061c5865d345bb02ca49fcef1391741b672b54a0bf7b679badec3142" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_repr" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98d0516900518c29efa217c298fa1f4e6c6ffc85ae29fd7f4ee48f176e1a9ed5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "sha2" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" +dependencies = [ + "block-buffer", + "cfg-if", + "cpufeatures", + "digest 0.9.0", + "opaque-debug 0.3.0", +] + +[[package]] +name = "signature" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2807892cfa58e081aa1f1111391c7a0649d4fa127a4ffbe34bcbfb35a1171a4" +dependencies = [ + "digest 0.9.0", + "rand_core 0.6.3", +] + +[[package]] +name = "sphinx" +version = "0.1.0" +source = "git+https://github.com/nymtech/sphinx?rev=e05a1992522ed0afd3c6fcac160313ffc9bb306a#e05a1992522ed0afd3c6fcac160313ffc9bb306a" +dependencies = [ + "aes", + "arrayref", + "blake2", + "bs58", + "byteorder", + "chacha", + "curve25519-dalek", + "digest 0.9.0", + "hkdf", + "hmac", + "lioness", + "log", + "rand", + "rand_distr", + "sha2", + "subtle 2.4.1", +] + +[[package]] +name = "spki" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d01ac02a6ccf3e07db148d2be087da624fea0221a16152ed01f0496a6b0a27" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "subtle" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d67a5a62ba6e01cb2192ff309324cb4875d0c451d55fe2319433abe7a05a8ee" + +[[package]] +name = "subtle" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" + +[[package]] +name = "subtle-encoding" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dcb1ed7b8330c5eed5441052651dd7a12c75e2ed88f2ec024ae1fa3a5e59945" +dependencies = [ + "zeroize", +] + +[[package]] +name = "syn" +version = "1.0.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a684ac3dcd8913827e18cd09a68384ee66c1de24157e3c556c9ab16d85695fb7" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "synstructure" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "unicode-xid", +] + +[[package]] +name = "thiserror" +version = "1.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "time" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "time" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d54b9298e05179c335de2b9645d061255bcd5155f843b3e328d2cfe0a5b413" +dependencies = [ + "itoa", + "libc", + "num_threads", + "time-macros", +] + +[[package]] +name = "time-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25eb0ca3468fc0acc11828786797f6ef9aa1555e4a211a60d64cc8e4d1be47d6" + +[[package]] +name = "tinyvec" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c1c1d5a42b6245520c249549ec267180beaffcc0615401ac8e31853d4b6d8d2" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" + +[[package]] +name = "typenum" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" + +[[package]] +name = "uint" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f03af7ccf01dd611cc450a0d10dbc9b745770d096473e2faf0ca6e2d66d1e0" +dependencies = [ + "byteorder", + "crunchy", + "hex", + "static_assertions", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a01404663e3db436ed2746d9fefef640d868edae3cceb81c3b8d5732fda678f" + +[[package]] +name = "unicode-normalization" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-xid" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" + +[[package]] +name = "url" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" +dependencies = [ + "form_urlencoded", + "idna", + "matches", + "percent-encoding", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "vergen" +version = "5.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cf88d94e969e7956d924ba70741316796177fa0c79a2c9f4ab04998d96e966e" +dependencies = [ + "anyhow", + "cfg-if", + "chrono", + "enum-iterator", + "getset", + "git2", + "rustc_version", + "rustversion", + "thiserror", +] + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "vesting-contract" +version = "1.1.2" +dependencies = [ + "contracts-common", + "cosmwasm-std", + "cw-storage-plus", + "mixnet-contract-common", + "schemars", + "serde", + "thiserror", + "vergen", + "vesting-contract-common", +] + +[[package]] +name = "vesting-contract-common" +version = "0.1.0" +dependencies = [ + "contracts-common", + "cosmwasm-std", + "mixnet-contract-common", + "schemars", + "serde", +] + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "wasi" +version = "0.10.2+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" + +[[package]] +name = "wasm-bindgen" +version = "0.2.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "632f73e236b219150ea279196e54e610f5dbafa5d61786303d4da54f84e47fce" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a317bf8f9fba2476b4b2c85ef4c4af8ff39c3c7f0cdfeed4f82c34a880aa837b" +dependencies = [ + "bumpalo", + "lazy_static", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56146e7c495528bf6587663bea13a8eb588d39b36b679d83972e1a2dbbdacf9" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7803e0eea25835f8abdc585cd3021b3deb11543c6fe226dcd30b228857c5c5ab" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0237232789cf037d5480773fe568aac745bfe2afbc11a863e97901780a6b47cc" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "x25519-dalek" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a0c105152107e3b96f6a00a65e86ce82d9b125230e1c4302940eca58ff71f4f" +dependencies = [ + "curve25519-dalek", + "rand_core 0.5.1", + "zeroize", +] + +[[package]] +name = "zeroize" +version = "1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c394b5bd0c6f669e7275d9c20aa90ae064cb22e75a1cad54e1b34088034b149f" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f8f187641dad4f680d25c4bfc4225b418165984179f26ca76ec4fb6441d3a17" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] diff --git a/contracts/mixnet/Cargo.toml b/contracts/mixnet/Cargo.toml index d4d0ac43a5..6dc7273c18 100644 --- a/contracts/mixnet/Cargo.toml +++ b/contracts/mixnet/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mixnet-contract" -version = "1.1.0" +version = "1.1.2" authors = ["Dave Hrycyszyn "] edition = "2021" diff --git a/contracts/vesting/Cargo.toml b/contracts/vesting/Cargo.toml index e13491a4f6..dd8d2966f0 100644 --- a/contracts/vesting/Cargo.toml +++ b/contracts/vesting/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "vesting-contract" -version = "1.1.0" +version = "1.1.2" authors = ["Drazen Urch "] edition = "2021" diff --git a/envs/mainnet.env b/envs/mainnet.env index d53b7f4b8b..751081adb8 100644 --- a/envs/mainnet.env +++ b/envs/mainnet.env @@ -9,7 +9,7 @@ MIX_DENOM_DISPLAY=nym STAKE_DENOM=unyx STAKE_DENOM_DISPLAY=nyx DENOMS_EXPONENT=6 -MIXNET_CONTRACT_ADDRESS=n14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9sjyvg3g +MIXNET_CONTRACT_ADDRESS=n17srjznxl9dvzdkpwpw24gg668wc73val88a6m5ajg6ankwvz9wtst0cznr VESTING_CONTRACT_ADDRESS=n1nc5tatafv6eyq7llkr2gv50ff9e22mnf70qgjlv737ktmt4eswrq73f2nw BANDWIDTH_CLAIM_CONTRACT_ADDRESS=n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0 COCONUT_BANDWIDTH_CONTRACT_ADDRESS=n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0 @@ -17,5 +17,5 @@ MULTISIG_CONTRACT_ADDRESS=n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0 COCONUT_DKG_CONTRACT_ADDRESS=n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0 REWARDING_VALIDATOR_ADDRESS=n10yyd98e2tuwu0f7ypz9dy3hhjw7v772q6287gy STATISTICS_SERVICE_DOMAIN_ADDRESS="https://mainnet-stats.nymte.ch:8090" -NYMD_VALIDATOR="https://rpc.nyx.nodes.guru/" +NYMD_VALIDATOR="https://rpc.nymtech.net"; API_VALIDATOR="https://validator.nymtech.net/api/" diff --git a/explorer/.env.prod b/explorer/.env.prod index e053bd085a..b1b1fe0d4c 100644 --- a/explorer/.env.prod +++ b/explorer/.env.prod @@ -1,6 +1,6 @@ EXPLORER_API_URL=https://explorer.nymtech.net/api/v1 VALIDATOR_API_URL=https://validator.nymtech.net -VALIDATOR_URL=https://rpc.nyx.nodes.guru +VALIDATOR_URL=https://rpc.nymtech.net BIG_DIPPER_URL=https://blocks.nymtech.net CURRENCY_DENOM=unym CURRENCY_STAKING_DENOM=unyx diff --git a/gateway/Cargo.toml b/gateway/Cargo.toml index 8be0948a85..e2137a574c 100644 --- a/gateway/Cargo.toml +++ b/gateway/Cargo.toml @@ -3,7 +3,7 @@ [package] name = "nym-gateway" -version = "1.1.1" +version = "1.1.2" authors = [ "Dave Hrycyszyn ", "Jędrzej Stuczyński ", diff --git a/gateway/src/config/mod.rs b/gateway/src/config/mod.rs index 9b04052217..ac4f734dad 100644 --- a/gateway/src/config/mod.rs +++ b/gateway/src/config/mod.rs @@ -5,6 +5,7 @@ use crate::config::template::config_template; use config::defaults::{DEFAULT_CLIENT_LISTENING_PORT, DEFAULT_MIX_LISTENING_PORT}; use config::NymConfig; use log::error; +use network_defaults::mainnet::{API_VALIDATOR, NYMD_VALIDATOR, STATISTICS_SERVICE_DOMAIN_ADDRESS}; use serde::{Deserialize, Serialize}; use std::net::IpAddr; use std::path::PathBuf; @@ -416,9 +417,9 @@ impl Default for Gateway { private_sphinx_key_file: Default::default(), public_sphinx_key_file: Default::default(), enabled_statistics: false, - statistics_service_url: Url::from_str("http://127.0.0.1").unwrap(), - validator_api_urls: vec![], - validator_nymd_urls: vec![], + statistics_service_url: Url::from_str(STATISTICS_SERVICE_DOMAIN_ADDRESS).expect("Invalid default statistics service URL"), + validator_api_urls: vec![Url::from_str(API_VALIDATOR).expect("Invalid default API URL")], + validator_nymd_urls: vec![Url::from_str(NYMD_VALIDATOR).expect("Invalid default nymd URL")], cosmos_mnemonic: bip39::Mnemonic::from_str("exact antique hybrid width raise anchor puzzle degree fee quit long crack net vague hip despair write put useless civil mechanic broom music day").unwrap(), nym_root_directory: Config::default_root_directory(), persistent_storage: Default::default(), 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 7a23c13655..9e13fd2e69 100644 --- a/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs +++ b/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs @@ -350,7 +350,7 @@ where match client_protocol { None => { warn!("the client we're connected to has not specified its protocol version. It's probably running version < 1.1.X, but that's still fine for now. It will become a hard error in 1.2.0"); - // note: in 1.2.0 we will have to return a hard error here + // note: in +1.2.0 we will have to return a hard error here Ok(()) } Some(v) if v != PROTOCOL_VERSION => { diff --git a/mixnode/Cargo.toml b/mixnode/Cargo.toml index 0806354403..6c958811d9 100644 --- a/mixnode/Cargo.toml +++ b/mixnode/Cargo.toml @@ -3,7 +3,7 @@ [package] name = "nym-mixnode" -version = "1.1.1" +version = "1.1.2" authors = [ "Dave Hrycyszyn ", "Jędrzej Stuczyński ", diff --git a/mixnode/src/config/mod.rs b/mixnode/src/config/mod.rs index 23a80a5127..3f5208cee8 100644 --- a/mixnode/src/config/mod.rs +++ b/mixnode/src/config/mod.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use crate::config::template::config_template; +use config::defaults::mainnet::API_VALIDATOR; use config::defaults::{ DEFAULT_HTTP_API_LISTENING_PORT, DEFAULT_MIX_LISTENING_PORT, DEFAULT_VERLOC_LISTENING_PORT, }; @@ -411,7 +412,7 @@ impl Default for MixNode { public_identity_key_file: Default::default(), private_sphinx_key_file: Default::default(), public_sphinx_key_file: Default::default(), - validator_api_urls: vec![], + validator_api_urls: vec![Url::from_str(API_VALIDATOR).expect("Invalid default API URL")], nym_root_directory: Config::default_root_directory(), wallet_address: "nymXXXXXXXX".to_string(), } diff --git a/mixnode/src/node/packet_delayforwarder.rs b/mixnode/src/node/packet_delayforwarder.rs index 9be8175bed..81e0303afd 100644 --- a/mixnode/src/node/packet_delayforwarder.rs +++ b/mixnode/src/node/packet_delayforwarder.rs @@ -194,7 +194,7 @@ mod tests { ]; SphinxPacketBuilder::new() .with_payload_size(size.payload_size()) - .build_packet(b"foomp".to_vec(), &route, &destination, &delays) + .build_packet(b"foomp", &route, &destination, &delays) .unwrap() } diff --git a/nym-connect/CHANGELOG.md b/nym-connect/CHANGELOG.md index 8c685a7226..1512078d20 100644 --- a/nym-connect/CHANGELOG.md +++ b/nym-connect/CHANGELOG.md @@ -1,5 +1,7 @@ ## UNRELEASED +## [nym-connect-v1.1.2](https://github.com/nymtech/nym/tree/nym-connect-v1.1.2) (2022-11-29) + - socks5-client: fix error with client failing and disconnecting unnecessarily. ## [nym-connect-v1.1.1](https://github.com/nymtech/nym/tree/nym-connect-v1.1.1) (2022-11-29) diff --git a/nym-connect/Cargo.lock b/nym-connect/Cargo.lock index 790daff951..68316bacc5 100644 --- a/nym-connect/Cargo.lock +++ b/nym-connect/Cargo.lock @@ -123,9 +123,9 @@ checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" [[package]] name = "async-trait" -version = "0.1.57" +version = "0.1.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76464446b8bc32758d7e88ee1a804d9914cd9b1cb264c029899680b0be29826f" +checksum = "1e805d94e6b5001b651426cf4cd446b1ab5f319d27bab5c644f61de0a804360c" dependencies = [ "proc-macro2", "quote", @@ -165,6 +165,15 @@ dependencies = [ "num-traits", ] +[[package]] +name = "atoi" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c57d12312ff59c811c0643f4d80830505833c9ffaebd193d819392b265be8e" +dependencies = [ + "num-traits", +] + [[package]] name = "attohttpc" version = "0.22.0" @@ -630,11 +639,13 @@ dependencies = [ [[package]] name = "client-core" -version = "1.1.1" +version = "1.1.2" dependencies = [ + "async-trait", "client-connections", "config", "crypto", + "dashmap", "dirs", "futures", "gateway-client", @@ -647,17 +658,21 @@ dependencies = [ "pemstore", "rand 0.7.3", "serde", - "sled", + "serde_json", + "sqlx 0.6.2", "tap", "task", "thiserror", + "time 0.3.17", "tokio", + "tokio-stream", "topology", "url", "validator-client", "wasm-bindgen", "wasm-bindgen-futures", "wasm-timer", + "wasm-utils", ] [[package]] @@ -938,7 +953,16 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49fc9a695bca7f35f5f4c15cddc84415f66a74ea78eef08e90c5024f2b540e23" dependencies = [ - "crc-catalog", + "crc-catalog 1.1.1", +] + +[[package]] +name = "crc" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53757d12b596c16c78b83458d732a5d1a17ab3f53f2f7412f6fb57cc8a140ab3" +dependencies = [ + "crc-catalog 2.1.0", ] [[package]] @@ -947,6 +971,12 @@ version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccaeedb56da03b09f598226e25e80088cb4cd25f316e6e4df7d695f0feeb1403" +[[package]] +name = "crc-catalog" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d0165d2900ae6778e36e80bbc4da3b5eefccee9ba939761f9c2882a5d9af3ff" + [[package]] name = "crc32fast" version = "1.3.2" @@ -962,7 +992,7 @@ version = "0.1.0" dependencies = [ "async-trait", "log", - "sqlx", + "sqlx 0.5.13", "thiserror", "tokio", ] @@ -990,20 +1020,6 @@ dependencies = [ "crossbeam-utils", ] -[[package]] -name = "crossbeam-epoch" -version = "0.9.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "045ebe27666471bb549370b4b0b3e51b07f56325befa4284db65fc89c02511b1" -dependencies = [ - "autocfg 1.1.0", - "cfg-if", - "crossbeam-utils", - "memoffset", - "once_cell", - "scopeguard", -] - [[package]] name = "crossbeam-queue" version = "0.3.6" @@ -1126,7 +1142,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1a816186fa68d9e426e3cb4ae4dff1fcd8e4a2c34b781bf7a822574a0d0aac8" dependencies = [ - "sct", + "sct 0.6.1", ] [[package]] @@ -1286,6 +1302,19 @@ dependencies = [ "syn", ] +[[package]] +name = "dashmap" +version = "5.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "907076dfda823b0b36d2a1bb5f90c96660a5bbcd7729e10727f07858f22c4edc" +dependencies = [ + "cfg-if", + "hashbrown 0.12.3", + "lock_api", + "once_cell", + "parking_lot_core 0.9.3", +] + [[package]] name = "deflate" version = "0.7.20" @@ -1415,18 +1444,18 @@ dependencies = [ "zeroize", ] -[[package]] -name = "doc-comment" -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.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03d8c417d7a8cb362e0c37e5d815f5eb7c37f79ff93707329d5a194e42e54ca0" + [[package]] name = "dtoa" version = "0.4.8" @@ -1722,16 +1751,6 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8cbd1169bd7b4a0a20d92b9af7a7e0422888bd38a6f5ec29c1fd8c1558a272e" -[[package]] -name = "fs2" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213" -dependencies = [ - "libc", - "winapi", -] - [[package]] name = "fuchsia-cprng" version = "0.1.1" @@ -2311,6 +2330,9 @@ name = "hashbrown" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash", +] [[package]] name = "hashlink" @@ -2321,6 +2343,15 @@ dependencies = [ "hashbrown 0.11.2", ] +[[package]] +name = "hashlink" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69fe1fcf8b4278d860ad0548329f892a3631fb63f82574df68275f34cdbe0ffa" +dependencies = [ + "hashbrown 0.12.3", +] + [[package]] name = "headers" version = "0.3.8" @@ -2540,9 +2571,9 @@ dependencies = [ "hyper-rustls", "rustls-native-certs", "tokio", - "tokio-rustls", + "tokio-rustls 0.22.0", "tower-service", - "webpki", + "webpki 0.21.4", ] [[package]] @@ -2555,12 +2586,12 @@ dependencies = [ "futures-util", "hyper", "log", - "rustls", + "rustls 0.19.1", "rustls-native-certs", "tokio", - "tokio-rustls", - "webpki", - "webpki-roots", + "tokio-rustls 0.22.0", + "webpki 0.21.4", + "webpki-roots 0.21.1", ] [[package]] @@ -2794,9 +2825,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.55" +version = "0.3.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cc9ffccd38c451a86bf13657df244e9c3f37493cce8e5e21e940963777acc84" +checksum = "49409df3e3bf0856b916e2ceaca09ee28e6871cf7d9ce97a692cacfdb2a25a47" dependencies = [ "wasm-bindgen", ] @@ -3118,7 +3149,7 @@ dependencies = [ "serde_json", "serde_repr", "thiserror", - "time 0.3.14", + "time 0.3.17", ] [[package]] @@ -3311,18 +3342,9 @@ dependencies = [ "syn", ] -[[package]] -name = "num_threads" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" -dependencies = [ - "libc", -] - [[package]] name = "nym-connect" -version = "1.1.0" +version = "1.1.2" dependencies = [ "bip39", "client-core", @@ -3355,7 +3377,7 @@ dependencies = [ [[package]] name = "nym-socks5-client" -version = "1.1.1" +version = "1.1.2" dependencies = [ "clap", "client-connections", @@ -3381,7 +3403,7 @@ dependencies = [ "proxy-helpers", "rand 0.7.3", "serde", - "snafu", + "serde_json", "socks5-requests", "tap", "task", @@ -3430,6 +3452,7 @@ dependencies = [ "nymsphinx-types", "rand 0.7.3", "rand_distr", + "thiserror", "tokio", "topology", ] @@ -3454,6 +3477,7 @@ dependencies = [ "crypto", "nymsphinx-types", "serde", + "thiserror", ] [[package]] @@ -3467,7 +3491,9 @@ dependencies = [ "nymsphinx-types", "rand 0.7.3", "serde", + "thiserror", "topology", + "wasm-bindgen", ] [[package]] @@ -3479,6 +3505,7 @@ dependencies = [ "nymsphinx-params", "nymsphinx-types", "rand 0.7.3", + "thiserror", ] [[package]] @@ -3493,6 +3520,7 @@ dependencies = [ "nymsphinx-params", "nymsphinx-types", "rand 0.7.3", + "thiserror", "topology", ] @@ -4019,7 +4047,7 @@ dependencies = [ "indexmap", "line-wrap", "serde", - "time 0.3.14", + "time 0.3.17", "xml-rs", ] @@ -4589,8 +4617,20 @@ dependencies = [ "base64", "log", "ring", - "sct", - "webpki", + "sct 0.6.1", + "webpki 0.21.4", +] + +[[package]] +name = "rustls" +version = "0.20.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "539a2bfe908f471bfa933876bd1eb6a19cf2176d375f82ef7f99530a40e48c2c" +dependencies = [ + "log", + "ring", + "sct 0.7.0", + "webpki 0.22.0", ] [[package]] @@ -4600,11 +4640,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a07b7c1885bd8ed3831c289b7870b13ef46fe0e856d288c30d9cc17d75a2092" dependencies = [ "openssl-probe", - "rustls", + "rustls 0.19.1", "schannel", "security-framework", ] +[[package]] +name = "rustls-pemfile" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0864aeff53f8c05aa08d86e5ef839d3dfcf07aeba2db32f12db0ef716e87bd55" +dependencies = [ + "base64", +] + [[package]] name = "rustversion" version = "1.0.9" @@ -4689,6 +4738,16 @@ dependencies = [ "untrusted", ] +[[package]] +name = "sct" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "sec1" version = "0.2.1" @@ -4823,9 +4882,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.85" +version = "1.0.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e55a28e3aaef9d5ce0506d0a14dbba8054ddc7e499ef522dd8b26859ec9d4a44" +checksum = "020ff22c755c2ed3f8cf162dbb41a7268d934702f3ed3631656ea597e08fc3db" dependencies = [ "itoa 1.0.3", "ryu", @@ -5012,22 +5071,6 @@ dependencies = [ "autocfg 1.1.0", ] -[[package]] -name = "sled" -version = "0.34.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f96b4737c2ce5987354855aed3797279def4ebf734436c6aa4552cf8e169935" -dependencies = [ - "crc32fast", - "crossbeam-epoch", - "crossbeam-utils", - "fs2", - "fxhash", - "libc", - "log", - "parking_lot 0.11.2", -] - [[package]] name = "smallvec" version = "0.6.14" @@ -5043,27 +5086,6 @@ version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fd0db749597d91ff862fd1d55ea87f7855a744a8425a64695b6fca237d1dad1" -[[package]] -name = "snafu" -version = "0.6.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eab12d3c261b2308b0d80c26fffb58d17eba81a4be97890101f416b478c79ca7" -dependencies = [ - "doc-comment", - "snafu-derive", -] - -[[package]] -name = "snafu-derive" -version = "0.6.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1508efa03c362e23817f96cde18abed596a25219a8b2c66e8db33c03543d315b" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "socket2" version = "0.4.7" @@ -5113,7 +5135,7 @@ dependencies = [ [[package]] name = "sphinx" version = "0.1.0" -source = "git+https://github.com/nymtech/sphinx?rev=c494250f2a78bed33a618d470792418eee932859#c494250f2a78bed33a618d470792418eee932859" +source = "git+https://github.com/nymtech/sphinx?rev=e05a1992522ed0afd3c6fcac160313ffc9bb306a#e05a1992522ed0afd3c6fcac160313ffc9bb306a" dependencies = [ "aes 0.7.5", "arrayref", @@ -5169,14 +5191,35 @@ dependencies = [ "unicode_categories", ] +[[package]] +name = "sqlformat" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87e292b4291f154971a43c3774364e2cbcaec599d3f5bf6fa9d122885dbc38a" +dependencies = [ + "itertools", + "nom", + "unicode_categories", +] + [[package]] name = "sqlx" version = "0.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "551873805652ba0d912fec5bbb0f8b4cdd96baf8e2ebf5970e5671092966019b" dependencies = [ - "sqlx-core", - "sqlx-macros", + "sqlx-core 0.5.13", + "sqlx-macros 0.5.13", +] + +[[package]] +name = "sqlx" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9249290c05928352f71c077cc44a464d880c63f26f7534728cca008e135c0428" +dependencies = [ + "sqlx-core 0.6.2", + "sqlx-macros 0.6.2", ] [[package]] @@ -5186,11 +5229,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e48c61941ccf5ddcada342cd59e3e5173b007c509e1e8e990dafc830294d9dc5" dependencies = [ "ahash", - "atoi", + "atoi 0.4.0", "bitflags", "byteorder", "bytes", - "crc", + "crc 2.1.0", "crossbeam-queue", "either", "event-listener", @@ -5200,7 +5243,7 @@ dependencies = [ "futures-executor", "futures-intrusive", "futures-util", - "hashlink", + "hashlink 0.7.0", "hex", "indexmap", "itoa 1.0.3", @@ -5211,17 +5254,63 @@ dependencies = [ "once_cell", "paste", "percent-encoding", - "rustls", + "rustls 0.19.1", "sha2 0.10.6", "smallvec 1.9.0", - "sqlformat", - "sqlx-rt", + "sqlformat 0.1.8", + "sqlx-rt 0.5.13", "stringprep", "thiserror", "tokio-stream", "url", - "webpki", - "webpki-roots", + "webpki 0.21.4", + "webpki-roots 0.21.1", +] + +[[package]] +name = "sqlx-core" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcbc16ddba161afc99e14d1713a453747a2b07fc097d2009f4c300ec99286105" +dependencies = [ + "ahash", + "atoi 1.0.0", + "bitflags", + "byteorder", + "bytes", + "crc 3.0.0", + "crossbeam-queue", + "dotenvy", + "either", + "event-listener", + "flume", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "hashlink 0.8.1", + "hex", + "indexmap", + "itoa 1.0.3", + "libc", + "libsqlite3-sys", + "log", + "memchr", + "once_cell", + "paste", + "percent-encoding", + "rustls 0.20.7", + "rustls-pemfile", + "sha2 0.10.6", + "smallvec 1.9.0", + "sqlformat 0.2.0", + "sqlx-rt 0.6.2", + "stringprep", + "thiserror", + "tokio-stream", + "url", + "webpki-roots 0.22.5", ] [[package]] @@ -5237,8 +5326,27 @@ dependencies = [ "proc-macro2", "quote", "sha2 0.10.6", - "sqlx-core", - "sqlx-rt", + "sqlx-core 0.5.13", + "sqlx-rt 0.5.13", + "syn", + "url", +] + +[[package]] +name = "sqlx-macros" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b850fa514dc11f2ee85be9d055c512aa866746adfacd1cb42d867d68e6a5b0d9" +dependencies = [ + "dotenvy", + "either", + "heck 0.4.0", + "once_cell", + "proc-macro2", + "quote", + "sha2 0.10.6", + "sqlx-core 0.6.2", + "sqlx-rt 0.6.2", "syn", "url", ] @@ -5251,7 +5359,18 @@ checksum = "4db708cd3e459078f85f39f96a00960bd841f66ee2a669e90bf36907f5a79aae" dependencies = [ "once_cell", "tokio", - "tokio-rustls", + "tokio-rustls 0.22.0", +] + +[[package]] +name = "sqlx-rt" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24c5b2d25fa654cc5f841750b8e1cdedbe21189bf9a9382ee90bfa9dd3562396" +dependencies = [ + "once_cell", + "tokio", + "tokio-rustls 0.23.4", ] [[package]] @@ -5469,6 +5588,8 @@ dependencies = [ "log", "thiserror", "tokio", + "wasm-bindgen", + "wasm-bindgen-futures", ] [[package]] @@ -5514,7 +5635,7 @@ dependencies = [ "tauri-utils", "tempfile", "thiserror", - "time 0.3.14", + "time 0.3.17", "tokio", "url", "uuid 1.1.2", @@ -5561,7 +5682,7 @@ dependencies = [ "sha2 0.10.6", "tauri-utils", "thiserror", - "time 0.3.14", + "time 0.3.17", "uuid 1.1.2", "walkdir", ] @@ -5689,7 +5810,7 @@ dependencies = [ "subtle 2.4.1", "subtle-encoding", "tendermint-proto", - "time 0.3.14", + "time 0.3.17", "zeroize", ] @@ -5722,7 +5843,7 @@ dependencies = [ "serde", "serde_bytes", "subtle-encoding", - "time 0.3.14", + "time 0.3.17", ] [[package]] @@ -5750,7 +5871,7 @@ dependencies = [ "tendermint-config", "tendermint-proto", "thiserror", - "time 0.3.14", + "time 0.3.17", "tokio", "tracing", "url", @@ -5832,27 +5953,37 @@ dependencies = [ [[package]] name = "time" -version = "0.3.14" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c3f9a28b618c3a6b9251b6908e9c99e04b9e5c02e6581ccbb67d59c34ef7f9b" +checksum = "a561bf4617eebd33bca6434b988f39ed798e527f51a1e797d0ee4f61c0a38376" dependencies = [ "itoa 1.0.3", - "libc", - "num_threads", + "js-sys", + "serde", + "time-core", "time-macros", ] [[package]] -name = "time-macros" -version = "0.2.4" +name = "time-core" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42657b1a6f4d817cda8e7a0ace261fe0cc946cf3a80314390b22cc61ae080792" +checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd" + +[[package]] +name = "time-macros" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d967f99f534ca7e495c575c62638eebc2898a8c84c119b89e250477bc4ba16b2" +dependencies = [ + "time-core", +] [[package]] name = "tokio" -version = "1.21.2" +version = "1.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9e03c497dc955702ba729190dc4aac6f2a0ce97f913e5b1b5912fc5039d9099" +checksum = "d76ce4a75fb488c605c54bf610f221cea8b0dafb53333c1a67e8ee199dcd2ae3" dependencies = [ "autocfg 1.1.0", "bytes", @@ -5894,9 +6025,20 @@ version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc6844de72e57df1980054b38be3a9f4702aba4858be64dd700181a8a6d0e1b6" dependencies = [ - "rustls", + "rustls 0.19.1", "tokio", - "webpki", + "webpki 0.21.4", +] + +[[package]] +name = "tokio-rustls" +version = "0.23.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59" +dependencies = [ + "rustls 0.20.7", + "tokio", + "webpki 0.22.0", ] [[package]] @@ -5959,6 +6101,7 @@ dependencies = [ "nymsphinx-addressing", "nymsphinx-types", "rand 0.7.3", + "thiserror", "version-checker", ] @@ -6303,7 +6446,7 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "vesting-contract" -version = "1.1.0" +version = "1.1.2" dependencies = [ "contracts-common", "cosmwasm-std", @@ -6546,13 +6689,32 @@ dependencies = [ "untrusted", ] +[[package]] +name = "webpki" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "webpki-roots" version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aabe153544e473b775453675851ecc86863d2a81d786d741f6b76778f2a48940" dependencies = [ - "webpki", + "webpki 0.21.4", +] + +[[package]] +name = "webpki-roots" +version = "0.22.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368bfe657969fb01238bb756d351dcade285e0f6fcbd36dcb23359a5169975be" +dependencies = [ + "webpki 0.22.0", ] [[package]] diff --git a/nym-connect/package.json b/nym-connect/package.json index 7e7824d6b6..74c152c8a6 100644 --- a/nym-connect/package.json +++ b/nym-connect/package.json @@ -1,6 +1,6 @@ { "name": "@nym/nym-connect", - "version": "1.1.0", + "version": "1.1.2", "main": "index.js", "license": "MIT", "scripts": { @@ -107,4 +107,4 @@ "webpack-favicons": "^1.3.8", "webpack-merge": "^5.8.0" } -} \ No newline at end of file +} diff --git a/nym-connect/src-tauri/Cargo.toml b/nym-connect/src-tauri/Cargo.toml index 25a60e0ec8..b07d377562 100644 --- a/nym-connect/src-tauri/Cargo.toml +++ b/nym-connect/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nym-connect" -version = "1.1.0" +version = "1.1.2" description = "nym-connect" authors = ["Nym Technologies SA"] license = "" @@ -32,7 +32,7 @@ reqwest = { version = "0.11", features = ["json"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" tap = "1.0.1" -tauri = { version = "^1.1.1", features = ["clipboard-write-text", "macos-private-api", "shell-open", "system-tray", "updater", "window-close", "window-start-dragging"] } +tauri = { version = "^1.1.1", features = ["clipboard-write-text", "macos-private-api", "shell-open", "system-tray", "updater", "window-close", "window-minimize", "window-start-dragging"] } tendermint-rpc = "0.23.0" thiserror = "1.0" tokio = { version = "1.21.2", features = ["sync", "time"] } diff --git a/nym-connect/src-tauri/src/config/mod.rs b/nym-connect/src-tauri/src/config/mod.rs index bf8a52558b..4663f3ecde 100644 --- a/nym-connect/src-tauri/src/config/mod.rs +++ b/nym-connect/src-tauri/src/config/mod.rs @@ -1,18 +1,14 @@ -use std::path::PathBuf; - -use client_core::config::GatewayEndpoint; -use std::sync::Arc; -use tap::TapFallible; -use tokio::sync::RwLock; - -use client_core::config::Config as BaseConfig; -use config_common::NymConfig; -use nym_socks5::client::config::Config as Socks5Config; - use crate::{ error::{BackendError, Result}, state::State, }; +use client_core::config::Config as BaseConfig; +use config_common::NymConfig; +use nym_socks5::client::config::Config as Socks5Config; +use std::path::PathBuf; +use std::sync::Arc; +use tap::TapFallible; +use tokio::sync::RwLock; static SOCKS5_CONFIG_ID: &str = "nym-connect"; @@ -116,6 +112,9 @@ pub async fn init_socks5_config(provider_address: String, chosen_gateway_id: Str // Future proofing. This flag exists for the other clients let user_wants_force_register = false; + // If the client was already initialized, don't generate new keys and don't re-register with + // the gateway (because this would create a new shared key). + // Unless the user really wants to. let register_gateway = !already_init || user_wants_force_register; log::trace!("Creating config for id: {}", id); @@ -127,31 +126,40 @@ pub async fn init_socks5_config(provider_address: String, chosen_gateway_id: Str .set_custom_validator_apis(config_common::parse_validators(&raw_validators)); } + // Setup gateway by either registering a new one, or reusing exiting keys + let gateway = client_core::init::setup_gateway::<_, Socks5Config, _>( config.get_base_mut().with_disabled_credentials(false); - let gateway = setup_gateway( - &id, register_gateway, - Some(&chosen_gateway_id), - config.get_socks5(), + Some(chosen_gateway_id), + config.get_base(), ) .await?; config.get_base_mut().with_gateway_endpoint(gateway); - let config_save_location = config.get_socks5().get_config_file_save_location(); config.get_socks5().save_to_file(None).tap_err(|_| { log::error!("Failed to save the config file"); })?; - log::info!("Saved configuration file to {:?}", config_save_location); + print_saved_config(&config); + + let address = client_core::init::get_client_address_from_stored_keys(config.get_base())?; + log::info!("The address of this client is: {}", address); + Ok(()) +} + +fn print_saved_config(config: &Config) { + log::info!( + "Saved configuration file to {:?}", + config.get_socks5().get_config_file_save_location() + ); log::info!("Gateway id: {}", config.get_base().get_gateway_id()); log::info!("Gateway owner: {}", config.get_base().get_gateway_owner()); log::info!( "Gateway listener: {}", config.get_base().get_gateway_listener() ); - log::info!( "Service provider address: {}", config.get_socks5().get_provider_mix_address() @@ -161,60 +169,4 @@ pub async fn init_socks5_config(provider_address: String, chosen_gateway_id: Str config.get_socks5().get_listening_port() ); log::info!("Client configuration completed."); - - client_core::init::show_address(config.get_base())?; - Ok(()) -} - -// TODO: deduplicate with same functions in other client -async fn setup_gateway( - id: &str, - register: bool, - user_chosen_gateway_id: Option<&str>, - config: &Socks5Config, -) -> Result { - if register { - // Get the gateway details by querying the validator-api. Either pick one at random or use - // the chosen one if it's among the available ones. - println!("Configuring gateway"); - let gateway = client_core::init::query_gateway_details( - config.get_base().get_validator_api_endpoints(), - user_chosen_gateway_id, - ) - .await?; - log::debug!("Querying gateway gives: {}", gateway); - - // Registering with gateway by setting up and writing shared keys to disk - log::trace!("Registering gateway"); - client_core::init::register_with_gateway_and_store_keys(gateway.clone(), config.get_base()) - .await?; - println!("Saved all generated keys"); - - Ok(gateway.into()) - } else if user_chosen_gateway_id.is_some() { - // Just set the config, don't register or create any keys - // This assumes that the user knows what they are doing, and that the existing keys are - // valid for the gateway being used - println!("Using gateway provided by user, keeping existing keys"); - let gateway = client_core::init::query_gateway_details( - config.get_base().get_validator_api_endpoints(), - user_chosen_gateway_id, - ) - .await?; - log::debug!("Querying gateway gives: {}", gateway); - Ok(gateway.into()) - } else { - println!("Not registering gateway, will reuse existing config and keys"); - let existing_config = Socks5Config::load_from_file(Some(id)).map_err(|err| { - log::error!( - "Unable to configure gateway: {err}. \n - Seems like the client was already initialized but it was not possible to read \ - the existing configuration file. \n - CAUTION: Consider backing up your gateway keys and try force gateway registration, or \ - removing the existing configuration and starting over." - ); - BackendError::CouldNotLoadExistingGatewayConfiguration(err) - })?; - Ok(existing_config.get_base().get_gateway_endpoint().clone()) - } } diff --git a/nym-connect/src-tauri/src/error.rs b/nym-connect/src-tauri/src/error.rs index b2b44ee345..cb677aac68 100644 --- a/nym-connect/src-tauri/src/error.rs +++ b/nym-connect/src-tauri/src/error.rs @@ -1,3 +1,4 @@ +use client_core::client::replies::reply_storage::fs_backend; use client_core::error::ClientCoreError; use serde::{Serialize, Serializer}; use thiserror::Error; @@ -33,7 +34,7 @@ pub enum BackendError { #[error("{source}")] ClientCoreError { #[from] - source: ClientCoreError, + source: ClientCoreError, }, #[error("Could not send disconnect signal to the SOCKS5 client")] diff --git a/nym-connect/src-tauri/src/tasks.rs b/nym-connect/src-tauri/src/tasks.rs index 2b0cd85f05..15b6daf297 100644 --- a/nym-connect/src-tauri/src/tasks.rs +++ b/nym-connect/src-tauri/src/tasks.rs @@ -1,4 +1,4 @@ -use client_core::config::GatewayEndpoint; +use client_core::config::{ClientCoreConfigTrait, GatewayEndpointConfig}; use futures::channel::mpsc; use std::sync::Arc; use tap::TapFallible; @@ -19,19 +19,23 @@ pub enum Socks5StatusMessage { /// The SOCKS5 task successfully stopped Stopped, /// The SOCKS5 task failed to start - FailedToStart, + Failed(Box), } /// The main SOCKS5 client task. It loads the configuration from file determined by the `id`. pub fn start_nym_socks5_client( id: &str, -) -> Result<(Socks5ControlMessageSender, StatusReceiver, GatewayEndpoint)> { +) -> Result<( + Socks5ControlMessageSender, + StatusReceiver, + GatewayEndpointConfig, +)> { log::info!("Loading config from file: {id}"); let config = Socks5Config::load_from_file(Some(id)) .tap_err(|_| log::warn!("Failed to load configuration file"))?; let used_gateway = config.get_base().get_gateway_endpoint().clone(); - let mut socks5_client = Socks5NymClient::new(config); + let socks5_client = Socks5NymClient::new(config); log::info!("Starting socks5 client"); // Channel to send control messages to the socks5 client @@ -52,7 +56,7 @@ pub fn start_nym_socks5_client( if let Err(err) = result { log::error!("SOCKS5 proxy failed: {err}"); socks5_status_tx - .send(Socks5StatusMessage::FailedToStart) + .send(Socks5StatusMessage::Failed(err)) .expect("Failed to send status message back to main task"); return; } @@ -94,14 +98,14 @@ pub fn start_disconnect_listener( ) .unwrap(); } - Ok(Socks5StatusMessage::FailedToStart) => { - log::info!("SOCKS5 task reported it failed to start"); + Ok(Socks5StatusMessage::Failed(err)) => { + log::info!("SOCKS5 task reported error: {}", err); window .emit( "socks5-event", Payload { title: "SOCKS5 error".into(), - message: "SOCKS5 failed to start".into(), + message: format!("SOCKS5 failed: {}", err), }, ) .unwrap(); diff --git a/nym-connect/src-tauri/tauri.conf.json b/nym-connect/src-tauri/tauri.conf.json index b9cbfd8233..3b7d0d3c5c 100644 --- a/nym-connect/src-tauri/tauri.conf.json +++ b/nym-connect/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "package": { "productName": "nym-connect", - "version": "1.1.0" + "version": "1.1.2" }, "build": { "distDir": "../dist", @@ -57,7 +57,8 @@ }, "window": { "startDragging": true, - "close": true + "close": true, + "minimize": true } }, "windows": [ diff --git a/nym-connect/src/components/CustomTitleBar.tsx b/nym-connect/src/components/CustomTitleBar.tsx index dc4e8c0061..41c71137b9 100644 --- a/nym-connect/src/components/CustomTitleBar.tsx +++ b/nym-connect/src/components/CustomTitleBar.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { ArrowBack, Close, HelpOutline } from '@mui/icons-material'; +import { ArrowBack, Close, HelpOutline, Minimize } from '@mui/icons-material'; import { Box, IconButton } from '@mui/material'; import { NymWordmark } from '@nymproject/react/logo/NymWordmark'; import { appWindow } from '@tauri-apps/api/window'; @@ -19,7 +19,7 @@ const customTitleBarStyles = { }; const CustomButton = ({ Icon, onClick }: { Icon: React.JSXElementConstructor; onClick: () => void }) => ( - + ); @@ -28,14 +28,22 @@ export const CustomTitleBar = () => { const { showHelp, handleShowHelp } = useClientContext(); return ( - { - handleShowHelp(); - }} - /> + {/* set width to keep logo centered */} + + { + handleShowHelp(); + }} + /> + + - appWindow.close()} /> + + + appWindow.minimize()} /> + appWindow.close()} /> + ); }; diff --git a/nym-wallet/CHANGELOG.md b/nym-wallet/CHANGELOG.md index 1f19602deb..2947400e8b 100644 --- a/nym-wallet/CHANGELOG.md +++ b/nym-wallet/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## [nym-wallet-v1.1.2](https://github.com/nymtech/nym/releases/tag/nym-wallet-v1.1.2) (2022-11-09) + +- wallet: Bity buy functionality + +## [nym-wallet-v1.1.1](https://github.com/nymtech/nym/releases/tag/nym-wallet-v1.1.1) (2022-11-09) + +- wallet: compatibility with nym-binaries-1.1.1 + + ## [nym-wallet-v1.1.0](https://github.com/nymtech/nym/releases/tag/nym-wallet-v1.1.0) (2022-11-09) - wallet: Add window for showing logs for when the terminal is not available. diff --git a/nym-wallet/Cargo.lock b/nym-wallet/Cargo.lock index 999378447f..3b99531457 100644 --- a/nym-wallet/Cargo.lock +++ b/nym-wallet/Cargo.lock @@ -2937,7 +2937,7 @@ dependencies = [ [[package]] name = "nym_wallet" -version = "1.1.1" +version = "1.1.2" dependencies = [ "aes-gcm", "argon2 0.3.4", @@ -5497,7 +5497,7 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "vesting-contract" -version = "1.1.0" +version = "1.1.2" dependencies = [ "contracts-common", "cosmwasm-std", diff --git a/nym-wallet/package.json b/nym-wallet/package.json index a9a55d2d0b..f31c68b9a6 100644 --- a/nym-wallet/package.json +++ b/nym-wallet/package.json @@ -1,6 +1,6 @@ { "name": "@nymproject/nym-wallet-app", - "version": "1.1.1", + "version": "1.1.2", "main": "index.js", "license": "MIT", "scripts": { diff --git a/nym-wallet/src-tauri/Cargo.toml b/nym-wallet/src-tauri/Cargo.toml index 835383188e..1a9be48cd0 100644 --- a/nym-wallet/src-tauri/Cargo.toml +++ b/nym-wallet/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nym_wallet" -version = "1.1.1" +version = "1.1.2" description = "Nym Native Wallet" authors = ["Nym Technologies SA"] license = "" diff --git a/nym-wallet/src-tauri/src/operations/signatures/sign.rs b/nym-wallet/src-tauri/src/operations/signatures/sign.rs index 4ebca7aa56..abb132c1c8 100644 --- a/nym-wallet/src-tauri/src/operations/signatures/sign.rs +++ b/nym-wallet/src-tauri/src/operations/signatures/sign.rs @@ -13,7 +13,7 @@ use serde_json::json; pub struct SignatureOutputJson { pub account_id: String, pub public_key: PublicKey, - pub signature: String, + pub signature_as_hex: String, } #[tauri::command] @@ -32,14 +32,14 @@ pub async fn sign( log::info!("<<< Signing message"); let signature = wallet.sign_raw_with_account(account, message.as_bytes())?; - let signature_as_hex_string = signature.to_string(); let output = SignatureOutputJson { account_id: account.address().to_string(), public_key: account.public_key(), - signature: signature_as_hex_string.to_string(), + signature_as_hex: signature.to_string(), }; - log::info!(">>> Signing data {}", json!(output),); - Ok(signature_as_hex_string) + let output_json = json!(output).to_string(); + log::info!(">>> Signing data {}", output_json); + Ok(output_json) } async fn get_pubkey_from_account_address( diff --git a/nym-wallet/src-tauri/tauri.conf.json b/nym-wallet/src-tauri/tauri.conf.json index b7b616b2e8..d572941588 100644 --- a/nym-wallet/src-tauri/tauri.conf.json +++ b/nym-wallet/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "package": { "productName": "nym-wallet", - "version": "1.1.1" + "version": "1.1.2" }, "build": { "distDir": "../dist", diff --git a/nym-wallet/src/components/AppBar.tsx b/nym-wallet/src/components/AppBar.tsx index 9aedf14d1a..24b2f93942 100644 --- a/nym-wallet/src/components/AppBar.tsx +++ b/nym-wallet/src/components/AppBar.tsx @@ -15,7 +15,7 @@ export const AppBar = () => { const navigate = useNavigate(); return ( - + diff --git a/nym-wallet/src/components/Bonding/BondedMixnode.tsx b/nym-wallet/src/components/Bonding/BondedMixnode.tsx index 5d63555ddb..6ff0151b29 100644 --- a/nym-wallet/src/components/Bonding/BondedMixnode.tsx +++ b/nym-wallet/src/components/Bonding/BondedMixnode.tsx @@ -114,7 +114,6 @@ export const BondedMixnode = ({ ), id: 'actions-cell', @@ -136,7 +135,7 @@ export const BondedMixnode = ({ {name && ( - + {name} diff --git a/nym-wallet/src/components/Bonding/BondedMixnodeActions.tsx b/nym-wallet/src/components/Bonding/BondedMixnodeActions.tsx index 37b52452c7..2a7d6e6bf4 100644 --- a/nym-wallet/src/components/Bonding/BondedMixnodeActions.tsx +++ b/nym-wallet/src/components/Bonding/BondedMixnodeActions.tsx @@ -35,7 +35,7 @@ export const BondedMixnodeActions = ({ )} R} + Icon={R} onClick={() => handleActionClick('redeem')} disabled={disabledRedeemAndCompound} /> diff --git a/nym-wallet/src/components/Bonding/forms/BondMixnodeForm.tsx b/nym-wallet/src/components/Bonding/forms/BondMixnodeForm.tsx index 721d114961..6f229a403d 100644 --- a/nym-wallet/src/components/Bonding/forms/BondMixnodeForm.tsx +++ b/nym-wallet/src/components/Bonding/forms/BondMixnodeForm.tsx @@ -1,14 +1,16 @@ -import React, { useEffect, useState } from 'react'; +import React, { useContext, useEffect, useState } from 'react'; import { useForm } from 'react-hook-form'; import { yupResolver } from '@hookform/resolvers/yup'; -import { Box, Checkbox, FormControlLabel, FormHelperText, Stack, TextField } from '@mui/material'; +import { Box, Checkbox, FormControlLabel, FormHelperText, Stack, TextField, Typography } from '@mui/material'; import { CurrencyFormField } from '@nymproject/react/currency/CurrencyFormField'; import { IdentityKeyFormField } from '@nymproject/react/mixnodes/IdentityKeyFormField'; import { CurrencyDenom, TNodeType } from '@nymproject/types'; import { checkHasEnoughFunds, checkHasEnoughLockedTokens } from 'src/utils'; import { NodeTypeSelector, TokenPoolSelector } from 'src/components'; import { MixnodeAmount, MixnodeData } from 'src/pages/bonding/types'; +import { ModalListItem } from 'src/components/Modals/ModalListItem'; import { amountSchema, mixnodeValidationSchema } from './mixnodeValidationSchema'; +import { AppContext } from 'src/context'; const NodeFormData = ({ mixnodeData, onNext }: { mixnodeData: MixnodeData; onNext: (data: any) => void }) => { const [showAdvancedOptions, setShowAdvancedOptions] = useState(false); @@ -138,6 +140,8 @@ const AmountFormData = ({ setError, } = useForm({ resolver: yupResolver(amountSchema), defaultValues: amountData }); + const { userBalance } = useContext(AppContext); + const handleRequestValidation = async (event: { detail: { step: number } }) => { let hasSufficientTokens = true; const values = getValues(); @@ -210,6 +214,17 @@ const AmountFormData = ({ and delegators. + + {!hasVestingTokens && ( + + )} + Est. fee for this transaction will be calculated in the next page + ); }; diff --git a/nym-wallet/src/components/Bonding/modals/BondMoreModal.tsx b/nym-wallet/src/components/Bonding/modals/BondMoreModal.tsx index 88518abc28..e2145e431e 100644 --- a/nym-wallet/src/components/Bonding/modals/BondMoreModal.tsx +++ b/nym-wallet/src/components/Bonding/modals/BondMoreModal.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useState } from 'react'; -import { Box, FormHelperText, Stack, TextField } from '@mui/material'; +import { Box, Stack } from '@mui/material'; import { CurrencyFormField } from '@nymproject/react/currency/CurrencyFormField'; import { ModalListItem } from 'src/components/Modals/ModalListItem'; import { SimpleModal } from 'src/components/Modals/SimpleModal'; @@ -7,66 +7,74 @@ import { DecCoin } from '@nymproject/types'; import { TokenPoolSelector, TPoolOption } from 'src/components/TokenPoolSelector'; import { ConfirmTx } from 'src/components/ConfirmTX'; import { useGetFee } from 'src/hooks/useGetFee'; -import { validateAmount, validateKey } from 'src/utils'; +import { validateAmount } from 'src/utils'; +import { simulateBondMore, simulateVestingBondMore } from 'src/requests'; +import { TBondMoreArgs } from 'src/types'; +import { TBondedMixnode } from 'src/context'; export const BondMoreModal = ({ - currentBond, + node, userBalance, - hasVestingTokens, - onConfirm, + onBondMore, onClose, + onError, }: { - currentBond: DecCoin; + node: TBondedMixnode; userBalance?: string; - hasVestingTokens: boolean; - onConfirm: (args: { additionalBond: DecCoin; signature: string; tokenPool: TPoolOption }) => Promise; + onBondMore: (data: TBondMoreArgs, tokenPool: TPoolOption) => Promise; onClose: () => void; + onError: (e: string) => void; }) => { - const { fee, resetFeeState } = useGetFee(); + const { bond: currentBond, proxy } = node; + const { fee, getFee, resetFeeState, feeError } = useGetFee(); const [additionalBond, setAdditionalBond] = useState({ amount: '0', denom: currentBond.denom }); - const [signature, setSignature] = useState(''); - const [tokenPool, setTokenPool] = useState('balance'); const [errorAmount, setErrorAmount] = useState(false); - const [errorSignature, setErrorSignature] = useState(false); - const handleOnOk = async () => { - const errors = { - amount: false, - signature: false, - }; - - if (!validateKey(signature || '', 64)) { - errors.signature = true; + useEffect(() => { + if (feeError) { + onError(feeError); } + }, [feeError]); - if (!additionalBond?.amount) { - errors.amount = true; - } + const handleConfirm = async () => { + const data = { additionalPledge: additionalBond }; + const tokenPool = proxy ? 'locked' : 'balance'; + await onBondMore(data, tokenPool); + }; - if (additionalBond && !(await validateAmount(additionalBond.amount, '1'))) { - errors.amount = true; - } + const handleAmountChanged = async (value: DecCoin) => { + setAdditionalBond(value); + const { amount } = value; - if (!errors.amount && !errors.signature) { - onConfirm({ additionalBond, signature, tokenPool }); + if (!amount) { + setErrorAmount(true); } else { - setErrorAmount(errors.amount); - setErrorSignature(errors.signature); + const validAmount = await validateAmount(amount, '1'); + if (!validAmount) { + setErrorAmount(true); + return; + } + setErrorAmount(false); } }; - useEffect(() => { - setErrorAmount(false); - }, [additionalBond]); + const handleOnOk = async () => { + if (!proxy) { + await getFee(simulateBondMore, { additionalPledge: additionalBond }); + } else { + await getFee(simulateVestingBondMore, { additionalPledge: additionalBond }); + } + }; if (fee) return ( onConfirm({ additionalBond, signature, tokenPool })} + onClose={onClose} onPrev={resetFeeState} + onConfirm={handleConfirm} > @@ -80,36 +88,23 @@ export const BondMoreModal = ({ subHeader="Bond more tokens on your node and receive more rewards" okLabel="Next" onOk={handleOnOk} - okDisabled={errorAmount || errorSignature} + okDisabled={errorAmount} onClose={onClose} > - {hasVestingTokens && setTokenPool(pool)} />} { - setAdditionalBond(value); - setErrorSignature(false); + handleAmountChanged(value); }} fullWidth validationError={errorAmount ? 'Please enter a valid amount' : undefined} /> - - setSignature(e.target.value)} - InputLabelProps={{ shrink: true }} - /> - {errorSignature && Invalid signature} - - diff --git a/nym-wallet/src/components/Bonding/modals/BondOversaturatedModal.tsx b/nym-wallet/src/components/Bonding/modals/BondOversaturatedModal.tsx new file mode 100644 index 0000000000..be3ea33869 --- /dev/null +++ b/nym-wallet/src/components/Bonding/modals/BondOversaturatedModal.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import { Stack, Typography } from '@mui/material'; +import { SimpleModal } from 'src/components/Modals/SimpleModal'; + +export const BondOversaturatedModal: React.FC<{ + open: boolean; + saturationPercentage: string; + onClose?: () => void; + onContinue?: () => void; +}> = ({ open, saturationPercentage, onClose, onContinue }) => ( + onContinue?.()} + header="Bond More" + okLabel="Bond More" + buttonFullWidth + > + + t.palette.nym.red }} + >{`Your node saturation: ${saturationPercentage}%`} + Your node is over saturated, are you sure you want to bond more? + + +); diff --git a/nym-wallet/src/components/Buy/SignMessageModal.stories.tsx b/nym-wallet/src/components/Buy/SignMessageModal.stories.tsx new file mode 100644 index 0000000000..201192ac16 --- /dev/null +++ b/nym-wallet/src/components/Buy/SignMessageModal.stories.tsx @@ -0,0 +1,35 @@ +import React, { useState } from 'react'; +import { Button } from '@mui/material'; +import { MockBuyContextProvider } from 'src/context/mocks/buy'; + +import { SignMessageModal } from './SignMessageModal'; + +export default { + title: 'Buy/SignMessage', + component: SignMessageModal, +}; + +export const SignMessage = () => { + const [open, setOpen] = useState(false); + + return ( + + + {open && ( + { + setOpen(false); + }} + /> + )} + + ); +}; diff --git a/nym-wallet/src/components/Buy/SignMessageModal.tsx b/nym-wallet/src/components/Buy/SignMessageModal.tsx new file mode 100644 index 0000000000..d076971cd7 --- /dev/null +++ b/nym-wallet/src/components/Buy/SignMessageModal.tsx @@ -0,0 +1,72 @@ +import * as React from 'react'; +import { useState } from 'react'; +import { Stack, TextField, Typography } from '@mui/material'; +import { useBuyContext } from 'src/context'; +import { SimpleModal } from '../Modals/SimpleModal'; +import { ErrorModal } from '../Modals/ErrorModal'; +import { CopyToClipboard } from '../CopyToClipboard'; + +export const SignMessageModal = ({ onClose }: { onClose: () => void }) => { + const [message, setMessage] = useState(); + const [signature, setSignature] = useState(); + + const { signMessage, loading, refresh, error } = useBuyContext(); + + const handleSign = async () => { + if (!message) { + return; + } + signMessage(message).then((sig) => { + setSignature(sig); + }); + }; + + const handleChange = (event: React.ChangeEvent) => { + setMessage(event.target.value); + }; + + if (error) { + return ( + { + refresh(); + }} + /> + ); + } + + return ( + + + + + + + + Copy signature + + + + + + ); +}; diff --git a/nym-wallet/src/components/Buy/Tutorial.stories.tsx b/nym-wallet/src/components/Buy/Tutorial.stories.tsx new file mode 100644 index 0000000000..ec73a796eb --- /dev/null +++ b/nym-wallet/src/components/Buy/Tutorial.stories.tsx @@ -0,0 +1,15 @@ +import React from 'react'; + +import { Tutorial } from './Tutorial'; +import { MockBuyContextProvider } from '../../context/mocks/buy'; + +export default { + title: 'Buy/Tutorial', + component: Tutorial, +}; + +export const TutorialPage = () => ( + + + +); diff --git a/nym-wallet/src/components/Buy/Tutorial.tsx b/nym-wallet/src/components/Buy/Tutorial.tsx new file mode 100644 index 0000000000..5f14e2c444 --- /dev/null +++ b/nym-wallet/src/components/Buy/Tutorial.tsx @@ -0,0 +1,129 @@ +import React, { useState } from 'react'; +import { Button, Stack, Typography, Grid, useMediaQuery, useTheme } from '@mui/material'; +import { Tune as TuneIcon, BorderColor as BorderColorIcon, Paid as PaidIcon } from '@mui/icons-material'; +import { NymCard } from '../NymCard'; +import { SignMessageModal } from './SignMessageModal'; + +// TODO retrieve this value from env +const EXCHANGE_URL = 'https://buy.nymtech.net'; + +const borderColor = 'rgba(141, 147, 153, 0.2)'; + +const TutorialStep = ({ + step, + title, + text, + icon, + borderRight, + borderBottom, +}: { + step: number; + title: string; + text: React.ReactNode; + icon: React.ReactNode; + borderRight?: boolean; + borderBottom?: boolean; +}) => ( + + + + {icon} + + {`STEP ${step}`} + + + + {title} + + {text} + + +); + +export const Tutorial = () => { + const [showSignModal, setShowSignModal] = useState(false); + const theme = useTheme(); + const showBorder = useMediaQuery(theme.breakpoints.up('md')); + + return ( + + + Follow below 3 steps to quickly and easily buy NYM tokens. You can purchase up to 1000 Swiss Francs per day + without KYC. + + {showSignModal && setShowSignModal(false)} />} + + } + text={ + t.palette.nym.text.muted }}> + Click on{' '} + + Buy NYM + {' '} + button and follow the steps in the browser window that opens. You will be asked for purchase details i.e. + amount, wallet address, etc. + + } + borderRight={showBorder} + borderBottom={!showBorder} + /> + } + text={ + t.palette.nym.text.muted }}> + When asked for signature, copy the message and sign it using{' '} + + Sign message + {' '} + button below. Then copy and paste your signature back in the browser window. + + } + borderRight={showBorder} + borderBottom={!showBorder} + /> + } + text={ + t.palette.nym.text.muted }}> + {`Send the defined BTC amount to Bity's address that's given to you. As soon as your BTC transaction has 4 + confirmations, Bity will send the purchased NYM tokens to your wallet.`} + + } + /> + + + + + + + ); +}; diff --git a/nym-wallet/src/components/CopyToClipboard.tsx b/nym-wallet/src/components/CopyToClipboard.tsx index 8df15668a8..d6de5cab9b 100644 --- a/nym-wallet/src/components/CopyToClipboard.tsx +++ b/nym-wallet/src/components/CopyToClipboard.tsx @@ -35,6 +35,7 @@ export const CopyToClipboard = ({ text = '', iconButton }: { text?: string; icon sx={{ color: 'text.primary', }} + disabled={!text} > {!copied ? : } @@ -51,6 +52,7 @@ export const CopyToClipboard = ({ text = '', iconButton }: { text?: string; icon }} onClick={() => handleCopy(text)} endIcon={copied && theme.palette.success.light }} />} + disabled={!text} > {!copied ? 'Copy' : 'Copied'} diff --git a/nym-wallet/src/components/Delegation/DelegationActions.tsx b/nym-wallet/src/components/Delegation/DelegationActions.tsx index 653e2a80e2..0784114d9d 100644 --- a/nym-wallet/src/components/Delegation/DelegationActions.tsx +++ b/nym-wallet/src/components/Delegation/DelegationActions.tsx @@ -92,7 +92,7 @@ export const DelegationsActionsMenu: React.FC<{ R} + Icon={R} onClick={() => handleActionSelect('redeem')} disabled={disableRedeemingRewards} /> diff --git a/nym-wallet/src/components/Modals/SimpleModal.stories.tsx b/nym-wallet/src/components/Modals/SimpleModal.stories.tsx index 35c4df516e..994cf7eb48 100644 --- a/nym-wallet/src/components/Modals/SimpleModal.stories.tsx +++ b/nym-wallet/src/components/Modals/SimpleModal.stories.tsx @@ -225,3 +225,36 @@ export const withBackButton = () => { ); }; + +export const withBackButtonAndCustomLabel = () => { + const [open, setOpen] = React.useState(false); + const handleClick = () => setOpen(true); + + const theme = useTheme(); + + return ( + + setOpen(false)} + onOk={async () => setOpen(false)} + header="This is a modal" + okLabel="Primary action" + onBack={() => setOpen(false)} + backLabel="Cancel" + backButtonFullWidth + {...storybookStyles(theme)} + > + + Tempor culpa est magna. Sit tempor cillum culpa sint ipsum nostrud ullamco voluptate exercitation dolore magna + elit ut mollit. + + + + Veniam dolor laborum labore sit reprehenderit enim mollit magna nulla adipisicing fugiat. Est ex irure quis. + + + + ); +}; diff --git a/nym-wallet/src/components/Modals/SimpleModal.tsx b/nym-wallet/src/components/Modals/SimpleModal.tsx index 85c6480619..591354ffe5 100644 --- a/nym-wallet/src/components/Modals/SimpleModal.tsx +++ b/nym-wallet/src/components/Modals/SimpleModal.tsx @@ -20,6 +20,8 @@ export const SimpleModal: React.FC<{ header: string | React.ReactNode; subHeader?: string; okLabel: string; + backLabel?: string; + backButtonFullWidth?: boolean; okDisabled?: boolean; sx?: SxProps; backdropProps?: object; @@ -38,6 +40,8 @@ export const SimpleModal: React.FC<{ header, subHeader, okLabel, + backLabel, + backButtonFullWidth, sx, children, backdropProps, @@ -71,7 +75,7 @@ export const SimpleModal: React.FC<{ {(onOk || onBack) && ( - {onBack && } + {onBack && } {onOk && (