diff --git a/.github/workflows/nightly_build_release.yml b/.github/workflows/nightly_build_release.yml index 9a1ba43dc5..4474b399b6 100644 --- a/.github/workflows/nightly_build_release.yml +++ b/.github/workflows/nightly_build_release.yml @@ -9,12 +9,12 @@ jobs: outputs: matrix: ${{ steps.set-matrix.outputs.matrix }} steps: - # creates the matrix strategy from nightly_build_release_matrix.json + # creates the matrix strategy from nightly_build_matrix_includes.json - uses: actions/checkout@v3 - id: set-matrix uses: JoshuaTheMiller/conditional-build-matrix@main with: - inputFile: '.github/workflows/nightly_build_release_matrix.json' + inputFile: '.github/workflows/nightly_build_matrix_includes.json' filter: '[?runOnEvent==`${{ github.event_name }}` || runOnEvent==`always`]' get_release: runs-on: ubuntu-latest @@ -174,7 +174,7 @@ jobs: args: --manifest-path nym-wallet/Cargo.toml --workspace --all-targets -- -D warnings notification: - needs: build + needs: [build,get_release] runs-on: ubuntu-latest steps: - name: Collect jobs status @@ -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: "${GITHUB_REF##*/}" + GIT_BRANCH: "https://github.com/nymtech/nym/tree/${{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 new file mode 100644 index 0000000000..71f89948c9 --- /dev/null +++ b/.github/workflows/nightly_build_release2.yml @@ -0,0 +1,203 @@ +name: Nightly builds on second latest release + +on: + schedule: + - cron: '24 2 * * *' +jobs: + matrix_prep: + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.set-matrix.outputs.matrix }} + steps: + # creates the matrix strategy from nightly_build_matrix_includes.json + - uses: actions/checkout@v3 + - id: set-matrix + uses: JoshuaTheMiller/conditional-build-matrix@main + with: + inputFile: '.github/workflows/nightly_build_matrix_includes.json' + filter: '[?runOnEvent==`${{ github.event_name }}` || runOnEvent==`always`]' + get_release: + runs-on: ubuntu-latest + needs: matrix_prep + outputs: + output1: ${{ steps.step2.outputs.latest_release }} + steps: + - name: Check out repository code + uses: actions/checkout@v3 + - name: Fetch all branches + run: git fetch --all + - name: Set output variable to latest release branch + id: step2 + run: echo "latest_release=$(git branch -r | grep -E 'release/v[0-9]+\.[0-9]+\.[0-9]+' | tail -n 2 | head -n 1 | sed 's/ origin\///')" >> $GITHUB_OUTPUT + build: + needs: [get_release,matrix_prep] + strategy: + matrix: ${{fromJson(needs.matrix_prep.outputs.matrix)}} + runs-on: ${{ matrix.os }} + continue-on-error: ${{ matrix.rust == 'nightly' || matrix.rust == 'beta' || matrix.rust == 'stable' }} + 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' + + - name: Check out latest release branch + uses: actions/checkout@v3 + with: + ref: ${{needs.get_release.outputs.output1}} + + - name: Install rust toolchain + uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: ${{ matrix.rust }} + override: true + components: rustfmt, clippy + + - name: Build all binaries + uses: actions-rs/cargo@v1 + with: + command: build + args: --workspace + + - name: Reclaim some disk space (because Windows is being annoying) + uses: actions-rs/cargo@v1 + if: ${{ matrix.os == 'windows-latest' }} + with: + command: clean + + - name: Run all tests + uses: actions-rs/cargo@v1 + with: + command: test + args: --workspace + + - name: Reclaim some disk space (because Windows is being annoying) + uses: actions-rs/cargo@v1 + if: ${{ matrix.os == 'windows-latest' }} + with: + command: clean + + - name: Run expensive tests + if: github.ref == 'refs/heads/develop' || github.event.pull_request.base.ref == 'develop' || github.event.pull_request.base.ref == 'master' + uses: actions-rs/cargo@v1 + with: + command: test + args: --workspace --all-features -- --ignored + + - name: Check formatting + uses: actions-rs/cargo@v1 + with: + command: fmt + args: --all -- --check + + - name: Reclaim some disk space (because Windows is being annoying) + uses: actions-rs/cargo@v1 + if: ${{ matrix.os == 'windows-latest' }} + with: + command: clean + + - uses: actions-rs/clippy-check@v1 + name: Clippy checks + with: + token: ${{ secrets.GITHUB_TOKEN }} + args: --all-features + + - name: Run clippy + uses: actions-rs/cargo@v1 + if: ${{ matrix.rust != 'nightly' }} + with: + command: clippy + args: --workspace --all-targets -- -D warnings + + - name: Reclaim some disk space + uses: actions-rs/cargo@v1 + if: ${{ matrix.os == 'windows-latest' || matrix.os == 'ubuntu-latest' }} + with: + command: clean + + # COCONUT stuff + - name: Build all binaries with coconut enabled + uses: actions-rs/cargo@v1 + with: + command: build + args: --workspace --features=coconut + + - name: Reclaim some disk space (because Windows is being annoying) + uses: actions-rs/cargo@v1 + if: ${{ matrix.os == 'windows-latest' }} + with: + command: clean + + - name: Run all tests with coconut enabled + uses: actions-rs/cargo@v1 + with: + command: test + args: --workspace --features=coconut + + - name: Reclaim some disk space (because Windows is being annoying) + uses: actions-rs/cargo@v1 + if: ${{ matrix.os == 'windows-latest' }} + with: + command: clean + + - name: Run clippy with coconut enabled + uses: actions-rs/cargo@v1 + if: ${{ matrix.rust != 'nightly' }} + with: + command: clippy + args: --workspace --all-targets --features=coconut -- -D warnings + + # nym-wallet (the rust part) + - name: Build nym-wallet rust code + uses: actions-rs/cargo@v1 + with: + command: build + args: --manifest-path nym-wallet/Cargo.toml --workspace + + - name: Run nym-wallet tests + uses: actions-rs/cargo@v1 + with: + command: test + args: --manifest-path nym-wallet/Cargo.toml --workspace + + - name: Check nym-wallet formatting + uses: actions-rs/cargo@v1 + with: + command: fmt + args: --manifest-path nym-wallet/Cargo.toml --all -- --check + + - name: Run clippy for nym-wallet + uses: actions-rs/cargo@v1 + if: ${{ matrix.rust != 'nightly' }} + with: + command: clippy + args: --manifest-path nym-wallet/Cargo.toml --workspace --all-targets -- -D warnings + + notification: + needs: [build,get_release] + runs-on: ubuntu-latest + steps: + - name: Collect jobs status + uses: technote-space/workflow-conclusion-action@v2 + - name: Check out repository code + uses: actions/checkout@v3 + - name: Keybase - Node Install + if: env.WORKFLOW_CONCLUSION == 'failure' + run: npm install + working-directory: .github/workflows/support-files + - name: Keybase - Send Notification + if: env.WORKFLOW_CONCLUSION == 'failure' + env: + NYM_NOTIFICATION_KIND: nightly + 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}}" + KEYBASE_NYMBOT_USERNAME: "${{ secrets.KEYBASE_NYMBOT_USERNAME }}" + KEYBASE_NYMBOT_PAPERKEY: "${{ secrets.KEYBASE_NYMBOT_PAPERKEY }}" + KEYBASE_NYMBOT_TEAM: "${{ secrets.KEYBASE_NYMBOT_TEAM }}" + KEYBASE_NYM_CHANNEL: "ci-nightly-release" + IS_SUCCESS: "${{ env.WORKFLOW_CONCLUSION == 'success' }}" + uses: docker://keybaseio/client:stable-node + with: + args: .github/workflows/support-files/notifications/entry_point.sh diff --git a/.github/workflows/nightly_build_release_matrix.json b/.github/workflows/nightly_build_release_matrix.json deleted file mode 100644 index c292817b6a..0000000000 --- a/.github/workflows/nightly_build_release_matrix.json +++ /dev/null @@ -1,50 +0,0 @@ -[ - { - "os":"ubuntu-latest", - "rust":"stable", - "runOnEvent":"workflow_dispatch" - }, - - { - "os":"windows-latest", - "rust":"stable", - "runOnEvent":"workflow_dispatch" - }, - { - "os":"macos-latest", - "rust":"stable", - "runOnEvent":"workflow_dispatch" - }, - - { - "os":"ubuntu-latest", - "rust":"beta", - "runOnEvent":"workflow_dispatch" - }, - { - "os":"windows-latest", - "rust":"beta", - "runOnEvent":"workflow_dispatch" - }, - { - "os":"macos-latest", - "rust":"beta", - "runOnEvent":"workflow_dispatch" - }, - - { - "os":"ubuntu-latest", - "rust":"nightly", - "runOnEvent":"workflow_dispatch" - }, - { - "os":"windows-latest", - "rust":"nightly", - "runOnEvent":"workflow_dispatch" - }, - { - "os":"macos-latest", - "rust":"nightly", - "runOnEvent":"workflow_dispatch" - } -] diff --git a/.github/workflows/nym-connect.yml b/.github/workflows/nym-connect.yml index 3a08744fc9..5a652e92d2 100644 --- a/.github/workflows/nym-connect.yml +++ b/.github/workflows/nym-connect.yml @@ -41,19 +41,19 @@ jobs: - name: Keybase - Node Install run: npm install working-directory: .github/workflows/support-files -# - name: Keybase - Send Notification -# env: -# NYM_NOTIFICATION_KIND: nym-connect -# NYM_PROJECT_NAME: "nym-connect" -# NYM_CI_WWW_BASE: "${{ secrets.NYM_CI_WWW_BASE }}" -# NYM_CI_WWW_LOCATION: "nym-connect-${{ env.GITHUB_REF_SLUG }}" -# GIT_COMMIT_MESSAGE: "${{ github.event.head_commit.message }}" -# GIT_BRANCH: "${GITHUB_REF##*/}" -# KEYBASE_NYMBOT_USERNAME: "${{ secrets.KEYBASE_NYMBOT_USERNAME }}" -# KEYBASE_NYMBOT_PAPERKEY: "${{ secrets.KEYBASE_NYMBOT_PAPERKEY }}" -# KEYBASE_NYMBOT_TEAM: "${{ secrets.KEYBASE_NYMBOT_TEAM }}" -# KEYBASE_NYM_CHANNEL: "ci-nym-connect" -# IS_SUCCESS: "${{ job.status == 'success' }}" -# uses: docker://keybaseio/client:stable-node -# with: -# args: .github/workflows/support-files/notifications/entry_point.sh + - name: Keybase - Send Notification + env: + NYM_NOTIFICATION_KIND: nym-connect + NYM_PROJECT_NAME: "nym-connect" + NYM_CI_WWW_BASE: "${{ secrets.NYM_CI_WWW_BASE }}" + NYM_CI_WWW_LOCATION: "nym-connect-${{ env.GITHUB_REF_SLUG }}" + GIT_COMMIT_MESSAGE: "${{ github.event.head_commit.message }}" + GIT_BRANCH: "${GITHUB_REF##*/}" + KEYBASE_NYMBOT_USERNAME: "${{ secrets.KEYBASE_NYMBOT_USERNAME }}" + KEYBASE_NYMBOT_PAPERKEY: "${{ secrets.KEYBASE_NYMBOT_PAPERKEY }}" + KEYBASE_NYMBOT_TEAM: "${{ secrets.KEYBASE_NYMBOT_TEAM }}" + KEYBASE_NYM_CHANNEL: "ci-nym-connect" + IS_SUCCESS: "${{ job.status == 'success' }}" + uses: docker://keybaseio/client:stable-node + with: + args: .github/workflows/support-files/notifications/entry_point.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index ef9c535d9f..dc22196386 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,13 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https:// ### Added +- socks5-client/network-requester: add support for socks4a protocol + + +## [v1.1.1](https://github.com/nymtech/nym/tree/v1.1.1) (2022-11-29) + +### Added + - binaries: add `-c` shortform for `--config-env-file` - websocket-requests: add server response signalling current packet queue length in the client - contracts: DKG contract that handles coconut key generation ([#1678][#1708][#1747]) @@ -23,12 +30,18 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https:// - gateway-client: fix decrypting stored messages on reconnect ([#1786]) +### Fixed + +- gateway-client: fix decrypting stored messages on reconnect ([#1786]) +- socks5-client: fix shutting down all tasks if anyone of them panics or errors out ([#1805]) + [#1678]: https://github.com/nymtech/nym/pull/1678 [#1708]: https://github.com/nymtech/nym/pull/1708 [#1720]: https://github.com/nymtech/nym/pull/1720 [#1747]: https://github.com/nymtech/nym/pull/1747 [#1783]: https://github.com/nymtech/nym/pull/1783 [#1786]: https://github.com/nymtech/nym/pull/1786 +[#1805]: https://github.com/nymtech/nym/pull/1805 ## [v1.1.0](https://github.com/nymtech/nym/tree/v1.1.0) (2022-11-09) diff --git a/Cargo.lock b/Cargo.lock index c0c0d5f2ae..d932dd1f3b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -586,7 +586,7 @@ dependencies = [ [[package]] name = "client-core" -version = "1.1.0" +version = "1.1.1" dependencies = [ "client-connections", "config", @@ -609,6 +609,7 @@ dependencies = [ "tempfile", "thiserror", "tokio", + "tokio-stream", "topology", "url", "validator-client", @@ -1633,7 +1634,7 @@ dependencies = [ [[package]] name = "explorer-api" -version = "1.1.0" +version = "1.1.1" dependencies = [ "chrono", "clap 3.2.8", @@ -3122,7 +3123,7 @@ dependencies = [ [[package]] name = "nym-cli" -version = "1.1.0" +version = "1.1.1" dependencies = [ "anyhow", "base64", @@ -3176,7 +3177,7 @@ dependencies = [ [[package]] name = "nym-client" -version = "1.1.0" +version = "1.1.1" dependencies = [ "clap 3.2.8", "client-connections", @@ -3216,7 +3217,7 @@ dependencies = [ [[package]] name = "nym-gateway" -version = "1.1.0" +version = "1.1.1" dependencies = [ "anyhow", "async-trait", @@ -3263,7 +3264,7 @@ dependencies = [ [[package]] name = "nym-mixnode" -version = "1.1.0" +version = "1.1.1" dependencies = [ "anyhow", "bs58", @@ -3305,7 +3306,7 @@ dependencies = [ [[package]] name = "nym-network-requester" -version = "1.1.0" +version = "1.1.1" dependencies = [ "async-trait", "clap 3.2.8", @@ -3362,7 +3363,7 @@ dependencies = [ [[package]] name = "nym-socks5-client" -version = "1.1.0" +version = "1.1.1" dependencies = [ "clap 3.2.8", "client-connections", @@ -3429,7 +3430,7 @@ dependencies = [ [[package]] name = "nym-validator-api" -version = "1.1.0" +version = "1.1.1" dependencies = [ "anyhow", "async-trait", @@ -5704,7 +5705,9 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" name = "task" version = "0.1.0" dependencies = [ + "futures", "log", + "thiserror", "tokio", ] @@ -5844,18 +5847,18 @@ checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" [[package]] name = "thiserror" -version = "1.0.35" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c53f98874615aea268107765aa1ed8f6116782501d18e53d08b471733bea6c85" +checksum = "10deb33631e3c9018b9baf9dcbbc4f737320d2b576bac10f6aefa048fa407e3e" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.35" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8b463991b4eab2d801e724172285ec4195c650e8ec79b149e6c2a8e6dd3f783" +checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb" dependencies = [ "proc-macro2", "quote", diff --git a/clients/client-core/Cargo.toml b/clients/client-core/Cargo.toml index e296a30591..3d84983358 100644 --- a/clients/client-core/Cargo.toml +++ b/clients/client-core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "client-core" -version = "1.1.0" +version = "1.1.1" authors = ["Dave Hrycyszyn "] edition = "2021" @@ -17,6 +17,7 @@ sled = { version = "0.34", optional = true } tap = "1.0.1" thiserror = "1.0.34" url = { version ="2.2", features = ["serde"] } +tokio = { version = "1.21.2", features = ["time", "macros"]} # internal config = { path = "../../common/config" } @@ -30,8 +31,11 @@ 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(target_arch = \"wasm32\")".dependencies.wasm-bindgen-futures] version = "0.4" @@ -47,8 +51,8 @@ rev = "b9d1a54ad514c2f230a026afe0dde341e98cd7b6" version = "0.2.4" features = ["futures"] -[target."cfg(not(target_arch = \"wasm32\"))".dependencies.task] -path = "../../common/task" +#[target."cfg(not(target_arch = \"wasm32\"))".dependencies.task] +#path = "../../common/task" [dev-dependencies] tempfile = "3.1.0" 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..9788cb26f8 --- /dev/null +++ b/clients/client-core/src/client/base_client/mod.rs @@ -0,0 +1,441 @@ +// 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::topology_control::{ + TopologyAccessor, TopologyRefresher, TopologyRefresherConfig, +}; +use crate::config::{Config, DebugConfig, GatewayEndpointConfig}; +use crate::error::ClientCoreError; +use client_connections::{ConnectionCommandReceiver, ConnectionCommandSender, LaneQueueLengths}; +use crypto::asymmetric::identity; +use futures::channel::mpsc; +use gateway_client::bandwidth::BandwidthController; +use gateway_client::{ + AcknowledgementReceiver, AcknowledgementSender, GatewayClient, MixnetMessageReceiver, + MixnetMessageSender, +}; +use log::info; +use nymsphinx::addressing::clients::Recipient; +use nymsphinx::addressing::nodes::NodeIdentity; +#[cfg(feature = "reply-surb")] +use std::path::PathBuf; +#[cfg(feature = "reply-surb")] +use tap::TapFallible; +use task::{ShutdownListener, ShutdownNotifier}; +use url::Url; + +// it's fine to do this disgusting compilation flag business here as this problem +// is going to go away in 1.2.0 +#[cfg(feature = "reply-surb")] +use crate::client::reply_key_storage::ReplyKeyStorage; + +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> { + // 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, + + #[cfg(feature = "reply-surb")] + reply_surb_keys_store_path: PathBuf, + + bandwidth_controller: Option, + key_manager: KeyManager, +} + +impl<'a> BaseClientBuilder<'a> { + pub fn new_from_base_config( + base_config: &'a Config, + key_manager: KeyManager, + bandwidth_controller: Option, + ) -> BaseClientBuilder<'a> { + 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, + key_manager, + #[cfg(feature = "reply-surb")] + reply_surb_keys_store_path: base_config.get_reply_encryption_key_store_path(), + } + } + + pub fn new( + gateway_config: &'a GatewayEndpointConfig, + debug_config: &'a DebugConfig, + key_manager: KeyManager, + bandwidth_controller: Option, + disabled_credentials: bool, + validator_api_endpoints: Vec, + #[cfg(feature = "reply-surb")] reply_surb_keys_store_path: PathBuf, + ) -> BaseClientBuilder<'a> { + BaseClientBuilder { + gateway_config, + debug_config, + disabled_credentials, + validator_api_endpoints, + bandwidth_controller, + key_manager, + #[cfg(feature = "reply-surb")] + reply_surb_keys_store_path, + } + } + + 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( + &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.debug_config.average_ack_delay, + self.debug_config.average_packet_delay, + self.debug_config.loop_cover_traffic_average_delay, + mix_tx, + self.as_mix_recipient(), + topology_accessor, + ); + + if let Some(size) = self.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( + &self, + topology_accessor: TopologyAccessor, + ack_receiver: AcknowledgementReceiver, + input_receiver: InputMessageReceiver, + mix_sender: BatchMixMessageSender, + lane_queue_lengths: LaneQueueLengths, + client_connection_rx: ConnectionCommandReceiver, + shutdown: ShutdownListener, + #[cfg(feature = "reply-surb")] reply_key_storage: ReplyKeyStorage, + ) { + let mut controller_config = real_messages_control::Config::new( + self.key_manager.ack_key(), + self.debug_config.ack_wait_multiplier, + self.debug_config.ack_wait_addition, + self.debug_config.average_ack_delay, + self.debug_config.message_sending_average_delay, + self.debug_config.average_packet_delay, + self.debug_config.disable_main_poisson_packet_distribution, + self.as_mix_recipient(), + ); + + 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()); + } + + info!("Starting real traffic stream..."); + + RealMessagesController::new( + controller_config, + ack_receiver, + input_receiver, + mix_sender, + topology_accessor, + lane_queue_lengths, + client_connection_rx, + #[cfg(feature = "reply-surb")] + reply_key_storage, + ) + .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, + shutdown: ShutdownListener, + #[cfg(feature = "reply-surb")] reply_key_storage: ReplyKeyStorage, + ) { + info!("Starting received messages buffer controller..."); + ReceivedMessagesBufferController::new( + self.key_manager.encryption_keypair(), + query_receiver, + mixnet_receiver, + #[cfg(feature = "reply-surb")] + 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.gateway_config.gateway_id.clone(); + if gateway_id.is_empty() { + panic!("The identity of the gateway is unknown - did you run `nym-client` init?") + } + let gateway_owner = self.gateway_config.gateway_owner.clone(); + if gateway_owner.is_empty() { + panic!("The owner of the gateway is unknown - did you run `nym-client` init?") + } + let gateway_address = self.gateway_config.gateway_listener.clone(); + 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!"); + + // 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(), + Some(shutdown), + ); + + gateway_client.set_disabled_credentials_mode(self.disabled_credentials); + + 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<(), ClientCoreError> { + let topology_refresher_config = TopologyRefresherConfig::new( + self.validator_api_endpoints.clone(), + self.debug_config.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); + } + + 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 + } + + 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(); + + #[cfg(feature = "reply-surb")] + let reply_key_storage = + ReplyKeyStorage::load(&self.reply_surb_keys_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, + shutdown.subscribe(), + #[cfg(feature = "reply-surb")] + reply_key_storage.clone(), + ); + + 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(), + ack_receiver, + input_receiver, + sphinx_message_sender.clone(), + shared_lane_queue_lengths.clone(), + client_connection_rx, + shutdown.subscribe(), + #[cfg(feature = "reply-surb")] + reply_key_storage, + ); + + if !self.debug_config.disable_loop_cover_traffic_stream { + self.start_cover_traffic_stream( + shared_topology_accessor, + sphinx_message_sender, + shutdown.subscribe(), + ); + } + + info!("Client startup finished!"); + info!("The address of this client is: {}", self.as_mix_recipient()); + + 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/cover_traffic_stream.rs b/clients/client-core/src/client/cover_traffic_stream.rs index ee3b7851cb..061e5f0b98 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!"); @@ -202,12 +212,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,17 +237,16 @@ impl LoopCoverTrafficStream { } } } - assert!(shutdown.is_shutdown_poll()); + 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)); + self.set_next_delay(sampled); spawn_future(async move { debug!("Started LoopCoverTrafficStream without graceful shutdown support"); 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 d5702cc0a8..0d3aab9513 100644 --- a/clients/client-core/src/client/mix_traffic.rs +++ b/clients/client-core/src/client/mix_traffic.rs @@ -67,7 +67,6 @@ impl MixTrafficController { } } - #[cfg(not(target_arch = "wasm32"))] pub fn start_with_shutdown(mut self, mut shutdown: task::ShutdownListener) { spawn_future(async move { debug!("Started MixTrafficController with graceful shutdown support"); @@ -88,12 +87,11 @@ impl MixTrafficController { } } } - assert!(shutdown.is_shutdown_poll()); + 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"); diff --git a/clients/client-core/src/client/mod.rs b/clients/client-core/src/client/mod.rs index bce1a089dc..8c09544ce6 100644 --- a/clients/client-core/src/client/mod.rs +++ b/clients/client-core/src/client/mod.rs @@ -1,5 +1,7 @@ -use std::sync::atomic::AtomicBool; +// 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; @@ -9,10 +11,3 @@ pub mod received_buffer; #[cfg(feature = "reply-surb")] pub mod reply_key_storage; pub mod topology_control; - -// This is *NOT* used to signal shutdown. -// It's critical that we don't have any tasks finishing early, this is an additional safety check -// that tasks exiting are doing so because shutdown has been signalled, and no other reason. -// In particular for tasks that rely on their associated channel being closed to signal shutdown, -// and don't have access to a shutdown listener channel. -pub static SHUTDOWN_HAS_BEEN_SIGNALLED: AtomicBool = AtomicBool::new(false); 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 9b285d8444..bd7b34e93d 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 @@ -70,7 +70,6 @@ impl AcknowledgementListener { } } - #[cfg(not(target_arch = "wasm32"))] pub(super) async fn run_with_shutdown(&mut self, mut shutdown: task::ShutdownListener) { debug!("Started AcknowledgementListener with graceful shutdown support"); @@ -88,11 +87,12 @@ impl AcknowledgementListener { } } } - assert!(shutdown.is_shutdown_poll()); + shutdown.recv_timeout().await; log::debug!("AcknowledgementListener: Exiting"); } - #[cfg(target_arch = "wasm32")] + // todo: think whether this is still required + #[allow(dead_code)] pub(super) async fn run(&mut self) { debug!("Started AcknowledgementListener without graceful shutdown support"); 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 2b21639230..09cf48185a 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 @@ -245,7 +245,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,11 +271,15 @@ impl ActionController { } } } - assert!(shutdown.is_shutdown_poll()); + #[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")] + // todo: think whether this is still required + #[allow(dead_code)] pub(super) async fn run(&mut self) { debug!("Started ActionController without graceful shutdown support"); 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 0738d0aa80..dec2d153d6 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 @@ -194,7 +194,6 @@ where } } - #[cfg(not(target_arch = "wasm32"))] pub(super) async fn run_with_shutdown(&mut self, mut shutdown: task::ShutdownListener) { debug!("Started InputMessageListener with graceful shutdown support"); @@ -214,11 +213,12 @@ where } } } - assert!(shutdown.is_shutdown_poll()); + shutdown.recv_timeout().await; log::debug!("InputMessageListener: Exiting"); } - #[cfg(target_arch = "wasm32")] + // todo: think whether this is still required + #[allow(dead_code)] pub(super) async fn run(&mut self) { debug!("Started InputMessageListener without graceful shutdown support"); while let Some(input_msg) = self.input_receiver.recv().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..5bf2331555 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 @@ -234,7 +234,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; @@ -280,7 +279,8 @@ where }); } - #[cfg(target_arch = "wasm32")] + // todo: think whether this is still required + #[allow(dead_code)] pub(super) fn start(self) { let mut acknowledgement_listener = self.acknowledgement_listener; let mut input_message_listener = self.input_message_listener; 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 4b0bc3cc99..190f23e50c 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 @@ -125,7 +125,6 @@ where .expect("BatchRealMessageReceiver has stopped receiving!"); } - #[cfg(not(target_arch = "wasm32"))] pub(super) async fn run_with_shutdown(&mut self, mut shutdown: task::ShutdownListener) { debug!("Started RetransmissionRequestListener with graceful shutdown support"); @@ -143,11 +142,12 @@ where } } } - assert!(shutdown.is_shutdown_poll()); + shutdown.recv_timeout().await; log::debug!("RetransmissionRequestListener: Exiting"); } - #[cfg(target_arch = "wasm32")] + // todo: think whether this is still required + #[allow(dead_code)] pub(super) async fn run(&mut self) { debug!("Started RetransmissionRequestListener without graceful shutdown support"); 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..958c696aa7 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 @@ -42,7 +42,6 @@ impl SentNotificationListener { .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"); @@ -66,7 +65,8 @@ impl SentNotificationListener { log::debug!("SentNotificationListener: Exiting"); } - #[cfg(target_arch = "wasm32")] + // todo: think whether this is still required + #[allow(dead_code)] pub(super) async fn run(&mut self) { debug!("Started SentNotificationListener without graceful shutdown support"); 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..e46c02e31e 100644 --- a/clients/client-core/src/client/real_messages_control/mod.rs +++ b/clients/client-core/src/client/real_messages_control/mod.rs @@ -113,9 +113,9 @@ impl RealMessagesController { input_receiver: InputMessageReceiver, mix_sender: BatchMixMessageSender, topology_access: TopologyAccessor, - #[cfg(feature = "reply-surb")] reply_key_storage: ReplyKeyStorage, lane_queue_lengths: LaneQueueLengths, client_connection_rx: ConnectionCommandReceiver, + #[cfg(feature = "reply-surb")] reply_key_storage: ReplyKeyStorage, ) -> Self { let rng = OsRng; @@ -175,7 +175,6 @@ impl RealMessagesController { } } - #[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; 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 503203922c..c6ec08c15e 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 @@ -483,6 +483,10 @@ where } else { log::debug!("{status_str}"); } + } + + #[cfg(not(target_arch = "wasm32"))] + 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: {}", @@ -491,34 +495,61 @@ where } } - #[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(1)); + #[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(); - } - 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; + } } } } - assert!(shutdown.is_shutdown_poll()); log::debug!("OutQueueControl: Exiting"); } - #[cfg(target_arch = "wasm32")] + // todo: think whether this is still required + #[allow(dead_code)] pub(super) async fn run(&mut self) { debug!("Started OutQueueControl without graceful shutdown support"); diff --git a/clients/client-core/src/client/real_messages_control/real_traffic_stream/sending_delay_controller.rs b/clients/client-core/src/client/real_messages_control/real_traffic_stream/sending_delay_controller.rs index c90f7b4983..b22ffae7ff 100644 --- a/clients/client-core/src/client/real_messages_control/real_traffic_stream/sending_delay_controller.rs +++ b/clients/client-core/src/client/real_messages_control/real_traffic_stream/sending_delay_controller.rs @@ -82,7 +82,7 @@ impl SendingDelayController { self.current_multiplier = (self.current_multiplier + 1).clamp(self.lower_bound, self.upper_bound); self.time_when_changed = get_time_now(); - log::debug!( + log::warn!( "Increasing sending delay multiplier to: {}", self.current_multiplier ); diff --git a/clients/client-core/src/client/received_buffer.rs b/clients/client-core/src/client/received_buffer.rs index fa61b9b9a0..ed693993a2 100644 --- a/clients/client-core/src/client/received_buffer.rs +++ b/clients/client-core/src/client/received_buffer.rs @@ -320,7 +320,6 @@ impl RequestReceiver { } } - #[cfg(not(target_arch = "wasm32"))] async fn run_with_shutdown(&mut self, mut shutdown: task::ShutdownListener) { debug!("Started RequestReceiver with graceful shutdown support"); while !shutdown.is_shutdown() { @@ -340,11 +339,12 @@ impl RequestReceiver { }, } } - assert!(shutdown.is_shutdown_poll()); + shutdown.recv_timeout().await; log::debug!("RequestReceiver: Exiting"); } - #[cfg(target_arch = "wasm32")] + // todo: think whether this is still required + #[allow(dead_code)] async fn run(&mut self) { debug!("Started RequestReceiver without graceful shutdown support"); @@ -370,7 +370,6 @@ impl FragmentedMessageReceiver { } } - #[cfg(not(target_arch = "wasm32"))] async fn run_with_shutdown(&mut self, mut shutdown: task::ShutdownListener) { debug!("Started FragmentedMessageReceiver with graceful shutdown support"); while !shutdown.is_shutdown() { @@ -389,11 +388,12 @@ impl FragmentedMessageReceiver { } } } - assert!(shutdown.is_shutdown_poll()); + shutdown.recv_timeout().await; log::debug!("FragmentedMessageReceiver: Exiting"); } - #[cfg(target_arch = "wasm32")] + // todo: think whether this is still required + #[allow(dead_code)] async fn run(&mut self) { debug!("Started FragmentedMessageReceiver without graceful shutdown support"); @@ -430,7 +430,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; diff --git a/clients/client-core/src/client/topology_control.rs b/clients/client-core/src/client/topology_control.rs index afedc7ad61..6d1b82e066 100644 --- a/clients/client-core/src/client/topology_control.rs +++ b/clients/client-core/src/client/topology_control.rs @@ -2,6 +2,7 @@ // 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; @@ -296,14 +297,22 @@ impl TopologyRefresher { self.topology_accessor.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() => { @@ -311,19 +320,23 @@ impl TopologyRefresher { }, } } - assert!(shutdown.is_shutdown_poll()); + shutdown.recv_timeout().await; log::debug!("TopologyRefresher: Exiting"); }) } - #[cfg(target_arch = "wasm32")] pub fn start(mut self) { - use futures::StreamExt; - spawn_future(async move { + #[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 let Some(_) = interval.next().await { + + while (interval.next().await).is_some() { self.refresh().await; } }) diff --git a/clients/client-core/src/config/mod.rs b/clients/client-core/src/config/mod.rs index 4a3d1c1273..82317a6431 100644 --- a/clients/client-core/src/config/mod.rs +++ b/clients/client-core/src/config/mod.rs @@ -34,7 +34,7 @@ pub fn missing_string_value() -> String { MISSING_VALUE.to_string() } -#[derive(Debug, Deserialize, PartialEq, Serialize)] +#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)] #[serde(deny_unknown_fields)] pub struct Config { client: Client, @@ -42,17 +42,23 @@ pub struct Config { #[serde(default)] logging: Logging, #[serde(default)] - debug: Debug, + debug: DebugConfig, } -impl Config { - pub fn new>(id: S) -> Self { +impl Config { + pub fn new>(id: S) -> Self + where + T: NymConfig, + { let mut cfg = Config::default(); cfg.with_id(id); cfg } - pub fn with_id>(&mut self, id: S) { + pub fn with_id>(&mut self, id: S) + where + T: NymConfig, + { let id = id.into(); // identity key setting @@ -117,7 +123,7 @@ impl Config { 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; } @@ -203,7 +209,11 @@ 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 + } + + pub fn get_gateway_endpoint(&self) -> &GatewayEndpointConfig { &self.client.gateway_endpoint } @@ -212,6 +222,10 @@ impl Config { } // 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,7 +271,7 @@ 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 { @@ -277,7 +291,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 +303,10 @@ pub struct GatewayEndpoint { pub gateway_listener: String, } -impl From for GatewayEndpoint { - fn from(node: topology::gateway::Node) -> GatewayEndpoint { +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 +314,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")] @@ -315,6 +329,7 @@ pub struct Client { disabled_credentials_mode: bool, /// Addresses to nymd validators via which the client can communicate with the chain. + #[serde(default)] validator_urls: Vec, /// Addresses to APIs running on validator from which the client gets the view of the network. @@ -345,7 +360,7 @@ pub struct Client { 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, @@ -415,13 +430,13 @@ impl Client { } } -#[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 @@ -487,7 +502,7 @@ pub struct Debug { pub use_extended_packet_size: Option, } -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "lowercase")] pub enum ExtendedPacketSize { Extended8, @@ -495,9 +510,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, diff --git a/clients/client-core/src/error.rs b/clients/client-core/src/error.rs index 24e846087f..e173a987fc 100644 --- a/clients/client-core/src/error.rs +++ b/clients/client-core/src/error.rs @@ -1,6 +1,8 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +#[cfg(feature = "reply-surb")] +use crate::client::reply_key_storage::ReplyKeyStorageError; use crypto::asymmetric::identity::Ed25519RecoveryError; use gateway_client::error::GatewayClientError; use validator_client::ValidatorClientError; @@ -16,6 +18,10 @@ pub enum ClientCoreError { #[error("Validator client error: {0}")] ValidatorClientError(#[from] ValidatorClientError), + #[cfg(feature = "reply-surb")] + #[error("Reply key storage error: {0}")] + ReplyKeyStorageError(#[from] ReplyKeyStorageError), + #[error("No gateway with id: {0}")] NoGatewayWithId(String), #[error("No gateways on network")] @@ -26,4 +32,7 @@ pub enum ClientCoreError { CouldNotLoadExistingGatewayConfiguration(std::io::Error), #[error("The current network topology seem to be insufficient to route any packets through")] InsufficientNetworkTopology, + + #[error("Unexpected exit")] + UnexpectedExit, } diff --git a/clients/client-core/src/init.rs b/clients/client-core/src/init.rs index be9758d363..5e7c1e6c2a 100644 --- a/clients/client-core/src/init.rs +++ b/clients/client-core/src/init.rs @@ -90,7 +90,6 @@ async fn register_with_gateway( gateway.owner.clone(), our_identity.clone(), timeout, - #[cfg(not(target_arch = "wasm32"))] None, ); gateway_client diff --git a/clients/native/Cargo.toml b/clients/native/Cargo.toml index ab22e76a9d..eb4c722501 100644 --- a/clients/native/Cargo.toml +++ b/clients/native/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nym-client" -version = "1.1.0" +version = "1.1.1" authors = ["Dave Hrycyszyn ", "Jędrzej Stuczyński "] description = "Implementation of the Nym Client" edition = "2021" diff --git a/clients/native/src/client/config/mod.rs b/clients/native/src/client/config/mod.rs index f9c04ca1d7..8ce268e9ea 100644 --- a/clients/native/src/client/config/mod.rs +++ b/clients/native/src/client/config/mod.rs @@ -27,6 +27,10 @@ impl SocketType { _ => SocketType::None, } } + + pub fn is_websocket(&self) -> bool { + matches!(self, SocketType::WebSocket) + } } #[derive(Debug, Default, Deserialize, PartialEq, Serialize)] diff --git a/clients/native/src/client/mod.rs b/clients/native/src/client/mod.rs index 4d60e3ad54..5dc01920cc 100644 --- a/clients/native/src/client/mod.rs +++ b/clients/native/src/client/mod.rs @@ -1,205 +1,46 @@ // Copyright 2021 - 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::{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::ReplySurb; +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(); @@ -212,163 +53,48 @@ 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); } - /// 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, - ) { - let lane = TransmissionLane::General; - let input_msg = InputMessage::new_fresh(recipient, message, with_reply_surb, lane); - - self.input_tx - .as_ref() - .expect("start method was not called before!") - .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, reply_surb: ReplySurb, message: Vec) { - let input_msg = InputMessage::new_reply(reply_surb, message); - - self.input_tx - .as_ref() - .expect("start method was not called before!") - .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) - /// Note: it waits for the first occurrence of messages being sent to ourselves. If you expect multiple - /// messages, you might have to call this function repeatedly. - // TODO: I guess this should really return something that `impl Stream` - pub async fn wait_for_messages(&mut self) -> Vec { - use futures::StreamExt; - - self.receive_tx - .as_mut() - .expect("start method was not called before!") - .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?; + /// blocking version of `start_socket` method. Will run forever (or until SIGINT is sent) + pub async fn run_socket_forever(self) -> Result<(), ClientError> { + let shutdown = self.start_socket().await?; wait_for_signal().await; println!( @@ -388,117 +114,117 @@ impl NymClient { 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(), - ); + pub async fn start_socket(self) -> Result { + if !self.config.get_socket_type().is_websocket() { + return Err(ClientError::InvalidSocketMode); } - 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(); + let base_builder = BaseClientBuilder::new_from_base_config( + self.config.get_base(), + self.key_manager, + Some(Self::create_bandwidth_controller(&self.config).await), + ); - // 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 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.receive_tx = Some(reconstructed_receiver); - self.input_tx = Some(input_sender); - } - } + Self::start_websocket_listener(&self.config, client_input, client_output, self_address); 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) + } + + 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), + ); + + 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, + ) { + let lane = TransmissionLane::General; + let input_msg = InputMessage::new_fresh(recipient, message, with_reply_surb, lane); + + 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, reply_surb: ReplySurb, message: Vec) { + let input_msg = InputMessage::new_reply(reply_surb, message); + + 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) + /// Note: it waits for the first occurrence of messages being sent to ourselves. If you expect multiple + /// messages, you might have to call this function repeatedly. + // TODO: I guess this should really return something that `impl Stream` + pub async fn wait_for_messages(&mut self) -> Vec { + use futures::StreamExt; + + self.reconstructed_receiver + .next() + .await + .expect("buffer controller seems to have somehow died!") } } diff --git a/clients/native/src/commands/init.rs b/clients/native/src/commands/init.rs index 6a38770eec..d97bcc8e60 100644 --- a/clients/native/src/commands/init.rs +++ b/clients/native/src/commands/init.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use clap::Args; -use client_core::{config::GatewayEndpoint, error::ClientCoreError}; +use client_core::{config::GatewayEndpointConfig, error::ClientCoreError}; use config::NymConfig; use crate::{ @@ -132,7 +132,7 @@ async fn setup_gateway( register: bool, user_chosen_gateway_id: Option<&str>, config: &Config, -) -> Result { +) -> 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. diff --git a/clients/native/src/commands/run.rs b/clients/native/src/commands/run.rs index d5c892fadc..df49471ea9 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, }; @@ -98,5 +98,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..06c45116b6 100644 --- a/clients/native/src/error.rs +++ b/clients/native/src/error.rs @@ -1,25 +1,15 @@ -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::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), #[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..6dc0d398ad 100644 --- a/clients/native/src/websocket/handler.rs +++ b/clients/native/src/websocket/handler.rs @@ -73,14 +73,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, diff --git a/clients/socks5/Cargo.toml b/clients/socks5/Cargo.toml index 859bacfe62..b5eb3a1a9d 100644 --- a/clients/socks5/Cargo.toml +++ b/clients/socks5/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nym-socks5-client" -version = "1.1.0" +version = "1.1.1" authors = ["Dave Hrycyszyn "] description = "A SOCKS5 localhost proxy that converts incoming messages to Sphinx and sends them to a Nym address" edition = "2021" diff --git a/clients/socks5/src/client/mod.rs b/clients/socks5/src/client/mod.rs index c6b734a749..0784666af8 100644 --- a/clients/socks5/src/client/mod.rs +++ b/clients/socks5/src/client/mod.rs @@ -1,44 +1,22 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use std::sync::atomic::Ordering; - use crate::client::config::Config; use crate::error::Socks5ClientError; 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::{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 task::{wait_for_signal, ShutdownListener, ShutdownNotifier}; +use std::error::Error; +use task::{wait_for_signal_and_error, ShutdownListener, ShutdownNotifier}; pub mod config; @@ -72,132 +50,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")] let bandwidth_controller = { let details = network_defaults::NymNetworkDetails::new_from_env(); @@ -210,140 +63,99 @@ 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, - shutdown: ShutdownListener, + config: &Config, + client_input: ClientInput, + client_output: ClientOutput, + self_address: Recipient, + mut 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, - shutdown, + config.get_provider_mix_address(), + self_address, + shared_lane_queue_lengths, + shutdown.clone(), ); tokio::spawn(async move { - sphinx_socks - .serve(msg_input, buffer_requester, client_connection_tx) + // 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( + input_sender, + received_buffer_request_sender, + connection_command_sender, + ) .await + { + shutdown.send_we_stopped(Box::new(res)); + } }); } /// blocking version of `start` method. Will run forever (or until SIGINT is sent) - pub async fn run_forever(&mut self) -> Result<(), Socks5ClientError> { - let mut shutdown = self.start().await?; - wait_for_signal().await; + pub async fn run_forever(self) -> Result<(), Box> { + let mut shutdown = self + .start() + .await + .map_err(|err| Box::new(err) as Box)?; + + let res = wait_for_signal_and_error(&mut shutdown).await; log::info!("Sending shutdown"); - client_core::client::SHUTDOWN_HAS_BEEN_SIGNALLED.store(true, Ordering::Relaxed); shutdown.signal_shutdown().ok(); log::info!("Waiting for tasks to finish... (Press ctrl-c to force)"); shutdown.wait_for_shutdown().await; log::info!("Stopping nym-socks5-client"); - Ok(()) + res } // Variant of `run_forever` that listends for remote control messages pub async fn run_and_listen( - &mut self, + self, mut receiver: Socks5ControlMessageReceiver, - ) -> Result<(), Socks5ClientError> { - let mut shutdown = self.start().await?; - tokio::select! { + ) -> Result<(), Box> { + // Start the main task + let mut shutdown = self + .start() + .await + .map_err(|err| Box::new(err) as Box)?; + + let res = tokio::select! { + biased; message = receiver.next() => { log::debug!("Received message: {:?}", message); match message { @@ -354,118 +166,51 @@ impl NymClient { log::info!("Channel closed, stopping"); } } + Ok(()) + } + Some(msg) = shutdown.wait_for_error() => { + log::info!("Task error: {:?}", msg); + Err(msg) } _ = tokio::signal::ctrl_c() => { log::info!("Received SIGINT"); + Ok(()) }, - } + }; log::info!("Sending shutdown"); - client_core::client::SHUTDOWN_HAS_BEEN_SIGNALLED.store(true, Ordering::Relaxed); shutdown.signal_shutdown().ok(); log::info!("Waiting for tasks to finish... (Press ctrl-c to force)"); shutdown.wait_for_shutdown().await; log::info!("Stopping nym-socks5-client"); - Ok(()) + 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), ); - 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..a1d123fc75 100644 --- a/clients/socks5/src/commands/init.rs +++ b/clients/socks5/src/commands/init.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use clap::Args; -use client_core::{config::GatewayEndpoint, error::ClientCoreError}; +use client_core::{config::GatewayEndpointConfig, error::ClientCoreError}; use config::NymConfig; use crate::{ @@ -131,7 +131,7 @@ async fn setup_gateway( register: bool, user_chosen_gateway_id: Option<&str>, config: &Config, -) -> Result { +) -> 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. diff --git a/clients/socks5/src/commands/mod.rs b/clients/socks5/src/commands/mod.rs index b069c13542..e1d42e0fb1 100644 --- a/clients/socks5/src/commands/mod.rs +++ b/clients/socks5/src/commands/mod.rs @@ -1,8 +1,9 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use std::error::Error; + use crate::client::config::Config; -use crate::error::Socks5ClientError; use clap::CommandFactory; use clap::{Parser, Subcommand}; use completions::{fig_generate, ArgShell}; @@ -87,7 +88,7 @@ pub(crate) struct OverrideConfig { enabled_credentials_mode: bool, } -pub(crate) async fn execute(args: &Cli) -> Result<(), Socks5ClientError> { +pub(crate) async fn execute(args: &Cli) -> Result<(), Box> { let bin_name = "nym-socks5-client"; match &args.command { diff --git a/clients/socks5/src/commands/run.rs b/clients/socks5/src/commands/run.rs index 406ae72d94..d74e6678fe 100644 --- a/clients/socks5/src/commands/run.rs +++ b/clients/socks5/src/commands/run.rs @@ -86,14 +86,16 @@ fn version_check(cfg: &Config) -> bool { } } -pub(crate) async fn execute(args: &Run) -> Result<(), Socks5ClientError> { +pub(crate) async fn execute(args: &Run) -> Result<(), Box> { let id = &args.id; let mut config = match Config::load_from_file(Some(id)) { Ok(cfg) => cfg, Err(err) => { error!("Failed to load config for {}. Are you sure you have run `init` before? (Error was: {})", id, err); - return Err(Socks5ClientError::FailedToLoadConfig(id.to_string())); + return Err(Box::new(Socks5ClientError::FailedToLoadConfig( + id.to_string(), + ))); } }; @@ -102,7 +104,7 @@ pub(crate) async fn execute(args: &Run) -> Result<(), Socks5ClientError> { if !version_check(&config) { error!("failed the local version check"); - return Err(Socks5ClientError::FailedLocalVersionCheck); + return Err(Box::new(Socks5ClientError::FailedLocalVersionCheck)); } NymClient::new(config).run_forever().await diff --git a/clients/socks5/src/error.rs b/clients/socks5/src/error.rs index 4ebca0e2e5..2ff6ffbfc2 100644 --- a/clients/socks5/src/error.rs +++ b/clients/socks5/src/error.rs @@ -1,25 +1,21 @@ -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::error::ClientCoreError; + +use crate::socks::types::SocksProxyError; #[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), + + #[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 d7a0fdf01f..d41e5e4bd8 100644 --- a/clients/socks5/src/main.rs +++ b/clients/socks5/src/main.rs @@ -1,8 +1,9 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use std::error::Error; + use clap::{crate_version, Parser}; -use error::Socks5ClientError; use logging::setup_logging; use network_defaults::setup_env; @@ -12,7 +13,7 @@ pub mod error; pub mod socks; #[tokio::main] -async fn main() -> Result<(), Socks5ClientError> { +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 df73d9ee7f..acb0a989ad 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; @@ -129,13 +129,13 @@ impl AsyncWrite for StreamState { /// 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 { 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,29 +158,32 @@ impl Drop for SocksClient { } impl SocksClient { - /// Create a new SOCKClient #[allow(clippy::too_many_arguments)] pub fn new( 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, - shutdown_listener: ShutdownListener, + mut shutdown_listener: ShutdownListener, ) -> Self { + // If this task fails and exits, we don't want to send shutdown signal + shutdown_listener.mark_as_success(); + let connection_id = Self::generate_random(); + SocksClient { 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, @@ -192,16 +195,49 @@ 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?; + self.shutdown_listener.mark_as_success(); Ok(()) } @@ -209,25 +245,27 @@ 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) { @@ -251,10 +289,15 @@ impl SocksClient { .await; let stream = self.stream.run_proxy(); - let local_stream_remote = stream - .peer_addr() - .expect("failed to extract peer address") - .to_string(); + let peer_addr = match stream.peer_addr() { + Ok(peer_addr) => peer_addr, + Err(err) => { + log::error!("Unable to extract the remote peer address: {err}"); + return; + } + }; + let local_stream_remote = peer_addr.to_string(); + let connection_id = self.connection_id; let input_sender = self.input_sender.clone(); @@ -285,8 +328,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(); @@ -295,7 +347,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 @@ -326,8 +381,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, @@ -341,13 +396,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: /// /// +----+------+----------+------+------------+ @@ -359,7 +431,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?; @@ -368,7 +440,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; @@ -404,11 +476,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 @@ -427,7 +499,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 760ec36d24..430b4322da 100644 --- a/clients/socks5/src/socks/mixnet_responses.rs +++ b/clients/socks5/src/socks/mixnet_responses.rs @@ -1,3 +1,5 @@ +use std::time::Duration; + use futures::channel::mpsc; use futures::StreamExt; use log::*; @@ -103,7 +105,10 @@ impl MixnetResponseListener { } } } - assert!(self.shutdown.is_shutdown_poll()); + #[cfg(not(target_arch = "wasm32"))] + tokio::time::timeout(Duration::from_secs(5), self.shutdown.recv()) + .await + .expect("Task stopped without shutdown called"); log::debug!("MixnetResponseListener: Exiting"); } } diff --git a/clients/socks5/src/socks/mod.rs b/clients/socks5/src/socks/mod.rs index 20dd99f125..2acdaa615b 100644 --- a/clients/socks5/src/socks/mod.rs +++ b/clients/socks5/src/socks/mod.rs @@ -1,5 +1,9 @@ #![forbid(unsafe_code)] +use std::convert::TryFrom; + +use self::types::SocksProxyError; + pub mod authentication; mod client; pub(crate) mod mixnet_responses; @@ -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 f61d6052a0..e31c8d2b0b 100644 --- a/clients/socks5/src/socks/server.rs +++ b/clients/socks5/src/socks/server.rs @@ -1,8 +1,7 @@ -use super::authentication::Authenticator; -use super::client::SocksClient; +use crate::error::Socks5ClientError; + use super::{ - mixnet_responses::MixnetResponseListener, - types::{ResponseCode, SocksProxyError}, + authentication::Authenticator, client::SocksClient, mixnet_responses::MixnetResponseListener, }; use client_connections::{ConnectionCommandSender, LaneQueueLengths}; use client_core::client::{ @@ -12,6 +11,7 @@ use log::*; use nymsphinx::addressing::clients::Recipient; use proxy_helpers::connection_controller::{BroadcastActiveConnections, Controller}; use std::net::SocketAddr; +use tap::TapFallible; use task::ShutdownListener; use tokio::net::TcpListener; @@ -56,8 +56,10 @@ impl SphinxSocksServer { input_sender: InputMessageSender, buffer_requester: ReceivedBufferRequestSender, client_connection_tx: ConnectionCommandSender, - ) -> Result<(), SocksProxyError> { - let listener = TcpListener::bind(self.listening_address).await.unwrap(); + ) -> Result<(), Socks5ClientError> { + let listener = TcpListener::bind(self.listening_address) + .await + .tap_err(|err| log::error!("Failed to bind to address: {err}"))?; info!("Serving Connections..."); // controller for managing all active connections @@ -83,47 +85,26 @@ impl SphinxSocksServer { loop { tokio::select! { Ok((stream, _remote)) = listener.accept() => { - // TODO Optimize this let mut client = SocksClient::new( 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..3d8e5b161c 100644 --- a/clients/socks5/src/socks/types.rs +++ b/clients/socks5/src/socks/types.rs @@ -1,7 +1,17 @@ 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 { +#[derive(Debug, Snafu)] +pub(crate) enum ResponseCodeV5 { Success = 0x00, #[snafu(display("SOCKS5 Server Failure"))] Failure = 0x01, @@ -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/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..0cbf2fe4d6 100644 --- a/clients/webassembly/js-example/worker.js +++ b/clients/webassembly/js-example/worker.js @@ -61,16 +61,9 @@ 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 diff --git a/clients/webassembly/src/client/config.rs b/clients/webassembly/src/client/config.rs index 2b8b8bd644..66c6ceefe7 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 { diff --git a/clients/webassembly/src/client/mod.rs b/clients/webassembly/src/client/mod.rs index c64dbe69e0..68a40cf99b 100644 --- a/clients/webassembly/src/client/mod.rs +++ b/clients/webassembly/src/client/mod.rs @@ -2,51 +2,44 @@ // 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 crate::client::response_pusher::ResponsePusher; +use client_connections::TransmissionLane; +use client_core::client::base_client::{BaseClientBuilder, ClientInput, ClientOutput}; +use client_core::client::{inbound_messages::InputMessage, key_manager::KeyManager}; use crypto::asymmetric::identity; -use futures::channel::mpsc; -use futures::StreamExt; -use gateway_client::{ - AcknowledgementReceiver, AcknowledgementSender, GatewayClient, MixnetMessageReceiver, - MixnetMessageSender, -}; use nymsphinx::addressing::clients::Recipient; use rand::rngs::OsRng; +use task::ShutdownNotifier; use wasm_bindgen::prelude::*; -use wasm_bindgen_futures::spawn_local; -use wasm_utils::console_log; +use wasm_utils::{console_error, console_log}; pub mod config; +mod response_pusher; #[wasm_bindgen] pub struct NymClient { config: Config, /// KeyManager object containing smart pointers to all relevant keys used by the client. - key_manager: KeyManager, + // due to disgusting workaround I had to wrap the key_manager in an Option + // so that the interface wouldn't change (i.e. both `start` and `new` would still return a `NymClient`) + key_manager: Option, + self_address: Option, // 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, + client_input: Option, // callbacks on_message: Option, on_binary_message: Option, on_gateway_connect: Option, + + // 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: Option, } #[wasm_bindgen] @@ -55,14 +48,19 @@ impl NymClient { pub fn new(config: Config) -> Self { Self { config, - key_manager: Self::setup_key_manager(), + key_manager: Some(Self::setup_key_manager()), on_message: None, on_binary_message: None, on_gateway_connect: None, - input_tx: None, + client_input: None, + self_address: None, + _shutdown: None, } } + // 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; @@ -84,298 +82,26 @@ impl NymClient { } fn as_mix_recipient(&self) -> Recipient { + // another disgusting (and hopefully temporary) workaround + let key_manager_ref = self + .key_manager + .as_ref() + .expect("attempting to call 'as_mix_recipient' after 'start'"); + Recipient::new( - *self.key_manager.identity_keypair().public_key(), - *self.key_manager.encryption_keypair().public_key(), + *key_manager_ref.identity_keypair().public_key(), + *key_manager_ref.encryption_keypair().public_key(), identity::PublicKey::from_base58_string(&self.config.gateway_endpoint.gateway_id) .expect("no gateway has been selected"), ) } pub fn self_address(&self) -> String { - self.as_mix_recipient().to_string() - } - - // 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..."); - - 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()); + if let Some(address) = &self.self_address { + address.clone() + } else { + self.as_mix_recipient().to_string() } - - 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" - ); - } - - 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!"); - } - } - } - }); - } - - 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 - - // 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(); - - // 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 @@ -395,13 +121,65 @@ impl NymClient { let input_msg = InputMessage::new_fresh(recipient, message, false, lane); - self.input_tx + self.client_input .as_ref() .expect("start method was not called before!") + .input_sender .send(input_msg) .await .expect("InputMessageReceiver has stopped receiving!"); self } + + fn start_reconstructed_pusher( + client_output: ClientOutput, + on_message: Option, + on_binary_message: Option, + ) { + ResponsePusher::new(client_output, on_message, on_binary_message).start() + } + + pub async fn start(mut self) -> NymClient { + console_log!("Starting the wasm client"); + + let base_builder = BaseClientBuilder::new( + &self.config.gateway_endpoint, + &self.config.debug, + self.key_manager.take().unwrap(), + None, + true, + vec![self.config.validator_api_url.clone()], + ); + + self.self_address = Some(base_builder.as_mix_recipient().to_string()); + let mut started_client = match base_builder.start_base().await { + Ok(base_client) => base_client, + Err(err) => { + console_error!("failed to start base client components - {}", err); + // proper error handling is left here as an exercise for the reader (hi Mark : )) + panic!("failed to start base client components - {err}") + } + }; + 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"), + }; + + // those should be moved to a completely different struct, but I don't want to break compatibility for now + let client_input = started_client.client_input.register_producer(); + let client_output = started_client.client_output.register_consumer(); + + let on_message = self.on_message.take(); + let on_binary_message = self.on_binary_message.take(); + Self::start_reconstructed_pusher(client_output, on_message, on_binary_message); + self.client_input = Some(client_input); + self._shutdown = Some(started_client.shutdown_notifier); + + self + } } diff --git a/clients/webassembly/src/client/response_pusher.rs b/clients/webassembly/src/client/response_pusher.rs new file mode 100644 index 0000000000..c5717a9bc3 --- /dev/null +++ b/clients/webassembly/src/client/response_pusher.rs @@ -0,0 +1,71 @@ +// 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 wasm_bindgen::JsValue; +use wasm_bindgen_futures::spawn_local; +use wasm_utils::console_log; + +pub(crate) struct ResponsePusher { + reconstructed_receiver: ReconstructedMessagesReceiver, + on_message: Option, + on_binary_message: Option, +} + +impl ResponsePusher { + pub(crate) fn new( + client_output: ClientOutput, + on_message: Option, + on_binary_message: Option, + ) -> Self { + if on_message.is_none() && on_binary_message.is_none() { + // exercise for the reader : ) + panic!("neither 'on_message' nor 'on_binary_message' was set!") + } + + // 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, + on_binary_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 msg in reconstructed { + if let Some(ref callback_binary) = self.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) = self.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!"); + } + } + } + }) + } +} 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-libs/gateway-client/Cargo.toml b/common/client-libs/gateway-client/Cargo.toml index 01557331fa..d1ea2abf1e 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" diff --git a/common/client-libs/gateway-client/src/bandwidth.rs b/common/client-libs/gateway-client/src/bandwidth.rs index 6f3b7722dc..00db2b1180 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")] diff --git a/common/client-libs/gateway-client/src/client.rs b/common/client-libs/gateway-client/src/client.rs index e88424b747..56ff03737a 100644 --- a/common/client-libs/gateway-client/src/client.rs +++ b/common/client-libs/gateway-client/src/client.rs @@ -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,8 +66,9 @@ pub struct GatewayClient { /// Delay between each subsequent reconnection attempt. reconnection_backoff: Duration, - #[cfg(not(target_arch = "wasm32"))] /// Listen to shutdown messages. + // TODO: fix this + #[cfg_attr(target_arch = "wasm32", allow(dead_code))] shutdown: Option, } @@ -85,7 +85,7 @@ impl GatewayClient { ack_sender: AcknowledgementSender, response_timeout_duration: Duration, bandwidth_controller: Option>, - #[cfg(not(target_arch = "wasm32"))] shutdown: Option, + shutdown: Option, ) -> Self { GatewayClient { authenticated: false, @@ -97,18 +97,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 +130,7 @@ impl GatewayClient { gateway_owner: String, local_identity: Arc, response_timeout_duration: Duration, - #[cfg(not(target_arch = "wasm32"))] shutdown: Option, + shutdown: Option, ) -> Self { use futures::channel::mpsc; @@ -144,12 +138,7 @@ 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 packet_router = PacketRouter::new(ack_tx, mix_tx, shutdown.clone()); GatewayClient { authenticated: false, @@ -167,7 +156,6 @@ impl GatewayClient { should_reconnect_on_failure: false, reconnection_attempts: DEFAULT_RECONNECTION_ATTEMPTS, reconnection_backoff: DEFAULT_RECONNECTION_BACKOFF, - #[cfg(not(target_arch = "wasm32"))] shutdown, } } @@ -307,6 +295,8 @@ impl GatewayClient { 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 @@ -401,13 +391,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 { @@ -427,13 +414,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 { diff --git a/common/client-libs/gateway-client/src/packet_router.rs b/common/client-libs/gateway-client/src/packet_router.rs index 6732d2202e..59d1d8add1 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,7 +21,6 @@ pub type AcknowledgementReceiver = mpsc::UnboundedReceiver>>; pub struct PacketRouter { ack_sender: AcknowledgementSender, mixnet_message_sender: MixnetMessageSender, - #[cfg(not(target_arch = "wasm32"))] shutdown: Option, } @@ -31,12 +28,11 @@ impl PacketRouter { pub fn new( ack_sender: AcknowledgementSender, mixnet_message_sender: MixnetMessageSender, - #[cfg(not(target_arch = "wasm32"))] shutdown: Option, + shutdown: Option, ) -> Self { PacketRouter { ack_sender, mixnet_message_sender, - #[cfg(not(target_arch = "wasm32"))] shutdown, } } @@ -86,7 +82,6 @@ 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 diff --git a/common/client-libs/validator-client/src/nymd/traits/mixnet_signing_client.rs b/common/client-libs/validator-client/src/nymd/traits/mixnet_signing_client.rs index 41fa7818a8..ad7773305a 100644 --- a/common/client-libs/validator-client/src/nymd/traits/mixnet_signing_client.rs +++ b/common/client-libs/validator-client/src/nymd/traits/mixnet_signing_client.rs @@ -180,6 +180,35 @@ pub trait MixnetSigningClient { .await } + async fn pledge_more( + &self, + additional_pledge: Coin, + fee: Option, + ) -> Result { + self.execute_mixnet_contract( + fee, + MixnetExecuteMsg::PledgeMore {}, + vec![additional_pledge], + ) + .await + } + + async fn pledge_more_on_behalf( + &self, + owner: AccountId, + additional_pledge: Coin, + fee: Option, + ) -> Result { + self.execute_mixnet_contract( + fee, + MixnetExecuteMsg::PledgeMoreOnBehalf { + owner: owner.to_string(), + }, + vec![additional_pledge], + ) + .await + } + async fn unbond_mixnode(&self, fee: Option) -> Result { self.execute_mixnet_contract(fee, MixnetExecuteMsg::UnbondMixnode {}, vec![]) .await diff --git a/common/client-libs/validator-client/src/nymd/traits/vesting_signing_client.rs b/common/client-libs/validator-client/src/nymd/traits/vesting_signing_client.rs index eebf0adfa3..120e9ca2e0 100644 --- a/common/client-libs/validator-client/src/nymd/traits/vesting_signing_client.rs +++ b/common/client-libs/validator-client/src/nymd/traits/vesting_signing_client.rs @@ -64,6 +64,21 @@ pub trait VestingSigningClient { fee: Option, ) -> Result; + async fn vesting_pledge_more( + &self, + additional_pledge: Coin, + fee: Option, + ) -> Result { + self.execute_vesting_contract( + fee, + VestingExecuteMsg::PledgeMore { + amount: additional_pledge.into(), + }, + vec![], + ) + .await + } + async fn vesting_unbond_mixnode(&self, fee: Option) -> Result; async fn vesting_track_unbond_mixnode( diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/events.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/events.rs index bba85e3ccd..f9f5de1802 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/events.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/events.rs @@ -12,6 +12,8 @@ pub const EVENT_VERSION_PREFIX: &str = "v2_"; pub enum MixnetEventType { MixnodeBonding, + PendingPledgeIncrease, + PledgeIncrease, GatewayBonding, GatewayUnbonding, PendingMixnodeUnbonding, @@ -51,6 +53,8 @@ impl ToString for MixnetEventType { fn to_string(&self) -> String { let event_name = match self { MixnetEventType::MixnodeBonding => "mixnode_bonding", + MixnetEventType::PendingPledgeIncrease => "pending_pledge_increase", + MixnetEventType::PledgeIncrease => "pledge_increase", MixnetEventType::GatewayBonding => "gateway_bonding", MixnetEventType::GatewayUnbonding => "gateway_unbonding", MixnetEventType::PendingMixnodeUnbonding => "pending_mixnode_unbonding", @@ -330,6 +334,19 @@ pub fn new_mixnode_bonding_event( .add_attribute(AMOUNT_KEY, amount.to_string()) } +pub fn new_pending_pledge_increase_event(mix_id: MixId, amount: &Coin) -> Event { + Event::new(MixnetEventType::PendingPledgeIncrease) + .add_attribute(MIX_ID_KEY, mix_id.to_string()) + .add_attribute(AMOUNT_KEY, amount.to_string()) +} + +pub fn new_pledge_increase_event(created_at: BlockHeight, mix_id: MixId, amount: &Coin) -> Event { + Event::new(MixnetEventType::PledgeIncrease) + .add_attribute(EVENT_CREATION_HEIGHT_KEY, created_at.to_string()) + .add_attribute(MIX_ID_KEY, mix_id.to_string()) + .add_attribute(AMOUNT_KEY, amount.to_string()) +} + pub fn new_mixnode_unbonding_event(created_at: BlockHeight, mix_id: MixId) -> Event { Event::new(MixnetEventType::MixnodeUnbonding) .add_attribute(EVENT_CREATION_HEIGHT_KEY, created_at.to_string()) diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/mixnode.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/mixnode.rs index 9bccf2d1ff..f8568e2c6e 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/mixnode.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/mixnode.rs @@ -325,6 +325,14 @@ impl MixNodeRewarding { Ok(()) } + pub fn increase_operator_uint128( + &mut self, + amount: Uint128, + ) -> Result<(), MixnetContractError> { + self.operator += amount.into_base_decimal()?; + Ok(()) + } + pub fn increase_delegates_uint128( &mut self, amount: Uint128, diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs index 1c00c64a9c..7bb3dd766c 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs @@ -113,6 +113,10 @@ pub enum ExecuteMsg { owner_signature: String, owner: String, }, + PledgeMore {}, + PledgeMoreOnBehalf { + owner: String, + }, UnbondMixnode {}, UnbondMixnodeOnBehalf { owner: String, @@ -223,6 +227,8 @@ impl ExecuteMsg { ExecuteMsg::BondMixnodeOnBehalf { mix_node, .. } => { format!("bonding mixnode {} on behalf", mix_node.identity_key) } + ExecuteMsg::PledgeMore {} => "pledging additional tokens".into(), + ExecuteMsg::PledgeMoreOnBehalf { .. } => "pledging additional tokens on behalf".into(), ExecuteMsg::UnbondMixnode { .. } => "unbonding mixnode".into(), ExecuteMsg::UnbondMixnodeOnBehalf { .. } => "unbonding mixnode on behalf".into(), ExecuteMsg::UpdateMixnodeCostParams { .. } => "updating mixnode cost parameters".into(), diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/pending_events.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/pending_events.rs index fce8bd8cf0..3fe817fcb6 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/pending_events.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/pending_events.rs @@ -34,6 +34,10 @@ pub enum PendingEpochEventKind { mix_id: MixId, proxy: Option, }, + PledgeMore { + mix_id: MixId, + amount: Coin, + }, UnbondMixnode { mix_id: MixId, }, @@ -78,7 +82,6 @@ pub enum PendingIntervalEventKind { mix_id: MixId, new_costs: MixNodeCostParams, }, - UpdateRewardingParams { update: IntervalRewardingParamsUpdate, }, diff --git a/common/cosmwasm-smart-contracts/vesting-contract/src/events.rs b/common/cosmwasm-smart-contracts/vesting-contract/src/events.rs index 896ead2c9e..4e5809beee 100644 --- a/common/cosmwasm-smart-contracts/vesting-contract/src/events.rs +++ b/common/cosmwasm-smart-contracts/vesting-contract/src/events.rs @@ -14,6 +14,7 @@ pub const VESTING_UNDELEGATION_EVENT_TYPE: &str = "vesting_undelegation"; pub const VESTING_GATEWAY_BONDING_EVENT_TYPE: &str = "vesting_gateway_bonding"; pub const VESTING_GATEWAY_UNBONDING_EVENT_TYPE: &str = "vesting_gateway_unbonding"; pub const VESTING_MIXNODE_BONDING_EVENT_TYPE: &str = "vesting_mixnode_bonding"; +pub const VESTING_PLEDGE_MORE_EVENT_TYPE: &str = "vesting_pledge_more"; pub const VESTING_MIXNODE_UNBONDING_EVENT_TYPE: &str = "vesting_mixnode_unbonding"; pub const VESTING_UPDATE_MIXNODE_CONFIG_EVENT_TYPE: &str = "vesting_update_mixnode_config"; pub const VESTING_UPDATE_MIXNODE_COST_PARAMS_EVENT_TYPE: &str = @@ -112,6 +113,10 @@ pub fn new_vesting_mixnode_bonding_event() -> Event { Event::new(VESTING_MIXNODE_BONDING_EVENT_TYPE) } +pub fn new_vesting_pledge_more_event() -> Event { + Event::new(VESTING_PLEDGE_MORE_EVENT_TYPE) +} + pub fn new_vesting_update_mixnode_config_event() -> Event { Event::new(VESTING_UPDATE_MIXNODE_CONFIG_EVENT_TYPE) } diff --git a/common/cosmwasm-smart-contracts/vesting-contract/src/messages.rs b/common/cosmwasm-smart-contracts/vesting-contract/src/messages.rs index 696df628aa..69eb323156 100644 --- a/common/cosmwasm-smart-contracts/vesting-contract/src/messages.rs +++ b/common/cosmwasm-smart-contracts/vesting-contract/src/messages.rs @@ -101,6 +101,9 @@ pub enum ExecuteMsg { owner_signature: String, amount: Coin, }, + PledgeMore { + amount: Coin, + }, UnbondMixnode {}, TrackUnbondMixnode { owner: String, @@ -145,6 +148,7 @@ impl ExecuteMsg { ExecuteMsg::WithdrawVestedCoins { .. } => "VestingExecuteMsg::WithdrawVestedCoins", ExecuteMsg::TrackUndelegation { .. } => "VestingExecuteMsg::TrackUndelegation", ExecuteMsg::BondMixnode { .. } => "VestingExecuteMsg::BondMixnode", + ExecuteMsg::PledgeMore { .. } => "VestingExecuteMsg::PledgeMore", ExecuteMsg::UnbondMixnode { .. } => "VestingExecuteMsg::UnbondMixnode", ExecuteMsg::TrackUnbondMixnode { .. } => "VestingExecuteMsg::TrackUnbondMixnode", ExecuteMsg::BondGateway { .. } => "VestingExecuteMsg::BondGateway", diff --git a/common/socks5/proxy-helpers/src/connection_controller.rs b/common/socks5/proxy-helpers/src/connection_controller.rs index 12fdba7dbb..97c4487af6 100644 --- a/common/socks5/proxy-helpers/src/connection_controller.rs +++ b/common/socks5/proxy-helpers/src/connection_controller.rs @@ -254,6 +254,10 @@ impl Controller { }, } } + #[cfg(not(target_arch = "wasm32"))] + tokio::time::timeout(Duration::from_secs(5), self.shutdown.recv()) + .await + .expect("Task stopped without shutdown called"); assert!(self.shutdown.is_shutdown_poll()); log::debug!("SOCKS5 Controller: Exiting"); } diff --git a/common/socks5/proxy-helpers/src/proxy_runner/inbound.rs b/common/socks5/proxy-helpers/src/proxy_runner/inbound.rs index 12caff2071..f2ebc2cd9b 100644 --- a/common/socks5/proxy-helpers/src/proxy_runner/inbound.rs +++ b/common/socks5/proxy-helpers/src/proxy_runner/inbound.rs @@ -208,5 +208,6 @@ where trace!("{} - inbound closed", connection_id); shutdown_notify.notify_one(); + shutdown_listener.mark_as_success(); reader } diff --git a/common/socks5/proxy-helpers/src/proxy_runner/mod.rs b/common/socks5/proxy-helpers/src/proxy_runner/mod.rs index fbd8cd21b3..defcbf4f5f 100644 --- a/common/socks5/proxy-helpers/src/proxy_runner/mod.rs +++ b/common/socks5/proxy-helpers/src/proxy_runner/mod.rs @@ -134,6 +134,7 @@ where } pub fn into_inner(mut self) -> (TcpStream, ConnectionReceiver) { + self.shutdown_listener.mark_as_success(); ( self.socket.take().unwrap(), self.mix_receiver.take().unwrap(), diff --git a/common/socks5/proxy-helpers/src/proxy_runner/outbound.rs b/common/socks5/proxy-helpers/src/proxy_runner/outbound.rs index 8fad665785..78db73afb6 100644 --- a/common/socks5/proxy-helpers/src/proxy_runner/outbound.rs +++ b/common/socks5/proxy-helpers/src/proxy_runner/outbound.rs @@ -90,5 +90,6 @@ pub(super) async fn run_outbound( trace!("{} - outbound closed", connection_id); shutdown_notify.notify_one(); + shutdown_listener.mark_as_success(); (writer, mix_receiver) } diff --git a/common/task/Cargo.toml b/common/task/Cargo.toml index 4c0c088d67..8240207575 100644 --- a/common/task/Cargo.toml +++ b/common/task/Cargo.toml @@ -6,8 +6,14 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +futures = "0.3" log = "0.4" -tokio = { version = "1.21.2", features = ["macros", "signal", "time", "sync"] } +thiserror = "1.0.37" +tokio = { version = "1.21.2", features = ["macros", "sync"] } + +[target."cfg(not(target_arch = \"wasm32\"))".dependencies.tokio] +version = "1.21.2" +features = ["signal", "time"] [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 b8ddb7076a..e2b5f623df 100644 --- a/common/task/src/lib.rs +++ b/common/task/src/lib.rs @@ -2,7 +2,9 @@ // SPDX-License-Identifier: Apache-2.0 pub mod shutdown; +#[cfg(not(target_arch = "wasm32"))] pub mod signal; pub use shutdown::{ShutdownListener, ShutdownNotifier}; -pub use signal::wait_for_signal; +#[cfg(not(target_arch = "wasm32"))] +pub use signal::{wait_for_signal, wait_for_signal_and_error}; diff --git a/common/task/src/shutdown.rs b/common/task/src/shutdown.rs index 21ea12569d..0f8efa51d7 100644 --- a/common/task/src/shutdown.rs +++ b/common/task/src/shutdown.rs @@ -1,27 +1,63 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use std::time::Duration; +use std::{error::Error, time::Duration}; -use tokio::sync::watch::{self, error::SendError}; +use futures::FutureExt; +use tokio::{ + sync::{ + mpsc, + watch::{self, error::SendError}, + }, + time::sleep, +}; const DEFAULT_SHUTDOWN_TIMER_SECS: u64 = 5; +pub(crate) type SentError = Box; +type ErrorSender = mpsc::UnboundedSender; +type ErrorReceiver = mpsc::UnboundedReceiver; + +#[derive(thiserror::Error, Debug)] +enum TaskError { + #[error("Task halted unexpectedly")] + UnexpectedHalt, +} + /// Used to notify other tasks to gracefully shutdown #[derive(Debug)] pub struct ShutdownNotifier { + // These channels have the dual purpose of signalling it's time to shutdown, but also to keep + // 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 + task_return_error_tx: ErrorSender, + task_return_error_rx: Option, + + // Also signal when the notifier is dropped, in case the task exits unexpectedly. + // Why are we not reusing the return error channel? Well, let me tell you kids, it's because I + // didn't manage to reliably get the explicitly sent error (and not the error sent during drop) + task_drop_tx: ErrorSender, + task_drop_rx: Option, } impl Default for ShutdownNotifier { fn default() -> Self { 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(); Self { notify_tx, notify_rx: Some(notify_rx), shutdown_timer_secs: DEFAULT_SHUTDOWN_TIMER_SECS, + task_return_error_tx: task_halt_tx, + task_return_error_rx: Some(task_halt_rx), + task_drop_tx, + task_drop_rx: Some(task_drop_rx), } } } @@ -40,6 +76,8 @@ impl ShutdownNotifier { .as_ref() .expect("Unable to subscribe to shutdown notifier that is already shutdown") .clone(), + self.task_return_error_tx.clone(), + self.task_drop_tx.clone(), ) } @@ -47,16 +85,45 @@ impl ShutdownNotifier { self.notify_tx.send(()) } + pub async fn wait_for_error(&mut self) -> Option { + let mut error_rx = self + .task_return_error_rx + .take() + .expect("Unable to wait for error: attempt to wait twice?"); + let mut drop_rx = self + .task_drop_rx + .take() + .expect("Unable to wait for error: attempt to wait twice?"); + + // During an error we are likely like to be swamped with drop notifications as well, this + // is a crude way to give priority to real errors (if there are any). + let drop_rx = drop_rx.recv().then(|msg| async move { + sleep(Duration::from_millis(50)).await; + msg + }); + + tokio::select! { + msg = error_rx.recv() => msg, + msg = drop_rx => msg + } + } + pub async fn wait_for_shutdown(&mut self) { + log::info!("Waiting for shutdown"); if let Some(notify_rx) = self.notify_rx.take() { 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"); }, - _ = tokio::signal::ctrl_c() => { + _ = tokio::signal::ctrl_c() => { log::info!("Forcing shutdown"); } _ = tokio::time::sleep(Duration::from_secs(self.shutdown_timer_secs)) => { @@ -69,15 +136,39 @@ impl ShutdownNotifier { /// Listen for shutdown notifications #[derive(Clone, Debug)] pub struct ShutdownListener { + // If a shutdown notification has been registered shutdown: bool, + + // Listen for shutdown notifications, as well as a mechanism to report back that we have + // finished (the receiver is closed). notify: watch::Receiver<()>, + + // Send back error if we stopped + return_error: ErrorSender, + + // 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, } impl ShutdownListener { - fn new(notify: watch::Receiver<()>) -> ShutdownListener { + #[cfg(not(target_arch = "wasm32"))] + const SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(5); + + fn new( + notify: watch::Receiver<()>, + return_error: ErrorSender, + drop_error: ErrorSender, + ) -> ShutdownListener { ShutdownListener { shutdown: false, notify, + return_error, + drop_error, + set_not_drop: false, } } @@ -93,6 +184,13 @@ impl ShutdownListener { self.shutdown = true; } + pub async fn recv_timeout(&mut self) { + #[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.shutdown { return true; @@ -111,6 +209,29 @@ impl ShutdownListener { } } } + + pub fn send_we_stopped(&mut self, err: SentError) { + 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() { + log::trace!("Notifying stop on unexpected drop"); + // If we can't send, well then there is not much to do + self.drop_error + .send(Box::new(TaskError::UnexpectedHalt)) + .ok(); + } + } } #[cfg(test)] diff --git a/common/task/src/signal.rs b/common/task/src/signal.rs index 40ca122848..a04a0f5601 100644 --- a/common/task/src/signal.rs +++ b/common/task/src/signal.rs @@ -1,3 +1,5 @@ +use crate::{shutdown::SentError, ShutdownNotifier}; + #[cfg(unix)] pub async fn wait_for_signal() { use tokio::signal::unix::{signal, SignalKind}; @@ -25,3 +27,44 @@ pub async fn wait_for_signal() { }, } } + +#[cfg(unix)] +pub async fn wait_for_signal_and_error(shutdown: &mut ShutdownNotifier) -> Result<(), SentError> { + use tokio::signal::unix::{signal, SignalKind}; + + let mut sigterm = signal(SignalKind::terminate()).expect("Failed to setup SIGTERM channel"); + let mut sigquit = signal(SignalKind::quit()).expect("Failed to setup SIGQUIT channel"); + + tokio::select! { + _ = tokio::signal::ctrl_c() => { + log::info!("Received SIGINT"); + Ok(()) + }, + _ = sigterm.recv() => { + log::info!("Received SIGTERM"); + Ok(()) + } + _ = sigquit.recv() => { + log::info!("Received SIGQUIT"); + Ok(()) + } + Some(msg) = shutdown.wait_for_error() => { + log::info!("Task error: {:?}", msg); + Err(msg) + } + } +} + +#[cfg(not(unix))] +pub async fn wait_for_signal_and_error(shutdown: &mut ShutdownNotifier) -> Result<(), SentError> { + tokio::select! { + _ = tokio::signal::ctrl_c() => { + log::info!("Received SIGINT"); + Ok(()) + }, + Some(msg) = shutdown.wait_for_error() => { + log::info!("Task error: {:?}", msg); + Err(msg) + } + } +} diff --git a/common/types/src/pending_events.rs b/common/types/src/pending_events.rs index afe5a54d9e..e9d578dc12 100644 --- a/common/types/src/pending_events.rs +++ b/common/types/src/pending_events.rs @@ -57,6 +57,10 @@ pub enum PendingEpochEventData { mix_id: MixId, proxy: Option, }, + PledgeMore { + mix_id: MixId, + amount: DecCoin, + }, UnbondMixnode { mix_id: MixId, }, @@ -91,6 +95,12 @@ impl PendingEpochEventData { mix_id, proxy: proxy.map(|p| p.into_string()), }), + MixnetContractPendingEpochEventKind::PledgeMore { mix_id, amount } => { + Ok(PendingEpochEventData::PledgeMore { + mix_id, + amount: reg.attempt_convert_to_display_dec_coin(amount.into())?, + }) + } MixnetContractPendingEpochEventKind::UnbondMixnode { mix_id } => { Ok(PendingEpochEventData::UnbondMixnode { mix_id }) } diff --git a/contracts/CHANGELOG.md b/contracts/CHANGELOG.md index f57a633956..c192eab3ae 100644 --- a/contracts/CHANGELOG.md +++ b/contracts/CHANGELOG.md @@ -3,7 +3,9 @@ ## 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]) +[#1679]: https://github.com/nymtech/nym/pull/1679 [#1759]: https://github.com/nymtech/nym/pull/1759 [#1769]: https://github.com/nymtech/nym/pull/1769 diff --git a/contracts/mixnet/src/contract.rs b/contracts/mixnet/src/contract.rs index e07b7ba32a..63bf3f1a72 100644 --- a/contracts/mixnet/src/contract.rs +++ b/contracts/mixnet/src/contract.rs @@ -168,6 +168,12 @@ pub fn execute( owner, owner_signature, ), + ExecuteMsg::PledgeMore {} => { + crate::mixnodes::transactions::try_increase_pledge(deps, env, info) + } + ExecuteMsg::PledgeMoreOnBehalf { owner } => { + crate::mixnodes::transactions::try_increase_pledge_on_behalf(deps, env, info, owner) + } ExecuteMsg::UnbondMixnode {} => { crate::mixnodes::transactions::try_remove_mixnode(deps, env, info) } diff --git a/contracts/mixnet/src/interval/pending_events.rs b/contracts/mixnet/src/interval/pending_events.rs index 8cb09566f2..b242944988 100644 --- a/contracts/mixnet/src/interval/pending_events.rs +++ b/contracts/mixnet/src/interval/pending_events.rs @@ -7,13 +7,14 @@ use crate::interval::helpers::change_interval_config; use crate::interval::storage; use crate::mixnet_contract_settings::storage as mixnet_params_storage; use crate::mixnodes::helpers::{cleanup_post_unbond_mixnode_storage, get_mixnode_details_by_id}; +use crate::mixnodes::storage as mixnodes_storage; use crate::rewards::storage as rewards_storage; use crate::support::helpers::send_to_proxy_or_owner; use cosmwasm_std::{wasm_execute, Addr, Coin, DepsMut, Env, Response}; use mixnet_contract_common::error::MixnetContractError; use mixnet_contract_common::events::{ new_active_set_update_event, new_delegation_event, new_delegation_on_unbonded_node_event, - new_mixnode_cost_params_update_event, new_mixnode_unbonding_event, + new_mixnode_cost_params_update_event, new_mixnode_unbonding_event, new_pledge_increase_event, new_rewarding_params_update_event, new_undelegation_event, }; use mixnet_contract_common::mixnode::MixNodeCostParams; @@ -258,6 +259,42 @@ pub(crate) fn update_active_set_size( Ok(Response::new().add_event(new_active_set_update_event(created_at, active_set_size))) } +pub(crate) fn increase_pledge( + deps: DepsMut<'_>, + created_at: BlockHeight, + mix_id: MixId, + increase: Coin, +) -> Result { + // note: we have already validated the amount to know it has the correct denomination + + // the target node MUST exist - we have checked it at the time of putting this event onto the queue + // we have also verified there were no preceding unbond events + let mix_details = get_mixnode_details_by_id(deps.storage, mix_id)?.ok_or( + MixnetContractError::InconsistentState { + comment: + "mixnode getting processed to increase its pledge doesn't exist in the storage" + .into(), + }, + )?; + + let mut updated_bond = mix_details.bond_information.clone(); + let mut updated_rewarding = mix_details.rewarding_details; + + updated_bond.original_pledge.amount += increase.amount; + updated_rewarding.increase_operator_uint128(increase.amount)?; + + // update both, bond information and rewarding details + mixnodes_storage::mixnode_bonds().replace( + deps.storage, + mix_id, + Some(&updated_bond), + Some(&mix_details.bond_information), + )?; + rewards_storage::MIXNODE_REWARDING.save(deps.storage, mix_id, &updated_rewarding)?; + + Ok(Response::new().add_event(new_pledge_increase_event(created_at, mix_id, &increase))) +} + impl ContractExecutableEvent for PendingEpochEventData { fn execute(self, deps: DepsMut<'_>, env: &Env) -> Result { // note that the basic validation on all those events was already performed before @@ -274,6 +311,9 @@ impl ContractExecutableEvent for PendingEpochEventData { mix_id, proxy, } => undelegate(deps, self.created_at, owner, mix_id, proxy), + PendingEpochEventKind::PledgeMore { mix_id, amount } => { + increase_pledge(deps, self.created_at, mix_id, amount) + } PendingEpochEventKind::UnbondMixnode { mix_id } => { unbond_mixnode(deps, env, self.created_at, mix_id) } @@ -1135,6 +1175,228 @@ mod tests { } } + #[cfg(test)] + mod increasing_pledge { + use super::*; + use cosmwasm_std::Uint128; + use mixnet_contract_common::rewarding::helpers::truncate_reward_amount; + + #[test] + fn returns_hard_error_if_mixnode_doesnt_exist() { + // this should have never happened so hard error MUST be thrown here + let mut test = TestSetup::new(); + + let amount = test.coin(123); + let res = increase_pledge(test.deps_mut(), 123, 1, amount); + assert!(matches!( + res, + Err(MixnetContractError::InconsistentState { .. }) + )); + } + + #[test] + fn updates_stored_bond_information_and_rewarding_details() { + let mut test = TestSetup::new(); + let mix_id = test.add_dummy_mixnode("mix-owner", None); + + let old_details = get_mixnode_details_by_id(test.deps().storage, mix_id) + .unwrap() + .unwrap(); + + let amount = test.coin(12345); + increase_pledge(test.deps_mut(), 123, mix_id, amount.clone()).unwrap(); + + let updated_details = get_mixnode_details_by_id(test.deps().storage, mix_id) + .unwrap() + .unwrap(); + + assert_eq!( + updated_details.bond_information.original_pledge.amount, + old_details.bond_information.original_pledge.amount + amount.amount + ); + + assert_eq!( + updated_details.rewarding_details.operator, + old_details.rewarding_details.operator + + Decimal::from_atomics(amount.amount, 0).unwrap() + ); + } + + #[test] + fn without_any_events_in_between_is_equivalent_to_pledging_the_same_amount_immediately() { + let mut test = TestSetup::new(); + let pledge1 = Uint128::new(150_000_000); + let pledge2 = Uint128::new(50_000_000); + let pledge3 = Uint128::new(200_000_000); + + let mix_id_repledge = test.add_dummy_mixnode("mix-owner1", Some(pledge1)); + let increase = test.coin(pledge2.u128()); + increase_pledge(test.deps_mut(), 123, mix_id_repledge, increase).unwrap(); + + let mix_id_full_pledge = test.add_dummy_mixnode("mix-owner2", Some(pledge3)); + + test.add_immediate_delegation("alice", 123_456_789u128, mix_id_repledge); + test.add_immediate_delegation("bob", 500_000_000u128, mix_id_repledge); + test.add_immediate_delegation("carol", 111_111_111u128, mix_id_repledge); + + test.add_immediate_delegation("alice", 123_456_789u128, mix_id_full_pledge); + test.add_immediate_delegation("bob", 500_000_000u128, mix_id_full_pledge); + test.add_immediate_delegation("carol", 111_111_111u128, mix_id_full_pledge); + + test.skip_to_next_epoch_end(); + test.update_rewarded_set(vec![mix_id_repledge, mix_id_full_pledge]); + + let dist1 = + test.reward_with_distribution(mix_id_repledge, test_helpers::performance(100.0)); + let dist2 = + test.reward_with_distribution(mix_id_full_pledge, test_helpers::performance(100.0)); + + assert_eq!(dist1, dist2) + } + + #[test] + fn correctly_increases_future_rewards() { + let mut test = TestSetup::new(); + let pledge1 = Uint128::new(150_000_000_000); + let pledge2 = Uint128::new(50_000_000_000); + + let mix_id_repledge = test.add_dummy_mixnode("mix-owner1", Some(pledge1)); + + test.add_immediate_delegation("alice", 123_456_789_000u128, mix_id_repledge); + test.add_immediate_delegation("bob", 500_000_000_000u128, mix_id_repledge); + test.add_immediate_delegation("carol", 111_111_111_000u128, mix_id_repledge); + + test.skip_to_next_epoch_end(); + test.update_rewarded_set(vec![mix_id_repledge]); + + let dist = + test.reward_with_distribution(mix_id_repledge, test_helpers::performance(100.0)); + + let increase = test.coin(pledge2.u128()); + increase_pledge(test.deps_mut(), 123, mix_id_repledge, increase).unwrap(); + + let pledge3 = Uint128::new(200_000_000_000) + truncate_reward_amount(dist.operator); + let mix_id_full_pledge = test.add_dummy_mixnode("mix-owner2", Some(pledge3)); + + test.add_immediate_delegation("alice", 123_456_789_000u128, mix_id_full_pledge); + test.add_immediate_delegation("bob", 500_000_000_000u128, mix_id_full_pledge); + test.add_immediate_delegation("carol", 111_111_111_000u128, mix_id_full_pledge); + + let lost_operator = dist.operator + - Decimal::from_atomics(truncate_reward_amount(dist.operator), 0).unwrap(); + let lost_delegates = dist.delegates + - Decimal::from_atomics(truncate_reward_amount(dist.delegates), 0).unwrap(); + + // add the tiny bit of lost precision manually + let mut mix_rewarding_full = test.mix_rewarding(mix_id_full_pledge); + mix_rewarding_full.delegates += lost_delegates; + mix_rewarding_full.operator += lost_operator; + rewards_storage::MIXNODE_REWARDING + .save( + test.deps_mut().storage, + mix_id_full_pledge, + &mix_rewarding_full, + ) + .unwrap(); + + test.add_immediate_delegation( + "dave", + truncate_reward_amount(dist.delegates).u128(), + mix_id_full_pledge, + ); + + test.skip_to_next_epoch_end(); + test.update_rewarded_set(vec![mix_id_repledge, mix_id_full_pledge]); + + // go through few epochs of rewarding + for _ in 0..500 { + test.skip_to_next_epoch_end(); + let dist1 = test + .reward_with_distribution(mix_id_repledge, test_helpers::performance(100.0)); + let dist2 = test + .reward_with_distribution(mix_id_full_pledge, test_helpers::performance(100.0)); + + assert_eq!(dist1, dist2) + } + } + + #[test] + fn correctly_increases_future_rewards_with_more_passed_epochs() { + let mut test = TestSetup::new(); + let pledge1 = Uint128::new(150_000_000_000); + let pledge2 = Uint128::new(50_000_000_000); + + let mix_id_repledge = test.add_dummy_mixnode("mix-owner1", Some(pledge1)); + + test.add_immediate_delegation("alice", 123_456_789_000u128, mix_id_repledge); + test.add_immediate_delegation("bob", 500_000_000_000u128, mix_id_repledge); + test.add_immediate_delegation("carol", 111_111_111_000u128, mix_id_repledge); + + test.skip_to_next_epoch_end(); + test.update_rewarded_set(vec![mix_id_repledge]); + + let mut cumulative_op_reward = Decimal::zero(); + let mut cumulative_del_reward = Decimal::zero(); + + // go few epochs of rewarding before adding more pledge + for _ in 0..500 { + test.skip_to_next_epoch_end(); + let dist = test + .reward_with_distribution(mix_id_repledge, test_helpers::performance(100.0)); + cumulative_op_reward += dist.operator; + cumulative_del_reward += dist.delegates; + } + + let increase = test.coin(pledge2.u128()); + increase_pledge(test.deps_mut(), 123, mix_id_repledge, increase).unwrap(); + + let pledge3 = + Uint128::new(200_000_000_000) + truncate_reward_amount(cumulative_op_reward); + let mix_id_full_pledge = test.add_dummy_mixnode("mix-owner2", Some(pledge3)); + + test.add_immediate_delegation("alice", 123_456_789_000u128, mix_id_full_pledge); + test.add_immediate_delegation("bob", 500_000_000_000u128, mix_id_full_pledge); + test.add_immediate_delegation("carol", 111_111_111_000u128, mix_id_full_pledge); + + let lost_operator = cumulative_op_reward + - Decimal::from_atomics(truncate_reward_amount(cumulative_op_reward), 0).unwrap(); + let lost_delegates = cumulative_del_reward + - Decimal::from_atomics(truncate_reward_amount(cumulative_del_reward), 0).unwrap(); + + // add the tiny bit of lost precision manually + let mut mix_rewarding_full = test.mix_rewarding(mix_id_full_pledge); + mix_rewarding_full.delegates += lost_delegates; + mix_rewarding_full.operator += lost_operator; + rewards_storage::MIXNODE_REWARDING + .save( + test.deps_mut().storage, + mix_id_full_pledge, + &mix_rewarding_full, + ) + .unwrap(); + + test.add_immediate_delegation( + "dave", + truncate_reward_amount(cumulative_del_reward).u128(), + mix_id_full_pledge, + ); + + test.skip_to_next_epoch_end(); + test.update_rewarded_set(vec![mix_id_repledge, mix_id_full_pledge]); + + // go through few more epochs of rewarding + for _ in 0..500 { + test.skip_to_next_epoch_end(); + let dist1 = test + .reward_with_distribution(mix_id_repledge, test_helpers::performance(100.0)); + let dist2 = test + .reward_with_distribution(mix_id_full_pledge, test_helpers::performance(100.0)); + + assert_eq!(dist1, dist2) + } + } + } + #[test] fn updating_active_set_updates_rewarding_params() { let mut test = TestSetup::new(); diff --git a/contracts/mixnet/src/mixnet_contract_settings/storage.rs b/contracts/mixnet/src/mixnet_contract_settings/storage.rs index 990c5e604d..45037a2ed5 100644 --- a/contracts/mixnet/src/mixnet_contract_settings/storage.rs +++ b/contracts/mixnet/src/mixnet_contract_settings/storage.rs @@ -37,7 +37,6 @@ pub(crate) fn minimum_delegation_stake( .map(|state| state.params.minimum_mixnode_delegation)?) } -#[allow(unused)] pub(crate) fn rewarding_denom(storage: &dyn Storage) -> Result { Ok(CONTRACT_STATE .load(storage) diff --git a/contracts/mixnet/src/mixnodes/helpers.rs b/contracts/mixnet/src/mixnodes/helpers.rs index 8244f67b13..383dbc6224 100644 --- a/contracts/mixnet/src/mixnodes/helpers.rs +++ b/contracts/mixnet/src/mixnodes/helpers.rs @@ -147,7 +147,7 @@ pub(crate) fn cleanup_post_unbond_mixnode_storage( } #[cfg(test)] -mod tests { +pub(crate) mod tests { use super::*; use crate::support::tests::fixtures::{ mix_node_cost_params_fixture, mix_node_fixture, TEST_COIN_DENOM, @@ -155,13 +155,13 @@ mod tests { use crate::support::tests::test_helpers::TestSetup; use cosmwasm_std::coin; - const OWNER_EXISTS: &str = "mix-owner-existing"; - const OWNER_UNBONDING: &str = "mix-owner-unbonding"; - const OWNER_UNBONDED: &str = "mix-owner-unbonded"; - const OWNER_UNBONDED_LEFTOVER: &str = "mix-owner-unbonded-leftover"; + pub(crate) const OWNER_EXISTS: &str = "mix-owner-existing"; + pub(crate) const OWNER_UNBONDING: &str = "mix-owner-unbonding"; + pub(crate) const OWNER_UNBONDED: &str = "mix-owner-unbonded"; + pub(crate) const OWNER_UNBONDED_LEFTOVER: &str = "mix-owner-unbonded-leftover"; // create a mixnode that is bonded, unbonded, in the process of unbonding and unbonded with leftover mix rewarding details - fn setup_mix_combinations(test: &mut TestSetup) -> Vec { + pub(crate) fn setup_mix_combinations(test: &mut TestSetup) -> Vec { let mix_id_exists = test.add_dummy_mixnode(OWNER_EXISTS, None); let mix_id_unbonding = test.add_dummy_mixnode(OWNER_UNBONDING, None); let mix_id_unbonded = test.add_dummy_mixnode(OWNER_UNBONDED, None); diff --git a/contracts/mixnet/src/mixnodes/transactions.rs b/contracts/mixnet/src/mixnodes/transactions.rs index 0a1e816ffd..92fa131a9e 100644 --- a/contracts/mixnet/src/mixnodes/transactions.rs +++ b/contracts/mixnet/src/mixnodes/transactions.rs @@ -5,16 +5,20 @@ use super::storage; use crate::interval::storage as interval_storage; use crate::interval::storage::push_new_interval_event; use crate::mixnet_contract_settings::storage as mixnet_params_storage; -use crate::mixnodes::helpers::{must_get_mixnode_bond_by_owner, save_new_mixnode}; +use crate::mixnet_contract_settings::storage::rewarding_denom; +use crate::mixnodes::helpers::{ + get_mixnode_details_by_owner, must_get_mixnode_bond_by_owner, save_new_mixnode, +}; use crate::support::helpers::{ ensure_bonded, ensure_no_existing_bond, ensure_proxy_match, validate_node_identity_signature, validate_pledge, }; -use cosmwasm_std::{Addr, Coin, DepsMut, Env, MessageInfo, Response}; +use cosmwasm_std::{coin, Addr, Coin, DepsMut, Env, MessageInfo, Response}; use mixnet_contract_common::error::MixnetContractError; use mixnet_contract_common::events::{ new_mixnode_bonding_event, new_mixnode_config_update_event, new_mixnode_pending_cost_params_update_event, new_pending_mixnode_unbonding_event, + new_pending_pledge_increase_event, }; use mixnet_contract_common::mixnode::{MixNodeConfigUpdate, MixNodeCostParams}; use mixnet_contract_common::pending_events::{PendingEpochEventKind, PendingIntervalEventKind}; @@ -117,6 +121,54 @@ fn _try_add_mixnode( ))) } +pub fn try_increase_pledge( + deps: DepsMut<'_>, + env: Env, + info: MessageInfo, +) -> Result { + _try_increase_pledge(deps, env, info.funds, info.sender, None) +} + +pub fn try_increase_pledge_on_behalf( + deps: DepsMut<'_>, + env: Env, + info: MessageInfo, + owner: String, +) -> Result { + let proxy = info.sender; + let owner = deps.api.addr_validate(&owner)?; + _try_increase_pledge(deps, env, info.funds, owner, Some(proxy)) +} + +pub fn _try_increase_pledge( + deps: DepsMut<'_>, + env: Env, + increase: Vec, + owner: Addr, + proxy: Option, +) -> Result { + let mix_details = get_mixnode_details_by_owner(deps.storage, owner.clone())? + .ok_or(MixnetContractError::NoAssociatedMixNodeBond { owner })?; + let mix_id = mix_details.mix_id(); + + ensure_proxy_match(&proxy, &mix_details.bond_information.proxy)?; + ensure_bonded(&mix_details.bond_information)?; + + let rewarding_denom = rewarding_denom(deps.storage)?; + let pledge_increase = validate_pledge(increase, coin(1, rewarding_denom))?; + + let cosmos_event = new_pending_pledge_increase_event(mix_id, &pledge_increase); + + // push the event to execute it at the end of the epoch + let epoch_event = PendingEpochEventKind::PledgeMore { + mix_id, + amount: pledge_increase, + }; + interval_storage::push_new_epoch_event(deps.storage, &env, epoch_event)?; + + Ok(Response::new().add_event(cosmos_event)) +} + pub fn try_remove_mixnode_on_behalf( deps: DepsMut<'_>, env: Env, @@ -680,4 +732,187 @@ pub mod tests { ) .is_err()); } + + #[cfg(test)] + mod increasing_mixnode_pledge { + use super::*; + use crate::mixnodes::helpers::tests::{ + setup_mix_combinations, OWNER_UNBONDED, OWNER_UNBONDED_LEFTOVER, OWNER_UNBONDING, + }; + use crate::support::tests::test_helpers::TestSetup; + + #[test] + fn is_not_allowed_if_account_doesnt_own_mixnode() { + let mut test = TestSetup::new(); + let env = test.env(); + let sender = mock_info("not-mix-owner", &[]); + + let res = try_increase_pledge(test.deps_mut(), env, sender); + assert_eq!( + res, + Err(MixnetContractError::NoAssociatedMixNodeBond { + owner: Addr::unchecked("not-mix-owner") + }) + ) + } + + #[test] + fn is_not_allowed_if_theres_proxy_mismatch() { + let mut test = TestSetup::new(); + let env = test.env(); + + let owner_without_proxy = Addr::unchecked("no-proxy"); + let owner_with_proxy = Addr::unchecked("with-proxy"); + let proxy = Addr::unchecked("proxy"); + let wrong_proxy = Addr::unchecked("unrelated-proxy"); + + test.add_dummy_mixnode(owner_without_proxy.as_str(), None); + test.add_dummy_mixnode_with_proxy(owner_with_proxy.as_str(), None, proxy.clone()); + + let res = _try_increase_pledge( + test.deps_mut(), + env.clone(), + Vec::new(), + owner_without_proxy.clone(), + Some(proxy), + ); + assert_eq!( + res, + Err(MixnetContractError::ProxyMismatch { + existing: "None".to_string(), + incoming: "proxy".to_string() + }) + ); + + let res = _try_increase_pledge( + test.deps_mut(), + env.clone(), + Vec::new(), + owner_with_proxy.clone(), + None, + ); + assert_eq!( + res, + Err(MixnetContractError::ProxyMismatch { + existing: "proxy".to_string(), + incoming: "None".to_string() + }) + ); + + let res = _try_increase_pledge( + test.deps_mut(), + env, + Vec::new(), + owner_with_proxy.clone(), + Some(wrong_proxy), + ); + assert_eq!( + res, + Err(MixnetContractError::ProxyMismatch { + existing: "proxy".to_string(), + incoming: "unrelated-proxy".to_string() + }) + ) + } + + #[test] + fn is_not_allowed_if_mixnode_has_unbonded_or_is_unbonding() { + let mut test = TestSetup::new(); + let env = test.env(); + + // TODO: I dislike this cross-test access, but it provides us with exactly what we need + // perhaps it should be refactored a bit? + let owner_unbonding = Addr::unchecked(OWNER_UNBONDING); + let owner_unbonded = Addr::unchecked(OWNER_UNBONDED); + let owner_unbonded_leftover = Addr::unchecked(OWNER_UNBONDED_LEFTOVER); + + let ids = setup_mix_combinations(&mut test); + let mix_id_unbonding = ids[1]; + + let res = try_increase_pledge( + test.deps_mut(), + env.clone(), + mock_info(owner_unbonding.as_str(), &[]), + ); + assert_eq!( + res, + Err(MixnetContractError::MixnodeIsUnbonding { + mix_id: mix_id_unbonding + }) + ); + + // if the nodes are gone we treat them as tey never existed in the first place + // (regardless of if there's some leftover data) + let res = try_increase_pledge( + test.deps_mut(), + env.clone(), + mock_info(owner_unbonded_leftover.as_str(), &[]), + ); + assert_eq!( + res, + Err(MixnetContractError::NoAssociatedMixNodeBond { + owner: owner_unbonded_leftover + }) + ); + + let res = try_increase_pledge( + test.deps_mut(), + env, + mock_info(owner_unbonded.as_str(), &[]), + ); + assert_eq!( + res, + Err(MixnetContractError::NoAssociatedMixNodeBond { + owner: owner_unbonded + }) + ) + } + + #[test] + fn is_not_allowed_if_no_tokens_were_sent() { + let mut test = TestSetup::new(); + let env = test.env(); + let owner = "mix-owner"; + + test.add_dummy_mixnode(owner, None); + + let sender_empty = mock_info(owner, &[]); + let res = try_increase_pledge(test.deps_mut(), env.clone(), sender_empty); + assert_eq!(res, Err(MixnetContractError::NoBondFound)); + + let sender_zero = mock_info(owner, &[test.coin(0)]); + let res = try_increase_pledge(test.deps_mut(), env, sender_zero); + assert_eq!( + res, + Err(MixnetContractError::InsufficientPledge { + received: test.coin(0), + minimum: test.coin(1) + }) + ) + } + + #[test] + fn with_valid_information_creates_pending_event() { + let mut test = TestSetup::new(); + let env = test.env(); + let owner = "mix-owner"; + let mix_id = test.add_dummy_mixnode(owner, None); + + let events = test.pending_epoch_events(); + assert!(events.is_empty()); + + let sender = mock_info(owner, &[test.coin(1000)]); + try_increase_pledge(test.deps_mut(), env, sender).unwrap(); + + let events = test.pending_epoch_events(); + + assert_eq!( + events[0].kind, + PendingEpochEventKind::PledgeMore { + mix_id, + amount: test.coin(1000) + } + ); + } + } } diff --git a/contracts/mixnet/src/support/tests/mod.rs b/contracts/mixnet/src/support/tests/mod.rs index a7cf4a3875..3d17b324df 100644 --- a/contracts/mixnet/src/support/tests/mod.rs +++ b/contracts/mixnet/src/support/tests/mod.rs @@ -28,6 +28,9 @@ pub mod test_helpers { use crate::mixnodes::transactions::{ try_add_mixnode, try_add_mixnode_on_behalf, try_remove_mixnode, }; + use crate::rewards::queries::{ + query_pending_delegator_reward, query_pending_mixnode_operator_reward, + }; use crate::rewards::storage as rewards_storage; use crate::rewards::transactions::try_reward_mixnode; use crate::support::tests; @@ -37,7 +40,7 @@ pub mod test_helpers { use cosmwasm_std::testing::mock_info; use cosmwasm_std::testing::MockApi; use cosmwasm_std::testing::MockQuerier; - use cosmwasm_std::{Addr, BankMsg, CosmosMsg, Storage}; + use cosmwasm_std::{coin, Addr, BankMsg, CosmosMsg, Storage}; use cosmwasm_std::{Coin, Order}; use cosmwasm_std::{Decimal, Empty, MemoryStorage}; use cosmwasm_std::{Deps, OwnedDeps}; @@ -138,6 +141,10 @@ pub mod test_helpers { .vesting_contract_address } + pub fn coin(&self, amount: u128) -> Coin { + coin(amount, rewarding_denom(self.deps().storage).unwrap()) + } + pub fn current_interval(&self) -> Interval { interval_storage::current_interval(self.deps().storage).unwrap() } @@ -308,6 +315,22 @@ pub mod test_helpers { .unwrap(); } + #[allow(unused)] + pub fn pending_operator_reward(&mut self, mix: MixId) -> Decimal { + query_pending_mixnode_operator_reward(self.deps(), mix) + .unwrap() + .amount_earned_detailed + .expect("no reward!") + } + + #[allow(unused)] + pub fn pending_delegator_reward(&mut self, delegator: &str, target: MixId) -> Decimal { + query_pending_delegator_reward(self.deps(), delegator.into(), target, None) + .unwrap() + .amount_earned_detailed + .expect("no reward!") + } + pub fn skip_to_next_epoch_end(&mut self) { self.skip_to_next_epoch(); self.skip_to_current_epoch_end(); @@ -337,8 +360,19 @@ pub mod test_helpers { self.env.block.time = Timestamp::from_seconds(epoch_end as u64 + 1); let advanced = interval.advance_epoch(); - if interval.current_epoch_id() != interval.epochs_in_interval() { + assert_eq!( + interval.current_epoch_absolute_id() + 1, + advanced.current_epoch_absolute_id() + ); + + if interval.current_epoch_id() != interval.epochs_in_interval() - 1 { assert_eq!(interval.current_epoch_id() + 1, advanced.current_epoch_id()) + } else { + assert_eq!(advanced.current_epoch_id(), 0); + assert_eq!( + interval.current_interval_id() + 1, + advanced.current_interval_id() + ) } interval_storage::save_interval(self.deps_mut().storage, &advanced).unwrap() diff --git a/contracts/vesting/src/contract.rs b/contracts/vesting/src/contract.rs index 5efb3d750a..90a72d2ae8 100644 --- a/contracts/vesting/src/contract.rs +++ b/contracts/vesting/src/contract.rs @@ -121,6 +121,7 @@ pub fn execute( env, deps, ), + ExecuteMsg::PledgeMore { amount } => try_pledge_more(deps, env, info, amount), ExecuteMsg::UnbondMixnode {} => try_unbond_mixnode(info, deps), ExecuteMsg::TrackUnbondMixnode { owner, amount } => { try_track_unbond_mixnode(&owner, amount, info, deps) @@ -331,6 +332,19 @@ pub fn try_bond_mixnode( ) } +pub fn try_pledge_more( + deps: DepsMut<'_>, + env: Env, + info: MessageInfo, + amount: Coin, +) -> Result { + let mix_denom = MIX_DENOM.load(deps.storage)?; + let additional_pledge = validate_funds(&[amount], mix_denom)?; + + let account = account_from_address(info.sender.as_str(), deps.storage, deps.api)?; + account.try_pledge_additional_tokens(additional_pledge, &env, deps.storage) +} + /// Unbond a mixnode, sends [mixnet_contract_common::ExecuteMsg::UnbondMixnodeOnBehalf] to [crate::storage::MIXNET_CONTRACT_ADDRESS]. pub fn try_unbond_mixnode(info: MessageInfo, deps: DepsMut<'_>) -> Result { let account = account_from_address(info.sender.as_str(), deps.storage, deps.api)?; diff --git a/contracts/vesting/src/traits/bonding_account.rs b/contracts/vesting/src/traits/bonding_account.rs index 36c248e393..491378d630 100644 --- a/contracts/vesting/src/traits/bonding_account.rs +++ b/contracts/vesting/src/traits/bonding_account.rs @@ -18,6 +18,13 @@ pub trait MixnodeBondingAccount { storage: &mut dyn Storage, ) -> Result; + fn try_pledge_additional_tokens( + &self, + additional_pledge: Coin, + env: &Env, + storage: &mut dyn Storage, + ) -> Result; + fn try_unbond_mixnode(&self, storage: &dyn Storage) -> Result; fn try_track_unbond_mixnode( diff --git a/contracts/vesting/src/vesting/account/mixnode_bonding_account.rs b/contracts/vesting/src/vesting/account/mixnode_bonding_account.rs index e3aec82584..74917a16c7 100644 --- a/contracts/vesting/src/vesting/account/mixnode_bonding_account.rs +++ b/contracts/vesting/src/vesting/account/mixnode_bonding_account.rs @@ -12,7 +12,8 @@ use mixnet_contract_common::mixnode::MixNodeCostParams; use mixnet_contract_common::{ExecuteMsg as MixnetExecuteMsg, MixNode}; use vesting_contract_common::events::{ new_vesting_mixnode_bonding_event, new_vesting_mixnode_unbonding_event, - new_vesting_update_mixnode_config_event, new_vesting_update_mixnode_cost_params_event, + new_vesting_pledge_more_event, new_vesting_update_mixnode_config_event, + new_vesting_update_mixnode_cost_params_event, }; use vesting_contract_common::PledgeData; @@ -84,6 +85,64 @@ impl MixnodeBondingAccount for Account { .add_event(new_vesting_mixnode_bonding_event())) } + fn try_pledge_additional_tokens( + &self, + additional_pledge: Coin, + env: &Env, + storage: &mut dyn Storage, + ) -> Result { + let current_balance = self.load_balance(storage)?; + let total_pledged_after = + self.total_pledged_locked(storage, env)? + additional_pledge.amount; + let locked_pledge_cap = self.absolute_pledge_cap()?; + + if locked_pledge_cap < total_pledged_after { + return Err(ContractError::LockedPledgeCapReached { + current: total_pledged_after, + cap: locked_pledge_cap, + }); + } + + if current_balance < additional_pledge.amount { + return Err(ContractError::InsufficientBalance( + self.owner_address().as_str().to_string(), + current_balance.u128(), + )); + } + + let mut pledge_data = if let Some(pledge_data) = self.load_mixnode_pledge(storage)? { + pledge_data + } else { + return Err(ContractError::NoBondFound( + self.owner_address().as_str().to_string(), + )); + }; + + // need a second pair of eyes here to make sure updating existing timestamp on pledge data + // is not going to have some unexpected consequences + pledge_data.amount.amount += additional_pledge.amount; + pledge_data.block_time = env.block.time; + + let msg = MixnetExecuteMsg::PledgeMoreOnBehalf { + owner: self.owner_address().into_string(), + }; + + let new_balance = Uint128::new(current_balance.u128() - additional_pledge.amount.u128()); + + let pledge_more_mag = wasm_execute( + MIXNET_CONTRACT_ADDRESS.load(storage)?, + &msg, + vec![additional_pledge], + )?; + + self.save_balance(new_balance, storage)?; + self.save_mixnode_pledge(pledge_data, storage)?; + + Ok(Response::new() + .add_message(pledge_more_mag) + .add_event(new_vesting_pledge_more_event())) + } + fn try_unbond_mixnode(&self, storage: &dyn Storage) -> Result { let msg = MixnetExecuteMsg::UnbondMixnodeOnBehalf { owner: self.owner_address().into_string(), diff --git a/explorer-api/Cargo.toml b/explorer-api/Cargo.toml index ab442f466f..cc665b1f83 100644 --- a/explorer-api/Cargo.toml +++ b/explorer-api/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "explorer-api" -version = "1.1.0" +version = "1.1.1" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/explorer/src/components/MobileNav.tsx b/explorer/src/components/MobileNav.tsx index 26e078fe08..cb44210e9d 100644 --- a/explorer/src/components/MobileNav.tsx +++ b/explorer/src/components/MobileNav.tsx @@ -34,7 +34,7 @@ export const MobileNav: React.FC<{ children: React.ReactNode }> = ({ children }: const { navState, updateNavState } = useMainContext(); const [drawerOpen, setDrawerOpen] = React.useState(false); // Set maintenance banner to false by default to don't display it - const [openMaintenance, setOpenMaintenance] = React.useState(true); + const [openMaintenance, setOpenMaintenance] = React.useState(false); const toggleDrawer = () => { setDrawerOpen(!drawerOpen); diff --git a/explorer/src/components/Nav.tsx b/explorer/src/components/Nav.tsx index 53bb922537..e2808871a0 100644 --- a/explorer/src/components/Nav.tsx +++ b/explorer/src/components/Nav.tsx @@ -235,7 +235,7 @@ export const Nav: React.FC = ({ children }) => { const [drawerIsOpen, setDrawerToOpen] = React.useState(false); const [fixedOpen, setFixedOpen] = React.useState(false); // Set maintenance banner to false by default to don't display it - const [openMaintenance, setOpenMaintenance] = React.useState(true); + const [openMaintenance, setOpenMaintenance] = React.useState(false); const theme = useTheme(); const setToActive = (id: number) => { diff --git a/gateway/Cargo.toml b/gateway/Cargo.toml index 6d1dbf3f07..8be0948a85 100644 --- a/gateway/Cargo.toml +++ b/gateway/Cargo.toml @@ -3,7 +3,7 @@ [package] name = "nym-gateway" -version = "1.1.0" +version = "1.1.1" authors = [ "Dave Hrycyszyn ", "Jędrzej Stuczyński ", diff --git a/mixnode/Cargo.toml b/mixnode/Cargo.toml index ea2dc207a0..0806354403 100644 --- a/mixnode/Cargo.toml +++ b/mixnode/Cargo.toml @@ -3,7 +3,7 @@ [package] name = "nym-mixnode" -version = "1.1.0" +version = "1.1.1" authors = [ "Dave Hrycyszyn ", "Jędrzej Stuczyński ", diff --git a/nym-connect/.storybook/preview.js b/nym-connect/.storybook/preview.js index 2dbe041198..9c14c3ec99 100644 --- a/nym-connect/.storybook/preview.js +++ b/nym-connect/.storybook/preview.js @@ -1,26 +1,26 @@ import { NymMixnetTheme } from '../src/theme'; -import { ClientContextProvider } from '../src/context/main'; import { Fonts } from './preview-fonts'; - -const withThemeProvider= (Story, context) =>{ +import { MockProvider } from '../src/context/mocks/main'; +const withThemeProvider = (Story, context) => { return ( - - + + - + - ) -} + ); +}; + export const decorators = [withThemeProvider]; export const parameters = { - actions: { argTypesRegex: "^on[A-Z].*" }, + actions: { argTypesRegex: '^on[A-Z].*' }, controls: { matchers: { color: /(background|color)$/i, date: /Date$/, }, }, -} \ No newline at end of file +}; diff --git a/nym-connect/CHANGELOG.md b/nym-connect/CHANGELOG.md index a273afb9ed..1a8bc41c11 100644 --- a/nym-connect/CHANGELOG.md +++ b/nym-connect/CHANGELOG.md @@ -1,3 +1,23 @@ +## [nym-connect-v1.1.1](https://github.com/nymtech/nym/tree/nym-connect-v1.1.1) (2022-11-29) + +- socks5-client: fix multiplex concurrent connections ([#1720], [#1777]) +- socks5-client: fix wait closing inbound connection until data is sent, and throttle incoming data in general ([#1772], [#1783],[#1789]) +- socks5-client: fix shutting down all background workers if anyone of them panics or errors out. This fixes an issue where the nym-connect UI was showing connected even though the socks5 tunnel was non-functional. ([#1805]) +- gateway-libs: fix decryping messages stored on the gateway between reconnects ([#1786]) + +- nymconnect: updated UI +- nymconnect: new help area +- nymconnect: listen for service errors and display on frontend + +[#1720]: https://github.com/nymtech/nym/pull/1720 +[#1772]: https://github.com/nymtech/nym/pull/1772 +[#1777]: https://github.com/nymtech/nym/pull/1777 +[#1783]: https://github.com/nymtech/nym/pull/1783 +[#1786]: https://github.com/nymtech/nym/pull/1786 +[#1789]: https://github.com/nymtech/nym/pull/1789 +[#1805]: https://github.com/nymtech/nym/pull/1805 + + ## [nym-connect-v1.1.0](https://github.com/nymtech/nym/tree/nym-connect-v1.1.0) (2022-11-09) - nym-connect: rework of rewarding changes the directory data structures that describe the mixnet topology ([#1472]) @@ -39,4 +59,4 @@ - nym-connect: reuse config id instead of creating a new id on each connection -[#1427]: https://github.com/nymtech/nym/pull/1427 \ No newline at end of file +[#1427]: https://github.com/nymtech/nym/pull/1427 diff --git a/nym-connect/Cargo.lock b/nym-connect/Cargo.lock index cab899eacf..8d2d5d1ceb 100644 --- a/nym-connect/Cargo.lock +++ b/nym-connect/Cargo.lock @@ -630,7 +630,7 @@ dependencies = [ [[package]] name = "client-core" -version = "1.1.0" +version = "1.1.1" dependencies = [ "client-connections", "config", @@ -652,6 +652,7 @@ dependencies = [ "task", "thiserror", "tokio", + "tokio-stream", "topology", "url", "validator-client", @@ -3351,7 +3352,7 @@ dependencies = [ [[package]] name = "nym-socks5-client" -version = "1.1.0" +version = "1.1.1" dependencies = [ "clap", "client-connections", @@ -5459,7 +5460,9 @@ dependencies = [ name = "task" version = "0.1.0" dependencies = [ + "futures", "log", + "thiserror", "tokio", ] @@ -5784,18 +5787,18 @@ checksum = "8eaa81235c7058867fa8c0e7314f33dcce9c215f535d1913822a2b3f5e289f3c" [[package]] name = "thiserror" -version = "1.0.35" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c53f98874615aea268107765aa1ed8f6116782501d18e53d08b471733bea6c85" +checksum = "10deb33631e3c9018b9baf9dcbbc4f737320d2b576bac10f6aefa048fa407e3e" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.35" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8b463991b4eab2d801e724172285ec4195c650e8ec79b149e6c2a8e6dd3f783" +checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb" dependencies = [ "proc-macro2", "quote", diff --git a/nym-connect/Cargo.toml b/nym-connect/Cargo.toml index b9fb14e41b..3ee3f20be3 100644 --- a/nym-connect/Cargo.toml +++ b/nym-connect/Cargo.toml @@ -1,2 +1,2 @@ [workspace] -members = ["src-tauri"] \ No newline at end of file +members = ["src-tauri"] diff --git a/nym-connect/README.md b/nym-connect/README.md index 5a247235ab..bbd9b55bbb 100644 --- a/nym-connect/README.md +++ b/nym-connect/README.md @@ -12,8 +12,8 @@ Nym Connects sets up a SOCKS5 proxy for local applications to use. ## Installation prerequisites - Linux / Mac - `Yarn` -- `NodeJS >= v16.8.0` -- `Rust & cargo >= v1.56` +- `NodeJS >= v16` +- `Rust & cargo` ## Installation prerequisites - Windows diff --git a/nym-connect/package.json b/nym-connect/package.json index 47298fbbbd..7e7824d6b6 100644 --- a/nym-connect/package.json +++ b/nym-connect/package.json @@ -1,6 +1,6 @@ { "name": "@nym/nym-connect", - "version": "1.0.0", + "version": "1.1.0", "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 11f79849b7..dfaf452dd1 100644 --- a/nym-connect/src-tauri/Cargo.toml +++ b/nym-connect/src-tauri/Cargo.toml @@ -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", "shell-open", "system-tray", "updater"] } +tauri = { version = "^1.1.1", features = ["clipboard-write-text", "macos-private-api", "shell-open", "system-tray", "updater", "window-close", "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 f4d260ea57..e9a46b26a1 100644 --- a/nym-connect/src-tauri/src/config/mod.rs +++ b/nym-connect/src-tauri/src/config/mod.rs @@ -1,6 +1,6 @@ use std::path::PathBuf; -use client_core::config::GatewayEndpoint; +use client_core::config::GatewayEndpointConfig; use std::sync::Arc; use tap::TapFallible; use tokio::sync::RwLock; @@ -169,7 +169,7 @@ async fn setup_gateway( register: bool, user_chosen_gateway_id: Option<&str>, config: &Socks5Config, -) -> Result { +) -> 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. diff --git a/nym-connect/src-tauri/src/models/mod.rs b/nym-connect/src-tauri/src/models/mod.rs index ded6a479fc..e5076ac5c0 100644 --- a/nym-connect/src-tauri/src/models/mod.rs +++ b/nym-connect/src-tauri/src/models/mod.rs @@ -45,7 +45,7 @@ pub struct AppEventConnectionStatusChangedPayload { } #[cfg_attr(test, derive(ts_rs::TS))] -#[derive(Clone, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] pub struct DirectoryService { pub id: String, pub description: String, @@ -53,7 +53,7 @@ pub struct DirectoryService { } #[cfg_attr(test, derive(ts_rs::TS))] -#[derive(Clone, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] pub struct DirectoryServiceProvider { pub id: String, pub description: String, diff --git a/nym-connect/src-tauri/src/operations/directory/mod.rs b/nym-connect/src-tauri/src/operations/directory/mod.rs index aaac2276d9..b6c45fa8c0 100644 --- a/nym-connect/src-tauri/src/operations/directory/mod.rs +++ b/nym-connect/src-tauri/src/operations/directory/mod.rs @@ -6,9 +6,11 @@ static SERVICE_PROVIDER_WELLKNOWN_URL: &str = #[tauri::command] pub async fn get_services() -> Result> { + log::trace!("Fetching services"); let res = reqwest::get(SERVICE_PROVIDER_WELLKNOWN_URL) .await? .json::>() .await?; + log::trace!("Received: {:#?}", res); Ok(res) } diff --git a/nym-connect/src-tauri/src/tasks.rs b/nym-connect/src-tauri/src/tasks.rs index 751f7c5566..267c20e08c 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::GatewayEndpointConfig; use futures::channel::mpsc; use std::sync::Arc; use tap::TapFallible; @@ -25,13 +25,17 @@ pub enum Socks5StatusMessage { /// 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 @@ -50,7 +54,7 @@ pub fn start_nym_socks5_client( .block_on(async move { socks5_client.run_and_listen(socks5_ctrl_rx).await }); if let Err(err) = result { - log::error!("SOCKS5 proxy failed to start: {err}"); + log::error!("SOCKS5 proxy failed: {err}"); socks5_status_tx .send(Socks5StatusMessage::FailedToStart) .expect("Failed to send status message back to main task"); @@ -66,6 +70,12 @@ pub fn start_nym_socks5_client( Ok((socks5_ctrl_tx, socks5_status_rx, used_gateway)) } +#[derive(Clone, serde::Serialize)] +struct Payload { + title: String, + message: String, +} + /// The disconnect listener listens to the channel setup between the socks5 proxy task and the main /// tauri task. Primarily it listens for shutdown messages, and updates the state accordingly. pub fn start_disconnect_listener( @@ -78,14 +88,39 @@ pub fn start_disconnect_listener( match status_receiver.await { Ok(Socks5StatusMessage::Stopped) => { log::info!("SOCKS5 task reported it has finished"); + window + .emit( + "socks5-event", + Payload { + title: "SOCKS5 finished".into(), + message: "SOCKS5 task reported it has finished".into(), + }, + ) + .unwrap(); } Ok(Socks5StatusMessage::FailedToStart) => { log::info!("SOCKS5 task reported it failed to start"); + window + .emit( + "socks5-event", + Payload { + title: "SOCKS5 error".into(), + message: "SOCKS5 failed to start".into(), + }, + ) + .unwrap(); } Err(_) => { log::info!("SOCKS5 task appears to have stopped abruptly"); - // TODO: we should probably generate some events here, or otherwise signal to the - // frontend. + window + .emit( + "socks5-event", + Payload { + title: "SOCKS5 error".into(), + message: "SOCKS5 stopped abruptly. Please try reconnecting.".into(), + }, + ) + .unwrap(); } } diff --git a/nym-connect/src-tauri/tauri.conf.json b/nym-connect/src-tauri/tauri.conf.json index c4fce76f06..b9cbfd8233 100644 --- a/nym-connect/src-tauri/tauri.conf.json +++ b/nym-connect/src-tauri/tauri.conf.json @@ -10,6 +10,7 @@ "beforeBuildCommand": "" }, "tauri": { + "macOSPrivateApi": true, "systemTray": { "iconPath": "icons/tray_icon.png", "iconAsTemplate": true @@ -18,13 +19,7 @@ "active": true, "targets": "all", "identifier": "net.nymtech.connect", - "icon": [ - "icons/32x32.png", - "icons/128x128.png", - "icons/128x128@2x.png", - "icons/icon.icns", - "icons/icon.ico" - ], + "icon": ["icons/32x32.png", "icons/128x128.png", "icons/128x128@2x.png", "icons/icon.icns", "icons/icon.ico"], "resources": [], "externalBin": [], "copyright": "Copyright © 2021-2022 Nym Technologies SA", @@ -49,9 +44,7 @@ }, "updater": { "active": true, - "endpoints": [ - "https://nymtech.net/.wellknown/connect/updater.json" - ], + "endpoints": ["https://nymtech.net/.wellknown/connect/updater.json"], "dialog": true, "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IENCNzQ2M0E5N0VFODE2NApSV1JrZ2U2WE9rYTNETTg1OTBKdE5uWUEra0hML2syOVUvQ2lxZmFZRzZ1T3NWbGM0eVRzUTVhVwo=" }, @@ -61,14 +54,20 @@ }, "clipboard": { "writeText": true + }, + "window": { + "startDragging": true, + "close": true } }, "windows": [ { "title": "NymConnect", "width": 240, - "height": 500, - "resizable": false + "height": 540, + "resizable": false, + "decorations": false, + "transparent": true } ], "security": { diff --git a/nym-connect/src/App.tsx b/nym-connect/src/App.tsx index 10a2995f34..90dd1a4230 100644 --- a/nym-connect/src/App.tsx +++ b/nym-connect/src/App.tsx @@ -1,37 +1,50 @@ -import React from 'react'; +import React, { useEffect } from 'react'; +import { DateTime } from 'luxon'; import { ConnectionStatusKind } from './types'; import { useClientContext } from './context/main'; import { DefaultLayout } from './layouts/DefaultLayout'; import { ConnectedLayout } from './layouts/ConnectedLayout'; +import { HelpGuideLayout } from './layouts/HelpGuideLayout'; export const App: React.FC = () => { const context = useClientContext(); const [busy, setBusy] = React.useState(); + const [showInfoModal, setShowInfoModal] = React.useState(false); const handleConnectClick = React.useCallback(async () => { - const oldStatus = context.connectionStatus; - if (oldStatus === ConnectionStatusKind.connected || oldStatus === ConnectionStatusKind.disconnected) { + const currentStatus = context.connectionStatus; + if (currentStatus === ConnectionStatusKind.connected || currentStatus === ConnectionStatusKind.disconnected) { setBusy(true); // eslint-disable-next-line default-case - switch (oldStatus) { + switch (currentStatus) { case ConnectionStatusKind.disconnected: await context.startConnecting(); + context.setConnectedSince(DateTime.now()); break; case ConnectionStatusKind.connected: await context.startDisconnecting(); + context.setConnectedSince(undefined); break; } setBusy(false); } }, [context.connectionStatus]); + useEffect(() => { + if (context.connectionStatus === ConnectionStatusKind.connected) setShowInfoModal(true); + }, [context.connectionStatus]); + + if (context.showHelp) return ; + if ( context.connectionStatus === ConnectionStatusKind.disconnected || context.connectionStatus === ConnectionStatusKind.connecting ) { return ( { return ( setShowInfoModal(false)} status={context.connectionStatus} busy={busy} onConnectClick={handleConnectClick} diff --git a/nym-connect/src/assets/help-step-four.png b/nym-connect/src/assets/help-step-four.png new file mode 100644 index 0000000000..5507962892 Binary files /dev/null and b/nym-connect/src/assets/help-step-four.png differ diff --git a/nym-connect/src/assets/help-step-one.png b/nym-connect/src/assets/help-step-one.png new file mode 100644 index 0000000000..5e91a29cff Binary files /dev/null and b/nym-connect/src/assets/help-step-one.png differ diff --git a/nym-connect/src/assets/help-step-three.png b/nym-connect/src/assets/help-step-three.png new file mode 100644 index 0000000000..932b6c1171 Binary files /dev/null and b/nym-connect/src/assets/help-step-three.png differ diff --git a/nym-connect/src/assets/help-step-two.png b/nym-connect/src/assets/help-step-two.png new file mode 100644 index 0000000000..f2910c98cf Binary files /dev/null and b/nym-connect/src/assets/help-step-two.png differ diff --git a/nym-connect/src/components/AppWindowFrame.tsx b/nym-connect/src/components/AppWindowFrame.tsx index fc30793a2d..5fc3d60ff6 100644 --- a/nym-connect/src/components/AppWindowFrame.tsx +++ b/nym-connect/src/components/AppWindowFrame.tsx @@ -1,21 +1,18 @@ import React from 'react'; import { Box } from '@mui/material'; -import { NymWordmark } from '@nymproject/react/logo/NymWordmark'; +import { CustomTitleBar } from './CustomTitleBar'; export const AppWindowFrame: React.FC = ({ children }) => ( t.palette.background.default, - borderRadius: '12px', - padding: '12px 16px', display: 'grid', - gridTemplateRows: '30px auto', - width: '240px', + borderRadius: '12px', + gridTemplateRows: '40px 1fr', + bgcolor: 'nym.background.dark', + height: '100vh', }} > - - - - {children} + + {children} ); diff --git a/nym-connect/src/components/ConnectionButton.tsx b/nym-connect/src/components/ConnectionButton.tsx index 0ab2ce1fa9..42903a80ea 100644 --- a/nym-connect/src/components/ConnectionButton.tsx +++ b/nym-connect/src/components/ConnectionButton.tsx @@ -19,16 +19,16 @@ const getStatusFillColor = (status: ConnectionStatusKind, hover: boolean, isErro switch (status) { case ConnectionStatusKind.disconnected: if (hover) { - return '#21D072'; + return '#FFFF33'; } - return '#F4B02D'; + return '#FFE600'; case ConnectionStatusKind.connecting: case ConnectionStatusKind.disconnecting: - return '#F4B02D'; + return '#FFE600'; default: // connected if (hover) { - return '#DA465B'; + return '#E43E3E'; } return '#21D072'; } @@ -81,69 +81,62 @@ export const ConnectionButton: React.FC<{ viewBox="0 0 208 208" fill="none" xmlns="http://www.w3.org/2000/svg" - onMouseEnter={() => !disabled && setHover(true)} - onMouseLeave={() => !disabled && setHover(false)} > - + !disabled && setHover(true)} + onMouseLeave={() => !disabled && setHover(false)} + > - - + + - - - - {busy && ( - - )} - + + + - + {status === ConnectionStatusKind.connected && hover ? ( ) : ( )} {statusText} - + - - + + - - + + diff --git a/nym-connect/src/components/ConnectionStatus.tsx b/nym-connect/src/components/ConnectionStatus.tsx index a866f8521d..c2bfcc82f0 100644 --- a/nym-connect/src/components/ConnectionStatus.tsx +++ b/nym-connect/src/components/ConnectionStatus.tsx @@ -1,12 +1,10 @@ import React from 'react'; import { Box, CircularProgress, Typography } from '@mui/material'; -import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline'; -import CircleOutlinedIcon from '@mui/icons-material/CircleOutlined'; import { DateTime } from 'luxon'; import { ConnectionStatusKind } from '../types'; import { ServiceProvider } from '../types/directory'; -const FONT_SIZE = '16px'; +const FONT_SIZE = '10px'; const FONT_WEIGHT = '600'; const FONT_STYLE = 'normal'; @@ -16,39 +14,41 @@ const ConnectionStatusContent: React.FC<{ switch (status) { case ConnectionStatusKind.connected: return ( - <> - - - Connected - - + + Connected + ); case ConnectionStatusKind.disconnecting: return ( - <> + Disconnecting... - + ); case ConnectionStatusKind.connecting: return ( - <> + Connecting... - + ); case ConnectionStatusKind.disconnected: return ( - <> - - - Disconnected - - + + You are not protected + ); default: return null; @@ -61,29 +61,22 @@ export const ConnectionStatus: React.FC<{ serviceProvider?: ServiceProvider; }> = ({ status, connectedSince, serviceProvider }) => { const color = - status === ConnectionStatusKind.connected || status === ConnectionStatusKind.disconnecting ? '#21D072' : '#888'; - const [duration, setDuration] = React.useState(); - React.useEffect(() => { - const intervalId = setInterval(() => { - if (connectedSince) { - setDuration(DateTime.now().diff(connectedSince).toFormat('hh:mm:ss')); - } - }, 500); - return () => { - clearInterval(intervalId); - }; - }, [status, connectedSince]); + status === ConnectionStatusKind.connected || status === ConnectionStatusKind.disconnecting + ? '#21D072' + : 'warning.main'; + return ( <> - - - - - - {status === ConnectionStatusKind.connected && duration} - + + + + + {serviceProvider && ( + + To {serviceProvider.description} + + )} - {serviceProvider && {serviceProvider.description}} ); }; diff --git a/nym-connect/src/components/ConntectionTimer.tsx b/nym-connect/src/components/ConntectionTimer.tsx new file mode 100644 index 0000000000..25e2af8d9c --- /dev/null +++ b/nym-connect/src/components/ConntectionTimer.tsx @@ -0,0 +1,26 @@ +import React, { useEffect } from 'react'; +import { Stack, Typography } from '@mui/material'; +import { DateTime } from 'luxon'; + +export const ConnectionTimer = ({ connectedSince }: { connectedSince?: DateTime }) => { + const [duration, setDuration] = React.useState(); + useEffect(() => { + const intervalId = setInterval(() => { + if (connectedSince) { + setDuration(DateTime.now().diff(connectedSince).toFormat('hh:mm:ss')); + } + }, 500); + return () => { + clearInterval(intervalId); + }; + }, [connectedSince]); + + return ( + + + Connection time + + {duration || '00:00:00'} + + ); +}; diff --git a/nym-connect/src/components/CustomTitleBar.tsx b/nym-connect/src/components/CustomTitleBar.tsx new file mode 100644 index 0000000000..dc4e8c0061 --- /dev/null +++ b/nym-connect/src/components/CustomTitleBar.tsx @@ -0,0 +1,41 @@ +import React from 'react'; +import { ArrowBack, Close, HelpOutline } from '@mui/icons-material'; +import { Box, IconButton } from '@mui/material'; +import { NymWordmark } from '@nymproject/react/logo/NymWordmark'; +import { appWindow } from '@tauri-apps/api/window'; +import { useClientContext } from 'src/context/main'; + +const customTitleBarStyles = { + titlebar: { + background: '#1D2125', + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + padding: '16px', + paddingBottom: '0px', + borderTopLeftRadius: '12px', + borderTopRightRadius: '12px', + }, +}; + +const CustomButton = ({ Icon, onClick }: { Icon: React.JSXElementConstructor; onClick: () => void }) => ( + + + +); + +export const CustomTitleBar = () => { + const { showHelp, handleShowHelp } = useClientContext(); + return ( + + { + handleShowHelp(); + }} + /> + + appWindow.close()} /> + + ); +}; diff --git a/nym-connect/src/components/HelpPage.tsx b/nym-connect/src/components/HelpPage.tsx new file mode 100644 index 0000000000..788f7264d5 --- /dev/null +++ b/nym-connect/src/components/HelpPage.tsx @@ -0,0 +1,33 @@ +import React from 'react'; +import { Stack, Typography } from '@mui/material'; +import { HelpPageActions } from './HelpPageActions'; +import { HelpImage } from './HelpPageImage'; +import { StepIndicator } from './HelpPageStepIndicator'; + +export const HelpPage = ({ + step, + description, + img, + onNext, + onPrev, +}: { + step: number; + description: string; + img: any; + onNext?: () => void; + onPrev?: () => void; +}) => ( + + + + + How to connect guide {step}/4 + + + {description} + + + + + +); diff --git a/nym-connect/src/components/HelpPageActions.tsx b/nym-connect/src/components/HelpPageActions.tsx new file mode 100644 index 0000000000..17e513120f --- /dev/null +++ b/nym-connect/src/components/HelpPageActions.tsx @@ -0,0 +1,20 @@ +import { ArrowBack, ArrowForward } from '@mui/icons-material'; +import { Box, Button } from '@mui/material'; +import React from 'react'; + +export const HelpPageActions = ({ onNext, onPrev }: { onNext?: () => void; onPrev?: () => void }) => ( + + {onPrev ? ( + + ) : ( +
+ )} + {onNext && ( + + )} + +); diff --git a/nym-connect/src/components/HelpPageImage.tsx b/nym-connect/src/components/HelpPageImage.tsx new file mode 100644 index 0000000000..27a8dcdca3 --- /dev/null +++ b/nym-connect/src/components/HelpPageImage.tsx @@ -0,0 +1,5 @@ +import React from 'react'; + +export const HelpImage = ({ img, imageDescription }: { img: any; imageDescription: string }) => ( + {imageDescription} +); diff --git a/nym-connect/src/components/HelpPageStepIndicator.tsx b/nym-connect/src/components/HelpPageStepIndicator.tsx new file mode 100644 index 0000000000..b5dff42b34 --- /dev/null +++ b/nym-connect/src/components/HelpPageStepIndicator.tsx @@ -0,0 +1,15 @@ +import { Box } from '@mui/material'; +import React from 'react'; + +const Step = ({ highlight }: { highlight: boolean }) => ( + +); + +export const StepIndicator = ({ step }: { step: number }) => ( + + + = 2} /> + = 3} /> + = 4} /> + +); diff --git a/nym-connect/src/components/InfoModal.tsx b/nym-connect/src/components/InfoModal.tsx new file mode 100644 index 0000000000..ed628de7eb --- /dev/null +++ b/nym-connect/src/components/InfoModal.tsx @@ -0,0 +1,69 @@ +import { Close, ErrorOutline } from '@mui/icons-material'; +import { Box, IconButton, Modal, Theme, Typography } from '@mui/material'; +import React from 'react'; + +const styles = { + position: 'absolute', + top: '50%', + left: '50%', + transform: 'translate(-50%, -50%)', + width: 200, + bgcolor: '#292E34', + p: 1.5, + borderRadius: 0.5, + height: 'fit-content', + border: (theme: Theme) => `1px solid ${theme.palette.grey[700]}`, +}; + +const ModalTitle = ({ title, withCloseIcon }: { title: string; withCloseIcon: boolean }) => ( + + + + {title} + + +); + +const ModalBody = ({ description, children }: { description: string; children?: React.ReactElement }) => ( + + {children} + + {description} + + +); + +export const InfoModal = ({ + title, + description, + show, + children, + Action, + onClose, +}: { + title: string; + description: string; + show: boolean; + children?: React.ReactElement; + Action?: React.ReactNode; + onClose?: () => void; +}) => ( + + + {onClose && ( + + + + + + )} + + {children} + {Action && ( + + {Action} + + )} + + +); diff --git a/nym-connect/src/components/IpAddressAndPort.tsx b/nym-connect/src/components/IpAddressAndPort.tsx index 435cf926fb..8d7d79c1f2 100644 --- a/nym-connect/src/components/IpAddressAndPort.tsx +++ b/nym-connect/src/components/IpAddressAndPort.tsx @@ -37,8 +37,12 @@ export const IpAddressAndPort: React.FC<{ return ( - {label} - Port + + {label} + + + Port + void; +}) => ( + Done} + > + + + Socks5 address + + + {ipAddress} + + + + + Port + + + {port} + + + + +); diff --git a/nym-connect/src/components/ServiceProviderSelector.tsx b/nym-connect/src/components/ServiceProviderSelector.tsx index 08cda8eb8a..9dee82fbb4 100644 --- a/nym-connect/src/components/ServiceProviderSelector.tsx +++ b/nym-connect/src/components/ServiceProviderSelector.tsx @@ -1,11 +1,5 @@ import React, { useEffect, useMemo } from 'react'; -import IconButton from '@mui/material/IconButton'; -import Menu from '@mui/material/Menu'; -import MenuItem from '@mui/material/MenuItem'; -import KeyboardArrowDownRoundedIcon from '@mui/icons-material/KeyboardArrowDownRounded'; -import KeyboardArrowUpRoundedIcon from '@mui/icons-material/KeyboardArrowUpRounded'; -import { Box, CircularProgress, Stack, Tooltip, Typography, ListItemIcon } from '@mui/material'; -import Check from '@mui/icons-material/Check'; +import { Box, CircularProgress, Stack, TextField, Tooltip, Typography, MenuItem, ListItemIcon } from '@mui/material'; import { ServiceProvider, Service, Services } from '../types/directory'; type ServiceWithRandomSp = { @@ -19,11 +13,8 @@ export const ServiceProviderSelector: React.FC<{ services?: Services; currentSp?: ServiceProvider; }> = ({ services, currentSp, onChange }) => { - const [service, setService] = React.useState(); + const [service, setService] = React.useState({ id: '', description: '', items: [] }); const [serviceProvider, setServiceProvider] = React.useState(currentSp); - const textEl = React.useRef(null); - const [anchorEl, setAnchorEl] = React.useState(null); - const open = Boolean(anchorEl); useEffect(() => { if (!serviceProvider && currentSp) { @@ -34,38 +25,34 @@ export const ServiceProviderSelector: React.FC<{ useEffect(() => { if (services && serviceProvider) { // retrieve the service corresponding to this service provider - setService( - services.find((s) => - s.items.some( - ({ id, address, gateway }) => - id === serviceProvider.id && address === serviceProvider.address && gateway === serviceProvider.gateway, - ), + + const match = services.find((s) => + s.items.some( + ({ id, address, gateway }) => + id === serviceProvider.id && address === serviceProvider.address && gateway === serviceProvider.gateway, ), ); + + if (match) { + setService(match); + } } }, [serviceProvider, services]); - const handleClick = () => { - setAnchorEl(textEl.current); - }; - const handleClose = (newServiceProvider?: ServiceProvider) => { + const handleSelectSp = (newServiceProvider?: ServiceProvider) => { if (newServiceProvider && newServiceProvider !== currentSp) { setServiceProvider(newServiceProvider); onChange?.(newServiceProvider); } - setAnchorEl(null); }; if (!services) { return ( - + theme.palette.common.white}> Loading services... - - {open ? : } - ); } @@ -80,68 +67,44 @@ export const ServiceProviderSelector: React.FC<{ [services], ); + if (!service) return null; + return ( - <> - `1px solid ${theme.palette.info.main}` }} - > - theme.palette.info.main}> - {!service ? 'Select a service' : service.description} - - - {open ? : } - - - handleClose()} - anchorOrigin={{ - vertical: 'bottom', - horizontal: 'right', - }} - transformOrigin={{ - vertical: 'top', - horizontal: 'left', - }} - PaperProps={{ + + {servicesWithRandomSp.map(({ id, description, sp }) => ( - handleClose(sp)} - > + handleSelectSp(sp)}> @@ -164,19 +127,9 @@ export const ServiceProviderSelector: React.FC<{ > {description} - {id === service?.id && ( - - - - )} ))} - - + + ); }; diff --git a/nym-connect/src/context/main.tsx b/nym-connect/src/context/main.tsx index 42bfed638e..5d2b049bf9 100644 --- a/nym-connect/src/context/main.tsx +++ b/nym-connect/src/context/main.tsx @@ -7,20 +7,26 @@ import { forage } from '@tauri-apps/tauri-forage'; import { ConnectionStatusKind } from '../types'; import { ConnectionStatsItem } from '../components/ConnectionStats'; import { ServiceProvider, Services } from '../types/directory'; +import { Error } from 'src/types/error'; +import { TauriEvent } from 'src/types/event'; const TAURI_EVENT_STATUS_CHANGED = 'app:connection-status-changed'; type ModeType = 'light' | 'dark'; -type TClientContext = { +export type TClientContext = { mode: ModeType; connectionStatus: ConnectionStatusKind; connectionStats?: ConnectionStatsItem[]; connectedSince?: DateTime; services?: Services; serviceProvider?: ServiceProvider; + showHelp: boolean; + error?: Error; setMode: (mode: ModeType) => void; + clearError: () => void; + handleShowHelp: () => void; setConnectionStatus: (connectionStatus: ConnectionStatusKind) => void; setConnectionStats: (connectionStats: ConnectionStatsItem[] | undefined) => void; setConnectedSince: (connectedSince: DateTime | undefined) => void; @@ -39,6 +45,8 @@ export const ClientContextProvider = ({ children }: { children: React.ReactNode const [connectedSince, setConnectedSince] = useState(); const [services, setServices] = React.useState([]); const [serviceProvider, setRawServiceProvider] = React.useState(); + const [showHelp, setShowHelp] = useState(false); + const [error, setError] = useState(); useEffect(() => { invoke('get_services').then((result) => { @@ -47,30 +55,45 @@ export const ClientContextProvider = ({ children }: { children: React.ReactNode }, []); useEffect(() => { - let unlisten: UnlistenFn | undefined; + const unlisten: UnlistenFn[] = []; // TODO: fix typings listen(TAURI_EVENT_STATUS_CHANGED, (event) => { const { status } = event.payload as any; console.log(TAURI_EVENT_STATUS_CHANGED, { status, event }); setConnectionStatus(status); + }) + .then((result) => { + unlisten.push(result); + }) + .catch((e) => console.log(e)); + + listen('socks5-event', (e: TauriEvent) => { + setError(e.payload); }).then((result) => { - unlisten = result; + unlisten.push(result); }); return () => { - if (unlisten) { - unlisten(); - } + unlisten.forEach((unsubscribe) => unsubscribe()); }; }, []); const startConnecting = useCallback(async () => { - await invoke('start_connecting'); + try { + await invoke('start_connecting'); + } catch (e) { + setError({ title: 'Could not connect', message: e as string }); + console.log(e); + } }, []); const startDisconnecting = useCallback(async () => { - await invoke('start_disconnecting'); + try { + await invoke('start_disconnecting'); + } catch (e) { + console.log(e); + } }, []); const setSpInStorage = async (sp: ServiceProvider) => { @@ -92,12 +115,17 @@ export const ClientContextProvider = ({ children }: { children: React.ReactNode const spFromStorage = await forage.getItem({ key: 'nym-connect-sp' })(); if (spFromStorage) { setRawServiceProvider(spFromStorage); + setServiceProvider(spFromStorage); } } catch (e) { console.warn(e); } }; + const handleShowHelp = () => setShowHelp((show) => !show); + + const clearError = () => setError(undefined); + useEffect(() => { const validityCheck = async () => { if (services.length > 0 && serviceProvider) { @@ -122,6 +150,8 @@ export const ClientContextProvider = ({ children }: { children: React.ReactNode () => ({ mode, setMode, + error, + clearError, connectionStatus, setConnectionStatus, connectionStats, @@ -133,8 +163,20 @@ export const ClientContextProvider = ({ children }: { children: React.ReactNode services, serviceProvider, setServiceProvider, + showHelp, + handleShowHelp, }), - [mode, connectedSince, connectionStatus, connectionStats, connectedSince, services, serviceProvider], + [ + mode, + error, + connectedSince, + showHelp, + connectionStatus, + connectionStats, + connectedSince, + services, + serviceProvider, + ], ); return {children}; diff --git a/nym-connect/src/context/mocks/main.tsx b/nym-connect/src/context/mocks/main.tsx new file mode 100644 index 0000000000..e2974413eb --- /dev/null +++ b/nym-connect/src/context/mocks/main.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import { ConnectionStatusKind } from 'src/types'; +import { ClientContext, TClientContext } from '../main'; + +const mockValues: TClientContext = { + mode: 'dark', + connectionStatus: ConnectionStatusKind.disconnected, + services: [], + showHelp: false, + serviceProvider: { id: '1', description: 'Keybase service provider', gateway: 'abc123', address: '123abc' }, + setMode: () => {}, + clearError: () => {}, + handleShowHelp: () => {}, + setConnectedSince: () => {}, + setConnectionStats: () => {}, + setConnectionStatus: () => {}, + setServiceProvider: () => {}, + startConnecting: async () => {}, + startDisconnecting: async () => {}, +}; + +export const MockProvider = ({ children }: { children: React.ReactNode }) => { + return {children}; +}; diff --git a/nym-connect/src/index.tsx b/nym-connect/src/index.tsx index 2938840fff..66a3e11d17 100644 --- a/nym-connect/src/index.tsx +++ b/nym-connect/src/index.tsx @@ -4,16 +4,18 @@ import { ErrorBoundary } from 'react-error-boundary'; import { ClientContextProvider } from './context/main'; import { ErrorFallback } from './components/Error'; import { NymMixnetTheme } from './theme'; -import './fonts/fonts.css'; import { App } from './App'; +import { AppWindowFrame } from './components/AppWindowFrame'; const root = document.getElementById('root'); ReactDOM.render( - - + + + + , diff --git a/nym-connect/src/layouts/ConnectedLayout.tsx b/nym-connect/src/layouts/ConnectedLayout.tsx index a031f18a56..8e34415b39 100644 --- a/nym-connect/src/layouts/ConnectedLayout.tsx +++ b/nym-connect/src/layouts/ConnectedLayout.tsx @@ -1,11 +1,11 @@ import React from 'react'; -import { Box } from '@mui/material'; +import { Box, Divider } from '@mui/material'; import { DateTime } from 'luxon'; -import { AppWindowFrame } from '../components/AppWindowFrame'; +import { IpAddressAndPortModal } from 'src/components/IpAddressAndPortModal'; +import { ConnectionTimer } from 'src/components/ConntectionTimer'; import { ConnectionStatus } from '../components/ConnectionStatus'; import { ConnectionStatusKind } from '../types'; -import { ConnectionStats, ConnectionStatsItem } from '../components/ConnectionStats'; -import { NeedHelp } from '../components/NeedHelp'; +import { ConnectionStatsItem } from '../components/ConnectionStats'; import { ConnectionButton } from '../components/ConnectionButton'; import { IpAddressAndPort } from '../components/IpAddressAndPort'; import { ServiceProvider } from '../types/directory'; @@ -17,19 +17,32 @@ export const ConnectedLayout: React.FC<{ port: number; connectedSince?: DateTime; busy?: boolean; + showInfoModal: boolean; isError?: boolean; + handleCloseInfoModal: () => void; onConnectClick?: (status: ConnectionStatusKind) => void; serviceProvider?: ServiceProvider; -}> = ({ status, stats, ipAddress, port, connectedSince, busy, isError, serviceProvider, onConnectClick }) => ( - +}> = ({ + status, + showInfoModal, + handleCloseInfoModal, + ipAddress, + port, + connectedSince, + busy, + isError, + serviceProvider, + onConnectClick, +}) => ( + <> + - - - - + + + {/* */} + - - + ); diff --git a/nym-connect/src/layouts/DefaultLayout.tsx b/nym-connect/src/layouts/DefaultLayout.tsx index ba59b7f95d..ed42aad5fe 100644 --- a/nym-connect/src/layouts/DefaultLayout.tsx +++ b/nym-connect/src/layouts/DefaultLayout.tsx @@ -1,48 +1,48 @@ import React from 'react'; import { Typography } from '@mui/material'; -import { AppWindowFrame } from '../components/AppWindowFrame'; +import { Box } from '@mui/material'; +import { ConnectionStatus } from 'src/components/ConnectionStatus'; +import { ConnectionTimer } from 'src/components/ConntectionTimer'; +import { InfoModal } from 'src/components/InfoModal'; +import { Error } from 'src/types/error'; import { ConnectionButton } from '../components/ConnectionButton'; -import { ConnectionStatusKind } from '../types'; -import { NeedHelp } from '../components/NeedHelp'; import { ServiceProviderSelector } from '../components/ServiceProviderSelector'; -import { ServiceProvider, Services } from '../types/directory'; import { useClientContext } from '../context/main'; +import { ConnectionStatusKind } from '../types'; +import { ServiceProvider, Services } from '../types/directory'; export const DefaultLayout: React.FC<{ + error?: Error; status: ConnectionStatusKind; services?: Services; busy?: boolean; isError?: boolean; + clearError: () => void; onConnectClick?: (status: ConnectionStatusKind) => void; onServiceProviderChange?: (serviceProvider: ServiceProvider) => void; -}> = ({ status, services, busy, isError, onConnectClick, onServiceProviderChange }) => { - const [serviceProvider, setServiceProvider] = React.useState(); +}> = ({ status, error, services, busy, isError, onConnectClick, onServiceProviderChange, clearError }) => { const handleServiceProviderChange = (newServiceProvider: ServiceProvider) => { - setServiceProvider(newServiceProvider); onServiceProviderChange?.(newServiceProvider); }; + const { serviceProvider: currentSp } = useClientContext(); return ( - - - This is experimental software.
- Do not rely on it for strong anonymity (yet). -
- - Connect to the -
- Nym mixnet for privacy. + + {error && } + + + Connect to the Nym
mixnet for privacy.
+ - -
+
); }; diff --git a/nym-connect/src/layouts/HelpGuideLayout.tsx b/nym-connect/src/layouts/HelpGuideLayout.tsx new file mode 100644 index 0000000000..d56fd6b0ae --- /dev/null +++ b/nym-connect/src/layouts/HelpGuideLayout.tsx @@ -0,0 +1,70 @@ +import React, { useState } from 'react'; +import { HelpPage } from 'src/components/HelpPage'; +import { Button, Link, Stack } from '@mui/material'; +import Image1 from '../assets/help-step-one.png'; +import Image2 from '../assets/help-step-two.png'; +import Image3 from '../assets/help-step-three.png'; +import Image4 from '../assets/help-step-four.png'; + +export const HelpGuideLayout = () => { + const [step, setStep] = useState(0); + + if (step === 1) + return ( + setStep(2)} + /> + ); + + if (step === 2) + return ( + setStep(1)} + onNext={() => setStep(3)} + /> + ); + + if (step === 3) + return ( + setStep(2)} + onNext={() => setStep(4)} + /> + ); + + if (step === 4) + return ( + setStep(3)} + /> + ); + + return ( + + + + + ); +}; diff --git a/nym-connect/src/stories/AppFlow.stories.tsx b/nym-connect/src/stories/AppFlow.stories.tsx index ef7a1ea434..34064e6202 100644 --- a/nym-connect/src/stories/AppFlow.stories.tsx +++ b/nym-connect/src/stories/AppFlow.stories.tsx @@ -67,20 +67,25 @@ export const Mock: ComponentStory = () => { context.connectionStatus === ConnectionStatusKind.connecting ) { return ( - + {}} /> - + ); } return ( - + { + return undefined; + }} status={context.connectionStatus} busy={busy} onConnectClick={handleConnectClick} @@ -101,6 +106,6 @@ export const Mock: ComponentStory = () => { }, ]} /> - + ); }; diff --git a/nym-connect/src/stories/ConnectedLayout.stories.tsx b/nym-connect/src/stories/ConnectedLayout.stories.tsx index 6063f83c28..53538457bf 100644 --- a/nym-connect/src/stories/ConnectedLayout.stories.tsx +++ b/nym-connect/src/stories/ConnectedLayout.stories.tsx @@ -11,8 +11,12 @@ export default { } as ComponentMeta; export const Default: ComponentStory = () => ( - + { + return undefined; + }} status={ConnectionStatusKind.connected} connectedSince={DateTime.now()} ipAddress="127.0.0.1" diff --git a/nym-connect/src/stories/DefaultLayout.stories.tsx b/nym-connect/src/stories/DefaultLayout.stories.tsx index 2c4ef513c2..ba311a410b 100644 --- a/nym-connect/src/stories/DefaultLayout.stories.tsx +++ b/nym-connect/src/stories/DefaultLayout.stories.tsx @@ -10,7 +10,24 @@ export default { } as ComponentMeta; export const Default: ComponentStory = () => ( - - + + {}} error={undefined} /> + +); + +export const WithServices: ComponentStory = () => ( + + {}} + error={undefined} + /> ); diff --git a/nym-connect/src/theme/index.tsx b/nym-connect/src/theme/index.tsx index 63f9db42fa..075aa7920a 100644 --- a/nym-connect/src/theme/index.tsx +++ b/nym-connect/src/theme/index.tsx @@ -3,12 +3,12 @@ import { createTheme, ThemeProvider } from '@mui/material/styles'; import { CssBaseline } from '@mui/material'; import { getDesignTokens } from './theme'; import { ClientContext } from '../context/main'; +import '../../../assets/fonts/non-variable/fonts.css'; /** - * Provides the theme for the Network Explorer by reacting to the light/dark mode choice stored in the app context. + * Provides the theme for Nym Connect by reacting to the light/dark mode choice stored in the app context. */ -export const NymMixnetTheme: React.FC = ({ children }) => { - const { mode } = useContext(ClientContext); +export const NymMixnetTheme: React.FC<{ mode: 'light' | 'dark' }> = ({ children, mode }) => { const theme = React.useMemo(() => createTheme(getDesignTokens(mode)), [mode]); return ( diff --git a/nym-connect/src/theme/mui-theme.d.ts b/nym-connect/src/theme/mui-theme.d.ts index a5b92073b3..ef042f6305 100644 --- a/nym-connect/src/theme/mui-theme.d.ts +++ b/nym-connect/src/theme/mui-theme.d.ts @@ -30,6 +30,7 @@ declare module '@mui/material/styles' { interface NymPalette { highlight: string; success: string; + warning: string; info: string; fee: string; background: { light: string; dark: string }; diff --git a/nym-connect/src/theme/theme.tsx b/nym-connect/src/theme/theme.tsx index e0153e6824..5b70f5118b 100644 --- a/nym-connect/src/theme/theme.tsx +++ b/nym-connect/src/theme/theme.tsx @@ -23,6 +23,7 @@ const nymPalette: NymPalette = { highlight: '#FB6E4E', success: '#21D073', info: '#60D7EF', + warning: '#FFE600', fee: '#967FF0', background: { light: '#F4F6F8', dark: '#1D2125' }, text: { @@ -93,6 +94,9 @@ const variantToMUIPalette = (variant: NymPaletteVariant): PaletteOptions => ({ info: { main: nymPalette.info, }, + warning: { + main: nymPalette.warning, + }, background: { default: variant.background.main, paper: variant.background.paper, diff --git a/nym-connect/src/types/error.ts b/nym-connect/src/types/error.ts new file mode 100644 index 0000000000..599b959c39 --- /dev/null +++ b/nym-connect/src/types/error.ts @@ -0,0 +1,4 @@ +export type Error = { + title: string; + message: string; +}; diff --git a/nym-connect/src/types/event.ts b/nym-connect/src/types/event.ts new file mode 100644 index 0000000000..66d1e514a6 --- /dev/null +++ b/nym-connect/src/types/event.ts @@ -0,0 +1,6 @@ +export type TauriEvent = { + payload: { + title: string; + message: string; + }; +}; diff --git a/nym-wallet/src-tauri/src/main.rs b/nym-wallet/src-tauri/src/main.rs index 9bd44028c2..125be1cb8c 100644 --- a/nym-wallet/src-tauri/src/main.rs +++ b/nym-wallet/src-tauri/src/main.rs @@ -55,6 +55,7 @@ fn main() { mixnet::admin::update_contract_settings, mixnet::bond::bond_gateway, mixnet::bond::bond_mixnode, + mixnet::bond::pledge_more, mixnet::bond::gateway_bond_details, mixnet::bond::get_pending_operator_rewards, mixnet::bond::mixnode_bond_details, @@ -105,6 +106,7 @@ fn main() { vesting::rewards::vesting_claim_operator_reward, vesting::bond::vesting_bond_gateway, vesting::bond::vesting_bond_mixnode, + vesting::bond::vesting_pledge_more, vesting::bond::vesting_unbond_gateway, vesting::bond::vesting_unbond_mixnode, vesting::bond::vesting_update_mixnode_cost_params, @@ -130,6 +132,7 @@ fn main() { simulate::mixnet::simulate_bond_gateway, simulate::mixnet::simulate_unbond_gateway, simulate::mixnet::simulate_bond_mixnode, + simulate::mixnet::simulate_pledge_more, simulate::mixnet::simulate_unbond_mixnode, simulate::mixnet::simulate_update_mixnode_config, simulate::mixnet::simulate_update_mixnode_cost_params, @@ -140,6 +143,7 @@ fn main() { simulate::vesting::simulate_vesting_bond_gateway, simulate::vesting::simulate_vesting_unbond_gateway, simulate::vesting::simulate_vesting_bond_mixnode, + simulate::vesting::simulate_vesting_pledge_more, simulate::vesting::simulate_vesting_unbond_mixnode, simulate::vesting::simulate_vesting_update_mixnode_config, simulate::vesting::simulate_vesting_update_mixnode_cost_params, diff --git a/nym-wallet/src-tauri/src/operations/mixnet/bond.rs b/nym-wallet/src-tauri/src/operations/mixnet/bond.rs index 7098577545..567dc84290 100644 --- a/nym-wallet/src-tauri/src/operations/mixnet/bond.rs +++ b/nym-wallet/src-tauri/src/operations/mixnet/bond.rs @@ -102,6 +102,33 @@ pub async fn bond_mixnode( )?) } +#[tauri::command] +pub async fn pledge_more( + fee: Option, + additional_pledge: DecCoin, + state: tauri::State<'_, WalletState>, +) -> Result { + let guard = state.read().await; + let additional_pledge_base = guard.attempt_convert_to_base_coin(additional_pledge.clone())?; + let fee_amount = guard.convert_tx_fee(fee.as_ref()); + log::info!( + ">>> Pledge more, additional_pledge_display = {}, additional_pledge_base = {}, fee = {:?}", + additional_pledge, + additional_pledge_base, + fee, + ); + let res = guard + .current_client()? + .nymd + .pledge_more(additional_pledge_base, fee) + .await?; + log::info!("<<< tx hash = {}", res.transaction_hash); + log::trace!("<<< {:?}", res); + Ok(TransactionExecuteResult::from_execute_result( + res, fee_amount, + )?) +} + #[tauri::command] pub async fn unbond_mixnode( fee: Option, diff --git a/nym-wallet/src-tauri/src/operations/simulate/mixnet.rs b/nym-wallet/src-tauri/src/operations/simulate/mixnet.rs index 51912e1332..33e649690b 100644 --- a/nym-wallet/src-tauri/src/operations/simulate/mixnet.rs +++ b/nym-wallet/src-tauri/src/operations/simulate/mixnet.rs @@ -84,6 +84,14 @@ pub async fn simulate_bond_mixnode( .await } +#[tauri::command] +pub async fn simulate_pledge_more( + additional_pledge: DecCoin, + state: tauri::State<'_, WalletState>, +) -> Result { + simulate_mixnet_operation(ExecuteMsg::PledgeMore {}, Some(additional_pledge), &state).await +} + #[tauri::command] pub async fn simulate_unbond_mixnode( state: tauri::State<'_, WalletState>, diff --git a/nym-wallet/src-tauri/src/operations/simulate/vesting.rs b/nym-wallet/src-tauri/src/operations/simulate/vesting.rs index c7cebf2ce5..213c8d3fd8 100644 --- a/nym-wallet/src-tauri/src/operations/simulate/vesting.rs +++ b/nym-wallet/src-tauri/src/operations/simulate/vesting.rs @@ -91,6 +91,19 @@ pub async fn simulate_vesting_bond_mixnode( .await } +#[tauri::command] +pub async fn simulate_vesting_pledge_more( + additional_pledge: DecCoin, + state: tauri::State<'_, WalletState>, +) -> Result { + let guard = state.read().await; + let amount = guard + .attempt_convert_to_base_coin(additional_pledge)? + .into(); + + simulate_vesting_operation(ExecuteMsg::PledgeMore { amount }, None, &state).await +} + #[tauri::command] pub async fn simulate_vesting_unbond_mixnode( state: tauri::State<'_, WalletState>, diff --git a/nym-wallet/src-tauri/src/operations/vesting/bond.rs b/nym-wallet/src-tauri/src/operations/vesting/bond.rs index 99f0086d9c..da5a71eee1 100644 --- a/nym-wallet/src-tauri/src/operations/vesting/bond.rs +++ b/nym-wallet/src-tauri/src/operations/vesting/bond.rs @@ -93,6 +93,33 @@ pub async fn vesting_bond_mixnode( )?) } +#[tauri::command] +pub async fn vesting_pledge_more( + fee: Option, + additional_pledge: DecCoin, + state: tauri::State<'_, WalletState>, +) -> Result { + let guard = state.read().await; + let additional_pledge_base = guard.attempt_convert_to_base_coin(additional_pledge.clone())?; + let fee_amount = guard.convert_tx_fee(fee.as_ref()); + log::info!( + ">>> Pledge more with locked tokens, additional_pledge_display = {}, additional_pledge_base = {}, fee = {:?}", + additional_pledge, + additional_pledge_base, + fee, + ); + let res = guard + .current_client()? + .nymd + .vesting_pledge_more(additional_pledge_base, fee) + .await?; + log::info!("<<< tx hash = {}", res.transaction_hash); + log::trace!("<<< {:?}", res); + Ok(TransactionExecuteResult::from_execute_result( + res, fee_amount, + )?) +} + #[tauri::command] pub async fn vesting_unbond_mixnode( fee: Option, diff --git a/service-providers/network-requester/Cargo.toml b/service-providers/network-requester/Cargo.toml index 8d81920af5..72e6c0685e 100644 --- a/service-providers/network-requester/Cargo.toml +++ b/service-providers/network-requester/Cargo.toml @@ -3,7 +3,7 @@ [package] name = "nym-network-requester" -version = "1.1.0" +version = "1.1.1" authors = ["Dave Hrycyszyn ", "Jędrzej Stuczyński "] edition = "2021" rust-version = "1.65" diff --git a/service-providers/network-requester/src/core.rs b/service-providers/network-requester/src/core.rs index 870376d896..e58bffaef0 100644 --- a/service-providers/network-requester/src/core.rs +++ b/service-providers/network-requester/src/core.rs @@ -157,9 +157,13 @@ impl ServiceProvider { lane_queue_lengths: LaneQueueLengths, ) -> Option { while let Some(msg) = websocket_reader.next().await { - let data = msg - .expect("we failed to read from the websocket!") - .into_data(); + let data = match msg { + Ok(msg) => msg.into_data(), + Err(err) => { + log::error!("Failed to read from the websocket: {err}"); + continue; + } + }; // try to recover the actual message from the mix network... let deserialized_message = match ServerResponse::deserialize(&data) { diff --git a/tools/nym-cli/Cargo.toml b/tools/nym-cli/Cargo.toml index 67c3bda09a..362e3005b5 100644 --- a/tools/nym-cli/Cargo.toml +++ b/tools/nym-cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nym-cli" -version = "1.1.0" +version = "1.1.1" authors = ["Nym Technologies SA"] edition = "2021" diff --git a/validator-api/Cargo.toml b/validator-api/Cargo.toml index 4fb4413f99..1207d6a145 100644 --- a/validator-api/Cargo.toml +++ b/validator-api/Cargo.toml @@ -3,7 +3,7 @@ [package] name = "nym-validator-api" -version = "1.1.0" +version = "1.1.1" authors = [ "Dave Hrycyszyn ", "Jędrzej Stuczyński ",